@hasna/todos 0.15.28 → 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 +15 -0
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-registration-commands.d.ts +5 -0
- package/dist/cli/commands/project-registration-commands.d.ts.map +1 -0
- 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 +13040 -10368
- package/dist/contracts.js +64 -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 +1489 -149
- 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 +2073 -140
- 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 +26 -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/schema.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 +873 -63
- package/dist/registry.js +64 -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 +2064 -131
- 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 +244 -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 `
|
|
@@ -1053,6 +1093,12 @@ function sqliteTodosProjectRegistrationSchemaSql() {
|
|
|
1053
1093
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
1054
1094
|
resource_kind, direction, idempotency_key
|
|
1055
1095
|
);
|
|
1096
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_source_identity
|
|
1097
|
+
ON todos_project_registration_receipts (
|
|
1098
|
+
authority_id, tenant_id, corpus_id, route, package_version,
|
|
1099
|
+
operation_id, step_id, resource_kind, direction, idempotency_key,
|
|
1100
|
+
target_selector
|
|
1101
|
+
);
|
|
1056
1102
|
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
|
|
1057
1103
|
ON todos_project_registration_receipts (
|
|
1058
1104
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
@@ -1152,6 +1198,12 @@ function postgresTodosProjectRegistrationSchemaSql() {
|
|
|
1152
1198
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
1153
1199
|
resource_kind, direction, idempotency_key
|
|
1154
1200
|
)`,
|
|
1201
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_source_identity_idx
|
|
1202
|
+
ON todos_project_registration_receipts (
|
|
1203
|
+
authority_id, tenant_id, corpus_id, route, package_version,
|
|
1204
|
+
operation_id, step_id, resource_kind, direction, idempotency_key,
|
|
1205
|
+
target_selector
|
|
1206
|
+
)`,
|
|
1155
1207
|
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
|
|
1156
1208
|
ON todos_project_registration_receipts (
|
|
1157
1209
|
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
@@ -9428,6 +9480,7 @@ function createTaskStored(input, d) {
|
|
|
9428
9480
|
let id = uuid();
|
|
9429
9481
|
for (let attempt = 0;attempt < 3; attempt++) {
|
|
9430
9482
|
try {
|
|
9483
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
9431
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)
|
|
9432
9485
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
9433
9486
|
id,
|
|
@@ -9806,6 +9859,7 @@ function updateTaskStored(id, input, db) {
|
|
|
9806
9859
|
throw new VersionConflictError(id, input.version, task.version);
|
|
9807
9860
|
}
|
|
9808
9861
|
input = sanitizeUpdateTaskInput(input);
|
|
9862
|
+
assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
|
|
9809
9863
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
|
|
9810
9864
|
const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
|
|
9811
9865
|
if (linkedProjectId) {
|
|
@@ -9859,6 +9913,10 @@ function updateTaskStored(id, input, db) {
|
|
|
9859
9913
|
sets.push("project_id = ?");
|
|
9860
9914
|
params.push(input.project_id);
|
|
9861
9915
|
}
|
|
9916
|
+
if (input.parent_id !== undefined) {
|
|
9917
|
+
sets.push("parent_id = ?");
|
|
9918
|
+
params.push(input.parent_id);
|
|
9919
|
+
}
|
|
9862
9920
|
if (input.assigned_to !== undefined) {
|
|
9863
9921
|
sets.push("assigned_to = ?");
|
|
9864
9922
|
params.push(input.assigned_to);
|
|
@@ -9974,6 +10032,8 @@ function updateTaskStored(id, input, db) {
|
|
|
9974
10032
|
logTaskChange2(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
9975
10033
|
if (input.title !== undefined && input.title !== task.title)
|
|
9976
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);
|
|
9977
10037
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
9978
10038
|
logTaskChange2(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
9979
10039
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
@@ -10031,7 +10091,8 @@ function updateTask2(id, input, db) {
|
|
|
10031
10091
|
if (!before)
|
|
10032
10092
|
throw new TaskNotFoundError(id);
|
|
10033
10093
|
const guardedPlanIds = [before.plan_id, input.plan_id];
|
|
10034
|
-
|
|
10094
|
+
const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
|
|
10095
|
+
if (!needsSerializedWrite)
|
|
10035
10096
|
return updateTaskStored(id, input, d);
|
|
10036
10097
|
return d.transaction(() => {
|
|
10037
10098
|
guardPlanRowsSqlite(guardedPlanIds, d);
|
|
@@ -10067,6 +10128,7 @@ var init_task_crud = __esm(() => {
|
|
|
10067
10128
|
init_checklists();
|
|
10068
10129
|
init_storage_tombstones();
|
|
10069
10130
|
init_prewrite_secrets();
|
|
10131
|
+
init_task_parent_integrity();
|
|
10070
10132
|
});
|
|
10071
10133
|
|
|
10072
10134
|
// src/db/task-status.ts
|
|
@@ -12657,11 +12719,11 @@ var init_tasks = __esm(() => {
|
|
|
12657
12719
|
});
|
|
12658
12720
|
|
|
12659
12721
|
// src/project-registration/authority.ts
|
|
12660
|
-
import { createHash as
|
|
12722
|
+
import { createHash as createHash7 } from "crypto";
|
|
12661
12723
|
// package.json
|
|
12662
12724
|
var package_default = {
|
|
12663
12725
|
name: "@hasna/todos",
|
|
12664
|
-
version: "0.15.
|
|
12726
|
+
version: "0.15.32",
|
|
12665
12727
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12666
12728
|
type: "module",
|
|
12667
12729
|
main: "dist/index.js",
|
|
@@ -13514,6 +13576,7 @@ function measureIntegrityRows(spec, sets) {
|
|
|
13514
13576
|
|
|
13515
13577
|
// src/storage/postgres-adapter.ts
|
|
13516
13578
|
init_redaction();
|
|
13579
|
+
init_task_parent_integrity();
|
|
13517
13580
|
|
|
13518
13581
|
// src/storage/audit-history-import.ts
|
|
13519
13582
|
var AUDIT_HISTORY_DIVERGENT_REPLAY = "AUDIT_HISTORY_DIVERGENT_REPLAY";
|
|
@@ -13585,8 +13648,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
13585
13648
|
resolveRef: (ref) => store.resolveTaskRef(ref),
|
|
13586
13649
|
list: (filter = {}) => store.listTasks(filter),
|
|
13587
13650
|
count: (filter = {}) => store.countTasks(filter),
|
|
13588
|
-
update: (id, input) => updateTask(id, input, store),
|
|
13589
|
-
delete: (id, context) => store.
|
|
13651
|
+
update: (id, input, context) => updateTask(id, input, store, context),
|
|
13652
|
+
delete: (id, context) => store.deleteTaskHierarchy(id, context),
|
|
13590
13653
|
start: (id, agentId) => startTask(id, agentId, store),
|
|
13591
13654
|
complete: (id, agentId, options2) => completeTask(id, agentId, options2, store),
|
|
13592
13655
|
fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, store),
|
|
@@ -14119,22 +14182,70 @@ class PostgresJsonRecordStore {
|
|
|
14119
14182
|
return "identical";
|
|
14120
14183
|
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
14121
14184
|
}
|
|
14122
|
-
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) {
|
|
14123
14195
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
14124
|
-
if (planIds.length === 0)
|
|
14196
|
+
if (planIds.length === 0 && !parentGuard)
|
|
14125
14197
|
return this.upsert("tasks", value, context);
|
|
14126
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;
|
|
14127
14203
|
const updatedAt = value.updated_at;
|
|
14128
14204
|
const targetPlanId = value.plan_id;
|
|
14129
|
-
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
|
+
),
|
|
14130
14211
|
locked_plans AS MATERIALIZED (
|
|
14131
14212
|
SELECT object_id, payload FROM ${this.tableName}
|
|
14132
14213
|
WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
|
|
14133
14214
|
AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
|
|
14134
14215
|
ORDER BY object_id
|
|
14135
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
|
|
14136
14238
|
), validation AS (
|
|
14137
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,
|
|
14138
14249
|
(SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
|
|
14139
14250
|
($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
|
|
14140
14251
|
(SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
|
|
@@ -14155,7 +14266,13 @@ class PostgresJsonRecordStore {
|
|
|
14155
14266
|
)
|
|
14156
14267
|
SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
|
|
14157
14268
|
FROM guarded
|
|
14158
|
-
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
|
|
14159
14276
|
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
14160
14277
|
payload = EXCLUDED.payload,
|
|
14161
14278
|
updated_at = EXCLUDED.updated_at,
|
|
@@ -14168,8 +14285,10 @@ class PostgresJsonRecordStore {
|
|
|
14168
14285
|
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
14169
14286
|
RETURNING payload
|
|
14170
14287
|
)
|
|
14171
|
-
SELECT guarded.
|
|
14172
|
-
|
|
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
|
|
14173
14292
|
FROM guarded`, [
|
|
14174
14293
|
this.service,
|
|
14175
14294
|
value.id,
|
|
@@ -14179,9 +14298,26 @@ class PostgresJsonRecordStore {
|
|
|
14179
14298
|
numberValue2(value.version),
|
|
14180
14299
|
jsonbParam(planIds),
|
|
14181
14300
|
targetPlanId,
|
|
14182
|
-
explicitProject
|
|
14301
|
+
explicitProject,
|
|
14302
|
+
Boolean(parentGuard),
|
|
14303
|
+
parentGuard?.parentId ?? null,
|
|
14304
|
+
parentGuard?.expectedVersion ?? null,
|
|
14305
|
+
parentGuard?.operation === "update"
|
|
14183
14306
|
]);
|
|
14184
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
|
+
}
|
|
14185
14321
|
if (!row?.all_plans_found || !row.target_plan_found) {
|
|
14186
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 });
|
|
14187
14323
|
}
|
|
@@ -14189,6 +14325,8 @@ class PostgresJsonRecordStore {
|
|
|
14189
14325
|
throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
|
|
14190
14326
|
}
|
|
14191
14327
|
if (!row.payload) {
|
|
14328
|
+
if (row.current_payload)
|
|
14329
|
+
return payloadRecord2(row.current_payload);
|
|
14192
14330
|
return await requireRecord("tasks", value.id, this);
|
|
14193
14331
|
}
|
|
14194
14332
|
return payloadRecord2(row.payload);
|
|
@@ -14514,6 +14652,80 @@ class PostgresJsonRecordStore {
|
|
|
14514
14652
|
version: numberValue2(existing["version"])
|
|
14515
14653
|
}, context);
|
|
14516
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
|
+
}
|
|
14517
14729
|
async getPlanProjectLinkReceipt(receiptId) {
|
|
14518
14730
|
const value = await this.get("plan_project_link_receipts", receiptId);
|
|
14519
14731
|
return value ? assertPlanProjectLinkReceipt(value) : null;
|
|
@@ -14934,9 +15146,8 @@ class PostgresJsonRecordStore {
|
|
|
14934
15146
|
}
|
|
14935
15147
|
async function createTask(input, store, context) {
|
|
14936
15148
|
const timestamp = new Date().toISOString();
|
|
14937
|
-
|
|
14938
|
-
|
|
14939
|
-
}
|
|
15149
|
+
const taskId = randomUUID();
|
|
15150
|
+
await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
|
|
14940
15151
|
const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
|
|
14941
15152
|
const requestedProjectId = input.project_id ?? context?.projectId ?? null;
|
|
14942
15153
|
if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
|
|
@@ -14945,7 +15156,7 @@ async function createTask(input, store, context) {
|
|
|
14945
15156
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
14946
15157
|
const shortId = effectiveProjectId ? await nextTaskShortId(effectiveProjectId, store, context) : null;
|
|
14947
15158
|
const task = {
|
|
14948
|
-
id:
|
|
15159
|
+
id: taskId,
|
|
14949
15160
|
short_id: shortId,
|
|
14950
15161
|
project_id: effectiveProjectId,
|
|
14951
15162
|
parent_id: input.parent_id ?? null,
|
|
@@ -15001,15 +15212,16 @@ async function createTask(input, store, context) {
|
|
|
15001
15212
|
synced_at: null,
|
|
15002
15213
|
archived_at: null
|
|
15003
15214
|
};
|
|
15004
|
-
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);
|
|
15005
15216
|
await logTaskChange(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
|
|
15006
15217
|
return storedTask;
|
|
15007
15218
|
}
|
|
15008
|
-
async function updateTask(id, input, store) {
|
|
15219
|
+
async function updateTask(id, input, store, context) {
|
|
15009
15220
|
const existing = await requireRecord("tasks", id, store);
|
|
15010
15221
|
if (existing.version !== input.version) {
|
|
15011
|
-
throw new
|
|
15222
|
+
throw new VersionConflictError(id, input.version, existing.version);
|
|
15012
15223
|
}
|
|
15224
|
+
await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
|
|
15013
15225
|
const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
|
|
15014
15226
|
const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
|
|
15015
15227
|
if (linkedPlan?.project_id) {
|
|
@@ -15034,10 +15246,19 @@ async function updateTask(id, input, store) {
|
|
|
15034
15246
|
metadata: input.metadata ?? existing.metadata,
|
|
15035
15247
|
requires_approval: input.requires_approval ?? existing.requires_approval,
|
|
15036
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,
|
|
15037
15250
|
created_by: existing.created_by,
|
|
15038
15251
|
completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
|
|
15039
15252
|
};
|
|
15040
|
-
|
|
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;
|
|
15041
15262
|
}
|
|
15042
15263
|
async function startTask(id, agentId, store) {
|
|
15043
15264
|
const task = await requireRecord("tasks", id, store);
|
|
@@ -15101,7 +15322,11 @@ async function patchTask(task, patch, store) {
|
|
|
15101
15322
|
version: task.version + 1,
|
|
15102
15323
|
updated_at: new Date().toISOString()
|
|
15103
15324
|
};
|
|
15104
|
-
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
|
+
});
|
|
15105
15330
|
}
|
|
15106
15331
|
var CLOUD_LOCK_EXPIRY_MINUTES = 30;
|
|
15107
15332
|
function sameCloudLockHolder(stored, incoming) {
|
|
@@ -15877,8 +16102,9 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
15877
16102
|
const result = await this.client.query(`
|
|
15878
16103
|
SELECT * FROM todos_project_registration_receipts
|
|
15879
16104
|
WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
|
|
15880
|
-
AND
|
|
15881
|
-
AND
|
|
16105
|
+
AND route = $4 AND package_version = $5
|
|
16106
|
+
AND operation_id = $6 AND step_id = $7 AND resource_kind = $8
|
|
16107
|
+
AND direction = $9 AND idempotency_key = $10 AND target_selector = $11
|
|
15882
16108
|
ORDER BY CASE outcome
|
|
15883
16109
|
WHEN 'terminal_nonacceptance' THEN 0
|
|
15884
16110
|
WHEN 'duplicate_of_accepted' THEN 1
|
|
@@ -15889,6 +16115,8 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
15889
16115
|
identity.authority_id,
|
|
15890
16116
|
identity.tenant_id,
|
|
15891
16117
|
identity.corpus_id,
|
|
16118
|
+
identity.route,
|
|
16119
|
+
identity.package_version,
|
|
15892
16120
|
identity.operation_id,
|
|
15893
16121
|
identity.step_id,
|
|
15894
16122
|
identity.resource_kind,
|
|
@@ -16088,6 +16316,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
16088
16316
|
AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
|
|
16089
16317
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
16090
16318
|
LIMIT 1
|
|
16319
|
+
FOR UPDATE
|
|
16091
16320
|
`, [this.service, path, taskListSlug]);
|
|
16092
16321
|
return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
|
|
16093
16322
|
}
|
|
@@ -16098,6 +16327,7 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
16098
16327
|
AND payload->>'project_id' = $2 AND payload->>'slug' = $3
|
|
16099
16328
|
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
16100
16329
|
LIMIT 1
|
|
16330
|
+
FOR UPDATE
|
|
16101
16331
|
`, [this.service, projectId, slug]);
|
|
16102
16332
|
return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
|
|
16103
16333
|
}
|
|
@@ -16108,10 +16338,24 @@ class PostgresTodosProjectRegistrationTransaction {
|
|
|
16108
16338
|
return await this.storage.taskLists.create(input);
|
|
16109
16339
|
}
|
|
16110
16340
|
async getProject(id) {
|
|
16111
|
-
|
|
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;
|
|
16112
16349
|
}
|
|
16113
16350
|
async getTaskList(id) {
|
|
16114
|
-
|
|
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;
|
|
16115
16359
|
}
|
|
16116
16360
|
async lockCompensationWrites() {
|
|
16117
16361
|
await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
|
|
@@ -16191,11 +16435,102 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
16191
16435
|
async getTaskList(id) {
|
|
16192
16436
|
return (await this.direct()).getTaskList(id);
|
|
16193
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
|
+
}
|
|
16194
16528
|
}
|
|
16195
16529
|
|
|
16196
16530
|
// src/project-registration/sqlite.ts
|
|
16197
16531
|
init_database();
|
|
16198
16532
|
init_storage_tombstones();
|
|
16533
|
+
import { createHash as createHash6 } from "crypto";
|
|
16199
16534
|
|
|
16200
16535
|
// src/storage/local-sqlite.ts
|
|
16201
16536
|
init_types();
|
|
@@ -17950,6 +18285,7 @@ class SqliteTodosProjectRegistrationTransaction {
|
|
|
17950
18285
|
const row = this.db.query(`
|
|
17951
18286
|
SELECT * FROM todos_project_registration_receipts
|
|
17952
18287
|
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
18288
|
+
AND route = ? AND package_version = ?
|
|
17953
18289
|
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
17954
18290
|
AND direction = ? AND idempotency_key = ? AND target_selector = ?
|
|
17955
18291
|
ORDER BY CASE outcome
|
|
@@ -17958,7 +18294,7 @@ class SqliteTodosProjectRegistrationTransaction {
|
|
|
17958
18294
|
ELSE 2
|
|
17959
18295
|
END, created_at DESC, receipt_id DESC
|
|
17960
18296
|
LIMIT 1
|
|
17961
|
-
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
|
|
18297
|
+
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.route, identity.package_version, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
|
|
17962
18298
|
return row ? receiptFromRow2(row) : null;
|
|
17963
18299
|
}
|
|
17964
18300
|
async getReceiptById(receiptId) {
|
|
@@ -18112,7 +18448,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
|
|
|
18112
18448
|
}
|
|
18113
18449
|
async lockStep(_identity) {}
|
|
18114
18450
|
async getReceiptForLookup(identity) {
|
|
18115
|
-
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
|
|
18451
|
+
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.route === identity.route && receipt.package_version === identity.package_version && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
|
|
18116
18452
|
const stored = await this.direct.getReceiptForLookup(identity);
|
|
18117
18453
|
if (stored)
|
|
18118
18454
|
staged.push(stored);
|
|
@@ -18502,6 +18838,64 @@ class SqliteTodosProjectRegistrationBackend {
|
|
|
18502
18838
|
getTaskList(id) {
|
|
18503
18839
|
return this.direct.getTaskList(id);
|
|
18504
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
|
+
}
|
|
18505
18899
|
}
|
|
18506
18900
|
|
|
18507
18901
|
// src/project-registration/authority.ts
|
|
@@ -18509,8 +18903,12 @@ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-
|
|
|
18509
18903
|
var WORKSPACE_ID_PATTERN = /^wks_[A-Za-z0-9][A-Za-z0-9_-]{11,}$/;
|
|
18510
18904
|
var OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
18511
18905
|
var STEP_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
18906
|
+
var AUTHORITY_ROUTE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
18907
|
+
var PACKAGE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
18512
18908
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
18513
18909
|
var IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
|
|
18910
|
+
var PROJECT_RESOURCE_PAGE_LIMIT = 500;
|
|
18911
|
+
var PROJECT_RESOURCE_CURSOR_VERSION = 1;
|
|
18514
18912
|
|
|
18515
18913
|
class WriteBoundaryError extends Error {
|
|
18516
18914
|
point;
|
|
@@ -18538,7 +18936,7 @@ function canonicalize3(value) {
|
|
|
18538
18936
|
return out;
|
|
18539
18937
|
}
|
|
18540
18938
|
function digestProjectRegistrationValue(value) {
|
|
18541
|
-
return
|
|
18939
|
+
return createHash7("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
|
|
18542
18940
|
}
|
|
18543
18941
|
function deriveTodosProjectRegistrationIdempotencyKey(input) {
|
|
18544
18942
|
return `prk_${digestProjectRegistrationValue({
|
|
@@ -18650,35 +19048,68 @@ function projectRecord(project) {
|
|
|
18650
19048
|
return {
|
|
18651
19049
|
target_id: project.id,
|
|
18652
19050
|
revision: project.updated_at,
|
|
18653
|
-
digest:
|
|
18654
|
-
id: project.id,
|
|
18655
|
-
name: project.name,
|
|
18656
|
-
path: project.path,
|
|
18657
|
-
description: project.description,
|
|
18658
|
-
task_list_id: project.task_list_id,
|
|
18659
|
-
task_prefix: project.task_prefix,
|
|
18660
|
-
task_counter: project.task_counter,
|
|
18661
|
-
created_at: project.created_at,
|
|
18662
|
-
updated_at: project.updated_at
|
|
18663
|
-
})
|
|
19051
|
+
digest: projectRegistrationDigest(project)
|
|
18664
19052
|
};
|
|
18665
19053
|
}
|
|
18666
19054
|
function taskListRecord(taskList) {
|
|
18667
19055
|
return {
|
|
18668
19056
|
target_id: taskList.id,
|
|
18669
19057
|
revision: taskList.updated_at,
|
|
18670
|
-
digest:
|
|
18671
|
-
|
|
18672
|
-
|
|
18673
|
-
|
|
18674
|
-
|
|
18675
|
-
|
|
18676
|
-
|
|
18677
|
-
|
|
18678
|
-
|
|
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
|
|
18679
19078
|
})
|
|
18680
19079
|
};
|
|
18681
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
|
+
}
|
|
18682
19113
|
function receiptId(input) {
|
|
18683
19114
|
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
18684
19115
|
}
|
|
@@ -18698,6 +19129,29 @@ function assertCapabilityRequest(request, capability) {
|
|
|
18698
19129
|
}
|
|
18699
19130
|
}
|
|
18700
19131
|
function normalizedCallDigest(request) {
|
|
19132
|
+
return digestProjectRegistrationValue({
|
|
19133
|
+
authority_route: request.authority_route,
|
|
19134
|
+
package_version: request.package_version,
|
|
19135
|
+
authority_id: request.authority_id,
|
|
19136
|
+
tenant_id: request.tenant_id,
|
|
19137
|
+
corpus_id: request.corpus_id,
|
|
19138
|
+
operation_id: request.operation_id,
|
|
19139
|
+
step_id: request.step_id,
|
|
19140
|
+
resource_kind: request.resource_kind,
|
|
19141
|
+
direction: request.direction,
|
|
19142
|
+
target_selector: request.target_selector,
|
|
19143
|
+
idempotency_key: request.idempotency_key,
|
|
19144
|
+
request_digest: request.request_digest,
|
|
19145
|
+
precondition_digest: request.precondition_digest,
|
|
19146
|
+
project_id: request.project_id,
|
|
19147
|
+
project_slug: request.project_slug,
|
|
19148
|
+
project_name: request.project_name,
|
|
19149
|
+
desired: request.desired,
|
|
19150
|
+
bind_existing: request.bind_existing === true,
|
|
19151
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
19152
|
+
});
|
|
19153
|
+
}
|
|
19154
|
+
function legacyNormalizedCallDigestBeforeBindExisting(request) {
|
|
18701
19155
|
return digestProjectRegistrationValue({
|
|
18702
19156
|
authority_route: request.authority_route,
|
|
18703
19157
|
package_version: request.package_version,
|
|
@@ -18719,6 +19173,11 @@ function normalizedCallDigest(request) {
|
|
|
18719
19173
|
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
18720
19174
|
});
|
|
18721
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
|
+
}
|
|
18722
19181
|
function assertCommonRequest(request, capability) {
|
|
18723
19182
|
assertBounds(request);
|
|
18724
19183
|
assertResourceKind(request.resource_kind);
|
|
@@ -18760,6 +19219,9 @@ function assertCommonRequest(request, capability) {
|
|
|
18760
19219
|
if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
|
|
18761
19220
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
|
|
18762
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
|
+
}
|
|
18763
19225
|
const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
|
|
18764
19226
|
operation_id: request.operation_id,
|
|
18765
19227
|
step_id: request.step_id,
|
|
@@ -18781,7 +19243,7 @@ function assertForwardRequest(request, capability) {
|
|
|
18781
19243
|
const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
|
|
18782
19244
|
const expectedPreconditionDigest = digestProjectRegistrationValue({
|
|
18783
19245
|
target_selector: request.target_selector,
|
|
18784
|
-
expected: "absent"
|
|
19246
|
+
expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
|
|
18785
19247
|
});
|
|
18786
19248
|
if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
|
|
18787
19249
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
|
|
@@ -18860,7 +19322,7 @@ function receiptBase(request, callDigest, capability) {
|
|
|
18860
19322
|
normalized_call_digest: callDigest
|
|
18861
19323
|
};
|
|
18862
19324
|
}
|
|
18863
|
-
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt) {
|
|
19325
|
+
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt, createdByOperation = true) {
|
|
18864
19326
|
return makeReceipt({
|
|
18865
19327
|
...receiptBase(request, callDigest, capability),
|
|
18866
19328
|
outcome: "accepted",
|
|
@@ -18870,7 +19332,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt)
|
|
|
18870
19332
|
result_digest: record.digest,
|
|
18871
19333
|
duplicate_of_receipt_id: null,
|
|
18872
19334
|
accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
|
|
18873
|
-
created_by_operation:
|
|
19335
|
+
created_by_operation: createdByOperation
|
|
18874
19336
|
}, createdAt);
|
|
18875
19337
|
}
|
|
18876
19338
|
function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt) {
|
|
@@ -18932,6 +19394,53 @@ function bindingFor(request, callDigest, timestamp2, capability) {
|
|
|
18932
19394
|
updated_at: timestamp2
|
|
18933
19395
|
};
|
|
18934
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
|
+
}
|
|
18935
19444
|
|
|
18936
19445
|
class PackageOwnedTodosProjectRegistrationAuthority {
|
|
18937
19446
|
backend;
|
|
@@ -18953,6 +19462,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
18953
19462
|
immutable_receipts: true,
|
|
18954
19463
|
exact_terminal_lookup: true,
|
|
18955
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,
|
|
18956
19469
|
conditional_inverse: true,
|
|
18957
19470
|
ambiguous_outcome_reconciliation: true
|
|
18958
19471
|
};
|
|
@@ -18997,6 +19510,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
18997
19510
|
async existingForwardResolution(transaction, request, callDigest) {
|
|
18998
19511
|
const exact = await transaction.getReceiptForLookup({
|
|
18999
19512
|
...authorityScope(this.capabilityValue),
|
|
19513
|
+
route: this.capabilityValue.route,
|
|
19514
|
+
package_version: this.capabilityValue.package_version,
|
|
19000
19515
|
operation_id: request.operation_id,
|
|
19001
19516
|
step_id: request.step_id,
|
|
19002
19517
|
resource_kind: request.resource_kind,
|
|
@@ -19011,7 +19526,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19011
19526
|
if (!accepted2) {
|
|
19012
19527
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
|
|
19013
19528
|
}
|
|
19014
|
-
if (accepted2
|
|
19529
|
+
if (!acceptedCallMatches(request, accepted2, callDigest)) {
|
|
19015
19530
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
|
|
19016
19531
|
}
|
|
19017
19532
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
@@ -19025,7 +19540,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19025
19540
|
});
|
|
19026
19541
|
if (!accepted)
|
|
19027
19542
|
return null;
|
|
19028
|
-
if (accepted
|
|
19543
|
+
if (acceptedCallMatches(request, accepted, callDigest)) {
|
|
19029
19544
|
return this.duplicateFor(transaction, request, callDigest, accepted);
|
|
19030
19545
|
}
|
|
19031
19546
|
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
@@ -19036,6 +19551,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19036
19551
|
const slug2 = taskListSlug(request.project_slug);
|
|
19037
19552
|
const conflict2 = await transaction.findProjectConflict(path, slug2);
|
|
19038
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
|
+
}
|
|
19039
19560
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
19040
19561
|
}
|
|
19041
19562
|
await this.fault("before_object_write", request);
|
|
@@ -19047,7 +19568,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19047
19568
|
task_prefix: deterministicTaskPrefix(request.project_slug)
|
|
19048
19569
|
});
|
|
19049
19570
|
await this.fault("after_object_write", request);
|
|
19050
|
-
return
|
|
19571
|
+
return {
|
|
19572
|
+
record: projectRecord(project),
|
|
19573
|
+
created_by_operation: true
|
|
19574
|
+
};
|
|
19051
19575
|
}
|
|
19052
19576
|
const todosProjectId = String(request.desired["todos_project_id"]);
|
|
19053
19577
|
const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
|
|
@@ -19061,6 +19585,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19061
19585
|
const slug = taskListSlug(request.project_slug);
|
|
19062
19586
|
const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
|
|
19063
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
|
+
}
|
|
19064
19594
|
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
19065
19595
|
}
|
|
19066
19596
|
await this.fault("before_object_write", request);
|
|
@@ -19077,7 +19607,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19077
19607
|
if (taskList.project_id !== todosProjectId) {
|
|
19078
19608
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
|
|
19079
19609
|
}
|
|
19080
|
-
return
|
|
19610
|
+
return {
|
|
19611
|
+
record: taskListRecord(taskList),
|
|
19612
|
+
created_by_operation: true
|
|
19613
|
+
};
|
|
19081
19614
|
}
|
|
19082
19615
|
async create(request) {
|
|
19083
19616
|
const startedAt = Date.now();
|
|
@@ -19099,9 +19632,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19099
19632
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
|
|
19100
19633
|
if (!claimed) {
|
|
19101
19634
|
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
|
|
19102
|
-
if (binding?.state === "accepted" && binding.
|
|
19635
|
+
if (binding?.state === "accepted" && binding.accepted_receipt_id) {
|
|
19103
19636
|
const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
|
|
19104
|
-
if (accepted2) {
|
|
19637
|
+
if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
|
|
19105
19638
|
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
19106
19639
|
}
|
|
19107
19640
|
}
|
|
@@ -19112,15 +19645,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19112
19645
|
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
19113
19646
|
return recordOrTerminal;
|
|
19114
19647
|
}
|
|
19115
|
-
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);
|
|
19116
19649
|
await this.fault("before_receipt_write", request);
|
|
19117
19650
|
const stored = await insertDeterministicReceipt(transaction, accepted);
|
|
19118
19651
|
await this.fault("after_receipt_write", request);
|
|
19119
19652
|
await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
|
|
19120
|
-
target_id: recordOrTerminal.target_id,
|
|
19653
|
+
target_id: recordOrTerminal.record.target_id,
|
|
19121
19654
|
accepted_receipt_id: stored.receipt_id,
|
|
19122
|
-
result_revision: recordOrTerminal.revision,
|
|
19123
|
-
result_digest: recordOrTerminal.digest,
|
|
19655
|
+
result_revision: recordOrTerminal.record.revision,
|
|
19656
|
+
result_digest: recordOrTerminal.record.digest,
|
|
19124
19657
|
updated_at: this.now()
|
|
19125
19658
|
});
|
|
19126
19659
|
return stored;
|
|
@@ -19149,6 +19682,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19149
19682
|
});
|
|
19150
19683
|
const exact = await transaction.getReceiptForLookup({
|
|
19151
19684
|
...authorityScope(this.capabilityValue),
|
|
19685
|
+
route: this.capabilityValue.route,
|
|
19686
|
+
package_version: this.capabilityValue.package_version,
|
|
19152
19687
|
operation_id: request.operation_id,
|
|
19153
19688
|
step_id: request.step_id,
|
|
19154
19689
|
resource_kind: request.resource_kind,
|
|
@@ -19166,7 +19701,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19166
19701
|
direction: request.direction
|
|
19167
19702
|
});
|
|
19168
19703
|
if (accepted) {
|
|
19169
|
-
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 });
|
|
19170
19705
|
}
|
|
19171
19706
|
const timestamp2 = this.now();
|
|
19172
19707
|
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
|
|
@@ -19199,9 +19734,18 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19199
19734
|
if (request.max_items !== 1) {
|
|
19200
19735
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
|
|
19201
19736
|
}
|
|
19202
|
-
if (request.authority !== "todos" || request.
|
|
19737
|
+
if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
|
|
19203
19738
|
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
|
|
19204
19739
|
}
|
|
19740
|
+
requireString(request.authority_route, "authority_route", {
|
|
19741
|
+
min: 3,
|
|
19742
|
+
max: 128,
|
|
19743
|
+
pattern: AUTHORITY_ROUTE_PATTERN
|
|
19744
|
+
});
|
|
19745
|
+
requireString(request.package_version, "package_version", {
|
|
19746
|
+
max: 128,
|
|
19747
|
+
pattern: PACKAGE_VERSION_PATTERN
|
|
19748
|
+
});
|
|
19205
19749
|
requireString(request.operation_id, "operation_id", {
|
|
19206
19750
|
min: 8,
|
|
19207
19751
|
max: 128,
|
|
@@ -19223,6 +19767,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19223
19767
|
}
|
|
19224
19768
|
const receipt = await this.backend.getReceiptForLookup({
|
|
19225
19769
|
...authorityScope(this.capabilityValue),
|
|
19770
|
+
route: request.authority_route,
|
|
19771
|
+
package_version: request.package_version,
|
|
19226
19772
|
operation_id: request.operation_id,
|
|
19227
19773
|
step_id: request.step_id,
|
|
19228
19774
|
resource_kind: request.resource_kind,
|
|
@@ -19235,6 +19781,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19235
19781
|
}
|
|
19236
19782
|
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
19237
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
|
+
}
|
|
19238
19942
|
async storedAcceptedReceipt(request, supplied) {
|
|
19239
19943
|
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
19240
19944
|
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
@@ -19261,6 +19965,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19261
19965
|
});
|
|
19262
19966
|
const exact = await transaction.getReceiptForLookup({
|
|
19263
19967
|
...authorityScope(this.capabilityValue),
|
|
19968
|
+
route: this.capabilityValue.route,
|
|
19969
|
+
package_version: this.capabilityValue.package_version,
|
|
19264
19970
|
operation_id: request.operation_id,
|
|
19265
19971
|
step_id: request.step_id,
|
|
19266
19972
|
resource_kind: request.resource_kind,
|
|
@@ -19355,6 +20061,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
|
|
|
19355
20061
|
await this.storedAcceptedReceipt(request, accepted);
|
|
19356
20062
|
const receipt = await this.backend.getReceiptForLookup({
|
|
19357
20063
|
...authorityScope(this.capabilityValue),
|
|
20064
|
+
route: this.capabilityValue.route,
|
|
20065
|
+
package_version: this.capabilityValue.package_version,
|
|
19358
20066
|
operation_id: request.operation_id,
|
|
19359
20067
|
step_id: request.step_id,
|
|
19360
20068
|
resource_kind: request.resource_kind,
|
|
@@ -19402,6 +20110,65 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
|
19402
20110
|
cursorTableName
|
|
19403
20111
|
}), authorityOptions);
|
|
19404
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
|
+
|
|
19405
20172
|
// src/project-registration/http.ts
|
|
19406
20173
|
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
19407
20174
|
function json(body, status = 200) {
|
|
@@ -19448,6 +20215,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
19448
20215
|
if ((action === "" || action === "capability") && method === "GET") {
|
|
19449
20216
|
return json({ capability: await authority.capability() });
|
|
19450
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
|
+
}
|
|
19451
20232
|
if (method !== "POST")
|
|
19452
20233
|
return json({ error: "method not allowed" }, 405);
|
|
19453
20234
|
const body = await readJson(req);
|
|
@@ -19470,6 +20251,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
|
|
|
19470
20251
|
record: await authority.readExact(body)
|
|
19471
20252
|
});
|
|
19472
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
|
+
}
|
|
19473
20260
|
if (action === "compensate") {
|
|
19474
20261
|
return json({
|
|
19475
20262
|
receipt: await authority.compensate(body)
|
|
@@ -19544,6 +20331,28 @@ class TodosProjectRegistrationHttpClient {
|
|
|
19544
20331
|
async lookupReceipt(request) {
|
|
19545
20332
|
return this.request("/receipts/lookup", { method: "POST", body: JSON.stringify(request) });
|
|
19546
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
|
+
}
|
|
19547
20356
|
async compensate(request) {
|
|
19548
20357
|
const body = await this.request("/compensate", { method: "POST", body: JSON.stringify(withoutTarget(request)) });
|
|
19549
20358
|
return body.receipt;
|
|
@@ -19569,6 +20378,7 @@ export {
|
|
|
19569
20378
|
createPostgresTodosProjectRegistrationAuthority,
|
|
19570
20379
|
createLocalTodosProjectRegistrationAuthority,
|
|
19571
20380
|
canonicalProjectRegistrationJson,
|
|
20381
|
+
assertTodosPriorRegistrationAdoptionValidationEnvelope,
|
|
19572
20382
|
TodosProjectRegistrationHttpClient,
|
|
19573
20383
|
TodosProjectRegistrationError,
|
|
19574
20384
|
TODOS_PROJECT_REGISTRATION_SCHEMA_VERSION,
|