@hasna/todos 0.15.6 → 0.15.9
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-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +4858 -1995
- package/dist/contracts.js +247 -3
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-lists.d.ts +5 -0
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2405 -27
- package/dist/lib/assignee-validation.d.ts +44 -0
- package/dist/lib/assignee-validation.d.ts.map +1 -1
- package/dist/lib/project-task-list-ensure.d.ts +19 -0
- package/dist/lib/project-task-list-ensure.d.ts.map +1 -0
- package/dist/mcp/index.js +3156 -374
- package/dist/mcp.js +7 -3
- package/dist/project-registration/authority.d.ts +45 -0
- package/dist/project-registration/authority.d.ts.map +1 -0
- package/dist/project-registration/backend.d.ts +83 -0
- package/dist/project-registration/backend.d.ts.map +1 -0
- package/dist/project-registration/http.d.ts +24 -0
- package/dist/project-registration/http.d.ts.map +1 -0
- package/dist/project-registration/index.d.ts +8 -0
- package/dist/project-registration/index.d.ts.map +1 -0
- package/dist/project-registration/postgres.d.ts +30 -0
- package/dist/project-registration/postgres.d.ts.map +1 -0
- package/dist/project-registration/schema.d.ts +3 -0
- package/dist/project-registration/schema.d.ts.map +1 -0
- package/dist/project-registration/sqlite.d.ts +17 -0
- package/dist/project-registration/sqlite.d.ts.map +1 -0
- package/dist/project-registration/types.d.ts +155 -0
- package/dist/project-registration/types.d.ts.map +1 -0
- package/dist/project-registration.d.ts +2 -0
- package/dist/project-registration.d.ts.map +1 -0
- package/dist/project-registration.js +18145 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +254 -3
- 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 +21 -0
- package/dist/sdk/v1.generated.d.ts +43 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +3 -0
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +5001 -2219
- package/dist/server/openapi.d.ts +311 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts +2 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +11 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage.js +242 -1
- package/dist/types/index.d.ts +29 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -3
package/dist/mcp/index.js
CHANGED
|
@@ -228,6 +228,206 @@ var init_types = __esm(() => {
|
|
|
228
228
|
};
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
+
// src/project-registration/schema.ts
|
|
232
|
+
function sqliteTodosProjectRegistrationSchemaSql() {
|
|
233
|
+
return `
|
|
234
|
+
CREATE TABLE IF NOT EXISTS todos_project_registration_receipts (
|
|
235
|
+
receipt_id TEXT PRIMARY KEY,
|
|
236
|
+
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
237
|
+
route TEXT NOT NULL,
|
|
238
|
+
package_version TEXT NOT NULL,
|
|
239
|
+
authority_id TEXT NOT NULL,
|
|
240
|
+
tenant_id TEXT NOT NULL,
|
|
241
|
+
corpus_id TEXT NOT NULL,
|
|
242
|
+
operation_id TEXT NOT NULL,
|
|
243
|
+
step_id TEXT NOT NULL,
|
|
244
|
+
resource_kind TEXT NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
245
|
+
direction TEXT NOT NULL CHECK(direction IN ('forward', 'inverse')),
|
|
246
|
+
target_selector TEXT NOT NULL,
|
|
247
|
+
idempotency_key TEXT NOT NULL,
|
|
248
|
+
request_digest TEXT NOT NULL,
|
|
249
|
+
precondition_digest TEXT NOT NULL,
|
|
250
|
+
normalized_call_digest TEXT NOT NULL,
|
|
251
|
+
outcome TEXT NOT NULL CHECK(outcome IN (
|
|
252
|
+
'accepted', 'duplicate_of_accepted', 'terminal_nonacceptance'
|
|
253
|
+
)),
|
|
254
|
+
reason TEXT,
|
|
255
|
+
target_id TEXT,
|
|
256
|
+
result_revision TEXT,
|
|
257
|
+
result_digest TEXT,
|
|
258
|
+
duplicate_of_receipt_id TEXT,
|
|
259
|
+
accepted_receipt_id TEXT,
|
|
260
|
+
created_by_operation INTEGER NOT NULL CHECK(created_by_operation IN (0, 1)),
|
|
261
|
+
created_at TEXT NOT NULL
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_lookup
|
|
265
|
+
ON todos_project_registration_receipts (
|
|
266
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
267
|
+
resource_kind, direction, idempotency_key
|
|
268
|
+
);
|
|
269
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
|
|
270
|
+
ON todos_project_registration_receipts (
|
|
271
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
272
|
+
resource_kind, direction, outcome
|
|
273
|
+
);
|
|
274
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_accepted_step
|
|
275
|
+
ON todos_project_registration_receipts (
|
|
276
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
277
|
+
resource_kind, direction
|
|
278
|
+
)
|
|
279
|
+
WHERE outcome = 'accepted';
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_target
|
|
281
|
+
ON todos_project_registration_receipts (
|
|
282
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
CREATE TABLE IF NOT EXISTS todos_project_registration_bindings (
|
|
286
|
+
authority_id TEXT NOT NULL,
|
|
287
|
+
tenant_id TEXT NOT NULL,
|
|
288
|
+
corpus_id TEXT NOT NULL,
|
|
289
|
+
resource_kind TEXT NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
290
|
+
target_selector TEXT NOT NULL,
|
|
291
|
+
operation_id TEXT NOT NULL,
|
|
292
|
+
step_id TEXT NOT NULL,
|
|
293
|
+
direction TEXT NOT NULL CHECK(direction = 'forward'),
|
|
294
|
+
idempotency_key TEXT NOT NULL,
|
|
295
|
+
request_digest TEXT NOT NULL,
|
|
296
|
+
precondition_digest TEXT NOT NULL,
|
|
297
|
+
normalized_call_digest TEXT NOT NULL,
|
|
298
|
+
state TEXT NOT NULL CHECK(state IN (
|
|
299
|
+
'pending', 'accepted', 'terminal_nonacceptance', 'removed'
|
|
300
|
+
)),
|
|
301
|
+
target_id TEXT,
|
|
302
|
+
accepted_receipt_id TEXT,
|
|
303
|
+
result_revision TEXT,
|
|
304
|
+
result_digest TEXT,
|
|
305
|
+
removed_receipt_id TEXT,
|
|
306
|
+
created_at TEXT NOT NULL,
|
|
307
|
+
updated_at TEXT NOT NULL,
|
|
308
|
+
PRIMARY KEY(
|
|
309
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector
|
|
310
|
+
),
|
|
311
|
+
UNIQUE(accepted_receipt_id)
|
|
312
|
+
);
|
|
313
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_project_registration_binding_target
|
|
314
|
+
ON todos_project_registration_bindings(
|
|
315
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
316
|
+
)
|
|
317
|
+
WHERE target_id IS NOT NULL;
|
|
318
|
+
|
|
319
|
+
CREATE TRIGGER IF NOT EXISTS todos_project_registration_receipts_immutable_update
|
|
320
|
+
BEFORE UPDATE ON todos_project_registration_receipts
|
|
321
|
+
BEGIN
|
|
322
|
+
SELECT RAISE(ABORT, 'todos project registration receipts are immutable');
|
|
323
|
+
END;
|
|
324
|
+
|
|
325
|
+
CREATE TRIGGER IF NOT EXISTS todos_project_registration_receipts_immutable_delete
|
|
326
|
+
BEFORE DELETE ON todos_project_registration_receipts
|
|
327
|
+
BEGIN
|
|
328
|
+
SELECT RAISE(ABORT, 'todos project registration receipts are immutable');
|
|
329
|
+
END;
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
function postgresTodosProjectRegistrationSchemaSql() {
|
|
333
|
+
return [
|
|
334
|
+
`CREATE TABLE IF NOT EXISTS todos_project_registration_receipts (
|
|
335
|
+
receipt_id text PRIMARY KEY,
|
|
336
|
+
authority text NOT NULL CHECK(authority = 'todos'),
|
|
337
|
+
route text NOT NULL,
|
|
338
|
+
package_version text NOT NULL,
|
|
339
|
+
authority_id text NOT NULL,
|
|
340
|
+
tenant_id text NOT NULL,
|
|
341
|
+
corpus_id text NOT NULL,
|
|
342
|
+
operation_id text NOT NULL,
|
|
343
|
+
step_id text NOT NULL,
|
|
344
|
+
resource_kind text NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
345
|
+
direction text NOT NULL CHECK(direction IN ('forward', 'inverse')),
|
|
346
|
+
target_selector text NOT NULL,
|
|
347
|
+
idempotency_key text NOT NULL,
|
|
348
|
+
request_digest text NOT NULL,
|
|
349
|
+
precondition_digest text NOT NULL,
|
|
350
|
+
normalized_call_digest text NOT NULL,
|
|
351
|
+
outcome text NOT NULL CHECK(outcome IN (
|
|
352
|
+
'accepted', 'duplicate_of_accepted', 'terminal_nonacceptance'
|
|
353
|
+
)),
|
|
354
|
+
reason text,
|
|
355
|
+
target_id text,
|
|
356
|
+
result_revision text,
|
|
357
|
+
result_digest text,
|
|
358
|
+
duplicate_of_receipt_id text,
|
|
359
|
+
accepted_receipt_id text,
|
|
360
|
+
created_by_operation boolean NOT NULL,
|
|
361
|
+
created_at timestamptz NOT NULL
|
|
362
|
+
)`,
|
|
363
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_lookup_idx
|
|
364
|
+
ON todos_project_registration_receipts (
|
|
365
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
366
|
+
resource_kind, direction, idempotency_key
|
|
367
|
+
)`,
|
|
368
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
|
|
369
|
+
ON todos_project_registration_receipts (
|
|
370
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
371
|
+
resource_kind, direction, outcome
|
|
372
|
+
)`,
|
|
373
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_project_registration_receipts_accepted_step_uidx
|
|
374
|
+
ON todos_project_registration_receipts (
|
|
375
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
376
|
+
resource_kind, direction
|
|
377
|
+
)
|
|
378
|
+
WHERE outcome = 'accepted'`,
|
|
379
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_target_idx
|
|
380
|
+
ON todos_project_registration_receipts (
|
|
381
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
382
|
+
)`,
|
|
383
|
+
`CREATE TABLE IF NOT EXISTS todos_project_registration_bindings (
|
|
384
|
+
authority_id text NOT NULL,
|
|
385
|
+
tenant_id text NOT NULL,
|
|
386
|
+
corpus_id text NOT NULL,
|
|
387
|
+
resource_kind text NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
388
|
+
target_selector text NOT NULL,
|
|
389
|
+
operation_id text NOT NULL,
|
|
390
|
+
step_id text NOT NULL,
|
|
391
|
+
direction text NOT NULL CHECK(direction = 'forward'),
|
|
392
|
+
idempotency_key text NOT NULL,
|
|
393
|
+
request_digest text NOT NULL,
|
|
394
|
+
precondition_digest text NOT NULL,
|
|
395
|
+
normalized_call_digest text NOT NULL,
|
|
396
|
+
state text NOT NULL CHECK(state IN (
|
|
397
|
+
'pending', 'accepted', 'terminal_nonacceptance', 'removed'
|
|
398
|
+
)),
|
|
399
|
+
target_id text,
|
|
400
|
+
accepted_receipt_id text UNIQUE,
|
|
401
|
+
result_revision text,
|
|
402
|
+
result_digest text,
|
|
403
|
+
removed_receipt_id text,
|
|
404
|
+
created_at timestamptz NOT NULL,
|
|
405
|
+
updated_at timestamptz NOT NULL,
|
|
406
|
+
PRIMARY KEY(
|
|
407
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector
|
|
408
|
+
)
|
|
409
|
+
)`,
|
|
410
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_project_registration_binding_target_uidx
|
|
411
|
+
ON todos_project_registration_bindings(
|
|
412
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
413
|
+
)
|
|
414
|
+
WHERE target_id IS NOT NULL`,
|
|
415
|
+
`CREATE OR REPLACE FUNCTION todos_project_registration_receipts_immutable()
|
|
416
|
+
RETURNS trigger
|
|
417
|
+
LANGUAGE plpgsql
|
|
418
|
+
AS $$
|
|
419
|
+
BEGIN
|
|
420
|
+
RAISE EXCEPTION 'todos project registration receipts are immutable';
|
|
421
|
+
END;
|
|
422
|
+
$$`,
|
|
423
|
+
`DROP TRIGGER IF EXISTS todos_project_registration_receipts_immutable
|
|
424
|
+
ON todos_project_registration_receipts`,
|
|
425
|
+
`CREATE TRIGGER todos_project_registration_receipts_immutable
|
|
426
|
+
BEFORE UPDATE OR DELETE ON todos_project_registration_receipts
|
|
427
|
+
FOR EACH ROW EXECUTE FUNCTION todos_project_registration_receipts_immutable()`
|
|
428
|
+
];
|
|
429
|
+
}
|
|
430
|
+
|
|
231
431
|
// src/db/migrations.ts
|
|
232
432
|
var MIGRATIONS;
|
|
233
433
|
var init_migrations = __esm(() => {
|
|
@@ -1833,6 +2033,11 @@ var init_migrations = __esm(() => {
|
|
|
1833
2033
|
INSERT OR IGNORE INTO _migrations (id) VALUES (68);
|
|
1834
2034
|
COMMIT;
|
|
1835
2035
|
PRAGMA foreign_keys = ON;
|
|
2036
|
+
`,
|
|
2037
|
+
`BEGIN;
|
|
2038
|
+
${sqliteTodosProjectRegistrationSchemaSql()}
|
|
2039
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (69);
|
|
2040
|
+
COMMIT;
|
|
1836
2041
|
`
|
|
1837
2042
|
];
|
|
1838
2043
|
});
|
|
@@ -3035,6 +3240,7 @@ function ensureSchema(db) {
|
|
|
3035
3240
|
)`);
|
|
3036
3241
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix)");
|
|
3037
3242
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at)");
|
|
3243
|
+
db.exec(sqliteTodosProjectRegistrationSchemaSql());
|
|
3038
3244
|
ensureTable("pr_groups", `
|
|
3039
3245
|
CREATE TABLE pr_groups (
|
|
3040
3246
|
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
@@ -11440,6 +11646,40 @@ function deleteTaskList(id, db) {
|
|
|
11440
11646
|
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
11441
11647
|
})();
|
|
11442
11648
|
}
|
|
11649
|
+
function deleteTaskListIfUnchangedAndUnused(id, expected, db) {
|
|
11650
|
+
const d = db || getDatabase();
|
|
11651
|
+
return d.transaction(() => {
|
|
11652
|
+
const current = getTaskList(id, d);
|
|
11653
|
+
if (!current) {
|
|
11654
|
+
return { status: "not_found", task_dependents: 0, plan_dependents: 0 };
|
|
11655
|
+
}
|
|
11656
|
+
const changed = current.project_id !== expected.project_id || current.slug !== expected.slug || current.name !== expected.name || current.description !== expected.description || current.updated_at !== expected.updated_at || JSON.stringify(current.metadata) !== JSON.stringify(expected.metadata);
|
|
11657
|
+
if (changed) {
|
|
11658
|
+
return { status: "changed", task_dependents: 0, plan_dependents: 0 };
|
|
11659
|
+
}
|
|
11660
|
+
const taskDependents = Number(d.query("SELECT COUNT(*) AS count FROM tasks WHERE task_list_id = ?").get(id).count);
|
|
11661
|
+
const planDependents = Number(d.query("SELECT COUNT(*) AS count FROM plans WHERE task_list_id = ?").get(id).count);
|
|
11662
|
+
if (taskDependents > 0 || planDependents > 0) {
|
|
11663
|
+
return {
|
|
11664
|
+
status: "has_dependents",
|
|
11665
|
+
task_dependents: taskDependents,
|
|
11666
|
+
plan_dependents: planDependents
|
|
11667
|
+
};
|
|
11668
|
+
}
|
|
11669
|
+
recordStorageTombstone({
|
|
11670
|
+
object_type: "task_lists",
|
|
11671
|
+
object_id: id,
|
|
11672
|
+
payload: current
|
|
11673
|
+
}, d);
|
|
11674
|
+
releaseCanonicalSlugClaims("task_list", id, d);
|
|
11675
|
+
const deleted = d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
11676
|
+
return {
|
|
11677
|
+
status: deleted ? "deleted" : "not_found",
|
|
11678
|
+
task_dependents: 0,
|
|
11679
|
+
plan_dependents: 0
|
|
11680
|
+
};
|
|
11681
|
+
})();
|
|
11682
|
+
}
|
|
11443
11683
|
function ensureTaskList(name, slug, projectId, db) {
|
|
11444
11684
|
const d = db || getDatabase();
|
|
11445
11685
|
const existing = getTaskListBySlug(slug, projectId, d);
|
|
@@ -20563,12 +20803,16 @@ function protectRemoteClient(client) {
|
|
|
20563
20803
|
function remoteAuthorityBase(client) {
|
|
20564
20804
|
return client.baseUrl.replace(/\/v1\/?$/, "");
|
|
20565
20805
|
}
|
|
20566
|
-
async function requiredRemoteRoute(client, route, request) {
|
|
20806
|
+
async function requiredRemoteRoute(client, route, request, recognized404Codes = []) {
|
|
20567
20807
|
try {
|
|
20568
20808
|
return await request();
|
|
20569
20809
|
} catch (error) {
|
|
20570
20810
|
const status = error && typeof error === "object" ? error.status : undefined;
|
|
20571
20811
|
if (status === 404) {
|
|
20812
|
+
const body = error && typeof error === "object" ? error.body : undefined;
|
|
20813
|
+
const code = body && typeof body === "object" && !Array.isArray(body) ? body.code : undefined;
|
|
20814
|
+
if (typeof code === "string" && recognized404Codes.includes(code))
|
|
20815
|
+
throw error;
|
|
20572
20816
|
throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} does not expose ${route}; ` + "deploy the @hasna/todos /v1 server contract before retrying; local SQLite fallback is disabled", { cause: error });
|
|
20573
20817
|
}
|
|
20574
20818
|
throw error;
|
|
@@ -20684,6 +20928,16 @@ async function cloudListProjects(client) {
|
|
|
20684
20928
|
const envelope = res.raw;
|
|
20685
20929
|
return Array.isArray(envelope?.projects) ? envelope.projects : res.items;
|
|
20686
20930
|
}
|
|
20931
|
+
function unwrapProject(raw) {
|
|
20932
|
+
if (raw && typeof raw === "object" && "project" in raw) {
|
|
20933
|
+
return raw.project;
|
|
20934
|
+
}
|
|
20935
|
+
return raw;
|
|
20936
|
+
}
|
|
20937
|
+
async function cloudGetProjectById(client, id) {
|
|
20938
|
+
const raw = await client.get("projects", id);
|
|
20939
|
+
return raw == null ? null : unwrapProject(raw);
|
|
20940
|
+
}
|
|
20687
20941
|
function cloudProjectSlug(value) {
|
|
20688
20942
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
20689
20943
|
}
|
|
@@ -20790,29 +21044,63 @@ async function cloudListTaskLists(client, projectId) {
|
|
|
20790
21044
|
return envelope.taskLists;
|
|
20791
21045
|
return Array.isArray(raw) ? raw : [];
|
|
20792
21046
|
}
|
|
21047
|
+
function unwrapTaskList(raw) {
|
|
21048
|
+
if (raw && typeof raw === "object" && "task_list" in raw) {
|
|
21049
|
+
return raw.task_list;
|
|
21050
|
+
}
|
|
21051
|
+
return raw;
|
|
21052
|
+
}
|
|
21053
|
+
async function cloudGetTaskList(client, id) {
|
|
21054
|
+
const raw = await client.get("task-lists", id);
|
|
21055
|
+
return raw == null ? null : unwrapTaskList(raw);
|
|
21056
|
+
}
|
|
21057
|
+
function resolveTaskListFromCandidates(lists, input) {
|
|
21058
|
+
const normalizedIdRef = input.toLowerCase();
|
|
21059
|
+
const matchGroups = [
|
|
21060
|
+
lists.filter((list) => list.id.toLowerCase() === normalizedIdRef),
|
|
21061
|
+
lists.filter((list) => list.slug === input),
|
|
21062
|
+
lists.filter((list) => list.id.toLowerCase().startsWith(normalizedIdRef))
|
|
21063
|
+
];
|
|
21064
|
+
for (const matches of matchGroups) {
|
|
21065
|
+
if (matches.length === 1)
|
|
21066
|
+
return matches[0].id;
|
|
21067
|
+
if (matches.length > 1) {
|
|
21068
|
+
throw new Error(`Task list reference is ambiguous: "${input}"`);
|
|
21069
|
+
}
|
|
21070
|
+
}
|
|
21071
|
+
return null;
|
|
21072
|
+
}
|
|
21073
|
+
async function legacyProjectTaskLists(client, projectId) {
|
|
21074
|
+
const project = await cloudGetProjectById(client, projectId);
|
|
21075
|
+
if (!project?.task_list_id)
|
|
21076
|
+
return [];
|
|
21077
|
+
return (await cloudListTaskLists(client)).filter((list) => list.project_id == null && list.slug === project.task_list_id);
|
|
21078
|
+
}
|
|
20793
21079
|
async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
20794
21080
|
const input = ref.trim();
|
|
20795
21081
|
const normalizedIdRef = input.toLowerCase();
|
|
20796
21082
|
if (UUID_RE.test(input) && !projectId)
|
|
20797
21083
|
return normalizedIdRef;
|
|
20798
|
-
|
|
20799
|
-
|
|
20800
|
-
|
|
20801
|
-
|
|
20802
|
-
|
|
20803
|
-
|
|
20804
|
-
|
|
20805
|
-
|
|
20806
|
-
|
|
20807
|
-
|
|
20808
|
-
|
|
20809
|
-
|
|
20810
|
-
}
|
|
20811
|
-
const
|
|
20812
|
-
if (
|
|
20813
|
-
return
|
|
20814
|
-
if (
|
|
20815
|
-
|
|
21084
|
+
if (UUID_RE.test(input) && projectId) {
|
|
21085
|
+
const direct = await cloudGetTaskList(client, normalizedIdRef);
|
|
21086
|
+
if (direct?.id?.toLowerCase() === normalizedIdRef) {
|
|
21087
|
+
if (direct.project_id === projectId)
|
|
21088
|
+
return direct.id;
|
|
21089
|
+
if (direct.project_id == null) {
|
|
21090
|
+
const project = await cloudGetProjectById(client, projectId);
|
|
21091
|
+
if (project?.task_list_id === direct.slug)
|
|
21092
|
+
return direct.id;
|
|
21093
|
+
}
|
|
21094
|
+
throw new Error(`Task list not found: "${input}"`);
|
|
21095
|
+
}
|
|
21096
|
+
}
|
|
21097
|
+
const scopedMatch = resolveTaskListFromCandidates(await cloudListTaskLists(client, projectId), input);
|
|
21098
|
+
if (scopedMatch)
|
|
21099
|
+
return scopedMatch;
|
|
21100
|
+
if (projectId) {
|
|
21101
|
+
const legacyMatch = resolveTaskListFromCandidates(await legacyProjectTaskLists(client, projectId), input);
|
|
21102
|
+
if (legacyMatch)
|
|
21103
|
+
return legacyMatch;
|
|
20816
21104
|
}
|
|
20817
21105
|
throw new Error(`Task list not found: "${input}"`);
|
|
20818
21106
|
}
|
|
@@ -34926,7 +35214,7 @@ var package_default;
|
|
|
34926
35214
|
var init_package = __esm(() => {
|
|
34927
35215
|
package_default = {
|
|
34928
35216
|
name: "@hasna/todos",
|
|
34929
|
-
version: "0.15.
|
|
35217
|
+
version: "0.15.9",
|
|
34930
35218
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
34931
35219
|
type: "module",
|
|
34932
35220
|
main: "dist/index.js",
|
|
@@ -34964,6 +35252,10 @@ var init_package = __esm(() => {
|
|
|
34964
35252
|
"./testing": {
|
|
34965
35253
|
types: "./dist/testing.d.ts",
|
|
34966
35254
|
import: "./dist/testing.js"
|
|
35255
|
+
},
|
|
35256
|
+
"./project-registration": {
|
|
35257
|
+
types: "./dist/project-registration.d.ts",
|
|
35258
|
+
import: "./dist/project-registration.js"
|
|
34967
35259
|
}
|
|
34968
35260
|
},
|
|
34969
35261
|
workspaces: [
|
|
@@ -34976,8 +35268,8 @@ var init_package = __esm(() => {
|
|
|
34976
35268
|
"README.md"
|
|
34977
35269
|
],
|
|
34978
35270
|
scripts: {
|
|
34979
|
-
build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
34980
|
-
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
35271
|
+
build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
35272
|
+
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
34981
35273
|
migrate: "bun run src/server/index.ts migrate",
|
|
34982
35274
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
34983
35275
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
@@ -44817,7 +45109,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
44817
45109
|
getBySlug: (slug, projectId) => getTaskListBySlug(slug, projectId, database()),
|
|
44818
45110
|
list: (projectId) => listTaskLists(projectId, database()),
|
|
44819
45111
|
update: (id, input) => updateTaskList(id, input, database()),
|
|
44820
|
-
delete: (id) => deleteTaskList(id, database())
|
|
45112
|
+
delete: (id) => deleteTaskList(id, database()),
|
|
45113
|
+
deleteIfUnchangedAndUnused: (id, expected) => deleteTaskListIfUnchangedAndUnused(id, expected, database())
|
|
44821
45114
|
},
|
|
44822
45115
|
templates: {
|
|
44823
45116
|
create: (input) => createTemplate(input, database()),
|
|
@@ -47723,6 +48016,2023 @@ var init_postgres = __esm(() => {
|
|
|
47723
48016
|
init_types3();
|
|
47724
48017
|
});
|
|
47725
48018
|
|
|
48019
|
+
// src/project-registration/types.ts
|
|
48020
|
+
var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1", TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1", TodosProjectRegistrationError;
|
|
48021
|
+
var init_types4 = __esm(() => {
|
|
48022
|
+
TodosProjectRegistrationError = class TodosProjectRegistrationError extends Error {
|
|
48023
|
+
code;
|
|
48024
|
+
details;
|
|
48025
|
+
constructor(code, message, details = {}) {
|
|
48026
|
+
super(message);
|
|
48027
|
+
this.code = code;
|
|
48028
|
+
this.details = details;
|
|
48029
|
+
this.name = "TodosProjectRegistrationError";
|
|
48030
|
+
}
|
|
48031
|
+
};
|
|
48032
|
+
});
|
|
48033
|
+
|
|
48034
|
+
// src/project-registration/postgres.ts
|
|
48035
|
+
function safeIdentifier(value, field) {
|
|
48036
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(value)) {
|
|
48037
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", `${field} must be a safe PostgreSQL identifier`);
|
|
48038
|
+
}
|
|
48039
|
+
return value;
|
|
48040
|
+
}
|
|
48041
|
+
function normalizeTimestamp2(value) {
|
|
48042
|
+
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
48043
|
+
}
|
|
48044
|
+
function parsePayload3(value) {
|
|
48045
|
+
if (typeof value === "string")
|
|
48046
|
+
return JSON.parse(value);
|
|
48047
|
+
return value;
|
|
48048
|
+
}
|
|
48049
|
+
function receiptFromRow(row) {
|
|
48050
|
+
return {
|
|
48051
|
+
...row,
|
|
48052
|
+
authority: "todos",
|
|
48053
|
+
created_by_operation: Boolean(row["created_by_operation"]),
|
|
48054
|
+
created_at: normalizeTimestamp2(row["created_at"])
|
|
48055
|
+
};
|
|
48056
|
+
}
|
|
48057
|
+
function bindingFromRow(row) {
|
|
48058
|
+
return {
|
|
48059
|
+
...row,
|
|
48060
|
+
created_at: normalizeTimestamp2(row["created_at"]),
|
|
48061
|
+
updated_at: normalizeTimestamp2(row["updated_at"])
|
|
48062
|
+
};
|
|
48063
|
+
}
|
|
48064
|
+
|
|
48065
|
+
class PostgresTodosProjectRegistrationTransaction {
|
|
48066
|
+
client;
|
|
48067
|
+
service;
|
|
48068
|
+
tableName;
|
|
48069
|
+
storage;
|
|
48070
|
+
constructor(client, service, tableName, cursorTableName) {
|
|
48071
|
+
this.client = client;
|
|
48072
|
+
this.service = service;
|
|
48073
|
+
this.tableName = tableName;
|
|
48074
|
+
this.storage = createPostgresTodosStorageAdapter({
|
|
48075
|
+
client,
|
|
48076
|
+
service,
|
|
48077
|
+
tableName,
|
|
48078
|
+
cursorTableName
|
|
48079
|
+
});
|
|
48080
|
+
}
|
|
48081
|
+
async lockStep(identity) {
|
|
48082
|
+
const key = [
|
|
48083
|
+
identity.authority_id,
|
|
48084
|
+
identity.tenant_id,
|
|
48085
|
+
identity.corpus_id,
|
|
48086
|
+
identity.operation_id,
|
|
48087
|
+
identity.step_id,
|
|
48088
|
+
identity.resource_kind,
|
|
48089
|
+
identity.direction
|
|
48090
|
+
].join("\x1F");
|
|
48091
|
+
await this.client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [key]);
|
|
48092
|
+
}
|
|
48093
|
+
async getReceiptForLookup(identity) {
|
|
48094
|
+
const result = await this.client.query(`
|
|
48095
|
+
SELECT * FROM todos_project_registration_receipts
|
|
48096
|
+
WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
|
|
48097
|
+
AND operation_id = $4 AND step_id = $5 AND resource_kind = $6
|
|
48098
|
+
AND direction = $7 AND idempotency_key = $8 AND target_selector = $9
|
|
48099
|
+
ORDER BY CASE outcome
|
|
48100
|
+
WHEN 'terminal_nonacceptance' THEN 0
|
|
48101
|
+
WHEN 'duplicate_of_accepted' THEN 1
|
|
48102
|
+
ELSE 2
|
|
48103
|
+
END, created_at DESC, receipt_id DESC
|
|
48104
|
+
LIMIT 1
|
|
48105
|
+
`, [
|
|
48106
|
+
identity.authority_id,
|
|
48107
|
+
identity.tenant_id,
|
|
48108
|
+
identity.corpus_id,
|
|
48109
|
+
identity.operation_id,
|
|
48110
|
+
identity.step_id,
|
|
48111
|
+
identity.resource_kind,
|
|
48112
|
+
identity.direction,
|
|
48113
|
+
identity.idempotency_key,
|
|
48114
|
+
identity.target_selector
|
|
48115
|
+
]);
|
|
48116
|
+
return result.rows[0] ? receiptFromRow(result.rows[0]) : null;
|
|
48117
|
+
}
|
|
48118
|
+
async getReceiptById(receiptId) {
|
|
48119
|
+
const result = await this.client.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = $1 LIMIT 1", [receiptId]);
|
|
48120
|
+
return result.rows[0] ? receiptFromRow(result.rows[0]) : null;
|
|
48121
|
+
}
|
|
48122
|
+
async getAcceptedReceiptForStep(identity) {
|
|
48123
|
+
const result = await this.client.query(`
|
|
48124
|
+
SELECT * FROM todos_project_registration_receipts
|
|
48125
|
+
WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
|
|
48126
|
+
AND operation_id = $4 AND step_id = $5 AND resource_kind = $6
|
|
48127
|
+
AND direction = $7 AND outcome = 'accepted'
|
|
48128
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
48129
|
+
LIMIT 1
|
|
48130
|
+
FOR UPDATE
|
|
48131
|
+
`, [
|
|
48132
|
+
identity.authority_id,
|
|
48133
|
+
identity.tenant_id,
|
|
48134
|
+
identity.corpus_id,
|
|
48135
|
+
identity.operation_id,
|
|
48136
|
+
identity.step_id,
|
|
48137
|
+
identity.resource_kind,
|
|
48138
|
+
identity.direction
|
|
48139
|
+
]);
|
|
48140
|
+
return result.rows[0] ? receiptFromRow(result.rows[0]) : null;
|
|
48141
|
+
}
|
|
48142
|
+
async insertReceipt(receipt) {
|
|
48143
|
+
const result = await this.client.query(`
|
|
48144
|
+
INSERT INTO todos_project_registration_receipts (
|
|
48145
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
48146
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
48147
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
48148
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
48149
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
48150
|
+
created_by_operation, created_at
|
|
48151
|
+
) VALUES (
|
|
48152
|
+
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,
|
|
48153
|
+
$19,$20,$21,$22,$23,$24,$25
|
|
48154
|
+
)
|
|
48155
|
+
ON CONFLICT (receipt_id) DO NOTHING
|
|
48156
|
+
RETURNING receipt_id
|
|
48157
|
+
`, [
|
|
48158
|
+
receipt.receipt_id,
|
|
48159
|
+
receipt.authority,
|
|
48160
|
+
receipt.route,
|
|
48161
|
+
receipt.package_version,
|
|
48162
|
+
receipt.authority_id,
|
|
48163
|
+
receipt.tenant_id,
|
|
48164
|
+
receipt.corpus_id,
|
|
48165
|
+
receipt.operation_id,
|
|
48166
|
+
receipt.step_id,
|
|
48167
|
+
receipt.resource_kind,
|
|
48168
|
+
receipt.direction,
|
|
48169
|
+
receipt.target_selector,
|
|
48170
|
+
receipt.idempotency_key,
|
|
48171
|
+
receipt.request_digest,
|
|
48172
|
+
receipt.precondition_digest,
|
|
48173
|
+
receipt.normalized_call_digest,
|
|
48174
|
+
receipt.outcome,
|
|
48175
|
+
receipt.reason,
|
|
48176
|
+
receipt.target_id,
|
|
48177
|
+
receipt.result_revision,
|
|
48178
|
+
receipt.result_digest,
|
|
48179
|
+
receipt.duplicate_of_receipt_id,
|
|
48180
|
+
receipt.accepted_receipt_id,
|
|
48181
|
+
receipt.created_by_operation,
|
|
48182
|
+
receipt.created_at
|
|
48183
|
+
]);
|
|
48184
|
+
return result.rows.length === 1;
|
|
48185
|
+
}
|
|
48186
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
48187
|
+
const result = await this.client.query(`
|
|
48188
|
+
SELECT * FROM todos_project_registration_bindings
|
|
48189
|
+
WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
|
|
48190
|
+
AND resource_kind = $4 AND target_selector = $5
|
|
48191
|
+
LIMIT 1
|
|
48192
|
+
FOR UPDATE
|
|
48193
|
+
`, [
|
|
48194
|
+
scope.authority_id,
|
|
48195
|
+
scope.tenant_id,
|
|
48196
|
+
scope.corpus_id,
|
|
48197
|
+
resourceKind,
|
|
48198
|
+
targetSelector
|
|
48199
|
+
]);
|
|
48200
|
+
return result.rows[0] ? bindingFromRow(result.rows[0]) : null;
|
|
48201
|
+
}
|
|
48202
|
+
async claimBinding(binding) {
|
|
48203
|
+
const result = await this.client.query(`
|
|
48204
|
+
INSERT INTO todos_project_registration_bindings (
|
|
48205
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
48206
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
48207
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
48208
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
48209
|
+
created_at, updated_at
|
|
48210
|
+
) VALUES (
|
|
48211
|
+
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,
|
|
48212
|
+
$18,$19,$20
|
|
48213
|
+
)
|
|
48214
|
+
ON CONFLICT (
|
|
48215
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector
|
|
48216
|
+
) DO NOTHING
|
|
48217
|
+
RETURNING target_selector
|
|
48218
|
+
`, [
|
|
48219
|
+
binding.authority_id,
|
|
48220
|
+
binding.tenant_id,
|
|
48221
|
+
binding.corpus_id,
|
|
48222
|
+
binding.resource_kind,
|
|
48223
|
+
binding.target_selector,
|
|
48224
|
+
binding.operation_id,
|
|
48225
|
+
binding.step_id,
|
|
48226
|
+
binding.direction,
|
|
48227
|
+
binding.idempotency_key,
|
|
48228
|
+
binding.request_digest,
|
|
48229
|
+
binding.precondition_digest,
|
|
48230
|
+
binding.normalized_call_digest,
|
|
48231
|
+
binding.state,
|
|
48232
|
+
binding.target_id,
|
|
48233
|
+
binding.accepted_receipt_id,
|
|
48234
|
+
binding.result_revision,
|
|
48235
|
+
binding.result_digest,
|
|
48236
|
+
binding.removed_receipt_id,
|
|
48237
|
+
binding.created_at,
|
|
48238
|
+
binding.updated_at
|
|
48239
|
+
]);
|
|
48240
|
+
return result.rows.length === 1;
|
|
48241
|
+
}
|
|
48242
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
48243
|
+
const result = await this.client.query(`
|
|
48244
|
+
UPDATE todos_project_registration_bindings
|
|
48245
|
+
SET state = 'accepted', target_id = $1, accepted_receipt_id = $2,
|
|
48246
|
+
result_revision = $3, result_digest = $4, updated_at = $5
|
|
48247
|
+
WHERE authority_id = $6 AND tenant_id = $7 AND corpus_id = $8
|
|
48248
|
+
AND resource_kind = $9 AND target_selector = $10 AND state = 'pending'
|
|
48249
|
+
RETURNING target_selector
|
|
48250
|
+
`, [
|
|
48251
|
+
update.target_id,
|
|
48252
|
+
update.accepted_receipt_id,
|
|
48253
|
+
update.result_revision,
|
|
48254
|
+
update.result_digest,
|
|
48255
|
+
update.updated_at,
|
|
48256
|
+
scope.authority_id,
|
|
48257
|
+
scope.tenant_id,
|
|
48258
|
+
scope.corpus_id,
|
|
48259
|
+
resourceKind,
|
|
48260
|
+
targetSelector
|
|
48261
|
+
]);
|
|
48262
|
+
if (result.rows.length !== 1) {
|
|
48263
|
+
throw new Error("Todos project registration binding was not pending at acceptance");
|
|
48264
|
+
}
|
|
48265
|
+
}
|
|
48266
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
48267
|
+
await this.client.query(`
|
|
48268
|
+
UPDATE todos_project_registration_bindings
|
|
48269
|
+
SET state = 'terminal_nonacceptance', updated_at = $1
|
|
48270
|
+
WHERE authority_id = $2 AND tenant_id = $3 AND corpus_id = $4
|
|
48271
|
+
AND resource_kind = $5 AND target_selector = $6 AND state = 'pending'
|
|
48272
|
+
`, [
|
|
48273
|
+
updatedAt,
|
|
48274
|
+
scope.authority_id,
|
|
48275
|
+
scope.tenant_id,
|
|
48276
|
+
scope.corpus_id,
|
|
48277
|
+
resourceKind,
|
|
48278
|
+
targetSelector
|
|
48279
|
+
]);
|
|
48280
|
+
}
|
|
48281
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
48282
|
+
const result = await this.client.query(`
|
|
48283
|
+
UPDATE todos_project_registration_bindings
|
|
48284
|
+
SET state = 'removed', removed_receipt_id = $1, updated_at = $2
|
|
48285
|
+
WHERE authority_id = $3 AND tenant_id = $4 AND corpus_id = $5
|
|
48286
|
+
AND resource_kind = $6 AND target_selector = $7 AND state = 'accepted'
|
|
48287
|
+
RETURNING target_selector
|
|
48288
|
+
`, [
|
|
48289
|
+
removedReceiptId,
|
|
48290
|
+
updatedAt,
|
|
48291
|
+
scope.authority_id,
|
|
48292
|
+
scope.tenant_id,
|
|
48293
|
+
scope.corpus_id,
|
|
48294
|
+
resourceKind,
|
|
48295
|
+
targetSelector
|
|
48296
|
+
]);
|
|
48297
|
+
if (result.rows.length !== 1) {
|
|
48298
|
+
throw new Error("Todos project registration binding was not accepted at removal");
|
|
48299
|
+
}
|
|
48300
|
+
}
|
|
48301
|
+
async findProjectConflict(path, taskListSlug) {
|
|
48302
|
+
const result = await this.client.query(`
|
|
48303
|
+
SELECT payload FROM ${this.tableName}
|
|
48304
|
+
WHERE service = $1 AND object_type = 'projects' AND deleted_at IS NULL
|
|
48305
|
+
AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
|
|
48306
|
+
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
48307
|
+
LIMIT 1
|
|
48308
|
+
`, [this.service, path, taskListSlug]);
|
|
48309
|
+
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
48310
|
+
}
|
|
48311
|
+
async findTaskListConflict(projectId, slug) {
|
|
48312
|
+
const result = await this.client.query(`
|
|
48313
|
+
SELECT payload FROM ${this.tableName}
|
|
48314
|
+
WHERE service = $1 AND object_type = 'task_lists' AND deleted_at IS NULL
|
|
48315
|
+
AND payload->>'project_id' = $2 AND payload->>'slug' = $3
|
|
48316
|
+
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
48317
|
+
LIMIT 1
|
|
48318
|
+
`, [this.service, projectId, slug]);
|
|
48319
|
+
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
48320
|
+
}
|
|
48321
|
+
async createProject(input) {
|
|
48322
|
+
return await this.storage.projects.create(input);
|
|
48323
|
+
}
|
|
48324
|
+
async createTaskList(input) {
|
|
48325
|
+
return await this.storage.taskLists.create(input);
|
|
48326
|
+
}
|
|
48327
|
+
async getProject(id) {
|
|
48328
|
+
return await this.storage.projects.get(id);
|
|
48329
|
+
}
|
|
48330
|
+
async getTaskList(id) {
|
|
48331
|
+
return await this.storage.taskLists.get(id);
|
|
48332
|
+
}
|
|
48333
|
+
async lockCompensationWrites() {
|
|
48334
|
+
await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
|
|
48335
|
+
}
|
|
48336
|
+
async hasDependents(resourceKind, targetId) {
|
|
48337
|
+
const referencePredicate = resourceKind === "project" ? `(
|
|
48338
|
+
payload->>'project_id' = $2
|
|
48339
|
+
OR payload->>'active_project_id' = $2
|
|
48340
|
+
OR payload->>'assigned_from_project' = $2
|
|
48341
|
+
OR payload->>'external_project_id' = $2
|
|
48342
|
+
)` : "payload->>'task_list_id' = $2";
|
|
48343
|
+
const result = await this.client.query(`
|
|
48344
|
+
SELECT EXISTS (
|
|
48345
|
+
SELECT 1 FROM ${this.tableName}
|
|
48346
|
+
WHERE service = $1 AND deleted_at IS NULL
|
|
48347
|
+
AND ${referencePredicate}
|
|
48348
|
+
LIMIT 1
|
|
48349
|
+
) AS exists
|
|
48350
|
+
`, [this.service, targetId]);
|
|
48351
|
+
return result.rows[0]?.exists === true;
|
|
48352
|
+
}
|
|
48353
|
+
async deleteProject(id) {
|
|
48354
|
+
return await this.storage.projects.delete(id);
|
|
48355
|
+
}
|
|
48356
|
+
async deleteTaskList(id) {
|
|
48357
|
+
return await this.storage.taskLists.delete(id);
|
|
48358
|
+
}
|
|
48359
|
+
}
|
|
48360
|
+
|
|
48361
|
+
class PostgresTodosProjectRegistrationBackend {
|
|
48362
|
+
client;
|
|
48363
|
+
kind = "postgresql";
|
|
48364
|
+
service;
|
|
48365
|
+
tableName;
|
|
48366
|
+
cursorTableName;
|
|
48367
|
+
schemaReady = null;
|
|
48368
|
+
constructor(client, options = {}) {
|
|
48369
|
+
this.client = client;
|
|
48370
|
+
this.service = options.service ?? "todos";
|
|
48371
|
+
this.tableName = safeIdentifier(options.tableName ?? "todos_sync_records", "tableName");
|
|
48372
|
+
this.cursorTableName = safeIdentifier(options.cursorTableName ?? "todos_sync_cursors", "cursorTableName");
|
|
48373
|
+
}
|
|
48374
|
+
async ensureSchema() {
|
|
48375
|
+
this.schemaReady ??= (async () => {
|
|
48376
|
+
for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
48377
|
+
await this.client.query(statement);
|
|
48378
|
+
}
|
|
48379
|
+
for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
|
|
48380
|
+
await this.client.query(statement);
|
|
48381
|
+
}
|
|
48382
|
+
})();
|
|
48383
|
+
await this.schemaReady;
|
|
48384
|
+
}
|
|
48385
|
+
async transaction(fn) {
|
|
48386
|
+
await this.ensureSchema();
|
|
48387
|
+
if (typeof this.client.transaction !== "function") {
|
|
48388
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE", "PostgreSQL project registration requires an authoritative transaction");
|
|
48389
|
+
}
|
|
48390
|
+
return this.client.transaction((transaction) => fn(new PostgresTodosProjectRegistrationTransaction(transaction, this.service, this.tableName, this.cursorTableName)));
|
|
48391
|
+
}
|
|
48392
|
+
async direct() {
|
|
48393
|
+
await this.ensureSchema();
|
|
48394
|
+
return new PostgresTodosProjectRegistrationTransaction(this.client, this.service, this.tableName, this.cursorTableName);
|
|
48395
|
+
}
|
|
48396
|
+
async getReceiptForLookup(identity) {
|
|
48397
|
+
return (await this.direct()).getReceiptForLookup(identity);
|
|
48398
|
+
}
|
|
48399
|
+
async getReceiptById(receiptId) {
|
|
48400
|
+
return (await this.direct()).getReceiptById(receiptId);
|
|
48401
|
+
}
|
|
48402
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
48403
|
+
return (await this.direct()).getBinding(scope, resourceKind, targetSelector);
|
|
48404
|
+
}
|
|
48405
|
+
async getProject(id) {
|
|
48406
|
+
return (await this.direct()).getProject(id);
|
|
48407
|
+
}
|
|
48408
|
+
async getTaskList(id) {
|
|
48409
|
+
return (await this.direct()).getTaskList(id);
|
|
48410
|
+
}
|
|
48411
|
+
}
|
|
48412
|
+
var init_postgres2 = __esm(() => {
|
|
48413
|
+
init_postgres_adapter();
|
|
48414
|
+
init_postgres_sync();
|
|
48415
|
+
init_types4();
|
|
48416
|
+
});
|
|
48417
|
+
|
|
48418
|
+
// src/project-registration/sqlite.ts
|
|
48419
|
+
function sameSqliteValue(left, right) {
|
|
48420
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
48421
|
+
}
|
|
48422
|
+
function taskListFromRow(row) {
|
|
48423
|
+
return {
|
|
48424
|
+
...row,
|
|
48425
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
48426
|
+
};
|
|
48427
|
+
}
|
|
48428
|
+
function selectProject(db, id) {
|
|
48429
|
+
return db.query("SELECT * FROM projects WHERE id = ? LIMIT 1").get(id);
|
|
48430
|
+
}
|
|
48431
|
+
function selectTaskList(db, id) {
|
|
48432
|
+
const row = db.query("SELECT * FROM task_lists WHERE id = ? LIMIT 1").get(id);
|
|
48433
|
+
return row ? taskListFromRow(row) : null;
|
|
48434
|
+
}
|
|
48435
|
+
function selectProjectConflict(db, path, taskListSlug) {
|
|
48436
|
+
return db.query(`
|
|
48437
|
+
SELECT * FROM projects
|
|
48438
|
+
WHERE path = ? OR task_list_id = ?
|
|
48439
|
+
ORDER BY created_at ASC, id ASC
|
|
48440
|
+
LIMIT 1
|
|
48441
|
+
`).get(path, taskListSlug);
|
|
48442
|
+
}
|
|
48443
|
+
function selectTaskListConflict(db, projectId, slug) {
|
|
48444
|
+
const row = db.query(`
|
|
48445
|
+
SELECT * FROM task_lists
|
|
48446
|
+
WHERE project_id = ? AND slug = ?
|
|
48447
|
+
LIMIT 1
|
|
48448
|
+
`).get(projectId, slug);
|
|
48449
|
+
return row ? taskListFromRow(row) : null;
|
|
48450
|
+
}
|
|
48451
|
+
function quoteSqliteIdentifier(value) {
|
|
48452
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
48453
|
+
}
|
|
48454
|
+
function hasSqliteDependents(db, resourceKind, targetId) {
|
|
48455
|
+
const targetTable = resourceKind === "project" ? "projects" : "task_lists";
|
|
48456
|
+
const semanticColumns = resourceKind === "project" ? PROJECT_REFERENCE_COLUMNS : TASK_LIST_REFERENCE_COLUMNS;
|
|
48457
|
+
const tables = db.query(`
|
|
48458
|
+
SELECT name FROM sqlite_schema
|
|
48459
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
48460
|
+
ORDER BY name
|
|
48461
|
+
`).all();
|
|
48462
|
+
for (const { name: tableName } of tables) {
|
|
48463
|
+
const quotedTable = quoteSqliteIdentifier(tableName);
|
|
48464
|
+
const columns = db.query(`PRAGMA table_info(${quotedTable})`).all();
|
|
48465
|
+
const foreignKeys = db.query(`PRAGMA foreign_key_list(${quotedTable})`).all();
|
|
48466
|
+
const referenceColumns = columns.map((column) => column.name).filter((columnName) => semanticColumns.has(columnName) || foreignKeys.some((foreignKey) => foreignKey.from === columnName && foreignKey.table === targetTable));
|
|
48467
|
+
for (const columnName of referenceColumns) {
|
|
48468
|
+
const row = db.query(`
|
|
48469
|
+
SELECT 1 AS found
|
|
48470
|
+
FROM ${quotedTable}
|
|
48471
|
+
WHERE ${quoteSqliteIdentifier(columnName)} = ?
|
|
48472
|
+
LIMIT 1
|
|
48473
|
+
`).get(targetId);
|
|
48474
|
+
if (row)
|
|
48475
|
+
return true;
|
|
48476
|
+
}
|
|
48477
|
+
}
|
|
48478
|
+
return false;
|
|
48479
|
+
}
|
|
48480
|
+
function receiptFromRow2(row) {
|
|
48481
|
+
return {
|
|
48482
|
+
...row,
|
|
48483
|
+
authority: "todos",
|
|
48484
|
+
created_by_operation: Number(row["created_by_operation"]) === 1
|
|
48485
|
+
};
|
|
48486
|
+
}
|
|
48487
|
+
function bindingFromRow2(row) {
|
|
48488
|
+
return row;
|
|
48489
|
+
}
|
|
48490
|
+
|
|
48491
|
+
class SqliteTodosProjectRegistrationTransaction {
|
|
48492
|
+
db;
|
|
48493
|
+
storage;
|
|
48494
|
+
constructor(db) {
|
|
48495
|
+
this.db = db;
|
|
48496
|
+
this.storage = createLocalSqliteTodosStorageAdapter({ db });
|
|
48497
|
+
}
|
|
48498
|
+
async lockStep(_identity) {}
|
|
48499
|
+
async getReceiptForLookup(identity) {
|
|
48500
|
+
const row = this.db.query(`
|
|
48501
|
+
SELECT * FROM todos_project_registration_receipts
|
|
48502
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48503
|
+
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
48504
|
+
AND direction = ? AND idempotency_key = ? AND target_selector = ?
|
|
48505
|
+
ORDER BY CASE outcome
|
|
48506
|
+
WHEN 'terminal_nonacceptance' THEN 0
|
|
48507
|
+
WHEN 'duplicate_of_accepted' THEN 1
|
|
48508
|
+
ELSE 2
|
|
48509
|
+
END, created_at DESC, receipt_id DESC
|
|
48510
|
+
LIMIT 1
|
|
48511
|
+
`).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);
|
|
48512
|
+
return row ? receiptFromRow2(row) : null;
|
|
48513
|
+
}
|
|
48514
|
+
async getReceiptById(receiptId) {
|
|
48515
|
+
const row = this.db.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = ? LIMIT 1").get(receiptId);
|
|
48516
|
+
return row ? receiptFromRow2(row) : null;
|
|
48517
|
+
}
|
|
48518
|
+
async getAcceptedReceiptForStep(identity) {
|
|
48519
|
+
const row = this.db.query(`
|
|
48520
|
+
SELECT * FROM todos_project_registration_receipts
|
|
48521
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48522
|
+
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
48523
|
+
AND direction = ? AND outcome = 'accepted'
|
|
48524
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
48525
|
+
LIMIT 1
|
|
48526
|
+
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction);
|
|
48527
|
+
return row ? receiptFromRow2(row) : null;
|
|
48528
|
+
}
|
|
48529
|
+
async insertReceipt(receipt) {
|
|
48530
|
+
const result = this.db.query(`
|
|
48531
|
+
INSERT OR IGNORE INTO todos_project_registration_receipts (
|
|
48532
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
48533
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
48534
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
48535
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
48536
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
48537
|
+
created_by_operation, created_at
|
|
48538
|
+
) VALUES (
|
|
48539
|
+
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
|
|
48540
|
+
)
|
|
48541
|
+
`).run(receipt.receipt_id, receipt.authority, receipt.route, receipt.package_version, receipt.authority_id, receipt.tenant_id, receipt.corpus_id, receipt.operation_id, receipt.step_id, receipt.resource_kind, receipt.direction, receipt.target_selector, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.normalized_call_digest, receipt.outcome, receipt.reason, receipt.target_id, receipt.result_revision, receipt.result_digest, receipt.duplicate_of_receipt_id, receipt.accepted_receipt_id, receipt.created_by_operation ? 1 : 0, receipt.created_at);
|
|
48542
|
+
return result.changes === 1;
|
|
48543
|
+
}
|
|
48544
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
48545
|
+
const row = this.db.query(`
|
|
48546
|
+
SELECT * FROM todos_project_registration_bindings
|
|
48547
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48548
|
+
AND resource_kind = ? AND target_selector = ?
|
|
48549
|
+
LIMIT 1
|
|
48550
|
+
`).get(scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48551
|
+
return row ? bindingFromRow2(row) : null;
|
|
48552
|
+
}
|
|
48553
|
+
async claimBinding(binding) {
|
|
48554
|
+
const result = this.db.query(`
|
|
48555
|
+
INSERT OR IGNORE INTO todos_project_registration_bindings (
|
|
48556
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
48557
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
48558
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
48559
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
48560
|
+
created_at, updated_at
|
|
48561
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
48562
|
+
`).run(binding.authority_id, binding.tenant_id, binding.corpus_id, binding.resource_kind, binding.target_selector, binding.operation_id, binding.step_id, binding.direction, binding.idempotency_key, binding.request_digest, binding.precondition_digest, binding.normalized_call_digest, binding.state, binding.target_id, binding.accepted_receipt_id, binding.result_revision, binding.result_digest, binding.removed_receipt_id, binding.created_at, binding.updated_at);
|
|
48563
|
+
return result.changes === 1;
|
|
48564
|
+
}
|
|
48565
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
48566
|
+
const result = this.db.query(`
|
|
48567
|
+
UPDATE todos_project_registration_bindings
|
|
48568
|
+
SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
|
|
48569
|
+
result_revision = ?, result_digest = ?, updated_at = ?
|
|
48570
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48571
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
48572
|
+
`).run(update.target_id, update.accepted_receipt_id, update.result_revision, update.result_digest, update.updated_at, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48573
|
+
if (result.changes !== 1) {
|
|
48574
|
+
throw new Error("Todos project registration binding was not pending at acceptance");
|
|
48575
|
+
}
|
|
48576
|
+
}
|
|
48577
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
48578
|
+
this.db.query(`
|
|
48579
|
+
UPDATE todos_project_registration_bindings
|
|
48580
|
+
SET state = 'terminal_nonacceptance', updated_at = ?
|
|
48581
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48582
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
48583
|
+
`).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48584
|
+
}
|
|
48585
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
48586
|
+
const result = this.db.query(`
|
|
48587
|
+
UPDATE todos_project_registration_bindings
|
|
48588
|
+
SET state = 'removed', removed_receipt_id = ?, updated_at = ?
|
|
48589
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48590
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
|
|
48591
|
+
`).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48592
|
+
if (result.changes !== 1) {
|
|
48593
|
+
throw new Error("Todos project registration binding was not accepted at removal");
|
|
48594
|
+
}
|
|
48595
|
+
}
|
|
48596
|
+
async findProjectConflict(path, taskListSlug) {
|
|
48597
|
+
const row = this.db.query(`
|
|
48598
|
+
SELECT * FROM projects
|
|
48599
|
+
WHERE path = ? OR task_list_id = ?
|
|
48600
|
+
ORDER BY created_at ASC, id ASC
|
|
48601
|
+
LIMIT 1
|
|
48602
|
+
`).get(path, taskListSlug);
|
|
48603
|
+
return row ?? null;
|
|
48604
|
+
}
|
|
48605
|
+
async findTaskListConflict(projectId, slug) {
|
|
48606
|
+
return await this.storage.taskLists.getBySlug(slug, projectId);
|
|
48607
|
+
}
|
|
48608
|
+
async createProject(input) {
|
|
48609
|
+
return await this.storage.projects.create(input);
|
|
48610
|
+
}
|
|
48611
|
+
async createTaskList(input) {
|
|
48612
|
+
return await this.storage.taskLists.create(input);
|
|
48613
|
+
}
|
|
48614
|
+
async getProject(id) {
|
|
48615
|
+
return await this.storage.projects.get(id);
|
|
48616
|
+
}
|
|
48617
|
+
async getTaskList(id) {
|
|
48618
|
+
return await this.storage.taskLists.get(id);
|
|
48619
|
+
}
|
|
48620
|
+
async lockCompensationWrites() {}
|
|
48621
|
+
async hasDependents(resourceKind, targetId) {
|
|
48622
|
+
return hasSqliteDependents(this.db, resourceKind, targetId);
|
|
48623
|
+
}
|
|
48624
|
+
async deleteProject(id) {
|
|
48625
|
+
return await this.storage.projects.delete(id);
|
|
48626
|
+
}
|
|
48627
|
+
async deleteTaskList(id) {
|
|
48628
|
+
return await this.storage.taskLists.delete(id);
|
|
48629
|
+
}
|
|
48630
|
+
}
|
|
48631
|
+
|
|
48632
|
+
class StagedSqliteTodosProjectRegistrationTransaction {
|
|
48633
|
+
db;
|
|
48634
|
+
direct;
|
|
48635
|
+
validators = [];
|
|
48636
|
+
mutations = [];
|
|
48637
|
+
receipts = new Map;
|
|
48638
|
+
bindings = new Map;
|
|
48639
|
+
projects = new Map;
|
|
48640
|
+
taskLists = new Map;
|
|
48641
|
+
constructor(db) {
|
|
48642
|
+
this.db = db;
|
|
48643
|
+
this.direct = new SqliteTodosProjectRegistrationTransaction(db);
|
|
48644
|
+
}
|
|
48645
|
+
commit() {
|
|
48646
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
48647
|
+
try {
|
|
48648
|
+
for (const validate of this.validators) {
|
|
48649
|
+
if (!validate()) {
|
|
48650
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration input changed before SQLite commit");
|
|
48651
|
+
}
|
|
48652
|
+
}
|
|
48653
|
+
for (const mutate of this.mutations)
|
|
48654
|
+
mutate();
|
|
48655
|
+
this.db.exec("COMMIT");
|
|
48656
|
+
} catch (error) {
|
|
48657
|
+
try {
|
|
48658
|
+
this.db.exec("ROLLBACK");
|
|
48659
|
+
} catch {}
|
|
48660
|
+
throw error;
|
|
48661
|
+
}
|
|
48662
|
+
}
|
|
48663
|
+
async lockStep(_identity) {}
|
|
48664
|
+
async getReceiptForLookup(identity) {
|
|
48665
|
+
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);
|
|
48666
|
+
const stored = await this.direct.getReceiptForLookup(identity);
|
|
48667
|
+
if (stored)
|
|
48668
|
+
staged.push(stored);
|
|
48669
|
+
const outcomeRank = (receipt) => receipt.outcome === "terminal_nonacceptance" ? 0 : receipt.outcome === "duplicate_of_accepted" ? 1 : 2;
|
|
48670
|
+
return staged.sort((left, right) => outcomeRank(left) - outcomeRank(right) || right.created_at.localeCompare(left.created_at) || right.receipt_id.localeCompare(left.receipt_id))[0] ?? null;
|
|
48671
|
+
}
|
|
48672
|
+
async getReceiptById(receiptId) {
|
|
48673
|
+
return this.receipts.get(receiptId) ?? this.direct.getReceiptById(receiptId);
|
|
48674
|
+
}
|
|
48675
|
+
async getAcceptedReceiptForStep(identity) {
|
|
48676
|
+
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.outcome === "accepted");
|
|
48677
|
+
const stored = await this.direct.getAcceptedReceiptForStep(identity);
|
|
48678
|
+
if (stored)
|
|
48679
|
+
staged.push(stored);
|
|
48680
|
+
return staged.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.receipt_id.localeCompare(right.receipt_id))[0] ?? null;
|
|
48681
|
+
}
|
|
48682
|
+
async insertReceipt(receipt) {
|
|
48683
|
+
if (this.receipts.has(receipt.receipt_id))
|
|
48684
|
+
return false;
|
|
48685
|
+
if (await this.direct.getReceiptById(receipt.receipt_id))
|
|
48686
|
+
return false;
|
|
48687
|
+
const planned = { ...receipt };
|
|
48688
|
+
this.receipts.set(planned.receipt_id, planned);
|
|
48689
|
+
this.mutations.push(() => {
|
|
48690
|
+
try {
|
|
48691
|
+
const result = this.db.query(`
|
|
48692
|
+
INSERT OR IGNORE INTO todos_project_registration_receipts (
|
|
48693
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
48694
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
48695
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
48696
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
48697
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
48698
|
+
created_by_operation, created_at
|
|
48699
|
+
) VALUES (
|
|
48700
|
+
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
|
|
48701
|
+
)
|
|
48702
|
+
`).run(planned.receipt_id, planned.authority, planned.route, planned.package_version, planned.authority_id, planned.tenant_id, planned.corpus_id, planned.operation_id, planned.step_id, planned.resource_kind, planned.direction, planned.target_selector, planned.idempotency_key, planned.request_digest, planned.precondition_digest, planned.normalized_call_digest, planned.outcome, planned.reason, planned.target_id, planned.result_revision, planned.result_digest, planned.duplicate_of_receipt_id, planned.accepted_receipt_id, planned.created_by_operation ? 1 : 0, planned.created_at);
|
|
48703
|
+
if (result.changes !== 1) {
|
|
48704
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt changed before SQLite commit");
|
|
48705
|
+
}
|
|
48706
|
+
} catch (error) {
|
|
48707
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
48708
|
+
throw error;
|
|
48709
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt conflicted at SQLite commit", { cause: error });
|
|
48710
|
+
}
|
|
48711
|
+
});
|
|
48712
|
+
return true;
|
|
48713
|
+
}
|
|
48714
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
48715
|
+
const key = this.bindingKey(scope, resourceKind, targetSelector);
|
|
48716
|
+
return this.bindings.get(key) ?? this.direct.getBinding(scope, resourceKind, targetSelector);
|
|
48717
|
+
}
|
|
48718
|
+
async claimBinding(binding) {
|
|
48719
|
+
const key = this.bindingKey(binding, binding.resource_kind, binding.target_selector);
|
|
48720
|
+
if (this.bindings.has(key))
|
|
48721
|
+
return false;
|
|
48722
|
+
if (await this.direct.getBinding(binding, binding.resource_kind, binding.target_selector)) {
|
|
48723
|
+
return false;
|
|
48724
|
+
}
|
|
48725
|
+
const planned = { ...binding };
|
|
48726
|
+
this.bindings.set(key, planned);
|
|
48727
|
+
this.mutations.push(() => {
|
|
48728
|
+
try {
|
|
48729
|
+
const result = this.db.query(`
|
|
48730
|
+
INSERT OR IGNORE INTO todos_project_registration_bindings (
|
|
48731
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
48732
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
48733
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
48734
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
48735
|
+
created_at, updated_at
|
|
48736
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
48737
|
+
`).run(planned.authority_id, planned.tenant_id, planned.corpus_id, planned.resource_kind, planned.target_selector, planned.operation_id, planned.step_id, planned.direction, planned.idempotency_key, planned.request_digest, planned.precondition_digest, planned.normalized_call_digest, planned.state, planned.target_id, planned.accepted_receipt_id, planned.result_revision, planned.result_digest, planned.removed_receipt_id, planned.created_at, planned.updated_at);
|
|
48738
|
+
if (result.changes !== 1) {
|
|
48739
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding changed before SQLite commit");
|
|
48740
|
+
}
|
|
48741
|
+
} catch (error) {
|
|
48742
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
48743
|
+
throw error;
|
|
48744
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding conflicted at SQLite commit", { cause: error });
|
|
48745
|
+
}
|
|
48746
|
+
});
|
|
48747
|
+
return true;
|
|
48748
|
+
}
|
|
48749
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
48750
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
|
|
48751
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
48752
|
+
...binding,
|
|
48753
|
+
state: "accepted",
|
|
48754
|
+
target_id: update.target_id,
|
|
48755
|
+
accepted_receipt_id: update.accepted_receipt_id,
|
|
48756
|
+
result_revision: update.result_revision,
|
|
48757
|
+
result_digest: update.result_digest,
|
|
48758
|
+
updated_at: update.updated_at
|
|
48759
|
+
});
|
|
48760
|
+
this.mutations.push(() => {
|
|
48761
|
+
const result = this.db.query(`
|
|
48762
|
+
UPDATE todos_project_registration_bindings
|
|
48763
|
+
SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
|
|
48764
|
+
result_revision = ?, result_digest = ?, updated_at = ?
|
|
48765
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48766
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
48767
|
+
`).run(update.target_id, update.accepted_receipt_id, update.result_revision, update.result_digest, update.updated_at, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48768
|
+
if (result.changes !== 1) {
|
|
48769
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
|
|
48770
|
+
}
|
|
48771
|
+
});
|
|
48772
|
+
}
|
|
48773
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
48774
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
|
|
48775
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
48776
|
+
...binding,
|
|
48777
|
+
state: "terminal_nonacceptance",
|
|
48778
|
+
updated_at: updatedAt
|
|
48779
|
+
});
|
|
48780
|
+
this.mutations.push(() => {
|
|
48781
|
+
const result = this.db.query(`
|
|
48782
|
+
UPDATE todos_project_registration_bindings
|
|
48783
|
+
SET state = 'terminal_nonacceptance', updated_at = ?
|
|
48784
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48785
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
48786
|
+
`).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48787
|
+
if (result.changes !== 1) {
|
|
48788
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
|
|
48789
|
+
}
|
|
48790
|
+
});
|
|
48791
|
+
}
|
|
48792
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
48793
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "accepted");
|
|
48794
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
48795
|
+
...binding,
|
|
48796
|
+
state: "removed",
|
|
48797
|
+
removed_receipt_id: removedReceiptId,
|
|
48798
|
+
updated_at: updatedAt
|
|
48799
|
+
});
|
|
48800
|
+
this.mutations.push(() => {
|
|
48801
|
+
const result = this.db.query(`
|
|
48802
|
+
UPDATE todos_project_registration_bindings
|
|
48803
|
+
SET state = 'removed', removed_receipt_id = ?, updated_at = ?
|
|
48804
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
48805
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
|
|
48806
|
+
`).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
48807
|
+
if (result.changes !== 1) {
|
|
48808
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer accepted at SQLite commit");
|
|
48809
|
+
}
|
|
48810
|
+
});
|
|
48811
|
+
}
|
|
48812
|
+
async findProjectConflict(path, taskListSlug) {
|
|
48813
|
+
const planned = [...this.projects.values()].find((project) => project?.path === path || project?.task_list_id === taskListSlug);
|
|
48814
|
+
if (planned)
|
|
48815
|
+
return planned;
|
|
48816
|
+
const observed = selectProjectConflict(this.db, path, taskListSlug);
|
|
48817
|
+
this.validators.push(() => sameSqliteValue(selectProjectConflict(this.db, path, taskListSlug), observed));
|
|
48818
|
+
return observed;
|
|
48819
|
+
}
|
|
48820
|
+
async findTaskListConflict(projectId, slug) {
|
|
48821
|
+
const planned = [...this.taskLists.values()].find((taskList) => taskList?.project_id === projectId && taskList.slug === slug);
|
|
48822
|
+
if (planned)
|
|
48823
|
+
return planned;
|
|
48824
|
+
const observed = selectTaskListConflict(this.db, projectId, slug);
|
|
48825
|
+
this.validators.push(() => sameSqliteValue(selectTaskListConflict(this.db, projectId, slug), observed));
|
|
48826
|
+
return observed;
|
|
48827
|
+
}
|
|
48828
|
+
async createProject(input) {
|
|
48829
|
+
const derivedSlug = normalizeSlug(input.name);
|
|
48830
|
+
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : normalizeSlug(input.task_list_id);
|
|
48831
|
+
if (!derivedSlug || !taskListId) {
|
|
48832
|
+
throw new Error("Project name and task-list slug must be non-empty");
|
|
48833
|
+
}
|
|
48834
|
+
const project = {
|
|
48835
|
+
id: uuid(),
|
|
48836
|
+
name: input.name,
|
|
48837
|
+
path: input.path,
|
|
48838
|
+
description: input.description || null,
|
|
48839
|
+
task_list_id: taskListId,
|
|
48840
|
+
task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
|
|
48841
|
+
task_counter: 0,
|
|
48842
|
+
created_at: now(),
|
|
48843
|
+
updated_at: now(),
|
|
48844
|
+
machine_id: currentStorageMachineId(this.db)
|
|
48845
|
+
};
|
|
48846
|
+
project.updated_at = project.created_at;
|
|
48847
|
+
this.projects.set(project.id, project);
|
|
48848
|
+
this.mutations.push(() => {
|
|
48849
|
+
try {
|
|
48850
|
+
const result = this.db.run(`INSERT INTO projects (
|
|
48851
|
+
id, name, path, description, task_list_id, task_prefix,
|
|
48852
|
+
task_counter, created_at, updated_at, machine_id
|
|
48853
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
|
|
48854
|
+
project.id,
|
|
48855
|
+
project.name,
|
|
48856
|
+
project.path,
|
|
48857
|
+
project.description,
|
|
48858
|
+
project.task_list_id,
|
|
48859
|
+
project.task_prefix,
|
|
48860
|
+
project.created_at,
|
|
48861
|
+
project.updated_at,
|
|
48862
|
+
project.machine_id ?? null
|
|
48863
|
+
]);
|
|
48864
|
+
if (result.changes < 1) {
|
|
48865
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite registration commit");
|
|
48866
|
+
}
|
|
48867
|
+
} catch (error) {
|
|
48868
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
48869
|
+
throw error;
|
|
48870
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project conflicted at SQLite registration commit", { cause: error });
|
|
48871
|
+
}
|
|
48872
|
+
});
|
|
48873
|
+
return project;
|
|
48874
|
+
}
|
|
48875
|
+
async createTaskList(input) {
|
|
48876
|
+
const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
|
|
48877
|
+
if (!slug)
|
|
48878
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
48879
|
+
const taskList = {
|
|
48880
|
+
id: uuid(),
|
|
48881
|
+
project_id: input.project_id || null,
|
|
48882
|
+
slug,
|
|
48883
|
+
name: input.name,
|
|
48884
|
+
description: input.description || null,
|
|
48885
|
+
metadata: input.metadata ?? {},
|
|
48886
|
+
created_at: now(),
|
|
48887
|
+
updated_at: now(),
|
|
48888
|
+
machine_id: currentStorageMachineId(this.db)
|
|
48889
|
+
};
|
|
48890
|
+
taskList.updated_at = taskList.created_at;
|
|
48891
|
+
this.taskLists.set(taskList.id, taskList);
|
|
48892
|
+
this.mutations.push(() => {
|
|
48893
|
+
try {
|
|
48894
|
+
const result = this.db.run(`INSERT INTO task_lists (
|
|
48895
|
+
id, project_id, slug, name, description, metadata,
|
|
48896
|
+
created_at, updated_at, machine_id
|
|
48897
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
48898
|
+
taskList.id,
|
|
48899
|
+
taskList.project_id,
|
|
48900
|
+
taskList.slug,
|
|
48901
|
+
taskList.name,
|
|
48902
|
+
taskList.description,
|
|
48903
|
+
JSON.stringify(taskList.metadata),
|
|
48904
|
+
taskList.created_at,
|
|
48905
|
+
taskList.updated_at,
|
|
48906
|
+
taskList.machine_id ?? null
|
|
48907
|
+
]);
|
|
48908
|
+
if (result.changes < 1) {
|
|
48909
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite registration commit");
|
|
48910
|
+
}
|
|
48911
|
+
} catch (error) {
|
|
48912
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
48913
|
+
throw error;
|
|
48914
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list conflicted at SQLite registration commit", { cause: error });
|
|
48915
|
+
}
|
|
48916
|
+
});
|
|
48917
|
+
return taskList;
|
|
48918
|
+
}
|
|
48919
|
+
async getProject(id) {
|
|
48920
|
+
if (this.projects.has(id))
|
|
48921
|
+
return this.projects.get(id) ?? null;
|
|
48922
|
+
const observed = selectProject(this.db, id);
|
|
48923
|
+
this.validators.push(() => sameSqliteValue(selectProject(this.db, id), observed));
|
|
48924
|
+
return observed;
|
|
48925
|
+
}
|
|
48926
|
+
async getTaskList(id) {
|
|
48927
|
+
if (this.taskLists.has(id))
|
|
48928
|
+
return this.taskLists.get(id) ?? null;
|
|
48929
|
+
const observed = selectTaskList(this.db, id);
|
|
48930
|
+
this.validators.push(() => sameSqliteValue(selectTaskList(this.db, id), observed));
|
|
48931
|
+
return observed;
|
|
48932
|
+
}
|
|
48933
|
+
async lockCompensationWrites() {}
|
|
48934
|
+
async hasDependents(resourceKind, targetId) {
|
|
48935
|
+
const observed = hasSqliteDependents(this.db, resourceKind, targetId);
|
|
48936
|
+
this.validators.push(() => hasSqliteDependents(this.db, resourceKind, targetId) === observed);
|
|
48937
|
+
return observed;
|
|
48938
|
+
}
|
|
48939
|
+
async deleteProject(id) {
|
|
48940
|
+
const project = await this.getProject(id);
|
|
48941
|
+
if (!project)
|
|
48942
|
+
return false;
|
|
48943
|
+
this.projects.set(id, null);
|
|
48944
|
+
this.mutations.push(() => {
|
|
48945
|
+
recordStorageTombstone({
|
|
48946
|
+
object_type: "projects",
|
|
48947
|
+
object_id: id,
|
|
48948
|
+
payload: project
|
|
48949
|
+
}, this.db);
|
|
48950
|
+
if (this.db.run("DELETE FROM projects WHERE id = ?", [id]).changes < 1) {
|
|
48951
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite compensation commit");
|
|
48952
|
+
}
|
|
48953
|
+
});
|
|
48954
|
+
return true;
|
|
48955
|
+
}
|
|
48956
|
+
async deleteTaskList(id) {
|
|
48957
|
+
const taskList = await this.getTaskList(id);
|
|
48958
|
+
if (!taskList)
|
|
48959
|
+
return false;
|
|
48960
|
+
this.taskLists.set(id, null);
|
|
48961
|
+
this.mutations.push(() => {
|
|
48962
|
+
recordStorageTombstone({
|
|
48963
|
+
object_type: "task_lists",
|
|
48964
|
+
object_id: id,
|
|
48965
|
+
payload: taskList
|
|
48966
|
+
}, this.db);
|
|
48967
|
+
if (this.db.run("DELETE FROM task_lists WHERE id = ?", [id]).changes < 1) {
|
|
48968
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite compensation commit");
|
|
48969
|
+
}
|
|
48970
|
+
});
|
|
48971
|
+
return true;
|
|
48972
|
+
}
|
|
48973
|
+
bindingKey(scope, resourceKind, targetSelector) {
|
|
48974
|
+
return JSON.stringify([
|
|
48975
|
+
scope.authority_id,
|
|
48976
|
+
scope.tenant_id,
|
|
48977
|
+
scope.corpus_id,
|
|
48978
|
+
resourceKind,
|
|
48979
|
+
targetSelector
|
|
48980
|
+
]);
|
|
48981
|
+
}
|
|
48982
|
+
async requireBinding(scope, resourceKind, targetSelector, state) {
|
|
48983
|
+
const binding = await this.getBinding(scope, resourceKind, targetSelector);
|
|
48984
|
+
if (!binding || binding.state !== state) {
|
|
48985
|
+
throw new Error(`Todos project registration binding was not ${state}`);
|
|
48986
|
+
}
|
|
48987
|
+
return binding;
|
|
48988
|
+
}
|
|
48989
|
+
availableProjectPrefix(name) {
|
|
48990
|
+
const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
|
|
48991
|
+
const prefix = words.length >= 3 ? words.slice(0, 3).map((word) => word[0].toUpperCase()).join("") : words.length === 2 ? (words[0].slice(0, 2) + words[1][0]).toUpperCase() : words[0].slice(0, 3).toUpperCase();
|
|
48992
|
+
let candidate = prefix;
|
|
48993
|
+
let suffix = 1;
|
|
48994
|
+
while (this.db.query("SELECT id FROM projects WHERE task_prefix = ? LIMIT 1").get(candidate) || [...this.projects.values()].some((project) => project?.task_prefix === candidate)) {
|
|
48995
|
+
suffix += 1;
|
|
48996
|
+
candidate = `${prefix}${suffix}`;
|
|
48997
|
+
}
|
|
48998
|
+
return candidate;
|
|
48999
|
+
}
|
|
49000
|
+
}
|
|
49001
|
+
var sqliteTransactionTails, PROJECT_REFERENCE_COLUMNS, TASK_LIST_REFERENCE_COLUMNS, SqliteRegistrationOptimisticConflict;
|
|
49002
|
+
var init_sqlite = __esm(() => {
|
|
49003
|
+
init_database();
|
|
49004
|
+
init_storage_tombstones();
|
|
49005
|
+
init_local_sqlite();
|
|
49006
|
+
sqliteTransactionTails = new WeakMap;
|
|
49007
|
+
PROJECT_REFERENCE_COLUMNS = new Set([
|
|
49008
|
+
"project_id",
|
|
49009
|
+
"active_project_id",
|
|
49010
|
+
"assigned_from_project",
|
|
49011
|
+
"external_project_id"
|
|
49012
|
+
]);
|
|
49013
|
+
TASK_LIST_REFERENCE_COLUMNS = new Set(["task_list_id"]);
|
|
49014
|
+
SqliteRegistrationOptimisticConflict = class SqliteRegistrationOptimisticConflict extends Error {
|
|
49015
|
+
constructor(message, options = {}) {
|
|
49016
|
+
super(message, options);
|
|
49017
|
+
this.name = "SqliteRegistrationOptimisticConflict";
|
|
49018
|
+
}
|
|
49019
|
+
};
|
|
49020
|
+
});
|
|
49021
|
+
|
|
49022
|
+
// src/project-registration/authority.ts
|
|
49023
|
+
import { createHash as createHash14 } from "crypto";
|
|
49024
|
+
function canonicalProjectRegistrationJson(value) {
|
|
49025
|
+
return JSON.stringify(canonicalize2(value));
|
|
49026
|
+
}
|
|
49027
|
+
function canonicalize2(value) {
|
|
49028
|
+
if (Array.isArray(value))
|
|
49029
|
+
return value.map(canonicalize2);
|
|
49030
|
+
if (!value || typeof value !== "object")
|
|
49031
|
+
return value;
|
|
49032
|
+
const out = {};
|
|
49033
|
+
for (const key of Object.keys(value).sort()) {
|
|
49034
|
+
const entry2 = value[key];
|
|
49035
|
+
if (entry2 !== undefined)
|
|
49036
|
+
out[key] = canonicalize2(entry2);
|
|
49037
|
+
}
|
|
49038
|
+
return out;
|
|
49039
|
+
}
|
|
49040
|
+
function digestProjectRegistrationValue(value) {
|
|
49041
|
+
return createHash14("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
|
|
49042
|
+
}
|
|
49043
|
+
function deriveTodosProjectRegistrationIdempotencyKey(input) {
|
|
49044
|
+
return `prk_${digestProjectRegistrationValue({
|
|
49045
|
+
route: TODOS_PROJECT_REGISTRATION_CALLER_ROUTE,
|
|
49046
|
+
...input
|
|
49047
|
+
}).slice(0, 48)}`;
|
|
49048
|
+
}
|
|
49049
|
+
function responseBytes(value) {
|
|
49050
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
49051
|
+
}
|
|
49052
|
+
function assertBounds(bounds) {
|
|
49053
|
+
if (!Number.isSafeInteger(bounds.response_byte_limit) || bounds.response_byte_limit <= 0) {
|
|
49054
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "response_byte_limit must be a positive integer");
|
|
49055
|
+
}
|
|
49056
|
+
if (!Number.isSafeInteger(bounds.time_budget_ms) || bounds.time_budget_ms <= 0) {
|
|
49057
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "time_budget_ms must be a positive integer");
|
|
49058
|
+
}
|
|
49059
|
+
}
|
|
49060
|
+
function assertResourceKind(value) {
|
|
49061
|
+
if (value !== "project" && value !== "task_list") {
|
|
49062
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "resource_kind must be project or task_list");
|
|
49063
|
+
}
|
|
49064
|
+
}
|
|
49065
|
+
function assertDirection(value) {
|
|
49066
|
+
if (value !== "forward" && value !== "inverse") {
|
|
49067
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "direction must be forward or inverse");
|
|
49068
|
+
}
|
|
49069
|
+
}
|
|
49070
|
+
function assertWithinBounds(value, bounds, startedAt) {
|
|
49071
|
+
const bytes = responseBytes(value);
|
|
49072
|
+
if (bytes > bounds.response_byte_limit) {
|
|
49073
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RESPONSE_TOO_LARGE", `registration response requires ${bytes} bytes but the bound is ${bounds.response_byte_limit}`, { response_bytes: bytes, response_byte_limit: bounds.response_byte_limit });
|
|
49074
|
+
}
|
|
49075
|
+
const elapsed = Date.now() - startedAt;
|
|
49076
|
+
if (elapsed > bounds.time_budget_ms) {
|
|
49077
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", `registration call took ${elapsed}ms but the bound is ${bounds.time_budget_ms}ms`, { elapsed_ms: elapsed, time_budget_ms: bounds.time_budget_ms });
|
|
49078
|
+
}
|
|
49079
|
+
return { response_bytes: bytes, elapsed_ms: elapsed };
|
|
49080
|
+
}
|
|
49081
|
+
function withResponseControl(payload, bounds, startedAt) {
|
|
49082
|
+
const envelope = {
|
|
49083
|
+
...payload,
|
|
49084
|
+
response_control: {
|
|
49085
|
+
response_byte_limit: bounds.response_byte_limit,
|
|
49086
|
+
time_budget_ms: bounds.time_budget_ms,
|
|
49087
|
+
response_bytes: 0,
|
|
49088
|
+
elapsed_ms: 0,
|
|
49089
|
+
complete: true,
|
|
49090
|
+
truncated: false
|
|
49091
|
+
}
|
|
49092
|
+
};
|
|
49093
|
+
for (let attempt = 0;attempt < 8; attempt += 1) {
|
|
49094
|
+
const measured = assertWithinBounds(envelope, bounds, startedAt);
|
|
49095
|
+
const stable3 = envelope.response_control.response_bytes === measured.response_bytes && envelope.response_control.elapsed_ms === measured.elapsed_ms;
|
|
49096
|
+
envelope.response_control = {
|
|
49097
|
+
response_byte_limit: bounds.response_byte_limit,
|
|
49098
|
+
time_budget_ms: bounds.time_budget_ms,
|
|
49099
|
+
response_bytes: measured.response_bytes,
|
|
49100
|
+
elapsed_ms: measured.elapsed_ms,
|
|
49101
|
+
complete: true,
|
|
49102
|
+
truncated: false
|
|
49103
|
+
};
|
|
49104
|
+
if (stable3)
|
|
49105
|
+
break;
|
|
49106
|
+
}
|
|
49107
|
+
const finalMeasurement = assertWithinBounds(envelope, bounds, startedAt);
|
|
49108
|
+
envelope.response_control.response_bytes = finalMeasurement.response_bytes;
|
|
49109
|
+
envelope.response_control.elapsed_ms = finalMeasurement.elapsed_ms;
|
|
49110
|
+
return envelope;
|
|
49111
|
+
}
|
|
49112
|
+
function requireString(value, field, options = {}) {
|
|
49113
|
+
const min = options.min ?? 1;
|
|
49114
|
+
const max = options.max ?? 512;
|
|
49115
|
+
if (typeof value !== "string" || value.length < min || value.length > max || /[\u0000-\u001f]/.test(value) || options.pattern && !options.pattern.test(value)) {
|
|
49116
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", `${field} is not a valid bounded registration identifier`);
|
|
49117
|
+
}
|
|
49118
|
+
return value;
|
|
49119
|
+
}
|
|
49120
|
+
function exactKeys(value, expected, field) {
|
|
49121
|
+
const actual = Object.keys(value).sort();
|
|
49122
|
+
const wanted = [...expected].sort();
|
|
49123
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
49124
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", `${field} must contain exactly: ${wanted.join(", ")}`);
|
|
49125
|
+
}
|
|
49126
|
+
}
|
|
49127
|
+
function publicReceipt(row) {
|
|
49128
|
+
const {
|
|
49129
|
+
target_selector: _targetSelector,
|
|
49130
|
+
normalized_call_digest: _normalizedCallDigest,
|
|
49131
|
+
...receipt
|
|
49132
|
+
} = row;
|
|
49133
|
+
return receipt;
|
|
49134
|
+
}
|
|
49135
|
+
function projectRegistrationPath(projectId) {
|
|
49136
|
+
return `hasna-project://${encodeURIComponent(projectId)}`;
|
|
49137
|
+
}
|
|
49138
|
+
function taskListSlug(projectSlug) {
|
|
49139
|
+
const slug = normalizeSlug(projectSlug);
|
|
49140
|
+
if (!slug || slug !== projectSlug) {
|
|
49141
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project_slug must be canonical kebab-case");
|
|
49142
|
+
}
|
|
49143
|
+
return `todos-${slug}`;
|
|
49144
|
+
}
|
|
49145
|
+
function deterministicTaskPrefix(projectSlug) {
|
|
49146
|
+
const letters = projectSlug.replace(/[^a-z0-9]/gi, "").toUpperCase();
|
|
49147
|
+
return (letters.slice(0, 3) || "PRJ").padEnd(3, "X");
|
|
49148
|
+
}
|
|
49149
|
+
function projectRecord(project) {
|
|
49150
|
+
return {
|
|
49151
|
+
target_id: project.id,
|
|
49152
|
+
revision: project.updated_at,
|
|
49153
|
+
digest: digestProjectRegistrationValue({
|
|
49154
|
+
id: project.id,
|
|
49155
|
+
name: project.name,
|
|
49156
|
+
path: project.path,
|
|
49157
|
+
description: project.description,
|
|
49158
|
+
task_list_id: project.task_list_id,
|
|
49159
|
+
task_prefix: project.task_prefix,
|
|
49160
|
+
task_counter: project.task_counter,
|
|
49161
|
+
created_at: project.created_at,
|
|
49162
|
+
updated_at: project.updated_at
|
|
49163
|
+
})
|
|
49164
|
+
};
|
|
49165
|
+
}
|
|
49166
|
+
function taskListRecord(taskList) {
|
|
49167
|
+
return {
|
|
49168
|
+
target_id: taskList.id,
|
|
49169
|
+
revision: taskList.updated_at,
|
|
49170
|
+
digest: digestProjectRegistrationValue({
|
|
49171
|
+
id: taskList.id,
|
|
49172
|
+
project_id: taskList.project_id,
|
|
49173
|
+
slug: taskList.slug,
|
|
49174
|
+
name: taskList.name,
|
|
49175
|
+
description: taskList.description,
|
|
49176
|
+
metadata: taskList.metadata,
|
|
49177
|
+
created_at: taskList.created_at,
|
|
49178
|
+
updated_at: taskList.updated_at
|
|
49179
|
+
})
|
|
49180
|
+
};
|
|
49181
|
+
}
|
|
49182
|
+
function receiptId(input) {
|
|
49183
|
+
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
49184
|
+
}
|
|
49185
|
+
function capabilityMatches(request, capability) {
|
|
49186
|
+
return request.authority_route === capability.route && request.package_version === capability.package_version && request.authority_id === capability.authority_id && request.tenant_id === capability.tenant_id && request.corpus_id === capability.corpus_id;
|
|
49187
|
+
}
|
|
49188
|
+
function authorityScope(capability) {
|
|
49189
|
+
return {
|
|
49190
|
+
authority_id: capability.authority_id,
|
|
49191
|
+
tenant_id: capability.tenant_id,
|
|
49192
|
+
corpus_id: capability.corpus_id
|
|
49193
|
+
};
|
|
49194
|
+
}
|
|
49195
|
+
function assertCapabilityRequest(request, capability) {
|
|
49196
|
+
if (!capabilityMatches(request, capability)) {
|
|
49197
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "registration request does not match this authority capability identity");
|
|
49198
|
+
}
|
|
49199
|
+
}
|
|
49200
|
+
function normalizedCallDigest(request) {
|
|
49201
|
+
return digestProjectRegistrationValue({
|
|
49202
|
+
authority_route: request.authority_route,
|
|
49203
|
+
package_version: request.package_version,
|
|
49204
|
+
authority_id: request.authority_id,
|
|
49205
|
+
tenant_id: request.tenant_id,
|
|
49206
|
+
corpus_id: request.corpus_id,
|
|
49207
|
+
operation_id: request.operation_id,
|
|
49208
|
+
step_id: request.step_id,
|
|
49209
|
+
resource_kind: request.resource_kind,
|
|
49210
|
+
direction: request.direction,
|
|
49211
|
+
target_selector: request.target_selector,
|
|
49212
|
+
idempotency_key: request.idempotency_key,
|
|
49213
|
+
request_digest: request.request_digest,
|
|
49214
|
+
precondition_digest: request.precondition_digest,
|
|
49215
|
+
project_id: request.project_id,
|
|
49216
|
+
project_slug: request.project_slug,
|
|
49217
|
+
project_name: request.project_name,
|
|
49218
|
+
desired: request.desired,
|
|
49219
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
49220
|
+
});
|
|
49221
|
+
}
|
|
49222
|
+
function assertCommonRequest(request, capability) {
|
|
49223
|
+
assertBounds(request);
|
|
49224
|
+
assertResourceKind(request.resource_kind);
|
|
49225
|
+
assertDirection(request.direction);
|
|
49226
|
+
assertCapabilityRequest(request, capability);
|
|
49227
|
+
requireString(request.operation_id, "operation_id", {
|
|
49228
|
+
min: 8,
|
|
49229
|
+
max: 128,
|
|
49230
|
+
pattern: OPERATION_PATTERN
|
|
49231
|
+
});
|
|
49232
|
+
requireString(request.step_id, "step_id", {
|
|
49233
|
+
min: 3,
|
|
49234
|
+
max: 128,
|
|
49235
|
+
pattern: STEP_PATTERN
|
|
49236
|
+
});
|
|
49237
|
+
requireString(request.target_selector, "target_selector", { max: 512 });
|
|
49238
|
+
requireString(request.project_id, "project_id", {
|
|
49239
|
+
min: 16,
|
|
49240
|
+
max: 128,
|
|
49241
|
+
pattern: WORKSPACE_ID_PATTERN
|
|
49242
|
+
});
|
|
49243
|
+
requireString(request.project_name, "project_name", { max: 256 });
|
|
49244
|
+
requireString(request.project_slug, "project_slug", { max: 128 });
|
|
49245
|
+
requireString(request.request_digest, "request_digest", {
|
|
49246
|
+
min: 64,
|
|
49247
|
+
max: 64,
|
|
49248
|
+
pattern: SHA256_PATTERN
|
|
49249
|
+
});
|
|
49250
|
+
requireString(request.precondition_digest, "precondition_digest", {
|
|
49251
|
+
min: 64,
|
|
49252
|
+
max: 64,
|
|
49253
|
+
pattern: SHA256_PATTERN
|
|
49254
|
+
});
|
|
49255
|
+
requireString(request.idempotency_key, "idempotency_key", {
|
|
49256
|
+
min: 52,
|
|
49257
|
+
max: 52,
|
|
49258
|
+
pattern: IDEMPOTENCY_PATTERN
|
|
49259
|
+
});
|
|
49260
|
+
if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
|
|
49261
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
|
|
49262
|
+
}
|
|
49263
|
+
const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
|
|
49264
|
+
operation_id: request.operation_id,
|
|
49265
|
+
step_id: request.step_id,
|
|
49266
|
+
direction: request.direction,
|
|
49267
|
+
target_selector: request.target_selector,
|
|
49268
|
+
request_digest: request.request_digest,
|
|
49269
|
+
precondition_digest: request.precondition_digest
|
|
49270
|
+
});
|
|
49271
|
+
if (request.idempotency_key !== expectedKey) {
|
|
49272
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/direction payload", { expected: expectedKey });
|
|
49273
|
+
}
|
|
49274
|
+
taskListSlug(request.project_slug);
|
|
49275
|
+
}
|
|
49276
|
+
function assertForwardRequest(request, capability) {
|
|
49277
|
+
assertCommonRequest(request, capability);
|
|
49278
|
+
if (request.direction !== "forward") {
|
|
49279
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "create requires direction=forward");
|
|
49280
|
+
}
|
|
49281
|
+
const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
|
|
49282
|
+
const expectedPreconditionDigest = digestProjectRegistrationValue({
|
|
49283
|
+
target_selector: request.target_selector,
|
|
49284
|
+
expected: "absent"
|
|
49285
|
+
});
|
|
49286
|
+
if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
|
|
49287
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
|
|
49288
|
+
expected_request_digest: expectedRequestDigest,
|
|
49289
|
+
expected_precondition_digest: expectedPreconditionDigest
|
|
49290
|
+
});
|
|
49291
|
+
}
|
|
49292
|
+
if (request.resource_kind === "project") {
|
|
49293
|
+
exactKeys(request.desired, ["source_project_id", "source_project_slug", "name"], "project desired");
|
|
49294
|
+
if (request.desired["source_project_id"] !== request.project_id || request.desired["source_project_slug"] !== request.project_slug || request.desired["name"] !== request.project_name || request.target_selector !== request.project_id) {
|
|
49295
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project desired state and target selector must match the complete Projects identity");
|
|
49296
|
+
}
|
|
49297
|
+
return;
|
|
49298
|
+
}
|
|
49299
|
+
if (request.resource_kind === "task_list") {
|
|
49300
|
+
exactKeys(request.desired, ["todos_project_id", "source_project_id", "name"], "task-list desired");
|
|
49301
|
+
const todosProjectId = request.desired["todos_project_id"];
|
|
49302
|
+
if (typeof todosProjectId !== "string" || !UUID_PATTERN.test(todosProjectId)) {
|
|
49303
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_EXACT_ID_REQUIRED", "task-list create requires the exact full Todos project UUID");
|
|
49304
|
+
}
|
|
49305
|
+
if (request.target_selector !== `${todosProjectId}:default` || request.desired["source_project_id"] !== request.project_id || request.desired["name"] !== request.project_name) {
|
|
49306
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "task-list desired state must bind the exact Todos project id and Projects identity");
|
|
49307
|
+
}
|
|
49308
|
+
return;
|
|
49309
|
+
}
|
|
49310
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "unsupported registration resource kind");
|
|
49311
|
+
}
|
|
49312
|
+
function assertInverseRequest(request, capability) {
|
|
49313
|
+
assertCommonRequest(request, capability);
|
|
49314
|
+
if (request.direction !== "inverse") {
|
|
49315
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "compensate requires direction=inverse");
|
|
49316
|
+
}
|
|
49317
|
+
const accepted = request.accepted_receipt;
|
|
49318
|
+
if (!accepted || accepted.authority !== "todos" || accepted.route !== capability.route || accepted.package_version !== capability.package_version || accepted.authority_id !== capability.authority_id || accepted.tenant_id !== capability.tenant_id || accepted.corpus_id !== capability.corpus_id || accepted.operation_id !== request.operation_id || accepted.step_id !== request.step_id || accepted.resource_kind !== request.resource_kind || accepted.direction !== "forward" || accepted.outcome !== "accepted" || !accepted.created_by_operation || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
|
|
49319
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "inverse requires the complete accepted forward receipt created by this operation");
|
|
49320
|
+
}
|
|
49321
|
+
exactKeys(request.desired, ["accepted_receipt_id", "target_id"], "inverse desired");
|
|
49322
|
+
const expectedDesired = {
|
|
49323
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
49324
|
+
target_id: accepted.target_id
|
|
49325
|
+
};
|
|
49326
|
+
const expectedPrecondition = {
|
|
49327
|
+
expected_revision: accepted.result_revision,
|
|
49328
|
+
expected_digest: accepted.result_digest
|
|
49329
|
+
};
|
|
49330
|
+
const expectedRequestDigest = digestProjectRegistrationValue(expectedDesired);
|
|
49331
|
+
const expectedPreconditionDigest = digestProjectRegistrationValue(expectedPrecondition);
|
|
49332
|
+
if (canonicalProjectRegistrationJson(request.desired) !== canonicalProjectRegistrationJson(expectedDesired) || request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest || request.target_selector !== accepted.target_id) {
|
|
49333
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "inverse request does not match the accepted receipt and exact readback precondition");
|
|
49334
|
+
}
|
|
49335
|
+
return accepted;
|
|
49336
|
+
}
|
|
49337
|
+
function makeReceipt(input, createdAt2) {
|
|
49338
|
+
return {
|
|
49339
|
+
...input,
|
|
49340
|
+
receipt_id: receiptId(input),
|
|
49341
|
+
created_at: createdAt2
|
|
49342
|
+
};
|
|
49343
|
+
}
|
|
49344
|
+
function receiptBase(request, callDigest, capability) {
|
|
49345
|
+
return {
|
|
49346
|
+
authority: "todos",
|
|
49347
|
+
route: capability.route,
|
|
49348
|
+
package_version: capability.package_version,
|
|
49349
|
+
authority_id: capability.authority_id,
|
|
49350
|
+
tenant_id: capability.tenant_id,
|
|
49351
|
+
corpus_id: capability.corpus_id,
|
|
49352
|
+
operation_id: request.operation_id,
|
|
49353
|
+
step_id: request.step_id,
|
|
49354
|
+
resource_kind: request.resource_kind,
|
|
49355
|
+
direction: request.direction,
|
|
49356
|
+
target_selector: request.target_selector,
|
|
49357
|
+
idempotency_key: request.idempotency_key,
|
|
49358
|
+
request_digest: request.request_digest,
|
|
49359
|
+
precondition_digest: request.precondition_digest,
|
|
49360
|
+
normalized_call_digest: callDigest
|
|
49361
|
+
};
|
|
49362
|
+
}
|
|
49363
|
+
function makeAcceptedReceipt(request, callDigest, capability, record, createdAt2) {
|
|
49364
|
+
return makeReceipt({
|
|
49365
|
+
...receiptBase(request, callDigest, capability),
|
|
49366
|
+
outcome: "accepted",
|
|
49367
|
+
reason: null,
|
|
49368
|
+
target_id: record.target_id,
|
|
49369
|
+
result_revision: record.revision,
|
|
49370
|
+
result_digest: record.digest,
|
|
49371
|
+
duplicate_of_receipt_id: null,
|
|
49372
|
+
accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
|
|
49373
|
+
created_by_operation: true
|
|
49374
|
+
}, createdAt2);
|
|
49375
|
+
}
|
|
49376
|
+
function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt2) {
|
|
49377
|
+
return makeReceipt({
|
|
49378
|
+
...receiptBase(request, callDigest, capability),
|
|
49379
|
+
outcome: "duplicate_of_accepted",
|
|
49380
|
+
reason: null,
|
|
49381
|
+
target_id: accepted.target_id,
|
|
49382
|
+
result_revision: accepted.result_revision,
|
|
49383
|
+
result_digest: accepted.result_digest,
|
|
49384
|
+
duplicate_of_receipt_id: accepted.receipt_id,
|
|
49385
|
+
accepted_receipt_id: null,
|
|
49386
|
+
created_by_operation: false
|
|
49387
|
+
}, createdAt2);
|
|
49388
|
+
}
|
|
49389
|
+
function makeTerminalReceipt(request, callDigest, capability, reason, createdAt2, options = {}) {
|
|
49390
|
+
return makeReceipt({
|
|
49391
|
+
...receiptBase(request, callDigest, capability),
|
|
49392
|
+
outcome: "terminal_nonacceptance",
|
|
49393
|
+
reason,
|
|
49394
|
+
target_id: options.targetId ?? null,
|
|
49395
|
+
result_revision: null,
|
|
49396
|
+
result_digest: null,
|
|
49397
|
+
duplicate_of_receipt_id: null,
|
|
49398
|
+
accepted_receipt_id: options.acceptedReceiptId ?? null,
|
|
49399
|
+
created_by_operation: false
|
|
49400
|
+
}, createdAt2);
|
|
49401
|
+
}
|
|
49402
|
+
async function insertDeterministicReceipt(transaction, receipt) {
|
|
49403
|
+
if (await transaction.insertReceipt(receipt))
|
|
49404
|
+
return receipt;
|
|
49405
|
+
const existing = await transaction.getReceiptById(receipt.receipt_id);
|
|
49406
|
+
const { created_at: _existingCreatedAt, ...existingContent } = existing ?? {};
|
|
49407
|
+
const { created_at: _receiptCreatedAt, ...receiptContent } = receipt;
|
|
49408
|
+
if (!existing || canonicalProjectRegistrationJson(existingContent) !== canonicalProjectRegistrationJson(receiptContent)) {
|
|
49409
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "deterministic receipt id is occupied by different immutable content", { receipt_id: receipt.receipt_id });
|
|
49410
|
+
}
|
|
49411
|
+
return existing;
|
|
49412
|
+
}
|
|
49413
|
+
function bindingFor(request, callDigest, timestamp4, capability) {
|
|
49414
|
+
return {
|
|
49415
|
+
...authorityScope(capability),
|
|
49416
|
+
resource_kind: request.resource_kind,
|
|
49417
|
+
target_selector: request.target_selector,
|
|
49418
|
+
operation_id: request.operation_id,
|
|
49419
|
+
step_id: request.step_id,
|
|
49420
|
+
direction: "forward",
|
|
49421
|
+
idempotency_key: request.idempotency_key,
|
|
49422
|
+
request_digest: request.request_digest,
|
|
49423
|
+
precondition_digest: request.precondition_digest,
|
|
49424
|
+
normalized_call_digest: callDigest,
|
|
49425
|
+
state: "pending",
|
|
49426
|
+
target_id: null,
|
|
49427
|
+
accepted_receipt_id: null,
|
|
49428
|
+
result_revision: null,
|
|
49429
|
+
result_digest: null,
|
|
49430
|
+
removed_receipt_id: null,
|
|
49431
|
+
created_at: timestamp4,
|
|
49432
|
+
updated_at: timestamp4
|
|
49433
|
+
};
|
|
49434
|
+
}
|
|
49435
|
+
|
|
49436
|
+
class PackageOwnedTodosProjectRegistrationAuthority {
|
|
49437
|
+
backend;
|
|
49438
|
+
authority = "todos";
|
|
49439
|
+
capabilityValue;
|
|
49440
|
+
now;
|
|
49441
|
+
faultInjector;
|
|
49442
|
+
constructor(backend, options = {}) {
|
|
49443
|
+
this.backend = backend;
|
|
49444
|
+
this.capabilityValue = {
|
|
49445
|
+
authority: "todos",
|
|
49446
|
+
route: TODOS_PROJECT_REGISTRATION_ROUTE,
|
|
49447
|
+
package_version: options.packageVersion ?? getPackageVersion(import.meta.url),
|
|
49448
|
+
authority_id: options.authorityId ?? "todos",
|
|
49449
|
+
tenant_id: options.tenantId ?? backend.kind,
|
|
49450
|
+
corpus_id: options.corpusId ?? `todos:${backend.kind}`,
|
|
49451
|
+
supported_resources: ["project", "task_list"],
|
|
49452
|
+
conditional_create: true,
|
|
49453
|
+
immutable_receipts: true,
|
|
49454
|
+
exact_terminal_lookup: true,
|
|
49455
|
+
exact_readback: true,
|
|
49456
|
+
conditional_inverse: true,
|
|
49457
|
+
ambiguous_outcome_reconciliation: true
|
|
49458
|
+
};
|
|
49459
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
49460
|
+
this.faultInjector = options.faultInjector;
|
|
49461
|
+
}
|
|
49462
|
+
async capability() {
|
|
49463
|
+
return {
|
|
49464
|
+
...this.capabilityValue,
|
|
49465
|
+
supported_resources: [...this.capabilityValue.supported_resources]
|
|
49466
|
+
};
|
|
49467
|
+
}
|
|
49468
|
+
async fault(point, request) {
|
|
49469
|
+
if (!this.faultInjector)
|
|
49470
|
+
return;
|
|
49471
|
+
try {
|
|
49472
|
+
await this.faultInjector(point, {
|
|
49473
|
+
operation_id: request.operation_id,
|
|
49474
|
+
step_id: request.step_id,
|
|
49475
|
+
resource_kind: request.resource_kind,
|
|
49476
|
+
direction: request.direction
|
|
49477
|
+
});
|
|
49478
|
+
} catch (cause) {
|
|
49479
|
+
throw new WriteBoundaryError(point, cause);
|
|
49480
|
+
}
|
|
49481
|
+
}
|
|
49482
|
+
async afterCommit(request) {
|
|
49483
|
+
await this.faultInjector?.("after_commit", {
|
|
49484
|
+
operation_id: request.operation_id,
|
|
49485
|
+
step_id: request.step_id,
|
|
49486
|
+
resource_kind: request.resource_kind,
|
|
49487
|
+
direction: request.direction
|
|
49488
|
+
});
|
|
49489
|
+
}
|
|
49490
|
+
async duplicateFor(transaction, request, callDigest, accepted) {
|
|
49491
|
+
const duplicate = makeDuplicateReceipt(request, callDigest, this.capabilityValue, accepted, this.now());
|
|
49492
|
+
return insertDeterministicReceipt(transaction, duplicate);
|
|
49493
|
+
}
|
|
49494
|
+
async terminalFor(transaction, request, callDigest, reason, options = {}) {
|
|
49495
|
+
return insertDeterministicReceipt(transaction, makeTerminalReceipt(request, callDigest, this.capabilityValue, reason, this.now(), options));
|
|
49496
|
+
}
|
|
49497
|
+
async existingForwardResolution(transaction, request, callDigest) {
|
|
49498
|
+
const exact = await transaction.getReceiptForLookup({
|
|
49499
|
+
...authorityScope(this.capabilityValue),
|
|
49500
|
+
operation_id: request.operation_id,
|
|
49501
|
+
step_id: request.step_id,
|
|
49502
|
+
resource_kind: request.resource_kind,
|
|
49503
|
+
direction: request.direction,
|
|
49504
|
+
idempotency_key: request.idempotency_key,
|
|
49505
|
+
target_selector: request.target_selector
|
|
49506
|
+
});
|
|
49507
|
+
if (exact) {
|
|
49508
|
+
if (exact.outcome === "terminal_nonacceptance")
|
|
49509
|
+
return exact;
|
|
49510
|
+
const accepted2 = exact.outcome === "accepted" ? exact : await transaction.getReceiptById(exact.duplicate_of_receipt_id);
|
|
49511
|
+
if (!accepted2) {
|
|
49512
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
|
|
49513
|
+
}
|
|
49514
|
+
if (accepted2.normalized_call_digest !== callDigest) {
|
|
49515
|
+
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
|
|
49516
|
+
}
|
|
49517
|
+
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
49518
|
+
}
|
|
49519
|
+
const accepted = await transaction.getAcceptedReceiptForStep({
|
|
49520
|
+
...authorityScope(this.capabilityValue),
|
|
49521
|
+
operation_id: request.operation_id,
|
|
49522
|
+
step_id: request.step_id,
|
|
49523
|
+
resource_kind: request.resource_kind,
|
|
49524
|
+
direction: "forward"
|
|
49525
|
+
});
|
|
49526
|
+
if (!accepted)
|
|
49527
|
+
return null;
|
|
49528
|
+
if (accepted.normalized_call_digest === callDigest) {
|
|
49529
|
+
return this.duplicateFor(transaction, request, callDigest, accepted);
|
|
49530
|
+
}
|
|
49531
|
+
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
49532
|
+
}
|
|
49533
|
+
async createObject(transaction, request) {
|
|
49534
|
+
if (request.resource_kind === "project") {
|
|
49535
|
+
const path = projectRegistrationPath(request.project_id);
|
|
49536
|
+
const slug2 = taskListSlug(request.project_slug);
|
|
49537
|
+
const conflict2 = await transaction.findProjectConflict(path, slug2);
|
|
49538
|
+
if (conflict2) {
|
|
49539
|
+
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
49540
|
+
}
|
|
49541
|
+
await this.fault("before_object_write", request);
|
|
49542
|
+
const project = await transaction.createProject({
|
|
49543
|
+
name: request.project_name,
|
|
49544
|
+
path,
|
|
49545
|
+
description: `Registered from Projects workspace ${request.project_id}`,
|
|
49546
|
+
task_list_id: slug2,
|
|
49547
|
+
task_prefix: deterministicTaskPrefix(request.project_slug)
|
|
49548
|
+
});
|
|
49549
|
+
await this.fault("after_object_write", request);
|
|
49550
|
+
return projectRecord(project);
|
|
49551
|
+
}
|
|
49552
|
+
const todosProjectId = String(request.desired["todos_project_id"]);
|
|
49553
|
+
const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
|
|
49554
|
+
if (!sourceBinding || sourceBinding.state !== "accepted" || sourceBinding.target_id !== todosProjectId) {
|
|
49555
|
+
return this.terminalFor(transaction, request, normalizedCallDigest(request), "exact_parent_registration_missing", { targetId: todosProjectId });
|
|
49556
|
+
}
|
|
49557
|
+
const parent = await transaction.getProject(todosProjectId);
|
|
49558
|
+
if (!parent) {
|
|
49559
|
+
return this.terminalFor(transaction, request, normalizedCallDigest(request), "exact_parent_project_missing", { targetId: todosProjectId });
|
|
49560
|
+
}
|
|
49561
|
+
const slug = taskListSlug(request.project_slug);
|
|
49562
|
+
const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
|
|
49563
|
+
if (conflict) {
|
|
49564
|
+
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
49565
|
+
}
|
|
49566
|
+
await this.fault("before_object_write", request);
|
|
49567
|
+
const taskList = await transaction.createTaskList({
|
|
49568
|
+
name: request.project_name,
|
|
49569
|
+
slug,
|
|
49570
|
+
project_id: todosProjectId,
|
|
49571
|
+
metadata: {
|
|
49572
|
+
source_project_id: request.project_id,
|
|
49573
|
+
registration_authority: "todos"
|
|
49574
|
+
}
|
|
49575
|
+
});
|
|
49576
|
+
await this.fault("after_object_write", request);
|
|
49577
|
+
if (taskList.project_id !== todosProjectId) {
|
|
49578
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
|
|
49579
|
+
}
|
|
49580
|
+
return taskListRecord(taskList);
|
|
49581
|
+
}
|
|
49582
|
+
async create(request) {
|
|
49583
|
+
const startedAt = Date.now();
|
|
49584
|
+
assertForwardRequest(request, this.capabilityValue);
|
|
49585
|
+
const callDigest = normalizedCallDigest(request);
|
|
49586
|
+
try {
|
|
49587
|
+
const row = await this.backend.transaction(async (transaction) => {
|
|
49588
|
+
await transaction.lockStep({
|
|
49589
|
+
...authorityScope(this.capabilityValue),
|
|
49590
|
+
operation_id: request.operation_id,
|
|
49591
|
+
step_id: request.step_id,
|
|
49592
|
+
resource_kind: request.resource_kind,
|
|
49593
|
+
direction: request.direction
|
|
49594
|
+
});
|
|
49595
|
+
const resolved = await this.existingForwardResolution(transaction, request, callDigest);
|
|
49596
|
+
if (resolved)
|
|
49597
|
+
return resolved;
|
|
49598
|
+
const timestamp4 = this.now();
|
|
49599
|
+
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp4, this.capabilityValue));
|
|
49600
|
+
if (!claimed) {
|
|
49601
|
+
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
|
|
49602
|
+
if (binding?.state === "accepted" && binding.normalized_call_digest === callDigest && binding.accepted_receipt_id) {
|
|
49603
|
+
const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
|
|
49604
|
+
if (accepted2) {
|
|
49605
|
+
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
49606
|
+
}
|
|
49607
|
+
}
|
|
49608
|
+
return this.terminalFor(transaction, request, callDigest, binding?.state === "removed" ? "target_registration_was_removed" : "target_already_registered", { targetId: binding?.target_id ?? null });
|
|
49609
|
+
}
|
|
49610
|
+
const recordOrTerminal = await this.createObject(transaction, request);
|
|
49611
|
+
if ("outcome" in recordOrTerminal) {
|
|
49612
|
+
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
49613
|
+
return recordOrTerminal;
|
|
49614
|
+
}
|
|
49615
|
+
const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal, this.now());
|
|
49616
|
+
await this.fault("before_receipt_write", request);
|
|
49617
|
+
const stored = await insertDeterministicReceipt(transaction, accepted);
|
|
49618
|
+
await this.fault("after_receipt_write", request);
|
|
49619
|
+
await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
|
|
49620
|
+
target_id: recordOrTerminal.target_id,
|
|
49621
|
+
accepted_receipt_id: stored.receipt_id,
|
|
49622
|
+
result_revision: recordOrTerminal.revision,
|
|
49623
|
+
result_digest: recordOrTerminal.digest,
|
|
49624
|
+
updated_at: this.now()
|
|
49625
|
+
});
|
|
49626
|
+
return stored;
|
|
49627
|
+
});
|
|
49628
|
+
await this.afterCommit(request);
|
|
49629
|
+
const receipt = publicReceipt(row);
|
|
49630
|
+
assertWithinBounds(receipt, request, startedAt);
|
|
49631
|
+
return receipt;
|
|
49632
|
+
} catch (error) {
|
|
49633
|
+
if (!(error instanceof WriteBoundaryError))
|
|
49634
|
+
throw error;
|
|
49635
|
+
const terminal = await this.recordWriteFailure(request, callDigest, error.point);
|
|
49636
|
+
const receipt = publicReceipt(terminal);
|
|
49637
|
+
assertWithinBounds(receipt, request, startedAt);
|
|
49638
|
+
return receipt;
|
|
49639
|
+
}
|
|
49640
|
+
}
|
|
49641
|
+
async recordWriteFailure(request, callDigest, point) {
|
|
49642
|
+
return this.backend.transaction(async (transaction) => {
|
|
49643
|
+
await transaction.lockStep({
|
|
49644
|
+
...authorityScope(this.capabilityValue),
|
|
49645
|
+
operation_id: request.operation_id,
|
|
49646
|
+
step_id: request.step_id,
|
|
49647
|
+
resource_kind: request.resource_kind,
|
|
49648
|
+
direction: request.direction
|
|
49649
|
+
});
|
|
49650
|
+
const exact = await transaction.getReceiptForLookup({
|
|
49651
|
+
...authorityScope(this.capabilityValue),
|
|
49652
|
+
operation_id: request.operation_id,
|
|
49653
|
+
step_id: request.step_id,
|
|
49654
|
+
resource_kind: request.resource_kind,
|
|
49655
|
+
direction: request.direction,
|
|
49656
|
+
idempotency_key: request.idempotency_key,
|
|
49657
|
+
target_selector: request.target_selector
|
|
49658
|
+
});
|
|
49659
|
+
if (exact)
|
|
49660
|
+
return exact;
|
|
49661
|
+
const accepted = await transaction.getAcceptedReceiptForStep({
|
|
49662
|
+
...authorityScope(this.capabilityValue),
|
|
49663
|
+
operation_id: request.operation_id,
|
|
49664
|
+
step_id: request.step_id,
|
|
49665
|
+
resource_kind: request.resource_kind,
|
|
49666
|
+
direction: request.direction
|
|
49667
|
+
});
|
|
49668
|
+
if (accepted) {
|
|
49669
|
+
return accepted.normalized_call_digest === callDigest ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
49670
|
+
}
|
|
49671
|
+
const timestamp4 = this.now();
|
|
49672
|
+
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp4, this.capabilityValue));
|
|
49673
|
+
const terminal = await this.terminalFor(transaction, request, callDigest, `write_failed:${point}`);
|
|
49674
|
+
if (claimed) {
|
|
49675
|
+
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
49676
|
+
}
|
|
49677
|
+
return terminal;
|
|
49678
|
+
});
|
|
49679
|
+
}
|
|
49680
|
+
async readExact(request) {
|
|
49681
|
+
const startedAt = Date.now();
|
|
49682
|
+
assertBounds(request);
|
|
49683
|
+
assertResourceKind(request.resource_kind);
|
|
49684
|
+
if (!UUID_PATTERN.test(request.target_id)) {
|
|
49685
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_EXACT_ID_REQUIRED", "exact readback requires a complete Todos object UUID");
|
|
49686
|
+
}
|
|
49687
|
+
const record = request.resource_kind === "project" ? await this.backend.getProject(request.target_id).then((value) => value ? projectRecord(value) : null) : await this.backend.getTaskList(request.target_id).then((value) => value ? taskListRecord(value) : null);
|
|
49688
|
+
if (!record) {
|
|
49689
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", `registered ${request.resource_kind} was not found by exact id`, { target_id: request.target_id });
|
|
49690
|
+
}
|
|
49691
|
+
assertWithinBounds(record, request, startedAt);
|
|
49692
|
+
return record;
|
|
49693
|
+
}
|
|
49694
|
+
async lookupReceipt(request) {
|
|
49695
|
+
const startedAt = Date.now();
|
|
49696
|
+
assertBounds(request);
|
|
49697
|
+
assertResourceKind(request.resource_kind);
|
|
49698
|
+
assertDirection(request.direction);
|
|
49699
|
+
if (request.max_items !== 1) {
|
|
49700
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
|
|
49701
|
+
}
|
|
49702
|
+
if (request.authority !== "todos" || request.authority_route !== this.capabilityValue.route || request.package_version !== this.capabilityValue.package_version || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
|
|
49703
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
|
|
49704
|
+
}
|
|
49705
|
+
requireString(request.operation_id, "operation_id", {
|
|
49706
|
+
min: 8,
|
|
49707
|
+
max: 128,
|
|
49708
|
+
pattern: OPERATION_PATTERN
|
|
49709
|
+
});
|
|
49710
|
+
requireString(request.step_id, "step_id", {
|
|
49711
|
+
min: 3,
|
|
49712
|
+
max: 128,
|
|
49713
|
+
pattern: STEP_PATTERN
|
|
49714
|
+
});
|
|
49715
|
+
requireString(request.target_selector, "target_selector", { max: 512 });
|
|
49716
|
+
requireString(request.idempotency_key, "idempotency_key", {
|
|
49717
|
+
min: 52,
|
|
49718
|
+
max: 52,
|
|
49719
|
+
pattern: IDEMPOTENCY_PATTERN
|
|
49720
|
+
});
|
|
49721
|
+
if (request.target_id !== undefined && !UUID_PATTERN.test(request.target_id)) {
|
|
49722
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_EXACT_ID_REQUIRED", "receipt lookup target_id must be a complete Todos object UUID");
|
|
49723
|
+
}
|
|
49724
|
+
const receipt = await this.backend.getReceiptForLookup({
|
|
49725
|
+
...authorityScope(this.capabilityValue),
|
|
49726
|
+
operation_id: request.operation_id,
|
|
49727
|
+
step_id: request.step_id,
|
|
49728
|
+
resource_kind: request.resource_kind,
|
|
49729
|
+
direction: request.direction,
|
|
49730
|
+
idempotency_key: request.idempotency_key,
|
|
49731
|
+
target_selector: request.target_selector
|
|
49732
|
+
});
|
|
49733
|
+
if (!receipt || request.target_id && receipt.target_id !== request.target_id) {
|
|
49734
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND", "no exact terminal receipt matched the bounded lookup");
|
|
49735
|
+
}
|
|
49736
|
+
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
49737
|
+
}
|
|
49738
|
+
async storedAcceptedReceipt(request, supplied) {
|
|
49739
|
+
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
49740
|
+
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
49741
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ACCEPTED_RECEIPT_NOT_FOUND", "accepted receipt is not an exact immutable receipt owned by this authority", { receipt_id: supplied.receipt_id });
|
|
49742
|
+
}
|
|
49743
|
+
if (stored.operation_id !== request.operation_id || stored.step_id !== request.step_id || stored.resource_kind !== request.resource_kind || stored.target_id !== supplied.target_id) {
|
|
49744
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ACCEPTED_RECEIPT_NOT_FOUND", "accepted receipt does not own this exact operation step and target");
|
|
49745
|
+
}
|
|
49746
|
+
return stored;
|
|
49747
|
+
}
|
|
49748
|
+
async compensate(request) {
|
|
49749
|
+
const startedAt = Date.now();
|
|
49750
|
+
const suppliedAccepted = assertInverseRequest(request, this.capabilityValue);
|
|
49751
|
+
const accepted = await this.storedAcceptedReceipt(request, suppliedAccepted);
|
|
49752
|
+
const callDigest = normalizedCallDigest(request);
|
|
49753
|
+
try {
|
|
49754
|
+
const row = await this.backend.transaction(async (transaction) => {
|
|
49755
|
+
await transaction.lockStep({
|
|
49756
|
+
...authorityScope(this.capabilityValue),
|
|
49757
|
+
operation_id: request.operation_id,
|
|
49758
|
+
step_id: request.step_id,
|
|
49759
|
+
resource_kind: request.resource_kind,
|
|
49760
|
+
direction: request.direction
|
|
49761
|
+
});
|
|
49762
|
+
const exact = await transaction.getReceiptForLookup({
|
|
49763
|
+
...authorityScope(this.capabilityValue),
|
|
49764
|
+
operation_id: request.operation_id,
|
|
49765
|
+
step_id: request.step_id,
|
|
49766
|
+
resource_kind: request.resource_kind,
|
|
49767
|
+
direction: "inverse",
|
|
49768
|
+
idempotency_key: request.idempotency_key,
|
|
49769
|
+
target_selector: request.target_selector
|
|
49770
|
+
});
|
|
49771
|
+
if (exact)
|
|
49772
|
+
return exact;
|
|
49773
|
+
const storedAccepted = await transaction.getReceiptById(accepted.receipt_id);
|
|
49774
|
+
if (!storedAccepted || storedAccepted.outcome !== "accepted" || !storedAccepted.created_by_operation) {
|
|
49775
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ACCEPTED_RECEIPT_NOT_FOUND", "accepted receipt disappeared before conditional inverse");
|
|
49776
|
+
}
|
|
49777
|
+
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), accepted.resource_kind, accepted.target_selector);
|
|
49778
|
+
if (!binding || binding.state !== "accepted" || binding.accepted_receipt_id !== accepted.receipt_id || binding.target_id !== accepted.target_id) {
|
|
49779
|
+
return this.terminalFor(transaction, request, callDigest, "target_not_owned_by_receipt", {
|
|
49780
|
+
targetId: accepted.target_id,
|
|
49781
|
+
acceptedReceiptId: accepted.receipt_id
|
|
49782
|
+
});
|
|
49783
|
+
}
|
|
49784
|
+
await transaction.lockCompensationWrites();
|
|
49785
|
+
const object = request.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
|
|
49786
|
+
if (!object) {
|
|
49787
|
+
return this.terminalFor(transaction, request, callDigest, "target_missing_before_inverse", {
|
|
49788
|
+
targetId: accepted.target_id,
|
|
49789
|
+
acceptedReceiptId: accepted.receipt_id
|
|
49790
|
+
});
|
|
49791
|
+
}
|
|
49792
|
+
const current = request.resource_kind === "project" ? projectRecord(object) : taskListRecord(object);
|
|
49793
|
+
if (current.revision !== accepted.result_revision || current.digest !== accepted.result_digest) {
|
|
49794
|
+
return this.terminalFor(transaction, request, callDigest, "target_drifted", {
|
|
49795
|
+
targetId: accepted.target_id,
|
|
49796
|
+
acceptedReceiptId: accepted.receipt_id
|
|
49797
|
+
});
|
|
49798
|
+
}
|
|
49799
|
+
if (await transaction.hasDependents(request.resource_kind, accepted.target_id)) {
|
|
49800
|
+
return this.terminalFor(transaction, request, callDigest, "target_has_dependents", {
|
|
49801
|
+
targetId: accepted.target_id,
|
|
49802
|
+
acceptedReceiptId: accepted.receipt_id
|
|
49803
|
+
});
|
|
49804
|
+
}
|
|
49805
|
+
await this.fault("before_object_write", request);
|
|
49806
|
+
const deleted = request.resource_kind === "project" ? await transaction.deleteProject(accepted.target_id) : await transaction.deleteTaskList(accepted.target_id);
|
|
49807
|
+
if (!deleted) {
|
|
49808
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "conditional inverse could not delete the exact accepted target");
|
|
49809
|
+
}
|
|
49810
|
+
await this.fault("after_object_write", request);
|
|
49811
|
+
const inverseRecord = {
|
|
49812
|
+
target_id: accepted.target_id,
|
|
49813
|
+
revision: "absent",
|
|
49814
|
+
digest: digestProjectRegistrationValue({
|
|
49815
|
+
target_id: accepted.target_id,
|
|
49816
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
49817
|
+
absent: true
|
|
49818
|
+
})
|
|
49819
|
+
};
|
|
49820
|
+
const inverse = makeAcceptedReceipt(request, callDigest, this.capabilityValue, inverseRecord, this.now());
|
|
49821
|
+
await this.fault("before_receipt_write", request);
|
|
49822
|
+
const stored = await insertDeterministicReceipt(transaction, inverse);
|
|
49823
|
+
await this.fault("after_receipt_write", request);
|
|
49824
|
+
await transaction.setBindingRemoved(authorityScope(this.capabilityValue), accepted.resource_kind, accepted.target_selector, stored.receipt_id, this.now());
|
|
49825
|
+
return stored;
|
|
49826
|
+
});
|
|
49827
|
+
await this.afterCommit(request);
|
|
49828
|
+
const receipt = publicReceipt(row);
|
|
49829
|
+
assertWithinBounds(receipt, request, startedAt);
|
|
49830
|
+
return receipt;
|
|
49831
|
+
} catch (error) {
|
|
49832
|
+
if (!(error instanceof WriteBoundaryError))
|
|
49833
|
+
throw error;
|
|
49834
|
+
const terminal = await this.backend.transaction(async (transaction) => {
|
|
49835
|
+
await transaction.lockStep({
|
|
49836
|
+
...authorityScope(this.capabilityValue),
|
|
49837
|
+
operation_id: request.operation_id,
|
|
49838
|
+
step_id: request.step_id,
|
|
49839
|
+
resource_kind: request.resource_kind,
|
|
49840
|
+
direction: request.direction
|
|
49841
|
+
});
|
|
49842
|
+
return this.terminalFor(transaction, request, callDigest, `write_failed:${error.point}`, {
|
|
49843
|
+
targetId: accepted.target_id,
|
|
49844
|
+
acceptedReceiptId: accepted.receipt_id
|
|
49845
|
+
});
|
|
49846
|
+
});
|
|
49847
|
+
const receipt = publicReceipt(terminal);
|
|
49848
|
+
assertWithinBounds(receipt, request, startedAt);
|
|
49849
|
+
return receipt;
|
|
49850
|
+
}
|
|
49851
|
+
}
|
|
49852
|
+
async verifyInverse(request) {
|
|
49853
|
+
const startedAt = Date.now();
|
|
49854
|
+
const accepted = assertInverseRequest(request, this.capabilityValue);
|
|
49855
|
+
await this.storedAcceptedReceipt(request, accepted);
|
|
49856
|
+
const receipt = await this.backend.getReceiptForLookup({
|
|
49857
|
+
...authorityScope(this.capabilityValue),
|
|
49858
|
+
operation_id: request.operation_id,
|
|
49859
|
+
step_id: request.step_id,
|
|
49860
|
+
resource_kind: request.resource_kind,
|
|
49861
|
+
direction: "inverse",
|
|
49862
|
+
idempotency_key: request.idempotency_key,
|
|
49863
|
+
target_selector: request.target_selector
|
|
49864
|
+
});
|
|
49865
|
+
if (!receipt || receipt.outcome !== "accepted" || receipt.accepted_receipt_id !== accepted.receipt_id || receipt.result_revision !== "absent" || !receipt.result_digest) {
|
|
49866
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND", "accepted conditional inverse receipt was not found");
|
|
49867
|
+
}
|
|
49868
|
+
const object = request.resource_kind === "project" ? await this.backend.getProject(accepted.target_id) : await this.backend.getTaskList(accepted.target_id);
|
|
49869
|
+
if (object) {
|
|
49870
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "inverse verification found the accepted target still present");
|
|
49871
|
+
}
|
|
49872
|
+
const verification = {
|
|
49873
|
+
target_id: accepted.target_id,
|
|
49874
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
49875
|
+
absent: true,
|
|
49876
|
+
digest: digestProjectRegistrationValue({
|
|
49877
|
+
target_id: accepted.target_id,
|
|
49878
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
49879
|
+
absent: true
|
|
49880
|
+
})
|
|
49881
|
+
};
|
|
49882
|
+
assertWithinBounds(verification, request, startedAt);
|
|
49883
|
+
if (verification.digest !== receipt.result_digest) {
|
|
49884
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "inverse verification digest does not match the immutable receipt");
|
|
49885
|
+
}
|
|
49886
|
+
return verification;
|
|
49887
|
+
}
|
|
49888
|
+
}
|
|
49889
|
+
function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
49890
|
+
const {
|
|
49891
|
+
service,
|
|
49892
|
+
tableName,
|
|
49893
|
+
cursorTableName,
|
|
49894
|
+
...authorityOptions
|
|
49895
|
+
} = options;
|
|
49896
|
+
return new PackageOwnedTodosProjectRegistrationAuthority(new PostgresTodosProjectRegistrationBackend(client, {
|
|
49897
|
+
service,
|
|
49898
|
+
tableName,
|
|
49899
|
+
cursorTableName
|
|
49900
|
+
}), authorityOptions);
|
|
49901
|
+
}
|
|
49902
|
+
var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, WriteBoundaryError;
|
|
49903
|
+
var init_authority = __esm(() => {
|
|
49904
|
+
init_package_version();
|
|
49905
|
+
init_postgres2();
|
|
49906
|
+
init_sqlite();
|
|
49907
|
+
init_types4();
|
|
49908
|
+
UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
49909
|
+
WORKSPACE_ID_PATTERN = /^wks_[A-Za-z0-9][A-Za-z0-9_-]{11,}$/;
|
|
49910
|
+
OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
49911
|
+
STEP_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
49912
|
+
SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
49913
|
+
IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
|
|
49914
|
+
WriteBoundaryError = class WriteBoundaryError extends Error {
|
|
49915
|
+
point;
|
|
49916
|
+
cause;
|
|
49917
|
+
constructor(point, cause) {
|
|
49918
|
+
super(`Todos project registration failed at ${point}`);
|
|
49919
|
+
this.point = point;
|
|
49920
|
+
this.cause = cause;
|
|
49921
|
+
}
|
|
49922
|
+
};
|
|
49923
|
+
});
|
|
49924
|
+
|
|
49925
|
+
// src/project-registration/http.ts
|
|
49926
|
+
function json(body, status = 200) {
|
|
49927
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
49928
|
+
}
|
|
49929
|
+
function errorStatus(error) {
|
|
49930
|
+
switch (error.code) {
|
|
49931
|
+
case "TODOS_PROJECT_REGISTRATION_INVALID_INPUT":
|
|
49932
|
+
case "TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS":
|
|
49933
|
+
case "TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH":
|
|
49934
|
+
case "TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH":
|
|
49935
|
+
case "TODOS_PROJECT_REGISTRATION_IDEMPOTENCY_MISMATCH":
|
|
49936
|
+
case "TODOS_PROJECT_REGISTRATION_EXACT_ID_REQUIRED":
|
|
49937
|
+
return 400;
|
|
49938
|
+
case "TODOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND":
|
|
49939
|
+
case "TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND":
|
|
49940
|
+
case "TODOS_PROJECT_REGISTRATION_ACCEPTED_RECEIPT_NOT_FOUND":
|
|
49941
|
+
return 404;
|
|
49942
|
+
case "TODOS_PROJECT_REGISTRATION_RESPONSE_TOO_LARGE":
|
|
49943
|
+
return 413;
|
|
49944
|
+
case "TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED":
|
|
49945
|
+
return 408;
|
|
49946
|
+
case "TODOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE":
|
|
49947
|
+
return 503;
|
|
49948
|
+
default:
|
|
49949
|
+
return 409;
|
|
49950
|
+
}
|
|
49951
|
+
}
|
|
49952
|
+
async function readJson(req) {
|
|
49953
|
+
try {
|
|
49954
|
+
const value = await req.json();
|
|
49955
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
49956
|
+
} catch {
|
|
49957
|
+
return null;
|
|
49958
|
+
}
|
|
49959
|
+
}
|
|
49960
|
+
async function handleTodosProjectRegistrationHttpRequest(req, url, authority, basePath = "/v1/project-registration") {
|
|
49961
|
+
const path = url.pathname;
|
|
49962
|
+
if (path !== basePath && !path.startsWith(`${basePath}/`))
|
|
49963
|
+
return null;
|
|
49964
|
+
const action = path.slice(basePath.length).split("/").filter(Boolean).join("/");
|
|
49965
|
+
const method = req.method.toUpperCase();
|
|
49966
|
+
try {
|
|
49967
|
+
if ((action === "" || action === "capability") && method === "GET") {
|
|
49968
|
+
return json({ capability: await authority.capability() });
|
|
49969
|
+
}
|
|
49970
|
+
if (method !== "POST")
|
|
49971
|
+
return json({ error: "method not allowed" }, 405);
|
|
49972
|
+
const body = await readJson(req);
|
|
49973
|
+
if (!body) {
|
|
49974
|
+
return json({
|
|
49975
|
+
error: "invalid JSON body",
|
|
49976
|
+
code: "TODOS_PROJECT_REGISTRATION_INVALID_INPUT"
|
|
49977
|
+
}, 400);
|
|
49978
|
+
}
|
|
49979
|
+
if (action === "create") {
|
|
49980
|
+
return json({
|
|
49981
|
+
receipt: await authority.create(body)
|
|
49982
|
+
}, 201);
|
|
49983
|
+
}
|
|
49984
|
+
if (action === "receipts/lookup") {
|
|
49985
|
+
return json(await authority.lookupReceipt(body));
|
|
49986
|
+
}
|
|
49987
|
+
if (action === "read-exact") {
|
|
49988
|
+
return json({
|
|
49989
|
+
record: await authority.readExact(body)
|
|
49990
|
+
});
|
|
49991
|
+
}
|
|
49992
|
+
if (action === "compensate") {
|
|
49993
|
+
return json({
|
|
49994
|
+
receipt: await authority.compensate(body)
|
|
49995
|
+
}, 201);
|
|
49996
|
+
}
|
|
49997
|
+
if (action === "verify-inverse") {
|
|
49998
|
+
return json({
|
|
49999
|
+
verification: await authority.verifyInverse(body)
|
|
50000
|
+
});
|
|
50001
|
+
}
|
|
50002
|
+
return json({
|
|
50003
|
+
error: "unknown Todos project-registration route",
|
|
50004
|
+
code: "TODOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND"
|
|
50005
|
+
}, 404);
|
|
50006
|
+
} catch (cause) {
|
|
50007
|
+
if (cause instanceof TodosProjectRegistrationError) {
|
|
50008
|
+
return json({
|
|
50009
|
+
error: cause.message,
|
|
50010
|
+
code: cause.code,
|
|
50011
|
+
details: cause.details,
|
|
50012
|
+
authoritative: true
|
|
50013
|
+
}, errorStatus(cause));
|
|
50014
|
+
}
|
|
50015
|
+
return json({
|
|
50016
|
+
error: cause instanceof Error ? cause.message : "internal registration error",
|
|
50017
|
+
code: "TODOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE"
|
|
50018
|
+
}, 500);
|
|
50019
|
+
}
|
|
50020
|
+
}
|
|
50021
|
+
var JSON_HEADERS;
|
|
50022
|
+
var init_http2 = __esm(() => {
|
|
50023
|
+
init_types4();
|
|
50024
|
+
JSON_HEADERS = { "Content-Type": "application/json" };
|
|
50025
|
+
});
|
|
50026
|
+
|
|
50027
|
+
// src/project-registration/index.ts
|
|
50028
|
+
var init_project_registration = __esm(() => {
|
|
50029
|
+
init_authority();
|
|
50030
|
+
init_http2();
|
|
50031
|
+
init_postgres2();
|
|
50032
|
+
init_sqlite();
|
|
50033
|
+
init_types4();
|
|
50034
|
+
});
|
|
50035
|
+
|
|
47726
50036
|
// src/storage/comment-redaction-backfill.ts
|
|
47727
50037
|
function assertSafeIdentifier2(value) {
|
|
47728
50038
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
@@ -47831,6 +50141,7 @@ __export(exports_cloud, {
|
|
|
47831
50141
|
isCloudModeEnabled: () => isCloudModeEnabled,
|
|
47832
50142
|
getCloudVerifier: () => getCloudVerifier,
|
|
47833
50143
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
50144
|
+
getCloudProjectRegistrationAuthority: () => getCloudProjectRegistrationAuthority,
|
|
47834
50145
|
getCloudPrGroupLedger: () => getCloudPrGroupLedger,
|
|
47835
50146
|
getApiKeyStore: () => getApiKeyStore,
|
|
47836
50147
|
ensureCloudTaskShortIdIndex: () => ensureCloudTaskShortIdIndex,
|
|
@@ -47879,6 +50190,17 @@ function getCloudPrGroupLedger() {
|
|
|
47879
50190
|
cachedPrGroupLedger = new PrGroupLedger(new PostgresPrGroupLedgerPersistence(getClient()));
|
|
47880
50191
|
return cachedPrGroupLedger;
|
|
47881
50192
|
}
|
|
50193
|
+
function getCloudProjectRegistrationAuthority() {
|
|
50194
|
+
if (cachedProjectRegistrationAuthority)
|
|
50195
|
+
return cachedProjectRegistrationAuthority;
|
|
50196
|
+
cachedProjectRegistrationAuthority = createPostgresTodosProjectRegistrationAuthority(getClient(), {
|
|
50197
|
+
service: TODOS_APP_SLUG,
|
|
50198
|
+
authorityId: TODOS_APP_SLUG,
|
|
50199
|
+
tenantId: process.env.HASNA_TODOS_TENANT_ID ?? "default",
|
|
50200
|
+
corpusId: process.env.HASNA_TODOS_CORPUS_ID ?? `${TODOS_APP_SLUG}:postgresql`
|
|
50201
|
+
});
|
|
50202
|
+
return cachedProjectRegistrationAuthority;
|
|
50203
|
+
}
|
|
47882
50204
|
function authClient() {
|
|
47883
50205
|
const client = getClient();
|
|
47884
50206
|
return {
|
|
@@ -47927,6 +50249,9 @@ async function ensureCloudSchema() {
|
|
|
47927
50249
|
for (const sql of postgresPrGroupSchemaSql()) {
|
|
47928
50250
|
await client.query(sql);
|
|
47929
50251
|
}
|
|
50252
|
+
for (const sql of postgresTodosProjectRegistrationSchemaSql()) {
|
|
50253
|
+
await client.query(sql);
|
|
50254
|
+
}
|
|
47930
50255
|
await getApiKeyStore().ensureSchema();
|
|
47931
50256
|
})();
|
|
47932
50257
|
return schemaEnsured;
|
|
@@ -47967,14 +50292,16 @@ async function closeCloud() {
|
|
|
47967
50292
|
cachedStore = null;
|
|
47968
50293
|
cachedVerifier = null;
|
|
47969
50294
|
cachedPrGroupLedger = null;
|
|
50295
|
+
cachedProjectRegistrationAuthority = null;
|
|
47970
50296
|
schemaEnsured = null;
|
|
47971
50297
|
}
|
|
47972
|
-
var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, schemaEnsured = null;
|
|
50298
|
+
var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, schemaEnsured = null;
|
|
47973
50299
|
var init_cloud = __esm(() => {
|
|
47974
50300
|
init_cloud_client();
|
|
47975
50301
|
init_postgres_adapter();
|
|
47976
50302
|
init_ledger();
|
|
47977
50303
|
init_postgres();
|
|
50304
|
+
init_project_registration();
|
|
47978
50305
|
init_postgres_sync();
|
|
47979
50306
|
init_comment_redaction_backfill();
|
|
47980
50307
|
});
|
|
@@ -48333,9 +50660,9 @@ function parseBoundedLimit(value, fallback, max) {
|
|
|
48333
50660
|
return fallback;
|
|
48334
50661
|
return Math.min(parsed, max);
|
|
48335
50662
|
}
|
|
48336
|
-
function mapTaskError(e,
|
|
50663
|
+
function mapTaskError(e, json3) {
|
|
48337
50664
|
if (e instanceof VersionConflictError) {
|
|
48338
|
-
return
|
|
50665
|
+
return json3({
|
|
48339
50666
|
error: e.message,
|
|
48340
50667
|
code: VersionConflictError.code,
|
|
48341
50668
|
expected_version: e.expectedVersion,
|
|
@@ -48343,23 +50670,23 @@ function mapTaskError(e, json2) {
|
|
|
48343
50670
|
}, 409);
|
|
48344
50671
|
}
|
|
48345
50672
|
if (e instanceof TaskNotFoundError) {
|
|
48346
|
-
return
|
|
50673
|
+
return json3({ error: e.message, code: TaskNotFoundError.code }, 404);
|
|
48347
50674
|
}
|
|
48348
50675
|
if (e instanceof LockError) {
|
|
48349
|
-
return
|
|
50676
|
+
return json3({ error: e.message, code: LockError.code }, 409);
|
|
48350
50677
|
}
|
|
48351
50678
|
if (e instanceof CompletionGuardError) {
|
|
48352
|
-
return
|
|
50679
|
+
return json3({
|
|
48353
50680
|
error: e.message,
|
|
48354
50681
|
code: CompletionGuardError.code,
|
|
48355
50682
|
retry_after: e.retryAfterSeconds ?? null
|
|
48356
50683
|
}, 409);
|
|
48357
50684
|
}
|
|
48358
50685
|
if (e instanceof TaskNotStartableError) {
|
|
48359
|
-
return
|
|
50686
|
+
return json3({ error: e.message, code: TaskNotStartableError.code }, 409);
|
|
48360
50687
|
}
|
|
48361
50688
|
if (e instanceof Error && / is blocked by /.test(e.message)) {
|
|
48362
|
-
return
|
|
50689
|
+
return json3({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
|
|
48363
50690
|
}
|
|
48364
50691
|
return null;
|
|
48365
50692
|
}
|
|
@@ -48444,11 +50771,11 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
48444
50771
|
}
|
|
48445
50772
|
});
|
|
48446
50773
|
}
|
|
48447
|
-
function handleHealth(_ctx,
|
|
50774
|
+
function handleHealth(_ctx, json3) {
|
|
48448
50775
|
const stats2 = getTaskStats();
|
|
48449
50776
|
const staleCount = getStaleTasks(30).length;
|
|
48450
50777
|
const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
|
|
48451
|
-
return
|
|
50778
|
+
return json3({
|
|
48452
50779
|
status: staleCount === 0 && overdueRecurring === 0 ? "ok" : "warn",
|
|
48453
50780
|
tasks: stats2.total,
|
|
48454
50781
|
stale: staleCount,
|
|
@@ -48456,18 +50783,18 @@ function handleHealth(_ctx, json2) {
|
|
|
48456
50783
|
timestamp: new Date().toISOString()
|
|
48457
50784
|
});
|
|
48458
50785
|
}
|
|
48459
|
-
function handleHeadlessBoundary(_ctx,
|
|
50786
|
+
function handleHeadlessBoundary(_ctx, json3) {
|
|
48460
50787
|
const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
|
|
48461
|
-
return
|
|
50788
|
+
return json3(getHeadlessBoundaryManifest2());
|
|
48462
50789
|
}
|
|
48463
|
-
function handleStats(_ctx,
|
|
50790
|
+
function handleStats(_ctx, json3) {
|
|
48464
50791
|
const stats2 = getTaskStats();
|
|
48465
50792
|
const byStatus = stats2.by_status;
|
|
48466
50793
|
const projects = listProjects();
|
|
48467
50794
|
const agents = listAgents();
|
|
48468
50795
|
const staleCount = getStaleTasks(30).length;
|
|
48469
50796
|
const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
|
|
48470
|
-
return
|
|
50797
|
+
return json3({
|
|
48471
50798
|
total_tasks: stats2.total,
|
|
48472
50799
|
pending: byStatus["pending"] ?? 0,
|
|
48473
50800
|
in_progress: byStatus["in_progress"] ?? 0,
|
|
@@ -48490,10 +50817,10 @@ function taskStatusQueryParam(url) {
|
|
|
48490
50817
|
return { ok: false, message: result.message };
|
|
48491
50818
|
return { ok: true, value: collapseEnumValues(result.values) };
|
|
48492
50819
|
}
|
|
48493
|
-
async function handleListTasks(_req, url, _ctx,
|
|
50820
|
+
async function handleListTasks(_req, url, _ctx, json3, taskToSummary2) {
|
|
48494
50821
|
const statusParam = taskStatusQueryParam(url);
|
|
48495
50822
|
if (!statusParam.ok)
|
|
48496
|
-
return
|
|
50823
|
+
return json3({ error: statusParam.message }, 400);
|
|
48497
50824
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
48498
50825
|
const sessionId = url.searchParams.get("session_id") || undefined;
|
|
48499
50826
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
@@ -48508,13 +50835,13 @@ async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
|
|
|
48508
50835
|
limit: limitParam ? parseInt(limitParam, 10) : undefined,
|
|
48509
50836
|
offset: offsetParam ? parseInt(offsetParam, 10) : undefined
|
|
48510
50837
|
});
|
|
48511
|
-
return
|
|
50838
|
+
return json3(tasks.map((t) => taskToSummary2(t, fields)));
|
|
48512
50839
|
}
|
|
48513
|
-
async function handleCreateTask(req, ctx,
|
|
50840
|
+
async function handleCreateTask(req, ctx, json3, taskToSummary2) {
|
|
48514
50841
|
try {
|
|
48515
50842
|
const body = await req.json();
|
|
48516
50843
|
if (!body.title)
|
|
48517
|
-
return
|
|
50844
|
+
return json3({ error: "Missing 'title'" }, 400);
|
|
48518
50845
|
const createdBy = body.created_by ?? body.agent_id ?? "dashboard";
|
|
48519
50846
|
const task2 = createTask({
|
|
48520
50847
|
title: body.title,
|
|
@@ -48526,19 +50853,19 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
|
|
|
48526
50853
|
...body.assigned_to ? { assigned_to: body.assigned_to } : {}
|
|
48527
50854
|
});
|
|
48528
50855
|
ctx.broadcastEvent({ type: "task", task_id: task2.id, action: "created", agent_id: task2.agent_id, project_id: task2.project_id });
|
|
48529
|
-
return
|
|
50856
|
+
return json3(taskToSummary2(task2), 201);
|
|
48530
50857
|
} catch (e) {
|
|
48531
|
-
return
|
|
50858
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
|
|
48532
50859
|
}
|
|
48533
50860
|
}
|
|
48534
|
-
async function handleUpsertTask(req, ctx,
|
|
50861
|
+
async function handleUpsertTask(req, ctx, json3, taskToSummary2) {
|
|
48535
50862
|
try {
|
|
48536
50863
|
const body = await req.json();
|
|
48537
50864
|
if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
|
|
48538
|
-
return
|
|
50865
|
+
return json3({ error: "Missing 'fingerprint'" }, 400);
|
|
48539
50866
|
}
|
|
48540
50867
|
if (typeof body["title"] !== "string" || body["title"].trim() === "") {
|
|
48541
|
-
return
|
|
50868
|
+
return json3({ error: "Missing 'title'" }, 400);
|
|
48542
50869
|
}
|
|
48543
50870
|
const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
|
|
48544
50871
|
for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
|
|
@@ -48559,9 +50886,9 @@ async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
|
|
|
48559
50886
|
metadata
|
|
48560
50887
|
});
|
|
48561
50888
|
ctx.broadcastEvent({ type: "task", task_id: result.task.id, action: result.created ? "created" : "updated", agent_id: result.task.agent_id, project_id: result.task.project_id });
|
|
48562
|
-
return
|
|
50889
|
+
return json3({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
|
|
48563
50890
|
} catch (e) {
|
|
48564
|
-
return
|
|
50891
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
|
|
48565
50892
|
}
|
|
48566
50893
|
}
|
|
48567
50894
|
function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
@@ -48613,11 +50940,11 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
|
48613
50940
|
}
|
|
48614
50941
|
});
|
|
48615
50942
|
}
|
|
48616
|
-
async function handleTasksBulk(req, _ctx,
|
|
50943
|
+
async function handleTasksBulk(req, _ctx, json3) {
|
|
48617
50944
|
try {
|
|
48618
50945
|
const body = await req.json();
|
|
48619
50946
|
if (!body.ids?.length || !body.action)
|
|
48620
|
-
return
|
|
50947
|
+
return json3({ error: "Missing ids or action" }, 400);
|
|
48621
50948
|
const results = [];
|
|
48622
50949
|
for (const id of body.ids) {
|
|
48623
50950
|
try {
|
|
@@ -48635,66 +50962,66 @@ async function handleTasksBulk(req, _ctx, json2) {
|
|
|
48635
50962
|
results.push({ id, success: false, error: e instanceof Error ? e.message : "Failed" });
|
|
48636
50963
|
}
|
|
48637
50964
|
}
|
|
48638
|
-
return
|
|
50965
|
+
return json3({ results, succeeded: results.filter((r) => r.success).length, failed: results.filter((r) => !r.success).length });
|
|
48639
50966
|
} catch (e) {
|
|
48640
|
-
return
|
|
50967
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
48641
50968
|
}
|
|
48642
50969
|
}
|
|
48643
|
-
function handleTasksStatus(_req, url, _ctx,
|
|
50970
|
+
function handleTasksStatus(_req, url, _ctx, json3) {
|
|
48644
50971
|
try {
|
|
48645
50972
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
48646
50973
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
48647
50974
|
const status = getStatus(projectId ? { project_id: projectId } : undefined, agentId);
|
|
48648
|
-
return
|
|
50975
|
+
return json3(status);
|
|
48649
50976
|
} catch (e) {
|
|
48650
|
-
return
|
|
50977
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
48651
50978
|
}
|
|
48652
50979
|
}
|
|
48653
|
-
function handleTasksNext(_req, url, _ctx,
|
|
50980
|
+
function handleTasksNext(_req, url, _ctx, json3, taskToSummary2) {
|
|
48654
50981
|
try {
|
|
48655
50982
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
48656
50983
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
48657
50984
|
const fields = parseFieldsParam(url);
|
|
48658
50985
|
const task2 = getNextTask(agentId, projectId ? { project_id: projectId } : undefined);
|
|
48659
|
-
return
|
|
50986
|
+
return json3({ task: task2 ? taskToSummary2(task2, fields) : null });
|
|
48660
50987
|
} catch (e) {
|
|
48661
|
-
return
|
|
50988
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
48662
50989
|
}
|
|
48663
50990
|
}
|
|
48664
|
-
function handleTasksActive(_req, url, _ctx,
|
|
50991
|
+
function handleTasksActive(_req, url, _ctx, json3) {
|
|
48665
50992
|
try {
|
|
48666
50993
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
48667
50994
|
const work = getActiveWork(projectId ? { project_id: projectId } : undefined);
|
|
48668
|
-
return
|
|
50995
|
+
return json3({ active: work, count: work.length });
|
|
48669
50996
|
} catch (e) {
|
|
48670
|
-
return
|
|
50997
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
48671
50998
|
}
|
|
48672
50999
|
}
|
|
48673
|
-
function handleTasksStale(_req, url, _ctx,
|
|
51000
|
+
function handleTasksStale(_req, url, _ctx, json3, taskToSummary2) {
|
|
48674
51001
|
try {
|
|
48675
51002
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
48676
51003
|
const minutes2 = parseInt(url.searchParams.get("minutes") || "30", 10);
|
|
48677
51004
|
const fields = parseFieldsParam(url);
|
|
48678
51005
|
const tasks = getStaleTasks(minutes2, projectId ? { project_id: projectId } : undefined);
|
|
48679
|
-
return
|
|
51006
|
+
return json3({ tasks: tasks.map((t) => taskToSummary2(t, fields)), count: tasks.length });
|
|
48680
51007
|
} catch (e) {
|
|
48681
|
-
return
|
|
51008
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
48682
51009
|
}
|
|
48683
51010
|
}
|
|
48684
|
-
function handleTasksChanged(_req, url, _ctx,
|
|
51011
|
+
function handleTasksChanged(_req, url, _ctx, json3, taskToSummary2) {
|
|
48685
51012
|
try {
|
|
48686
51013
|
const since = url.searchParams.get("since");
|
|
48687
51014
|
if (!since)
|
|
48688
|
-
return
|
|
51015
|
+
return json3({ error: "since parameter required (ISO date string)" }, 400);
|
|
48689
51016
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
48690
51017
|
const fields = parseFieldsParam(url);
|
|
48691
51018
|
const tasks = getTasksChangedSince(since, projectId ? { project_id: projectId } : undefined);
|
|
48692
|
-
return
|
|
51019
|
+
return json3({ tasks: tasks.map((t) => taskToSummary2(t, fields)), count: tasks.length, since });
|
|
48693
51020
|
} catch (e) {
|
|
48694
|
-
return
|
|
51021
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
48695
51022
|
}
|
|
48696
51023
|
}
|
|
48697
|
-
function handleTasksContext(_req, url, _ctx,
|
|
51024
|
+
function handleTasksContext(_req, url, _ctx, json3, taskToSummary2) {
|
|
48698
51025
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
48699
51026
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
48700
51027
|
const format = url.searchParams.get("format") || "text";
|
|
@@ -48703,7 +51030,7 @@ function handleTasksContext(_req, url, _ctx, json2, taskToSummary2) {
|
|
|
48703
51030
|
const status = getStatus(filters, agentId);
|
|
48704
51031
|
const next = getNextTask(agentId, filters);
|
|
48705
51032
|
if (format === "json") {
|
|
48706
|
-
return
|
|
51033
|
+
return json3({ status, next_task: next ? taskToSummary2(next, fields) : null });
|
|
48707
51034
|
}
|
|
48708
51035
|
const lines = [];
|
|
48709
51036
|
lines.push(`Tasks: ${status.pending} pending | ${status.in_progress} active | ${status.completed} done`);
|
|
@@ -48720,18 +51047,18 @@ function handleTasksContext(_req, url, _ctx, json2, taskToSummary2) {
|
|
|
48720
51047
|
`);
|
|
48721
51048
|
return new Response(text2, { headers: { "Content-Type": "text/plain" } });
|
|
48722
51049
|
}
|
|
48723
|
-
function handleTaskAttachments(id, _ctx,
|
|
51050
|
+
function handleTaskAttachments(id, _ctx, json3) {
|
|
48724
51051
|
const task2 = getTask(id);
|
|
48725
51052
|
if (!task2)
|
|
48726
|
-
return
|
|
51053
|
+
return json3({ error: "Task not found" }, 404);
|
|
48727
51054
|
const evidence = task2.metadata?._evidence || {};
|
|
48728
51055
|
const attachmentIds = evidence.attachments || [];
|
|
48729
|
-
return
|
|
51056
|
+
return json3({ task_id: id, short_id: task2.short_id, attachment_ids: attachmentIds, count: attachmentIds.length, files_changed: evidence.files_changed, commit_hash: evidence.commit_hash, notes: evidence.notes });
|
|
48730
51057
|
}
|
|
48731
|
-
async function handleTaskProgress(id, req, method, _ctx,
|
|
51058
|
+
async function handleTaskProgress(id, req, method, _ctx, json3, url) {
|
|
48732
51059
|
const task2 = getTask(id);
|
|
48733
51060
|
if (!task2)
|
|
48734
|
-
return
|
|
51061
|
+
return json3({ error: "Task not found" }, 404);
|
|
48735
51062
|
if (method === "GET") {
|
|
48736
51063
|
const all = listComments(id);
|
|
48737
51064
|
const progress = all.filter((c) => c.type === "progress");
|
|
@@ -48739,7 +51066,7 @@ async function handleTaskProgress(id, req, method, _ctx, json2, url) {
|
|
|
48739
51066
|
const format = url?.searchParams.get("format") || "compact";
|
|
48740
51067
|
const limit = parseBoundedLimit(url?.searchParams.get("limit") || null, 20, 200);
|
|
48741
51068
|
const progressEntries = format === "full" ? progress : progress.slice(-limit);
|
|
48742
|
-
return
|
|
51069
|
+
return json3({
|
|
48743
51070
|
task_id: id,
|
|
48744
51071
|
progress_entries: progressEntries,
|
|
48745
51072
|
latest,
|
|
@@ -48756,27 +51083,27 @@ async function handleTaskProgress(id, req, method, _ctx, json2, url) {
|
|
|
48756
51083
|
try {
|
|
48757
51084
|
const body = await req.json();
|
|
48758
51085
|
if (!body.message)
|
|
48759
|
-
return
|
|
51086
|
+
return json3({ error: "message required" }, 400);
|
|
48760
51087
|
const comment = logProgress(id, body.message, body.pct_complete, body.agent_id);
|
|
48761
|
-
return
|
|
51088
|
+
return json3(comment, 201);
|
|
48762
51089
|
} catch (e) {
|
|
48763
|
-
return
|
|
51090
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to log progress" }, 500);
|
|
48764
51091
|
}
|
|
48765
51092
|
}
|
|
48766
51093
|
return null;
|
|
48767
51094
|
}
|
|
48768
|
-
function handleGetTask(id, _ctx,
|
|
51095
|
+
function handleGetTask(id, _ctx, json3, taskToSummary2, url) {
|
|
48769
51096
|
const task2 = getTask(id);
|
|
48770
51097
|
if (!task2)
|
|
48771
|
-
return
|
|
48772
|
-
return
|
|
51098
|
+
return json3({ error: "Task not found" }, 404);
|
|
51099
|
+
return json3(taskToSummary2(task2, url ? parseFieldsParam(url) : undefined));
|
|
48773
51100
|
}
|
|
48774
|
-
async function handlePatchTask(id, req, _ctx,
|
|
51101
|
+
async function handlePatchTask(id, req, _ctx, json3, taskToSummary2) {
|
|
48775
51102
|
try {
|
|
48776
51103
|
const body = await req.json();
|
|
48777
51104
|
const task2 = getTask(id);
|
|
48778
51105
|
if (!task2)
|
|
48779
|
-
return
|
|
51106
|
+
return json3({ error: "Task not found" }, 404);
|
|
48780
51107
|
const ALLOWED = new Set(["title", "description", "status", "priority", "assigned_to", "plan_id", "task_list_id", "tags", "metadata", "due_at", "estimated_minutes", "actual_minutes", "confidence", "retry_count", "max_retries", "retry_after", "task_type"]);
|
|
48781
51108
|
const safeBody = {};
|
|
48782
51109
|
for (const [key, value] of Object.entries(body)) {
|
|
@@ -48788,85 +51115,85 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
|
|
|
48788
51115
|
...safeBody,
|
|
48789
51116
|
version: clientVersion
|
|
48790
51117
|
});
|
|
48791
|
-
return
|
|
51118
|
+
return json3(taskToSummary2(updated));
|
|
48792
51119
|
} catch (e) {
|
|
48793
|
-
const mapped = mapTaskError(e,
|
|
51120
|
+
const mapped = mapTaskError(e, json3);
|
|
48794
51121
|
if (mapped)
|
|
48795
51122
|
return mapped;
|
|
48796
|
-
return
|
|
51123
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
|
|
48797
51124
|
}
|
|
48798
51125
|
}
|
|
48799
|
-
function handleDeleteTask(id, _ctx,
|
|
51126
|
+
function handleDeleteTask(id, _ctx, json3) {
|
|
48800
51127
|
const deleted = deleteTask(id);
|
|
48801
51128
|
if (!deleted)
|
|
48802
|
-
return
|
|
48803
|
-
return
|
|
51129
|
+
return json3({ error: "Task not found" }, 404);
|
|
51130
|
+
return json3({ success: true });
|
|
48804
51131
|
}
|
|
48805
|
-
function handleStartTask(id, ctx,
|
|
51132
|
+
function handleStartTask(id, ctx, json3, taskToSummary2) {
|
|
48806
51133
|
try {
|
|
48807
51134
|
const task2 = startTask(id, "dashboard");
|
|
48808
51135
|
ctx.broadcastEvent({ type: "task", task_id: task2.id, action: "started", agent_id: "dashboard", project_id: task2.project_id });
|
|
48809
|
-
return
|
|
51136
|
+
return json3(taskToSummary2(task2));
|
|
48810
51137
|
} catch (e) {
|
|
48811
|
-
const mapped = mapTaskError(e,
|
|
51138
|
+
const mapped = mapTaskError(e, json3);
|
|
48812
51139
|
if (mapped)
|
|
48813
51140
|
return mapped;
|
|
48814
|
-
return
|
|
51141
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
|
|
48815
51142
|
}
|
|
48816
51143
|
}
|
|
48817
|
-
async function handleFailTask(id, req, ctx,
|
|
51144
|
+
async function handleFailTask(id, req, ctx, json3, taskToSummary2) {
|
|
48818
51145
|
try {
|
|
48819
51146
|
const body = await req.json().catch(() => ({}));
|
|
48820
51147
|
const result = failTask(id, body.agent_id, body.reason, { retry: body.retry, error_code: body.error_code });
|
|
48821
51148
|
ctx.broadcastEvent({ type: "task", task_id: id, action: "failed", agent_id: body.agent_id || null, project_id: result.task.project_id });
|
|
48822
|
-
return
|
|
51149
|
+
return json3({ task: taskToSummary2(result.task), retry_task: result.retryTask ? taskToSummary2(result.retryTask) : null });
|
|
48823
51150
|
} catch (e) {
|
|
48824
|
-
return
|
|
51151
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to fail task" }, 500);
|
|
48825
51152
|
}
|
|
48826
51153
|
}
|
|
48827
|
-
function handleCompleteTask(id, ctx,
|
|
51154
|
+
function handleCompleteTask(id, ctx, json3, taskToSummary2) {
|
|
48828
51155
|
try {
|
|
48829
51156
|
const task2 = completeTask(id, "dashboard");
|
|
48830
51157
|
ctx.broadcastEvent({ type: "task", task_id: task2.id, action: "completed", agent_id: "dashboard", project_id: task2.project_id });
|
|
48831
|
-
return
|
|
51158
|
+
return json3(taskToSummary2(task2));
|
|
48832
51159
|
} catch (e) {
|
|
48833
|
-
const mapped = mapTaskError(e,
|
|
51160
|
+
const mapped = mapTaskError(e, json3);
|
|
48834
51161
|
if (mapped)
|
|
48835
51162
|
return mapped;
|
|
48836
|
-
return
|
|
51163
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
|
|
48837
51164
|
}
|
|
48838
51165
|
}
|
|
48839
|
-
function handleListProjects(url, _ctx,
|
|
51166
|
+
function handleListProjects(url, _ctx, json3) {
|
|
48840
51167
|
const pFieldsParam = url.searchParams.get("fields");
|
|
48841
51168
|
const pFields = pFieldsParam ? pFieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
48842
51169
|
const projects = listProjects();
|
|
48843
|
-
return
|
|
51170
|
+
return json3(pFields ? projects.map((p) => Object.fromEntries(pFields.map((f) => [f, p[f] ?? null]))) : projects);
|
|
48844
51171
|
}
|
|
48845
|
-
async function handleCreateProject(req, _ctx,
|
|
51172
|
+
async function handleCreateProject(req, _ctx, json3) {
|
|
48846
51173
|
try {
|
|
48847
51174
|
const body = await req.json();
|
|
48848
51175
|
if (!body.name || !body.path)
|
|
48849
|
-
return
|
|
51176
|
+
return json3({ error: "Missing name or path" }, 400);
|
|
48850
51177
|
const project = createProject({ name: body.name, path: body.path, description: body.description });
|
|
48851
|
-
return
|
|
51178
|
+
return json3(project, 201);
|
|
48852
51179
|
} catch (e) {
|
|
48853
|
-
return
|
|
51180
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to create project" }, 500);
|
|
48854
51181
|
}
|
|
48855
51182
|
}
|
|
48856
|
-
function handleDeleteProject(id, _ctx,
|
|
51183
|
+
function handleDeleteProject(id, _ctx, json3) {
|
|
48857
51184
|
const deleted = deleteProject(id);
|
|
48858
51185
|
if (!deleted)
|
|
48859
|
-
return
|
|
48860
|
-
return
|
|
51186
|
+
return json3({ error: "Project not found" }, 404);
|
|
51187
|
+
return json3({ success: true });
|
|
48861
51188
|
}
|
|
48862
|
-
async function handleAgentMe(_req, url, _ctx,
|
|
51189
|
+
async function handleAgentMe(_req, url, _ctx, json3, taskToSummary2) {
|
|
48863
51190
|
try {
|
|
48864
51191
|
const name = url.searchParams.get("name");
|
|
48865
51192
|
if (!name)
|
|
48866
|
-
return
|
|
51193
|
+
return json3({ error: "Missing name param" }, 400);
|
|
48867
51194
|
const agentResult = registerAgent({ name });
|
|
48868
51195
|
if (isAgentConflict(agentResult))
|
|
48869
|
-
return
|
|
51196
|
+
return json3({ error: agentResult.message, conflict: true }, 409);
|
|
48870
51197
|
const agent = agentResult;
|
|
48871
51198
|
const tasks = listTasks({ assigned_to: agent.name });
|
|
48872
51199
|
const agentIdTasks = listTasks({ agent_id: agent.id });
|
|
@@ -48874,7 +51201,7 @@ async function handleAgentMe(_req, url, _ctx, json2, taskToSummary2) {
|
|
|
48874
51201
|
const pending = allTasks.filter((t) => t.status === "pending");
|
|
48875
51202
|
const inProgress = allTasks.filter((t) => t.status === "in_progress");
|
|
48876
51203
|
const completed = allTasks.filter((t) => t.status === "completed");
|
|
48877
|
-
return
|
|
51204
|
+
return json3({
|
|
48878
51205
|
agent,
|
|
48879
51206
|
pending_tasks: pending.map((t) => taskToSummary2(t)),
|
|
48880
51207
|
in_progress_tasks: inProgress.map((t) => taskToSummary2(t)),
|
|
@@ -48888,132 +51215,132 @@ async function handleAgentMe(_req, url, _ctx, json2, taskToSummary2) {
|
|
|
48888
51215
|
});
|
|
48889
51216
|
} catch (e) {
|
|
48890
51217
|
if (e instanceof InvalidAgentNameError)
|
|
48891
|
-
return
|
|
48892
|
-
return
|
|
51218
|
+
return json3({ error: e.message, suggestions: e.suggestions }, 400);
|
|
51219
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to get agent profile" }, 500);
|
|
48893
51220
|
}
|
|
48894
51221
|
}
|
|
48895
|
-
function handleAgentQueue(agentId, _ctx,
|
|
51222
|
+
function handleAgentQueue(agentId, _ctx, json3, taskToSummary2) {
|
|
48896
51223
|
const aliasSet = assignedToAliasSet(getDatabase(), agentId);
|
|
48897
51224
|
const pending = listTasks({ status: "pending" });
|
|
48898
51225
|
const queue = pending.filter((t) => aliasSet.has((t.assigned_to ?? "").toLowerCase()) || t.agent_id === agentId || !t.assigned_to && !t.locked_by);
|
|
48899
51226
|
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
48900
51227
|
queue.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4) || new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
48901
|
-
return
|
|
51228
|
+
return json3(queue.map((t) => taskToSummary2(t)));
|
|
48902
51229
|
}
|
|
48903
|
-
async function handleClaimTask(req, _ctx,
|
|
51230
|
+
async function handleClaimTask(req, _ctx, json3, taskToSummary2) {
|
|
48904
51231
|
try {
|
|
48905
51232
|
const body = await req.json();
|
|
48906
51233
|
const agentId = body.agent_id || "anonymous";
|
|
48907
51234
|
const task2 = claimNextTask(agentId, body.project_id ? { project_id: body.project_id } : undefined);
|
|
48908
|
-
return
|
|
51235
|
+
return json3({ task: task2 ? taskToSummary2(task2) : null });
|
|
48909
51236
|
} catch (e) {
|
|
48910
|
-
return
|
|
51237
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to claim" }, 500);
|
|
48911
51238
|
}
|
|
48912
51239
|
}
|
|
48913
|
-
function handleListOrgs(_ctx,
|
|
48914
|
-
return
|
|
51240
|
+
function handleListOrgs(_ctx, json3) {
|
|
51241
|
+
return json3(listOrgs());
|
|
48915
51242
|
}
|
|
48916
|
-
async function handleCreateOrg(req, _ctx,
|
|
51243
|
+
async function handleCreateOrg(req, _ctx, json3) {
|
|
48917
51244
|
try {
|
|
48918
51245
|
const body = await req.json();
|
|
48919
51246
|
if (!body.name)
|
|
48920
|
-
return
|
|
48921
|
-
return
|
|
51247
|
+
return json3({ error: "Missing name" }, 400);
|
|
51248
|
+
return json3(createOrg(body), 201);
|
|
48922
51249
|
} catch (e) {
|
|
48923
|
-
return
|
|
51250
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
48924
51251
|
}
|
|
48925
51252
|
}
|
|
48926
|
-
async function handleUpdateOrg(id, req, _ctx,
|
|
51253
|
+
async function handleUpdateOrg(id, req, _ctx, json3) {
|
|
48927
51254
|
try {
|
|
48928
51255
|
const body = await req.json();
|
|
48929
|
-
return
|
|
51256
|
+
return json3(updateOrg(id, body));
|
|
48930
51257
|
} catch (e) {
|
|
48931
|
-
return
|
|
51258
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
48932
51259
|
}
|
|
48933
51260
|
}
|
|
48934
|
-
function handleDeleteOrg(id, _ctx,
|
|
51261
|
+
function handleDeleteOrg(id, _ctx, json3) {
|
|
48935
51262
|
const deleted = deleteOrg(id);
|
|
48936
|
-
return
|
|
51263
|
+
return json3(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
|
|
48937
51264
|
}
|
|
48938
|
-
function handleOrgChart(_ctx,
|
|
48939
|
-
return
|
|
51265
|
+
function handleOrgChart(_ctx, json3) {
|
|
51266
|
+
return json3(getOrgChart());
|
|
48940
51267
|
}
|
|
48941
|
-
function handleAgentTeam(agentId, _ctx,
|
|
48942
|
-
return
|
|
51268
|
+
function handleAgentTeam(agentId, _ctx, json3) {
|
|
51269
|
+
return json3(getDirectReports(decodeURIComponent(agentId)));
|
|
48943
51270
|
}
|
|
48944
|
-
function handleListAgents(url, _ctx,
|
|
51271
|
+
function handleListAgents(url, _ctx, json3) {
|
|
48945
51272
|
const aFieldsParam = url.searchParams.get("fields");
|
|
48946
51273
|
const aFields = aFieldsParam ? aFieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
48947
51274
|
const agents = listAgents();
|
|
48948
|
-
return
|
|
51275
|
+
return json3(aFields ? agents.map((a) => Object.fromEntries(aFields.map((f) => [f, a[f] ?? null]))) : agents);
|
|
48949
51276
|
}
|
|
48950
|
-
async function handleRegisterAgent(req, _ctx,
|
|
51277
|
+
async function handleRegisterAgent(req, _ctx, json3) {
|
|
48951
51278
|
try {
|
|
48952
51279
|
const body = await req.json();
|
|
48953
51280
|
if (!body.name)
|
|
48954
|
-
return
|
|
51281
|
+
return json3({ error: "Missing name" }, 400);
|
|
48955
51282
|
const result = registerAgent({ name: body.name, description: body.description, session_id: body.session_id, working_dir: body.working_dir });
|
|
48956
51283
|
if (isAgentConflict(result))
|
|
48957
|
-
return
|
|
48958
|
-
return
|
|
51284
|
+
return json3({ error: result.message, conflict: true }, 409);
|
|
51285
|
+
return json3(result, 201);
|
|
48959
51286
|
} catch (e) {
|
|
48960
51287
|
if (e instanceof InvalidAgentNameError)
|
|
48961
|
-
return
|
|
48962
|
-
return
|
|
51288
|
+
return json3({ error: e.message, suggestions: e.suggestions }, 400);
|
|
51289
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to register agent" }, 500);
|
|
48963
51290
|
}
|
|
48964
51291
|
}
|
|
48965
|
-
async function handleUpdateAgent(id, req, _ctx,
|
|
51292
|
+
async function handleUpdateAgent(id, req, _ctx, json3) {
|
|
48966
51293
|
try {
|
|
48967
51294
|
const body = await req.json();
|
|
48968
51295
|
const agent = updateAgent(id, body);
|
|
48969
|
-
return
|
|
51296
|
+
return json3(agent);
|
|
48970
51297
|
} catch (e) {
|
|
48971
51298
|
if (e instanceof InvalidAgentNameError)
|
|
48972
|
-
return
|
|
48973
|
-
return
|
|
51299
|
+
return json3({ error: e.message, suggestions: e.suggestions }, 400);
|
|
51300
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to update agent" }, 500);
|
|
48974
51301
|
}
|
|
48975
51302
|
}
|
|
48976
|
-
function handleDeleteAgent(id, _ctx,
|
|
51303
|
+
function handleDeleteAgent(id, _ctx, json3) {
|
|
48977
51304
|
const deleted = deleteAgent(id);
|
|
48978
51305
|
if (!deleted)
|
|
48979
|
-
return
|
|
48980
|
-
return
|
|
51306
|
+
return json3({ error: "Agent not found" }, 404);
|
|
51307
|
+
return json3({ success: true });
|
|
48981
51308
|
}
|
|
48982
|
-
async function handleBulkDeleteAgents(req, _ctx,
|
|
51309
|
+
async function handleBulkDeleteAgents(req, _ctx, json3) {
|
|
48983
51310
|
try {
|
|
48984
51311
|
const body = await req.json();
|
|
48985
51312
|
if (!body.ids?.length || body.action !== "delete")
|
|
48986
|
-
return
|
|
51313
|
+
return json3({ error: "Missing ids or invalid action" }, 400);
|
|
48987
51314
|
let succeeded = 0;
|
|
48988
51315
|
for (const id of body.ids) {
|
|
48989
51316
|
if (deleteAgent(id))
|
|
48990
51317
|
succeeded++;
|
|
48991
51318
|
}
|
|
48992
|
-
return
|
|
51319
|
+
return json3({ succeeded, failed: body.ids.length - succeeded });
|
|
48993
51320
|
} catch (e) {
|
|
48994
|
-
return
|
|
51321
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
48995
51322
|
}
|
|
48996
51323
|
}
|
|
48997
|
-
async function handleBulkDeleteProjects(req, _ctx,
|
|
51324
|
+
async function handleBulkDeleteProjects(req, _ctx, json3) {
|
|
48998
51325
|
try {
|
|
48999
51326
|
const body = await req.json();
|
|
49000
51327
|
if (!body.ids?.length || body.action !== "delete")
|
|
49001
|
-
return
|
|
51328
|
+
return json3({ error: "Missing ids or invalid action" }, 400);
|
|
49002
51329
|
let succeeded = 0;
|
|
49003
51330
|
for (const id of body.ids) {
|
|
49004
51331
|
if (deleteProject(id))
|
|
49005
51332
|
succeeded++;
|
|
49006
51333
|
}
|
|
49007
|
-
return
|
|
51334
|
+
return json3({ succeeded, failed: body.ids.length - succeeded });
|
|
49008
51335
|
} catch (e) {
|
|
49009
|
-
return
|
|
51336
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
49010
51337
|
}
|
|
49011
51338
|
}
|
|
49012
|
-
function handleDoctor(_ctx,
|
|
51339
|
+
function handleDoctor(_ctx, json3) {
|
|
49013
51340
|
const { runTodosDoctor: runTodosDoctor2 } = (init_doctor(), __toCommonJS(exports_doctor));
|
|
49014
|
-
return
|
|
51341
|
+
return json3(runTodosDoctor2({ apply: false }));
|
|
49015
51342
|
}
|
|
49016
|
-
function handleReport(_req, url, _ctx,
|
|
51343
|
+
function handleReport(_req, url, _ctx, json3) {
|
|
49017
51344
|
const days = parseInt(url.searchParams.get("days") || "7", 10);
|
|
49018
51345
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
49019
51346
|
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
@@ -49029,62 +51356,62 @@ function handleReport(_req, url, _ctx, json2) {
|
|
|
49029
51356
|
byDay[day] = (byDay[day] || 0) + 1;
|
|
49030
51357
|
}
|
|
49031
51358
|
const completionRate = changed.length > 0 ? Math.round(completed.length / changed.length * 100) : 0;
|
|
49032
|
-
return
|
|
51359
|
+
return json3({ days, period_since: since, total: all.length, stats: stats2, changed: changed.length, completed: completed.length, failed: failed.length, completion_rate: completionRate, by_day: byDay });
|
|
49033
51360
|
}
|
|
49034
|
-
function handleActivity(_req, url, _ctx,
|
|
51361
|
+
function handleActivity(_req, url, _ctx, json3) {
|
|
49035
51362
|
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
49036
|
-
return
|
|
51363
|
+
return json3(getRecentActivity(limit));
|
|
49037
51364
|
}
|
|
49038
|
-
function handleTaskHistory(id, _ctx,
|
|
51365
|
+
function handleTaskHistory(id, _ctx, json3, url) {
|
|
49039
51366
|
const history = getTaskHistory(id);
|
|
49040
51367
|
const format = url?.searchParams.get("format") || "compact";
|
|
49041
51368
|
const limit = parseBoundedLimit(url?.searchParams.get("limit") || null, 20, 500);
|
|
49042
|
-
return
|
|
51369
|
+
return json3(format === "full" ? history : history.slice(0, limit));
|
|
49043
51370
|
}
|
|
49044
|
-
function handleListWebhooks(_ctx,
|
|
49045
|
-
return
|
|
51371
|
+
function handleListWebhooks(_ctx, json3) {
|
|
51372
|
+
return json3(listWebhooks());
|
|
49046
51373
|
}
|
|
49047
|
-
async function handleCreateWebhook(req, _ctx,
|
|
51374
|
+
async function handleCreateWebhook(req, _ctx, json3) {
|
|
49048
51375
|
try {
|
|
49049
51376
|
const body = await req.json();
|
|
49050
51377
|
if (!body.url)
|
|
49051
|
-
return
|
|
49052
|
-
return
|
|
51378
|
+
return json3({ error: "Missing url" }, 400);
|
|
51379
|
+
return json3(createWebhook(body), 201);
|
|
49053
51380
|
} catch (e) {
|
|
49054
|
-
return
|
|
51381
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
49055
51382
|
}
|
|
49056
51383
|
}
|
|
49057
|
-
function handleDeleteWebhook(id, _ctx,
|
|
51384
|
+
function handleDeleteWebhook(id, _ctx, json3) {
|
|
49058
51385
|
const deleted = deleteWebhook(id);
|
|
49059
|
-
return
|
|
51386
|
+
return json3(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
|
|
49060
51387
|
}
|
|
49061
|
-
function handleListTemplates(_ctx,
|
|
49062
|
-
return
|
|
51388
|
+
function handleListTemplates(_ctx, json3) {
|
|
51389
|
+
return json3(listTemplates());
|
|
49063
51390
|
}
|
|
49064
|
-
async function handleCreateTemplate(req, _ctx,
|
|
51391
|
+
async function handleCreateTemplate(req, _ctx, json3) {
|
|
49065
51392
|
try {
|
|
49066
51393
|
const body = await req.json();
|
|
49067
51394
|
if (!body.name || !body.title_pattern)
|
|
49068
|
-
return
|
|
49069
|
-
return
|
|
51395
|
+
return json3({ error: "Missing name or title_pattern" }, 400);
|
|
51396
|
+
return json3(createTemplate(body), 201);
|
|
49070
51397
|
} catch (e) {
|
|
49071
|
-
return
|
|
51398
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
49072
51399
|
}
|
|
49073
51400
|
}
|
|
49074
|
-
function handleDeleteTemplate(id, _ctx,
|
|
51401
|
+
function handleDeleteTemplate(id, _ctx, json3) {
|
|
49075
51402
|
const deleted = deleteTemplate(id);
|
|
49076
|
-
return
|
|
51403
|
+
return json3(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
|
|
49077
51404
|
}
|
|
49078
|
-
function handleListPlans(url, _ctx,
|
|
51405
|
+
function handleListPlans(url, _ctx, json3) {
|
|
49079
51406
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
49080
51407
|
const plans = listPlans(projectId);
|
|
49081
|
-
return
|
|
51408
|
+
return json3(plans);
|
|
49082
51409
|
}
|
|
49083
|
-
async function handleCreatePlan(req, _ctx,
|
|
51410
|
+
async function handleCreatePlan(req, _ctx, json3) {
|
|
49084
51411
|
try {
|
|
49085
51412
|
const body = await req.json();
|
|
49086
51413
|
if (!body.name)
|
|
49087
|
-
return
|
|
51414
|
+
return json3({ error: "Missing 'name'" }, 400);
|
|
49088
51415
|
const plan = createPlan({
|
|
49089
51416
|
name: body.name,
|
|
49090
51417
|
slug: body.slug,
|
|
@@ -49094,49 +51421,49 @@ async function handleCreatePlan(req, _ctx, json2) {
|
|
|
49094
51421
|
agent_id: body.agent_id,
|
|
49095
51422
|
status: body.status
|
|
49096
51423
|
});
|
|
49097
|
-
return
|
|
51424
|
+
return json3(plan, 201);
|
|
49098
51425
|
} catch (e) {
|
|
49099
|
-
return
|
|
51426
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to create plan" }, 500);
|
|
49100
51427
|
}
|
|
49101
51428
|
}
|
|
49102
|
-
async function handleBulkDeletePlans(req, _ctx,
|
|
51429
|
+
async function handleBulkDeletePlans(req, _ctx, json3) {
|
|
49103
51430
|
try {
|
|
49104
51431
|
const body = await req.json();
|
|
49105
51432
|
if (!body.ids?.length || body.action !== "delete")
|
|
49106
|
-
return
|
|
51433
|
+
return json3({ error: "Missing ids or invalid action" }, 400);
|
|
49107
51434
|
let succeeded = 0;
|
|
49108
51435
|
for (const id of body.ids) {
|
|
49109
51436
|
if (deletePlan(id))
|
|
49110
51437
|
succeeded++;
|
|
49111
51438
|
}
|
|
49112
|
-
return
|
|
51439
|
+
return json3({ succeeded, failed: body.ids.length - succeeded });
|
|
49113
51440
|
} catch (e) {
|
|
49114
|
-
return
|
|
51441
|
+
return json3({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
49115
51442
|
}
|
|
49116
51443
|
}
|
|
49117
|
-
function handleGetPlan(id, _ctx,
|
|
51444
|
+
function handleGetPlan(id, _ctx, json3, taskToSummary2) {
|
|
49118
51445
|
const plan = getPlan(id);
|
|
49119
51446
|
if (!plan)
|
|
49120
|
-
return
|
|
51447
|
+
return json3({ error: "Plan not found" }, 404);
|
|
49121
51448
|
const tasks = listTasks({ plan_id: id });
|
|
49122
|
-
return
|
|
51449
|
+
return json3({ ...plan, tasks: tasks.map((t) => taskToSummary2(t)) });
|
|
49123
51450
|
}
|
|
49124
|
-
async function handleUpdatePlan(id, req, _ctx,
|
|
51451
|
+
async function handleUpdatePlan(id, req, _ctx, json3) {
|
|
49125
51452
|
try {
|
|
49126
51453
|
const body = await req.json();
|
|
49127
51454
|
const plan = updatePlan(id, body);
|
|
49128
|
-
return
|
|
51455
|
+
return json3(plan);
|
|
49129
51456
|
} catch (e) {
|
|
49130
|
-
return
|
|
51457
|
+
return json3({ error: e instanceof Error ? e.message : "Failed to update plan" }, 500);
|
|
49131
51458
|
}
|
|
49132
51459
|
}
|
|
49133
|
-
function handleDeletePlan(id, _ctx,
|
|
51460
|
+
function handleDeletePlan(id, _ctx, json3) {
|
|
49134
51461
|
const deleted = deletePlan(id);
|
|
49135
51462
|
if (!deleted)
|
|
49136
|
-
return
|
|
49137
|
-
return
|
|
51463
|
+
return json3({ error: "Plan not found" }, 404);
|
|
51464
|
+
return json3({ success: true });
|
|
49138
51465
|
}
|
|
49139
|
-
function handleStaticFiles(path, method, ctx,
|
|
51466
|
+
function handleStaticFiles(path, method, ctx, json3, serveStaticFile2) {
|
|
49140
51467
|
if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
|
|
49141
51468
|
return null;
|
|
49142
51469
|
if (path !== "/") {
|
|
@@ -49144,7 +51471,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
49144
51471
|
const resolvedFile = resolve16(filePath);
|
|
49145
51472
|
const resolvedBase = resolve16(ctx.dashboardDir);
|
|
49146
51473
|
if (!resolvedFile.startsWith(resolvedBase + sep3) && resolvedFile !== resolvedBase) {
|
|
49147
|
-
return
|
|
51474
|
+
return json3({ error: "Forbidden" }, 403);
|
|
49148
51475
|
}
|
|
49149
51476
|
const res2 = serveStaticFile2(filePath);
|
|
49150
51477
|
if (res2)
|
|
@@ -49194,6 +51521,9 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49194
51521
|
Task: taskSchema,
|
|
49195
51522
|
Project: projectSchema,
|
|
49196
51523
|
TaskList: taskListSchema,
|
|
51524
|
+
ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
|
|
51525
|
+
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
51526
|
+
ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
|
|
49197
51527
|
TaskComment: taskCommentSchema,
|
|
49198
51528
|
Plan: planSchema,
|
|
49199
51529
|
Template: templateSchema,
|
|
@@ -49271,6 +51601,29 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49271
51601
|
name: { type: "string", minLength: 1 }
|
|
49272
51602
|
}
|
|
49273
51603
|
},
|
|
51604
|
+
ProjectTaskListEnsureApplyInput: {
|
|
51605
|
+
type: "object",
|
|
51606
|
+
additionalProperties: false,
|
|
51607
|
+
required: ["expected_project_revision"],
|
|
51608
|
+
properties: {
|
|
51609
|
+
expected_project_revision: { type: "string", minLength: 1 },
|
|
51610
|
+
idempotency_key: {
|
|
51611
|
+
type: "string",
|
|
51612
|
+
minLength: 8,
|
|
51613
|
+
maxLength: 128,
|
|
51614
|
+
pattern: "^[A-Za-z0-9._:-]+$"
|
|
51615
|
+
}
|
|
51616
|
+
}
|
|
51617
|
+
},
|
|
51618
|
+
ProjectTaskListRollbackInput: {
|
|
51619
|
+
type: "object",
|
|
51620
|
+
additionalProperties: false,
|
|
51621
|
+
required: ["receipt_id", "expected_task_list_revision"],
|
|
51622
|
+
properties: {
|
|
51623
|
+
receipt_id: { type: "string", minLength: 1 },
|
|
51624
|
+
expected_task_list_revision: { type: "string", minLength: 1 }
|
|
51625
|
+
}
|
|
51626
|
+
},
|
|
49274
51627
|
ErrorResponse: {
|
|
49275
51628
|
type: "object",
|
|
49276
51629
|
required: ["error"],
|
|
@@ -50373,6 +52726,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
50373
52726
|
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
|
|
50374
52727
|
}
|
|
50375
52728
|
},
|
|
52729
|
+
"/v1/projects/{id}/task-list/ensure": {
|
|
52730
|
+
get: {
|
|
52731
|
+
operationId: "planProjectTaskListEnsure",
|
|
52732
|
+
summary: "Plan a non-mutating repair of a project's declared task list",
|
|
52733
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
52734
|
+
responses: {
|
|
52735
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
|
|
52736
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
52737
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
52738
|
+
}
|
|
52739
|
+
},
|
|
52740
|
+
post: {
|
|
52741
|
+
operationId: "ensureProjectTaskList",
|
|
52742
|
+
summary: "Idempotently create an existing project's declared task list",
|
|
52743
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
52744
|
+
requestBody: {
|
|
52745
|
+
required: true,
|
|
52746
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureApplyInput" } } }
|
|
52747
|
+
},
|
|
52748
|
+
responses: {
|
|
52749
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
|
|
52750
|
+
"201": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListEnsureResult" } } } },
|
|
52751
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
52752
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
52753
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
52754
|
+
}
|
|
52755
|
+
}
|
|
52756
|
+
},
|
|
52757
|
+
"/v1/projects/{id}/task-list/rollback": {
|
|
52758
|
+
post: {
|
|
52759
|
+
operationId: "rollbackProjectTaskListEnsure",
|
|
52760
|
+
summary: "Conditionally remove an unchanged task list created by an accepted ensure receipt",
|
|
52761
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
52762
|
+
requestBody: {
|
|
52763
|
+
required: true,
|
|
52764
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListRollbackInput" } } }
|
|
52765
|
+
},
|
|
52766
|
+
responses: {
|
|
52767
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/ProjectTaskListRollbackResult" } } } },
|
|
52768
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
52769
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
52770
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
52771
|
+
}
|
|
52772
|
+
}
|
|
52773
|
+
},
|
|
50376
52774
|
"/v1/projects/{id}/rename": {
|
|
50377
52775
|
post: {
|
|
50378
52776
|
operationId: "renameProject",
|
|
@@ -50654,7 +53052,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
50654
53052
|
}
|
|
50655
53053
|
};
|
|
50656
53054
|
}
|
|
50657
|
-
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
53055
|
+
var taskSchema, projectSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
50658
53056
|
var init_openapi = __esm(() => {
|
|
50659
53057
|
init_package_version();
|
|
50660
53058
|
init_types();
|
|
@@ -50702,6 +53100,80 @@ var init_openapi = __esm(() => {
|
|
|
50702
53100
|
updated_at: { type: "string" }
|
|
50703
53101
|
}
|
|
50704
53102
|
};
|
|
53103
|
+
projectTaskListEnsureReceiptSchema = {
|
|
53104
|
+
type: "object",
|
|
53105
|
+
additionalProperties: false,
|
|
53106
|
+
required: [
|
|
53107
|
+
"schema_version",
|
|
53108
|
+
"receipt_id",
|
|
53109
|
+
"idempotency_key",
|
|
53110
|
+
"project_id",
|
|
53111
|
+
"task_list_id",
|
|
53112
|
+
"slug",
|
|
53113
|
+
"created_by_operation",
|
|
53114
|
+
"result_revision",
|
|
53115
|
+
"result_digest",
|
|
53116
|
+
"rollback_supported",
|
|
53117
|
+
"created_at"
|
|
53118
|
+
],
|
|
53119
|
+
properties: {
|
|
53120
|
+
schema_version: { type: "string", enum: ["todos.project-task-list-ensure.v1"] },
|
|
53121
|
+
receipt_id: { type: "string" },
|
|
53122
|
+
idempotency_key: { type: "string" },
|
|
53123
|
+
project_id: { type: "string" },
|
|
53124
|
+
task_list_id: { type: "string" },
|
|
53125
|
+
slug: { type: "string" },
|
|
53126
|
+
created_by_operation: { type: "boolean" },
|
|
53127
|
+
result_revision: { type: "string" },
|
|
53128
|
+
result_digest: { type: "string" },
|
|
53129
|
+
rollback_supported: { type: "boolean" },
|
|
53130
|
+
created_at: { type: "string", format: "date-time" }
|
|
53131
|
+
}
|
|
53132
|
+
};
|
|
53133
|
+
projectTaskListEnsureResultSchema = {
|
|
53134
|
+
type: "object",
|
|
53135
|
+
additionalProperties: false,
|
|
53136
|
+
required: ["mode", "action", "project", "task_list", "receipt"],
|
|
53137
|
+
properties: {
|
|
53138
|
+
mode: { type: "string", enum: ["plan", "apply"] },
|
|
53139
|
+
action: { type: "string", enum: ["would_create", "created", "already_present"] },
|
|
53140
|
+
project: { $ref: "#/components/schemas/Project" },
|
|
53141
|
+
task_list: {
|
|
53142
|
+
oneOf: [
|
|
53143
|
+
{ $ref: "#/components/schemas/TaskList" },
|
|
53144
|
+
{ type: "null" }
|
|
53145
|
+
]
|
|
53146
|
+
},
|
|
53147
|
+
receipt: {
|
|
53148
|
+
oneOf: [
|
|
53149
|
+
{ $ref: "#/components/schemas/ProjectTaskListEnsureReceipt" },
|
|
53150
|
+
{ type: "null" }
|
|
53151
|
+
]
|
|
53152
|
+
}
|
|
53153
|
+
}
|
|
53154
|
+
};
|
|
53155
|
+
projectTaskListRollbackResultSchema = {
|
|
53156
|
+
type: "object",
|
|
53157
|
+
additionalProperties: false,
|
|
53158
|
+
required: [
|
|
53159
|
+
"schema_version",
|
|
53160
|
+
"action",
|
|
53161
|
+
"project_id",
|
|
53162
|
+
"task_list_id",
|
|
53163
|
+
"accepted_receipt_id",
|
|
53164
|
+
"rollback_receipt_id",
|
|
53165
|
+
"removed_at"
|
|
53166
|
+
],
|
|
53167
|
+
properties: {
|
|
53168
|
+
schema_version: { type: "string", enum: ["todos.project-task-list-ensure.v1"] },
|
|
53169
|
+
action: { type: "string", enum: ["removed"] },
|
|
53170
|
+
project_id: { type: "string" },
|
|
53171
|
+
task_list_id: { type: "string" },
|
|
53172
|
+
accepted_receipt_id: { type: "string" },
|
|
53173
|
+
rollback_receipt_id: { type: "string" },
|
|
53174
|
+
removed_at: { type: "string", format: "date-time" }
|
|
53175
|
+
}
|
|
53176
|
+
};
|
|
50705
53177
|
taskCommentSchema = {
|
|
50706
53178
|
type: "object",
|
|
50707
53179
|
required: ["id", "task_id", "agent_id", "session_id", "content", "type", "progress_pct", "created_at"],
|
|
@@ -50805,10 +53277,10 @@ var exports_pr_groups = {};
|
|
|
50805
53277
|
__export(exports_pr_groups, {
|
|
50806
53278
|
handlePrGroupHttpRequest: () => handlePrGroupHttpRequest
|
|
50807
53279
|
});
|
|
50808
|
-
function
|
|
50809
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
53280
|
+
function json3(body, status = 200) {
|
|
53281
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS2 });
|
|
50810
53282
|
}
|
|
50811
|
-
function
|
|
53283
|
+
function errorStatus2(error) {
|
|
50812
53284
|
switch (error.code) {
|
|
50813
53285
|
case "PR_GROUP_INVALID_INPUT":
|
|
50814
53286
|
case "PR_GROUP_EXACT_HEAD_REQUIRED":
|
|
@@ -50822,7 +53294,7 @@ function errorStatus(error) {
|
|
|
50822
53294
|
return 409;
|
|
50823
53295
|
}
|
|
50824
53296
|
}
|
|
50825
|
-
async function
|
|
53297
|
+
async function readJson2(req) {
|
|
50826
53298
|
try {
|
|
50827
53299
|
const value = await req.json();
|
|
50828
53300
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -50840,26 +53312,26 @@ async function handlePrGroupHttpRequest(req, url, ledger, basePath, principal) {
|
|
|
50840
53312
|
const method = req.method.toUpperCase();
|
|
50841
53313
|
try {
|
|
50842
53314
|
if (!groupId && method === "POST" && action === undefined) {
|
|
50843
|
-
return
|
|
53315
|
+
return json3({ error: "unknown PR-group route", code: "PR_GROUP_NOT_FOUND" }, 404);
|
|
50844
53316
|
}
|
|
50845
53317
|
if (groupId === "admit" && !action) {
|
|
50846
53318
|
if (method !== "POST")
|
|
50847
|
-
return
|
|
50848
|
-
const body = await
|
|
53319
|
+
return json3({ error: "method not allowed" }, 405);
|
|
53320
|
+
const body = await readJson2(req);
|
|
50849
53321
|
if (!body)
|
|
50850
|
-
return
|
|
50851
|
-
return
|
|
53322
|
+
return json3({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
|
|
53323
|
+
return json3(await ledger.admit(body), 201);
|
|
50852
53324
|
}
|
|
50853
53325
|
if (!groupId)
|
|
50854
|
-
return
|
|
53326
|
+
return json3({ error: "PR group id is required", code: "PR_GROUP_INVALID_INPUT" }, 400);
|
|
50855
53327
|
if (!action && method === "GET") {
|
|
50856
|
-
return
|
|
53328
|
+
return json3({ view: await ledger.get(groupId) });
|
|
50857
53329
|
}
|
|
50858
53330
|
if (action === "events") {
|
|
50859
53331
|
if (method === "GET") {
|
|
50860
53332
|
const limit = url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined;
|
|
50861
53333
|
const afterSequence = url.searchParams.has("after_sequence") ? Number(url.searchParams.get("after_sequence")) : undefined;
|
|
50862
|
-
return
|
|
53334
|
+
return json3({
|
|
50863
53335
|
history: await ledger.events(groupId, {
|
|
50864
53336
|
...limit !== undefined ? { limit } : {},
|
|
50865
53337
|
...afterSequence !== undefined ? { after_sequence: afterSequence } : {}
|
|
@@ -50867,49 +53339,49 @@ async function handlePrGroupHttpRequest(req, url, ledger, basePath, principal) {
|
|
|
50867
53339
|
});
|
|
50868
53340
|
}
|
|
50869
53341
|
if (method === "POST") {
|
|
50870
|
-
const body = await
|
|
53342
|
+
const body = await readJson2(req);
|
|
50871
53343
|
if (!body)
|
|
50872
|
-
return
|
|
50873
|
-
return
|
|
53344
|
+
return json3({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
|
|
53345
|
+
return json3(await ledger.append({
|
|
50874
53346
|
...body,
|
|
50875
53347
|
group_id: groupId,
|
|
50876
53348
|
authenticated_actor_id: principal?.actor_id ?? undefined,
|
|
50877
53349
|
authenticated_actor_run_id: principal?.actor_run_id ?? undefined
|
|
50878
53350
|
}), 201);
|
|
50879
53351
|
}
|
|
50880
|
-
return
|
|
53352
|
+
return json3({ error: "method not allowed" }, 405);
|
|
50881
53353
|
}
|
|
50882
53354
|
if (action === "recover") {
|
|
50883
53355
|
if (method !== "POST")
|
|
50884
|
-
return
|
|
50885
|
-
const body = await
|
|
53356
|
+
return json3({ error: "method not allowed" }, 405);
|
|
53357
|
+
const body = await readJson2(req);
|
|
50886
53358
|
if (!body)
|
|
50887
|
-
return
|
|
50888
|
-
return
|
|
53359
|
+
return json3({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
|
|
53360
|
+
return json3(await ledger.recover({
|
|
50889
53361
|
...body,
|
|
50890
53362
|
group_id: groupId
|
|
50891
53363
|
}), 201);
|
|
50892
53364
|
}
|
|
50893
|
-
return
|
|
53365
|
+
return json3({ error: "unknown PR-group route", code: "PR_GROUP_NOT_FOUND" }, 404);
|
|
50894
53366
|
} catch (cause) {
|
|
50895
53367
|
if (cause instanceof PrGroupLedgerError) {
|
|
50896
|
-
return
|
|
53368
|
+
return json3({
|
|
50897
53369
|
error: cause.message,
|
|
50898
53370
|
code: cause.code,
|
|
50899
53371
|
details: cause.details,
|
|
50900
53372
|
authoritative: true
|
|
50901
|
-
},
|
|
53373
|
+
}, errorStatus2(cause));
|
|
50902
53374
|
}
|
|
50903
|
-
return
|
|
53375
|
+
return json3({
|
|
50904
53376
|
error: cause instanceof Error ? cause.message : "internal PR-group error",
|
|
50905
53377
|
code: "PR_GROUP_ATOMICITY_UNAVAILABLE"
|
|
50906
53378
|
}, 500);
|
|
50907
53379
|
}
|
|
50908
53380
|
}
|
|
50909
|
-
var
|
|
53381
|
+
var JSON_HEADERS2;
|
|
50910
53382
|
var init_pr_groups = __esm(() => {
|
|
50911
53383
|
init_types3();
|
|
50912
|
-
|
|
53384
|
+
JSON_HEADERS2 = { "Content-Type": "application/json" };
|
|
50913
53385
|
});
|
|
50914
53386
|
|
|
50915
53387
|
// src/lib/comment-cursor.ts
|
|
@@ -50935,6 +53407,261 @@ function decodeCommentCursor(value) {
|
|
|
50935
53407
|
}
|
|
50936
53408
|
var MAX_COMMENT_CURSOR_LENGTH = 1024;
|
|
50937
53409
|
|
|
53410
|
+
// src/lib/project-task-list-ensure.ts
|
|
53411
|
+
import { createHash as createHash15 } from "crypto";
|
|
53412
|
+
function canonicalJson(value) {
|
|
53413
|
+
if (value === null || typeof value !== "object")
|
|
53414
|
+
return JSON.stringify(value);
|
|
53415
|
+
if (Array.isArray(value))
|
|
53416
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
53417
|
+
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
53418
|
+
}
|
|
53419
|
+
function digest(value) {
|
|
53420
|
+
return createHash15("sha256").update(canonicalJson(value)).digest("hex");
|
|
53421
|
+
}
|
|
53422
|
+
function deriveIdempotencyKey(projectId, slug) {
|
|
53423
|
+
return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
|
|
53424
|
+
}
|
|
53425
|
+
function normalizeIdempotencyKey(value, projectId, slug) {
|
|
53426
|
+
const key = value?.trim() || deriveIdempotencyKey(projectId, slug);
|
|
53427
|
+
if (key.length < 8 || key.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(key)) {
|
|
53428
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID", "idempotency_key must be 8-128 ASCII letters, digits, dots, underscores, colons, or hyphens");
|
|
53429
|
+
}
|
|
53430
|
+
return key;
|
|
53431
|
+
}
|
|
53432
|
+
function receiptId2(projectId, slug, idempotencyKey) {
|
|
53433
|
+
return `ptlr_${digest({ project_id: projectId, slug, idempotency_key: idempotencyKey }).slice(0, 48)}`;
|
|
53434
|
+
}
|
|
53435
|
+
function semanticListDigest(list) {
|
|
53436
|
+
const metadata = { ...list.metadata ?? {} };
|
|
53437
|
+
delete metadata[RECEIPT_METADATA_KEY];
|
|
53438
|
+
return digest({
|
|
53439
|
+
project_id: list.project_id,
|
|
53440
|
+
slug: list.slug,
|
|
53441
|
+
name: list.name,
|
|
53442
|
+
description: list.description,
|
|
53443
|
+
metadata
|
|
53444
|
+
});
|
|
53445
|
+
}
|
|
53446
|
+
function storedMarker(list) {
|
|
53447
|
+
const value = list.metadata?.[RECEIPT_METADATA_KEY];
|
|
53448
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
53449
|
+
return null;
|
|
53450
|
+
const marker = value;
|
|
53451
|
+
if (marker.schema_version !== PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION || typeof marker.receipt_id !== "string" || typeof marker.idempotency_key !== "string" || typeof marker.project_id !== "string" || typeof marker.slug !== "string" || typeof marker.result_digest !== "string" || typeof marker.created_at !== "string")
|
|
53452
|
+
return null;
|
|
53453
|
+
return marker;
|
|
53454
|
+
}
|
|
53455
|
+
function receiptFor(store, project, list, idempotencyKey) {
|
|
53456
|
+
const marker = storedMarker(list);
|
|
53457
|
+
const owned = marker?.project_id === project.id && marker.slug === list.slug;
|
|
53458
|
+
if (owned && marker.idempotency_key !== idempotencyKey) {
|
|
53459
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
|
|
53460
|
+
project_id: project.id,
|
|
53461
|
+
task_list_id: list.id,
|
|
53462
|
+
receipt_id: marker.receipt_id
|
|
53463
|
+
});
|
|
53464
|
+
}
|
|
53465
|
+
return {
|
|
53466
|
+
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
53467
|
+
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
|
|
53468
|
+
idempotency_key: owned ? marker.idempotency_key : idempotencyKey,
|
|
53469
|
+
project_id: project.id,
|
|
53470
|
+
task_list_id: list.id,
|
|
53471
|
+
slug: list.slug,
|
|
53472
|
+
created_by_operation: owned,
|
|
53473
|
+
result_revision: list.updated_at,
|
|
53474
|
+
result_digest: owned ? marker.result_digest : semanticListDigest(list),
|
|
53475
|
+
rollback_supported: Boolean(owned && semanticListDigest(list) === marker.result_digest && store.taskLists.deleteIfUnchangedAndUnused),
|
|
53476
|
+
created_at: owned ? marker.created_at : list.created_at
|
|
53477
|
+
};
|
|
53478
|
+
}
|
|
53479
|
+
async function exactProjectState(store, projectId) {
|
|
53480
|
+
const project = await store.projects.get(projectId);
|
|
53481
|
+
if (!project) {
|
|
53482
|
+
throw new ProjectTaskListEnsureError("PROJECT_NOT_FOUND", `Project not found: ${projectId}`, { project_id: projectId });
|
|
53483
|
+
}
|
|
53484
|
+
const slug = project.task_list_id?.trim();
|
|
53485
|
+
if (!slug) {
|
|
53486
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_NOT_DECLARED", "Project does not declare a canonical task_list_id slug", { project_id: project.id });
|
|
53487
|
+
}
|
|
53488
|
+
const all = await store.taskLists.list();
|
|
53489
|
+
const scopedMatches = all.filter((list) => list.project_id === project.id && list.slug === slug);
|
|
53490
|
+
if (scopedMatches.length > 1) {
|
|
53491
|
+
throw new ProjectTaskListEnsureError("TASK_LIST_SCOPE_COLLISION", "More than one task list matches the project's exact id and declared slug", { project_id: project.id, slug, task_list_ids: scopedMatches.map((list) => list.id) });
|
|
53492
|
+
}
|
|
53493
|
+
const globalMatches = all.filter((list) => list.project_id === null && list.slug === slug);
|
|
53494
|
+
if (globalMatches.length > 0 && scopedMatches.length === 0) {
|
|
53495
|
+
throw new ProjectTaskListEnsureError("TASK_LIST_SCOPE_COLLISION", "A legacy global task list already owns the declared slug; refusing to create a second locator", { project_id: project.id, slug, task_list_ids: globalMatches.map((list) => list.id) });
|
|
53496
|
+
}
|
|
53497
|
+
return { project, scoped: scopedMatches[0] ?? null, globalCollision: globalMatches[0] ?? null };
|
|
53498
|
+
}
|
|
53499
|
+
async function planProjectTaskListEnsure(store, projectId) {
|
|
53500
|
+
const { project, scoped } = await exactProjectState(store, projectId);
|
|
53501
|
+
return {
|
|
53502
|
+
mode: "plan",
|
|
53503
|
+
action: scoped ? "already_present" : "would_create",
|
|
53504
|
+
project,
|
|
53505
|
+
task_list: scoped,
|
|
53506
|
+
receipt: null
|
|
53507
|
+
};
|
|
53508
|
+
}
|
|
53509
|
+
async function applyProjectTaskListEnsure(store, projectId, options) {
|
|
53510
|
+
const state = await exactProjectState(store, projectId);
|
|
53511
|
+
const { project } = state;
|
|
53512
|
+
if (project.updated_at !== options.expected_project_revision) {
|
|
53513
|
+
throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", "Project changed after the ensure plan; fetch a fresh plan before applying", {
|
|
53514
|
+
project_id: project.id,
|
|
53515
|
+
expected_project_revision: options.expected_project_revision,
|
|
53516
|
+
current_project_revision: project.updated_at
|
|
53517
|
+
});
|
|
53518
|
+
}
|
|
53519
|
+
const slug = project.task_list_id;
|
|
53520
|
+
const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
|
|
53521
|
+
if (state.scoped) {
|
|
53522
|
+
return {
|
|
53523
|
+
mode: "apply",
|
|
53524
|
+
action: "already_present",
|
|
53525
|
+
project,
|
|
53526
|
+
task_list: state.scoped,
|
|
53527
|
+
receipt: receiptFor(store, project, state.scoped, idempotencyKey)
|
|
53528
|
+
};
|
|
53529
|
+
}
|
|
53530
|
+
const marker = {
|
|
53531
|
+
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
53532
|
+
receipt_id: receiptId2(project.id, slug, idempotencyKey),
|
|
53533
|
+
idempotency_key: idempotencyKey,
|
|
53534
|
+
project_id: project.id,
|
|
53535
|
+
slug,
|
|
53536
|
+
result_digest: semanticListDigest({
|
|
53537
|
+
project_id: project.id,
|
|
53538
|
+
slug,
|
|
53539
|
+
name: project.name,
|
|
53540
|
+
description: null,
|
|
53541
|
+
metadata: {}
|
|
53542
|
+
}),
|
|
53543
|
+
created_at: new Date().toISOString()
|
|
53544
|
+
};
|
|
53545
|
+
let list;
|
|
53546
|
+
try {
|
|
53547
|
+
list = await store.taskLists.create({
|
|
53548
|
+
name: project.name,
|
|
53549
|
+
slug,
|
|
53550
|
+
project_id: project.id,
|
|
53551
|
+
metadata: { [RECEIPT_METADATA_KEY]: marker }
|
|
53552
|
+
});
|
|
53553
|
+
} catch (error) {
|
|
53554
|
+
if (!(error instanceof ResourceConflictError))
|
|
53555
|
+
throw error;
|
|
53556
|
+
const raced = await exactProjectState(store, projectId);
|
|
53557
|
+
if (!raced.scoped)
|
|
53558
|
+
throw error;
|
|
53559
|
+
if (raced.project.updated_at !== options.expected_project_revision || raced.project.task_list_id !== slug) {
|
|
53560
|
+
throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", "Project changed while the task list was being created; fetch a fresh plan before retrying", {
|
|
53561
|
+
project_id: raced.project.id,
|
|
53562
|
+
expected_project_revision: options.expected_project_revision,
|
|
53563
|
+
current_project_revision: raced.project.updated_at
|
|
53564
|
+
});
|
|
53565
|
+
}
|
|
53566
|
+
return {
|
|
53567
|
+
mode: "apply",
|
|
53568
|
+
action: "already_present",
|
|
53569
|
+
project: raced.project,
|
|
53570
|
+
task_list: raced.scoped,
|
|
53571
|
+
receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
|
|
53572
|
+
};
|
|
53573
|
+
}
|
|
53574
|
+
const projectReadback = await store.projects.get(project.id);
|
|
53575
|
+
if (!projectReadback || projectReadback.updated_at !== options.expected_project_revision || projectReadback.task_list_id !== slug) {
|
|
53576
|
+
let compensated = false;
|
|
53577
|
+
const unchanged = await store.taskLists.get(list.id);
|
|
53578
|
+
const unchangedMarker = unchanged ? storedMarker(unchanged) : null;
|
|
53579
|
+
if (unchanged && unchangedMarker?.receipt_id === marker.receipt_id && semanticListDigest(unchanged) === marker.result_digest && store.taskLists.deleteIfUnchangedAndUnused) {
|
|
53580
|
+
const deletion = await store.taskLists.deleteIfUnchangedAndUnused(list.id, {
|
|
53581
|
+
project_id: unchanged.project_id,
|
|
53582
|
+
slug: unchanged.slug,
|
|
53583
|
+
name: unchanged.name,
|
|
53584
|
+
description: unchanged.description,
|
|
53585
|
+
metadata: unchanged.metadata,
|
|
53586
|
+
updated_at: unchanged.updated_at
|
|
53587
|
+
});
|
|
53588
|
+
compensated = deletion.status === "deleted";
|
|
53589
|
+
}
|
|
53590
|
+
throw new ProjectTaskListEnsureError("PROJECT_REVISION_CONFLICT", compensated ? "Project changed while the task list was being created; the new list was rolled back" : "Project changed while the task list was being created; the new list was retained because safe conditional rollback could not be proven", { project_id: project.id, task_list_id: list.id, compensated });
|
|
53591
|
+
}
|
|
53592
|
+
const readback = await store.taskLists.get(list.id);
|
|
53593
|
+
if (!readback || readback.project_id !== project.id || readback.slug !== slug) {
|
|
53594
|
+
throw new ProjectTaskListEnsureError("TASK_LIST_SCOPE_COLLISION", "Task-list create did not preserve the exact project id and declared slug", { project_id: project.id, task_list_id: list.id, slug });
|
|
53595
|
+
}
|
|
53596
|
+
return {
|
|
53597
|
+
mode: "apply",
|
|
53598
|
+
action: "created",
|
|
53599
|
+
project: projectReadback,
|
|
53600
|
+
task_list: readback,
|
|
53601
|
+
receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
|
|
53602
|
+
};
|
|
53603
|
+
}
|
|
53604
|
+
async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
53605
|
+
const conditionalDelete = store.taskLists.deleteIfUnchangedAndUnused;
|
|
53606
|
+
if (!conditionalDelete) {
|
|
53607
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "This storage backend cannot guarantee atomic conditional rollback; refusing to delete", { project_id: projectId, receipt_id: options.receipt_id });
|
|
53608
|
+
}
|
|
53609
|
+
const project = await store.projects.get(projectId);
|
|
53610
|
+
if (!project) {
|
|
53611
|
+
throw new ProjectTaskListEnsureError("PROJECT_NOT_FOUND", `Project not found: ${projectId}`);
|
|
53612
|
+
}
|
|
53613
|
+
const candidates = (await store.taskLists.list(project.id)).filter((list2) => storedMarker(list2)?.receipt_id === options.receipt_id);
|
|
53614
|
+
if (candidates.length !== 1) {
|
|
53615
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_RECEIPT_NOT_FOUND", "No exact operation-owned task list matches this rollback receipt", { project_id: project.id, receipt_id: options.receipt_id });
|
|
53616
|
+
}
|
|
53617
|
+
const list = candidates[0];
|
|
53618
|
+
const marker = storedMarker(list);
|
|
53619
|
+
if (marker.project_id !== project.id || marker.slug !== list.slug || list.project_id !== project.id || list.updated_at !== options.expected_task_list_revision || semanticListDigest(list) !== marker.result_digest) {
|
|
53620
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "The operation-owned task list drifted; refusing conditional rollback", { project_id: project.id, task_list_id: list.id, receipt_id: options.receipt_id });
|
|
53621
|
+
}
|
|
53622
|
+
const deletion = await conditionalDelete.call(store.taskLists, list.id, {
|
|
53623
|
+
project_id: list.project_id,
|
|
53624
|
+
slug: list.slug,
|
|
53625
|
+
name: list.name,
|
|
53626
|
+
description: list.description,
|
|
53627
|
+
metadata: list.metadata,
|
|
53628
|
+
updated_at: list.updated_at
|
|
53629
|
+
});
|
|
53630
|
+
if (deletion.status === "has_dependents") {
|
|
53631
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_HAS_DEPENDENTS", "The operation-owned task list has dependents; refusing conditional rollback", {
|
|
53632
|
+
task_list_id: list.id,
|
|
53633
|
+
task_dependents: deletion.task_dependents,
|
|
53634
|
+
plan_dependents: deletion.plan_dependents
|
|
53635
|
+
});
|
|
53636
|
+
}
|
|
53637
|
+
if (deletion.status !== "deleted" || await store.taskLists.get(list.id)) {
|
|
53638
|
+
throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_ROLLBACK_CONFLICT", "Conditional rollback did not remove the exact task list", { task_list_id: list.id });
|
|
53639
|
+
}
|
|
53640
|
+
return {
|
|
53641
|
+
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
53642
|
+
action: "removed",
|
|
53643
|
+
project_id: project.id,
|
|
53644
|
+
task_list_id: list.id,
|
|
53645
|
+
accepted_receipt_id: options.receipt_id,
|
|
53646
|
+
rollback_receipt_id: `ptlr_inverse_${digest({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
|
|
53647
|
+
removed_at: new Date().toISOString()
|
|
53648
|
+
};
|
|
53649
|
+
}
|
|
53650
|
+
var PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION = "todos.project-task-list-ensure.v1", RECEIPT_METADATA_KEY = "todos_project_task_list_ensure", ProjectTaskListEnsureError;
|
|
53651
|
+
var init_project_task_list_ensure = __esm(() => {
|
|
53652
|
+
init_types();
|
|
53653
|
+
ProjectTaskListEnsureError = class ProjectTaskListEnsureError extends Error {
|
|
53654
|
+
code;
|
|
53655
|
+
details;
|
|
53656
|
+
constructor(code, message, details = {}) {
|
|
53657
|
+
super(message);
|
|
53658
|
+
this.code = code;
|
|
53659
|
+
this.details = details;
|
|
53660
|
+
this.name = "ProjectTaskListEnsureError";
|
|
53661
|
+
}
|
|
53662
|
+
};
|
|
53663
|
+
});
|
|
53664
|
+
|
|
50938
53665
|
// src/server/v1.ts
|
|
50939
53666
|
var exports_v1 = {};
|
|
50940
53667
|
__export(exports_v1, {
|
|
@@ -50942,11 +53669,11 @@ __export(exports_v1, {
|
|
|
50942
53669
|
handleV1Request: () => handleV1Request,
|
|
50943
53670
|
countSnapshotRecords: () => countSnapshotRecords
|
|
50944
53671
|
});
|
|
50945
|
-
function
|
|
50946
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
53672
|
+
function json4(body, status = 200) {
|
|
53673
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS3 });
|
|
50947
53674
|
}
|
|
50948
53675
|
function error(status, message, extra) {
|
|
50949
|
-
return
|
|
53676
|
+
return json4({ error: message, ...extra ?? {} }, status);
|
|
50950
53677
|
}
|
|
50951
53678
|
function enumQueryParam(url, name, vocabulary) {
|
|
50952
53679
|
const raw = url.searchParams.get(name);
|
|
@@ -51197,7 +53924,7 @@ function validateTemplatePatch(value) {
|
|
|
51197
53924
|
...body.plan_id === null ? { plan_id: null } : {}
|
|
51198
53925
|
} };
|
|
51199
53926
|
}
|
|
51200
|
-
async function
|
|
53927
|
+
async function readJson3(req) {
|
|
51201
53928
|
try {
|
|
51202
53929
|
const text2 = await req.text();
|
|
51203
53930
|
if (!text2)
|
|
@@ -51267,6 +53994,9 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51267
53994
|
if (path === "/v1/pr-groups" || path.startsWith("/v1/pr-groups/")) {
|
|
51268
53995
|
return handlePrGroupHttpRequest(req, url, (dependencies.getPrGroupLedger ?? getCloudPrGroupLedger)(), "/v1/pr-groups", { actor_id: principal.agent, actor_run_id: principal.kid });
|
|
51269
53996
|
}
|
|
53997
|
+
if (path === "/v1/project-registration" || path.startsWith("/v1/project-registration/")) {
|
|
53998
|
+
return handleTodosProjectRegistrationHttpRequest(req, url, (dependencies.getProjectRegistrationAuthority ?? getCloudProjectRegistrationAuthority)());
|
|
53999
|
+
}
|
|
51270
54000
|
const store = (dependencies.getStorageAdapter ?? getCloudStorageAdapter)();
|
|
51271
54001
|
const segments = path.split("/").filter(Boolean);
|
|
51272
54002
|
const resource = segments[1];
|
|
@@ -51278,7 +54008,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51278
54008
|
if (id === "exists" && !action) {
|
|
51279
54009
|
if (method !== "POST")
|
|
51280
54010
|
return error(405, `method ${method} not allowed on /v1/tasks/exists`);
|
|
51281
|
-
const body = await
|
|
54011
|
+
const body = await readJson3(req);
|
|
51282
54012
|
const ids2 = Array.isArray(body?.ids) ? Array.from(new Set(body.ids.filter((v) => typeof v === "string" && v.length > 0))) : [];
|
|
51283
54013
|
if (ids2.length === 0)
|
|
51284
54014
|
return error(400, "provide a non-empty string array `ids`");
|
|
@@ -51288,7 +54018,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51288
54018
|
const presentSet = new Set(found.map((t) => t.id));
|
|
51289
54019
|
const present = ids2.filter((i) => presentSet.has(i));
|
|
51290
54020
|
const missing = ids2.filter((i) => !presentSet.has(i));
|
|
51291
|
-
return
|
|
54021
|
+
return json4({
|
|
51292
54022
|
requested: ids2.length,
|
|
51293
54023
|
present_count: present.length,
|
|
51294
54024
|
missing_count: missing.length,
|
|
@@ -51301,7 +54031,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51301
54031
|
if (typeof store.tasks.getByFingerprint !== "function") {
|
|
51302
54032
|
return error(501, "fingerprint upsert is not supported by this storage backend");
|
|
51303
54033
|
}
|
|
51304
|
-
const body = await
|
|
54034
|
+
const body = await readJson3(req) ?? {};
|
|
51305
54035
|
const fingerprint3 = typeof body.fingerprint === "string" ? body.fingerprint.trim() : "";
|
|
51306
54036
|
if (!fingerprint3)
|
|
51307
54037
|
return error(400, "fingerprint is required");
|
|
@@ -51338,11 +54068,11 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51338
54068
|
}
|
|
51339
54069
|
if (!existing) {
|
|
51340
54070
|
const task2 = await store.tasks.create({ ...fields, title: body.title }, contextFromPrincipal(principal, body));
|
|
51341
|
-
return
|
|
54071
|
+
return json4({ task: task2, created: true }, 201);
|
|
51342
54072
|
}
|
|
51343
54073
|
try {
|
|
51344
54074
|
const task2 = await store.tasks.update(existing.id, { ...fields, version: existing.version }, contextFromPrincipal(principal, body));
|
|
51345
|
-
return
|
|
54075
|
+
return json4({ task: task2, created: false });
|
|
51346
54076
|
} catch (e) {
|
|
51347
54077
|
const msg = e.message || "";
|
|
51348
54078
|
if (msg.includes("version conflict"))
|
|
@@ -51384,15 +54114,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51384
54114
|
const tasks = await store.tasks.list(filter);
|
|
51385
54115
|
const { limit: _l, offset: _o, ...countFilter } = filter;
|
|
51386
54116
|
const total = await store.tasks.count(countFilter);
|
|
51387
|
-
return
|
|
54117
|
+
return json4({ tasks, count: tasks.length, total });
|
|
51388
54118
|
}
|
|
51389
54119
|
if (method === "POST") {
|
|
51390
|
-
const body = await
|
|
54120
|
+
const body = await readJson3(req);
|
|
51391
54121
|
if (!body || typeof body.title !== "string" || !body.title.trim()) {
|
|
51392
54122
|
return error(400, "title is required");
|
|
51393
54123
|
}
|
|
51394
54124
|
const task2 = await store.tasks.create(body, contextFromPrincipal(principal, body));
|
|
51395
|
-
return
|
|
54125
|
+
return json4({ task: task2 }, 201);
|
|
51396
54126
|
}
|
|
51397
54127
|
return error(405, `method ${method} not allowed on /v1/tasks`);
|
|
51398
54128
|
}
|
|
@@ -51409,7 +54139,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51409
54139
|
if (legacyPage.length > LEGACY_COMMENT_RESPONSE_LIMIT) {
|
|
51410
54140
|
return error(426, "task has too many comments for this client; upgrade @hasna/todos to use cursor pagination");
|
|
51411
54141
|
}
|
|
51412
|
-
return
|
|
54142
|
+
return json4({
|
|
51413
54143
|
comments: legacyPage,
|
|
51414
54144
|
count: legacyPage.length,
|
|
51415
54145
|
has_more: false,
|
|
@@ -51434,7 +54164,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51434
54164
|
const page = (await store.audit.getCommentsPage(id, { limit: limit + 1, ...before ? { before } : {} }, contextFromPrincipal(principal))).map(redactComment3);
|
|
51435
54165
|
const hasMore = page.length > limit;
|
|
51436
54166
|
const comments = hasMore ? page.slice(1) : page;
|
|
51437
|
-
return
|
|
54167
|
+
return json4({
|
|
51438
54168
|
comments,
|
|
51439
54169
|
count: comments.length,
|
|
51440
54170
|
has_more: hasMore,
|
|
@@ -51442,7 +54172,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51442
54172
|
});
|
|
51443
54173
|
}
|
|
51444
54174
|
if (method === "POST") {
|
|
51445
|
-
const body2 = await
|
|
54175
|
+
const body2 = await readJson3(req) ?? {};
|
|
51446
54176
|
if (typeof body2.content !== "string" || !body2.content.trim()) {
|
|
51447
54177
|
return error(400, "content is required");
|
|
51448
54178
|
}
|
|
@@ -51457,7 +54187,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51457
54187
|
type: body2.type,
|
|
51458
54188
|
progress_pct: body2.progress_pct
|
|
51459
54189
|
}, contextFromPrincipal(principal, body2));
|
|
51460
|
-
return
|
|
54190
|
+
return json4({ comment: redactComment3(comment) }, 201);
|
|
51461
54191
|
}
|
|
51462
54192
|
return error(405, `method ${method} not allowed on /v1/tasks/:id/comments`);
|
|
51463
54193
|
}
|
|
@@ -51467,19 +54197,19 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51467
54197
|
if (!await store.tasks.get(id))
|
|
51468
54198
|
return error(404, "task not found");
|
|
51469
54199
|
const history = await store.audit.getTaskHistory(id);
|
|
51470
|
-
return
|
|
54200
|
+
return json4({ history, count: history.length });
|
|
51471
54201
|
}
|
|
51472
54202
|
if (action === "lock" || action === "unlock") {
|
|
51473
54203
|
if (method !== "POST")
|
|
51474
54204
|
return error(405, `method ${method} not allowed on /v1/tasks/:id/${action}`);
|
|
51475
|
-
const body2 = await
|
|
54205
|
+
const body2 = await readJson3(req) ?? {};
|
|
51476
54206
|
if (!await store.tasks.get(id))
|
|
51477
54207
|
return error(404, "task not found");
|
|
51478
54208
|
if (action === "lock") {
|
|
51479
54209
|
if (typeof store.tasks.lock !== "function")
|
|
51480
54210
|
return error(501, "task locking is not supported by this storage backend");
|
|
51481
54211
|
const agentId3 = body2.agent_id || principal.agent || "todos-serve";
|
|
51482
|
-
return
|
|
54212
|
+
return json4({ result: await store.tasks.lock(id, agentId3) });
|
|
51483
54213
|
}
|
|
51484
54214
|
if (typeof store.tasks.unlock !== "function")
|
|
51485
54215
|
return error(501, "task unlocking is not supported by this storage backend");
|
|
@@ -51487,7 +54217,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51487
54217
|
if (!principal.scopes.includes("todos:*"))
|
|
51488
54218
|
return error(403, "force unlock requires todos:* scope");
|
|
51489
54219
|
const released2 = await store.tasks.unlock(id);
|
|
51490
|
-
return
|
|
54220
|
+
return json4({ success: released2 });
|
|
51491
54221
|
}
|
|
51492
54222
|
if (body2.agent_id && body2.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
|
|
51493
54223
|
return error(403, "unlock agent_id must match the authenticated agent");
|
|
@@ -51496,7 +54226,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51496
54226
|
if (!agentId2)
|
|
51497
54227
|
return error(403, "unlock requires an agent-bound key or force=true");
|
|
51498
54228
|
const released = await store.tasks.unlock(id, agentId2);
|
|
51499
|
-
return
|
|
54229
|
+
return json4({ success: released });
|
|
51500
54230
|
}
|
|
51501
54231
|
if (action === "dependencies") {
|
|
51502
54232
|
if (!store.dependencies)
|
|
@@ -51505,16 +54235,16 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51505
54235
|
if (!await store.tasks.get(id))
|
|
51506
54236
|
return error(404, "task not found");
|
|
51507
54237
|
const edges = await store.dependencies.list(id);
|
|
51508
|
-
return
|
|
54238
|
+
return json4(edges);
|
|
51509
54239
|
}
|
|
51510
54240
|
if (method === "POST") {
|
|
51511
|
-
const body2 = await
|
|
54241
|
+
const body2 = await readJson3(req) ?? {};
|
|
51512
54242
|
if (typeof body2.depends_on !== "string" || !body2.depends_on.trim()) {
|
|
51513
54243
|
return error(400, "depends_on is required");
|
|
51514
54244
|
}
|
|
51515
54245
|
try {
|
|
51516
54246
|
const dependency = await store.dependencies.add(id, body2.depends_on, contextFromPrincipal(principal));
|
|
51517
|
-
return
|
|
54247
|
+
return json4({ dependency }, 201);
|
|
51518
54248
|
} catch (e) {
|
|
51519
54249
|
const msg = e.message || "";
|
|
51520
54250
|
if (msg.includes("not found"))
|
|
@@ -51528,7 +54258,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51528
54258
|
if (!subId)
|
|
51529
54259
|
return error(400, "dependency target id is required (/v1/tasks/:id/dependencies/:dep)");
|
|
51530
54260
|
const removed = await store.dependencies.remove(id, subId);
|
|
51531
|
-
return
|
|
54261
|
+
return json4({ removed });
|
|
51532
54262
|
}
|
|
51533
54263
|
return error(405, `method ${method} not allowed on /v1/tasks/:id/dependencies`);
|
|
51534
54264
|
}
|
|
@@ -51539,10 +54269,10 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51539
54269
|
if (!await store.tasks.get(id))
|
|
51540
54270
|
return error(404, "task not found");
|
|
51541
54271
|
const verifications = await store.verifications.list(id);
|
|
51542
|
-
return
|
|
54272
|
+
return json4({ verifications, count: verifications.length });
|
|
51543
54273
|
}
|
|
51544
54274
|
if (method === "POST") {
|
|
51545
|
-
const body2 = await
|
|
54275
|
+
const body2 = await readJson3(req) ?? {};
|
|
51546
54276
|
if (typeof body2.command !== "string" || !body2.command.trim()) {
|
|
51547
54277
|
return error(400, "command is required");
|
|
51548
54278
|
}
|
|
@@ -51555,7 +54285,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51555
54285
|
artifact_path: body2.artifact_path,
|
|
51556
54286
|
agent_id: body2.agent_id
|
|
51557
54287
|
}, contextFromPrincipal(principal, body2));
|
|
51558
|
-
return
|
|
54288
|
+
return json4({ verification }, 201);
|
|
51559
54289
|
} catch (e) {
|
|
51560
54290
|
const msg = e.message || "";
|
|
51561
54291
|
if (msg.includes("not found"))
|
|
@@ -51572,10 +54302,10 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51572
54302
|
if (!await store.tasks.get(id))
|
|
51573
54303
|
return error(404, "task not found");
|
|
51574
54304
|
const commits = await store.commits.list(id);
|
|
51575
|
-
return
|
|
54305
|
+
return json4({ commits, count: commits.length });
|
|
51576
54306
|
}
|
|
51577
54307
|
if (method === "POST") {
|
|
51578
|
-
const body2 = await
|
|
54308
|
+
const body2 = await readJson3(req) ?? {};
|
|
51579
54309
|
if (typeof body2.sha !== "string" || !body2.sha.trim())
|
|
51580
54310
|
return error(400, "sha is required");
|
|
51581
54311
|
try {
|
|
@@ -51586,7 +54316,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51586
54316
|
author: body2.author,
|
|
51587
54317
|
files_changed: Array.isArray(body2.files_changed) ? body2.files_changed : undefined
|
|
51588
54318
|
}, contextFromPrincipal(principal));
|
|
51589
|
-
return
|
|
54319
|
+
return json4({ commit }, 201);
|
|
51590
54320
|
} catch (e) {
|
|
51591
54321
|
const msg = e.message || "";
|
|
51592
54322
|
if (msg.includes("not found"))
|
|
@@ -51603,10 +54333,10 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51603
54333
|
if (!await store.tasks.get(id))
|
|
51604
54334
|
return error(404, "task not found");
|
|
51605
54335
|
const refs = await store.gitRefs.list(id);
|
|
51606
|
-
return
|
|
54336
|
+
return json4({ refs, count: refs.length });
|
|
51607
54337
|
}
|
|
51608
54338
|
if (method === "POST") {
|
|
51609
|
-
const body2 = await
|
|
54339
|
+
const body2 = await readJson3(req) ?? {};
|
|
51610
54340
|
const refType = body2.ref_type === "pull_request" || body2.ref_type === "branch" ? body2.ref_type : "branch";
|
|
51611
54341
|
if (typeof body2.name !== "string" || !body2.name.trim())
|
|
51612
54342
|
return error(400, "name is required");
|
|
@@ -51619,7 +54349,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51619
54349
|
provider: body2.provider,
|
|
51620
54350
|
metadata: body2.metadata
|
|
51621
54351
|
}, contextFromPrincipal(principal));
|
|
51622
|
-
return
|
|
54352
|
+
return json4({ ref }, 201);
|
|
51623
54353
|
} catch (e) {
|
|
51624
54354
|
const msg = e.message || "";
|
|
51625
54355
|
if (msg.includes("not found"))
|
|
@@ -51635,21 +54365,21 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51635
54365
|
const body = actionJson.value && typeof actionJson.value === "object" && !Array.isArray(actionJson.value) ? actionJson.value : {};
|
|
51636
54366
|
const agentId = typeof body.agent_id === "string" ? body.agent_id : principal.agent || "todos-serve";
|
|
51637
54367
|
if (action === "start" && method === "POST") {
|
|
51638
|
-
return
|
|
54368
|
+
return json4({ task: await store.tasks.start(id, agentId) });
|
|
51639
54369
|
}
|
|
51640
54370
|
if (action === "complete" && method === "POST") {
|
|
51641
54371
|
const parsed = validateTaskCompletion(actionJson.value);
|
|
51642
54372
|
if (!parsed.ok)
|
|
51643
54373
|
return error(400, parsed.message);
|
|
51644
|
-
return
|
|
54374
|
+
return json4({
|
|
51645
54375
|
task: await store.tasks.complete(id, parsed.agentId || principal.agent || "todos-serve", parsed.options, contextFromPrincipal(principal, body))
|
|
51646
54376
|
});
|
|
51647
54377
|
}
|
|
51648
54378
|
if (action === "fail" && method === "POST") {
|
|
51649
|
-
return
|
|
54379
|
+
return json4({ result: await store.tasks.fail(id, agentId, typeof body.reason === "string" ? body.reason : "failed", {}) });
|
|
51650
54380
|
}
|
|
51651
54381
|
if (action === "claim" && method === "POST") {
|
|
51652
|
-
return
|
|
54382
|
+
return json4({ task: await store.tasks.claimNext(agentId, {}) });
|
|
51653
54383
|
}
|
|
51654
54384
|
return error(404, `unknown task action: ${action}`);
|
|
51655
54385
|
}
|
|
@@ -51672,10 +54402,10 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51672
54402
|
throw e;
|
|
51673
54403
|
}
|
|
51674
54404
|
}
|
|
51675
|
-
return task2 ?
|
|
54405
|
+
return task2 ? json4({ task: task2 }) : error(404, "task not found");
|
|
51676
54406
|
}
|
|
51677
54407
|
if (method === "PATCH" || method === "PUT") {
|
|
51678
|
-
const body = await
|
|
54408
|
+
const body = await readJson3(req);
|
|
51679
54409
|
if (!body)
|
|
51680
54410
|
return error(400, "invalid JSON body");
|
|
51681
54411
|
const current = await store.tasks.get(id);
|
|
@@ -51687,7 +54417,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51687
54417
|
};
|
|
51688
54418
|
try {
|
|
51689
54419
|
const task2 = await store.tasks.update(id, patch);
|
|
51690
|
-
return task2 ?
|
|
54420
|
+
return task2 ? json4({ task: task2 }) : error(404, "task not found");
|
|
51691
54421
|
} catch (e) {
|
|
51692
54422
|
const msg = e.message || "";
|
|
51693
54423
|
if (msg.includes("version conflict"))
|
|
@@ -51697,7 +54427,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51697
54427
|
}
|
|
51698
54428
|
if (method === "DELETE") {
|
|
51699
54429
|
await store.tasks.delete(id, contextFromPrincipal(principal));
|
|
51700
|
-
return
|
|
54430
|
+
return json4({ deleted: true, id });
|
|
51701
54431
|
}
|
|
51702
54432
|
return error(405, `method ${method} not allowed on /v1/tasks/:id`);
|
|
51703
54433
|
}
|
|
@@ -51705,24 +54435,70 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51705
54435
|
if (!id) {
|
|
51706
54436
|
if (method === "GET") {
|
|
51707
54437
|
const projects = await store.projects.list();
|
|
51708
|
-
return
|
|
54438
|
+
return json4({ projects, count: projects.length });
|
|
51709
54439
|
}
|
|
51710
54440
|
if (method === "POST") {
|
|
51711
|
-
const body = await
|
|
54441
|
+
const body = await readJson3(req);
|
|
51712
54442
|
if (!body)
|
|
51713
54443
|
return error(400, "invalid JSON body");
|
|
51714
54444
|
const validated = validateProjectCreate(body);
|
|
51715
54445
|
if (!validated.ok)
|
|
51716
54446
|
return error(400, validated.message);
|
|
51717
54447
|
const project = await store.projects.create(validated.input, contextFromPrincipal(principal));
|
|
51718
|
-
return
|
|
54448
|
+
return json4({ project }, 201);
|
|
51719
54449
|
}
|
|
51720
54450
|
return error(405, `method ${method} not allowed on /v1/projects`);
|
|
51721
54451
|
}
|
|
54452
|
+
if (action === "task-list" && subId === "ensure") {
|
|
54453
|
+
if (method === "GET") {
|
|
54454
|
+
return json4(await planProjectTaskListEnsure(store, id));
|
|
54455
|
+
}
|
|
54456
|
+
if (method !== "POST") {
|
|
54457
|
+
return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/ensure`);
|
|
54458
|
+
}
|
|
54459
|
+
const body = await readJson3(req);
|
|
54460
|
+
if (!body)
|
|
54461
|
+
return error(400, "invalid JSON body");
|
|
54462
|
+
const unknown = Object.keys(body).find((key) => !["expected_project_revision", "idempotency_key"].includes(key));
|
|
54463
|
+
if (unknown)
|
|
54464
|
+
return error(400, `unknown task-list ensure field: ${unknown}`);
|
|
54465
|
+
if (typeof body.expected_project_revision !== "string" || !body.expected_project_revision.trim()) {
|
|
54466
|
+
return error(400, "expected_project_revision must be a non-empty string from a fresh ensure plan");
|
|
54467
|
+
}
|
|
54468
|
+
if (body.idempotency_key !== undefined && typeof body.idempotency_key !== "string") {
|
|
54469
|
+
return error(400, "idempotency_key must be a string");
|
|
54470
|
+
}
|
|
54471
|
+
const result = await applyProjectTaskListEnsure(store, id, {
|
|
54472
|
+
expected_project_revision: body.expected_project_revision,
|
|
54473
|
+
...typeof body.idempotency_key === "string" ? { idempotency_key: body.idempotency_key } : {}
|
|
54474
|
+
});
|
|
54475
|
+
return json4(result, result.action === "created" ? 201 : 200);
|
|
54476
|
+
}
|
|
54477
|
+
if (action === "task-list" && subId === "rollback") {
|
|
54478
|
+
if (method !== "POST") {
|
|
54479
|
+
return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/rollback`);
|
|
54480
|
+
}
|
|
54481
|
+
const body = await readJson3(req);
|
|
54482
|
+
if (!body)
|
|
54483
|
+
return error(400, "invalid JSON body");
|
|
54484
|
+
const unknown = Object.keys(body).find((key) => !["receipt_id", "expected_task_list_revision"].includes(key));
|
|
54485
|
+
if (unknown)
|
|
54486
|
+
return error(400, `unknown task-list rollback field: ${unknown}`);
|
|
54487
|
+
if (typeof body.receipt_id !== "string" || !body.receipt_id.trim()) {
|
|
54488
|
+
return error(400, "receipt_id must be a non-empty string");
|
|
54489
|
+
}
|
|
54490
|
+
if (typeof body.expected_task_list_revision !== "string" || !body.expected_task_list_revision.trim()) {
|
|
54491
|
+
return error(400, "expected_task_list_revision must be a non-empty string from the accepted receipt");
|
|
54492
|
+
}
|
|
54493
|
+
return json4(await rollbackProjectTaskListEnsure(store, id, {
|
|
54494
|
+
receipt_id: body.receipt_id,
|
|
54495
|
+
expected_task_list_revision: body.expected_task_list_revision
|
|
54496
|
+
}));
|
|
54497
|
+
}
|
|
51722
54498
|
if (action === "rename") {
|
|
51723
54499
|
if (method !== "POST")
|
|
51724
54500
|
return error(405, `method ${method} not allowed on /v1/projects/:id/rename`);
|
|
51725
|
-
const body = await
|
|
54501
|
+
const body = await readJson3(req);
|
|
51726
54502
|
if (!body || typeof body.new_slug !== "string" || !body.new_slug.trim() || !normalizeSlug(body.new_slug)) {
|
|
51727
54503
|
return error(400, "new_slug must be a non-empty string");
|
|
51728
54504
|
}
|
|
@@ -51732,14 +54508,14 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51732
54508
|
const unknownField = Object.keys(body).find((key) => !["new_slug", "name"].includes(key));
|
|
51733
54509
|
if (unknownField)
|
|
51734
54510
|
return error(400, `unknown project rename field: ${unknownField}`);
|
|
51735
|
-
return
|
|
54511
|
+
return json4(await store.projects.rename(id, body, contextFromPrincipal(principal)));
|
|
51736
54512
|
}
|
|
51737
54513
|
if (method === "GET") {
|
|
51738
54514
|
const project = await store.projects.get(id);
|
|
51739
|
-
return project ?
|
|
54515
|
+
return project ? json4({ project }) : error(404, "project not found");
|
|
51740
54516
|
}
|
|
51741
54517
|
if (method === "PATCH" || method === "PUT") {
|
|
51742
|
-
const body = await
|
|
54518
|
+
const body = await readJson3(req);
|
|
51743
54519
|
if (!body)
|
|
51744
54520
|
return error(400, "invalid JSON body");
|
|
51745
54521
|
const validated = validateProjectPatch(body);
|
|
@@ -51748,21 +54524,21 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51748
54524
|
if (!await store.projects.get(id))
|
|
51749
54525
|
return error(404, "project not found");
|
|
51750
54526
|
const project = await store.projects.update(id, validated.patch);
|
|
51751
|
-
return
|
|
54527
|
+
return json4({ project });
|
|
51752
54528
|
}
|
|
51753
54529
|
if (method === "DELETE") {
|
|
51754
54530
|
await store.projects.delete(id, contextFromPrincipal(principal));
|
|
51755
|
-
return
|
|
54531
|
+
return json4({ deleted: true, id });
|
|
51756
54532
|
}
|
|
51757
54533
|
return error(405, `method ${method} not allowed on /v1/projects/:id`);
|
|
51758
54534
|
}
|
|
51759
54535
|
if (resource === "plans") {
|
|
51760
54536
|
if (!id && method === "GET") {
|
|
51761
54537
|
const plans = await store.plans.list(url.searchParams.get("project_id") ?? undefined);
|
|
51762
|
-
return
|
|
54538
|
+
return json4({ plans, count: plans.length });
|
|
51763
54539
|
}
|
|
51764
54540
|
if (!id && method === "POST") {
|
|
51765
|
-
const body = await
|
|
54541
|
+
const body = await readJson3(req);
|
|
51766
54542
|
const validated = validatePlanCreate(body);
|
|
51767
54543
|
if (!validated.ok)
|
|
51768
54544
|
return error(400, validated.message);
|
|
@@ -51777,14 +54553,14 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51777
54553
|
}
|
|
51778
54554
|
}
|
|
51779
54555
|
const plan = await store.plans.create(validated.input, contextFromPrincipal(principal, validated.input));
|
|
51780
|
-
return
|
|
54556
|
+
return json4({ plan }, 201);
|
|
51781
54557
|
}
|
|
51782
54558
|
if (id && method === "GET") {
|
|
51783
54559
|
const plan = await store.plans.get(id);
|
|
51784
|
-
return plan ?
|
|
54560
|
+
return plan ? json4({ plan }) : error(404, "plan not found");
|
|
51785
54561
|
}
|
|
51786
54562
|
if (id && (method === "PATCH" || method === "PUT")) {
|
|
51787
|
-
const body = await
|
|
54563
|
+
const body = await readJson3(req);
|
|
51788
54564
|
if (!body || Object.keys(body).length === 0)
|
|
51789
54565
|
return error(400, "plan patch is required");
|
|
51790
54566
|
const allowed = new Set(["name", "slug", "description", "status", "task_list_id", "agent_id"]);
|
|
@@ -51821,12 +54597,12 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51821
54597
|
}
|
|
51822
54598
|
}
|
|
51823
54599
|
const plan = await store.plans.update(id, body);
|
|
51824
|
-
return
|
|
54600
|
+
return json4({ plan });
|
|
51825
54601
|
}
|
|
51826
54602
|
if (id && method === "DELETE") {
|
|
51827
54603
|
if (!await store.plans.delete(id, contextFromPrincipal(principal)))
|
|
51828
54604
|
return error(404, "plan not found");
|
|
51829
|
-
return
|
|
54605
|
+
return json4({ deleted: true, id });
|
|
51830
54606
|
}
|
|
51831
54607
|
if (id)
|
|
51832
54608
|
return error(405, `method ${method} not allowed on /v1/plans/:id`);
|
|
@@ -51835,50 +54611,50 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51835
54611
|
if (!id && method === "GET") {
|
|
51836
54612
|
const projectId = url.searchParams.get("project_id");
|
|
51837
54613
|
const templates = (await store.templates.list()).filter((template) => projectId === null || template.project_id === projectId);
|
|
51838
|
-
return
|
|
54614
|
+
return json4({ templates, count: templates.length });
|
|
51839
54615
|
}
|
|
51840
54616
|
if (!id && method === "POST") {
|
|
51841
|
-
const body = await
|
|
54617
|
+
const body = await readJson3(req);
|
|
51842
54618
|
const validated = validateTemplateCreate(body);
|
|
51843
54619
|
if (!validated.ok)
|
|
51844
54620
|
return error(400, validated.message);
|
|
51845
54621
|
const template = await store.templates.create(validated.input, contextFromPrincipal(principal));
|
|
51846
|
-
return
|
|
54622
|
+
return json4({ template: await store.templates.getWithTasks(template.id) }, 201);
|
|
51847
54623
|
}
|
|
51848
54624
|
if (!id)
|
|
51849
54625
|
return error(405, `method ${method} not allowed on /v1/templates`);
|
|
51850
54626
|
if (method === "GET") {
|
|
51851
54627
|
const template = await store.templates.getWithTasks(id);
|
|
51852
|
-
return template ?
|
|
54628
|
+
return template ? json4({ template }) : error(404, "template not found");
|
|
51853
54629
|
}
|
|
51854
54630
|
if (method === "PATCH" || method === "PUT") {
|
|
51855
|
-
const body = await
|
|
54631
|
+
const body = await readJson3(req);
|
|
51856
54632
|
const validated = validateTemplatePatch(body);
|
|
51857
54633
|
if (!validated.ok)
|
|
51858
54634
|
return error(400, validated.message);
|
|
51859
54635
|
const template = await store.templates.update(id, validated.patch, contextFromPrincipal(principal));
|
|
51860
|
-
return template ?
|
|
54636
|
+
return template ? json4({ template: await store.templates.getWithTasks(id) }) : error(404, "template not found");
|
|
51861
54637
|
}
|
|
51862
54638
|
if (method === "DELETE") {
|
|
51863
54639
|
const deleted = await store.templates.delete(id, contextFromPrincipal(principal));
|
|
51864
|
-
return deleted ?
|
|
54640
|
+
return deleted ? json4({ deleted: true, id }) : error(404, "template not found");
|
|
51865
54641
|
}
|
|
51866
54642
|
return error(405, `method ${method} not allowed on /v1/templates/:id`);
|
|
51867
54643
|
}
|
|
51868
54644
|
if (resource === "agents") {
|
|
51869
54645
|
if (!id && method === "GET") {
|
|
51870
54646
|
const agents = await store.agents.list();
|
|
51871
|
-
return
|
|
54647
|
+
return json4({ agents, count: agents.length });
|
|
51872
54648
|
}
|
|
51873
54649
|
if (!id && method === "POST") {
|
|
51874
|
-
const body = await
|
|
54650
|
+
const body = await readJson3(req);
|
|
51875
54651
|
if (!body || typeof body.name !== "string" || !body.name.trim())
|
|
51876
54652
|
return error(400, "name is required");
|
|
51877
54653
|
const result = await store.agents.register(body, contextFromPrincipal(principal));
|
|
51878
54654
|
if (result && typeof result === "object" && "conflict" in result) {
|
|
51879
54655
|
return error(409, result.message ?? "agent name conflict", { conflict: true });
|
|
51880
54656
|
}
|
|
51881
|
-
return
|
|
54657
|
+
return json4({ agent: result }, 201);
|
|
51882
54658
|
}
|
|
51883
54659
|
if (id && action === "heartbeat") {
|
|
51884
54660
|
if (method !== "POST")
|
|
@@ -51887,7 +54663,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51887
54663
|
return error(501, "agent heartbeat is not supported by this storage backend");
|
|
51888
54664
|
}
|
|
51889
54665
|
const agent = await store.agents.heartbeat(id, contextFromPrincipal(principal));
|
|
51890
|
-
return agent ?
|
|
54666
|
+
return agent ? json4({ agent }) : error(404, "agent not found");
|
|
51891
54667
|
}
|
|
51892
54668
|
if (id && action === "release") {
|
|
51893
54669
|
if (method !== "POST")
|
|
@@ -51895,18 +54671,18 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51895
54671
|
if (typeof store.agents.release !== "function") {
|
|
51896
54672
|
return error(501, "agent release is not supported by this storage backend");
|
|
51897
54673
|
}
|
|
51898
|
-
const body = await
|
|
54674
|
+
const body = await readJson3(req) ?? {};
|
|
51899
54675
|
const result = await store.agents.release(id, body.session_id, contextFromPrincipal(principal));
|
|
51900
54676
|
if (!result)
|
|
51901
54677
|
return error(404, "agent not found");
|
|
51902
54678
|
if (!result.released) {
|
|
51903
54679
|
return error(409, "release denied: session_id does not match agent's current session", { released: false });
|
|
51904
54680
|
}
|
|
51905
|
-
return
|
|
54681
|
+
return json4({ agent: result.agent, released: true });
|
|
51906
54682
|
}
|
|
51907
54683
|
if (id && method === "GET") {
|
|
51908
54684
|
const agent = await store.agents.get(id);
|
|
51909
|
-
return agent ?
|
|
54685
|
+
return agent ? json4({ agent }) : error(404, "agent not found");
|
|
51910
54686
|
}
|
|
51911
54687
|
}
|
|
51912
54688
|
if (resource === "activity" && !id) {
|
|
@@ -51915,16 +54691,16 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51915
54691
|
const limitParam = url.searchParams.get("limit");
|
|
51916
54692
|
const limit = limitParam ? Math.max(1, Math.min(1e4, Number(limitParam) || 50)) : 50;
|
|
51917
54693
|
const activity = await store.audit.getRecentActivity(limit);
|
|
51918
|
-
return
|
|
54694
|
+
return json4({ activity, count: activity.length });
|
|
51919
54695
|
}
|
|
51920
54696
|
if (resource === "task-lists") {
|
|
51921
54697
|
if (!id && method === "GET") {
|
|
51922
54698
|
const projectId = url.searchParams.get("project_id") ?? undefined;
|
|
51923
54699
|
const taskLists = await store.taskLists.list(projectId);
|
|
51924
|
-
return
|
|
54700
|
+
return json4({ task_lists: taskLists, count: taskLists.length });
|
|
51925
54701
|
}
|
|
51926
54702
|
if (!id && method === "POST") {
|
|
51927
|
-
const body = await
|
|
54703
|
+
const body = await readJson3(req);
|
|
51928
54704
|
if (!body || typeof body.name !== "string" || !body.name.trim())
|
|
51929
54705
|
return error(400, "name is required");
|
|
51930
54706
|
const unknownField = Object.keys(body).find((key) => !["name", "slug", "project_id", "description", "metadata"].includes(key));
|
|
@@ -51943,14 +54719,14 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51943
54719
|
return error(400, "task-list slug must be non-empty kebab-case");
|
|
51944
54720
|
}
|
|
51945
54721
|
const taskList = await store.taskLists.create(body, contextFromPrincipal(principal));
|
|
51946
|
-
return
|
|
54722
|
+
return json4({ task_list: taskList }, 201);
|
|
51947
54723
|
}
|
|
51948
54724
|
if (id && method === "GET") {
|
|
51949
54725
|
const taskList = await store.taskLists.get(id);
|
|
51950
|
-
return taskList ?
|
|
54726
|
+
return taskList ? json4({ task_list: taskList }) : error(404, "task list not found");
|
|
51951
54727
|
}
|
|
51952
54728
|
if (id && (method === "PATCH" || method === "PUT")) {
|
|
51953
|
-
const body = await
|
|
54729
|
+
const body = await readJson3(req);
|
|
51954
54730
|
if (!body)
|
|
51955
54731
|
return error(400, "invalid JSON body");
|
|
51956
54732
|
const unknownField = Object.keys(body).find((key) => !["slug", "name", "description", "metadata"].includes(key));
|
|
@@ -51970,11 +54746,11 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51970
54746
|
if (!await store.taskLists.get(id))
|
|
51971
54747
|
return error(404, "task list not found");
|
|
51972
54748
|
const taskList = await store.taskLists.update(id, body);
|
|
51973
|
-
return
|
|
54749
|
+
return json4({ task_list: taskList });
|
|
51974
54750
|
}
|
|
51975
54751
|
if (id && method === "DELETE") {
|
|
51976
54752
|
const deleted = await store.taskLists.delete(id, contextFromPrincipal(principal));
|
|
51977
|
-
return deleted ?
|
|
54753
|
+
return deleted ? json4({ deleted: true, id }) : error(404, "task list not found");
|
|
51978
54754
|
}
|
|
51979
54755
|
return error(405, `method ${method} not allowed on /v1/task-lists${id ? "/:id" : ""}`);
|
|
51980
54756
|
}
|
|
@@ -51985,7 +54761,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51985
54761
|
return error(501, "dependency edge listing is not supported by this storage backend");
|
|
51986
54762
|
}
|
|
51987
54763
|
const dependencies2 = await store.dependencies.listAll();
|
|
51988
|
-
return
|
|
54764
|
+
return json4({ dependencies: dependencies2, count: dependencies2.length });
|
|
51989
54765
|
}
|
|
51990
54766
|
if (resource === "commits" && id) {
|
|
51991
54767
|
if (method !== "GET")
|
|
@@ -51993,7 +54769,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51993
54769
|
if (!store.commits)
|
|
51994
54770
|
return error(501, "commit links are not supported by this storage backend");
|
|
51995
54771
|
const commit = await store.commits.find(id);
|
|
51996
|
-
return
|
|
54772
|
+
return json4({ commit: commit ?? null });
|
|
51997
54773
|
}
|
|
51998
54774
|
if (resource === "refs" && id) {
|
|
51999
54775
|
if (method !== "GET")
|
|
@@ -52007,7 +54783,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
52007
54783
|
return error(400, "ref path segment has invalid percent encoding");
|
|
52008
54784
|
}
|
|
52009
54785
|
const refs = await store.gitRefs.find(decodedRef);
|
|
52010
|
-
return
|
|
54786
|
+
return json4({ refs, count: refs.length });
|
|
52011
54787
|
}
|
|
52012
54788
|
if (resource === "next" && !id) {
|
|
52013
54789
|
if (method !== "GET")
|
|
@@ -52019,7 +54795,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
52019
54795
|
...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {}
|
|
52020
54796
|
};
|
|
52021
54797
|
const task2 = await store.tasks.getNext(agent, filters);
|
|
52022
|
-
return
|
|
54798
|
+
return json4({ task: task2 ?? null });
|
|
52023
54799
|
}
|
|
52024
54800
|
if (resource === "stats" && method === "GET") {
|
|
52025
54801
|
const [tasks, tasksAll, projects] = await Promise.all([
|
|
@@ -52027,7 +54803,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
52027
54803
|
store.tasks.count({ include_subtasks: true }),
|
|
52028
54804
|
store.projects.list()
|
|
52029
54805
|
]);
|
|
52030
|
-
return
|
|
54806
|
+
return json4({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
|
|
52031
54807
|
}
|
|
52032
54808
|
if (resource === "integrity" && !id) {
|
|
52033
54809
|
if (method !== "GET")
|
|
@@ -52036,7 +54812,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
52036
54812
|
return error(501, "referential-integrity reporting is not supported by this storage backend");
|
|
52037
54813
|
}
|
|
52038
54814
|
const integrity = await store.integrity.report();
|
|
52039
|
-
return
|
|
54815
|
+
return json4({ integrity });
|
|
52040
54816
|
}
|
|
52041
54817
|
if (resource === "import") {
|
|
52042
54818
|
if (method !== "POST")
|
|
@@ -52044,7 +54820,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
52044
54820
|
if (typeof store.sync.importSnapshot !== "function") {
|
|
52045
54821
|
return error(501, "snapshot import is not supported by this storage backend");
|
|
52046
54822
|
}
|
|
52047
|
-
const raw = await
|
|
54823
|
+
const raw = await readJson3(req);
|
|
52048
54824
|
if (raw === null)
|
|
52049
54825
|
return error(400, "invalid JSON body");
|
|
52050
54826
|
const snapshot = normalizeImportSnapshot(raw);
|
|
@@ -52053,10 +54829,14 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
52053
54829
|
return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
|
|
52054
54830
|
}
|
|
52055
54831
|
const result = await store.sync.importSnapshot(snapshot, contextFromPrincipal(principal));
|
|
52056
|
-
return
|
|
54832
|
+
return json4({ result, received });
|
|
52057
54833
|
}
|
|
52058
54834
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
52059
54835
|
} catch (e) {
|
|
54836
|
+
if (e instanceof ProjectTaskListEnsureError) {
|
|
54837
|
+
const status = e.code === "PROJECT_NOT_FOUND" || e.code === "PROJECT_TASK_LIST_RECEIPT_NOT_FOUND" ? 404 : e.code === "PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID" ? 400 : 409;
|
|
54838
|
+
return error(status, e.message, { code: e.code, conflict: status === 409, ...e.details });
|
|
54839
|
+
}
|
|
52060
54840
|
if (e instanceof TaskReferenceAmbiguousError) {
|
|
52061
54841
|
return error(409, e.message, {
|
|
52062
54842
|
code: TaskReferenceAmbiguousError.code,
|
|
@@ -52076,13 +54856,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
52076
54856
|
return error(500, e.message || "internal error");
|
|
52077
54857
|
}
|
|
52078
54858
|
}
|
|
52079
|
-
var
|
|
54859
|
+
var JSON_HEADERS3, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500;
|
|
52080
54860
|
var init_v1 = __esm(() => {
|
|
52081
54861
|
init_types();
|
|
52082
54862
|
init_cloud();
|
|
52083
54863
|
init_pr_groups();
|
|
54864
|
+
init_project_registration();
|
|
52084
54865
|
init_redaction();
|
|
52085
|
-
|
|
54866
|
+
init_project_task_list_ensure();
|
|
54867
|
+
JSON_HEADERS3 = { "Content-Type": "application/json" };
|
|
52086
54868
|
});
|
|
52087
54869
|
|
|
52088
54870
|
// src/pr-groups/sqlite.ts
|
|
@@ -52241,12 +55023,12 @@ class SqlitePrGroupLedgerPersistence {
|
|
|
52241
55023
|
this.tx = new SqlitePrGroupTransaction(db);
|
|
52242
55024
|
}
|
|
52243
55025
|
async transaction(fn) {
|
|
52244
|
-
const previous =
|
|
55026
|
+
const previous = sqliteTransactionTails2.get(this.db) ?? Promise.resolve();
|
|
52245
55027
|
let release;
|
|
52246
55028
|
const current = new Promise((resolve17) => {
|
|
52247
55029
|
release = resolve17;
|
|
52248
55030
|
});
|
|
52249
|
-
|
|
55031
|
+
sqliteTransactionTails2.set(this.db, current);
|
|
52250
55032
|
await previous;
|
|
52251
55033
|
try {
|
|
52252
55034
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -52260,8 +55042,8 @@ class SqlitePrGroupLedgerPersistence {
|
|
|
52260
55042
|
throw error2;
|
|
52261
55043
|
} finally {
|
|
52262
55044
|
release();
|
|
52263
|
-
if (
|
|
52264
|
-
|
|
55045
|
+
if (sqliteTransactionTails2.get(this.db) === current)
|
|
55046
|
+
sqliteTransactionTails2.delete(this.db);
|
|
52265
55047
|
}
|
|
52266
55048
|
}
|
|
52267
55049
|
getGroup(id) {
|
|
@@ -52291,9 +55073,9 @@ class SqlitePrGroupLedgerPersistence {
|
|
|
52291
55073
|
return row ? eventFromRow2(row) : null;
|
|
52292
55074
|
}
|
|
52293
55075
|
}
|
|
52294
|
-
var
|
|
52295
|
-
var
|
|
52296
|
-
|
|
55076
|
+
var sqliteTransactionTails2;
|
|
55077
|
+
var init_sqlite2 = __esm(() => {
|
|
55078
|
+
sqliteTransactionTails2 = new WeakMap;
|
|
52297
55079
|
});
|
|
52298
55080
|
|
|
52299
55081
|
// src/pr-groups/index.ts
|
|
@@ -52323,10 +55105,10 @@ function createLocalPrGroupLedger(db = getDatabase()) {
|
|
|
52323
55105
|
var init_pr_groups2 = __esm(() => {
|
|
52324
55106
|
init_database();
|
|
52325
55107
|
init_ledger();
|
|
52326
|
-
|
|
55108
|
+
init_sqlite2();
|
|
52327
55109
|
init_types3();
|
|
52328
55110
|
init_ledger();
|
|
52329
|
-
|
|
55111
|
+
init_sqlite2();
|
|
52330
55112
|
init_http_client();
|
|
52331
55113
|
init_postgres();
|
|
52332
55114
|
});
|
|
@@ -52337,7 +55119,7 @@ __export(exports_serve, {
|
|
|
52337
55119
|
taskToSummary: () => taskToSummary,
|
|
52338
55120
|
startServer: () => startServer,
|
|
52339
55121
|
serveStaticFile: () => serveStaticFile,
|
|
52340
|
-
json: () =>
|
|
55122
|
+
json: () => json2,
|
|
52341
55123
|
checkAuth: () => checkAuth,
|
|
52342
55124
|
SECURITY_HEADERS: () => SECURITY_HEADERS,
|
|
52343
55125
|
MIME_TYPES: () => MIME_TYPES
|
|
@@ -52423,7 +55205,7 @@ function checkRateLimit(ip) {
|
|
|
52423
55205
|
}
|
|
52424
55206
|
return { allowed: true };
|
|
52425
55207
|
}
|
|
52426
|
-
function
|
|
55208
|
+
function json2(data, status = 200, headers) {
|
|
52427
55209
|
return new Response(JSON.stringify(data), {
|
|
52428
55210
|
status,
|
|
52429
55211
|
headers: {
|
|
@@ -52568,7 +55350,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52568
55350
|
"Access-Control-Allow-Headers": "Content-Type, X-API-Key, Authorization",
|
|
52569
55351
|
Vary: "Origin"
|
|
52570
55352
|
} : undefined;
|
|
52571
|
-
const jsonWithCors = (data, status = 200) =>
|
|
55353
|
+
const jsonWithCors = (data, status = 200) => json2(data, status, corsHeaders);
|
|
52572
55354
|
if (method === "OPTIONS") {
|
|
52573
55355
|
return new Response(null, {
|
|
52574
55356
|
headers: corsHeaders || {
|
|
@@ -52649,13 +55431,13 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52649
55431
|
return res;
|
|
52650
55432
|
}
|
|
52651
55433
|
if (path === "/api/health" && method === "GET") {
|
|
52652
|
-
return handleHealth(ctx,
|
|
55434
|
+
return handleHealth(ctx, json2);
|
|
52653
55435
|
}
|
|
52654
55436
|
if (path === "/api/headless" && method === "GET") {
|
|
52655
|
-
return handleHeadlessBoundary(ctx,
|
|
55437
|
+
return handleHeadlessBoundary(ctx, json2);
|
|
52656
55438
|
}
|
|
52657
55439
|
if (path === "/api/stats" && method === "GET") {
|
|
52658
|
-
return handleStats(ctx,
|
|
55440
|
+
return handleStats(ctx, json2);
|
|
52659
55441
|
}
|
|
52660
55442
|
if (path === "/api/tasks" && method === "GET") {
|
|
52661
55443
|
return handleListTasks(req, url, ctx, jsonWithCors, taskToSummary);
|
|
@@ -52670,16 +55452,16 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52670
55452
|
return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
|
|
52671
55453
|
}
|
|
52672
55454
|
if (path === "/api/tasks/bulk" && method === "POST") {
|
|
52673
|
-
return handleTasksBulk(req, ctx,
|
|
55455
|
+
return handleTasksBulk(req, ctx, json2);
|
|
52674
55456
|
}
|
|
52675
55457
|
if (path === "/api/tasks/status" && method === "GET") {
|
|
52676
|
-
return handleTasksStatus(req, url, ctx,
|
|
55458
|
+
return handleTasksStatus(req, url, ctx, json2);
|
|
52677
55459
|
}
|
|
52678
55460
|
if (path === "/api/tasks/next" && method === "GET") {
|
|
52679
55461
|
return handleTasksNext(req, url, ctx, jsonWithCors, taskToSummary);
|
|
52680
55462
|
}
|
|
52681
55463
|
if (path === "/api/tasks/active" && method === "GET") {
|
|
52682
|
-
return handleTasksActive(req, url, ctx,
|
|
55464
|
+
return handleTasksActive(req, url, ctx, json2);
|
|
52683
55465
|
}
|
|
52684
55466
|
if (path === "/api/tasks/stale" && method === "GET") {
|
|
52685
55467
|
return handleTasksStale(req, url, ctx, jsonWithCors, taskToSummary);
|
|
@@ -52692,11 +55474,11 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52692
55474
|
}
|
|
52693
55475
|
const attachmentsMatch = path.match(/^\/api\/tasks\/([^/]+)\/attachments$/);
|
|
52694
55476
|
if (attachmentsMatch && method === "GET") {
|
|
52695
|
-
return handleTaskAttachments(attachmentsMatch[1], ctx,
|
|
55477
|
+
return handleTaskAttachments(attachmentsMatch[1], ctx, json2);
|
|
52696
55478
|
}
|
|
52697
55479
|
const progressMatch = path.match(/^\/api\/tasks\/([^/]+)\/progress$/);
|
|
52698
55480
|
if (progressMatch) {
|
|
52699
|
-
const res = await handleTaskProgress(progressMatch[1], req, method, ctx,
|
|
55481
|
+
const res = await handleTaskProgress(progressMatch[1], req, method, ctx, json2, url);
|
|
52700
55482
|
if (res !== null)
|
|
52701
55483
|
return res;
|
|
52702
55484
|
}
|
|
@@ -52710,7 +55492,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52710
55492
|
return handlePatchTask(id, req, ctx, jsonWithCors, taskToSummary);
|
|
52711
55493
|
}
|
|
52712
55494
|
if (method === "DELETE") {
|
|
52713
|
-
return handleDeleteTask(id, ctx,
|
|
55495
|
+
return handleDeleteTask(id, ctx, json2);
|
|
52714
55496
|
}
|
|
52715
55497
|
}
|
|
52716
55498
|
const startMatch = path.match(/^\/api\/tasks\/([^/]+)\/start$/);
|
|
@@ -52726,7 +55508,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52726
55508
|
return handleCompleteTask(completeMatch[1], ctx, jsonWithCors, taskToSummary);
|
|
52727
55509
|
}
|
|
52728
55510
|
if (path === "/api/projects" && method === "GET") {
|
|
52729
|
-
return handleListProjects(url, ctx,
|
|
55511
|
+
return handleListProjects(url, ctx, json2);
|
|
52730
55512
|
}
|
|
52731
55513
|
if (path === "/api/agents/me" && method === "GET") {
|
|
52732
55514
|
return handleAgentMe(req, url, ctx, jsonWithCors, taskToSummary);
|
|
@@ -52739,92 +55521,92 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52739
55521
|
return handleClaimTask(req, ctx, jsonWithCors, taskToSummary);
|
|
52740
55522
|
}
|
|
52741
55523
|
if (path === "/api/orgs" && method === "GET") {
|
|
52742
|
-
return handleListOrgs(ctx,
|
|
55524
|
+
return handleListOrgs(ctx, json2);
|
|
52743
55525
|
}
|
|
52744
55526
|
if (path === "/api/orgs" && method === "POST") {
|
|
52745
|
-
return handleCreateOrg(req, ctx,
|
|
55527
|
+
return handleCreateOrg(req, ctx, json2);
|
|
52746
55528
|
}
|
|
52747
55529
|
const orgMatch = path.match(/^\/api\/orgs\/([^/]+)$/);
|
|
52748
55530
|
if (orgMatch && method === "PATCH") {
|
|
52749
|
-
return handleUpdateOrg(orgMatch[1], req, ctx,
|
|
55531
|
+
return handleUpdateOrg(orgMatch[1], req, ctx, json2);
|
|
52750
55532
|
}
|
|
52751
55533
|
if (orgMatch && method === "DELETE") {
|
|
52752
|
-
return handleDeleteOrg(orgMatch[1], ctx,
|
|
55534
|
+
return handleDeleteOrg(orgMatch[1], ctx, json2);
|
|
52753
55535
|
}
|
|
52754
55536
|
if (path === "/api/org" && method === "GET") {
|
|
52755
|
-
return handleOrgChart(ctx,
|
|
55537
|
+
return handleOrgChart(ctx, json2);
|
|
52756
55538
|
}
|
|
52757
55539
|
const teamMatch = path.match(/^\/api\/agents\/([^/]+)\/team$/);
|
|
52758
55540
|
if (teamMatch && method === "GET") {
|
|
52759
|
-
return handleAgentTeam(teamMatch[1], ctx,
|
|
55541
|
+
return handleAgentTeam(teamMatch[1], ctx, json2);
|
|
52760
55542
|
}
|
|
52761
55543
|
if (path === "/api/agents" && method === "GET") {
|
|
52762
|
-
return handleListAgents(url, ctx,
|
|
55544
|
+
return handleListAgents(url, ctx, json2);
|
|
52763
55545
|
}
|
|
52764
55546
|
if (path === "/api/projects" && method === "POST") {
|
|
52765
|
-
return handleCreateProject(req, ctx,
|
|
55547
|
+
return handleCreateProject(req, ctx, json2);
|
|
52766
55548
|
}
|
|
52767
55549
|
const projectDeleteMatch = path.match(/^\/api\/projects\/([^/]+)$/);
|
|
52768
55550
|
if (projectDeleteMatch && method === "DELETE") {
|
|
52769
|
-
return handleDeleteProject(projectDeleteMatch[1], ctx,
|
|
55551
|
+
return handleDeleteProject(projectDeleteMatch[1], ctx, json2);
|
|
52770
55552
|
}
|
|
52771
55553
|
if (path === "/api/agents" && method === "POST") {
|
|
52772
|
-
return handleRegisterAgent(req, ctx,
|
|
55554
|
+
return handleRegisterAgent(req, ctx, json2);
|
|
52773
55555
|
}
|
|
52774
55556
|
const agentMatch = path.match(/^\/api\/agents\/([^/]+)$/);
|
|
52775
55557
|
if (agentMatch && method === "PATCH") {
|
|
52776
|
-
return handleUpdateAgent(agentMatch[1], req, ctx,
|
|
55558
|
+
return handleUpdateAgent(agentMatch[1], req, ctx, json2);
|
|
52777
55559
|
}
|
|
52778
55560
|
if (agentMatch && method === "DELETE") {
|
|
52779
|
-
return handleDeleteAgent(agentMatch[1], ctx,
|
|
55561
|
+
return handleDeleteAgent(agentMatch[1], ctx, json2);
|
|
52780
55562
|
}
|
|
52781
55563
|
if (path === "/api/agents/bulk" && method === "POST") {
|
|
52782
|
-
return handleBulkDeleteAgents(req, ctx,
|
|
55564
|
+
return handleBulkDeleteAgents(req, ctx, json2);
|
|
52783
55565
|
}
|
|
52784
55566
|
if (path === "/api/projects/bulk" && method === "POST") {
|
|
52785
|
-
return handleBulkDeleteProjects(req, ctx,
|
|
55567
|
+
return handleBulkDeleteProjects(req, ctx, json2);
|
|
52786
55568
|
}
|
|
52787
55569
|
if (path === "/api/doctor" && method === "GET") {
|
|
52788
|
-
return handleDoctor(ctx,
|
|
55570
|
+
return handleDoctor(ctx, json2);
|
|
52789
55571
|
}
|
|
52790
55572
|
if (path === "/api/report" && method === "GET") {
|
|
52791
|
-
return handleReport(req, url, ctx,
|
|
55573
|
+
return handleReport(req, url, ctx, json2);
|
|
52792
55574
|
}
|
|
52793
55575
|
if (path === "/api/activity" && method === "GET") {
|
|
52794
|
-
return handleActivity(req, url, ctx,
|
|
55576
|
+
return handleActivity(req, url, ctx, json2);
|
|
52795
55577
|
}
|
|
52796
55578
|
const historyMatch = path.match(/^\/api\/tasks\/([^/]+)\/history$/);
|
|
52797
55579
|
if (historyMatch && method === "GET") {
|
|
52798
|
-
return handleTaskHistory(historyMatch[1], ctx,
|
|
55580
|
+
return handleTaskHistory(historyMatch[1], ctx, json2, url);
|
|
52799
55581
|
}
|
|
52800
55582
|
if (path === "/api/webhooks" && method === "GET") {
|
|
52801
|
-
return handleListWebhooks(ctx,
|
|
55583
|
+
return handleListWebhooks(ctx, json2);
|
|
52802
55584
|
}
|
|
52803
55585
|
if (path === "/api/webhooks" && method === "POST") {
|
|
52804
|
-
return handleCreateWebhook(req, ctx,
|
|
55586
|
+
return handleCreateWebhook(req, ctx, json2);
|
|
52805
55587
|
}
|
|
52806
55588
|
const webhookMatch = path.match(/^\/api\/webhooks\/([^/]+)$/);
|
|
52807
55589
|
if (webhookMatch && method === "DELETE") {
|
|
52808
|
-
return handleDeleteWebhook(webhookMatch[1], ctx,
|
|
55590
|
+
return handleDeleteWebhook(webhookMatch[1], ctx, json2);
|
|
52809
55591
|
}
|
|
52810
55592
|
if (path === "/api/templates" && method === "GET") {
|
|
52811
|
-
return handleListTemplates(ctx,
|
|
55593
|
+
return handleListTemplates(ctx, json2);
|
|
52812
55594
|
}
|
|
52813
55595
|
if (path === "/api/templates" && method === "POST") {
|
|
52814
|
-
return handleCreateTemplate(req, ctx,
|
|
55596
|
+
return handleCreateTemplate(req, ctx, json2);
|
|
52815
55597
|
}
|
|
52816
55598
|
const templateMatch = path.match(/^\/api\/templates\/([^/]+)$/);
|
|
52817
55599
|
if (templateMatch && method === "DELETE") {
|
|
52818
|
-
return handleDeleteTemplate(templateMatch[1], ctx,
|
|
55600
|
+
return handleDeleteTemplate(templateMatch[1], ctx, json2);
|
|
52819
55601
|
}
|
|
52820
55602
|
if (path === "/api/plans" && method === "GET") {
|
|
52821
|
-
return handleListPlans(url, ctx,
|
|
55603
|
+
return handleListPlans(url, ctx, json2);
|
|
52822
55604
|
}
|
|
52823
55605
|
if (path === "/api/plans" && method === "POST") {
|
|
52824
|
-
return handleCreatePlan(req, ctx,
|
|
55606
|
+
return handleCreatePlan(req, ctx, json2);
|
|
52825
55607
|
}
|
|
52826
55608
|
if (path === "/api/plans/bulk" && method === "POST") {
|
|
52827
|
-
return handleBulkDeletePlans(req, ctx,
|
|
55609
|
+
return handleBulkDeletePlans(req, ctx, json2);
|
|
52828
55610
|
}
|
|
52829
55611
|
const planMatch = path.match(/^\/api\/plans\/([^/]+)$/);
|
|
52830
55612
|
if (planMatch) {
|
|
@@ -52833,16 +55615,16 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52833
55615
|
return handleGetPlan(id, ctx, jsonWithCors, taskToSummary);
|
|
52834
55616
|
}
|
|
52835
55617
|
if (method === "PATCH") {
|
|
52836
|
-
return handleUpdatePlan(id, req, ctx,
|
|
55618
|
+
return handleUpdatePlan(id, req, ctx, json2);
|
|
52837
55619
|
}
|
|
52838
55620
|
if (method === "DELETE") {
|
|
52839
|
-
return handleDeletePlan(id, ctx,
|
|
55621
|
+
return handleDeletePlan(id, ctx, json2);
|
|
52840
55622
|
}
|
|
52841
55623
|
}
|
|
52842
55624
|
const staticRes = handleStaticFiles(path, method, ctx, jsonWithCors, serveStaticFile);
|
|
52843
55625
|
if (staticRes)
|
|
52844
55626
|
return staticRes;
|
|
52845
|
-
return
|
|
55627
|
+
return json2({ error: "Not found" }, 404);
|
|
52846
55628
|
}
|
|
52847
55629
|
});
|
|
52848
55630
|
const shutdown = () => {
|