@vendoai/store 0.1.0 → 0.4.0

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.
Files changed (112) hide show
  1. package/README.md +81 -0
  2. package/dist/blobs.d.ts +5 -0
  3. package/dist/blobs.d.ts.map +1 -0
  4. package/dist/blobs.js +62 -0
  5. package/dist/blobs.js.map +1 -0
  6. package/dist/crypto.d.ts +10 -0
  7. package/dist/crypto.d.ts.map +1 -0
  8. package/dist/crypto.js +44 -0
  9. package/dist/crypto.js.map +1 -0
  10. package/dist/db.d.ts +26 -26
  11. package/dist/db.d.ts.map +1 -0
  12. package/dist/db.js +335 -86
  13. package/dist/db.js.map +1 -0
  14. package/dist/erase.d.ts +33 -0
  15. package/dist/erase.d.ts.map +1 -0
  16. package/dist/erase.js +107 -0
  17. package/dist/erase.js.map +1 -0
  18. package/dist/helpers/approvals.d.ts +12 -0
  19. package/dist/helpers/approvals.d.ts.map +1 -0
  20. package/dist/helpers/approvals.js +37 -0
  21. package/dist/helpers/approvals.js.map +1 -0
  22. package/dist/helpers/apps.d.ts +15 -0
  23. package/dist/helpers/apps.d.ts.map +1 -0
  24. package/dist/helpers/apps.js +42 -0
  25. package/dist/helpers/apps.js.map +1 -0
  26. package/dist/helpers/audit.d.ts +24 -0
  27. package/dist/helpers/audit.d.ts.map +1 -0
  28. package/dist/helpers/audit.js +79 -0
  29. package/dist/helpers/audit.js.map +1 -0
  30. package/dist/helpers/grants.d.ts +14 -0
  31. package/dist/helpers/grants.d.ts.map +1 -0
  32. package/dist/helpers/grants.js +46 -0
  33. package/dist/helpers/grants.js.map +1 -0
  34. package/dist/helpers/rows.d.ts +30 -0
  35. package/dist/helpers/rows.d.ts.map +1 -0
  36. package/dist/helpers/rows.js +191 -0
  37. package/dist/helpers/rows.js.map +1 -0
  38. package/dist/helpers/runs.d.ts +19 -0
  39. package/dist/helpers/runs.d.ts.map +1 -0
  40. package/dist/helpers/runs.js +47 -0
  41. package/dist/helpers/runs.js.map +1 -0
  42. package/dist/helpers/state.d.ts +9 -0
  43. package/dist/helpers/state.d.ts.map +1 -0
  44. package/dist/helpers/state.js +22 -0
  45. package/dist/helpers/state.js.map +1 -0
  46. package/dist/helpers/subjects.d.ts +32 -0
  47. package/dist/helpers/subjects.d.ts.map +1 -0
  48. package/dist/helpers/subjects.js +78 -0
  49. package/dist/helpers/subjects.js.map +1 -0
  50. package/dist/helpers/threads.d.ts +19 -0
  51. package/dist/helpers/threads.d.ts.map +1 -0
  52. package/dist/helpers/threads.js +38 -0
  53. package/dist/helpers/threads.js.map +1 -0
  54. package/dist/helpers/types.d.ts +60 -0
  55. package/dist/helpers/types.d.ts.map +1 -0
  56. package/dist/helpers/types.js +2 -0
  57. package/dist/helpers/types.js.map +1 -0
  58. package/dist/helpers/utils.d.ts +37 -0
  59. package/dist/helpers/utils.d.ts.map +1 -0
  60. package/dist/helpers/utils.js +94 -0
  61. package/dist/helpers/utils.js.map +1 -0
  62. package/dist/index.d.ts +13 -11
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +17 -15
  65. package/dist/index.js.map +1 -0
  66. package/dist/records.d.ts +7 -0
  67. package/dist/records.d.ts.map +1 -0
  68. package/dist/records.js +208 -0
  69. package/dist/records.js.map +1 -0
  70. package/dist/routing.d.ts +7 -0
  71. package/dist/routing.d.ts.map +1 -0
  72. package/dist/routing.js +393 -0
  73. package/dist/routing.js.map +1 -0
  74. package/dist/schema.d.ts +18 -1420
  75. package/dist/schema.d.ts.map +1 -0
  76. package/dist/schema.js +206 -114
  77. package/dist/schema.js.map +1 -0
  78. package/dist/secrets.d.ts +13 -0
  79. package/dist/secrets.d.ts.map +1 -0
  80. package/dist/secrets.js +86 -0
  81. package/dist/secrets.js.map +1 -0
  82. package/dist/sessions.d.ts +53 -0
  83. package/dist/sessions.d.ts.map +1 -0
  84. package/dist/sessions.js +85 -0
  85. package/dist/sessions.js.map +1 -0
  86. package/dist/store.d.ts +24 -0
  87. package/dist/store.d.ts.map +1 -0
  88. package/dist/store.js +49 -0
  89. package/dist/store.js.map +1 -0
  90. package/dist/validate.d.ts +25 -0
  91. package/dist/validate.d.ts.map +1 -0
  92. package/dist/validate.js +144 -0
  93. package/dist/validate.js.map +1 -0
  94. package/package.json +15 -14
  95. package/dist/automation-store.d.ts +0 -62
  96. package/dist/automation-store.js +0 -675
  97. package/dist/connections-store.d.ts +0 -25
  98. package/dist/connections-store.js +0 -102
  99. package/dist/decision-store.d.ts +0 -8
  100. package/dist/decision-store.js +0 -39
  101. package/dist/meta.d.ts +0 -5
  102. package/dist/meta.js +0 -20
  103. package/dist/thread-store.d.ts +0 -5
  104. package/dist/thread-store.js +0 -209
  105. package/dist/vendo-registry.d.ts +0 -5
  106. package/dist/vendo-registry.js +0 -52
  107. package/migrations/.gitkeep +0 -0
  108. package/migrations/0000_living_kate_bishop.sql +0 -110
  109. package/migrations/0001_brainy_tinkerer.sql +0 -13
  110. package/migrations/meta/0000_snapshot.json +0 -757
  111. package/migrations/meta/0001_snapshot.json +0 -859
  112. package/migrations/meta/_journal.json +0 -20
@@ -0,0 +1,208 @@
1
+ import { canonicalJson, VendoError, } from "@vendoai/core";
2
+ import { appScopeId, cursorMs, decodeCursor, encodeCursor, iso, jsonParam, pageLimit, requireKnownApp, text, unknownAppError, } from "./helpers/utils.js";
3
+ function recordFromRow(row) {
4
+ const refs = row["refs"];
5
+ const revision = row["revision"];
6
+ if (revision !== undefined && !(typeof revision === "string" || typeof revision === "number" || typeof revision === "bigint")) {
7
+ throw new Error("Expected database revision");
8
+ }
9
+ return {
10
+ id: text(row["id"]),
11
+ data: row["data"],
12
+ ...(refs == null ? {} : { refs }),
13
+ createdAt: iso(row["created_at"]),
14
+ updatedAt: iso(row["updated_at"]),
15
+ ...(revision === undefined ? {} : { revision: String(revision) }),
16
+ };
17
+ }
18
+ function sameRecordValue(current, expected) {
19
+ return canonicalJson(current.data) === canonicalJson(expected.data)
20
+ && canonicalJson(current.refs ?? null) === canonicalJson(expected.refs ?? null);
21
+ }
22
+ export function requireRevision(value) {
23
+ if (!/^[1-9]\d*$/.test(value))
24
+ throw new VendoError("validation", "malformed record revision");
25
+ }
26
+ /** 01-core §12 */
27
+ export function createRecordStore(db, collection, dedicatedTable) {
28
+ const table = dedicatedTable ?? "vendo_records";
29
+ const usesCollection = dedicatedTable === undefined;
30
+ const appId = appScopeId(collection);
31
+ // STORE-1: app-scoped collections fail WRITES closed when the owning app has
32
+ // no vendo_apps row — the app never existed, or its ephemeral session was
33
+ // swept (kill-list B3: swept sessions are erased from disk, apps included).
34
+ // Without this, a stale write would recreate rows no erase cascade could ever
35
+ // reach again. Row-CREATING statements carry the gate IN the statement
36
+ // (`appGate` below), so a sweep racing the write can never orphan a row —
37
+ // structural, not ordering care. Reads need no guard: an unknown app has no
38
+ // rows, so they come back empty (a stale client sees an expired session, not
39
+ // an error storm). Dedicated door tables are never app-scoped (appId is
40
+ // undefined there), so the gate is generic-path only.
41
+ const appGate = (param) => appId === undefined ? "" : `WHERE EXISTS (SELECT 1 FROM vendo_apps WHERE id = $${param})`;
42
+ const appGateParams = appId === undefined ? [] : [appId];
43
+ const getRecord = async (id) => {
44
+ const result = usesCollection
45
+ ? await db.query("SELECT id, data, refs, created_at, updated_at, revision FROM vendo_records WHERE collection = $1 AND id = $2", [collection, id])
46
+ : await db.query(`SELECT id, data, refs, created_at, updated_at FROM ${table} WHERE id = $1`, [id]);
47
+ return result.rows[0] ? recordFromRow(result.rows[0]) : null;
48
+ };
49
+ const compareAndMutate = async (expected, replacement) => {
50
+ const expectedId = expected.kind === "revision" ? expected.id : expected.record.id;
51
+ if (replacement !== undefined && replacement.id !== expectedId) {
52
+ throw new Error("Record comparison and replacement ids must match");
53
+ }
54
+ if (expected.kind === "revision")
55
+ requireRevision(expected.revision);
56
+ // UPDATE/DELETE never create rows, so the pre-check is only the error signal.
57
+ await requireKnownApp(db, appId);
58
+ const clauses = [];
59
+ const params = [];
60
+ if (usesCollection) {
61
+ if (expected.kind !== "revision")
62
+ throw new Error("Generic record comparisons require a revision");
63
+ params.push(collection, expected.id, expected.revision);
64
+ clauses.push("collection = $1", "id = $2", "revision = $3::bigint");
65
+ if (expected.value !== undefined) {
66
+ params.push(jsonParam(expected.value.data));
67
+ clauses.push(`data = $${params.length}::jsonb`);
68
+ params.push(expected.value.refs === undefined ? null : jsonParam(expected.value.refs));
69
+ clauses.push(`refs IS NOT DISTINCT FROM $${params.length}::jsonb`);
70
+ }
71
+ }
72
+ else {
73
+ if (expected.kind !== "value")
74
+ throw new Error("Dedicated record comparisons require an expected value");
75
+ params.push(expected.record.id, jsonParam(expected.record.data), expected.record.refs === undefined ? null : jsonParam(expected.record.refs));
76
+ clauses.push("id = $1", "data = $2::jsonb", "refs IS NOT DISTINCT FROM $3::jsonb");
77
+ }
78
+ const returning = `id, data, refs, created_at, updated_at${usesCollection ? ", revision" : ""}`;
79
+ if (replacement === undefined) {
80
+ const result = await db.query(`DELETE FROM ${table} WHERE ${clauses.join(" AND ")} RETURNING ${returning}`, params);
81
+ return result.rows[0] ? recordFromRow(result.rows[0]) : null;
82
+ }
83
+ params.push(jsonParam(replacement.data));
84
+ const dataParam = params.length;
85
+ params.push(replacement.refs === undefined ? null : jsonParam(replacement.refs));
86
+ const refsParam = params.length;
87
+ params.push(new Date().toISOString());
88
+ const updatedAtParam = params.length;
89
+ const result = await db.query(`UPDATE ${table}
90
+ SET data = $${dataParam}::jsonb, refs = $${refsParam}::jsonb, updated_at = $${updatedAtParam}${usesCollection ? ", revision = revision + 1" : ""}
91
+ WHERE ${clauses.join(" AND ")}
92
+ RETURNING ${returning}`, params);
93
+ return result.rows[0] ? recordFromRow(result.rows[0]) : null;
94
+ };
95
+ const atomic = {
96
+ async insertIfAbsent(record) {
97
+ const now = new Date().toISOString();
98
+ // Pre-check for the clean error signal; the statement's own gate is the
99
+ // structural guarantee (a gate lost to a racing sweep returns null —
100
+ // "not inserted" — never an orphan).
101
+ await requireKnownApp(db, appId);
102
+ const result = await db.query(`INSERT INTO vendo_records (collection, id, data, refs, created_at, updated_at, revision)
103
+ SELECT $1::text, $2::text, $3::jsonb, $4::jsonb, $5::timestamptz, $5::timestamptz, 1
104
+ ${appGate(6)}
105
+ ON CONFLICT (collection, id) DO NOTHING
106
+ RETURNING id, data, refs, created_at, updated_at, revision`, [collection, record.id, jsonParam(record.data), record.refs === undefined ? null : jsonParam(record.refs), now, ...appGateParams]);
107
+ return result.rows[0] ? recordFromRow(result.rows[0]) : null;
108
+ },
109
+ async compareAndSwap(record, expectedRevision) {
110
+ return await compareAndMutate({ kind: "revision", id: record.id, revision: expectedRevision }, record);
111
+ },
112
+ };
113
+ return {
114
+ get: getRecord,
115
+ async put(record) {
116
+ const now = new Date().toISOString();
117
+ const result = usesCollection
118
+ ? await db.query(`INSERT INTO vendo_records (collection, id, data, refs, created_at, updated_at, revision)
119
+ SELECT $1::text, $2::text, $3::jsonb, $4::jsonb, $5::timestamptz, $5::timestamptz, 1
120
+ ${appGate(6)}
121
+ ON CONFLICT (collection, id) DO UPDATE
122
+ SET data = EXCLUDED.data, refs = EXCLUDED.refs, updated_at = EXCLUDED.updated_at,
123
+ revision = vendo_records.revision + 1
124
+ RETURNING id, data, refs, created_at, updated_at, revision`, [collection, record.id, jsonParam(record.data), record.refs === undefined ? null : jsonParam(record.refs), now, ...appGateParams])
125
+ : await db.query(`INSERT INTO ${table} (id, data, refs, created_at, updated_at)
126
+ VALUES ($1, $2::jsonb, $3::jsonb, $4, $4)
127
+ ON CONFLICT (id) DO UPDATE
128
+ SET data = EXCLUDED.data, refs = EXCLUDED.refs, updated_at = EXCLUDED.updated_at
129
+ RETURNING id, data, refs, created_at, updated_at`, [record.id, jsonParam(record.data), record.refs === undefined ? null : jsonParam(record.refs), now]);
130
+ const row = result.rows[0];
131
+ if (row === undefined) {
132
+ // The upsert always returns a row unless the app-existence gate
133
+ // refused it (only present when appId is defined).
134
+ if (appId !== undefined)
135
+ throw unknownAppError(appId);
136
+ throw new Error("record upsert returned no row");
137
+ }
138
+ return recordFromRow(row);
139
+ },
140
+ async claim(expected, replacement) {
141
+ const current = await getRecord(expected.id);
142
+ if (current === null || !sameRecordValue(current, expected))
143
+ return false;
144
+ let expectation;
145
+ if (usesCollection) {
146
+ if (current.revision === undefined)
147
+ throw new Error("Generic record is missing its revision");
148
+ expectation = {
149
+ kind: "revision",
150
+ id: expected.id,
151
+ revision: current.revision,
152
+ value: expected,
153
+ };
154
+ }
155
+ else {
156
+ expectation = { kind: "value", record: expected };
157
+ }
158
+ const next = replacement === undefined
159
+ ? undefined
160
+ : {
161
+ id: expected.id,
162
+ data: replacement.data,
163
+ ...(replacement.refs === undefined ? {} : { refs: replacement.refs }),
164
+ };
165
+ return await compareAndMutate(expectation, next) !== null;
166
+ },
167
+ async delete(id) {
168
+ // DELETE never creates rows, so the pre-check is only the error signal.
169
+ await requireKnownApp(db, appId);
170
+ if (usesCollection) {
171
+ await db.query("DELETE FROM vendo_records WHERE collection = $1 AND id = $2", [collection, id]);
172
+ }
173
+ else {
174
+ await db.query(`DELETE FROM ${table} WHERE id = $1`, [id]);
175
+ }
176
+ },
177
+ async list(query = {}) {
178
+ const limit = pageLimit(query.limit);
179
+ const clauses = usesCollection ? ["collection = $1"] : [];
180
+ const params = usesCollection ? [collection] : [];
181
+ if (query.refs !== undefined) {
182
+ params.push(jsonParam(query.refs));
183
+ clauses.push(`refs @> $${params.length}::jsonb`);
184
+ }
185
+ if (query.ids !== undefined) {
186
+ params.push(query.ids);
187
+ clauses.push(`id = ANY($${params.length}::text[])`);
188
+ }
189
+ if (query.cursor !== undefined) {
190
+ const cursor = decodeCursor(query.cursor);
191
+ params.push(cursor.c, cursor.i);
192
+ clauses.push(`(${cursorMs("created_at")}, id) < (${cursorMs(`$${params.length - 1}::timestamptz`)}, $${params.length})`);
193
+ }
194
+ params.push(limit + 1);
195
+ const result = await db.query(`SELECT id, data, refs, created_at, updated_at${usesCollection ? ", revision" : ""} FROM ${table}
196
+ ${clauses.length ? `WHERE ${clauses.join(" AND ")}` : ""}
197
+ ORDER BY ${cursorMs("created_at")} DESC, id DESC LIMIT $${params.length}`, params);
198
+ const rows = result.rows.slice(0, limit).map(recordFromRow);
199
+ const last = rows.at(-1);
200
+ return {
201
+ records: rows,
202
+ ...(result.rows.length > limit && last ? { cursor: encodeCursor(last.createdAt, last.id) } : {}),
203
+ };
204
+ },
205
+ ...(usesCollection ? { atomic } : {}),
206
+ };
207
+ }
208
+ //# sourceMappingURL=records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"records.js","sourceRoot":"","sources":["../src/records.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { type RecordStore } from "@vendoai/core";
2
+ import type { Db } from "./db.js";
3
+ export declare const RESERVED_COLLECTIONS: readonly ["vendo_grants", "vendo_approvals", "vendo_audit", "vendo_threads", "vendo_runs", "vendo_apps", "vendo_state"];
4
+ export declare const DEDICATED_RECORD_COLLECTIONS: readonly ["vendo_mcp_clients", "vendo_mcp_grants"];
5
+ export type ReservedCollection = typeof RESERVED_COLLECTIONS[number];
6
+ export declare function createReservedRecordStore(db: Db, collection: string): RecordStore | undefined;
7
+ //# sourceMappingURL=routing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../src/routing.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAmClC,eAAO,MAAM,oBAAoB,yHAQvB,CAAC;AAEX,eAAO,MAAM,4BAA4B,oDAAqD,CAAC;AAE/F,MAAM,MAAM,kBAAkB,GAAG,OAAO,oBAAoB,CAAC,MAAM,CAAC,CAAC;AAsarE,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,EAAE,EACN,UAAU,EAAE,MAAM,GACjB,WAAW,GAAG,SAAS,CAMzB"}
@@ -0,0 +1,393 @@
1
+ import { VendoError, } from "@vendoai/core";
2
+ import { createRecordStore, requireRevision } from "./records.js";
3
+ import { appFromRow, approvalFromRow, grantFromRow, putAppRow, putApprovalRow, putAuditRow, putGrantRow, putRunRow, putStateRow, putThreadRow, runFromRow, stateRowFromRow, threadFromRow, } from "./helpers/rows.js";
4
+ import { cursorMs, decodeCursor, encodeCursor, pageLimit } from "./helpers/utils.js";
5
+ import { invalid, parseAppData, parseApprovalData, parseAuditEvent, parsePermissionGrant, parseRunData, parseThreadData, requireJson, requireMatchingId, requireRecordId, } from "./validate.js";
6
+ export const RESERVED_COLLECTIONS = [
7
+ "vendo_grants",
8
+ "vendo_approvals",
9
+ "vendo_audit",
10
+ "vendo_threads",
11
+ "vendo_runs",
12
+ "vendo_apps",
13
+ "vendo_state",
14
+ ];
15
+ export const DEDICATED_RECORD_COLLECTIONS = ["vendo_mcp_clients", "vendo_mcp_grants"];
16
+ function refs(values) {
17
+ return Object.fromEntries(Object.entries(values).filter((entry) => entry[1] !== undefined));
18
+ }
19
+ function grantRecord(grant) {
20
+ return {
21
+ id: grant.id,
22
+ data: grant,
23
+ refs: refs({ subject: grant.subject, tool: grant.tool, app_id: grant.appId }),
24
+ createdAt: grant.grantedAt,
25
+ updatedAt: grant.revokedAt ?? grant.grantedAt,
26
+ };
27
+ }
28
+ function approvalRecord(row) {
29
+ const data = {
30
+ request: row.request,
31
+ status: row.status,
32
+ ...(row.decidedAt === undefined ? {} : { decidedAt: row.decidedAt }),
33
+ ...(row.sessionId === undefined ? {} : { sessionId: row.sessionId }),
34
+ ...(row.consumedAt === undefined ? {} : { consumedAt: row.consumedAt }),
35
+ };
36
+ return {
37
+ id: row.id,
38
+ data,
39
+ refs: { subject: row.subject, status: row.status },
40
+ createdAt: row.request.createdAt,
41
+ updatedAt: row.consumedAt ?? row.decidedAt ?? row.request.createdAt,
42
+ };
43
+ }
44
+ function auditRecord(event) {
45
+ return {
46
+ id: event.id,
47
+ data: event,
48
+ refs: refs({
49
+ subject: event.principal.subject,
50
+ kind: event.kind,
51
+ app_id: event.appId,
52
+ tool: event.tool,
53
+ }),
54
+ createdAt: event.at,
55
+ updatedAt: event.at,
56
+ };
57
+ }
58
+ function threadRecord(row) {
59
+ const data = {
60
+ subject: row.subject,
61
+ messages: row.messages,
62
+ ...(row.title === undefined || row.title === null ? {} : { title: row.title }),
63
+ };
64
+ return {
65
+ id: row.id,
66
+ data,
67
+ refs: { subject: row.subject },
68
+ createdAt: row.createdAt,
69
+ updatedAt: row.updatedAt,
70
+ // Present when the row carries the write counter (01 §12: opaque token when
71
+ // atomic is present); the messages-less listSelect projection omits it.
72
+ ...(row.revision === undefined ? {} : { revision: row.revision }),
73
+ };
74
+ }
75
+ function runRecord(row) {
76
+ const { id, ...data } = row;
77
+ return {
78
+ id,
79
+ data,
80
+ refs: { app_id: row.appId, status: row.status },
81
+ createdAt: row.startedAt,
82
+ updatedAt: row.finishedAt ?? row.startedAt,
83
+ };
84
+ }
85
+ function appRecord(row) {
86
+ const data = { subject: row.subject, enabled: row.enabled, doc: row.doc };
87
+ return {
88
+ id: row.id,
89
+ data,
90
+ // trigger_kind mirrors the persisted generated column (schema.ts) so the
91
+ // automations tick / emit query can filter on it.
92
+ refs: refs({ subject: row.subject, trigger_kind: row.doc.trigger?.on.kind }),
93
+ createdAt: row.createdAt,
94
+ updatedAt: row.updatedAt,
95
+ // Present when the row carries the write counter (01 §12: opaque token
96
+ // when atomic is present) — Wave 7's lifecycle/schedule-claim arbitration.
97
+ ...(row.revision === undefined ? {} : { revision: row.revision }),
98
+ };
99
+ }
100
+ function stateRecord(row) {
101
+ return {
102
+ id: `${row.appId}:${row.subject}`,
103
+ data: row.data,
104
+ refs: { app_id: row.appId, subject: row.subject },
105
+ createdAt: row.createdAt,
106
+ updatedAt: row.updatedAt,
107
+ };
108
+ }
109
+ /**
110
+ * apps writes state through `records("vendo_state")` with id `${appId}:${subject}`.
111
+ * App ids are `app_...` and never contain a colon, so the first colon splits id into
112
+ * its app_id and subject (subjects may themselves contain colons).
113
+ *
114
+ * The colon-free app-id shape is REQUIRED, not assumed: without it `<appId>:<subject>`
115
+ * is not uniquely decodable — (app_a:b, c) and (app_a, b:c) would both encode to
116
+ * "app_a:b:c" and collide on read/write/delete. The apps runtime mints colon-free
117
+ * ids; this enforces it at the door so a doctored id can never target another row.
118
+ */
119
+ const APP_ID_SEGMENT = /^app_[^:]+$/;
120
+ function splitStateId(id) {
121
+ const colon = id.indexOf(":");
122
+ if (colon === -1)
123
+ invalid(`vendo_state record id must be "<appId>:<subject>": ${id}`);
124
+ const appId = id.slice(0, colon);
125
+ if (!APP_ID_SEGMENT.test(appId)) {
126
+ invalid(`vendo_state record id must start with a colon-free app id ("app_..."): ${id}`);
127
+ }
128
+ const subject = id.slice(colon + 1);
129
+ // An empty subject ("app_x:") would route a state row to no principal — reject it
130
+ // (the apps runtime always writes a non-empty subject).
131
+ if (subject === "") {
132
+ invalid(`vendo_state record id must have a non-empty subject after the colon: ${id}`);
133
+ }
134
+ return { appId, subject };
135
+ }
136
+ function createTableRecordStore(db, config) {
137
+ return {
138
+ async get(id) {
139
+ requireRecordId(id);
140
+ const result = await db.query(`${config.select} WHERE id = $1`, [id]);
141
+ return result.rows[0] ? config.fromDb(result.rows[0]) : null;
142
+ },
143
+ async put(record) {
144
+ requireRecordId(record.id);
145
+ // Reserved collections derive refs from typed columns; caller refs never participate in writes.
146
+ return await config.put(record);
147
+ },
148
+ async delete(id) {
149
+ requireRecordId(id);
150
+ if (config.appendOnly === true) {
151
+ throw new VendoError("blocked", `${config.table} is append-only; rows are erased only via the store erase API (02-store §5)`);
152
+ }
153
+ config.validateId?.(id);
154
+ await db.query(`DELETE FROM ${config.table} WHERE id = $1`, [id]);
155
+ },
156
+ async list(query = {}) {
157
+ const limit = pageLimit(query.limit);
158
+ if (query.refs !== undefined) {
159
+ for (const key of Object.keys(query.refs)) {
160
+ if (config.refs[key] === undefined)
161
+ invalid(`Unknown ${config.table} ref key: ${key}`);
162
+ }
163
+ }
164
+ const params = [];
165
+ const clauses = [];
166
+ for (const [key, value] of Object.entries(query.refs ?? {})) {
167
+ params.push(value);
168
+ clauses.push(`${config.refs[key]} = $${params.length}`);
169
+ }
170
+ if (query.ids !== undefined) {
171
+ params.push(query.ids);
172
+ clauses.push(`id = ANY($${params.length}::text[])`);
173
+ }
174
+ if (query.cursor !== undefined) {
175
+ const cursor = decodeCursor(query.cursor);
176
+ params.push(cursor.c, cursor.i);
177
+ clauses.push(`(${cursorMs(config.cursorColumn)}, id) < (${cursorMs(`$${params.length - 1}::timestamptz`)}, $${params.length})`);
178
+ }
179
+ params.push(limit + 1);
180
+ const result = await db.query(`${config.listSelect ?? config.select}${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""}
181
+ ORDER BY ${cursorMs(config.cursorColumn)} DESC, id DESC LIMIT $${params.length}`, params);
182
+ const records = result.rows.slice(0, limit).map(config.fromDb);
183
+ const last = records.at(-1);
184
+ return {
185
+ records,
186
+ ...(result.rows.length > limit && last ? { cursor: encodeCursor(last.createdAt, last.id) } : {}),
187
+ };
188
+ },
189
+ ...(config.atomic === undefined ? {} : { atomic: config.atomic }),
190
+ };
191
+ }
192
+ function configFor(db, collection) {
193
+ switch (collection) {
194
+ case "vendo_grants":
195
+ return {
196
+ table: collection,
197
+ select: "SELECT * FROM vendo_grants",
198
+ cursorColumn: "granted_at",
199
+ refs: { subject: "subject", tool: "tool", app_id: "app_id" },
200
+ fromDb: (row) => grantRecord(grantFromRow(row)),
201
+ async put(record) {
202
+ const grant = parsePermissionGrant(record.data);
203
+ requireMatchingId(record.id, grant.id, "permission grant id");
204
+ // The guarded upsert refuses cross-subject flips (02 §2).
205
+ await putGrantRow(db, grant);
206
+ return grantRecord(grant);
207
+ },
208
+ };
209
+ case "vendo_approvals":
210
+ return {
211
+ table: collection,
212
+ select: "SELECT * FROM vendo_approvals",
213
+ cursorColumn: "created_at",
214
+ refs: { subject: "subject", status: "status" },
215
+ fromDb: (row) => approvalRecord(approvalFromRow(row)),
216
+ async put(record) {
217
+ const data = parseApprovalData(record.data, record.id);
218
+ const row = {
219
+ id: data.request.id,
220
+ subject: data.request.ctx.principal.subject,
221
+ request: data.request,
222
+ status: data.status,
223
+ ...(data.decidedAt === undefined ? {} : { decidedAt: data.decidedAt }),
224
+ ...(data.sessionId === undefined ? {} : { sessionId: data.sessionId }),
225
+ ...(data.consumedAt === undefined ? {} : { consumedAt: data.consumedAt }),
226
+ createdAt: data.request.createdAt,
227
+ };
228
+ await putApprovalRow(db, row);
229
+ return approvalRecord(row);
230
+ },
231
+ };
232
+ case "vendo_audit":
233
+ return {
234
+ table: collection,
235
+ select: "SELECT * FROM vendo_audit",
236
+ appendOnly: true,
237
+ cursorColumn: "at",
238
+ refs: { subject: "subject", kind: "kind", app_id: "app_id", tool: "tool" },
239
+ fromDb: (row) => auditRecord(row["event"]),
240
+ async put(record) {
241
+ const event = parseAuditEvent(record.data);
242
+ requireMatchingId(record.id, event.id, "audit event id");
243
+ // putAuditRow refuses to replace an existing row (append-only, 02 §2).
244
+ await putAuditRow(db, event);
245
+ return auditRecord(event);
246
+ },
247
+ };
248
+ case "vendo_threads":
249
+ return {
250
+ table: collection,
251
+ select: "SELECT * FROM vendo_threads",
252
+ // Listing derives only a title + timestamps; skip the (potentially large) messages
253
+ // column once a row carries a stored title. Legacy rows (title still NULL, written
254
+ // before the column existed) keep returning messages so the title stays derivable.
255
+ listSelect: `SELECT id, subject, title,
256
+ CASE WHEN title IS NULL THEN messages ELSE '[]'::jsonb END AS messages,
257
+ created_at, updated_at FROM vendo_threads`,
258
+ cursorColumn: "created_at",
259
+ refs: { subject: "subject" },
260
+ fromDb: (row) => threadRecord(threadFromRow(row)),
261
+ async put(record) {
262
+ const data = parseThreadData(record.data, record.id);
263
+ // The guarded upsert refuses cross-subject flips (03 §5).
264
+ const row = await putThreadRow(db, { id: record.id, ...data });
265
+ return threadRecord(row);
266
+ },
267
+ // ENG-310: guarded writes (01 §12) backed by the vendo_threads revision
268
+ // counter, so a concurrent-turn persist can read-merge-write without
269
+ // last-write-wins clobbering. Both verbs keep the same cross-subject
270
+ // refusal as put: a foreign-subject write NEVER lands (it just loses —
271
+ // insertIfAbsent finds the id taken, compareAndSwap's guarded WHERE
272
+ // fails — and returns null).
273
+ atomic: {
274
+ async insertIfAbsent(record) {
275
+ requireRecordId(record.id);
276
+ const data = parseThreadData(record.data, record.id);
277
+ const now = new Date().toISOString();
278
+ const result = await db.query(`INSERT INTO vendo_threads (id, subject, messages, title, created_at, updated_at, revision)
279
+ VALUES ($1, $2, $3::jsonb, $4, $5, $5, 1)
280
+ ON CONFLICT (id) DO NOTHING
281
+ RETURNING id, subject, messages, title, created_at, updated_at, revision`, [record.id, data.subject, JSON.stringify(data.messages), data.title ?? null, now]);
282
+ return result.rows[0]
283
+ ? threadRecord(threadFromRow(result.rows[0]))
284
+ : null;
285
+ },
286
+ async compareAndSwap(record, expectedRevision) {
287
+ requireRecordId(record.id);
288
+ requireRevision(expectedRevision);
289
+ const data = parseThreadData(record.data, record.id);
290
+ const now = new Date().toISOString();
291
+ const result = await db.query(`UPDATE vendo_threads
292
+ SET messages = $3::jsonb, title = $4, updated_at = $5, revision = revision + 1
293
+ WHERE id = $1 AND subject = $2 AND revision = $6::bigint
294
+ RETURNING id, subject, messages, title, created_at, updated_at, revision`, [record.id, data.subject, JSON.stringify(data.messages), data.title ?? null, now, expectedRevision]);
295
+ return result.rows[0]
296
+ ? threadRecord(threadFromRow(result.rows[0]))
297
+ : null;
298
+ },
299
+ },
300
+ };
301
+ case "vendo_runs":
302
+ return {
303
+ table: collection,
304
+ select: "SELECT * FROM vendo_runs",
305
+ cursorColumn: "started_at",
306
+ refs: { app_id: "app_id", status: "status" },
307
+ fromDb: (row) => runRecord(runFromRow(row)),
308
+ async put(record) {
309
+ const data = parseRunData(record.data, record.id);
310
+ const row = { id: record.id, ...data };
311
+ await putRunRow(db, row);
312
+ return runRecord(row);
313
+ },
314
+ };
315
+ case "vendo_apps":
316
+ return {
317
+ table: collection,
318
+ select: "SELECT * FROM vendo_apps",
319
+ cursorColumn: "created_at",
320
+ refs: { subject: "subject", trigger_kind: "trigger_kind" },
321
+ fromDb: (row) => appRecord(appFromRow(row)),
322
+ async put(record) {
323
+ const data = parseAppData(record.data, record.id);
324
+ // The guarded upsert refuses cross-subject flips (02 §2).
325
+ const row = await putAppRow(db, { id: record.id, ...data });
326
+ return appRecord(row);
327
+ },
328
+ // Wave 7: guarded writes (01 §12) backed by the vendo_apps revision
329
+ // counter, so the machine lifecycle and the schedule engine's fire
330
+ // claims (updateAppRow's read-mutate-CAS) arbitrate racers on the dev
331
+ // store instead of degrading to read-then-put. Both verbs keep the
332
+ // same cross-subject refusal as put: a foreign-subject write NEVER
333
+ // lands (insertIfAbsent finds the id taken, compareAndSwap's guarded
334
+ // WHERE fails — and returns null). The doc's trigger_kind projection
335
+ // is a generated column, so guarded writes keep it for free.
336
+ atomic: {
337
+ async insertIfAbsent(record) {
338
+ requireRecordId(record.id);
339
+ const data = parseAppData(record.data, record.id);
340
+ const now = new Date().toISOString();
341
+ const result = await db.query(`INSERT INTO vendo_apps (id, subject, enabled, doc, created_at, updated_at, revision)
342
+ VALUES ($1, $2, $3, $4::jsonb, $5, $5, 1)
343
+ ON CONFLICT (id) DO NOTHING
344
+ RETURNING id, subject, enabled, doc, created_at, updated_at, revision`, [record.id, data.subject, data.enabled, JSON.stringify(data.doc), now]);
345
+ return result.rows[0]
346
+ ? appRecord(appFromRow(result.rows[0]))
347
+ : null;
348
+ },
349
+ async compareAndSwap(record, expectedRevision) {
350
+ requireRecordId(record.id);
351
+ requireRevision(expectedRevision);
352
+ const data = parseAppData(record.data, record.id);
353
+ const now = new Date().toISOString();
354
+ const result = await db.query(`UPDATE vendo_apps
355
+ SET enabled = $3, doc = $4::jsonb, updated_at = $5, revision = revision + 1
356
+ WHERE id = $1 AND subject = $2 AND revision = $6::bigint
357
+ RETURNING id, subject, enabled, doc, created_at, updated_at, revision`, [record.id, data.subject, data.enabled, JSON.stringify(data.doc), now, expectedRevision]);
358
+ return result.rows[0]
359
+ ? appRecord(appFromRow(result.rows[0]))
360
+ : null;
361
+ },
362
+ },
363
+ };
364
+ case "vendo_state":
365
+ return {
366
+ table: collection,
367
+ // `id` is the generated (app_id || ':' || subject) column — a real,
368
+ // indexed column, so point lookups and id filters no longer seq-scan.
369
+ select: "SELECT id, app_id, subject, data, created_at, updated_at FROM vendo_state",
370
+ // Page on the STABLE created_at (like every other collection), not the
371
+ // mutable updated_at — a mid-sweep update must never skip an unvisited row.
372
+ cursorColumn: "created_at",
373
+ refs: { app_id: "app_id", subject: "subject" },
374
+ fromDb: (row) => stateRecord(stateRowFromRow(row)),
375
+ validateId: (id) => void splitStateId(id),
376
+ async put(record) {
377
+ const { appId, subject } = splitStateId(record.id);
378
+ const data = requireJson(record.data, "state data");
379
+ // Shared persistent write path with stateStore.put (helpers/rows).
380
+ return stateRecord(await putStateRow(db, { appId, subject, data }));
381
+ },
382
+ };
383
+ }
384
+ }
385
+ export function createReservedRecordStore(db, collection) {
386
+ if (DEDICATED_RECORD_COLLECTIONS.includes(collection)) {
387
+ return createRecordStore(db, collection, collection);
388
+ }
389
+ if (!RESERVED_COLLECTIONS.includes(collection))
390
+ return undefined;
391
+ return createTableRecordStore(db, configFor(db, collection));
392
+ }
393
+ //# sourceMappingURL=routing.js.map