diff --git a/frontend/src/app/api/tasks/route.ts b/frontend/src/app/api/tasks/route.ts
index 5746c85..889467d 100644
--- a/frontend/src/app/api/tasks/route.ts
+++ b/frontend/src/app/api/tasks/route.ts
@@ -35,7 +35,7 @@ export async function GET(request: Request) {
? (difficultyParam as TaskDifficulty)
: undefined,
technology: searchParams.get("technology") ?? undefined,
- organization: searchParams.get("organization") ?? undefined,
+ organizationId: searchParams.get("organizationId") ?? undefined,
sort: VALID_SORTS.includes(sortParam as TaskSortOrder)
? (sortParam as TaskSortOrder)
: undefined,
@@ -109,7 +109,7 @@ export async function POST(request: Request) {
technologies: Array.isArray(payload.technologies)
? payload.technologies.map((tech) => String(tech))
: undefined,
- organization: payload.organization ? String(payload.organization) : undefined,
+ organizationId: payload.organizationId ? String(payload.organizationId) : undefined,
});
if (!result.ok) {
diff --git a/frontend/src/components/BountyList.tsx b/frontend/src/components/BountyList.tsx
index 8f5c8af..622664c 100644
--- a/frontend/src/components/BountyList.tsx
+++ b/frontend/src/components/BountyList.tsx
@@ -63,7 +63,7 @@ export function BountyList({ tasks, pagination, isLoading, error, onPageChange }
{task.title}
- {task.organization ? `${task.organization} • ` : ""}
+ {task.organizationId ? `Org ${task.organizationId} • ` : ""}
Deadline {formatDeadline(task.deadline)}
diff --git a/frontend/src/lib/organization-store.test.ts b/frontend/src/lib/organization-store.test.ts
new file mode 100644
index 0000000..1a6f36d
--- /dev/null
+++ b/frontend/src/lib/organization-store.test.ts
@@ -0,0 +1,403 @@
+/**
+ * Organization Store Tests
+ *
+ * Tests organization persistence and task linkage.
+ */
+
+import { describe, it, expect, beforeEach } from "vitest";
+import {
+ createOrganization,
+ getOrganization,
+ getOrganizationByName,
+ listOrganizations,
+ resetOrganizationStore,
+} from "./organization-store";
+import {
+ createTask,
+ listTasks,
+ resetTaskWorkflowStore,
+} from "./task-workflow";
+
+describe("Organization Store", () => {
+ beforeEach(() => {
+ resetOrganizationStore();
+ resetTaskWorkflowStore();
+ });
+
+ describe("Organization Persistence", () => {
+ it("creates an organization with required fields", () => {
+ const result = createOrganization({
+ name: "Stellar Development Foundation",
+ });
+
+ expect(result.ok).toBe(true);
+ if (!result.ok) return;
+
+ expect(result.organization.id).toBeDefined();
+ expect(result.organization.name).toBe("Stellar Development Foundation");
+ expect(result.organization.description).toBe("");
+ expect(result.organization.website).toBe("");
+ expect(result.organization.createdAt).toBeDefined();
+ });
+
+ it("creates an organization with all fields", () => {
+ const result = createOrganization({
+ name: "Soroswap Labs",
+ description: "Building the premier DEX on Stellar",
+ website: "https://soroswap.finance",
+ });
+
+ expect(result.ok).toBe(true);
+ if (!result.ok) return;
+
+ expect(result.organization.name).toBe("Soroswap Labs");
+ expect(result.organization.description).toBe("Building the premier DEX on Stellar");
+ expect(result.organization.website).toBe("https://soroswap.finance");
+ });
+
+ it("retrieves an organization by ID", () => {
+ const createResult = createOrganization({
+ name: "Blend Protocol",
+ description: "Lending on Stellar",
+ });
+
+ expect(createResult.ok).toBe(true);
+ if (!createResult.ok) return;
+
+ const getResult = getOrganization(createResult.organization.id);
+
+ expect(getResult.ok).toBe(true);
+ if (!getResult.ok) return;
+
+ expect(getResult.organization.id).toBe(createResult.organization.id);
+ expect(getResult.organization.name).toBe("Blend Protocol");
+ });
+
+ it("retrieves an organization by name (case-insensitive)", () => {
+ createOrganization({
+ name: "Stellar Foundation",
+ });
+
+ const result1 = getOrganizationByName("Stellar Foundation");
+ expect(result1.ok).toBe(true);
+
+ const result2 = getOrganizationByName("stellar foundation");
+ expect(result2.ok).toBe(true);
+
+ const result3 = getOrganizationByName("STELLAR FOUNDATION");
+ expect(result3.ok).toBe(true);
+ });
+
+ it("returns error for non-existent organization", () => {
+ const result = getOrganization("nonexistent");
+
+ expect(result.ok).toBe(false);
+ if (result.ok) return;
+
+ expect(result.status).toBe(404);
+ expect(result.error).toBe("Organization not found.");
+ });
+
+ it("enforces unique organization names (case-insensitive)", () => {
+ const result1 = createOrganization({ name: "Unique Org" });
+ expect(result1.ok).toBe(true);
+
+ const result2 = createOrganization({ name: "Unique Org" });
+ expect(result2.ok).toBe(false);
+ if (result2.ok) return;
+
+ expect(result2.status).toBe(409);
+ expect(result2.error).toBe("An organization with this name already exists.");
+
+ // Case-insensitive duplicate
+ const result3 = createOrganization({ name: "unique org" });
+ expect(result3.ok).toBe(false);
+ });
+
+ it("validates required organization name", () => {
+ const result = createOrganization({ name: "" });
+
+ expect(result.ok).toBe(false);
+ if (result.ok) return;
+
+ expect(result.status).toBe(400);
+ expect(result.details).toContain("Organization name is required.");
+ });
+
+ it("validates organization name length", () => {
+ const result1 = createOrganization({ name: "A" });
+ expect(result1.ok).toBe(false);
+ if (result1.ok) return;
+ expect(result1.details).toContain("Organization name must be at least 2 characters.");
+
+ const result2 = createOrganization({ name: "A".repeat(101) });
+ expect(result2.ok).toBe(false);
+ if (result2.ok) return;
+ expect(result2.details).toContain("Organization name must not exceed 100 characters.");
+ });
+
+ it("validates website URL format", () => {
+ const result1 = createOrganization({
+ name: "Test Org",
+ website: "not-a-url",
+ });
+
+ expect(result1.ok).toBe(false);
+ if (result1.ok) return;
+ expect(result1.details).toContain("Website must be a valid URL (http:// or https://).");
+
+ const result2 = createOrganization({
+ name: "Test Org 2",
+ website: "https://example.com",
+ });
+ expect(result2.ok).toBe(true);
+ });
+
+ it("validates description length", () => {
+ const result = createOrganization({
+ name: "Test Org",
+ description: "A".repeat(501),
+ });
+
+ expect(result.ok).toBe(false);
+ if (result.ok) return;
+ expect(result.details).toContain("Description must not exceed 500 characters.");
+ });
+
+ it("lists all organizations", () => {
+ createOrganization({ name: "Org 1" });
+ createOrganization({ name: "Org 2" });
+ createOrganization({ name: "Org 3" });
+
+ const orgs = listOrganizations();
+
+ expect(orgs).toHaveLength(3);
+ expect(orgs.map((o) => o.name)).toEqual(
+ expect.arrayContaining(["Org 1", "Org 2", "Org 3"])
+ );
+ });
+
+ it("lists empty array when no organizations exist", () => {
+ const orgs = listOrganizations();
+ expect(orgs).toHaveLength(0);
+ });
+ });
+
+ describe("Task-Organization Linkage", () => {
+ it("creates a task linked to an organization", () => {
+ const orgResult = createOrganization({
+ name: "TaskBounty DAO",
+ description: "Decentralized task marketplace",
+ });
+
+ expect(orgResult.ok).toBe(true);
+ if (!orgResult.ok) return;
+
+ const taskResult = createTask({
+ poster: "GABC123",
+ title: "Build a frontend component",
+ description: "Create a React component for task listing",
+ reward: 10_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 3,
+ organizationId: orgResult.organization.id,
+ });
+
+ expect(taskResult.ok).toBe(true);
+ if (!taskResult.ok) return;
+
+ expect(taskResult.task.organizationId).toBe(orgResult.organization.id);
+ });
+
+ it("creates a task without an organization", () => {
+ const taskResult = createTask({
+ poster: "GXYZ789",
+ title: "Independent task",
+ description: "Task without organization",
+ reward: 5_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 1,
+ });
+
+ expect(taskResult.ok).toBe(true);
+ if (!taskResult.ok) return;
+
+ expect(taskResult.task.organizationId).toBe("");
+ });
+
+ it("filters tasks by organization ID", () => {
+ const org1Result = createOrganization({ name: "Org Alpha" });
+ const org2Result = createOrganization({ name: "Org Beta" });
+
+ expect(org1Result.ok && org2Result.ok).toBe(true);
+ if (!org1Result.ok || !org2Result.ok) return;
+
+ createTask({
+ poster: "GTEST1",
+ title: "Task 1",
+ description: "First task",
+ reward: 1_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 1,
+ organizationId: org1Result.organization.id,
+ });
+
+ createTask({
+ poster: "GTEST2",
+ title: "Task 2",
+ description: "Second task",
+ reward: 2_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 1,
+ organizationId: org1Result.organization.id,
+ });
+
+ createTask({
+ poster: "GTEST3",
+ title: "Task 3",
+ description: "Third task",
+ reward: 3_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 1,
+ organizationId: org2Result.organization.id,
+ });
+
+ const org1Tasks = listTasks({ organizationId: org1Result.organization.id });
+ expect(org1Tasks.tasks).toHaveLength(2);
+ expect(org1Tasks.tasks.map((t) => t.title)).toEqual(
+ expect.arrayContaining(["Task 1", "Task 2"])
+ );
+
+ const org2Tasks = listTasks({ organizationId: org2Result.organization.id });
+ expect(org2Tasks.tasks).toHaveLength(1);
+ expect(org2Tasks.tasks[0].title).toBe("Task 3");
+ });
+
+ it("allows multiple tasks per organization", () => {
+ const orgResult = createOrganization({ name: "Prolific Org" });
+ expect(orgResult.ok).toBe(true);
+ if (!orgResult.ok) return;
+
+ const taskCount = 5;
+ for (let i = 1; i <= taskCount; i++) {
+ const result = createTask({
+ poster: `GUSER${i}`,
+ title: `Task ${i}`,
+ description: `Description ${i}`,
+ reward: i * 1_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 1,
+ organizationId: orgResult.organization.id,
+ });
+ expect(result.ok).toBe(true);
+ }
+
+ const tasks = listTasks({ organizationId: orgResult.organization.id });
+ expect(tasks.tasks).toHaveLength(taskCount);
+ });
+
+ it("lists all tasks for a given organization", () => {
+ const orgResult = createOrganization({ name: "Test Organization" });
+ expect(orgResult.ok).toBe(true);
+ if (!orgResult.ok) return;
+
+ createTask({
+ poster: "GPOSTER1",
+ title: "Frontend Task",
+ description: "Build UI",
+ reward: 10_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 2,
+ organizationId: orgResult.organization.id,
+ });
+
+ createTask({
+ poster: "GPOSTER2",
+ title: "Backend Task",
+ description: "Build API",
+ reward: 15_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 1,
+ organizationId: orgResult.organization.id,
+ });
+
+ createTask({
+ poster: "GPOSTER3",
+ title: "Unrelated Task",
+ description: "Different org",
+ reward: 5_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 1,
+ organizationId: "",
+ });
+
+ const orgTasks = listTasks({ organizationId: orgResult.organization.id });
+ expect(orgTasks.tasks).toHaveLength(2);
+ expect(orgTasks.tasks.every((t) => t.organizationId === orgResult.organization.id)).toBe(
+ true
+ );
+ });
+
+ it("returns empty list when organization has no tasks", () => {
+ const orgResult = createOrganization({ name: "Empty Org" });
+ expect(orgResult.ok).toBe(true);
+ if (!orgResult.ok) return;
+
+ const tasks = listTasks({ organizationId: orgResult.organization.id });
+ expect(tasks.tasks).toHaveLength(0);
+ });
+
+ it("handles tasks with non-existent organization ID", () => {
+ createTask({
+ poster: "GBAD123",
+ title: "Orphaned Task",
+ description: "Task with invalid org ID",
+ reward: 1_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 1,
+ organizationId: "nonexistent",
+ });
+
+ const tasks = listTasks({ organizationId: "nonexistent" });
+ expect(tasks.tasks).toHaveLength(1);
+ });
+ });
+
+ describe("Organization Query Integration", () => {
+ it("combines organization filter with other filters", () => {
+ const orgResult = createOrganization({ name: "Filter Test Org" });
+ expect(orgResult.ok).toBe(true);
+ if (!orgResult.ok) return;
+
+ createTask({
+ poster: "GUSER1",
+ title: "Low Reward Task",
+ description: "Easy task",
+ reward: 2_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 1,
+ difficulty: "beginner",
+ organizationId: orgResult.organization.id,
+ });
+
+ createTask({
+ poster: "GUSER2",
+ title: "High Reward Task",
+ description: "Hard task",
+ reward: 20_000_000,
+ deadline: Math.floor(Date.now() / 1000) + 86400,
+ maxSubmissions: 1,
+ difficulty: "advanced",
+ organizationId: orgResult.organization.id,
+ });
+
+ const filtered = listTasks({
+ organizationId: orgResult.organization.id,
+ minReward: 10_000_000,
+ });
+
+ expect(filtered.tasks).toHaveLength(1);
+ expect(filtered.tasks[0].title).toBe("High Reward Task");
+ });
+ });
+});
diff --git a/frontend/src/lib/organization-store.ts b/frontend/src/lib/organization-store.ts
new file mode 100644
index 0000000..85ac95d
--- /dev/null
+++ b/frontend/src/lib/organization-store.ts
@@ -0,0 +1,156 @@
+/**
+ * Organization Store
+ *
+ * In-memory storage and management for organizations that publish/manage tasks.
+ * Follows the same pattern as task-workflow.ts for consistency.
+ */
+
+import type { Organization, CreateOrganizationInput } from "@/types/organization";
+
+type OrganizationSuccess = { ok: true } & T;
+
+type OrganizationFailure = {
+ ok: false;
+ status: 400 | 404 | 409;
+ error: string;
+ details?: string[];
+};
+
+export type OrganizationResult = OrganizationSuccess | OrganizationFailure;
+
+// In-memory storage
+const organizations = new Map();
+const organizationsByName = new Map(); // name -> id mapping
+let nextOrganizationId = 1;
+
+/**
+ * Validates organization creation input
+ */
+function validateCreateOrganizationInput(input: CreateOrganizationInput): string[] {
+ const errors: string[] = [];
+
+ if (!input.name?.trim()) {
+ errors.push("Organization name is required.");
+ }
+
+ if (input.name && input.name.trim().length < 2) {
+ errors.push("Organization name must be at least 2 characters.");
+ }
+
+ if (input.name && input.name.trim().length > 100) {
+ errors.push("Organization name must not exceed 100 characters.");
+ }
+
+ if (input.website && input.website.trim()) {
+ const urlPattern = /^https?:\/\/.+\..+/i;
+ if (!urlPattern.test(input.website.trim())) {
+ errors.push("Website must be a valid URL (http:// or https://).");
+ }
+ }
+
+ if (input.description && input.description.trim().length > 500) {
+ errors.push("Description must not exceed 500 characters.");
+ }
+
+ return errors;
+}
+
+/**
+ * Creates a new organization.
+ */
+export function createOrganization(
+ input: CreateOrganizationInput,
+ now: Date = new Date(),
+): OrganizationResult<{ organization: Organization }> {
+ const errors = validateCreateOrganizationInput(input);
+
+ if (errors.length > 0) {
+ return {
+ ok: false,
+ status: 400,
+ error: "Invalid organization data.",
+ details: errors,
+ };
+ }
+
+ const name = input.name.trim();
+ const nameLower = name.toLowerCase();
+
+ // Check for duplicate name
+ if (organizationsByName.has(nameLower)) {
+ return {
+ ok: false,
+ status: 409,
+ error: "An organization with this name already exists.",
+ };
+ }
+
+ const id = String(nextOrganizationId++);
+ const organization: Organization = {
+ id,
+ name,
+ description: input.description?.trim() ?? "",
+ website: input.website?.trim() ?? "",
+ createdAt: now.toISOString(),
+ };
+
+ organizations.set(id, organization);
+ organizationsByName.set(nameLower, id);
+
+ return { ok: true, organization };
+}
+
+/**
+ * Retrieves an organization by ID.
+ */
+export function getOrganization(
+ organizationId: string,
+): OrganizationResult<{ organization: Organization }> {
+ const organization = organizations.get(organizationId);
+
+ if (!organization) {
+ return {
+ ok: false,
+ status: 404,
+ error: "Organization not found.",
+ };
+ }
+
+ return { ok: true, organization: { ...organization } };
+}
+
+/**
+ * Retrieves an organization by name (case-insensitive).
+ */
+export function getOrganizationByName(
+ name: string,
+): OrganizationResult<{ organization: Organization }> {
+ const nameLower = name.trim().toLowerCase();
+ const organizationId = organizationsByName.get(nameLower);
+
+ if (!organizationId) {
+ return {
+ ok: false,
+ status: 404,
+ error: "Organization not found.",
+ };
+ }
+
+ return getOrganization(organizationId);
+}
+
+/**
+ * Lists all organizations.
+ */
+export function listOrganizations(): Organization[] {
+ return Array.from(organizations.values()).map((org) => ({ ...org }));
+}
+
+/**
+ * Resets the organization store (for testing).
+ */
+export function resetOrganizationStore(): void {
+ organizations.clear();
+ organizationsByName.clear();
+ nextOrganizationId = 1;
+}
diff --git a/frontend/src/lib/task-listing.test.ts b/frontend/src/lib/task-listing.test.ts
index 7532725..d062ed4 100644
--- a/frontend/src/lib/task-listing.test.ts
+++ b/frontend/src/lib/task-listing.test.ts
@@ -17,7 +17,7 @@ function seedTasks() {
maxSubmissions: 3,
difficulty: "advanced",
technologies: ["Rust", "Soroban"],
- organization: "Stellar Development Foundation",
+ organizationId: "org-sdf",
},
new Date("2026-01-01T00:00:00.000Z"),
);
@@ -32,7 +32,7 @@ function seedTasks() {
maxSubmissions: 2,
difficulty: "beginner",
technologies: ["Figma", "CSS"],
- organization: "Acme DAO",
+ organizationId: "org-acme",
},
new Date("2026-01-02T00:00:00.000Z"),
);
@@ -47,7 +47,7 @@ function seedTasks() {
maxSubmissions: 1,
difficulty: "intermediate",
technologies: ["React", "TypeScript"],
- organization: "Acme DAO",
+ organizationId: "org-acme",
},
new Date("2026-01-03T00:00:00.000Z"),
);
@@ -72,7 +72,7 @@ describe("listTasks", () => {
if (!result.ok) return;
expect(result.task.difficulty).toBe("intermediate");
expect(result.task.technologies).toEqual([]);
- expect(result.task.organization).toBe("");
+ expect(result.task.organizationId).toBe("");
});
it("returns all tasks with no filters, newest first by default", () => {
@@ -115,10 +115,10 @@ describe("listTasks", () => {
expect(result.tasks[0].title).toBe("Build a Soroban escrow contract");
});
- it("filters by organization (case-insensitive substring match)", () => {
+ it("filters by organization ID", () => {
seedTasks();
- const result = listTasks({ organization: "acme" });
+ const result = listTasks({ organizationId: "org-acme" });
expect(result.tasks).toHaveLength(2);
expect(result.tasks.map((t) => t.title).sort()).toEqual(
@@ -138,12 +138,12 @@ describe("listTasks", () => {
it("combines multiple filters with AND semantics", () => {
seedTasks();
- const result = listTasks({ organization: "acme", difficulty: "beginner" });
+ const result = listTasks({ organizationId: "org-acme", difficulty: "beginner" });
expect(result.tasks).toHaveLength(1);
expect(result.tasks[0].title).toBe("Design a landing page");
- const empty = listTasks({ organization: "acme", difficulty: "advanced" });
+ const empty = listTasks({ organizationId: "org-acme", difficulty: "advanced" });
expect(empty.tasks).toHaveLength(0);
expect(empty.total).toBe(0);
});
@@ -233,7 +233,7 @@ describe("listTasks", () => {
maxSubmissions: 1,
difficulty: i % 2 === 0 ? "beginner" : "advanced",
technologies: ["TypeScript"],
- organization: "Bulk Org",
+ organizationId: "org-bulk",
});
}
diff --git a/frontend/src/lib/task-workflow.ts b/frontend/src/lib/task-workflow.ts
index e1a3337..6ec5381 100644
--- a/frontend/src/lib/task-workflow.ts
+++ b/frontend/src/lib/task-workflow.ts
@@ -114,7 +114,7 @@ export function createTask(
technologies: (input.technologies ?? [])
.map((tech) => tech.trim())
.filter((tech) => tech.length > 0),
- organization: input.organization?.trim() ?? "",
+ organizationId: input.organizationId?.trim() ?? "",
};
tasks.set(id, task);
@@ -157,7 +157,7 @@ export function getTask(taskId: string): WorkflowResult<{ task: TaskRecord }> {
export function listTasks(query: ListTasksQuery = {}): ListTasksResult {
const search = query.search?.trim().toLowerCase();
const technology = query.technology?.trim().toLowerCase();
- const organization = query.organization?.trim().toLowerCase();
+ const organizationId = query.organizationId?.trim();
const filtered = Array.from(tasks.values()).filter((task) => {
if (typeof query.minReward === "number" && task.reward < query.minReward) {
@@ -175,7 +175,7 @@ export function listTasks(query: ListTasksQuery = {}): ListTasksResult {
) {
return false;
}
- if (organization && !task.organization.toLowerCase().includes(organization)) {
+ if (organizationId && task.organizationId !== organizationId) {
return false;
}
if (search) {
diff --git a/frontend/src/types/organization.ts b/frontend/src/types/organization.ts
new file mode 100644
index 0000000..3473991
--- /dev/null
+++ b/frontend/src/types/organization.ts
@@ -0,0 +1,19 @@
+/**
+ * Organization Types
+ *
+ * Represents organizations that publish or manage tasks/bounties.
+ */
+
+export interface Organization {
+ id: string;
+ name: string;
+ description: string;
+ website: string;
+ createdAt: string;
+}
+
+export interface CreateOrganizationInput {
+ name: string;
+ description?: string;
+ website?: string;
+}
diff --git a/frontend/src/types/task-workflow.ts b/frontend/src/types/task-workflow.ts
index 3b07e17..16b9288 100644
--- a/frontend/src/types/task-workflow.ts
+++ b/frontend/src/types/task-workflow.ts
@@ -18,8 +18,8 @@ export interface TaskRecord {
difficulty: TaskDifficulty;
/** Technology/skill tags, e.g. ["Rust", "Soroban"]. */
technologies: string[];
- /** Name of the organization or team posting the bounty, if any. */
- organization: string;
+ /** ID of the organization posting the bounty, if any. */
+ organizationId: string;
}
export interface SubmissionRecord {
@@ -48,7 +48,7 @@ export interface CreateTaskInput {
maxSubmissions: number;
difficulty?: TaskDifficulty;
technologies?: string[];
- organization?: string;
+ organizationId?: string;
}
export interface SubmitTaskInput {
@@ -84,8 +84,8 @@ export interface ListTasksQuery {
difficulty?: TaskDifficulty;
/** Matches tasks whose technologies list includes this value (case-insensitive). */
technology?: string;
- /** Case-insensitive substring match against organization. */
- organization?: string;
+ /** Filter by organization ID. */
+ organizationId?: string;
sort?: TaskSortOrder;
/** 1-based page number. Defaults to 1. */
page?: number;