@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/index.js
CHANGED
|
@@ -39,6 +39,206 @@ var __export = (target, all) => {
|
|
|
39
39
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
40
40
|
var __require = import.meta.require;
|
|
41
41
|
|
|
42
|
+
// src/project-registration/schema.ts
|
|
43
|
+
function sqliteTodosProjectRegistrationSchemaSql() {
|
|
44
|
+
return `
|
|
45
|
+
CREATE TABLE IF NOT EXISTS todos_project_registration_receipts (
|
|
46
|
+
receipt_id TEXT PRIMARY KEY,
|
|
47
|
+
authority TEXT NOT NULL CHECK(authority = 'todos'),
|
|
48
|
+
route TEXT NOT NULL,
|
|
49
|
+
package_version TEXT NOT NULL,
|
|
50
|
+
authority_id TEXT NOT NULL,
|
|
51
|
+
tenant_id TEXT NOT NULL,
|
|
52
|
+
corpus_id TEXT NOT NULL,
|
|
53
|
+
operation_id TEXT NOT NULL,
|
|
54
|
+
step_id TEXT NOT NULL,
|
|
55
|
+
resource_kind TEXT NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
56
|
+
direction TEXT NOT NULL CHECK(direction IN ('forward', 'inverse')),
|
|
57
|
+
target_selector TEXT NOT NULL,
|
|
58
|
+
idempotency_key TEXT NOT NULL,
|
|
59
|
+
request_digest TEXT NOT NULL,
|
|
60
|
+
precondition_digest TEXT NOT NULL,
|
|
61
|
+
normalized_call_digest TEXT NOT NULL,
|
|
62
|
+
outcome TEXT NOT NULL CHECK(outcome IN (
|
|
63
|
+
'accepted', 'duplicate_of_accepted', 'terminal_nonacceptance'
|
|
64
|
+
)),
|
|
65
|
+
reason TEXT,
|
|
66
|
+
target_id TEXT,
|
|
67
|
+
result_revision TEXT,
|
|
68
|
+
result_digest TEXT,
|
|
69
|
+
duplicate_of_receipt_id TEXT,
|
|
70
|
+
accepted_receipt_id TEXT,
|
|
71
|
+
created_by_operation INTEGER NOT NULL CHECK(created_by_operation IN (0, 1)),
|
|
72
|
+
created_at TEXT NOT NULL
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_lookup
|
|
76
|
+
ON todos_project_registration_receipts (
|
|
77
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
78
|
+
resource_kind, direction, idempotency_key
|
|
79
|
+
);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
|
|
81
|
+
ON todos_project_registration_receipts (
|
|
82
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
83
|
+
resource_kind, direction, outcome
|
|
84
|
+
);
|
|
85
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_accepted_step
|
|
86
|
+
ON todos_project_registration_receipts (
|
|
87
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
88
|
+
resource_kind, direction
|
|
89
|
+
)
|
|
90
|
+
WHERE outcome = 'accepted';
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_target
|
|
92
|
+
ON todos_project_registration_receipts (
|
|
93
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS todos_project_registration_bindings (
|
|
97
|
+
authority_id TEXT NOT NULL,
|
|
98
|
+
tenant_id TEXT NOT NULL,
|
|
99
|
+
corpus_id TEXT NOT NULL,
|
|
100
|
+
resource_kind TEXT NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
101
|
+
target_selector TEXT NOT NULL,
|
|
102
|
+
operation_id TEXT NOT NULL,
|
|
103
|
+
step_id TEXT NOT NULL,
|
|
104
|
+
direction TEXT NOT NULL CHECK(direction = 'forward'),
|
|
105
|
+
idempotency_key TEXT NOT NULL,
|
|
106
|
+
request_digest TEXT NOT NULL,
|
|
107
|
+
precondition_digest TEXT NOT NULL,
|
|
108
|
+
normalized_call_digest TEXT NOT NULL,
|
|
109
|
+
state TEXT NOT NULL CHECK(state IN (
|
|
110
|
+
'pending', 'accepted', 'terminal_nonacceptance', 'removed'
|
|
111
|
+
)),
|
|
112
|
+
target_id TEXT,
|
|
113
|
+
accepted_receipt_id TEXT,
|
|
114
|
+
result_revision TEXT,
|
|
115
|
+
result_digest TEXT,
|
|
116
|
+
removed_receipt_id TEXT,
|
|
117
|
+
created_at TEXT NOT NULL,
|
|
118
|
+
updated_at TEXT NOT NULL,
|
|
119
|
+
PRIMARY KEY(
|
|
120
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector
|
|
121
|
+
),
|
|
122
|
+
UNIQUE(accepted_receipt_id)
|
|
123
|
+
);
|
|
124
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_project_registration_binding_target
|
|
125
|
+
ON todos_project_registration_bindings(
|
|
126
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
127
|
+
)
|
|
128
|
+
WHERE target_id IS NOT NULL;
|
|
129
|
+
|
|
130
|
+
CREATE TRIGGER IF NOT EXISTS todos_project_registration_receipts_immutable_update
|
|
131
|
+
BEFORE UPDATE ON todos_project_registration_receipts
|
|
132
|
+
BEGIN
|
|
133
|
+
SELECT RAISE(ABORT, 'todos project registration receipts are immutable');
|
|
134
|
+
END;
|
|
135
|
+
|
|
136
|
+
CREATE TRIGGER IF NOT EXISTS todos_project_registration_receipts_immutable_delete
|
|
137
|
+
BEFORE DELETE ON todos_project_registration_receipts
|
|
138
|
+
BEGIN
|
|
139
|
+
SELECT RAISE(ABORT, 'todos project registration receipts are immutable');
|
|
140
|
+
END;
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
function postgresTodosProjectRegistrationSchemaSql() {
|
|
144
|
+
return [
|
|
145
|
+
`CREATE TABLE IF NOT EXISTS todos_project_registration_receipts (
|
|
146
|
+
receipt_id text PRIMARY KEY,
|
|
147
|
+
authority text NOT NULL CHECK(authority = 'todos'),
|
|
148
|
+
route text NOT NULL,
|
|
149
|
+
package_version text NOT NULL,
|
|
150
|
+
authority_id text NOT NULL,
|
|
151
|
+
tenant_id text NOT NULL,
|
|
152
|
+
corpus_id text NOT NULL,
|
|
153
|
+
operation_id text NOT NULL,
|
|
154
|
+
step_id text NOT NULL,
|
|
155
|
+
resource_kind text NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
156
|
+
direction text NOT NULL CHECK(direction IN ('forward', 'inverse')),
|
|
157
|
+
target_selector text NOT NULL,
|
|
158
|
+
idempotency_key text NOT NULL,
|
|
159
|
+
request_digest text NOT NULL,
|
|
160
|
+
precondition_digest text NOT NULL,
|
|
161
|
+
normalized_call_digest text NOT NULL,
|
|
162
|
+
outcome text NOT NULL CHECK(outcome IN (
|
|
163
|
+
'accepted', 'duplicate_of_accepted', 'terminal_nonacceptance'
|
|
164
|
+
)),
|
|
165
|
+
reason text,
|
|
166
|
+
target_id text,
|
|
167
|
+
result_revision text,
|
|
168
|
+
result_digest text,
|
|
169
|
+
duplicate_of_receipt_id text,
|
|
170
|
+
accepted_receipt_id text,
|
|
171
|
+
created_by_operation boolean NOT NULL,
|
|
172
|
+
created_at timestamptz NOT NULL
|
|
173
|
+
)`,
|
|
174
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_lookup_idx
|
|
175
|
+
ON todos_project_registration_receipts (
|
|
176
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
177
|
+
resource_kind, direction, idempotency_key
|
|
178
|
+
)`,
|
|
179
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
|
|
180
|
+
ON todos_project_registration_receipts (
|
|
181
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
182
|
+
resource_kind, direction, outcome
|
|
183
|
+
)`,
|
|
184
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_project_registration_receipts_accepted_step_uidx
|
|
185
|
+
ON todos_project_registration_receipts (
|
|
186
|
+
authority_id, tenant_id, corpus_id, operation_id, step_id,
|
|
187
|
+
resource_kind, direction
|
|
188
|
+
)
|
|
189
|
+
WHERE outcome = 'accepted'`,
|
|
190
|
+
`CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_target_idx
|
|
191
|
+
ON todos_project_registration_receipts (
|
|
192
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
193
|
+
)`,
|
|
194
|
+
`CREATE TABLE IF NOT EXISTS todos_project_registration_bindings (
|
|
195
|
+
authority_id text NOT NULL,
|
|
196
|
+
tenant_id text NOT NULL,
|
|
197
|
+
corpus_id text NOT NULL,
|
|
198
|
+
resource_kind text NOT NULL CHECK(resource_kind IN ('project', 'task_list')),
|
|
199
|
+
target_selector text NOT NULL,
|
|
200
|
+
operation_id text NOT NULL,
|
|
201
|
+
step_id text NOT NULL,
|
|
202
|
+
direction text NOT NULL CHECK(direction = 'forward'),
|
|
203
|
+
idempotency_key text NOT NULL,
|
|
204
|
+
request_digest text NOT NULL,
|
|
205
|
+
precondition_digest text NOT NULL,
|
|
206
|
+
normalized_call_digest text NOT NULL,
|
|
207
|
+
state text NOT NULL CHECK(state IN (
|
|
208
|
+
'pending', 'accepted', 'terminal_nonacceptance', 'removed'
|
|
209
|
+
)),
|
|
210
|
+
target_id text,
|
|
211
|
+
accepted_receipt_id text UNIQUE,
|
|
212
|
+
result_revision text,
|
|
213
|
+
result_digest text,
|
|
214
|
+
removed_receipt_id text,
|
|
215
|
+
created_at timestamptz NOT NULL,
|
|
216
|
+
updated_at timestamptz NOT NULL,
|
|
217
|
+
PRIMARY KEY(
|
|
218
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector
|
|
219
|
+
)
|
|
220
|
+
)`,
|
|
221
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS todos_project_registration_binding_target_uidx
|
|
222
|
+
ON todos_project_registration_bindings(
|
|
223
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_id
|
|
224
|
+
)
|
|
225
|
+
WHERE target_id IS NOT NULL`,
|
|
226
|
+
`CREATE OR REPLACE FUNCTION todos_project_registration_receipts_immutable()
|
|
227
|
+
RETURNS trigger
|
|
228
|
+
LANGUAGE plpgsql
|
|
229
|
+
AS $$
|
|
230
|
+
BEGIN
|
|
231
|
+
RAISE EXCEPTION 'todos project registration receipts are immutable';
|
|
232
|
+
END;
|
|
233
|
+
$$`,
|
|
234
|
+
`DROP TRIGGER IF EXISTS todos_project_registration_receipts_immutable
|
|
235
|
+
ON todos_project_registration_receipts`,
|
|
236
|
+
`CREATE TRIGGER todos_project_registration_receipts_immutable
|
|
237
|
+
BEFORE UPDATE OR DELETE ON todos_project_registration_receipts
|
|
238
|
+
FOR EACH ROW EXECUTE FUNCTION todos_project_registration_receipts_immutable()`
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
|
|
42
242
|
// src/db/migrations.ts
|
|
43
243
|
var MIGRATIONS;
|
|
44
244
|
var init_migrations = __esm(() => {
|
|
@@ -1644,6 +1844,11 @@ var init_migrations = __esm(() => {
|
|
|
1644
1844
|
INSERT OR IGNORE INTO _migrations (id) VALUES (68);
|
|
1645
1845
|
COMMIT;
|
|
1646
1846
|
PRAGMA foreign_keys = ON;
|
|
1847
|
+
`,
|
|
1848
|
+
`BEGIN;
|
|
1849
|
+
${sqliteTodosProjectRegistrationSchemaSql()}
|
|
1850
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (69);
|
|
1851
|
+
COMMIT;
|
|
1647
1852
|
`
|
|
1648
1853
|
];
|
|
1649
1854
|
});
|
|
@@ -2846,6 +3051,7 @@ function ensureSchema(db) {
|
|
|
2846
3051
|
)`);
|
|
2847
3052
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix)");
|
|
2848
3053
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at)");
|
|
3054
|
+
db.exec(sqliteTodosProjectRegistrationSchemaSql());
|
|
2849
3055
|
ensureTable("pr_groups", `
|
|
2850
3056
|
CREATE TABLE pr_groups (
|
|
2851
3057
|
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
@@ -6541,6 +6747,40 @@ function deleteTaskList(id, db) {
|
|
|
6541
6747
|
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
6542
6748
|
})();
|
|
6543
6749
|
}
|
|
6750
|
+
function deleteTaskListIfUnchangedAndUnused(id, expected, db) {
|
|
6751
|
+
const d = db || getDatabase();
|
|
6752
|
+
return d.transaction(() => {
|
|
6753
|
+
const current = getTaskList(id, d);
|
|
6754
|
+
if (!current) {
|
|
6755
|
+
return { status: "not_found", task_dependents: 0, plan_dependents: 0 };
|
|
6756
|
+
}
|
|
6757
|
+
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);
|
|
6758
|
+
if (changed) {
|
|
6759
|
+
return { status: "changed", task_dependents: 0, plan_dependents: 0 };
|
|
6760
|
+
}
|
|
6761
|
+
const taskDependents = Number(d.query("SELECT COUNT(*) AS count FROM tasks WHERE task_list_id = ?").get(id).count);
|
|
6762
|
+
const planDependents = Number(d.query("SELECT COUNT(*) AS count FROM plans WHERE task_list_id = ?").get(id).count);
|
|
6763
|
+
if (taskDependents > 0 || planDependents > 0) {
|
|
6764
|
+
return {
|
|
6765
|
+
status: "has_dependents",
|
|
6766
|
+
task_dependents: taskDependents,
|
|
6767
|
+
plan_dependents: planDependents
|
|
6768
|
+
};
|
|
6769
|
+
}
|
|
6770
|
+
recordStorageTombstone({
|
|
6771
|
+
object_type: "task_lists",
|
|
6772
|
+
object_id: id,
|
|
6773
|
+
payload: current
|
|
6774
|
+
}, d);
|
|
6775
|
+
releaseCanonicalSlugClaims("task_list", id, d);
|
|
6776
|
+
const deleted = d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
6777
|
+
return {
|
|
6778
|
+
status: deleted ? "deleted" : "not_found",
|
|
6779
|
+
task_dependents: 0,
|
|
6780
|
+
plan_dependents: 0
|
|
6781
|
+
};
|
|
6782
|
+
})();
|
|
6783
|
+
}
|
|
6544
6784
|
function ensureTaskList(name, slug, projectId, db) {
|
|
6545
6785
|
const d = db || getDatabase();
|
|
6546
6786
|
const existing = getTaskListBySlug(slug, projectId, d);
|
|
@@ -12157,7 +12397,7 @@ var init_dispatches = __esm(() => {
|
|
|
12157
12397
|
// package.json
|
|
12158
12398
|
var package_default = {
|
|
12159
12399
|
name: "@hasna/todos",
|
|
12160
|
-
version: "0.15.
|
|
12400
|
+
version: "0.15.9",
|
|
12161
12401
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12162
12402
|
type: "module",
|
|
12163
12403
|
main: "dist/index.js",
|
|
@@ -12195,6 +12435,10 @@ var package_default = {
|
|
|
12195
12435
|
"./testing": {
|
|
12196
12436
|
types: "./dist/testing.d.ts",
|
|
12197
12437
|
import: "./dist/testing.js"
|
|
12438
|
+
},
|
|
12439
|
+
"./project-registration": {
|
|
12440
|
+
types: "./dist/project-registration.d.ts",
|
|
12441
|
+
import: "./dist/project-registration.js"
|
|
12198
12442
|
}
|
|
12199
12443
|
},
|
|
12200
12444
|
workspaces: [
|
|
@@ -12207,8 +12451,8 @@ var package_default = {
|
|
|
12207
12451
|
"README.md"
|
|
12208
12452
|
],
|
|
12209
12453
|
scripts: {
|
|
12210
|
-
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",
|
|
12211
|
-
"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/*'",
|
|
12454
|
+
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",
|
|
12455
|
+
"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/*'",
|
|
12212
12456
|
migrate: "bun run src/server/index.ts migrate",
|
|
12213
12457
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
12214
12458
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
@@ -23851,6 +24095,13 @@ var TODOS_PACKAGE_EXPORTS = [
|
|
|
23851
24095
|
types: "./dist/testing.d.ts",
|
|
23852
24096
|
description: "Test-store isolation helpers that keep a consumer's test suite off a shared todos store.",
|
|
23853
24097
|
stability: "stable"
|
|
24098
|
+
},
|
|
24099
|
+
{
|
|
24100
|
+
subpath: "./project-registration",
|
|
24101
|
+
import: "./dist/project-registration.js",
|
|
24102
|
+
types: "./dist/project-registration.d.ts",
|
|
24103
|
+
description: "Conditional Projects-to-Todos project and task-list registration authority.",
|
|
24104
|
+
stability: "stable"
|
|
23854
24105
|
}
|
|
23855
24106
|
];
|
|
23856
24107
|
function source8(version) {
|
|
@@ -25479,7 +25730,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
25479
25730
|
getBySlug: (slug, projectId) => getTaskListBySlug(slug, projectId, database()),
|
|
25480
25731
|
list: (projectId) => listTaskLists(projectId, database()),
|
|
25481
25732
|
update: (id, input) => updateTaskList(id, input, database()),
|
|
25482
|
-
delete: (id) => deleteTaskList(id, database())
|
|
25733
|
+
delete: (id) => deleteTaskList(id, database()),
|
|
25734
|
+
deleteIfUnchangedAndUnused: (id, expected) => deleteTaskListIfUnchangedAndUnused(id, expected, database())
|
|
25483
25735
|
},
|
|
25484
25736
|
templates: {
|
|
25485
25737
|
create: (input) => createTemplate(input, database()),
|
|
@@ -31896,6 +32148,2115 @@ class PrGroupHttpClient {
|
|
|
31896
32148
|
function createLocalPrGroupLedger(db = getDatabase()) {
|
|
31897
32149
|
return new PrGroupLedger(new SqlitePrGroupLedgerPersistence(db));
|
|
31898
32150
|
}
|
|
32151
|
+
// src/project-registration/authority.ts
|
|
32152
|
+
import { createHash as createHash12 } from "crypto";
|
|
32153
|
+
// src/project-registration/types.ts
|
|
32154
|
+
var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1";
|
|
32155
|
+
var TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1";
|
|
32156
|
+
var TODOS_PROJECT_REGISTRATION_SCHEMA_VERSION = 1;
|
|
32157
|
+
|
|
32158
|
+
class TodosProjectRegistrationError extends Error {
|
|
32159
|
+
code;
|
|
32160
|
+
details;
|
|
32161
|
+
constructor(code, message, details = {}) {
|
|
32162
|
+
super(message);
|
|
32163
|
+
this.code = code;
|
|
32164
|
+
this.details = details;
|
|
32165
|
+
this.name = "TodosProjectRegistrationError";
|
|
32166
|
+
}
|
|
32167
|
+
}
|
|
32168
|
+
|
|
32169
|
+
// src/project-registration/postgres.ts
|
|
32170
|
+
function safeIdentifier(value, field2) {
|
|
32171
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(value)) {
|
|
32172
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", `${field2} must be a safe PostgreSQL identifier`);
|
|
32173
|
+
}
|
|
32174
|
+
return value;
|
|
32175
|
+
}
|
|
32176
|
+
function normalizeTimestamp(value) {
|
|
32177
|
+
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
32178
|
+
}
|
|
32179
|
+
function parsePayload3(value) {
|
|
32180
|
+
if (typeof value === "string")
|
|
32181
|
+
return JSON.parse(value);
|
|
32182
|
+
return value;
|
|
32183
|
+
}
|
|
32184
|
+
function receiptFromRow(row) {
|
|
32185
|
+
return {
|
|
32186
|
+
...row,
|
|
32187
|
+
authority: "todos",
|
|
32188
|
+
created_by_operation: Boolean(row["created_by_operation"]),
|
|
32189
|
+
created_at: normalizeTimestamp(row["created_at"])
|
|
32190
|
+
};
|
|
32191
|
+
}
|
|
32192
|
+
function bindingFromRow(row) {
|
|
32193
|
+
return {
|
|
32194
|
+
...row,
|
|
32195
|
+
created_at: normalizeTimestamp(row["created_at"]),
|
|
32196
|
+
updated_at: normalizeTimestamp(row["updated_at"])
|
|
32197
|
+
};
|
|
32198
|
+
}
|
|
32199
|
+
|
|
32200
|
+
class PostgresTodosProjectRegistrationTransaction {
|
|
32201
|
+
client;
|
|
32202
|
+
service;
|
|
32203
|
+
tableName;
|
|
32204
|
+
storage;
|
|
32205
|
+
constructor(client, service, tableName, cursorTableName) {
|
|
32206
|
+
this.client = client;
|
|
32207
|
+
this.service = service;
|
|
32208
|
+
this.tableName = tableName;
|
|
32209
|
+
this.storage = createPostgresTodosStorageAdapter({
|
|
32210
|
+
client,
|
|
32211
|
+
service,
|
|
32212
|
+
tableName,
|
|
32213
|
+
cursorTableName
|
|
32214
|
+
});
|
|
32215
|
+
}
|
|
32216
|
+
async lockStep(identity) {
|
|
32217
|
+
const key = [
|
|
32218
|
+
identity.authority_id,
|
|
32219
|
+
identity.tenant_id,
|
|
32220
|
+
identity.corpus_id,
|
|
32221
|
+
identity.operation_id,
|
|
32222
|
+
identity.step_id,
|
|
32223
|
+
identity.resource_kind,
|
|
32224
|
+
identity.direction
|
|
32225
|
+
].join("\x1F");
|
|
32226
|
+
await this.client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [key]);
|
|
32227
|
+
}
|
|
32228
|
+
async getReceiptForLookup(identity) {
|
|
32229
|
+
const result = await this.client.query(`
|
|
32230
|
+
SELECT * FROM todos_project_registration_receipts
|
|
32231
|
+
WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
|
|
32232
|
+
AND operation_id = $4 AND step_id = $5 AND resource_kind = $6
|
|
32233
|
+
AND direction = $7 AND idempotency_key = $8 AND target_selector = $9
|
|
32234
|
+
ORDER BY CASE outcome
|
|
32235
|
+
WHEN 'terminal_nonacceptance' THEN 0
|
|
32236
|
+
WHEN 'duplicate_of_accepted' THEN 1
|
|
32237
|
+
ELSE 2
|
|
32238
|
+
END, created_at DESC, receipt_id DESC
|
|
32239
|
+
LIMIT 1
|
|
32240
|
+
`, [
|
|
32241
|
+
identity.authority_id,
|
|
32242
|
+
identity.tenant_id,
|
|
32243
|
+
identity.corpus_id,
|
|
32244
|
+
identity.operation_id,
|
|
32245
|
+
identity.step_id,
|
|
32246
|
+
identity.resource_kind,
|
|
32247
|
+
identity.direction,
|
|
32248
|
+
identity.idempotency_key,
|
|
32249
|
+
identity.target_selector
|
|
32250
|
+
]);
|
|
32251
|
+
return result.rows[0] ? receiptFromRow(result.rows[0]) : null;
|
|
32252
|
+
}
|
|
32253
|
+
async getReceiptById(receiptId) {
|
|
32254
|
+
const result = await this.client.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = $1 LIMIT 1", [receiptId]);
|
|
32255
|
+
return result.rows[0] ? receiptFromRow(result.rows[0]) : null;
|
|
32256
|
+
}
|
|
32257
|
+
async getAcceptedReceiptForStep(identity) {
|
|
32258
|
+
const result = await this.client.query(`
|
|
32259
|
+
SELECT * FROM todos_project_registration_receipts
|
|
32260
|
+
WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
|
|
32261
|
+
AND operation_id = $4 AND step_id = $5 AND resource_kind = $6
|
|
32262
|
+
AND direction = $7 AND outcome = 'accepted'
|
|
32263
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
32264
|
+
LIMIT 1
|
|
32265
|
+
FOR UPDATE
|
|
32266
|
+
`, [
|
|
32267
|
+
identity.authority_id,
|
|
32268
|
+
identity.tenant_id,
|
|
32269
|
+
identity.corpus_id,
|
|
32270
|
+
identity.operation_id,
|
|
32271
|
+
identity.step_id,
|
|
32272
|
+
identity.resource_kind,
|
|
32273
|
+
identity.direction
|
|
32274
|
+
]);
|
|
32275
|
+
return result.rows[0] ? receiptFromRow(result.rows[0]) : null;
|
|
32276
|
+
}
|
|
32277
|
+
async insertReceipt(receipt) {
|
|
32278
|
+
const result = await this.client.query(`
|
|
32279
|
+
INSERT INTO todos_project_registration_receipts (
|
|
32280
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
32281
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
32282
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
32283
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
32284
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
32285
|
+
created_by_operation, created_at
|
|
32286
|
+
) VALUES (
|
|
32287
|
+
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,
|
|
32288
|
+
$19,$20,$21,$22,$23,$24,$25
|
|
32289
|
+
)
|
|
32290
|
+
ON CONFLICT (receipt_id) DO NOTHING
|
|
32291
|
+
RETURNING receipt_id
|
|
32292
|
+
`, [
|
|
32293
|
+
receipt.receipt_id,
|
|
32294
|
+
receipt.authority,
|
|
32295
|
+
receipt.route,
|
|
32296
|
+
receipt.package_version,
|
|
32297
|
+
receipt.authority_id,
|
|
32298
|
+
receipt.tenant_id,
|
|
32299
|
+
receipt.corpus_id,
|
|
32300
|
+
receipt.operation_id,
|
|
32301
|
+
receipt.step_id,
|
|
32302
|
+
receipt.resource_kind,
|
|
32303
|
+
receipt.direction,
|
|
32304
|
+
receipt.target_selector,
|
|
32305
|
+
receipt.idempotency_key,
|
|
32306
|
+
receipt.request_digest,
|
|
32307
|
+
receipt.precondition_digest,
|
|
32308
|
+
receipt.normalized_call_digest,
|
|
32309
|
+
receipt.outcome,
|
|
32310
|
+
receipt.reason,
|
|
32311
|
+
receipt.target_id,
|
|
32312
|
+
receipt.result_revision,
|
|
32313
|
+
receipt.result_digest,
|
|
32314
|
+
receipt.duplicate_of_receipt_id,
|
|
32315
|
+
receipt.accepted_receipt_id,
|
|
32316
|
+
receipt.created_by_operation,
|
|
32317
|
+
receipt.created_at
|
|
32318
|
+
]);
|
|
32319
|
+
return result.rows.length === 1;
|
|
32320
|
+
}
|
|
32321
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
32322
|
+
const result = await this.client.query(`
|
|
32323
|
+
SELECT * FROM todos_project_registration_bindings
|
|
32324
|
+
WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
|
|
32325
|
+
AND resource_kind = $4 AND target_selector = $5
|
|
32326
|
+
LIMIT 1
|
|
32327
|
+
FOR UPDATE
|
|
32328
|
+
`, [
|
|
32329
|
+
scope.authority_id,
|
|
32330
|
+
scope.tenant_id,
|
|
32331
|
+
scope.corpus_id,
|
|
32332
|
+
resourceKind,
|
|
32333
|
+
targetSelector
|
|
32334
|
+
]);
|
|
32335
|
+
return result.rows[0] ? bindingFromRow(result.rows[0]) : null;
|
|
32336
|
+
}
|
|
32337
|
+
async claimBinding(binding) {
|
|
32338
|
+
const result = await this.client.query(`
|
|
32339
|
+
INSERT INTO todos_project_registration_bindings (
|
|
32340
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
32341
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
32342
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
32343
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
32344
|
+
created_at, updated_at
|
|
32345
|
+
) VALUES (
|
|
32346
|
+
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,
|
|
32347
|
+
$18,$19,$20
|
|
32348
|
+
)
|
|
32349
|
+
ON CONFLICT (
|
|
32350
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector
|
|
32351
|
+
) DO NOTHING
|
|
32352
|
+
RETURNING target_selector
|
|
32353
|
+
`, [
|
|
32354
|
+
binding.authority_id,
|
|
32355
|
+
binding.tenant_id,
|
|
32356
|
+
binding.corpus_id,
|
|
32357
|
+
binding.resource_kind,
|
|
32358
|
+
binding.target_selector,
|
|
32359
|
+
binding.operation_id,
|
|
32360
|
+
binding.step_id,
|
|
32361
|
+
binding.direction,
|
|
32362
|
+
binding.idempotency_key,
|
|
32363
|
+
binding.request_digest,
|
|
32364
|
+
binding.precondition_digest,
|
|
32365
|
+
binding.normalized_call_digest,
|
|
32366
|
+
binding.state,
|
|
32367
|
+
binding.target_id,
|
|
32368
|
+
binding.accepted_receipt_id,
|
|
32369
|
+
binding.result_revision,
|
|
32370
|
+
binding.result_digest,
|
|
32371
|
+
binding.removed_receipt_id,
|
|
32372
|
+
binding.created_at,
|
|
32373
|
+
binding.updated_at
|
|
32374
|
+
]);
|
|
32375
|
+
return result.rows.length === 1;
|
|
32376
|
+
}
|
|
32377
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
32378
|
+
const result = await this.client.query(`
|
|
32379
|
+
UPDATE todos_project_registration_bindings
|
|
32380
|
+
SET state = 'accepted', target_id = $1, accepted_receipt_id = $2,
|
|
32381
|
+
result_revision = $3, result_digest = $4, updated_at = $5
|
|
32382
|
+
WHERE authority_id = $6 AND tenant_id = $7 AND corpus_id = $8
|
|
32383
|
+
AND resource_kind = $9 AND target_selector = $10 AND state = 'pending'
|
|
32384
|
+
RETURNING target_selector
|
|
32385
|
+
`, [
|
|
32386
|
+
update.target_id,
|
|
32387
|
+
update.accepted_receipt_id,
|
|
32388
|
+
update.result_revision,
|
|
32389
|
+
update.result_digest,
|
|
32390
|
+
update.updated_at,
|
|
32391
|
+
scope.authority_id,
|
|
32392
|
+
scope.tenant_id,
|
|
32393
|
+
scope.corpus_id,
|
|
32394
|
+
resourceKind,
|
|
32395
|
+
targetSelector
|
|
32396
|
+
]);
|
|
32397
|
+
if (result.rows.length !== 1) {
|
|
32398
|
+
throw new Error("Todos project registration binding was not pending at acceptance");
|
|
32399
|
+
}
|
|
32400
|
+
}
|
|
32401
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
32402
|
+
await this.client.query(`
|
|
32403
|
+
UPDATE todos_project_registration_bindings
|
|
32404
|
+
SET state = 'terminal_nonacceptance', updated_at = $1
|
|
32405
|
+
WHERE authority_id = $2 AND tenant_id = $3 AND corpus_id = $4
|
|
32406
|
+
AND resource_kind = $5 AND target_selector = $6 AND state = 'pending'
|
|
32407
|
+
`, [
|
|
32408
|
+
updatedAt,
|
|
32409
|
+
scope.authority_id,
|
|
32410
|
+
scope.tenant_id,
|
|
32411
|
+
scope.corpus_id,
|
|
32412
|
+
resourceKind,
|
|
32413
|
+
targetSelector
|
|
32414
|
+
]);
|
|
32415
|
+
}
|
|
32416
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
32417
|
+
const result = await this.client.query(`
|
|
32418
|
+
UPDATE todos_project_registration_bindings
|
|
32419
|
+
SET state = 'removed', removed_receipt_id = $1, updated_at = $2
|
|
32420
|
+
WHERE authority_id = $3 AND tenant_id = $4 AND corpus_id = $5
|
|
32421
|
+
AND resource_kind = $6 AND target_selector = $7 AND state = 'accepted'
|
|
32422
|
+
RETURNING target_selector
|
|
32423
|
+
`, [
|
|
32424
|
+
removedReceiptId,
|
|
32425
|
+
updatedAt,
|
|
32426
|
+
scope.authority_id,
|
|
32427
|
+
scope.tenant_id,
|
|
32428
|
+
scope.corpus_id,
|
|
32429
|
+
resourceKind,
|
|
32430
|
+
targetSelector
|
|
32431
|
+
]);
|
|
32432
|
+
if (result.rows.length !== 1) {
|
|
32433
|
+
throw new Error("Todos project registration binding was not accepted at removal");
|
|
32434
|
+
}
|
|
32435
|
+
}
|
|
32436
|
+
async findProjectConflict(path, taskListSlug) {
|
|
32437
|
+
const result = await this.client.query(`
|
|
32438
|
+
SELECT payload FROM ${this.tableName}
|
|
32439
|
+
WHERE service = $1 AND object_type = 'projects' AND deleted_at IS NULL
|
|
32440
|
+
AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
|
|
32441
|
+
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
32442
|
+
LIMIT 1
|
|
32443
|
+
`, [this.service, path, taskListSlug]);
|
|
32444
|
+
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
32445
|
+
}
|
|
32446
|
+
async findTaskListConflict(projectId, slug) {
|
|
32447
|
+
const result = await this.client.query(`
|
|
32448
|
+
SELECT payload FROM ${this.tableName}
|
|
32449
|
+
WHERE service = $1 AND object_type = 'task_lists' AND deleted_at IS NULL
|
|
32450
|
+
AND payload->>'project_id' = $2 AND payload->>'slug' = $3
|
|
32451
|
+
ORDER BY payload->>'created_at' ASC, object_id ASC
|
|
32452
|
+
LIMIT 1
|
|
32453
|
+
`, [this.service, projectId, slug]);
|
|
32454
|
+
return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
|
|
32455
|
+
}
|
|
32456
|
+
async createProject(input) {
|
|
32457
|
+
return await this.storage.projects.create(input);
|
|
32458
|
+
}
|
|
32459
|
+
async createTaskList(input) {
|
|
32460
|
+
return await this.storage.taskLists.create(input);
|
|
32461
|
+
}
|
|
32462
|
+
async getProject(id) {
|
|
32463
|
+
return await this.storage.projects.get(id);
|
|
32464
|
+
}
|
|
32465
|
+
async getTaskList(id) {
|
|
32466
|
+
return await this.storage.taskLists.get(id);
|
|
32467
|
+
}
|
|
32468
|
+
async lockCompensationWrites() {
|
|
32469
|
+
await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
|
|
32470
|
+
}
|
|
32471
|
+
async hasDependents(resourceKind, targetId) {
|
|
32472
|
+
const referencePredicate = resourceKind === "project" ? `(
|
|
32473
|
+
payload->>'project_id' = $2
|
|
32474
|
+
OR payload->>'active_project_id' = $2
|
|
32475
|
+
OR payload->>'assigned_from_project' = $2
|
|
32476
|
+
OR payload->>'external_project_id' = $2
|
|
32477
|
+
)` : "payload->>'task_list_id' = $2";
|
|
32478
|
+
const result = await this.client.query(`
|
|
32479
|
+
SELECT EXISTS (
|
|
32480
|
+
SELECT 1 FROM ${this.tableName}
|
|
32481
|
+
WHERE service = $1 AND deleted_at IS NULL
|
|
32482
|
+
AND ${referencePredicate}
|
|
32483
|
+
LIMIT 1
|
|
32484
|
+
) AS exists
|
|
32485
|
+
`, [this.service, targetId]);
|
|
32486
|
+
return result.rows[0]?.exists === true;
|
|
32487
|
+
}
|
|
32488
|
+
async deleteProject(id) {
|
|
32489
|
+
return await this.storage.projects.delete(id);
|
|
32490
|
+
}
|
|
32491
|
+
async deleteTaskList(id) {
|
|
32492
|
+
return await this.storage.taskLists.delete(id);
|
|
32493
|
+
}
|
|
32494
|
+
}
|
|
32495
|
+
|
|
32496
|
+
class PostgresTodosProjectRegistrationBackend {
|
|
32497
|
+
client;
|
|
32498
|
+
kind = "postgresql";
|
|
32499
|
+
service;
|
|
32500
|
+
tableName;
|
|
32501
|
+
cursorTableName;
|
|
32502
|
+
schemaReady = null;
|
|
32503
|
+
constructor(client, options = {}) {
|
|
32504
|
+
this.client = client;
|
|
32505
|
+
this.service = options.service ?? "todos";
|
|
32506
|
+
this.tableName = safeIdentifier(options.tableName ?? "todos_sync_records", "tableName");
|
|
32507
|
+
this.cursorTableName = safeIdentifier(options.cursorTableName ?? "todos_sync_cursors", "cursorTableName");
|
|
32508
|
+
}
|
|
32509
|
+
async ensureSchema() {
|
|
32510
|
+
this.schemaReady ??= (async () => {
|
|
32511
|
+
for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
32512
|
+
await this.client.query(statement);
|
|
32513
|
+
}
|
|
32514
|
+
for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
|
|
32515
|
+
await this.client.query(statement);
|
|
32516
|
+
}
|
|
32517
|
+
})();
|
|
32518
|
+
await this.schemaReady;
|
|
32519
|
+
}
|
|
32520
|
+
async transaction(fn) {
|
|
32521
|
+
await this.ensureSchema();
|
|
32522
|
+
if (typeof this.client.transaction !== "function") {
|
|
32523
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE", "PostgreSQL project registration requires an authoritative transaction");
|
|
32524
|
+
}
|
|
32525
|
+
return this.client.transaction((transaction) => fn(new PostgresTodosProjectRegistrationTransaction(transaction, this.service, this.tableName, this.cursorTableName)));
|
|
32526
|
+
}
|
|
32527
|
+
async direct() {
|
|
32528
|
+
await this.ensureSchema();
|
|
32529
|
+
return new PostgresTodosProjectRegistrationTransaction(this.client, this.service, this.tableName, this.cursorTableName);
|
|
32530
|
+
}
|
|
32531
|
+
async getReceiptForLookup(identity) {
|
|
32532
|
+
return (await this.direct()).getReceiptForLookup(identity);
|
|
32533
|
+
}
|
|
32534
|
+
async getReceiptById(receiptId) {
|
|
32535
|
+
return (await this.direct()).getReceiptById(receiptId);
|
|
32536
|
+
}
|
|
32537
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
32538
|
+
return (await this.direct()).getBinding(scope, resourceKind, targetSelector);
|
|
32539
|
+
}
|
|
32540
|
+
async getProject(id) {
|
|
32541
|
+
return (await this.direct()).getProject(id);
|
|
32542
|
+
}
|
|
32543
|
+
async getTaskList(id) {
|
|
32544
|
+
return (await this.direct()).getTaskList(id);
|
|
32545
|
+
}
|
|
32546
|
+
}
|
|
32547
|
+
|
|
32548
|
+
// src/project-registration/sqlite.ts
|
|
32549
|
+
init_database();
|
|
32550
|
+
init_storage_tombstones();
|
|
32551
|
+
var sqliteTransactionTails2 = new WeakMap;
|
|
32552
|
+
var PROJECT_REFERENCE_COLUMNS = new Set([
|
|
32553
|
+
"project_id",
|
|
32554
|
+
"active_project_id",
|
|
32555
|
+
"assigned_from_project",
|
|
32556
|
+
"external_project_id"
|
|
32557
|
+
]);
|
|
32558
|
+
var TASK_LIST_REFERENCE_COLUMNS = new Set(["task_list_id"]);
|
|
32559
|
+
var SQLITE_TRANSACTION_RETRY_LIMIT = 8;
|
|
32560
|
+
|
|
32561
|
+
class SqliteRegistrationOptimisticConflict extends Error {
|
|
32562
|
+
constructor(message, options = {}) {
|
|
32563
|
+
super(message, options);
|
|
32564
|
+
this.name = "SqliteRegistrationOptimisticConflict";
|
|
32565
|
+
}
|
|
32566
|
+
}
|
|
32567
|
+
function sameSqliteValue(left, right) {
|
|
32568
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
32569
|
+
}
|
|
32570
|
+
function taskListFromRow(row) {
|
|
32571
|
+
return {
|
|
32572
|
+
...row,
|
|
32573
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
32574
|
+
};
|
|
32575
|
+
}
|
|
32576
|
+
function selectProject(db, id) {
|
|
32577
|
+
return db.query("SELECT * FROM projects WHERE id = ? LIMIT 1").get(id);
|
|
32578
|
+
}
|
|
32579
|
+
function selectTaskList(db, id) {
|
|
32580
|
+
const row = db.query("SELECT * FROM task_lists WHERE id = ? LIMIT 1").get(id);
|
|
32581
|
+
return row ? taskListFromRow(row) : null;
|
|
32582
|
+
}
|
|
32583
|
+
function selectProjectConflict(db, path, taskListSlug) {
|
|
32584
|
+
return db.query(`
|
|
32585
|
+
SELECT * FROM projects
|
|
32586
|
+
WHERE path = ? OR task_list_id = ?
|
|
32587
|
+
ORDER BY created_at ASC, id ASC
|
|
32588
|
+
LIMIT 1
|
|
32589
|
+
`).get(path, taskListSlug);
|
|
32590
|
+
}
|
|
32591
|
+
function selectTaskListConflict(db, projectId, slug) {
|
|
32592
|
+
const row = db.query(`
|
|
32593
|
+
SELECT * FROM task_lists
|
|
32594
|
+
WHERE project_id = ? AND slug = ?
|
|
32595
|
+
LIMIT 1
|
|
32596
|
+
`).get(projectId, slug);
|
|
32597
|
+
return row ? taskListFromRow(row) : null;
|
|
32598
|
+
}
|
|
32599
|
+
function quoteSqliteIdentifier(value) {
|
|
32600
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
32601
|
+
}
|
|
32602
|
+
function hasSqliteDependents(db, resourceKind, targetId) {
|
|
32603
|
+
const targetTable = resourceKind === "project" ? "projects" : "task_lists";
|
|
32604
|
+
const semanticColumns = resourceKind === "project" ? PROJECT_REFERENCE_COLUMNS : TASK_LIST_REFERENCE_COLUMNS;
|
|
32605
|
+
const tables = db.query(`
|
|
32606
|
+
SELECT name FROM sqlite_schema
|
|
32607
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
32608
|
+
ORDER BY name
|
|
32609
|
+
`).all();
|
|
32610
|
+
for (const { name: tableName } of tables) {
|
|
32611
|
+
const quotedTable = quoteSqliteIdentifier(tableName);
|
|
32612
|
+
const columns = db.query(`PRAGMA table_info(${quotedTable})`).all();
|
|
32613
|
+
const foreignKeys = db.query(`PRAGMA foreign_key_list(${quotedTable})`).all();
|
|
32614
|
+
const referenceColumns = columns.map((column) => column.name).filter((columnName) => semanticColumns.has(columnName) || foreignKeys.some((foreignKey) => foreignKey.from === columnName && foreignKey.table === targetTable));
|
|
32615
|
+
for (const columnName of referenceColumns) {
|
|
32616
|
+
const row = db.query(`
|
|
32617
|
+
SELECT 1 AS found
|
|
32618
|
+
FROM ${quotedTable}
|
|
32619
|
+
WHERE ${quoteSqliteIdentifier(columnName)} = ?
|
|
32620
|
+
LIMIT 1
|
|
32621
|
+
`).get(targetId);
|
|
32622
|
+
if (row)
|
|
32623
|
+
return true;
|
|
32624
|
+
}
|
|
32625
|
+
}
|
|
32626
|
+
return false;
|
|
32627
|
+
}
|
|
32628
|
+
function receiptFromRow2(row) {
|
|
32629
|
+
return {
|
|
32630
|
+
...row,
|
|
32631
|
+
authority: "todos",
|
|
32632
|
+
created_by_operation: Number(row["created_by_operation"]) === 1
|
|
32633
|
+
};
|
|
32634
|
+
}
|
|
32635
|
+
function bindingFromRow2(row) {
|
|
32636
|
+
return row;
|
|
32637
|
+
}
|
|
32638
|
+
|
|
32639
|
+
class SqliteTodosProjectRegistrationTransaction {
|
|
32640
|
+
db;
|
|
32641
|
+
storage;
|
|
32642
|
+
constructor(db) {
|
|
32643
|
+
this.db = db;
|
|
32644
|
+
this.storage = createLocalSqliteTodosStorageAdapter({ db });
|
|
32645
|
+
}
|
|
32646
|
+
async lockStep(_identity) {}
|
|
32647
|
+
async getReceiptForLookup(identity) {
|
|
32648
|
+
const row = this.db.query(`
|
|
32649
|
+
SELECT * FROM todos_project_registration_receipts
|
|
32650
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32651
|
+
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
32652
|
+
AND direction = ? AND idempotency_key = ? AND target_selector = ?
|
|
32653
|
+
ORDER BY CASE outcome
|
|
32654
|
+
WHEN 'terminal_nonacceptance' THEN 0
|
|
32655
|
+
WHEN 'duplicate_of_accepted' THEN 1
|
|
32656
|
+
ELSE 2
|
|
32657
|
+
END, created_at DESC, receipt_id DESC
|
|
32658
|
+
LIMIT 1
|
|
32659
|
+
`).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);
|
|
32660
|
+
return row ? receiptFromRow2(row) : null;
|
|
32661
|
+
}
|
|
32662
|
+
async getReceiptById(receiptId) {
|
|
32663
|
+
const row = this.db.query("SELECT * FROM todos_project_registration_receipts WHERE receipt_id = ? LIMIT 1").get(receiptId);
|
|
32664
|
+
return row ? receiptFromRow2(row) : null;
|
|
32665
|
+
}
|
|
32666
|
+
async getAcceptedReceiptForStep(identity) {
|
|
32667
|
+
const row = this.db.query(`
|
|
32668
|
+
SELECT * FROM todos_project_registration_receipts
|
|
32669
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32670
|
+
AND operation_id = ? AND step_id = ? AND resource_kind = ?
|
|
32671
|
+
AND direction = ? AND outcome = 'accepted'
|
|
32672
|
+
ORDER BY created_at ASC, receipt_id ASC
|
|
32673
|
+
LIMIT 1
|
|
32674
|
+
`).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction);
|
|
32675
|
+
return row ? receiptFromRow2(row) : null;
|
|
32676
|
+
}
|
|
32677
|
+
async insertReceipt(receipt) {
|
|
32678
|
+
const result = this.db.query(`
|
|
32679
|
+
INSERT OR IGNORE INTO todos_project_registration_receipts (
|
|
32680
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
32681
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
32682
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
32683
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
32684
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
32685
|
+
created_by_operation, created_at
|
|
32686
|
+
) VALUES (
|
|
32687
|
+
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
|
|
32688
|
+
)
|
|
32689
|
+
`).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);
|
|
32690
|
+
return result.changes === 1;
|
|
32691
|
+
}
|
|
32692
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
32693
|
+
const row = this.db.query(`
|
|
32694
|
+
SELECT * FROM todos_project_registration_bindings
|
|
32695
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32696
|
+
AND resource_kind = ? AND target_selector = ?
|
|
32697
|
+
LIMIT 1
|
|
32698
|
+
`).get(scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
32699
|
+
return row ? bindingFromRow2(row) : null;
|
|
32700
|
+
}
|
|
32701
|
+
async claimBinding(binding) {
|
|
32702
|
+
const result = this.db.query(`
|
|
32703
|
+
INSERT OR IGNORE INTO todos_project_registration_bindings (
|
|
32704
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
32705
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
32706
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
32707
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
32708
|
+
created_at, updated_at
|
|
32709
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
32710
|
+
`).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);
|
|
32711
|
+
return result.changes === 1;
|
|
32712
|
+
}
|
|
32713
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
32714
|
+
const result = this.db.query(`
|
|
32715
|
+
UPDATE todos_project_registration_bindings
|
|
32716
|
+
SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
|
|
32717
|
+
result_revision = ?, result_digest = ?, updated_at = ?
|
|
32718
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32719
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
32720
|
+
`).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);
|
|
32721
|
+
if (result.changes !== 1) {
|
|
32722
|
+
throw new Error("Todos project registration binding was not pending at acceptance");
|
|
32723
|
+
}
|
|
32724
|
+
}
|
|
32725
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
32726
|
+
this.db.query(`
|
|
32727
|
+
UPDATE todos_project_registration_bindings
|
|
32728
|
+
SET state = 'terminal_nonacceptance', updated_at = ?
|
|
32729
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32730
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
32731
|
+
`).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
32732
|
+
}
|
|
32733
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
32734
|
+
const result = this.db.query(`
|
|
32735
|
+
UPDATE todos_project_registration_bindings
|
|
32736
|
+
SET state = 'removed', removed_receipt_id = ?, updated_at = ?
|
|
32737
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32738
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
|
|
32739
|
+
`).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
32740
|
+
if (result.changes !== 1) {
|
|
32741
|
+
throw new Error("Todos project registration binding was not accepted at removal");
|
|
32742
|
+
}
|
|
32743
|
+
}
|
|
32744
|
+
async findProjectConflict(path, taskListSlug) {
|
|
32745
|
+
const row = this.db.query(`
|
|
32746
|
+
SELECT * FROM projects
|
|
32747
|
+
WHERE path = ? OR task_list_id = ?
|
|
32748
|
+
ORDER BY created_at ASC, id ASC
|
|
32749
|
+
LIMIT 1
|
|
32750
|
+
`).get(path, taskListSlug);
|
|
32751
|
+
return row ?? null;
|
|
32752
|
+
}
|
|
32753
|
+
async findTaskListConflict(projectId, slug) {
|
|
32754
|
+
return await this.storage.taskLists.getBySlug(slug, projectId);
|
|
32755
|
+
}
|
|
32756
|
+
async createProject(input) {
|
|
32757
|
+
return await this.storage.projects.create(input);
|
|
32758
|
+
}
|
|
32759
|
+
async createTaskList(input) {
|
|
32760
|
+
return await this.storage.taskLists.create(input);
|
|
32761
|
+
}
|
|
32762
|
+
async getProject(id) {
|
|
32763
|
+
return await this.storage.projects.get(id);
|
|
32764
|
+
}
|
|
32765
|
+
async getTaskList(id) {
|
|
32766
|
+
return await this.storage.taskLists.get(id);
|
|
32767
|
+
}
|
|
32768
|
+
async lockCompensationWrites() {}
|
|
32769
|
+
async hasDependents(resourceKind, targetId) {
|
|
32770
|
+
return hasSqliteDependents(this.db, resourceKind, targetId);
|
|
32771
|
+
}
|
|
32772
|
+
async deleteProject(id) {
|
|
32773
|
+
return await this.storage.projects.delete(id);
|
|
32774
|
+
}
|
|
32775
|
+
async deleteTaskList(id) {
|
|
32776
|
+
return await this.storage.taskLists.delete(id);
|
|
32777
|
+
}
|
|
32778
|
+
}
|
|
32779
|
+
|
|
32780
|
+
class StagedSqliteTodosProjectRegistrationTransaction {
|
|
32781
|
+
db;
|
|
32782
|
+
direct;
|
|
32783
|
+
validators = [];
|
|
32784
|
+
mutations = [];
|
|
32785
|
+
receipts = new Map;
|
|
32786
|
+
bindings = new Map;
|
|
32787
|
+
projects = new Map;
|
|
32788
|
+
taskLists = new Map;
|
|
32789
|
+
constructor(db) {
|
|
32790
|
+
this.db = db;
|
|
32791
|
+
this.direct = new SqliteTodosProjectRegistrationTransaction(db);
|
|
32792
|
+
}
|
|
32793
|
+
commit() {
|
|
32794
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
32795
|
+
try {
|
|
32796
|
+
for (const validate of this.validators) {
|
|
32797
|
+
if (!validate()) {
|
|
32798
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration input changed before SQLite commit");
|
|
32799
|
+
}
|
|
32800
|
+
}
|
|
32801
|
+
for (const mutate of this.mutations)
|
|
32802
|
+
mutate();
|
|
32803
|
+
this.db.exec("COMMIT");
|
|
32804
|
+
} catch (error) {
|
|
32805
|
+
try {
|
|
32806
|
+
this.db.exec("ROLLBACK");
|
|
32807
|
+
} catch {}
|
|
32808
|
+
throw error;
|
|
32809
|
+
}
|
|
32810
|
+
}
|
|
32811
|
+
async lockStep(_identity) {}
|
|
32812
|
+
async getReceiptForLookup(identity) {
|
|
32813
|
+
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);
|
|
32814
|
+
const stored = await this.direct.getReceiptForLookup(identity);
|
|
32815
|
+
if (stored)
|
|
32816
|
+
staged.push(stored);
|
|
32817
|
+
const outcomeRank = (receipt) => receipt.outcome === "terminal_nonacceptance" ? 0 : receipt.outcome === "duplicate_of_accepted" ? 1 : 2;
|
|
32818
|
+
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;
|
|
32819
|
+
}
|
|
32820
|
+
async getReceiptById(receiptId) {
|
|
32821
|
+
return this.receipts.get(receiptId) ?? this.direct.getReceiptById(receiptId);
|
|
32822
|
+
}
|
|
32823
|
+
async getAcceptedReceiptForStep(identity) {
|
|
32824
|
+
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");
|
|
32825
|
+
const stored = await this.direct.getAcceptedReceiptForStep(identity);
|
|
32826
|
+
if (stored)
|
|
32827
|
+
staged.push(stored);
|
|
32828
|
+
return staged.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.receipt_id.localeCompare(right.receipt_id))[0] ?? null;
|
|
32829
|
+
}
|
|
32830
|
+
async insertReceipt(receipt) {
|
|
32831
|
+
if (this.receipts.has(receipt.receipt_id))
|
|
32832
|
+
return false;
|
|
32833
|
+
if (await this.direct.getReceiptById(receipt.receipt_id))
|
|
32834
|
+
return false;
|
|
32835
|
+
const planned = { ...receipt };
|
|
32836
|
+
this.receipts.set(planned.receipt_id, planned);
|
|
32837
|
+
this.mutations.push(() => {
|
|
32838
|
+
try {
|
|
32839
|
+
const result = this.db.query(`
|
|
32840
|
+
INSERT OR IGNORE INTO todos_project_registration_receipts (
|
|
32841
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
32842
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
32843
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
32844
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
32845
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
32846
|
+
created_by_operation, created_at
|
|
32847
|
+
) VALUES (
|
|
32848
|
+
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
|
|
32849
|
+
)
|
|
32850
|
+
`).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);
|
|
32851
|
+
if (result.changes !== 1) {
|
|
32852
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt changed before SQLite commit");
|
|
32853
|
+
}
|
|
32854
|
+
} catch (error) {
|
|
32855
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
32856
|
+
throw error;
|
|
32857
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt conflicted at SQLite commit", { cause: error });
|
|
32858
|
+
}
|
|
32859
|
+
});
|
|
32860
|
+
return true;
|
|
32861
|
+
}
|
|
32862
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
32863
|
+
const key = this.bindingKey(scope, resourceKind, targetSelector);
|
|
32864
|
+
return this.bindings.get(key) ?? this.direct.getBinding(scope, resourceKind, targetSelector);
|
|
32865
|
+
}
|
|
32866
|
+
async claimBinding(binding) {
|
|
32867
|
+
const key = this.bindingKey(binding, binding.resource_kind, binding.target_selector);
|
|
32868
|
+
if (this.bindings.has(key))
|
|
32869
|
+
return false;
|
|
32870
|
+
if (await this.direct.getBinding(binding, binding.resource_kind, binding.target_selector)) {
|
|
32871
|
+
return false;
|
|
32872
|
+
}
|
|
32873
|
+
const planned = { ...binding };
|
|
32874
|
+
this.bindings.set(key, planned);
|
|
32875
|
+
this.mutations.push(() => {
|
|
32876
|
+
try {
|
|
32877
|
+
const result = this.db.query(`
|
|
32878
|
+
INSERT OR IGNORE INTO todos_project_registration_bindings (
|
|
32879
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
32880
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
32881
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
32882
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
32883
|
+
created_at, updated_at
|
|
32884
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
32885
|
+
`).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);
|
|
32886
|
+
if (result.changes !== 1) {
|
|
32887
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding changed before SQLite commit");
|
|
32888
|
+
}
|
|
32889
|
+
} catch (error) {
|
|
32890
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
32891
|
+
throw error;
|
|
32892
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding conflicted at SQLite commit", { cause: error });
|
|
32893
|
+
}
|
|
32894
|
+
});
|
|
32895
|
+
return true;
|
|
32896
|
+
}
|
|
32897
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
32898
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
|
|
32899
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
32900
|
+
...binding,
|
|
32901
|
+
state: "accepted",
|
|
32902
|
+
target_id: update.target_id,
|
|
32903
|
+
accepted_receipt_id: update.accepted_receipt_id,
|
|
32904
|
+
result_revision: update.result_revision,
|
|
32905
|
+
result_digest: update.result_digest,
|
|
32906
|
+
updated_at: update.updated_at
|
|
32907
|
+
});
|
|
32908
|
+
this.mutations.push(() => {
|
|
32909
|
+
const result = this.db.query(`
|
|
32910
|
+
UPDATE todos_project_registration_bindings
|
|
32911
|
+
SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
|
|
32912
|
+
result_revision = ?, result_digest = ?, updated_at = ?
|
|
32913
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32914
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
32915
|
+
`).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);
|
|
32916
|
+
if (result.changes !== 1) {
|
|
32917
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
|
|
32918
|
+
}
|
|
32919
|
+
});
|
|
32920
|
+
}
|
|
32921
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
32922
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
|
|
32923
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
32924
|
+
...binding,
|
|
32925
|
+
state: "terminal_nonacceptance",
|
|
32926
|
+
updated_at: updatedAt
|
|
32927
|
+
});
|
|
32928
|
+
this.mutations.push(() => {
|
|
32929
|
+
const result = this.db.query(`
|
|
32930
|
+
UPDATE todos_project_registration_bindings
|
|
32931
|
+
SET state = 'terminal_nonacceptance', updated_at = ?
|
|
32932
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32933
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
32934
|
+
`).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
32935
|
+
if (result.changes !== 1) {
|
|
32936
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
|
|
32937
|
+
}
|
|
32938
|
+
});
|
|
32939
|
+
}
|
|
32940
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
32941
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "accepted");
|
|
32942
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
32943
|
+
...binding,
|
|
32944
|
+
state: "removed",
|
|
32945
|
+
removed_receipt_id: removedReceiptId,
|
|
32946
|
+
updated_at: updatedAt
|
|
32947
|
+
});
|
|
32948
|
+
this.mutations.push(() => {
|
|
32949
|
+
const result = this.db.query(`
|
|
32950
|
+
UPDATE todos_project_registration_bindings
|
|
32951
|
+
SET state = 'removed', removed_receipt_id = ?, updated_at = ?
|
|
32952
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32953
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
|
|
32954
|
+
`).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
32955
|
+
if (result.changes !== 1) {
|
|
32956
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer accepted at SQLite commit");
|
|
32957
|
+
}
|
|
32958
|
+
});
|
|
32959
|
+
}
|
|
32960
|
+
async findProjectConflict(path, taskListSlug) {
|
|
32961
|
+
const planned = [...this.projects.values()].find((project) => project?.path === path || project?.task_list_id === taskListSlug);
|
|
32962
|
+
if (planned)
|
|
32963
|
+
return planned;
|
|
32964
|
+
const observed = selectProjectConflict(this.db, path, taskListSlug);
|
|
32965
|
+
this.validators.push(() => sameSqliteValue(selectProjectConflict(this.db, path, taskListSlug), observed));
|
|
32966
|
+
return observed;
|
|
32967
|
+
}
|
|
32968
|
+
async findTaskListConflict(projectId, slug) {
|
|
32969
|
+
const planned = [...this.taskLists.values()].find((taskList) => taskList?.project_id === projectId && taskList.slug === slug);
|
|
32970
|
+
if (planned)
|
|
32971
|
+
return planned;
|
|
32972
|
+
const observed = selectTaskListConflict(this.db, projectId, slug);
|
|
32973
|
+
this.validators.push(() => sameSqliteValue(selectTaskListConflict(this.db, projectId, slug), observed));
|
|
32974
|
+
return observed;
|
|
32975
|
+
}
|
|
32976
|
+
async createProject(input) {
|
|
32977
|
+
const derivedSlug = normalizeSlug(input.name);
|
|
32978
|
+
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : normalizeSlug(input.task_list_id);
|
|
32979
|
+
if (!derivedSlug || !taskListId) {
|
|
32980
|
+
throw new Error("Project name and task-list slug must be non-empty");
|
|
32981
|
+
}
|
|
32982
|
+
const project = {
|
|
32983
|
+
id: uuid(),
|
|
32984
|
+
name: input.name,
|
|
32985
|
+
path: input.path,
|
|
32986
|
+
description: input.description || null,
|
|
32987
|
+
task_list_id: taskListId,
|
|
32988
|
+
task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
|
|
32989
|
+
task_counter: 0,
|
|
32990
|
+
created_at: now(),
|
|
32991
|
+
updated_at: now(),
|
|
32992
|
+
machine_id: currentStorageMachineId(this.db)
|
|
32993
|
+
};
|
|
32994
|
+
project.updated_at = project.created_at;
|
|
32995
|
+
this.projects.set(project.id, project);
|
|
32996
|
+
this.mutations.push(() => {
|
|
32997
|
+
try {
|
|
32998
|
+
const result = this.db.run(`INSERT INTO projects (
|
|
32999
|
+
id, name, path, description, task_list_id, task_prefix,
|
|
33000
|
+
task_counter, created_at, updated_at, machine_id
|
|
33001
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
|
|
33002
|
+
project.id,
|
|
33003
|
+
project.name,
|
|
33004
|
+
project.path,
|
|
33005
|
+
project.description,
|
|
33006
|
+
project.task_list_id,
|
|
33007
|
+
project.task_prefix,
|
|
33008
|
+
project.created_at,
|
|
33009
|
+
project.updated_at,
|
|
33010
|
+
project.machine_id ?? null
|
|
33011
|
+
]);
|
|
33012
|
+
if (result.changes < 1) {
|
|
33013
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite registration commit");
|
|
33014
|
+
}
|
|
33015
|
+
} catch (error) {
|
|
33016
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
33017
|
+
throw error;
|
|
33018
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project conflicted at SQLite registration commit", { cause: error });
|
|
33019
|
+
}
|
|
33020
|
+
});
|
|
33021
|
+
return project;
|
|
33022
|
+
}
|
|
33023
|
+
async createTaskList(input) {
|
|
33024
|
+
const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
|
|
33025
|
+
if (!slug)
|
|
33026
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
33027
|
+
const taskList = {
|
|
33028
|
+
id: uuid(),
|
|
33029
|
+
project_id: input.project_id || null,
|
|
33030
|
+
slug,
|
|
33031
|
+
name: input.name,
|
|
33032
|
+
description: input.description || null,
|
|
33033
|
+
metadata: input.metadata ?? {},
|
|
33034
|
+
created_at: now(),
|
|
33035
|
+
updated_at: now(),
|
|
33036
|
+
machine_id: currentStorageMachineId(this.db)
|
|
33037
|
+
};
|
|
33038
|
+
taskList.updated_at = taskList.created_at;
|
|
33039
|
+
this.taskLists.set(taskList.id, taskList);
|
|
33040
|
+
this.mutations.push(() => {
|
|
33041
|
+
try {
|
|
33042
|
+
const result = this.db.run(`INSERT INTO task_lists (
|
|
33043
|
+
id, project_id, slug, name, description, metadata,
|
|
33044
|
+
created_at, updated_at, machine_id
|
|
33045
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
33046
|
+
taskList.id,
|
|
33047
|
+
taskList.project_id,
|
|
33048
|
+
taskList.slug,
|
|
33049
|
+
taskList.name,
|
|
33050
|
+
taskList.description,
|
|
33051
|
+
JSON.stringify(taskList.metadata),
|
|
33052
|
+
taskList.created_at,
|
|
33053
|
+
taskList.updated_at,
|
|
33054
|
+
taskList.machine_id ?? null
|
|
33055
|
+
]);
|
|
33056
|
+
if (result.changes < 1) {
|
|
33057
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite registration commit");
|
|
33058
|
+
}
|
|
33059
|
+
} catch (error) {
|
|
33060
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
33061
|
+
throw error;
|
|
33062
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list conflicted at SQLite registration commit", { cause: error });
|
|
33063
|
+
}
|
|
33064
|
+
});
|
|
33065
|
+
return taskList;
|
|
33066
|
+
}
|
|
33067
|
+
async getProject(id) {
|
|
33068
|
+
if (this.projects.has(id))
|
|
33069
|
+
return this.projects.get(id) ?? null;
|
|
33070
|
+
const observed = selectProject(this.db, id);
|
|
33071
|
+
this.validators.push(() => sameSqliteValue(selectProject(this.db, id), observed));
|
|
33072
|
+
return observed;
|
|
33073
|
+
}
|
|
33074
|
+
async getTaskList(id) {
|
|
33075
|
+
if (this.taskLists.has(id))
|
|
33076
|
+
return this.taskLists.get(id) ?? null;
|
|
33077
|
+
const observed = selectTaskList(this.db, id);
|
|
33078
|
+
this.validators.push(() => sameSqliteValue(selectTaskList(this.db, id), observed));
|
|
33079
|
+
return observed;
|
|
33080
|
+
}
|
|
33081
|
+
async lockCompensationWrites() {}
|
|
33082
|
+
async hasDependents(resourceKind, targetId) {
|
|
33083
|
+
const observed = hasSqliteDependents(this.db, resourceKind, targetId);
|
|
33084
|
+
this.validators.push(() => hasSqliteDependents(this.db, resourceKind, targetId) === observed);
|
|
33085
|
+
return observed;
|
|
33086
|
+
}
|
|
33087
|
+
async deleteProject(id) {
|
|
33088
|
+
const project = await this.getProject(id);
|
|
33089
|
+
if (!project)
|
|
33090
|
+
return false;
|
|
33091
|
+
this.projects.set(id, null);
|
|
33092
|
+
this.mutations.push(() => {
|
|
33093
|
+
recordStorageTombstone({
|
|
33094
|
+
object_type: "projects",
|
|
33095
|
+
object_id: id,
|
|
33096
|
+
payload: project
|
|
33097
|
+
}, this.db);
|
|
33098
|
+
if (this.db.run("DELETE FROM projects WHERE id = ?", [id]).changes < 1) {
|
|
33099
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite compensation commit");
|
|
33100
|
+
}
|
|
33101
|
+
});
|
|
33102
|
+
return true;
|
|
33103
|
+
}
|
|
33104
|
+
async deleteTaskList(id) {
|
|
33105
|
+
const taskList = await this.getTaskList(id);
|
|
33106
|
+
if (!taskList)
|
|
33107
|
+
return false;
|
|
33108
|
+
this.taskLists.set(id, null);
|
|
33109
|
+
this.mutations.push(() => {
|
|
33110
|
+
recordStorageTombstone({
|
|
33111
|
+
object_type: "task_lists",
|
|
33112
|
+
object_id: id,
|
|
33113
|
+
payload: taskList
|
|
33114
|
+
}, this.db);
|
|
33115
|
+
if (this.db.run("DELETE FROM task_lists WHERE id = ?", [id]).changes < 1) {
|
|
33116
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite compensation commit");
|
|
33117
|
+
}
|
|
33118
|
+
});
|
|
33119
|
+
return true;
|
|
33120
|
+
}
|
|
33121
|
+
bindingKey(scope, resourceKind, targetSelector) {
|
|
33122
|
+
return JSON.stringify([
|
|
33123
|
+
scope.authority_id,
|
|
33124
|
+
scope.tenant_id,
|
|
33125
|
+
scope.corpus_id,
|
|
33126
|
+
resourceKind,
|
|
33127
|
+
targetSelector
|
|
33128
|
+
]);
|
|
33129
|
+
}
|
|
33130
|
+
async requireBinding(scope, resourceKind, targetSelector, state) {
|
|
33131
|
+
const binding = await this.getBinding(scope, resourceKind, targetSelector);
|
|
33132
|
+
if (!binding || binding.state !== state) {
|
|
33133
|
+
throw new Error(`Todos project registration binding was not ${state}`);
|
|
33134
|
+
}
|
|
33135
|
+
return binding;
|
|
33136
|
+
}
|
|
33137
|
+
availableProjectPrefix(name) {
|
|
33138
|
+
const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
|
|
33139
|
+
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();
|
|
33140
|
+
let candidate = prefix;
|
|
33141
|
+
let suffix = 1;
|
|
33142
|
+
while (this.db.query("SELECT id FROM projects WHERE task_prefix = ? LIMIT 1").get(candidate) || [...this.projects.values()].some((project) => project?.task_prefix === candidate)) {
|
|
33143
|
+
suffix += 1;
|
|
33144
|
+
candidate = `${prefix}${suffix}`;
|
|
33145
|
+
}
|
|
33146
|
+
return candidate;
|
|
33147
|
+
}
|
|
33148
|
+
}
|
|
33149
|
+
|
|
33150
|
+
class SqliteTodosProjectRegistrationBackend {
|
|
33151
|
+
db;
|
|
33152
|
+
kind = "sqlite";
|
|
33153
|
+
direct;
|
|
33154
|
+
constructor(db) {
|
|
33155
|
+
this.db = db;
|
|
33156
|
+
db.exec(sqliteTodosProjectRegistrationSchemaSql());
|
|
33157
|
+
this.direct = new SqliteTodosProjectRegistrationTransaction(db);
|
|
33158
|
+
}
|
|
33159
|
+
async transaction(fn) {
|
|
33160
|
+
const previous = sqliteTransactionTails2.get(this.db) ?? Promise.resolve();
|
|
33161
|
+
let release;
|
|
33162
|
+
const current = new Promise((resolve10) => {
|
|
33163
|
+
release = resolve10;
|
|
33164
|
+
});
|
|
33165
|
+
sqliteTransactionTails2.set(this.db, current);
|
|
33166
|
+
await previous;
|
|
33167
|
+
try {
|
|
33168
|
+
for (let attempt = 0;attempt < SQLITE_TRANSACTION_RETRY_LIMIT; attempt += 1) {
|
|
33169
|
+
const transaction = new StagedSqliteTodosProjectRegistrationTransaction(this.db);
|
|
33170
|
+
const result = await fn(transaction);
|
|
33171
|
+
try {
|
|
33172
|
+
transaction.commit();
|
|
33173
|
+
return result;
|
|
33174
|
+
} catch (error) {
|
|
33175
|
+
if (!(error instanceof SqliteRegistrationOptimisticConflict) || attempt === SQLITE_TRANSACTION_RETRY_LIMIT - 1) {
|
|
33176
|
+
throw error;
|
|
33177
|
+
}
|
|
33178
|
+
}
|
|
33179
|
+
}
|
|
33180
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration exhausted SQLite optimistic retries");
|
|
33181
|
+
} finally {
|
|
33182
|
+
release();
|
|
33183
|
+
if (sqliteTransactionTails2.get(this.db) === current) {
|
|
33184
|
+
sqliteTransactionTails2.delete(this.db);
|
|
33185
|
+
}
|
|
33186
|
+
}
|
|
33187
|
+
}
|
|
33188
|
+
getReceiptForLookup(identity) {
|
|
33189
|
+
return this.direct.getReceiptForLookup(identity);
|
|
33190
|
+
}
|
|
33191
|
+
getReceiptById(receiptId) {
|
|
33192
|
+
return this.direct.getReceiptById(receiptId);
|
|
33193
|
+
}
|
|
33194
|
+
getBinding(scope, resourceKind, targetSelector) {
|
|
33195
|
+
return this.direct.getBinding(scope, resourceKind, targetSelector);
|
|
33196
|
+
}
|
|
33197
|
+
getProject(id) {
|
|
33198
|
+
return this.direct.getProject(id);
|
|
33199
|
+
}
|
|
33200
|
+
getTaskList(id) {
|
|
33201
|
+
return this.direct.getTaskList(id);
|
|
33202
|
+
}
|
|
33203
|
+
}
|
|
33204
|
+
|
|
33205
|
+
// src/project-registration/authority.ts
|
|
33206
|
+
var 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;
|
|
33207
|
+
var WORKSPACE_ID_PATTERN = /^wks_[A-Za-z0-9][A-Za-z0-9_-]{11,}$/;
|
|
33208
|
+
var OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
33209
|
+
var STEP_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
|
|
33210
|
+
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
33211
|
+
var IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
|
|
33212
|
+
|
|
33213
|
+
class WriteBoundaryError extends Error {
|
|
33214
|
+
point;
|
|
33215
|
+
cause;
|
|
33216
|
+
constructor(point, cause) {
|
|
33217
|
+
super(`Todos project registration failed at ${point}`);
|
|
33218
|
+
this.point = point;
|
|
33219
|
+
this.cause = cause;
|
|
33220
|
+
}
|
|
33221
|
+
}
|
|
33222
|
+
function canonicalProjectRegistrationJson(value) {
|
|
33223
|
+
return JSON.stringify(canonicalize3(value));
|
|
33224
|
+
}
|
|
33225
|
+
function canonicalize3(value) {
|
|
33226
|
+
if (Array.isArray(value))
|
|
33227
|
+
return value.map(canonicalize3);
|
|
33228
|
+
if (!value || typeof value !== "object")
|
|
33229
|
+
return value;
|
|
33230
|
+
const out = {};
|
|
33231
|
+
for (const key of Object.keys(value).sort()) {
|
|
33232
|
+
const entry2 = value[key];
|
|
33233
|
+
if (entry2 !== undefined)
|
|
33234
|
+
out[key] = canonicalize3(entry2);
|
|
33235
|
+
}
|
|
33236
|
+
return out;
|
|
33237
|
+
}
|
|
33238
|
+
function digestProjectRegistrationValue(value) {
|
|
33239
|
+
return createHash12("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
|
|
33240
|
+
}
|
|
33241
|
+
function deriveTodosProjectRegistrationIdempotencyKey(input) {
|
|
33242
|
+
return `prk_${digestProjectRegistrationValue({
|
|
33243
|
+
route: TODOS_PROJECT_REGISTRATION_CALLER_ROUTE,
|
|
33244
|
+
...input
|
|
33245
|
+
}).slice(0, 48)}`;
|
|
33246
|
+
}
|
|
33247
|
+
function responseBytes(value) {
|
|
33248
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
33249
|
+
}
|
|
33250
|
+
function assertBounds(bounds) {
|
|
33251
|
+
if (!Number.isSafeInteger(bounds.response_byte_limit) || bounds.response_byte_limit <= 0) {
|
|
33252
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "response_byte_limit must be a positive integer");
|
|
33253
|
+
}
|
|
33254
|
+
if (!Number.isSafeInteger(bounds.time_budget_ms) || bounds.time_budget_ms <= 0) {
|
|
33255
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "time_budget_ms must be a positive integer");
|
|
33256
|
+
}
|
|
33257
|
+
}
|
|
33258
|
+
function assertResourceKind(value) {
|
|
33259
|
+
if (value !== "project" && value !== "task_list") {
|
|
33260
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "resource_kind must be project or task_list");
|
|
33261
|
+
}
|
|
33262
|
+
}
|
|
33263
|
+
function assertDirection(value) {
|
|
33264
|
+
if (value !== "forward" && value !== "inverse") {
|
|
33265
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "direction must be forward or inverse");
|
|
33266
|
+
}
|
|
33267
|
+
}
|
|
33268
|
+
function assertWithinBounds(value, bounds, startedAt) {
|
|
33269
|
+
const bytes = responseBytes(value);
|
|
33270
|
+
if (bytes > bounds.response_byte_limit) {
|
|
33271
|
+
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 });
|
|
33272
|
+
}
|
|
33273
|
+
const elapsed = Date.now() - startedAt;
|
|
33274
|
+
if (elapsed > bounds.time_budget_ms) {
|
|
33275
|
+
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 });
|
|
33276
|
+
}
|
|
33277
|
+
return { response_bytes: bytes, elapsed_ms: elapsed };
|
|
33278
|
+
}
|
|
33279
|
+
function withResponseControl(payload, bounds, startedAt) {
|
|
33280
|
+
const envelope = {
|
|
33281
|
+
...payload,
|
|
33282
|
+
response_control: {
|
|
33283
|
+
response_byte_limit: bounds.response_byte_limit,
|
|
33284
|
+
time_budget_ms: bounds.time_budget_ms,
|
|
33285
|
+
response_bytes: 0,
|
|
33286
|
+
elapsed_ms: 0,
|
|
33287
|
+
complete: true,
|
|
33288
|
+
truncated: false
|
|
33289
|
+
}
|
|
33290
|
+
};
|
|
33291
|
+
for (let attempt = 0;attempt < 8; attempt += 1) {
|
|
33292
|
+
const measured = assertWithinBounds(envelope, bounds, startedAt);
|
|
33293
|
+
const stable2 = envelope.response_control.response_bytes === measured.response_bytes && envelope.response_control.elapsed_ms === measured.elapsed_ms;
|
|
33294
|
+
envelope.response_control = {
|
|
33295
|
+
response_byte_limit: bounds.response_byte_limit,
|
|
33296
|
+
time_budget_ms: bounds.time_budget_ms,
|
|
33297
|
+
response_bytes: measured.response_bytes,
|
|
33298
|
+
elapsed_ms: measured.elapsed_ms,
|
|
33299
|
+
complete: true,
|
|
33300
|
+
truncated: false
|
|
33301
|
+
};
|
|
33302
|
+
if (stable2)
|
|
33303
|
+
break;
|
|
33304
|
+
}
|
|
33305
|
+
const finalMeasurement = assertWithinBounds(envelope, bounds, startedAt);
|
|
33306
|
+
envelope.response_control.response_bytes = finalMeasurement.response_bytes;
|
|
33307
|
+
envelope.response_control.elapsed_ms = finalMeasurement.elapsed_ms;
|
|
33308
|
+
return envelope;
|
|
33309
|
+
}
|
|
33310
|
+
function requireString(value, field2, options = {}) {
|
|
33311
|
+
const min = options.min ?? 1;
|
|
33312
|
+
const max = options.max ?? 512;
|
|
33313
|
+
if (typeof value !== "string" || value.length < min || value.length > max || /[\u0000-\u001f]/.test(value) || options.pattern && !options.pattern.test(value)) {
|
|
33314
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", `${field2} is not a valid bounded registration identifier`);
|
|
33315
|
+
}
|
|
33316
|
+
return value;
|
|
33317
|
+
}
|
|
33318
|
+
function exactKeys(value, expected, field2) {
|
|
33319
|
+
const actual = Object.keys(value).sort();
|
|
33320
|
+
const wanted = [...expected].sort();
|
|
33321
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
33322
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", `${field2} must contain exactly: ${wanted.join(", ")}`);
|
|
33323
|
+
}
|
|
33324
|
+
}
|
|
33325
|
+
function publicReceipt(row) {
|
|
33326
|
+
const {
|
|
33327
|
+
target_selector: _targetSelector,
|
|
33328
|
+
normalized_call_digest: _normalizedCallDigest,
|
|
33329
|
+
...receipt
|
|
33330
|
+
} = row;
|
|
33331
|
+
return receipt;
|
|
33332
|
+
}
|
|
33333
|
+
function projectRegistrationPath(projectId) {
|
|
33334
|
+
return `hasna-project://${encodeURIComponent(projectId)}`;
|
|
33335
|
+
}
|
|
33336
|
+
function taskListSlug(projectSlug) {
|
|
33337
|
+
const slug = normalizeSlug(projectSlug);
|
|
33338
|
+
if (!slug || slug !== projectSlug) {
|
|
33339
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project_slug must be canonical kebab-case");
|
|
33340
|
+
}
|
|
33341
|
+
return `todos-${slug}`;
|
|
33342
|
+
}
|
|
33343
|
+
function deterministicTaskPrefix(projectSlug) {
|
|
33344
|
+
const letters = projectSlug.replace(/[^a-z0-9]/gi, "").toUpperCase();
|
|
33345
|
+
return (letters.slice(0, 3) || "PRJ").padEnd(3, "X");
|
|
33346
|
+
}
|
|
33347
|
+
function projectRecord(project) {
|
|
33348
|
+
return {
|
|
33349
|
+
target_id: project.id,
|
|
33350
|
+
revision: project.updated_at,
|
|
33351
|
+
digest: digestProjectRegistrationValue({
|
|
33352
|
+
id: project.id,
|
|
33353
|
+
name: project.name,
|
|
33354
|
+
path: project.path,
|
|
33355
|
+
description: project.description,
|
|
33356
|
+
task_list_id: project.task_list_id,
|
|
33357
|
+
task_prefix: project.task_prefix,
|
|
33358
|
+
task_counter: project.task_counter,
|
|
33359
|
+
created_at: project.created_at,
|
|
33360
|
+
updated_at: project.updated_at
|
|
33361
|
+
})
|
|
33362
|
+
};
|
|
33363
|
+
}
|
|
33364
|
+
function taskListRecord(taskList) {
|
|
33365
|
+
return {
|
|
33366
|
+
target_id: taskList.id,
|
|
33367
|
+
revision: taskList.updated_at,
|
|
33368
|
+
digest: digestProjectRegistrationValue({
|
|
33369
|
+
id: taskList.id,
|
|
33370
|
+
project_id: taskList.project_id,
|
|
33371
|
+
slug: taskList.slug,
|
|
33372
|
+
name: taskList.name,
|
|
33373
|
+
description: taskList.description,
|
|
33374
|
+
metadata: taskList.metadata,
|
|
33375
|
+
created_at: taskList.created_at,
|
|
33376
|
+
updated_at: taskList.updated_at
|
|
33377
|
+
})
|
|
33378
|
+
};
|
|
33379
|
+
}
|
|
33380
|
+
function receiptId(input) {
|
|
33381
|
+
return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
|
|
33382
|
+
}
|
|
33383
|
+
function capabilityMatches(request, capability2) {
|
|
33384
|
+
return request.authority_route === capability2.route && request.package_version === capability2.package_version && request.authority_id === capability2.authority_id && request.tenant_id === capability2.tenant_id && request.corpus_id === capability2.corpus_id;
|
|
33385
|
+
}
|
|
33386
|
+
function authorityScope(capability2) {
|
|
33387
|
+
return {
|
|
33388
|
+
authority_id: capability2.authority_id,
|
|
33389
|
+
tenant_id: capability2.tenant_id,
|
|
33390
|
+
corpus_id: capability2.corpus_id
|
|
33391
|
+
};
|
|
33392
|
+
}
|
|
33393
|
+
function assertCapabilityRequest(request, capability2) {
|
|
33394
|
+
if (!capabilityMatches(request, capability2)) {
|
|
33395
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "registration request does not match this authority capability identity");
|
|
33396
|
+
}
|
|
33397
|
+
}
|
|
33398
|
+
function normalizedCallDigest(request) {
|
|
33399
|
+
return digestProjectRegistrationValue({
|
|
33400
|
+
authority_route: request.authority_route,
|
|
33401
|
+
package_version: request.package_version,
|
|
33402
|
+
authority_id: request.authority_id,
|
|
33403
|
+
tenant_id: request.tenant_id,
|
|
33404
|
+
corpus_id: request.corpus_id,
|
|
33405
|
+
operation_id: request.operation_id,
|
|
33406
|
+
step_id: request.step_id,
|
|
33407
|
+
resource_kind: request.resource_kind,
|
|
33408
|
+
direction: request.direction,
|
|
33409
|
+
target_selector: request.target_selector,
|
|
33410
|
+
idempotency_key: request.idempotency_key,
|
|
33411
|
+
request_digest: request.request_digest,
|
|
33412
|
+
precondition_digest: request.precondition_digest,
|
|
33413
|
+
project_id: request.project_id,
|
|
33414
|
+
project_slug: request.project_slug,
|
|
33415
|
+
project_name: request.project_name,
|
|
33416
|
+
desired: request.desired,
|
|
33417
|
+
accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
|
|
33418
|
+
});
|
|
33419
|
+
}
|
|
33420
|
+
function assertCommonRequest(request, capability2) {
|
|
33421
|
+
assertBounds(request);
|
|
33422
|
+
assertResourceKind(request.resource_kind);
|
|
33423
|
+
assertDirection(request.direction);
|
|
33424
|
+
assertCapabilityRequest(request, capability2);
|
|
33425
|
+
requireString(request.operation_id, "operation_id", {
|
|
33426
|
+
min: 8,
|
|
33427
|
+
max: 128,
|
|
33428
|
+
pattern: OPERATION_PATTERN
|
|
33429
|
+
});
|
|
33430
|
+
requireString(request.step_id, "step_id", {
|
|
33431
|
+
min: 3,
|
|
33432
|
+
max: 128,
|
|
33433
|
+
pattern: STEP_PATTERN
|
|
33434
|
+
});
|
|
33435
|
+
requireString(request.target_selector, "target_selector", { max: 512 });
|
|
33436
|
+
requireString(request.project_id, "project_id", {
|
|
33437
|
+
min: 16,
|
|
33438
|
+
max: 128,
|
|
33439
|
+
pattern: WORKSPACE_ID_PATTERN
|
|
33440
|
+
});
|
|
33441
|
+
requireString(request.project_name, "project_name", { max: 256 });
|
|
33442
|
+
requireString(request.project_slug, "project_slug", { max: 128 });
|
|
33443
|
+
requireString(request.request_digest, "request_digest", {
|
|
33444
|
+
min: 64,
|
|
33445
|
+
max: 64,
|
|
33446
|
+
pattern: SHA256_PATTERN
|
|
33447
|
+
});
|
|
33448
|
+
requireString(request.precondition_digest, "precondition_digest", {
|
|
33449
|
+
min: 64,
|
|
33450
|
+
max: 64,
|
|
33451
|
+
pattern: SHA256_PATTERN
|
|
33452
|
+
});
|
|
33453
|
+
requireString(request.idempotency_key, "idempotency_key", {
|
|
33454
|
+
min: 52,
|
|
33455
|
+
max: 52,
|
|
33456
|
+
pattern: IDEMPOTENCY_PATTERN
|
|
33457
|
+
});
|
|
33458
|
+
if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
|
|
33459
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
|
|
33460
|
+
}
|
|
33461
|
+
const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
|
|
33462
|
+
operation_id: request.operation_id,
|
|
33463
|
+
step_id: request.step_id,
|
|
33464
|
+
direction: request.direction,
|
|
33465
|
+
target_selector: request.target_selector,
|
|
33466
|
+
request_digest: request.request_digest,
|
|
33467
|
+
precondition_digest: request.precondition_digest
|
|
33468
|
+
});
|
|
33469
|
+
if (request.idempotency_key !== expectedKey) {
|
|
33470
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/direction payload", { expected: expectedKey });
|
|
33471
|
+
}
|
|
33472
|
+
taskListSlug(request.project_slug);
|
|
33473
|
+
}
|
|
33474
|
+
function assertForwardRequest(request, capability2) {
|
|
33475
|
+
assertCommonRequest(request, capability2);
|
|
33476
|
+
if (request.direction !== "forward") {
|
|
33477
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "create requires direction=forward");
|
|
33478
|
+
}
|
|
33479
|
+
const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
|
|
33480
|
+
const expectedPreconditionDigest = digestProjectRegistrationValue({
|
|
33481
|
+
target_selector: request.target_selector,
|
|
33482
|
+
expected: "absent"
|
|
33483
|
+
});
|
|
33484
|
+
if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
|
|
33485
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
|
|
33486
|
+
expected_request_digest: expectedRequestDigest,
|
|
33487
|
+
expected_precondition_digest: expectedPreconditionDigest
|
|
33488
|
+
});
|
|
33489
|
+
}
|
|
33490
|
+
if (request.resource_kind === "project") {
|
|
33491
|
+
exactKeys(request.desired, ["source_project_id", "source_project_slug", "name"], "project desired");
|
|
33492
|
+
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) {
|
|
33493
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "project desired state and target selector must match the complete Projects identity");
|
|
33494
|
+
}
|
|
33495
|
+
return;
|
|
33496
|
+
}
|
|
33497
|
+
if (request.resource_kind === "task_list") {
|
|
33498
|
+
exactKeys(request.desired, ["todos_project_id", "source_project_id", "name"], "task-list desired");
|
|
33499
|
+
const todosProjectId = request.desired["todos_project_id"];
|
|
33500
|
+
if (typeof todosProjectId !== "string" || !UUID_PATTERN.test(todosProjectId)) {
|
|
33501
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_EXACT_ID_REQUIRED", "task-list create requires the exact full Todos project UUID");
|
|
33502
|
+
}
|
|
33503
|
+
if (request.target_selector !== `${todosProjectId}:default` || request.desired["source_project_id"] !== request.project_id || request.desired["name"] !== request.project_name) {
|
|
33504
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "task-list desired state must bind the exact Todos project id and Projects identity");
|
|
33505
|
+
}
|
|
33506
|
+
return;
|
|
33507
|
+
}
|
|
33508
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "unsupported registration resource kind");
|
|
33509
|
+
}
|
|
33510
|
+
function assertInverseRequest(request, capability2) {
|
|
33511
|
+
assertCommonRequest(request, capability2);
|
|
33512
|
+
if (request.direction !== "inverse") {
|
|
33513
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "compensate requires direction=inverse");
|
|
33514
|
+
}
|
|
33515
|
+
const accepted = request.accepted_receipt;
|
|
33516
|
+
if (!accepted || accepted.authority !== "todos" || accepted.route !== capability2.route || accepted.package_version !== capability2.package_version || accepted.authority_id !== capability2.authority_id || accepted.tenant_id !== capability2.tenant_id || accepted.corpus_id !== capability2.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) {
|
|
33517
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "inverse requires the complete accepted forward receipt created by this operation");
|
|
33518
|
+
}
|
|
33519
|
+
exactKeys(request.desired, ["accepted_receipt_id", "target_id"], "inverse desired");
|
|
33520
|
+
const expectedDesired = {
|
|
33521
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
33522
|
+
target_id: accepted.target_id
|
|
33523
|
+
};
|
|
33524
|
+
const expectedPrecondition = {
|
|
33525
|
+
expected_revision: accepted.result_revision,
|
|
33526
|
+
expected_digest: accepted.result_digest
|
|
33527
|
+
};
|
|
33528
|
+
const expectedRequestDigest = digestProjectRegistrationValue(expectedDesired);
|
|
33529
|
+
const expectedPreconditionDigest = digestProjectRegistrationValue(expectedPrecondition);
|
|
33530
|
+
if (canonicalProjectRegistrationJson(request.desired) !== canonicalProjectRegistrationJson(expectedDesired) || request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest || request.target_selector !== accepted.target_id) {
|
|
33531
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "inverse request does not match the accepted receipt and exact readback precondition");
|
|
33532
|
+
}
|
|
33533
|
+
return accepted;
|
|
33534
|
+
}
|
|
33535
|
+
function makeReceipt(input, createdAt2) {
|
|
33536
|
+
return {
|
|
33537
|
+
...input,
|
|
33538
|
+
receipt_id: receiptId(input),
|
|
33539
|
+
created_at: createdAt2
|
|
33540
|
+
};
|
|
33541
|
+
}
|
|
33542
|
+
function receiptBase(request, callDigest, capability2) {
|
|
33543
|
+
return {
|
|
33544
|
+
authority: "todos",
|
|
33545
|
+
route: capability2.route,
|
|
33546
|
+
package_version: capability2.package_version,
|
|
33547
|
+
authority_id: capability2.authority_id,
|
|
33548
|
+
tenant_id: capability2.tenant_id,
|
|
33549
|
+
corpus_id: capability2.corpus_id,
|
|
33550
|
+
operation_id: request.operation_id,
|
|
33551
|
+
step_id: request.step_id,
|
|
33552
|
+
resource_kind: request.resource_kind,
|
|
33553
|
+
direction: request.direction,
|
|
33554
|
+
target_selector: request.target_selector,
|
|
33555
|
+
idempotency_key: request.idempotency_key,
|
|
33556
|
+
request_digest: request.request_digest,
|
|
33557
|
+
precondition_digest: request.precondition_digest,
|
|
33558
|
+
normalized_call_digest: callDigest
|
|
33559
|
+
};
|
|
33560
|
+
}
|
|
33561
|
+
function makeAcceptedReceipt(request, callDigest, capability2, record, createdAt2) {
|
|
33562
|
+
return makeReceipt({
|
|
33563
|
+
...receiptBase(request, callDigest, capability2),
|
|
33564
|
+
outcome: "accepted",
|
|
33565
|
+
reason: null,
|
|
33566
|
+
target_id: record.target_id,
|
|
33567
|
+
result_revision: record.revision,
|
|
33568
|
+
result_digest: record.digest,
|
|
33569
|
+
duplicate_of_receipt_id: null,
|
|
33570
|
+
accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
|
|
33571
|
+
created_by_operation: true
|
|
33572
|
+
}, createdAt2);
|
|
33573
|
+
}
|
|
33574
|
+
function makeDuplicateReceipt(request, callDigest, capability2, accepted, createdAt2) {
|
|
33575
|
+
return makeReceipt({
|
|
33576
|
+
...receiptBase(request, callDigest, capability2),
|
|
33577
|
+
outcome: "duplicate_of_accepted",
|
|
33578
|
+
reason: null,
|
|
33579
|
+
target_id: accepted.target_id,
|
|
33580
|
+
result_revision: accepted.result_revision,
|
|
33581
|
+
result_digest: accepted.result_digest,
|
|
33582
|
+
duplicate_of_receipt_id: accepted.receipt_id,
|
|
33583
|
+
accepted_receipt_id: null,
|
|
33584
|
+
created_by_operation: false
|
|
33585
|
+
}, createdAt2);
|
|
33586
|
+
}
|
|
33587
|
+
function makeTerminalReceipt(request, callDigest, capability2, reason, createdAt2, options = {}) {
|
|
33588
|
+
return makeReceipt({
|
|
33589
|
+
...receiptBase(request, callDigest, capability2),
|
|
33590
|
+
outcome: "terminal_nonacceptance",
|
|
33591
|
+
reason,
|
|
33592
|
+
target_id: options.targetId ?? null,
|
|
33593
|
+
result_revision: null,
|
|
33594
|
+
result_digest: null,
|
|
33595
|
+
duplicate_of_receipt_id: null,
|
|
33596
|
+
accepted_receipt_id: options.acceptedReceiptId ?? null,
|
|
33597
|
+
created_by_operation: false
|
|
33598
|
+
}, createdAt2);
|
|
33599
|
+
}
|
|
33600
|
+
async function insertDeterministicReceipt(transaction, receipt) {
|
|
33601
|
+
if (await transaction.insertReceipt(receipt))
|
|
33602
|
+
return receipt;
|
|
33603
|
+
const existing = await transaction.getReceiptById(receipt.receipt_id);
|
|
33604
|
+
const { created_at: _existingCreatedAt, ...existingContent } = existing ?? {};
|
|
33605
|
+
const { created_at: _receiptCreatedAt, ...receiptContent } = receipt;
|
|
33606
|
+
if (!existing || canonicalProjectRegistrationJson(existingContent) !== canonicalProjectRegistrationJson(receiptContent)) {
|
|
33607
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "deterministic receipt id is occupied by different immutable content", { receipt_id: receipt.receipt_id });
|
|
33608
|
+
}
|
|
33609
|
+
return existing;
|
|
33610
|
+
}
|
|
33611
|
+
function bindingFor(request, callDigest, timestamp3, capability2) {
|
|
33612
|
+
return {
|
|
33613
|
+
...authorityScope(capability2),
|
|
33614
|
+
resource_kind: request.resource_kind,
|
|
33615
|
+
target_selector: request.target_selector,
|
|
33616
|
+
operation_id: request.operation_id,
|
|
33617
|
+
step_id: request.step_id,
|
|
33618
|
+
direction: "forward",
|
|
33619
|
+
idempotency_key: request.idempotency_key,
|
|
33620
|
+
request_digest: request.request_digest,
|
|
33621
|
+
precondition_digest: request.precondition_digest,
|
|
33622
|
+
normalized_call_digest: callDigest,
|
|
33623
|
+
state: "pending",
|
|
33624
|
+
target_id: null,
|
|
33625
|
+
accepted_receipt_id: null,
|
|
33626
|
+
result_revision: null,
|
|
33627
|
+
result_digest: null,
|
|
33628
|
+
removed_receipt_id: null,
|
|
33629
|
+
created_at: timestamp3,
|
|
33630
|
+
updated_at: timestamp3
|
|
33631
|
+
};
|
|
33632
|
+
}
|
|
33633
|
+
|
|
33634
|
+
class PackageOwnedTodosProjectRegistrationAuthority {
|
|
33635
|
+
backend;
|
|
33636
|
+
authority = "todos";
|
|
33637
|
+
capabilityValue;
|
|
33638
|
+
now;
|
|
33639
|
+
faultInjector;
|
|
33640
|
+
constructor(backend, options = {}) {
|
|
33641
|
+
this.backend = backend;
|
|
33642
|
+
this.capabilityValue = {
|
|
33643
|
+
authority: "todos",
|
|
33644
|
+
route: TODOS_PROJECT_REGISTRATION_ROUTE,
|
|
33645
|
+
package_version: options.packageVersion ?? getPackageVersion(import.meta.url),
|
|
33646
|
+
authority_id: options.authorityId ?? "todos",
|
|
33647
|
+
tenant_id: options.tenantId ?? backend.kind,
|
|
33648
|
+
corpus_id: options.corpusId ?? `todos:${backend.kind}`,
|
|
33649
|
+
supported_resources: ["project", "task_list"],
|
|
33650
|
+
conditional_create: true,
|
|
33651
|
+
immutable_receipts: true,
|
|
33652
|
+
exact_terminal_lookup: true,
|
|
33653
|
+
exact_readback: true,
|
|
33654
|
+
conditional_inverse: true,
|
|
33655
|
+
ambiguous_outcome_reconciliation: true
|
|
33656
|
+
};
|
|
33657
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
33658
|
+
this.faultInjector = options.faultInjector;
|
|
33659
|
+
}
|
|
33660
|
+
async capability() {
|
|
33661
|
+
return {
|
|
33662
|
+
...this.capabilityValue,
|
|
33663
|
+
supported_resources: [...this.capabilityValue.supported_resources]
|
|
33664
|
+
};
|
|
33665
|
+
}
|
|
33666
|
+
async fault(point, request) {
|
|
33667
|
+
if (!this.faultInjector)
|
|
33668
|
+
return;
|
|
33669
|
+
try {
|
|
33670
|
+
await this.faultInjector(point, {
|
|
33671
|
+
operation_id: request.operation_id,
|
|
33672
|
+
step_id: request.step_id,
|
|
33673
|
+
resource_kind: request.resource_kind,
|
|
33674
|
+
direction: request.direction
|
|
33675
|
+
});
|
|
33676
|
+
} catch (cause) {
|
|
33677
|
+
throw new WriteBoundaryError(point, cause);
|
|
33678
|
+
}
|
|
33679
|
+
}
|
|
33680
|
+
async afterCommit(request) {
|
|
33681
|
+
await this.faultInjector?.("after_commit", {
|
|
33682
|
+
operation_id: request.operation_id,
|
|
33683
|
+
step_id: request.step_id,
|
|
33684
|
+
resource_kind: request.resource_kind,
|
|
33685
|
+
direction: request.direction
|
|
33686
|
+
});
|
|
33687
|
+
}
|
|
33688
|
+
async duplicateFor(transaction, request, callDigest, accepted) {
|
|
33689
|
+
const duplicate = makeDuplicateReceipt(request, callDigest, this.capabilityValue, accepted, this.now());
|
|
33690
|
+
return insertDeterministicReceipt(transaction, duplicate);
|
|
33691
|
+
}
|
|
33692
|
+
async terminalFor(transaction, request, callDigest, reason, options = {}) {
|
|
33693
|
+
return insertDeterministicReceipt(transaction, makeTerminalReceipt(request, callDigest, this.capabilityValue, reason, this.now(), options));
|
|
33694
|
+
}
|
|
33695
|
+
async existingForwardResolution(transaction, request, callDigest) {
|
|
33696
|
+
const exact = await transaction.getReceiptForLookup({
|
|
33697
|
+
...authorityScope(this.capabilityValue),
|
|
33698
|
+
operation_id: request.operation_id,
|
|
33699
|
+
step_id: request.step_id,
|
|
33700
|
+
resource_kind: request.resource_kind,
|
|
33701
|
+
direction: request.direction,
|
|
33702
|
+
idempotency_key: request.idempotency_key,
|
|
33703
|
+
target_selector: request.target_selector
|
|
33704
|
+
});
|
|
33705
|
+
if (exact) {
|
|
33706
|
+
if (exact.outcome === "terminal_nonacceptance")
|
|
33707
|
+
return exact;
|
|
33708
|
+
const accepted2 = exact.outcome === "accepted" ? exact : await transaction.getReceiptById(exact.duplicate_of_receipt_id);
|
|
33709
|
+
if (!accepted2) {
|
|
33710
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
|
|
33711
|
+
}
|
|
33712
|
+
if (accepted2.normalized_call_digest !== callDigest) {
|
|
33713
|
+
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
|
|
33714
|
+
}
|
|
33715
|
+
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
33716
|
+
}
|
|
33717
|
+
const accepted = await transaction.getAcceptedReceiptForStep({
|
|
33718
|
+
...authorityScope(this.capabilityValue),
|
|
33719
|
+
operation_id: request.operation_id,
|
|
33720
|
+
step_id: request.step_id,
|
|
33721
|
+
resource_kind: request.resource_kind,
|
|
33722
|
+
direction: "forward"
|
|
33723
|
+
});
|
|
33724
|
+
if (!accepted)
|
|
33725
|
+
return null;
|
|
33726
|
+
if (accepted.normalized_call_digest === callDigest) {
|
|
33727
|
+
return this.duplicateFor(transaction, request, callDigest, accepted);
|
|
33728
|
+
}
|
|
33729
|
+
return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
|
|
33730
|
+
}
|
|
33731
|
+
async createObject(transaction, request) {
|
|
33732
|
+
if (request.resource_kind === "project") {
|
|
33733
|
+
const path = projectRegistrationPath(request.project_id);
|
|
33734
|
+
const slug2 = taskListSlug(request.project_slug);
|
|
33735
|
+
const conflict2 = await transaction.findProjectConflict(path, slug2);
|
|
33736
|
+
if (conflict2) {
|
|
33737
|
+
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
|
|
33738
|
+
}
|
|
33739
|
+
await this.fault("before_object_write", request);
|
|
33740
|
+
const project = await transaction.createProject({
|
|
33741
|
+
name: request.project_name,
|
|
33742
|
+
path,
|
|
33743
|
+
description: `Registered from Projects workspace ${request.project_id}`,
|
|
33744
|
+
task_list_id: slug2,
|
|
33745
|
+
task_prefix: deterministicTaskPrefix(request.project_slug)
|
|
33746
|
+
});
|
|
33747
|
+
await this.fault("after_object_write", request);
|
|
33748
|
+
return projectRecord(project);
|
|
33749
|
+
}
|
|
33750
|
+
const todosProjectId = String(request.desired["todos_project_id"]);
|
|
33751
|
+
const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
|
|
33752
|
+
if (!sourceBinding || sourceBinding.state !== "accepted" || sourceBinding.target_id !== todosProjectId) {
|
|
33753
|
+
return this.terminalFor(transaction, request, normalizedCallDigest(request), "exact_parent_registration_missing", { targetId: todosProjectId });
|
|
33754
|
+
}
|
|
33755
|
+
const parent = await transaction.getProject(todosProjectId);
|
|
33756
|
+
if (!parent) {
|
|
33757
|
+
return this.terminalFor(transaction, request, normalizedCallDigest(request), "exact_parent_project_missing", { targetId: todosProjectId });
|
|
33758
|
+
}
|
|
33759
|
+
const slug = taskListSlug(request.project_slug);
|
|
33760
|
+
const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
|
|
33761
|
+
if (conflict) {
|
|
33762
|
+
return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
|
|
33763
|
+
}
|
|
33764
|
+
await this.fault("before_object_write", request);
|
|
33765
|
+
const taskList = await transaction.createTaskList({
|
|
33766
|
+
name: request.project_name,
|
|
33767
|
+
slug,
|
|
33768
|
+
project_id: todosProjectId,
|
|
33769
|
+
metadata: {
|
|
33770
|
+
source_project_id: request.project_id,
|
|
33771
|
+
registration_authority: "todos"
|
|
33772
|
+
}
|
|
33773
|
+
});
|
|
33774
|
+
await this.fault("after_object_write", request);
|
|
33775
|
+
if (taskList.project_id !== todosProjectId) {
|
|
33776
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
|
|
33777
|
+
}
|
|
33778
|
+
return taskListRecord(taskList);
|
|
33779
|
+
}
|
|
33780
|
+
async create(request) {
|
|
33781
|
+
const startedAt = Date.now();
|
|
33782
|
+
assertForwardRequest(request, this.capabilityValue);
|
|
33783
|
+
const callDigest = normalizedCallDigest(request);
|
|
33784
|
+
try {
|
|
33785
|
+
const row = await this.backend.transaction(async (transaction) => {
|
|
33786
|
+
await transaction.lockStep({
|
|
33787
|
+
...authorityScope(this.capabilityValue),
|
|
33788
|
+
operation_id: request.operation_id,
|
|
33789
|
+
step_id: request.step_id,
|
|
33790
|
+
resource_kind: request.resource_kind,
|
|
33791
|
+
direction: request.direction
|
|
33792
|
+
});
|
|
33793
|
+
const resolved = await this.existingForwardResolution(transaction, request, callDigest);
|
|
33794
|
+
if (resolved)
|
|
33795
|
+
return resolved;
|
|
33796
|
+
const timestamp3 = this.now();
|
|
33797
|
+
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp3, this.capabilityValue));
|
|
33798
|
+
if (!claimed) {
|
|
33799
|
+
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
|
|
33800
|
+
if (binding?.state === "accepted" && binding.normalized_call_digest === callDigest && binding.accepted_receipt_id) {
|
|
33801
|
+
const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
|
|
33802
|
+
if (accepted2) {
|
|
33803
|
+
return this.duplicateFor(transaction, request, callDigest, accepted2);
|
|
33804
|
+
}
|
|
33805
|
+
}
|
|
33806
|
+
return this.terminalFor(transaction, request, callDigest, binding?.state === "removed" ? "target_registration_was_removed" : "target_already_registered", { targetId: binding?.target_id ?? null });
|
|
33807
|
+
}
|
|
33808
|
+
const recordOrTerminal = await this.createObject(transaction, request);
|
|
33809
|
+
if ("outcome" in recordOrTerminal) {
|
|
33810
|
+
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
33811
|
+
return recordOrTerminal;
|
|
33812
|
+
}
|
|
33813
|
+
const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal, this.now());
|
|
33814
|
+
await this.fault("before_receipt_write", request);
|
|
33815
|
+
const stored = await insertDeterministicReceipt(transaction, accepted);
|
|
33816
|
+
await this.fault("after_receipt_write", request);
|
|
33817
|
+
await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
|
|
33818
|
+
target_id: recordOrTerminal.target_id,
|
|
33819
|
+
accepted_receipt_id: stored.receipt_id,
|
|
33820
|
+
result_revision: recordOrTerminal.revision,
|
|
33821
|
+
result_digest: recordOrTerminal.digest,
|
|
33822
|
+
updated_at: this.now()
|
|
33823
|
+
});
|
|
33824
|
+
return stored;
|
|
33825
|
+
});
|
|
33826
|
+
await this.afterCommit(request);
|
|
33827
|
+
const receipt = publicReceipt(row);
|
|
33828
|
+
assertWithinBounds(receipt, request, startedAt);
|
|
33829
|
+
return receipt;
|
|
33830
|
+
} catch (error) {
|
|
33831
|
+
if (!(error instanceof WriteBoundaryError))
|
|
33832
|
+
throw error;
|
|
33833
|
+
const terminal = await this.recordWriteFailure(request, callDigest, error.point);
|
|
33834
|
+
const receipt = publicReceipt(terminal);
|
|
33835
|
+
assertWithinBounds(receipt, request, startedAt);
|
|
33836
|
+
return receipt;
|
|
33837
|
+
}
|
|
33838
|
+
}
|
|
33839
|
+
async recordWriteFailure(request, callDigest, point) {
|
|
33840
|
+
return this.backend.transaction(async (transaction) => {
|
|
33841
|
+
await transaction.lockStep({
|
|
33842
|
+
...authorityScope(this.capabilityValue),
|
|
33843
|
+
operation_id: request.operation_id,
|
|
33844
|
+
step_id: request.step_id,
|
|
33845
|
+
resource_kind: request.resource_kind,
|
|
33846
|
+
direction: request.direction
|
|
33847
|
+
});
|
|
33848
|
+
const exact = await transaction.getReceiptForLookup({
|
|
33849
|
+
...authorityScope(this.capabilityValue),
|
|
33850
|
+
operation_id: request.operation_id,
|
|
33851
|
+
step_id: request.step_id,
|
|
33852
|
+
resource_kind: request.resource_kind,
|
|
33853
|
+
direction: request.direction,
|
|
33854
|
+
idempotency_key: request.idempotency_key,
|
|
33855
|
+
target_selector: request.target_selector
|
|
33856
|
+
});
|
|
33857
|
+
if (exact)
|
|
33858
|
+
return exact;
|
|
33859
|
+
const accepted = await transaction.getAcceptedReceiptForStep({
|
|
33860
|
+
...authorityScope(this.capabilityValue),
|
|
33861
|
+
operation_id: request.operation_id,
|
|
33862
|
+
step_id: request.step_id,
|
|
33863
|
+
resource_kind: request.resource_kind,
|
|
33864
|
+
direction: request.direction
|
|
33865
|
+
});
|
|
33866
|
+
if (accepted) {
|
|
33867
|
+
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 });
|
|
33868
|
+
}
|
|
33869
|
+
const timestamp3 = this.now();
|
|
33870
|
+
const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp3, this.capabilityValue));
|
|
33871
|
+
const terminal = await this.terminalFor(transaction, request, callDigest, `write_failed:${point}`);
|
|
33872
|
+
if (claimed) {
|
|
33873
|
+
await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
|
|
33874
|
+
}
|
|
33875
|
+
return terminal;
|
|
33876
|
+
});
|
|
33877
|
+
}
|
|
33878
|
+
async readExact(request) {
|
|
33879
|
+
const startedAt = Date.now();
|
|
33880
|
+
assertBounds(request);
|
|
33881
|
+
assertResourceKind(request.resource_kind);
|
|
33882
|
+
if (!UUID_PATTERN.test(request.target_id)) {
|
|
33883
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_EXACT_ID_REQUIRED", "exact readback requires a complete Todos object UUID");
|
|
33884
|
+
}
|
|
33885
|
+
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);
|
|
33886
|
+
if (!record) {
|
|
33887
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", `registered ${request.resource_kind} was not found by exact id`, { target_id: request.target_id });
|
|
33888
|
+
}
|
|
33889
|
+
assertWithinBounds(record, request, startedAt);
|
|
33890
|
+
return record;
|
|
33891
|
+
}
|
|
33892
|
+
async lookupReceipt(request) {
|
|
33893
|
+
const startedAt = Date.now();
|
|
33894
|
+
assertBounds(request);
|
|
33895
|
+
assertResourceKind(request.resource_kind);
|
|
33896
|
+
assertDirection(request.direction);
|
|
33897
|
+
if (request.max_items !== 1) {
|
|
33898
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
|
|
33899
|
+
}
|
|
33900
|
+
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) {
|
|
33901
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
|
|
33902
|
+
}
|
|
33903
|
+
requireString(request.operation_id, "operation_id", {
|
|
33904
|
+
min: 8,
|
|
33905
|
+
max: 128,
|
|
33906
|
+
pattern: OPERATION_PATTERN
|
|
33907
|
+
});
|
|
33908
|
+
requireString(request.step_id, "step_id", {
|
|
33909
|
+
min: 3,
|
|
33910
|
+
max: 128,
|
|
33911
|
+
pattern: STEP_PATTERN
|
|
33912
|
+
});
|
|
33913
|
+
requireString(request.target_selector, "target_selector", { max: 512 });
|
|
33914
|
+
requireString(request.idempotency_key, "idempotency_key", {
|
|
33915
|
+
min: 52,
|
|
33916
|
+
max: 52,
|
|
33917
|
+
pattern: IDEMPOTENCY_PATTERN
|
|
33918
|
+
});
|
|
33919
|
+
if (request.target_id !== undefined && !UUID_PATTERN.test(request.target_id)) {
|
|
33920
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_EXACT_ID_REQUIRED", "receipt lookup target_id must be a complete Todos object UUID");
|
|
33921
|
+
}
|
|
33922
|
+
const receipt = await this.backend.getReceiptForLookup({
|
|
33923
|
+
...authorityScope(this.capabilityValue),
|
|
33924
|
+
operation_id: request.operation_id,
|
|
33925
|
+
step_id: request.step_id,
|
|
33926
|
+
resource_kind: request.resource_kind,
|
|
33927
|
+
direction: request.direction,
|
|
33928
|
+
idempotency_key: request.idempotency_key,
|
|
33929
|
+
target_selector: request.target_selector
|
|
33930
|
+
});
|
|
33931
|
+
if (!receipt || request.target_id && receipt.target_id !== request.target_id) {
|
|
33932
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND", "no exact terminal receipt matched the bounded lookup");
|
|
33933
|
+
}
|
|
33934
|
+
return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
|
|
33935
|
+
}
|
|
33936
|
+
async storedAcceptedReceipt(request, supplied) {
|
|
33937
|
+
const stored = await this.backend.getReceiptById(supplied.receipt_id);
|
|
33938
|
+
if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
|
|
33939
|
+
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 });
|
|
33940
|
+
}
|
|
33941
|
+
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) {
|
|
33942
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ACCEPTED_RECEIPT_NOT_FOUND", "accepted receipt does not own this exact operation step and target");
|
|
33943
|
+
}
|
|
33944
|
+
return stored;
|
|
33945
|
+
}
|
|
33946
|
+
async compensate(request) {
|
|
33947
|
+
const startedAt = Date.now();
|
|
33948
|
+
const suppliedAccepted = assertInverseRequest(request, this.capabilityValue);
|
|
33949
|
+
const accepted = await this.storedAcceptedReceipt(request, suppliedAccepted);
|
|
33950
|
+
const callDigest = normalizedCallDigest(request);
|
|
33951
|
+
try {
|
|
33952
|
+
const row = await this.backend.transaction(async (transaction) => {
|
|
33953
|
+
await transaction.lockStep({
|
|
33954
|
+
...authorityScope(this.capabilityValue),
|
|
33955
|
+
operation_id: request.operation_id,
|
|
33956
|
+
step_id: request.step_id,
|
|
33957
|
+
resource_kind: request.resource_kind,
|
|
33958
|
+
direction: request.direction
|
|
33959
|
+
});
|
|
33960
|
+
const exact = await transaction.getReceiptForLookup({
|
|
33961
|
+
...authorityScope(this.capabilityValue),
|
|
33962
|
+
operation_id: request.operation_id,
|
|
33963
|
+
step_id: request.step_id,
|
|
33964
|
+
resource_kind: request.resource_kind,
|
|
33965
|
+
direction: "inverse",
|
|
33966
|
+
idempotency_key: request.idempotency_key,
|
|
33967
|
+
target_selector: request.target_selector
|
|
33968
|
+
});
|
|
33969
|
+
if (exact)
|
|
33970
|
+
return exact;
|
|
33971
|
+
const storedAccepted = await transaction.getReceiptById(accepted.receipt_id);
|
|
33972
|
+
if (!storedAccepted || storedAccepted.outcome !== "accepted" || !storedAccepted.created_by_operation) {
|
|
33973
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ACCEPTED_RECEIPT_NOT_FOUND", "accepted receipt disappeared before conditional inverse");
|
|
33974
|
+
}
|
|
33975
|
+
const binding = await transaction.getBinding(authorityScope(this.capabilityValue), accepted.resource_kind, accepted.target_selector);
|
|
33976
|
+
if (!binding || binding.state !== "accepted" || binding.accepted_receipt_id !== accepted.receipt_id || binding.target_id !== accepted.target_id) {
|
|
33977
|
+
return this.terminalFor(transaction, request, callDigest, "target_not_owned_by_receipt", {
|
|
33978
|
+
targetId: accepted.target_id,
|
|
33979
|
+
acceptedReceiptId: accepted.receipt_id
|
|
33980
|
+
});
|
|
33981
|
+
}
|
|
33982
|
+
await transaction.lockCompensationWrites();
|
|
33983
|
+
const object = request.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
|
|
33984
|
+
if (!object) {
|
|
33985
|
+
return this.terminalFor(transaction, request, callDigest, "target_missing_before_inverse", {
|
|
33986
|
+
targetId: accepted.target_id,
|
|
33987
|
+
acceptedReceiptId: accepted.receipt_id
|
|
33988
|
+
});
|
|
33989
|
+
}
|
|
33990
|
+
const current = request.resource_kind === "project" ? projectRecord(object) : taskListRecord(object);
|
|
33991
|
+
if (current.revision !== accepted.result_revision || current.digest !== accepted.result_digest) {
|
|
33992
|
+
return this.terminalFor(transaction, request, callDigest, "target_drifted", {
|
|
33993
|
+
targetId: accepted.target_id,
|
|
33994
|
+
acceptedReceiptId: accepted.receipt_id
|
|
33995
|
+
});
|
|
33996
|
+
}
|
|
33997
|
+
if (await transaction.hasDependents(request.resource_kind, accepted.target_id)) {
|
|
33998
|
+
return this.terminalFor(transaction, request, callDigest, "target_has_dependents", {
|
|
33999
|
+
targetId: accepted.target_id,
|
|
34000
|
+
acceptedReceiptId: accepted.receipt_id
|
|
34001
|
+
});
|
|
34002
|
+
}
|
|
34003
|
+
await this.fault("before_object_write", request);
|
|
34004
|
+
const deleted = request.resource_kind === "project" ? await transaction.deleteProject(accepted.target_id) : await transaction.deleteTaskList(accepted.target_id);
|
|
34005
|
+
if (!deleted) {
|
|
34006
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "conditional inverse could not delete the exact accepted target");
|
|
34007
|
+
}
|
|
34008
|
+
await this.fault("after_object_write", request);
|
|
34009
|
+
const inverseRecord = {
|
|
34010
|
+
target_id: accepted.target_id,
|
|
34011
|
+
revision: "absent",
|
|
34012
|
+
digest: digestProjectRegistrationValue({
|
|
34013
|
+
target_id: accepted.target_id,
|
|
34014
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
34015
|
+
absent: true
|
|
34016
|
+
})
|
|
34017
|
+
};
|
|
34018
|
+
const inverse = makeAcceptedReceipt(request, callDigest, this.capabilityValue, inverseRecord, this.now());
|
|
34019
|
+
await this.fault("before_receipt_write", request);
|
|
34020
|
+
const stored = await insertDeterministicReceipt(transaction, inverse);
|
|
34021
|
+
await this.fault("after_receipt_write", request);
|
|
34022
|
+
await transaction.setBindingRemoved(authorityScope(this.capabilityValue), accepted.resource_kind, accepted.target_selector, stored.receipt_id, this.now());
|
|
34023
|
+
return stored;
|
|
34024
|
+
});
|
|
34025
|
+
await this.afterCommit(request);
|
|
34026
|
+
const receipt = publicReceipt(row);
|
|
34027
|
+
assertWithinBounds(receipt, request, startedAt);
|
|
34028
|
+
return receipt;
|
|
34029
|
+
} catch (error) {
|
|
34030
|
+
if (!(error instanceof WriteBoundaryError))
|
|
34031
|
+
throw error;
|
|
34032
|
+
const terminal = await this.backend.transaction(async (transaction) => {
|
|
34033
|
+
await transaction.lockStep({
|
|
34034
|
+
...authorityScope(this.capabilityValue),
|
|
34035
|
+
operation_id: request.operation_id,
|
|
34036
|
+
step_id: request.step_id,
|
|
34037
|
+
resource_kind: request.resource_kind,
|
|
34038
|
+
direction: request.direction
|
|
34039
|
+
});
|
|
34040
|
+
return this.terminalFor(transaction, request, callDigest, `write_failed:${error.point}`, {
|
|
34041
|
+
targetId: accepted.target_id,
|
|
34042
|
+
acceptedReceiptId: accepted.receipt_id
|
|
34043
|
+
});
|
|
34044
|
+
});
|
|
34045
|
+
const receipt = publicReceipt(terminal);
|
|
34046
|
+
assertWithinBounds(receipt, request, startedAt);
|
|
34047
|
+
return receipt;
|
|
34048
|
+
}
|
|
34049
|
+
}
|
|
34050
|
+
async verifyInverse(request) {
|
|
34051
|
+
const startedAt = Date.now();
|
|
34052
|
+
const accepted = assertInverseRequest(request, this.capabilityValue);
|
|
34053
|
+
await this.storedAcceptedReceipt(request, accepted);
|
|
34054
|
+
const receipt = await this.backend.getReceiptForLookup({
|
|
34055
|
+
...authorityScope(this.capabilityValue),
|
|
34056
|
+
operation_id: request.operation_id,
|
|
34057
|
+
step_id: request.step_id,
|
|
34058
|
+
resource_kind: request.resource_kind,
|
|
34059
|
+
direction: "inverse",
|
|
34060
|
+
idempotency_key: request.idempotency_key,
|
|
34061
|
+
target_selector: request.target_selector
|
|
34062
|
+
});
|
|
34063
|
+
if (!receipt || receipt.outcome !== "accepted" || receipt.accepted_receipt_id !== accepted.receipt_id || receipt.result_revision !== "absent" || !receipt.result_digest) {
|
|
34064
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND", "accepted conditional inverse receipt was not found");
|
|
34065
|
+
}
|
|
34066
|
+
const object = request.resource_kind === "project" ? await this.backend.getProject(accepted.target_id) : await this.backend.getTaskList(accepted.target_id);
|
|
34067
|
+
if (object) {
|
|
34068
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "inverse verification found the accepted target still present");
|
|
34069
|
+
}
|
|
34070
|
+
const verification = {
|
|
34071
|
+
target_id: accepted.target_id,
|
|
34072
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
34073
|
+
absent: true,
|
|
34074
|
+
digest: digestProjectRegistrationValue({
|
|
34075
|
+
target_id: accepted.target_id,
|
|
34076
|
+
accepted_receipt_id: accepted.receipt_id,
|
|
34077
|
+
absent: true
|
|
34078
|
+
})
|
|
34079
|
+
};
|
|
34080
|
+
assertWithinBounds(verification, request, startedAt);
|
|
34081
|
+
if (verification.digest !== receipt.result_digest) {
|
|
34082
|
+
throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "inverse verification digest does not match the immutable receipt");
|
|
34083
|
+
}
|
|
34084
|
+
return verification;
|
|
34085
|
+
}
|
|
34086
|
+
}
|
|
34087
|
+
function createLocalTodosProjectRegistrationAuthority(db, options = {}) {
|
|
34088
|
+
return new PackageOwnedTodosProjectRegistrationAuthority(new SqliteTodosProjectRegistrationBackend(db), options);
|
|
34089
|
+
}
|
|
34090
|
+
function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
|
|
34091
|
+
const {
|
|
34092
|
+
service,
|
|
34093
|
+
tableName,
|
|
34094
|
+
cursorTableName,
|
|
34095
|
+
...authorityOptions
|
|
34096
|
+
} = options;
|
|
34097
|
+
return new PackageOwnedTodosProjectRegistrationAuthority(new PostgresTodosProjectRegistrationBackend(client, {
|
|
34098
|
+
service,
|
|
34099
|
+
tableName,
|
|
34100
|
+
cursorTableName
|
|
34101
|
+
}), authorityOptions);
|
|
34102
|
+
}
|
|
34103
|
+
// src/project-registration/http.ts
|
|
34104
|
+
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
34105
|
+
function json(body, status = 200) {
|
|
34106
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
34107
|
+
}
|
|
34108
|
+
function errorStatus(error) {
|
|
34109
|
+
switch (error.code) {
|
|
34110
|
+
case "TODOS_PROJECT_REGISTRATION_INVALID_INPUT":
|
|
34111
|
+
case "TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS":
|
|
34112
|
+
case "TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH":
|
|
34113
|
+
case "TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH":
|
|
34114
|
+
case "TODOS_PROJECT_REGISTRATION_IDEMPOTENCY_MISMATCH":
|
|
34115
|
+
case "TODOS_PROJECT_REGISTRATION_EXACT_ID_REQUIRED":
|
|
34116
|
+
return 400;
|
|
34117
|
+
case "TODOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND":
|
|
34118
|
+
case "TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND":
|
|
34119
|
+
case "TODOS_PROJECT_REGISTRATION_ACCEPTED_RECEIPT_NOT_FOUND":
|
|
34120
|
+
return 404;
|
|
34121
|
+
case "TODOS_PROJECT_REGISTRATION_RESPONSE_TOO_LARGE":
|
|
34122
|
+
return 413;
|
|
34123
|
+
case "TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED":
|
|
34124
|
+
return 408;
|
|
34125
|
+
case "TODOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE":
|
|
34126
|
+
return 503;
|
|
34127
|
+
default:
|
|
34128
|
+
return 409;
|
|
34129
|
+
}
|
|
34130
|
+
}
|
|
34131
|
+
async function readJson(req) {
|
|
34132
|
+
try {
|
|
34133
|
+
const value = await req.json();
|
|
34134
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
34135
|
+
} catch {
|
|
34136
|
+
return null;
|
|
34137
|
+
}
|
|
34138
|
+
}
|
|
34139
|
+
async function handleTodosProjectRegistrationHttpRequest(req, url, authority, basePath = "/v1/project-registration") {
|
|
34140
|
+
const path = url.pathname;
|
|
34141
|
+
if (path !== basePath && !path.startsWith(`${basePath}/`))
|
|
34142
|
+
return null;
|
|
34143
|
+
const action = path.slice(basePath.length).split("/").filter(Boolean).join("/");
|
|
34144
|
+
const method = req.method.toUpperCase();
|
|
34145
|
+
try {
|
|
34146
|
+
if ((action === "" || action === "capability") && method === "GET") {
|
|
34147
|
+
return json({ capability: await authority.capability() });
|
|
34148
|
+
}
|
|
34149
|
+
if (method !== "POST")
|
|
34150
|
+
return json({ error: "method not allowed" }, 405);
|
|
34151
|
+
const body = await readJson(req);
|
|
34152
|
+
if (!body) {
|
|
34153
|
+
return json({
|
|
34154
|
+
error: "invalid JSON body",
|
|
34155
|
+
code: "TODOS_PROJECT_REGISTRATION_INVALID_INPUT"
|
|
34156
|
+
}, 400);
|
|
34157
|
+
}
|
|
34158
|
+
if (action === "create") {
|
|
34159
|
+
return json({
|
|
34160
|
+
receipt: await authority.create(body)
|
|
34161
|
+
}, 201);
|
|
34162
|
+
}
|
|
34163
|
+
if (action === "receipts/lookup") {
|
|
34164
|
+
return json(await authority.lookupReceipt(body));
|
|
34165
|
+
}
|
|
34166
|
+
if (action === "read-exact") {
|
|
34167
|
+
return json({
|
|
34168
|
+
record: await authority.readExact(body)
|
|
34169
|
+
});
|
|
34170
|
+
}
|
|
34171
|
+
if (action === "compensate") {
|
|
34172
|
+
return json({
|
|
34173
|
+
receipt: await authority.compensate(body)
|
|
34174
|
+
}, 201);
|
|
34175
|
+
}
|
|
34176
|
+
if (action === "verify-inverse") {
|
|
34177
|
+
return json({
|
|
34178
|
+
verification: await authority.verifyInverse(body)
|
|
34179
|
+
});
|
|
34180
|
+
}
|
|
34181
|
+
return json({
|
|
34182
|
+
error: "unknown Todos project-registration route",
|
|
34183
|
+
code: "TODOS_PROJECT_REGISTRATION_RECEIPT_NOT_FOUND"
|
|
34184
|
+
}, 404);
|
|
34185
|
+
} catch (cause) {
|
|
34186
|
+
if (cause instanceof TodosProjectRegistrationError) {
|
|
34187
|
+
return json({
|
|
34188
|
+
error: cause.message,
|
|
34189
|
+
code: cause.code,
|
|
34190
|
+
details: cause.details,
|
|
34191
|
+
authoritative: true
|
|
34192
|
+
}, errorStatus(cause));
|
|
34193
|
+
}
|
|
34194
|
+
return json({
|
|
34195
|
+
error: cause instanceof Error ? cause.message : "internal registration error",
|
|
34196
|
+
code: "TODOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE"
|
|
34197
|
+
}, 500);
|
|
34198
|
+
}
|
|
34199
|
+
}
|
|
34200
|
+
function withoutTarget(value) {
|
|
34201
|
+
const { target: _target, ...serializable } = value;
|
|
34202
|
+
return serializable;
|
|
34203
|
+
}
|
|
34204
|
+
|
|
34205
|
+
class TodosProjectRegistrationHttpClient {
|
|
34206
|
+
authority = "todos";
|
|
34207
|
+
baseUrl;
|
|
34208
|
+
fetchImpl;
|
|
34209
|
+
headers;
|
|
34210
|
+
constructor(options) {
|
|
34211
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
34212
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
34213
|
+
this.headers = {
|
|
34214
|
+
...options.headers,
|
|
34215
|
+
...options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
|
|
34216
|
+
"Content-Type": "application/json"
|
|
34217
|
+
};
|
|
34218
|
+
}
|
|
34219
|
+
async request(action, init = {}) {
|
|
34220
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/project-registration${action}`, {
|
|
34221
|
+
...init,
|
|
34222
|
+
headers: { ...this.headers, ...init.headers ?? {} }
|
|
34223
|
+
});
|
|
34224
|
+
const body = await response.json();
|
|
34225
|
+
if (!response.ok) {
|
|
34226
|
+
throw new TodosProjectRegistrationError(typeof body["code"] === "string" ? body["code"] : "TODOS_PROJECT_REGISTRATION_ATOMICITY_UNAVAILABLE", typeof body["error"] === "string" ? body["error"] : `Todos project registration HTTP ${response.status}`, body["details"] && typeof body["details"] === "object" ? body["details"] : {});
|
|
34227
|
+
}
|
|
34228
|
+
return body;
|
|
34229
|
+
}
|
|
34230
|
+
async capability() {
|
|
34231
|
+
const body = await this.request("/capability");
|
|
34232
|
+
return body.capability;
|
|
34233
|
+
}
|
|
34234
|
+
async create(request) {
|
|
34235
|
+
const body = await this.request("/create", { method: "POST", body: JSON.stringify(withoutTarget(request)) });
|
|
34236
|
+
return body.receipt;
|
|
34237
|
+
}
|
|
34238
|
+
async readExact(request) {
|
|
34239
|
+
const body = await this.request("/read-exact", { method: "POST", body: JSON.stringify(withoutTarget(request)) });
|
|
34240
|
+
return body.record;
|
|
34241
|
+
}
|
|
34242
|
+
async lookupReceipt(request) {
|
|
34243
|
+
return this.request("/receipts/lookup", { method: "POST", body: JSON.stringify(request) });
|
|
34244
|
+
}
|
|
34245
|
+
async compensate(request) {
|
|
34246
|
+
const body = await this.request("/compensate", { method: "POST", body: JSON.stringify(withoutTarget(request)) });
|
|
34247
|
+
return body.receipt;
|
|
34248
|
+
}
|
|
34249
|
+
async verifyInverse(request) {
|
|
34250
|
+
const body = await this.request("/verify-inverse", {
|
|
34251
|
+
method: "POST",
|
|
34252
|
+
body: JSON.stringify(withoutTarget(request))
|
|
34253
|
+
});
|
|
34254
|
+
return body.verification;
|
|
34255
|
+
}
|
|
34256
|
+
}
|
|
34257
|
+
function createTodosProjectRegistrationHttpClient(options) {
|
|
34258
|
+
return new TodosProjectRegistrationHttpClient(options);
|
|
34259
|
+
}
|
|
31899
34260
|
// src/lib/native-storage-status.ts
|
|
31900
34261
|
function getNativeStorageStatus(env = process.env) {
|
|
31901
34262
|
const issues = [];
|
|
@@ -32031,7 +34392,7 @@ init_task_lifecycle();
|
|
|
32031
34392
|
init_task_crud();
|
|
32032
34393
|
init_redaction();
|
|
32033
34394
|
import { Database as Database3 } from "bun:sqlite";
|
|
32034
|
-
import { createHash as
|
|
34395
|
+
import { createHash as createHash13 } from "crypto";
|
|
32035
34396
|
import { existsSync as existsSync9, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
|
|
32036
34397
|
import { basename as basename2, dirname as dirname6, join as join9, resolve as resolve10 } from "path";
|
|
32037
34398
|
|
|
@@ -32314,7 +34675,7 @@ function normalizePath3(input) {
|
|
|
32314
34675
|
return resolve10(input);
|
|
32315
34676
|
}
|
|
32316
34677
|
function sourceStoreId(sourceDbPath) {
|
|
32317
|
-
const digest =
|
|
34678
|
+
const digest = createHash13("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
|
|
32318
34679
|
return `sqlite:${digest}`;
|
|
32319
34680
|
}
|
|
32320
34681
|
function inferSourceRepoPath(sourceDbPath) {
|
|
@@ -34192,7 +36553,7 @@ function addSourceOnce(projectId, type, name, uri, metadata, db) {
|
|
|
34192
36553
|
function bootstrapProject(options = {}, db) {
|
|
34193
36554
|
const d = db || getDatabase();
|
|
34194
36555
|
const discovery = discoverProjectWorkspace(options.path);
|
|
34195
|
-
const
|
|
36556
|
+
const taskListSlug2 = options.taskListSlug || `todos-${slugify(options.name || discovery.projectName)}`;
|
|
34196
36557
|
if (options.dryRun) {
|
|
34197
36558
|
return {
|
|
34198
36559
|
dryRun: true,
|
|
@@ -34206,15 +36567,15 @@ function bootstrapProject(options = {}, db) {
|
|
|
34206
36567
|
const beforeProject = getProjectByCanonicalPath(discovery.projectPath, d);
|
|
34207
36568
|
let project = ensureProject(options.name || discovery.projectName, discovery.projectPath, d);
|
|
34208
36569
|
const createdProject = !beforeProject;
|
|
34209
|
-
if (project.task_list_id !==
|
|
36570
|
+
if (project.task_list_id !== taskListSlug2 || options.name && project.name !== options.name) {
|
|
34210
36571
|
project = renameProject(project.id, {
|
|
34211
36572
|
name: options.name ?? project.name,
|
|
34212
|
-
new_slug:
|
|
36573
|
+
new_slug: taskListSlug2
|
|
34213
36574
|
}, d).project;
|
|
34214
36575
|
}
|
|
34215
36576
|
setMachineLocalPath(project.id, discovery.projectPath, d);
|
|
34216
|
-
const beforeTaskList = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(project.id,
|
|
34217
|
-
let taskList = ensureTaskList(`${project.name} Tasks`,
|
|
36577
|
+
const beforeTaskList = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(project.id, taskListSlug2);
|
|
36578
|
+
let taskList = ensureTaskList(`${project.name} Tasks`, taskListSlug2, project.id, d);
|
|
34218
36579
|
if (options.routeEnabled && taskList.metadata.route_enabled !== true) {
|
|
34219
36580
|
taskList = updateTaskList(taskList.id, {
|
|
34220
36581
|
metadata: {
|
|
@@ -34276,7 +36637,7 @@ init_comments();
|
|
|
34276
36637
|
|
|
34277
36638
|
// src/db/api-keys.ts
|
|
34278
36639
|
init_database();
|
|
34279
|
-
import { createHash as
|
|
36640
|
+
import { createHash as createHash14, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
34280
36641
|
function rowToRecord(row) {
|
|
34281
36642
|
return {
|
|
34282
36643
|
id: row.id,
|
|
@@ -34290,7 +36651,7 @@ function rowToRecord(row) {
|
|
|
34290
36651
|
};
|
|
34291
36652
|
}
|
|
34292
36653
|
function hashApiKey(key) {
|
|
34293
|
-
return
|
|
36654
|
+
return createHash14("sha256").update(key).digest("hex");
|
|
34294
36655
|
}
|
|
34295
36656
|
function safeEqualHex(a, b) {
|
|
34296
36657
|
if (a.length !== b.length)
|
|
@@ -37350,10 +39711,10 @@ function extractEmbeddedBridge(markdown) {
|
|
|
37350
39711
|
const match = markdown.match(/<!--\s*hasna\.todos\.bridge\s*\n([\s\S]*?)\n\s*-->/);
|
|
37351
39712
|
if (!match)
|
|
37352
39713
|
return null;
|
|
37353
|
-
const
|
|
39714
|
+
const json2 = match[1].split(`
|
|
37354
39715
|
`).map((line) => line.replace(/^ /, "")).join(`
|
|
37355
39716
|
`);
|
|
37356
|
-
return JSON.parse(
|
|
39717
|
+
return JSON.parse(json2);
|
|
37357
39718
|
}
|
|
37358
39719
|
function frontmatterValue(markdown, key) {
|
|
37359
39720
|
const match = markdown.match(/^---\n([\s\S]*?)\n---/);
|
|
@@ -38737,7 +41098,7 @@ init_database();
|
|
|
38737
41098
|
init_tasks();
|
|
38738
41099
|
import { existsSync as existsSync17, readFileSync as readFileSync13 } from "fs";
|
|
38739
41100
|
import { basename as basename5 } from "path";
|
|
38740
|
-
import { createHash as
|
|
41101
|
+
import { createHash as createHash15 } from "crypto";
|
|
38741
41102
|
init_secret_redaction();
|
|
38742
41103
|
var INBOX_INTAKE_SCHEMA = "todos.inbox_intake.v1";
|
|
38743
41104
|
var INTAKE_SOURCE_TYPES = [
|
|
@@ -38750,7 +41111,7 @@ var INTAKE_SOURCE_TYPES = [
|
|
|
38750
41111
|
];
|
|
38751
41112
|
var INTAKE_TRIAGE_STATUSES = ["preview", "triaged", "duplicate", "created"];
|
|
38752
41113
|
function fingerprint2(text) {
|
|
38753
|
-
return
|
|
41114
|
+
return createHash15("sha256").update(text).digest("hex").slice(0, 16);
|
|
38754
41115
|
}
|
|
38755
41116
|
function loadRawContent(input) {
|
|
38756
41117
|
if (input.github_url) {
|
|
@@ -44210,7 +46571,7 @@ init_database();
|
|
|
44210
46571
|
init_tasks();
|
|
44211
46572
|
init_redaction();
|
|
44212
46573
|
init_sync_utils();
|
|
44213
|
-
import { createHash as
|
|
46574
|
+
import { createHash as createHash16 } from "crypto";
|
|
44214
46575
|
import { existsSync as existsSync22, readFileSync as readFileSync19, statSync as statSync9 } from "fs";
|
|
44215
46576
|
import { hostname as hostname3, platform, arch } from "os";
|
|
44216
46577
|
import { dirname as dirname15, join as join18, resolve as resolve17 } from "path";
|
|
@@ -44232,7 +46593,7 @@ var CONFIG_FILES = [
|
|
|
44232
46593
|
"dashboard/vite.config.ts"
|
|
44233
46594
|
];
|
|
44234
46595
|
function sha2566(value) {
|
|
44235
|
-
return
|
|
46596
|
+
return createHash16("sha256").update(value).digest("hex");
|
|
44236
46597
|
}
|
|
44237
46598
|
function fileRecord(root, relativePath) {
|
|
44238
46599
|
const path = join18(root, relativePath);
|
|
@@ -44484,7 +46845,7 @@ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
|
|
|
44484
46845
|
init_database();
|
|
44485
46846
|
init_projects();
|
|
44486
46847
|
init_plans();
|
|
44487
|
-
import { createHash as
|
|
46848
|
+
import { createHash as createHash17 } from "crypto";
|
|
44488
46849
|
import { mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "fs";
|
|
44489
46850
|
import { dirname as dirname16, join as join19 } from "path";
|
|
44490
46851
|
var DECISION_RECORD_SCHEMA = "todos.decision_record.v1";
|
|
@@ -44540,7 +46901,7 @@ function rowToDecisionRecord(row) {
|
|
|
44540
46901
|
}
|
|
44541
46902
|
function stableSnapshotHash(payload) {
|
|
44542
46903
|
const { captured_at: _capturedAt, ...rest } = payload;
|
|
44543
|
-
return
|
|
46904
|
+
return createHash17("sha256").update(JSON.stringify(rest)).digest("hex");
|
|
44544
46905
|
}
|
|
44545
46906
|
function createDecisionRecord(input, db) {
|
|
44546
46907
|
const d = db || getDatabase();
|
|
@@ -49031,7 +51392,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
|
|
|
49031
51392
|
init_tasks();
|
|
49032
51393
|
init_task_files();
|
|
49033
51394
|
import { existsSync as existsSync27, readFileSync as readFileSync23, statSync as statSync10 } from "fs";
|
|
49034
|
-
import { createHash as
|
|
51395
|
+
import { createHash as createHash18 } from "crypto";
|
|
49035
51396
|
import { relative as relative6, resolve as resolve18, join as join25 } from "path";
|
|
49036
51397
|
var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
49037
51398
|
var DEFAULT_EXTENSIONS = new Set([
|
|
@@ -49096,7 +51457,7 @@ var SKIP_DIRS2 = new Set([
|
|
|
49096
51457
|
".parcel-cache"
|
|
49097
51458
|
]);
|
|
49098
51459
|
function stableHash(value) {
|
|
49099
|
-
return
|
|
51460
|
+
return createHash18("sha256").update(value).digest("hex");
|
|
49100
51461
|
}
|
|
49101
51462
|
function normalizePathForMatch(value) {
|
|
49102
51463
|
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -49994,7 +52355,7 @@ function renderWorkflowStatesMarkdown(states = listWorkflowStates()) {
|
|
|
49994
52355
|
}
|
|
49995
52356
|
// src/lib/agent-replay-simulator.ts
|
|
49996
52357
|
init_redaction();
|
|
49997
|
-
import { createHash as
|
|
52358
|
+
import { createHash as createHash19 } from "crypto";
|
|
49998
52359
|
import { readFileSync as readFileSync24 } from "fs";
|
|
49999
52360
|
function isObject(value) {
|
|
50000
52361
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -50016,7 +52377,7 @@ function stable2(value) {
|
|
|
50016
52377
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable2(value[key])]));
|
|
50017
52378
|
}
|
|
50018
52379
|
function fingerprint3(value) {
|
|
50019
|
-
return
|
|
52380
|
+
return createHash19("sha256").update(JSON.stringify(stable2(value))).digest("hex");
|
|
50020
52381
|
}
|
|
50021
52382
|
function unpackFixture(input) {
|
|
50022
52383
|
if (!isObject(input))
|
|
@@ -50255,7 +52616,7 @@ function renderAgentReplaySimulationMarkdown(simulation) {
|
|
|
50255
52616
|
}
|
|
50256
52617
|
// src/lib/local-extensions.ts
|
|
50257
52618
|
init_config2();
|
|
50258
|
-
import { createHash as
|
|
52619
|
+
import { createHash as createHash20, createVerify } from "crypto";
|
|
50259
52620
|
import { existsSync as existsSync28, readdirSync as readdirSync6, readFileSync as readFileSync25, statSync as statSync11 } from "fs";
|
|
50260
52621
|
import { basename as basename6, join as join26, resolve as resolve19 } from "path";
|
|
50261
52622
|
init_redaction();
|
|
@@ -50343,7 +52704,7 @@ function parseJson2(path) {
|
|
|
50343
52704
|
return JSON.parse(readFileSync25(path, "utf8"));
|
|
50344
52705
|
}
|
|
50345
52706
|
function sha2567(bytes) {
|
|
50346
|
-
return `sha256:${
|
|
52707
|
+
return `sha256:${createHash20("sha256").update(bytes).digest("hex")}`;
|
|
50347
52708
|
}
|
|
50348
52709
|
function compareVersions(a, b) {
|
|
50349
52710
|
const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -52976,6 +55337,7 @@ export {
|
|
|
52976
55337
|
startTaskRun,
|
|
52977
55338
|
startTask,
|
|
52978
55339
|
startFocusSession,
|
|
55340
|
+
sqliteTodosProjectRegistrationSchemaSql,
|
|
52979
55341
|
softDeleteArtifact,
|
|
52980
55342
|
snoozeReminder,
|
|
52981
55343
|
slugify,
|
|
@@ -53164,6 +55526,7 @@ export {
|
|
|
53164
55526
|
previewInboxIntake,
|
|
53165
55527
|
previewBuiltinTemplate,
|
|
53166
55528
|
postgresTodosSyncSchemaSql,
|
|
55529
|
+
postgresTodosProjectRegistrationSchemaSql,
|
|
53167
55530
|
pollLocalSnapshots,
|
|
53168
55531
|
planRunArtifactsS3Sync,
|
|
53169
55532
|
pauseFocusSession,
|
|
@@ -53354,6 +55717,7 @@ export {
|
|
|
53354
55717
|
importActivityLog,
|
|
53355
55718
|
hasSecretFindings,
|
|
53356
55719
|
hasActiveApiKeys,
|
|
55720
|
+
handleTodosProjectRegistrationHttpRequest,
|
|
53357
55721
|
getWorkspaceTrustStatus,
|
|
53358
55722
|
getWorkflowPrompt,
|
|
53359
55723
|
getWebhook,
|
|
@@ -53657,6 +56021,7 @@ export {
|
|
|
53657
56021
|
discoverTaskRouteSources,
|
|
53658
56022
|
discoverProjectWorkspace,
|
|
53659
56023
|
discoverLocalExtensions,
|
|
56024
|
+
digestProjectRegistrationValue,
|
|
53660
56025
|
deterministicPrGroupId,
|
|
53661
56026
|
deterministicPrGroupAttemptId,
|
|
53662
56027
|
detectSourceType,
|
|
@@ -53665,6 +56030,7 @@ export {
|
|
|
53665
56030
|
detectInboxSourceType,
|
|
53666
56031
|
detectCyclesFromEdges,
|
|
53667
56032
|
describeTerminalNotificationRule,
|
|
56033
|
+
deriveTodosProjectRegistrationIdempotencyKey,
|
|
53668
56034
|
deriveInboxTitle,
|
|
53669
56035
|
deleteWebhook,
|
|
53670
56036
|
deleteTemplate,
|
|
@@ -53699,6 +56065,7 @@ export {
|
|
|
53699
56065
|
createTodosStorageAdapter,
|
|
53700
56066
|
createTodosS3ArtifactStore,
|
|
53701
56067
|
createTodosRegistry,
|
|
56068
|
+
createTodosProjectRegistrationHttpClient,
|
|
53702
56069
|
createTemplate,
|
|
53703
56070
|
createTaskList,
|
|
53704
56071
|
createTaskBoard,
|
|
@@ -53719,6 +56086,7 @@ export {
|
|
|
53719
56086
|
createProject,
|
|
53720
56087
|
createPostgresTodosSyncStore,
|
|
53721
56088
|
createPostgresTodosStorageAdapter,
|
|
56089
|
+
createPostgresTodosProjectRegistrationAuthority,
|
|
53722
56090
|
createPlanWithSteps,
|
|
53723
56091
|
createPlan,
|
|
53724
56092
|
createOrg,
|
|
@@ -53726,6 +56094,7 @@ export {
|
|
|
53726
56094
|
createMilestone,
|
|
53727
56095
|
createMcpManifest,
|
|
53728
56096
|
createLocalUsageLedger,
|
|
56097
|
+
createLocalTodosProjectRegistrationAuthority,
|
|
53729
56098
|
createLocalSqliteTodosStorageAdapter,
|
|
53730
56099
|
createLocalReport,
|
|
53731
56100
|
createLocalPrGroupLedger,
|
|
@@ -53799,6 +56168,7 @@ export {
|
|
|
53799
56168
|
categorizeMcpTool,
|
|
53800
56169
|
captureKnowledgeSnapshot,
|
|
53801
56170
|
captureEnvironmentSnapshot,
|
|
56171
|
+
canonicalProjectRegistrationJson,
|
|
53802
56172
|
cancelDispatch,
|
|
53803
56173
|
cancelAgentRunDispatch,
|
|
53804
56174
|
cancelAgentRun,
|
|
@@ -53876,6 +56246,8 @@ export {
|
|
|
53876
56246
|
VersionConflictError,
|
|
53877
56247
|
VERIFICATION_EVIDENCE_SCHEMA,
|
|
53878
56248
|
USER_SCAFFOLD_SCHEMA,
|
|
56249
|
+
TodosProjectRegistrationHttpClient,
|
|
56250
|
+
TodosProjectRegistrationError,
|
|
53879
56251
|
TodosClient,
|
|
53880
56252
|
TaskNotFoundError,
|
|
53881
56253
|
TaskListNotFoundError,
|
|
@@ -53887,6 +56259,9 @@ export {
|
|
|
53887
56259
|
TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION,
|
|
53888
56260
|
TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT,
|
|
53889
56261
|
TODOS_REGISTRY,
|
|
56262
|
+
TODOS_PROJECT_REGISTRATION_SCHEMA_VERSION,
|
|
56263
|
+
TODOS_PROJECT_REGISTRATION_ROUTE,
|
|
56264
|
+
TODOS_PROJECT_REGISTRATION_CALLER_ROUTE,
|
|
53890
56265
|
TODOS_PACKAGE_EXPORTS,
|
|
53891
56266
|
TODOS_ONBOARDING_FIXTURE_SOURCE,
|
|
53892
56267
|
TODOS_ONBOARDING_FIXTURE_LIBRARY_VERSION,
|
|
@@ -53925,6 +56300,7 @@ export {
|
|
|
53925
56300
|
TASK_FINDING_SCHEMA_VERSION,
|
|
53926
56301
|
TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
53927
56302
|
TASK_DEPENDENCY_EDGES_SCHEMA,
|
|
56303
|
+
SqliteTodosProjectRegistrationBackend,
|
|
53928
56304
|
STORAGE_TABLES,
|
|
53929
56305
|
SECRET_REDACTION_SCHEMA,
|
|
53930
56306
|
SCHEMA_SEMVER,
|
|
@@ -53951,7 +56327,9 @@ export {
|
|
|
53951
56327
|
PrGroupLedgerError,
|
|
53952
56328
|
PrGroupLedger,
|
|
53953
56329
|
PrGroupHttpClient,
|
|
56330
|
+
PostgresTodosProjectRegistrationBackend,
|
|
53954
56331
|
PlanNotFoundError,
|
|
56332
|
+
PackageOwnedTodosProjectRegistrationAuthority,
|
|
53955
56333
|
PROJECT_DEPENDENCY_GRAPH_SCHEMA,
|
|
53956
56334
|
PREWRITE_SECRET_SCAN_SCHEMA,
|
|
53957
56335
|
PLAN_STATUSES,
|