@vendoai/store 0.1.0 → 0.4.1
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/README.md +81 -0
- package/dist/blobs.d.ts +5 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +62 -0
- package/dist/blobs.js.map +1 -0
- package/dist/crypto.d.ts +10 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +44 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.d.ts +26 -26
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +335 -86
- package/dist/db.js.map +1 -0
- package/dist/erase.d.ts +33 -0
- package/dist/erase.d.ts.map +1 -0
- package/dist/erase.js +107 -0
- package/dist/erase.js.map +1 -0
- package/dist/helpers/approvals.d.ts +12 -0
- package/dist/helpers/approvals.d.ts.map +1 -0
- package/dist/helpers/approvals.js +37 -0
- package/dist/helpers/approvals.js.map +1 -0
- package/dist/helpers/apps.d.ts +15 -0
- package/dist/helpers/apps.d.ts.map +1 -0
- package/dist/helpers/apps.js +42 -0
- package/dist/helpers/apps.js.map +1 -0
- package/dist/helpers/audit.d.ts +24 -0
- package/dist/helpers/audit.d.ts.map +1 -0
- package/dist/helpers/audit.js +79 -0
- package/dist/helpers/audit.js.map +1 -0
- package/dist/helpers/grants.d.ts +14 -0
- package/dist/helpers/grants.d.ts.map +1 -0
- package/dist/helpers/grants.js +46 -0
- package/dist/helpers/grants.js.map +1 -0
- package/dist/helpers/rows.d.ts +30 -0
- package/dist/helpers/rows.d.ts.map +1 -0
- package/dist/helpers/rows.js +191 -0
- package/dist/helpers/rows.js.map +1 -0
- package/dist/helpers/runs.d.ts +19 -0
- package/dist/helpers/runs.d.ts.map +1 -0
- package/dist/helpers/runs.js +47 -0
- package/dist/helpers/runs.js.map +1 -0
- package/dist/helpers/state.d.ts +9 -0
- package/dist/helpers/state.d.ts.map +1 -0
- package/dist/helpers/state.js +22 -0
- package/dist/helpers/state.js.map +1 -0
- package/dist/helpers/subjects.d.ts +32 -0
- package/dist/helpers/subjects.d.ts.map +1 -0
- package/dist/helpers/subjects.js +78 -0
- package/dist/helpers/subjects.js.map +1 -0
- package/dist/helpers/threads.d.ts +19 -0
- package/dist/helpers/threads.d.ts.map +1 -0
- package/dist/helpers/threads.js +38 -0
- package/dist/helpers/threads.js.map +1 -0
- package/dist/helpers/types.d.ts +60 -0
- package/dist/helpers/types.d.ts.map +1 -0
- package/dist/helpers/types.js +2 -0
- package/dist/helpers/types.js.map +1 -0
- package/dist/helpers/utils.d.ts +37 -0
- package/dist/helpers/utils.d.ts.map +1 -0
- package/dist/helpers/utils.js +94 -0
- package/dist/helpers/utils.js.map +1 -0
- package/dist/index.d.ts +13 -11
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -15
- package/dist/index.js.map +1 -0
- package/dist/records.d.ts +7 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +208 -0
- package/dist/records.js.map +1 -0
- package/dist/routing.d.ts +7 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +393 -0
- package/dist/routing.js.map +1 -0
- package/dist/schema.d.ts +18 -1420
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +206 -114
- package/dist/schema.js.map +1 -0
- package/dist/secrets.d.ts +13 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +86 -0
- package/dist/secrets.js.map +1 -0
- package/dist/sessions.d.ts +53 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +85 -0
- package/dist/sessions.js.map +1 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +49 -0
- package/dist/store.js.map +1 -0
- package/dist/validate.d.ts +25 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +144 -0
- package/dist/validate.js.map +1 -0
- package/package.json +15 -14
- package/dist/automation-store.d.ts +0 -62
- package/dist/automation-store.js +0 -675
- package/dist/connections-store.d.ts +0 -25
- package/dist/connections-store.js +0 -102
- package/dist/decision-store.d.ts +0 -8
- package/dist/decision-store.js +0 -39
- package/dist/meta.d.ts +0 -5
- package/dist/meta.js +0 -20
- package/dist/thread-store.d.ts +0 -5
- package/dist/thread-store.js +0 -209
- package/dist/vendo-registry.d.ts +0 -5
- package/dist/vendo-registry.js +0 -52
- package/migrations/.gitkeep +0 -0
- package/migrations/0000_living_kate_bishop.sql +0 -110
- package/migrations/0001_brainy_tinkerer.sql +0 -13
- package/migrations/meta/0000_snapshot.json +0 -757
- package/migrations/meta/0001_snapshot.json +0 -859
- package/migrations/meta/_journal.json +0 -20
package/dist/records.js
ADDED
|
@@ -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"}
|
package/dist/routing.js
ADDED
|
@@ -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
|