@hasna/todos 0.15.29 → 0.15.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cloud-router.d.ts +13 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-registration-commands.d.ts +2 -0
- package/dist/cli/commands/project-registration-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2835 -334
- package/dist/contracts.js +52 -2
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1447 -144
- package/dist/lib/task-parent-integrity.d.ts +14 -0
- package/dist/lib/task-parent-integrity.d.ts.map +1 -0
- package/dist/mcp/index.js +2031 -135
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/project-registration/adoption-validation.d.ts +3 -0
- package/dist/project-registration/adoption-validation.d.ts.map +1 -0
- package/dist/project-registration/authority.d.ts +3 -1
- package/dist/project-registration/authority.d.ts.map +1 -1
- package/dist/project-registration/backend.d.ts +24 -1
- package/dist/project-registration/backend.d.ts.map +1 -1
- package/dist/project-registration/http.d.ts +3 -1
- package/dist/project-registration/http.d.ts.map +1 -1
- package/dist/project-registration/index.d.ts +2 -1
- package/dist/project-registration/index.d.ts.map +1 -1
- package/dist/project-registration/page-validation.d.ts +5 -0
- package/dist/project-registration/page-validation.d.ts.map +1 -0
- package/dist/project-registration/postgres.d.ts +13 -1
- package/dist/project-registration/postgres.d.ts.map +1 -1
- package/dist/project-registration/sqlite.d.ts +13 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration/types.d.ts +68 -1
- package/dist/project-registration/types.d.ts.map +1 -1
- package/dist/project-registration.js +831 -58
- package/dist/registry.js +52 -2
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +151 -0
- package/dist/sdk/v1.generated.d.ts +282 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +2022 -126
- package/dist/server/openapi.d.ts +2856 -1357
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +8 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.js +232 -19
- package/dist/task-manifest/authority.d.ts +13 -1
- package/dist/task-manifest/authority.d.ts.map +1 -1
- package/dist/task-manifest/backend.d.ts +5 -0
- package/dist/task-manifest/backend.d.ts.map +1 -1
- package/dist/task-manifest/index.d.ts +2 -2
- package/dist/task-manifest/index.d.ts.map +1 -1
- package/dist/task-manifest/plan-slug.d.ts +20 -0
- package/dist/task-manifest/plan-slug.d.ts.map +1 -1
- package/dist/task-manifest/postgres.d.ts +1 -0
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest/schema-sql.d.ts.map +1 -1
- package/dist/task-manifest/schema.d.ts.map +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -0
- package/dist/task-manifest/sqlite.d.ts.map +1 -1
- package/dist/task-manifest/types.d.ts +21 -1
- package/dist/task-manifest/types.d.ts.map +1 -1
- package/dist/task-manifest.js +592 -61
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1015,6 +1015,46 @@ var init_stale_lock_handoff = __esm(() => {
|
|
|
1015
1015
|
CANONICAL_LOCK_VERSION_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
1016
1016
|
});
|
|
1017
1017
|
|
|
1018
|
+
// src/lib/task-parent-integrity.ts
|
|
1019
|
+
function parentCycleError(taskId, parentId) {
|
|
1020
|
+
return new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentId} to task ${taskId} would create or retain a parent cycle`);
|
|
1021
|
+
}
|
|
1022
|
+
function assertTaskParentIntegrity(taskId, parentId, getTask) {
|
|
1023
|
+
if (parentId === undefined || parentId === null)
|
|
1024
|
+
return;
|
|
1025
|
+
const visited = new Set;
|
|
1026
|
+
let cursor = parentId;
|
|
1027
|
+
while (cursor) {
|
|
1028
|
+
if (cursor === taskId || visited.has(cursor)) {
|
|
1029
|
+
throw parentCycleError(taskId, parentId);
|
|
1030
|
+
}
|
|
1031
|
+
visited.add(cursor);
|
|
1032
|
+
const parent = getTask(cursor);
|
|
1033
|
+
if (!parent)
|
|
1034
|
+
throw new TaskNotFoundError(cursor);
|
|
1035
|
+
cursor = parent.parent_id;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
async function assertTaskParentIntegrityAsync(taskId, parentId, getTask) {
|
|
1039
|
+
if (parentId === undefined || parentId === null)
|
|
1040
|
+
return;
|
|
1041
|
+
const visited = new Set;
|
|
1042
|
+
let cursor = parentId;
|
|
1043
|
+
while (cursor) {
|
|
1044
|
+
if (cursor === taskId || visited.has(cursor)) {
|
|
1045
|
+
throw parentCycleError(taskId, parentId);
|
|
1046
|
+
}
|
|
1047
|
+
visited.add(cursor);
|
|
1048
|
+
const parent = await getTask(cursor);
|
|
1049
|
+
if (!parent)
|
|
1050
|
+
throw new TaskNotFoundError(cursor);
|
|
1051
|
+
cursor = parent.parent_id;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
var init_task_parent_integrity = __esm(() => {
|
|
1055
|
+
init_types();
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1018
1058
|
// src/project-registration/schema.ts
|
|
1019
1059
|
function sqliteTodosProjectRegistrationSchemaSql() {
|
|
1020
1060
|
return `
|
|
@@ -9440,6 +9480,7 @@ function createTaskStored(input, d) {
|
|
|
9440
9480
|
let id = uuid();
|
|
9441
9481
|
for (let attempt = 0;attempt < 3; attempt++) {
|
|
9442
9482
|
try {
|
|
9483
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
9443
9484
|
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, created_by, assigned_from_project, task_type, machine_id)
|
|
9444
9485
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
9445
9486
|
id,
|
|
@@ -9818,6 +9859,7 @@ function updateTaskStored(id, input, db) {
|
|
|
9818
9859
|
throw new VersionConflictError(id, input.version, task.version);
|
|
9819
9860
|
}
|
|
9820
9861
|
input = sanitizeUpdateTaskInput(input);
|
|
9862
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
9821
9863
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
|
|
9822
9864
|
const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
|
|
9823
9865
|
if (linkedProjectId) {
|
|
@@ -9871,6 +9913,10 @@ function updateTaskStored(id, input, db) {
|
|
|
9871
9913
|
sets.push("project_id = ?");
|
|
9872
9914
|
params.push(input.project_id);
|
|
9873
9915
|
}
|
|
9916
|
+
if (input.parent_id !== undefined) {
|
|
9917
|
+
sets.push("parent_id = ?");
|
|
9918
|
+
params.push(input.parent_id);
|
|
9919
|
+
}
|
|
9874
9920
|
if (input.assigned_to !== undefined) {
|
|
9875
9921
|
sets.push("assigned_to = ?");
|
|
9876
9922
|
params.push(input.assigned_to);
|
|
@@ -9986,6 +10032,8 @@ function updateTaskStored(id, input, db) {
|
|
|
9986
10032
|
logTaskChange2(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
9987
10033
|
if (input.title !== undefined && input.title !== task.title)
|
|
9988
10034
|
logTaskChange2(id, "update", "title", task.title, input.title, agentId, d);
|
|
10035
|
+
if (input.parent_id !== undefined && input.parent_id !== task.parent_id)
|
|
10036
|
+
logTaskChange2(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
|
|
9989
10037
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
9990
10038
|
logTaskChange2(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
9991
10039
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
@@ -10043,7 +10091,8 @@ function updateTask2(id, input, db) {
|
|
|
10043
10091
|
if (!before)
|
|
10044
10092
|
throw new TaskNotFoundError(id);
|
|
10045
10093
|
const guardedPlanIds = [before.plan_id, input.plan_id];
|
|
10046
|
-
|
|
10094
|
+
const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
|
|
10095
|
+
if (!needsSerializedWrite)
|
|
10047
10096
|
return updateTaskStored(id, input, d);
|
|
10048
10097
|
return d.transaction(() => {
|
|
10049
10098
|
guardPlanRowsSqlite(guardedPlanIds, d);
|
|
@@ -10079,6 +10128,7 @@ var init_task_crud = __esm(() => {
|
|
|
10079
10128
|
init_checklists();
|
|
10080
10129
|
init_storage_tombstones();
|
|
10081
10130
|
init_prewrite_secrets();
|
|
10131
|
+
init_task_parent_integrity();
|
|
10082
10132
|
});
|
|
10083
10133
|
|
|
10084
10134
|
// src/db/task-status.ts
|
|
@@ -12669,11 +12719,11 @@ var init_tasks = __esm(() => {
|
|
|
12669
12719
|
});
|
|
12670
12720
|
|
|
12671
12721
|
// src/project-registration/authority.ts
|
|
12672
|
-
import { createHash as
|
|
12722
|
+
import { createHash as createHash7 } from "crypto";
|
|
12673
12723
|
// package.json
|
|
12674
12724
|
var package_default = {
|
|
12675
12725
|
name: "@hasna/todos",
|
|
12676
|
-
version: "0.15.
|
|
12726
|
+
version: "0.15.32",
|
|
12677
12727
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12678
12728
|
type: "module",
|
|
12679
12729
|
main: "dist/index.js",
|
|
@@ -13526,6 +13576,7 @@ function measureIntegrityRows(spec, sets) {
|
|
|
13526
13576
|
|
|
13527
13577
|
// src/storage/postgres-adapter.ts
|
|
13528
13578
|
init_redaction();
|
|
13579
|
+
init_task_parent_integrity();
|
|
13529
13580
|
|
|
13530
13581
|
// src/storage/audit-history-import.ts
|
|
13531
13582
|
var AUDIT_HISTORY_DIVERGENT_REPLAY = "AUDIT_HISTORY_DIVERGENT_REPLAY";
|
|
@@ -13597,8 +13648,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
13597
13648
|
resolveRef: (ref) => store.resolveTaskRef(ref),
|
|
13598
13649
|
list: (filter = {}) => store.listTasks(filter),
|
|
13599
13650
|
count: (filter = {}) => store.countTasks(filter),
|
|
13600
|
-
update: (id, input) => updateTask(id, input, store),
|
|
13601
|
-
delete: (id, context) => store.
|
|
13651
|
+
update: (id, input, context) => updateTask(id, input, store, context),
|
|
13652
|
+
delete: (id, context) => store.deleteTaskHierarchy(id, context),
|
|
13602
13653
|
start: (id, agentId) => startTask(id, agentId, store),
|
|
13603
13654
|
complete: (id, agentId, options2) => completeTask(id, agentId, options2, store),
|
|
13604
13655
|
fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, store),
|
|
@@ -14131,22 +14182,70 @@ class PostgresJsonRecordStore {
|
|
|
14131
14182
|
return "identical";
|
|
14132
14183
|
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
14133
14184
|
}
|
|
14134
|
-
async
|
|
14185
|
+
async withTaskParentIntegrityTransaction(fn) {
|
|
14186
|
+
if (typeof this.options.client.transaction !== "function") {
|
|
14187
|
+
throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
|
|
14188
|
+
}
|
|
14189
|
+
return this.options.client.transaction(async (client) => {
|
|
14190
|
+
await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
|
|
14191
|
+
return fn(client);
|
|
14192
|
+
});
|
|
14193
|
+
}
|
|
14194
|
+
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
|
|
14135
14195
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
14136
|
-
if (planIds.length === 0)
|
|
14196
|
+
if (planIds.length === 0 && !parentGuard)
|
|
14137
14197
|
return this.upsert("tasks", value, context);
|
|
14138
14198
|
await this.ensureSchema();
|
|
14199
|
+
if (parentGuard && !queryClient) {
|
|
14200
|
+
return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
|
|
14201
|
+
}
|
|
14202
|
+
const client = queryClient ?? this.options.client;
|
|
14139
14203
|
const updatedAt = value.updated_at;
|
|
14140
14204
|
const targetPlanId = value.plan_id;
|
|
14141
|
-
const result = await
|
|
14205
|
+
const result = await client.query(`/* todos:task-plan-membership-guard todos:task-parent-integrity-guard */ WITH RECURSIVE
|
|
14206
|
+
locked_task AS MATERIALIZED (
|
|
14207
|
+
SELECT payload FROM ${this.tableName}
|
|
14208
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
|
|
14209
|
+
FOR UPDATE
|
|
14210
|
+
),
|
|
14142
14211
|
locked_plans AS MATERIALIZED (
|
|
14143
14212
|
SELECT object_id, payload FROM ${this.tableName}
|
|
14144
14213
|
WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
|
|
14145
14214
|
AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
|
|
14146
14215
|
ORDER BY object_id
|
|
14147
14216
|
FOR UPDATE
|
|
14217
|
+
), parent_chain(object_id, payload, path, cycle) AS (
|
|
14218
|
+
SELECT parent.object_id, parent.payload, ARRAY[parent.object_id], false
|
|
14219
|
+
FROM ${this.tableName} AS parent
|
|
14220
|
+
WHERE $10::boolean
|
|
14221
|
+
AND $11::text IS NOT NULL
|
|
14222
|
+
AND parent.service = $1
|
|
14223
|
+
AND parent.object_type = 'tasks'
|
|
14224
|
+
AND parent.object_id = $11
|
|
14225
|
+
AND parent.deleted_at IS NULL
|
|
14226
|
+
UNION ALL
|
|
14227
|
+
SELECT ancestor.object_id,
|
|
14228
|
+
ancestor.payload,
|
|
14229
|
+
chain.path || ancestor.object_id,
|
|
14230
|
+
ancestor.object_id = ANY(chain.path)
|
|
14231
|
+
FROM parent_chain AS chain
|
|
14232
|
+
JOIN ${this.tableName} AS ancestor
|
|
14233
|
+
ON ancestor.service = $1
|
|
14234
|
+
AND ancestor.object_type = 'tasks'
|
|
14235
|
+
AND ancestor.object_id = chain.payload->>'parent_id'
|
|
14236
|
+
AND ancestor.deleted_at IS NULL
|
|
14237
|
+
WHERE NOT chain.cycle
|
|
14148
14238
|
), validation AS (
|
|
14149
14239
|
SELECT
|
|
14240
|
+
(NOT $10::boolean OR NOT $13::boolean OR EXISTS (SELECT 1 FROM locked_task)) AS task_found,
|
|
14241
|
+
(NOT $10::boolean OR NOT $13::boolean
|
|
14242
|
+
OR (SELECT (payload->>'version')::integer FROM locked_task) = $12::integer) AS version_matches,
|
|
14243
|
+
(NOT $10::boolean OR $11::text IS NULL
|
|
14244
|
+
OR EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $11)) AS parent_found,
|
|
14245
|
+
(NOT $10::boolean OR $11::text IS NULL
|
|
14246
|
+
OR ($11::text <> $2
|
|
14247
|
+
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
|
|
14248
|
+
AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
|
|
14150
14249
|
(SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
|
|
14151
14250
|
($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
14152
14251
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
@@ -14167,7 +14266,13 @@ class PostgresJsonRecordStore {
|
|
|
14167
14266
|
)
|
|
14168
14267
|
SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
|
|
14169
14268
|
FROM guarded
|
|
14170
|
-
WHERE guarded.
|
|
14269
|
+
WHERE guarded.task_found
|
|
14270
|
+
AND guarded.version_matches
|
|
14271
|
+
AND guarded.parent_found
|
|
14272
|
+
AND guarded.parent_acyclic
|
|
14273
|
+
AND guarded.all_plans_found
|
|
14274
|
+
AND guarded.target_plan_found
|
|
14275
|
+
AND NOT guarded.project_conflict
|
|
14171
14276
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
14172
14277
|
payload = EXCLUDED.payload,
|
|
14173
14278
|
updated_at = EXCLUDED.updated_at,
|
|
@@ -14180,8 +14285,10 @@ class PostgresJsonRecordStore {
|
|
|
14180
14285
|
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
14181
14286
|
RETURNING payload
|
|
14182
14287
|
)
|
|
14183
|
-
SELECT guarded.
|
|
14184
|
-
|
|
14288
|
+
SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
|
|
14289
|
+
guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
|
|
14290
|
+
(SELECT payload FROM stored) AS payload,
|
|
14291
|
+
(SELECT payload FROM locked_task) AS current_payload
|
|
14185
14292
|
FROM guarded`, [
|
|
14186
14293
|
this.service,
|
|
14187
14294
|
value.id,
|
|
@@ -14191,9 +14298,26 @@ class PostgresJsonRecordStore {
|
|
|
14191
14298
|
numberValue2(value.version),
|
|
14192
14299
|
jsonbParam(planIds),
|
|
14193
14300
|
targetPlanId,
|
|
14194
|
-
explicitProject
|
|
14301
|
+
explicitProject,
|
|
14302
|
+
Boolean(parentGuard),
|
|
14303
|
+
parentGuard?.parentId ?? null,
|
|
14304
|
+
parentGuard?.expectedVersion ?? null,
|
|
14305
|
+
parentGuard?.operation === "update"
|
|
14195
14306
|
]);
|
|
14196
14307
|
const row = result.rows[0];
|
|
14308
|
+
if (parentGuard && !row?.task_found) {
|
|
14309
|
+
throw new TaskNotFoundError(value.id);
|
|
14310
|
+
}
|
|
14311
|
+
if (parentGuard && !row?.version_matches) {
|
|
14312
|
+
const current = row?.current_payload ? payloadRecord2(row.current_payload) : await this.get("tasks", value.id);
|
|
14313
|
+
throw new VersionConflictError(value.id, parentGuard.expectedVersion, current?.version ?? -1);
|
|
14314
|
+
}
|
|
14315
|
+
if (parentGuard && !row?.parent_found && parentGuard.parentId) {
|
|
14316
|
+
throw new TaskNotFoundError(parentGuard.parentId);
|
|
14317
|
+
}
|
|
14318
|
+
if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
|
|
14319
|
+
throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
|
|
14320
|
+
}
|
|
14197
14321
|
if (!row?.all_plans_found || !row.target_plan_found) {
|
|
14198
14322
|
throw new PlanProjectLinkError("PLAN_PROJECT_LINK_PLAN_NOT_FOUND", `Plan membership changed through a missing plan: ${targetPlanId ?? planIds.join(", ")}`, { plan_ids: planIds, target_plan_id: targetPlanId });
|
|
14199
14323
|
}
|
|
@@ -14201,6 +14325,8 @@ class PostgresJsonRecordStore {
|
|
|
14201
14325
|
throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
|
|
14202
14326
|
}
|
|
14203
14327
|
if (!row.payload) {
|
|
14328
|
+
if (row.current_payload)
|
|
14329
|
+
return payloadRecord2(row.current_payload);
|
|
14204
14330
|
return await requireRecord("tasks", value.id, this);
|
|
14205
14331
|
}
|
|
14206
14332
|
return payloadRecord2(row.payload);
|
|
@@ -14526,6 +14652,80 @@ class PostgresJsonRecordStore {
|
|
|
14526
14652
|
version: numberValue2(existing["version"])
|
|
14527
14653
|
}, context);
|
|
14528
14654
|
}
|
|
14655
|
+
async deleteTaskHierarchy(id, context = {}) {
|
|
14656
|
+
await this.ensureSchema();
|
|
14657
|
+
return this.withTaskParentIntegrityTransaction(async (client) => {
|
|
14658
|
+
const timestamp = new Date().toISOString();
|
|
14659
|
+
const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
|
|
14660
|
+
task_tree(object_id, path, cycle) AS (
|
|
14661
|
+
SELECT task.object_id, ARRAY[task.object_id], false
|
|
14662
|
+
FROM ${this.tableName} AS task
|
|
14663
|
+
WHERE task.service = $1
|
|
14664
|
+
AND task.object_type = 'tasks'
|
|
14665
|
+
AND task.object_id = $2
|
|
14666
|
+
AND task.deleted_at IS NULL
|
|
14667
|
+
UNION ALL
|
|
14668
|
+
SELECT child.object_id,
|
|
14669
|
+
tree.path || child.object_id,
|
|
14670
|
+
child.object_id = ANY(tree.path)
|
|
14671
|
+
FROM task_tree AS tree
|
|
14672
|
+
JOIN ${this.tableName} AS child
|
|
14673
|
+
ON child.service = $1
|
|
14674
|
+
AND child.object_type = 'tasks'
|
|
14675
|
+
AND child.payload->>'parent_id' = tree.object_id
|
|
14676
|
+
AND child.deleted_at IS NULL
|
|
14677
|
+
WHERE NOT tree.cycle
|
|
14678
|
+
), tombstoned AS (
|
|
14679
|
+
UPDATE ${this.tableName} AS task
|
|
14680
|
+
SET deleted_at = $3::timestamptz,
|
|
14681
|
+
updated_at = $3::timestamptz,
|
|
14682
|
+
source_machine_id = $4
|
|
14683
|
+
WHERE task.service = $1
|
|
14684
|
+
AND task.object_type = 'tasks'
|
|
14685
|
+
AND task.deleted_at IS NULL
|
|
14686
|
+
AND task.object_id IN (
|
|
14687
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
14688
|
+
)
|
|
14689
|
+
RETURNING task.object_id
|
|
14690
|
+
), tombstoned_related AS (
|
|
14691
|
+
UPDATE ${this.tableName} AS related
|
|
14692
|
+
SET deleted_at = $3::timestamptz,
|
|
14693
|
+
updated_at = $3::timestamptz,
|
|
14694
|
+
source_machine_id = $4
|
|
14695
|
+
WHERE related.service = $1
|
|
14696
|
+
AND related.deleted_at IS NULL
|
|
14697
|
+
AND (
|
|
14698
|
+
(
|
|
14699
|
+
related.object_type = 'dependencies'
|
|
14700
|
+
AND (
|
|
14701
|
+
related.payload->>'task_id' IN (
|
|
14702
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
14703
|
+
)
|
|
14704
|
+
OR related.payload->>'depends_on' IN (
|
|
14705
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
14706
|
+
)
|
|
14707
|
+
)
|
|
14708
|
+
)
|
|
14709
|
+
OR (
|
|
14710
|
+
related.object_type IN ('comments', 'verifications', 'commits', 'refs')
|
|
14711
|
+
AND related.payload->>'task_id' IN (
|
|
14712
|
+
SELECT object_id FROM task_tree WHERE NOT cycle
|
|
14713
|
+
)
|
|
14714
|
+
)
|
|
14715
|
+
)
|
|
14716
|
+
RETURNING related.object_id
|
|
14717
|
+
)
|
|
14718
|
+
SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
|
|
14719
|
+
(SELECT count(*) FROM tombstoned) AS deleted_count,
|
|
14720
|
+
(SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
|
|
14721
|
+
this.service,
|
|
14722
|
+
id,
|
|
14723
|
+
timestamp,
|
|
14724
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
14725
|
+
]);
|
|
14726
|
+
return Boolean(result.rows[0]?.found);
|
|
14727
|
+
});
|
|
14728
|
+
}
|
|
14529
14729
|
async getPlanProjectLinkReceipt(receiptId) {
|
|
14530
14730
|
const value = await this.get("plan_project_link_receipts", receiptId);
|
|
14531
14731
|
return value ? assertPlanProjectLinkReceipt(value) : null;
|
|
@@ -14946,9 +15146,8 @@ class PostgresJsonRecordStore {
|
|
|
14946
15146
|
}
|
|
14947
15147
|
async function createTask(input, store, context) {
|
|
14948
15148
|
const timestamp = new Date().toISOString();
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
}
|
|
15149
|
+
const taskId = randomUUID();
|
|
15150
|
+
await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
|
|
14952
15151
|
const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
|
|
14953
15152
|
const requestedProjectId = input.project_id ?? context?.projectId ?? null;
|
|
14954
15153
|
if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
|
|
@@ -14957,7 +15156,7 @@ async function createTask(input, store, context) {
|
|
|
14957
15156
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
14958
15157
|
const shortId = effectiveProjectId ? await nextTaskShortId(effectiveProjectId, store, context) : null;
|
|
14959
15158
|
const task = {
|
|
14960
|
-
id:
|
|
15159
|
+
id: taskId,
|
|
14961
15160
|
short_id: shortId,
|
|
14962
15161
|
project_id: effectiveProjectId,
|
|
14963
15162
|
parent_id: input.parent_id ?? null,
|
|
@@ -15013,15 +15212,16 @@ async function createTask(input, store, context) {
|
|
|
15013
15212
|
synced_at: null,
|
|
15014
15213
|
archived_at: null
|
|
15015
15214
|
};
|
|
15016
|
-
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, task.plan_id ? [task.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context);
|
|
15215
|
+
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, task.plan_id ? [task.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context, input.parent_id ? { operation: "create", expectedVersion: 0, parentId: input.parent_id } : undefined);
|
|
15017
15216
|
await logTaskChange(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
|
|
15018
15217
|
return storedTask;
|
|
15019
15218
|
}
|
|
15020
|
-
async function updateTask(id, input, store) {
|
|
15219
|
+
async function updateTask(id, input, store, context) {
|
|
15021
15220
|
const existing = await requireRecord("tasks", id, store);
|
|
15022
15221
|
if (existing.version !== input.version) {
|
|
15023
|
-
throw new
|
|
15222
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
15024
15223
|
}
|
|
15224
|
+
await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
|
|
15025
15225
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
|
|
15026
15226
|
const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
|
|
15027
15227
|
if (linkedPlan?.project_id) {
|
|
@@ -15046,10 +15246,19 @@ async function updateTask(id, input, store) {
|
|
|
15046
15246
|
metadata: input.metadata ?? existing.metadata,
|
|
15047
15247
|
requires_approval: input.requires_approval ?? existing.requires_approval,
|
|
15048
15248
|
task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
|
|
15249
|
+
parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
|
|
15049
15250
|
created_by: existing.created_by,
|
|
15050
15251
|
completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
|
|
15051
15252
|
};
|
|
15052
|
-
|
|
15253
|
+
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
|
|
15254
|
+
operation: "update",
|
|
15255
|
+
expectedVersion: input.version,
|
|
15256
|
+
parentId: input.parent_id !== undefined ? input.parent_id : existing.parent_id
|
|
15257
|
+
});
|
|
15258
|
+
if (input.parent_id !== undefined && input.parent_id !== existing.parent_id) {
|
|
15259
|
+
await logTaskChange(id, "update", "parent_id", existing.parent_id, input.parent_id, existing.assigned_to ?? existing.agent_id, store, context);
|
|
15260
|
+
}
|
|
15261
|
+
return storedTask;
|
|
15053
15262
|
}
|
|
15054
15263
|
async function startTask(id, agentId, store) {
|
|
15055
15264
|
const task = await requireRecord("tasks", id, store);
|
|
@@ -15113,7 +15322,11 @@ async function patchTask(task, patch, store) {
|
|
|
15113
15322
|
version: task.version + 1,
|
|
15114
15323
|
updated_at: new Date().toISOString()
|
|
15115
15324
|
};
|
|
15116
|
-
return store.upsertTaskWithPlanMembershipGuard(updated, [task.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id")
|
|
15325
|
+
return store.upsertTaskWithPlanMembershipGuard(updated, [task.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"), {}, {
|
|
15326
|
+
operation: "update",
|
|
15327
|
+
expectedVersion: task.version,
|
|
15328
|
+
parentId: updated.parent_id
|
|
15329
|
+
});
|
|
15117
15330
|
}
|
|
15118
15331
|
var CLOUD_LOCK_EXPIRY_MINUTES = 30;
|
|
15119
15332
|
function sameCloudLockHolder(stored, incoming) {
|
|
@@ -16103,6 +16316,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
16103
16316
|
AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
|
|
16104
16317
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
16105
16318
|
LIMIT 1
|
|
16319
|
+
FOR UPDATE
|
|
16106
16320
|
`, [this.service, path, taskListSlug]);
|
|
16107
16321
|
return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
|
|
16108
16322
|
}
|
|
@@ -16113,6 +16327,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
16113
16327
|
AND payload->>'project_id' = $2 AND payload->>'slug' = $3
|
|
16114
16328
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
16115
16329
|
LIMIT 1
|
|
16330
|
+
FOR UPDATE
|
|
16116
16331
|
`, [this.service, projectId, slug]);
|
|
16117
16332
|
return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
|
|
16118
16333
|
}
|
|
@@ -16123,10 +16338,24 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
16123
16338
|
return await this.storage.taskLists.create(input);
|
|
16124
16339
|
}
|
|
16125
16340
|
async getProject(id) {
|
|
16126
|
-
|
|
16341
|
+
const result = await this.client.query(`
|
|
16342
|
+
SELECT payload FROM ${this.tableName}
|
|
16343
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id = $2
|
|
16344
|
+
AND deleted_at IS NULL
|
|
16345
|
+
LIMIT 1
|
|
16346
|
+
FOR SHARE
|
|
16347
|
+
`, [this.service, id]);
|
|
16348
|
+
return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
|
|
16127
16349
|
}
|
|
16128
16350
|
async getTaskList(id) {
|
|
16129
|
-
|
|
16351
|
+
const result = await this.client.query(`
|
|
16352
|
+
SELECT payload FROM ${this.tableName}
|
|
16353
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
|
|
16354
|
+
AND deleted_at IS NULL
|
|
16355
|
+
LIMIT 1
|
|
16356
|
+
FOR SHARE
|
|
16357
|
+
`, [this.service, id]);
|
|
16358
|
+
return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
|
|
16130
16359
|
}
|
|
16131
16360
|
async lockCompensationWrites() {
|
|
16132
16361
|
await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
|
|
@@ -16206,11 +16435,102 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
16206
16435
|
async getTaskList(id) {
|
|
16207
16436
|
return (await this.direct()).getTaskList(id);
|
|
16208
16437
|
}
|
|
16438
|
+
async getProjectResourceCollectionRevision(input) {
|
|
16439
|
+
await this.ensureSchema();
|
|
16440
|
+
const result = await this.client.query(`
|
|
16441
|
+
WITH resources(kind_rank, target_id, revision) AS (
|
|
16442
|
+
SELECT 0, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
16443
|
+
FROM ${this.tableName}
|
|
16444
|
+
WHERE service = $1 AND object_type = 'projects'
|
|
16445
|
+
AND deleted_at IS NULL AND object_id = $2
|
|
16446
|
+
UNION ALL
|
|
16447
|
+
SELECT 1, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
16448
|
+
FROM ${this.tableName}
|
|
16449
|
+
WHERE service = $1 AND object_type = 'task_lists'
|
|
16450
|
+
AND deleted_at IS NULL AND object_id = $3
|
|
16451
|
+
AND payload->>'project_id' = $2
|
|
16452
|
+
UNION ALL
|
|
16453
|
+
SELECT 2, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
16454
|
+
FROM ${this.tableName}
|
|
16455
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'plans'
|
|
16456
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
16457
|
+
UNION ALL
|
|
16458
|
+
SELECT 3, object_id, COALESCE(payload->>'updated_at', updated_at::text)
|
|
16459
|
+
FROM ${this.tableName}
|
|
16460
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
|
|
16461
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
16462
|
+
)
|
|
16463
|
+
SELECT 'md5:' || md5(COALESCE(string_agg(
|
|
16464
|
+
kind_rank::text || chr(31) || target_id || chr(31) || revision,
|
|
16465
|
+
chr(30) ORDER BY kind_rank ASC, target_id ASC
|
|
16466
|
+
), '')) AS revision
|
|
16467
|
+
FROM resources
|
|
16468
|
+
`, [
|
|
16469
|
+
this.service,
|
|
16470
|
+
input.todos_project_id,
|
|
16471
|
+
input.task_list_id,
|
|
16472
|
+
input.include_anchors
|
|
16473
|
+
]);
|
|
16474
|
+
const revision = result.rows[0]?.revision;
|
|
16475
|
+
if (!revision) {
|
|
16476
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "could not derive the hosted project-resource collection revision");
|
|
16477
|
+
}
|
|
16478
|
+
return revision;
|
|
16479
|
+
}
|
|
16480
|
+
async listProjectResourceCandidates(input) {
|
|
16481
|
+
await this.ensureSchema();
|
|
16482
|
+
const afterRank = input.after?.kind_rank ?? -1;
|
|
16483
|
+
const afterId = input.after?.target_id ?? "";
|
|
16484
|
+
const result = await this.client.query(`
|
|
16485
|
+
WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
|
|
16486
|
+
SELECT 'project'::text, 0, object_id, NULL::text,
|
|
16487
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
16488
|
+
FROM ${this.tableName}
|
|
16489
|
+
WHERE service = $1 AND object_type = 'projects'
|
|
16490
|
+
AND deleted_at IS NULL AND object_id = $2
|
|
16491
|
+
UNION ALL
|
|
16492
|
+
SELECT 'task_list'::text, 1, object_id, payload->>'project_id',
|
|
16493
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
16494
|
+
FROM ${this.tableName}
|
|
16495
|
+
WHERE service = $1 AND object_type = 'task_lists'
|
|
16496
|
+
AND deleted_at IS NULL AND object_id = $3
|
|
16497
|
+
AND payload->>'project_id' = $2
|
|
16498
|
+
UNION ALL
|
|
16499
|
+
SELECT 'plan'::text, 2, object_id, payload->>'project_id',
|
|
16500
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
16501
|
+
FROM ${this.tableName}
|
|
16502
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'plans'
|
|
16503
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
16504
|
+
UNION ALL
|
|
16505
|
+
SELECT 'task'::text, 3, object_id,
|
|
16506
|
+
COALESCE(payload->>'plan_id', payload->>'project_id'),
|
|
16507
|
+
COALESCE(payload->>'updated_at', updated_at::text)
|
|
16508
|
+
FROM ${this.tableName}
|
|
16509
|
+
WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
|
|
16510
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $2
|
|
16511
|
+
)
|
|
16512
|
+
SELECT kind, kind_rank, target_id, parent_id, revision
|
|
16513
|
+
FROM resources
|
|
16514
|
+
WHERE kind_rank > $5 OR (kind_rank = $5 AND target_id > $6)
|
|
16515
|
+
ORDER BY kind_rank ASC, target_id ASC
|
|
16516
|
+
LIMIT $7
|
|
16517
|
+
`, [
|
|
16518
|
+
this.service,
|
|
16519
|
+
input.todos_project_id,
|
|
16520
|
+
input.task_list_id,
|
|
16521
|
+
input.include_anchors,
|
|
16522
|
+
afterRank,
|
|
16523
|
+
afterId,
|
|
16524
|
+
input.limit
|
|
16525
|
+
]);
|
|
16526
|
+
return result.rows;
|
|
16527
|
+
}
|
|
16209
16528
|
}
|
|
16210
16529
|
|
|
16211
16530
|
// src/project-registration/sqlite.ts
|
|
16212
16531
|
init_database();
|
|
16213
16532
|
init_storage_tombstones();
|
|
16533
|
+
import { createHash as createHash6 } from "crypto";
|
|
16214
16534
|
|
|
16215
16535
|
// src/storage/local-sqlite.ts
|
|
16216
16536
|
init_types();
|
|
@@ -18518,6 +18838,64 @@ class SqliteTodosProjectRegistrationBackend {
|
|
|
18518
18838
|
getTaskList(id) {
|
|
18519
18839
|
return this.direct.getTaskList(id);
|
|
18520
18840
|
}
|
|
18841
|
+
async getProjectResourceCollectionRevision(input) {
|
|
18842
|
+
const digest = createHash6("sha256");
|
|
18843
|
+
const rows = this.db.query(`
|
|
18844
|
+
WITH resources(kind_rank, target_id, revision) AS (
|
|
18845
|
+
SELECT 0, id, updated_at
|
|
18846
|
+
FROM projects
|
|
18847
|
+
WHERE id = ?
|
|
18848
|
+
UNION ALL
|
|
18849
|
+
SELECT 1, id, updated_at
|
|
18850
|
+
FROM task_lists
|
|
18851
|
+
WHERE id = ? AND project_id = ?
|
|
18852
|
+
UNION ALL
|
|
18853
|
+
SELECT 2, id, updated_at
|
|
18854
|
+
FROM plans
|
|
18855
|
+
WHERE ? = 1 AND project_id = ?
|
|
18856
|
+
UNION ALL
|
|
18857
|
+
SELECT 3, id, updated_at
|
|
18858
|
+
FROM tasks
|
|
18859
|
+
WHERE ? = 1 AND project_id = ?
|
|
18860
|
+
)
|
|
18861
|
+
SELECT kind_rank, target_id, revision
|
|
18862
|
+
FROM resources
|
|
18863
|
+
ORDER BY kind_rank ASC, target_id ASC
|
|
18864
|
+
`).iterate(input.todos_project_id, input.task_list_id, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id);
|
|
18865
|
+
for (const row of rows) {
|
|
18866
|
+
digest.update(`${row.kind_rank}\x00${row.target_id}\x00${row.revision}
|
|
18867
|
+
`);
|
|
18868
|
+
}
|
|
18869
|
+
return `sha256:${digest.digest("hex")}`;
|
|
18870
|
+
}
|
|
18871
|
+
async listProjectResourceCandidates(input) {
|
|
18872
|
+
const afterRank = input.after?.kind_rank ?? -1;
|
|
18873
|
+
const afterId = input.after?.target_id ?? "";
|
|
18874
|
+
return this.db.query(`
|
|
18875
|
+
WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
|
|
18876
|
+
SELECT 'project', 0, id, NULL, updated_at
|
|
18877
|
+
FROM projects
|
|
18878
|
+
WHERE id = ?
|
|
18879
|
+
UNION ALL
|
|
18880
|
+
SELECT 'task_list', 1, id, project_id, updated_at
|
|
18881
|
+
FROM task_lists
|
|
18882
|
+
WHERE id = ? AND project_id = ?
|
|
18883
|
+
UNION ALL
|
|
18884
|
+
SELECT 'plan', 2, id, project_id, updated_at
|
|
18885
|
+
FROM plans
|
|
18886
|
+
WHERE ? = 1 AND project_id = ?
|
|
18887
|
+
UNION ALL
|
|
18888
|
+
SELECT 'task', 3, id, COALESCE(plan_id, project_id), updated_at
|
|
18889
|
+
FROM tasks
|
|
18890
|
+
WHERE ? = 1 AND project_id = ?
|
|
18891
|
+
)
|
|
18892
|
+
SELECT kind, kind_rank, target_id, parent_id, revision
|
|
18893
|
+
FROM resources
|
|
18894
|
+
WHERE kind_rank > ? OR (kind_rank = ? AND target_id > ?)
|
|
18895
|
+
ORDER BY kind_rank ASC, target_id ASC
|
|
18896
|
+
LIMIT ?
|
|
18897
|
+
`).all(input.todos_project_id, input.task_list_id, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, afterRank, afterRank, afterId, input.limit);
|
|
18898
|
+
}
|
|
18521
18899
|
}
|
|
18522
18900
|
|
|
18523
18901
|
// src/project-registration/authority.ts
|
|
@@ -18529,6 +18907,8 @@ var AUTHORITY_ROUTE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
|
18529
18907
|
var PACKAGE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
18530
18908
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
18531
18909
|
var IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
|
|
18910
|
+
var PROJECT_RESOURCE_PAGE_LIMIT = 500;
|
|
18911
|
+
var PROJECT_RESOURCE_CURSOR_VERSION = 1;
|
|
18532
18912
|
|
|
18533
18913
|
class WriteBoundaryError extends Error {
|
|
18534
18914
|
point;
|
|
@@ -18556,7 +18936,7 @@ function canonicalize3(value) {
|
|
|
18556
18936
|
return out;
|
|
18557
18937
|
}
|
|
18558
18938
|
function digestProjectRegistrationValue(value) {
|
|
18559
|
-
return
|
|
18939
|
+
return createHash7("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
|
|
18560
18940
|
}
|
|
18561
18941
|
function deriveTodosProjectRegistrationIdempotencyKey(input) {
|
|
18562
18942
|
return `prk_${digestProjectRegistrationValue({
|
|
@@ -18668,35 +19048,68 @@ function projectRecord(project) {
|
|
|
18668
19048
|
return {
|
|
18669
19049
|
target_id: project.id,
|
|
18670
19050
|
revision: project.updated_at,
|
|
18671
|
-
digest:
|
|
18672
|
-
id: project.id,
|
|
18673
|
-
name: project.name,
|
|
18674
|
-
path: project.path,
|
|
18675
|
-
description: project.description,
|
|
18676
|
-
task_list_id: project.task_list_id,
|
|
18677
|
-
task_prefix: project.task_prefix,
|
|
18678
|
-
task_counter: project.task_counter,
|
|
18679
|
-
created_at: project.created_at,
|
|
18680
|
-
updated_at: project.updated_at
|
|
18681
|
-
})
|
|
19051
|
+
digest: projectRegistrationDigest(project)
|
|
18682
19052
|
};
|
|
18683
19053
|
}
|
|
18684
19054
|
function taskListRecord(taskList) {
|
|
18685
19055
|
return {
|
|
18686
19056
|
target_id: taskList.id,
|
|
18687
19057
|
revision: taskList.updated_at,
|
|
18688
|
-
digest:
|
|
18689
|
-
|
|
18690
|
-
|
|
18691
|
-
|
|
18692
|
-
|
|
18693
|
-
|
|
18694
|
-
|
|
18695
|
-
|
|
18696
|
-
|
|
19058
|
+
digest: taskListRegistrationDigest(taskList)
|
|
19059
|
+
};
|
|
19060
|
+
}
|
|
19061
|
+
function boundExistingProjectRecord(project) {
|
|
19062
|
+
return {
|
|
19063
|
+
target_id: project.id,
|
|
19064
|
+
revision: project.created_at,
|
|
19065
|
+
digest: projectRegistrationDigest({
|
|
19066
|
+
...project,
|
|
19067
|
+
updated_at: project.created_at
|
|
19068
|
+
})
|
|
19069
|
+
};
|
|
19070
|
+
}
|
|
19071
|
+
function boundExistingTaskListRecord(taskList) {
|
|
19072
|
+
return {
|
|
19073
|
+
target_id: taskList.id,
|
|
19074
|
+
revision: taskList.created_at,
|
|
19075
|
+
digest: taskListRegistrationDigest({
|
|
19076
|
+
...taskList,
|
|
19077
|
+
updated_at: taskList.created_at
|
|
18697
19078
|
})
|
|
18698
19079
|
};
|
|
18699
19080
|
}
|
|
19081
|
+
function projectRegistrationDigest(project) {
|
|
19082
|
+
return digestProjectRegistrationValue({
|
|
19083
|
+
id: project.id,
|
|
19084
|
+
name: project.name,
|
|
19085
|
+
path: project.path,
|
|
19086
|
+
description: project.description,
|
|
19087
|
+
task_list_id: project.task_list_id,
|
|
19088
|
+
task_prefix: project.task_prefix,
|
|
19089
|
+
task_counter: project.task_counter,
|
|
19090
|
+
created_at: project.created_at,
|
|
19091
|
+
updated_at: project.updated_at
|
|
19092
|
+
});
|
|
19093
|
+
}
|
|
19094
|
+
function taskListRegistrationDigest(taskList) {
|
|
19095
|
+
return digestProjectRegistrationValue({
|
|
19096
|
+
id: taskList.id,
|
|
19097
|
+
project_id: taskList.project_id,
|
|
19098
|
+
slug: taskList.slug,
|
|
19099
|
+
name: taskList.name,
|
|
19100
|
+
description: taskList.description,
|
|
19101
|
+
metadata: taskList.metadata,
|
|
19102
|
+
created_at: taskList.created_at,
|
|
19103
|
+
updated_at: taskList.updated_at
|
|
19104
|
+
});
|
|
19105
|
+
}
|
|
19106
|
+
function canonicalValuesEqual(left, right) {
|
|
19107
|
+
try {
|
|
19108
|
+
return canonicalProjectRegistrationJson(left) === canonicalProjectRegistrationJson(right);
|
|
19109
|
+
} catch {
|
|
19110
|
+
return false;
|
|
19111
|
+
}
|
|
19112
|
+
}
|
|
18700
19113
|
function receiptId(input) {
|
|
18701
19114
|
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
18702
19115
|
}
|
|
@@ -18734,9 +19147,37 @@ function normalizedCallDigest(request) {
|
|
|
18734
19147
|
project_slug: request.project_slug,
|
|
18735
19148
|
project_name: request.project_name,
|
|
18736
19149
|
desired: request.desired,
|
|
19150
|
+
bind_existing: request.bind_existing === true,
|
|
18737
19151
|
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
18738
19152
|
});
|
|
18739
19153
|
}
|
|
19154
|
+
function legacyNormalizedCallDigestBeforeBindExisting(request) {
|
|
19155
|
+
return digestProjectRegistrationValue({
|
|
19156
|
+
authority_route: request.authority_route,
|
|
19157
|
+
package_version: request.package_version,
|
|
19158
|
+
authority_id: request.authority_id,
|
|
19159
|
+
tenant_id: request.tenant_id,
|
|
19160
|
+
corpus_id: request.corpus_id,
|
|
19161
|
+
operation_id: request.operation_id,
|
|
19162
|
+
step_id: request.step_id,
|
|
19163
|
+
resource_kind: request.resource_kind,
|
|
19164
|
+
direction: request.direction,
|
|
19165
|
+
target_selector: request.target_selector,
|
|
19166
|
+
idempotency_key: request.idempotency_key,
|
|
19167
|
+
request_digest: request.request_digest,
|
|
19168
|
+
precondition_digest: request.precondition_digest,
|
|
19169
|
+
project_id: request.project_id,
|
|
19170
|
+
project_slug: request.project_slug,
|
|
19171
|
+
project_name: request.project_name,
|
|
19172
|
+
desired: request.desired,
|
|
19173
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
19174
|
+
});
|
|
19175
|
+
}
|
|
19176
|
+
function acceptedCallMatches(request, accepted, callDigest = normalizedCallDigest(request)) {
|
|
19177
|
+
if (accepted.normalized_call_digest === callDigest)
|
|
19178
|
+
return true;
|
|
19179
|
+
return request.bind_existing !== true && accepted.normalized_call_digest === legacyNormalizedCallDigestBeforeBindExisting(request);
|
|
19180
|
+
}
|
|
18740
19181
|
function assertCommonRequest(request, capability) {
|
|
18741
19182
|
assertBounds(request);
|
|
18742
19183
|
assertResourceKind(request.resource_kind);
|
|
@@ -18778,6 +19219,9 @@ function assertCommonRequest(request, capability) {
|
|
|
18778
19219
|
if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
|
|
18779
19220
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
|
|
18780
19221
|
}
|
|
19222
|
+
if (request.bind_existing !== undefined && typeof request.bind_existing !== "boolean") {
|
|
19223
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "bind_existing must be boolean when supplied");
|
|
19224
|
+
}
|
|
18781
19225
|
const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
|
|
18782
19226
|
operation_id: request.operation_id,
|
|
18783
19227
|
step_id: request.step_id,
|
|
@@ -18799,7 +19243,7 @@ function assertForwardRequest(request, capability) {
|
|
|
18799
19243
|
const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
|
|
18800
19244
|
const expectedPreconditionDigest = digestProjectRegistrationValue({
|
|
18801
19245
|
target_selector: request.target_selector,
|
|
18802
|
-
expected: "absent"
|
|
19246
|
+
expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
|
|
18803
19247
|
});
|
|
18804
19248
|
if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
|
|
18805
19249
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
|
|
@@ -18878,7 +19322,7 @@ function receiptBase(request, callDigest, capability) {
|
|
|
18878
19322
|
normalized_call_digest: callDigest
|
|
18879
19323
|
};
|
|
18880
19324
|
}
|
|
18881
|
-
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt) {
|
|
19325
|
+
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt, createdByOperation = true) {
|
|
18882
19326
|
return makeReceipt({
|
|
18883
19327
|
...receiptBase(request, callDigest, capability),
|
|
18884
19328
|
outcome: "accepted",
|
|
@@ -18888,7 +19332,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt)
|
|
|
18888
19332
|
result_digest: record.digest,
|
|
18889
19333
|
duplicate_of_receipt_id: null,
|
|
18890
19334
|
accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
|
|
18891
|
-
created_by_operation:
|
|
19335
|
+
created_by_operation: createdByOperation
|
|
18892
19336
|
}, createdAt);
|
|
18893
19337
|
}
|
|
18894
19338
|
function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt) {
|
|
@@ -18950,6 +19394,53 @@ function bindingFor(request, callDigest, timestamp2, capability) {
|
|
|
18950
19394
|
updated_at: timestamp2
|
|
18951
19395
|
};
|
|
18952
19396
|
}
|
|
19397
|
+
function encodeProjectResourceCursor(input) {
|
|
19398
|
+
return Buffer.from(JSON.stringify({
|
|
19399
|
+
version: PROJECT_RESOURCE_CURSOR_VERSION,
|
|
19400
|
+
...input
|
|
19401
|
+
}), "utf8").toString("base64url");
|
|
19402
|
+
}
|
|
19403
|
+
function decodeProjectResourceCursor(cursor, expected) {
|
|
19404
|
+
if (!cursor)
|
|
19405
|
+
return null;
|
|
19406
|
+
let parsed;
|
|
19407
|
+
try {
|
|
19408
|
+
parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
19409
|
+
} catch {
|
|
19410
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
|
|
19411
|
+
}
|
|
19412
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
19413
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
|
|
19414
|
+
}
|
|
19415
|
+
const value = parsed;
|
|
19416
|
+
if (value["version"] !== PROJECT_RESOURCE_CURSOR_VERSION || value["source_project_id"] !== expected.source_project_id || value["include_anchors"] !== expected.include_anchors || !Number.isSafeInteger(value["kind_rank"]) || Number(value["kind_rank"]) < 0 || Number(value["kind_rank"]) > 3 || typeof value["target_id"] !== "string" || !UUID_PATTERN.test(value["target_id"]) || typeof value["collection_revision"] !== "string" || value["collection_revision"].length < 16 || value["collection_revision"].length > 128) {
|
|
19417
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor does not match this project-resource query");
|
|
19418
|
+
}
|
|
19419
|
+
return {
|
|
19420
|
+
kind_rank: Number(value["kind_rank"]),
|
|
19421
|
+
target_id: value["target_id"],
|
|
19422
|
+
collection_revision: value["collection_revision"]
|
|
19423
|
+
};
|
|
19424
|
+
}
|
|
19425
|
+
function projectResourceFromCandidate(sourceProjectId, candidate) {
|
|
19426
|
+
const scope = candidate.kind === "project" || candidate.kind === "task_list" ? "collection" : "resource";
|
|
19427
|
+
return {
|
|
19428
|
+
source_project_id: sourceProjectId,
|
|
19429
|
+
kind: candidate.kind,
|
|
19430
|
+
scope,
|
|
19431
|
+
target_id: candidate.target_id,
|
|
19432
|
+
parent_id: candidate.parent_id,
|
|
19433
|
+
revision: candidate.revision,
|
|
19434
|
+
digest: digestProjectRegistrationValue({
|
|
19435
|
+
source_project_id: sourceProjectId,
|
|
19436
|
+
kind: candidate.kind,
|
|
19437
|
+
scope,
|
|
19438
|
+
target_id: candidate.target_id,
|
|
19439
|
+
parent_id: candidate.parent_id,
|
|
19440
|
+
revision: candidate.revision
|
|
19441
|
+
})
|
|
19442
|
+
};
|
|
19443
|
+
}
|
|
18953
19444
|
|
|
18954
19445
|
class PackageOwnedTodosProjectRegistrationAuthority {
|
|
18955
19446
|
backend;
|
|
@@ -18971,6 +19462,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
18971
19462
|
immutable_receipts: true,
|
|
18972
19463
|
exact_terminal_lookup: true,
|
|
18973
19464
|
exact_readback: true,
|
|
19465
|
+
bind_existing_adoption: true,
|
|
19466
|
+
prior_registration_adoption_validation: true,
|
|
19467
|
+
project_resource_enumeration: true,
|
|
19468
|
+
project_resource_page_limit: PROJECT_RESOURCE_PAGE_LIMIT,
|
|
18974
19469
|
conditional_inverse: true,
|
|
18975
19470
|
ambiguous_outcome_reconciliation: true
|
|
18976
19471
|
};
|
|
@@ -19031,7 +19526,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19031
19526
|
if (!accepted2) {
|
|
19032
19527
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
|
|
19033
19528
|
}
|
|
19034
|
-
if (accepted2
|
|
19529
|
+
if (!acceptedCallMatches(request, accepted2, callDigest)) {
|
|
19035
19530
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
|
|
19036
19531
|
}
|
|
19037
19532
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
@@ -19045,7 +19540,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19045
19540
|
});
|
|
19046
19541
|
if (!accepted)
|
|
19047
19542
|
return null;
|
|
19048
|
-
if (accepted
|
|
19543
|
+
if (acceptedCallMatches(request, accepted, callDigest)) {
|
|
19049
19544
|
return this.duplicateFor(transaction, request, callDigest, accepted);
|
|
19050
19545
|
}
|
|
19051
19546
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
@@ -19056,6 +19551,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19056
19551
|
const slug2 = taskListSlug(request.project_slug);
|
|
19057
19552
|
const conflict2 = await transaction.findProjectConflict(path, slug2);
|
|
19058
19553
|
if (conflict2) {
|
|
19554
|
+
if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === slug2) {
|
|
19555
|
+
return {
|
|
19556
|
+
record: boundExistingProjectRecord(conflict2),
|
|
19557
|
+
created_by_operation: false
|
|
19558
|
+
};
|
|
19559
|
+
}
|
|
19059
19560
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
19060
19561
|
}
|
|
19061
19562
|
await this.fault("before_object_write", request);
|
|
@@ -19067,7 +19568,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19067
19568
|
task_prefix: deterministicTaskPrefix(request.project_slug)
|
|
19068
19569
|
});
|
|
19069
19570
|
await this.fault("after_object_write", request);
|
|
19070
|
-
return
|
|
19571
|
+
return {
|
|
19572
|
+
record: projectRecord(project),
|
|
19573
|
+
created_by_operation: true
|
|
19574
|
+
};
|
|
19071
19575
|
}
|
|
19072
19576
|
const todosProjectId = String(request.desired["todos_project_id"]);
|
|
19073
19577
|
const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
|
|
@@ -19081,6 +19585,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19081
19585
|
const slug = taskListSlug(request.project_slug);
|
|
19082
19586
|
const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
|
|
19083
19587
|
if (conflict) {
|
|
19588
|
+
if (request.bind_existing === true && conflict.project_id === todosProjectId && conflict.slug === slug) {
|
|
19589
|
+
return {
|
|
19590
|
+
record: boundExistingTaskListRecord(conflict),
|
|
19591
|
+
created_by_operation: false
|
|
19592
|
+
};
|
|
19593
|
+
}
|
|
19084
19594
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
19085
19595
|
}
|
|
19086
19596
|
await this.fault("before_object_write", request);
|
|
@@ -19097,7 +19607,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19097
19607
|
if (taskList.project_id !== todosProjectId) {
|
|
19098
19608
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
|
|
19099
19609
|
}
|
|
19100
|
-
return
|
|
19610
|
+
return {
|
|
19611
|
+
record: taskListRecord(taskList),
|
|
19612
|
+
created_by_operation: true
|
|
19613
|
+
};
|
|
19101
19614
|
}
|
|
19102
19615
|
async create(request) {
|
|
19103
19616
|
const startedAt = Date.now();
|
|
@@ -19119,9 +19632,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19119
19632
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
|
|
19120
19633
|
if (!claimed) {
|
|
19121
19634
|
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
|
|
19122
|
-
if (binding?.state === "accepted" && binding.
|
|
19635
|
+
if (binding?.state === "accepted" && binding.accepted_receipt_id) {
|
|
19123
19636
|
const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
|
|
19124
|
-
if (accepted2) {
|
|
19637
|
+
if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
|
|
19125
19638
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
19126
19639
|
}
|
|
19127
19640
|
}
|
|
@@ -19132,15 +19645,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19132
19645
|
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
19133
19646
|
return recordOrTerminal;
|
|
19134
19647
|
}
|
|
19135
|
-
const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal, this.now());
|
|
19648
|
+
const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal.record, this.now(), recordOrTerminal.created_by_operation);
|
|
19136
19649
|
await this.fault("before_receipt_write", request);
|
|
19137
19650
|
const stored = await insertDeterministicReceipt(transaction, accepted);
|
|
19138
19651
|
await this.fault("after_receipt_write", request);
|
|
19139
19652
|
await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
|
|
19140
|
-
target_id: recordOrTerminal.target_id,
|
|
19653
|
+
target_id: recordOrTerminal.record.target_id,
|
|
19141
19654
|
accepted_receipt_id: stored.receipt_id,
|
|
19142
|
-
result_revision: recordOrTerminal.revision,
|
|
19143
|
-
result_digest: recordOrTerminal.digest,
|
|
19655
|
+
result_revision: recordOrTerminal.record.revision,
|
|
19656
|
+
result_digest: recordOrTerminal.record.digest,
|
|
19144
19657
|
updated_at: this.now()
|
|
19145
19658
|
});
|
|
19146
19659
|
return stored;
|
|
@@ -19188,7 +19701,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19188
19701
|
direction: request.direction
|
|
19189
19702
|
});
|
|
19190
19703
|
if (accepted) {
|
|
19191
|
-
return accepted
|
|
19704
|
+
return acceptedCallMatches(request, accepted, callDigest) ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
19192
19705
|
}
|
|
19193
19706
|
const timestamp2 = this.now();
|
|
19194
19707
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
|
|
@@ -19268,6 +19781,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19268
19781
|
}
|
|
19269
19782
|
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
19270
19783
|
}
|
|
19784
|
+
async listProjectResources(request) {
|
|
19785
|
+
const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
|
|
19786
|
+
if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
|
|
19787
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
|
|
19788
|
+
}
|
|
19789
|
+
if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
|
|
19790
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
|
|
19791
|
+
}
|
|
19792
|
+
const includeAnchors = request.include_anchors === true;
|
|
19793
|
+
const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
|
|
19794
|
+
if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
|
|
19795
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted Todos project binding exists for this exact Projects workspace id", { source_project_id: sourceProjectId });
|
|
19796
|
+
}
|
|
19797
|
+
const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
|
|
19798
|
+
if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
|
|
19799
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
|
|
19800
|
+
source_project_id: sourceProjectId,
|
|
19801
|
+
todos_project_id: projectBinding.target_id
|
|
19802
|
+
});
|
|
19803
|
+
}
|
|
19804
|
+
const cursor = decodeProjectResourceCursor(request.cursor, {
|
|
19805
|
+
source_project_id: sourceProjectId,
|
|
19806
|
+
include_anchors: includeAnchors
|
|
19807
|
+
});
|
|
19808
|
+
const collectionInput = {
|
|
19809
|
+
todos_project_id: projectBinding.target_id,
|
|
19810
|
+
task_list_id: taskListBinding.target_id,
|
|
19811
|
+
include_anchors: includeAnchors
|
|
19812
|
+
};
|
|
19813
|
+
const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
19814
|
+
if (cursor && cursor.collection_revision !== collectionRevision) {
|
|
19815
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
|
|
19816
|
+
source_project_id: sourceProjectId,
|
|
19817
|
+
expected_collection_revision: cursor.collection_revision,
|
|
19818
|
+
current_collection_revision: collectionRevision
|
|
19819
|
+
});
|
|
19820
|
+
}
|
|
19821
|
+
const candidates = await this.backend.listProjectResourceCandidates({
|
|
19822
|
+
...collectionInput,
|
|
19823
|
+
after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
|
|
19824
|
+
limit: request.limit + 1
|
|
19825
|
+
});
|
|
19826
|
+
const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
|
|
19827
|
+
if (verifiedCollectionRevision !== collectionRevision) {
|
|
19828
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
|
|
19829
|
+
source_project_id: sourceProjectId,
|
|
19830
|
+
expected_collection_revision: collectionRevision,
|
|
19831
|
+
current_collection_revision: verifiedCollectionRevision
|
|
19832
|
+
});
|
|
19833
|
+
}
|
|
19834
|
+
const hasMore = candidates.length > request.limit;
|
|
19835
|
+
const pageCandidates = candidates.slice(0, request.limit);
|
|
19836
|
+
const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
|
|
19837
|
+
const last = pageCandidates.at(-1);
|
|
19838
|
+
return {
|
|
19839
|
+
authority: "todos",
|
|
19840
|
+
route: this.capabilityValue.route,
|
|
19841
|
+
package_version: this.capabilityValue.package_version,
|
|
19842
|
+
authority_id: this.capabilityValue.authority_id,
|
|
19843
|
+
tenant_id: this.capabilityValue.tenant_id,
|
|
19844
|
+
corpus_id: this.capabilityValue.corpus_id,
|
|
19845
|
+
source_project_id: sourceProjectId,
|
|
19846
|
+
todos_project_id: projectBinding.target_id,
|
|
19847
|
+
task_list_id: taskListBinding.target_id,
|
|
19848
|
+
include_anchors: includeAnchors,
|
|
19849
|
+
collection_revision: collectionRevision,
|
|
19850
|
+
limit: request.limit,
|
|
19851
|
+
count: resources.length,
|
|
19852
|
+
resources,
|
|
19853
|
+
has_more: hasMore,
|
|
19854
|
+
next_cursor: hasMore && last ? encodeProjectResourceCursor({
|
|
19855
|
+
source_project_id: sourceProjectId,
|
|
19856
|
+
include_anchors: includeAnchors,
|
|
19857
|
+
collection_revision: collectionRevision,
|
|
19858
|
+
kind_rank: last.kind_rank,
|
|
19859
|
+
target_id: last.target_id
|
|
19860
|
+
}) : null,
|
|
19861
|
+
complete: !hasMore,
|
|
19862
|
+
truncated: false
|
|
19863
|
+
};
|
|
19864
|
+
}
|
|
19865
|
+
async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
|
|
19866
|
+
const startedAt = Date.now();
|
|
19867
|
+
if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
|
|
19868
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
|
|
19869
|
+
}
|
|
19870
|
+
requireString(sourceRequest.package_version, "package_version", {
|
|
19871
|
+
max: 128,
|
|
19872
|
+
pattern: PACKAGE_VERSION_PATTERN
|
|
19873
|
+
});
|
|
19874
|
+
assertForwardRequest(sourceRequest, {
|
|
19875
|
+
...this.capabilityValue,
|
|
19876
|
+
package_version: sourceRequest.package_version
|
|
19877
|
+
});
|
|
19878
|
+
const validation = await this.backend.transaction(async (transaction) => {
|
|
19879
|
+
const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
|
|
19880
|
+
if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
|
|
19881
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
19882
|
+
}
|
|
19883
|
+
const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
|
|
19884
|
+
if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
|
|
19885
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
|
|
19886
|
+
}
|
|
19887
|
+
const receiptLineageMatches = (receipt) => receipt.authority === "todos" && receipt.route === sourceRequest.authority_route && receipt.package_version === sourceRequest.package_version && receipt.authority_id === sourceRequest.authority_id && receipt.tenant_id === sourceRequest.tenant_id && receipt.corpus_id === sourceRequest.corpus_id && receipt.operation_id === sourceRequest.operation_id && receipt.step_id === sourceRequest.step_id && receipt.resource_kind === sourceRequest.resource_kind && receipt.direction === "forward" && receipt.target_selector === sourceRequest.target_selector && receipt.idempotency_key === sourceRequest.idempotency_key && receipt.request_digest === sourceRequest.request_digest && receipt.precondition_digest === sourceRequest.precondition_digest && acceptedCallMatches(sourceRequest, receipt);
|
|
19888
|
+
if (!receiptLineageMatches(storedSource) || !receiptLineageMatches(accepted) || storedSource.outcome === "duplicate_of_accepted" && (storedSource.duplicate_of_receipt_id !== accepted.receipt_id || storedSource.target_id !== accepted.target_id || storedSource.result_revision !== accepted.result_revision || storedSource.result_digest !== accepted.result_digest)) {
|
|
19889
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
|
|
19890
|
+
}
|
|
19891
|
+
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), sourceRequest.resource_kind, sourceRequest.target_selector);
|
|
19892
|
+
if (!binding || binding.state !== "accepted" || binding.operation_id !== sourceRequest.operation_id || binding.step_id !== sourceRequest.step_id || binding.direction !== "forward" || binding.idempotency_key !== sourceRequest.idempotency_key || binding.request_digest !== sourceRequest.request_digest || binding.precondition_digest !== sourceRequest.precondition_digest || binding.normalized_call_digest !== accepted.normalized_call_digest || binding.target_id !== accepted.target_id || binding.accepted_receipt_id !== accepted.receipt_id || binding.result_revision !== accepted.result_revision || binding.result_digest !== accepted.result_digest) {
|
|
19893
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
|
|
19894
|
+
}
|
|
19895
|
+
const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
|
|
19896
|
+
if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
|
|
19897
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
|
|
19898
|
+
}
|
|
19899
|
+
let stableMatch = false;
|
|
19900
|
+
if (sourceRequest.resource_kind === "task_list") {
|
|
19901
|
+
stableMatch = taskListRegistrationDigest({
|
|
19902
|
+
...current,
|
|
19903
|
+
updated_at: accepted.result_revision
|
|
19904
|
+
}) === accepted.result_digest;
|
|
19905
|
+
} else {
|
|
19906
|
+
const project = current;
|
|
19907
|
+
if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
|
|
19908
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
|
|
19909
|
+
}
|
|
19910
|
+
for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
|
|
19911
|
+
if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
|
|
19912
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
|
|
19913
|
+
}
|
|
19914
|
+
if (projectRegistrationDigest({
|
|
19915
|
+
...project,
|
|
19916
|
+
task_counter: priorTaskCounter,
|
|
19917
|
+
updated_at: accepted.result_revision
|
|
19918
|
+
}) === accepted.result_digest) {
|
|
19919
|
+
stableMatch = true;
|
|
19920
|
+
break;
|
|
19921
|
+
}
|
|
19922
|
+
}
|
|
19923
|
+
}
|
|
19924
|
+
if (!stableMatch) {
|
|
19925
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
|
|
19926
|
+
}
|
|
19927
|
+
return {
|
|
19928
|
+
valid: true,
|
|
19929
|
+
resource_kind: sourceRequest.resource_kind,
|
|
19930
|
+
target_id: accepted.target_id,
|
|
19931
|
+
source_receipt_id: storedSource.receipt_id,
|
|
19932
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
19933
|
+
source_outcome: storedSource.outcome,
|
|
19934
|
+
created_at: current.created_at,
|
|
19935
|
+
current_revision: current.updated_at,
|
|
19936
|
+
accepted_result_digest: accepted.result_digest
|
|
19937
|
+
};
|
|
19938
|
+
});
|
|
19939
|
+
assertWithinBounds(validation, sourceRequest, startedAt);
|
|
19940
|
+
return validation;
|
|
19941
|
+
}
|
|
19271
19942
|
async storedAcceptedReceipt(request, supplied) {
|
|
19272
19943
|
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
19273
19944
|
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
@@ -19439,6 +20110,65 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
|
19439
20110
|
cursorTableName
|
|
19440
20111
|
}), authorityOptions);
|
|
19441
20112
|
}
|
|
20113
|
+
// src/project-registration/adoption-validation.ts
|
|
20114
|
+
var VALIDATION_KEYS = [
|
|
20115
|
+
"valid",
|
|
20116
|
+
"resource_kind",
|
|
20117
|
+
"target_id",
|
|
20118
|
+
"source_receipt_id",
|
|
20119
|
+
"accepted_receipt_id",
|
|
20120
|
+
"source_outcome",
|
|
20121
|
+
"created_at",
|
|
20122
|
+
"current_revision",
|
|
20123
|
+
"accepted_result_digest"
|
|
20124
|
+
];
|
|
20125
|
+
function isRecord(value) {
|
|
20126
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
20127
|
+
}
|
|
20128
|
+
function isNonEmptyString(value) {
|
|
20129
|
+
return typeof value === "string" && value.length > 0;
|
|
20130
|
+
}
|
|
20131
|
+
function hasExactKeys(value, expected) {
|
|
20132
|
+
const actual = Object.keys(value).sort();
|
|
20133
|
+
const wanted = [...expected].sort();
|
|
20134
|
+
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
|
|
20135
|
+
}
|
|
20136
|
+
function adoptionRejected(message) {
|
|
20137
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", `TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED: ${message}`);
|
|
20138
|
+
}
|
|
20139
|
+
function assertTodosPriorRegistrationAdoptionValidationEnvelope(value, input) {
|
|
20140
|
+
if (!isRecord(input) || !hasExactKeys(input, [
|
|
20141
|
+
"source_request",
|
|
20142
|
+
"source_receipt",
|
|
20143
|
+
"current_record"
|
|
20144
|
+
])) {
|
|
20145
|
+
adoptionRejected("prior-adoption validation input is incomplete");
|
|
20146
|
+
}
|
|
20147
|
+
const request = input["source_request"];
|
|
20148
|
+
const receipt = input["source_receipt"];
|
|
20149
|
+
const current = input["current_record"];
|
|
20150
|
+
if (!isRecord(request) || !isRecord(receipt) || !isRecord(current)) {
|
|
20151
|
+
adoptionRejected("prior-adoption validation input records are incomplete");
|
|
20152
|
+
}
|
|
20153
|
+
const resourceKind = request["resource_kind"];
|
|
20154
|
+
const sourceOutcome = receipt["outcome"];
|
|
20155
|
+
const acceptedReceiptId = sourceOutcome === "accepted" ? receipt["receipt_id"] : sourceOutcome === "duplicate_of_accepted" ? receipt["duplicate_of_receipt_id"] : null;
|
|
20156
|
+
if (resourceKind !== "project" && resourceKind !== "task_list" || request["direction"] !== "forward" || sourceOutcome !== "accepted" && sourceOutcome !== "duplicate_of_accepted" || !isNonEmptyString(acceptedReceiptId) || !isNonEmptyString(receipt["receipt_id"]) || !isNonEmptyString(receipt["target_id"]) || !isNonEmptyString(receipt["result_revision"]) || !isNonEmptyString(receipt["result_digest"]) || !isNonEmptyString(current["id"]) || !isNonEmptyString(current["created_at"]) || !isNonEmptyString(current["updated_at"]) || receipt["authority"] !== "todos" || receipt["route"] !== request["authority_route"] || receipt["package_version"] !== request["package_version"] || receipt["authority_id"] !== request["authority_id"] || receipt["tenant_id"] !== request["tenant_id"] || receipt["corpus_id"] !== request["corpus_id"] || receipt["operation_id"] !== request["operation_id"] || receipt["step_id"] !== request["step_id"] || receipt["resource_kind"] !== resourceKind || receipt["direction"] !== "forward" || receipt["idempotency_key"] !== request["idempotency_key"] || receipt["request_digest"] !== request["request_digest"] || receipt["precondition_digest"] !== request["precondition_digest"] || receipt["accepted_receipt_id"] !== null || receipt["target_id"] !== current["id"] || receipt["result_revision"] !== current["created_at"]) {
|
|
20157
|
+
adoptionRejected("prior-adoption validation input does not carry one complete accepted receipt and current target incarnation");
|
|
20158
|
+
}
|
|
20159
|
+
if (sourceOutcome === "accepted" && receipt["duplicate_of_receipt_id"] !== null || sourceOutcome === "duplicate_of_accepted" && receipt["duplicate_of_receipt_id"] !== acceptedReceiptId) {
|
|
20160
|
+
adoptionRejected("prior-adoption validation source receipt lineage is incomplete");
|
|
20161
|
+
}
|
|
20162
|
+
if (!isRecord(value) || !hasExactKeys(value, ["validation"]) || !isRecord(value["validation"]) || !hasExactKeys(value["validation"], VALIDATION_KEYS)) {
|
|
20163
|
+
adoptionRejected("prior-adoption validation response envelope is incomplete");
|
|
20164
|
+
}
|
|
20165
|
+
const validation = value["validation"];
|
|
20166
|
+
if (validation["valid"] !== true || validation["resource_kind"] !== resourceKind || validation["target_id"] !== current["id"] || validation["source_receipt_id"] !== receipt["receipt_id"] || validation["accepted_receipt_id"] !== acceptedReceiptId || validation["source_outcome"] !== sourceOutcome || validation["created_at"] !== current["created_at"] || validation["current_revision"] !== current["updated_at"] || validation["accepted_result_digest"] !== receipt["result_digest"]) {
|
|
20167
|
+
adoptionRejected("prior-adoption validation response does not prove the exact accepted receipt and current target");
|
|
20168
|
+
}
|
|
20169
|
+
return validation;
|
|
20170
|
+
}
|
|
20171
|
+
|
|
19442
20172
|
// src/project-registration/http.ts
|
|
19443
20173
|
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
19444
20174
|
function json(body, status = 200) {
|
|
@@ -19485,6 +20215,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
19485
20215
|
if ((action === "" || action === "capability") && method === "GET") {
|
|
19486
20216
|
return json({ capability: await authority.capability() });
|
|
19487
20217
|
}
|
|
20218
|
+
if (action === "resources" && method === "GET") {
|
|
20219
|
+
const sourceProjectId = url.searchParams.get("source_project_id");
|
|
20220
|
+
const limit = Number(url.searchParams.get("limit") ?? "100");
|
|
20221
|
+
const includeAnchorsRaw = url.searchParams.get("include_anchors");
|
|
20222
|
+
const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
|
|
20223
|
+
return json({
|
|
20224
|
+
page: await authority.listProjectResources({
|
|
20225
|
+
source_project_id: sourceProjectId,
|
|
20226
|
+
limit,
|
|
20227
|
+
include_anchors: includeAnchors,
|
|
20228
|
+
cursor: url.searchParams.get("cursor") ?? undefined
|
|
20229
|
+
})
|
|
20230
|
+
});
|
|
20231
|
+
}
|
|
19488
20232
|
if (method !== "POST")
|
|
19489
20233
|
return json({ error: "method not allowed" }, 405);
|
|
19490
20234
|
const body = await readJson(req);
|
|
@@ -19507,6 +20251,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
19507
20251
|
record: await authority.readExact(body)
|
|
19508
20252
|
});
|
|
19509
20253
|
}
|
|
20254
|
+
if (action === "validate-prior-adoption") {
|
|
20255
|
+
const input = body;
|
|
20256
|
+
return json({
|
|
20257
|
+
validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
|
|
20258
|
+
});
|
|
20259
|
+
}
|
|
19510
20260
|
if (action === "compensate") {
|
|
19511
20261
|
return json({
|
|
19512
20262
|
receipt: await authority.compensate(body)
|
|
@@ -19581,6 +20331,28 @@ class TodosProjectRegistrationHttpClient {
|
|
|
19581
20331
|
async lookupReceipt(request) {
|
|
19582
20332
|
return this.request("/receipts/lookup", { method: "POST", body: JSON.stringify(request) });
|
|
19583
20333
|
}
|
|
20334
|
+
async listProjectResources(request) {
|
|
20335
|
+
const query = new URLSearchParams({
|
|
20336
|
+
source_project_id: request.source_project_id,
|
|
20337
|
+
limit: String(request.limit),
|
|
20338
|
+
include_anchors: String(request.include_anchors === true),
|
|
20339
|
+
...request.cursor ? { cursor: request.cursor } : {}
|
|
20340
|
+
});
|
|
20341
|
+
const body = await this.request(`/resources?${query.toString()}`);
|
|
20342
|
+
return body.page;
|
|
20343
|
+
}
|
|
20344
|
+
async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
|
|
20345
|
+
const input = {
|
|
20346
|
+
source_request: withoutTarget(sourceRequest),
|
|
20347
|
+
source_receipt: sourceReceipt,
|
|
20348
|
+
current_record: currentRecord
|
|
20349
|
+
};
|
|
20350
|
+
const body = await this.request("/validate-prior-adoption", {
|
|
20351
|
+
method: "POST",
|
|
20352
|
+
body: JSON.stringify(input)
|
|
20353
|
+
});
|
|
20354
|
+
return assertTodosPriorRegistrationAdoptionValidationEnvelope(body, input);
|
|
20355
|
+
}
|
|
19584
20356
|
async compensate(request) {
|
|
19585
20357
|
const body = await this.request("/compensate", { method: "POST", body: JSON.stringify(withoutTarget(request)) });
|
|
19586
20358
|
return body.receipt;
|
|
@@ -19606,6 +20378,7 @@ export {
|
|
|
19606
20378
|
createPostgresTodosProjectRegistrationAuthority,
|
|
19607
20379
|
createLocalTodosProjectRegistrationAuthority,
|
|
19608
20380
|
canonicalProjectRegistrationJson,
|
|
20381
|
+
assertTodosPriorRegistrationAdoptionValidationEnvelope,
|
|
19609
20382
|
TodosProjectRegistrationHttpClient,
|
|
19610
20383
|
TodosProjectRegistrationError,
|
|
19611
20384
|
TODOS_PROJECT_REGISTRATION_SCHEMA_VERSION,
|