@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.
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
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # @vendoai/store
2
+
3
+ `@vendoai/store` implements the `@vendoai/core` persistence seams with one Postgres schema. It uses PGlite for a zero-config local database and the same schema on a hosted Postgres service.
4
+
5
+ Read [Persistence](https://docs.vendo.run/deploy/persistence).
6
+
7
+ ```ts
8
+ import { createStore } from "@vendoai/store";
9
+
10
+ const store = createStore({ dataDir: ".vendo/data" });
11
+ await store.ensureSchema();
12
+ ```
13
+
14
+ For production, pass a Postgres connection string explicitly, for example `createStore({ url: process.env.POSTGRES_URL })`. Without `url`, PGlite stores durable data in `dataDir` (default `.vendo/data`); `memory://` is also supported. PGlite is rejected on known serverless filesystems.
15
+
16
+ ## Tables
17
+
18
+ | Table | Key columns (stable) | Holds |
19
+ | --- | --- | --- |
20
+ | `vendo_meta` | `key, value` | schema version, boot id |
21
+ | `vendo_apps` | `id, subject, enabled, doc, created_at, updated_at` | each user's app document and ownership |
22
+ | `vendo_records` | `collection, id, data, refs, created_at, updated_at, revision` | app data collections; `refs` is GIN-indexed for host joins; `revision` backs atomic writes |
23
+ | `vendo_blobs` | `namespace, key, bytes, content_type, created_at` | file storage, exports, screenshots |
24
+ | `vendo_state` | `app_id, subject, data, updated_at` | built-in per-user-per-app state singleton |
25
+ | `vendo_threads` | `id, subject, messages, created_at, updated_at` | conversation threads |
26
+ | `vendo_grants` | `id, subject, tool, descriptor_hash, scope, duration, app_id, source, granted_at, revoked_at, expires_at` | permission grants |
27
+ | `vendo_approvals` | `id, subject, request, status, decided_at, session_id, consumed_at, created_at` | approval queue |
28
+ | `vendo_audit` | `id, at, kind, subject, venue, presence, app_id, tool, event` | append-only audit log |
29
+ | `vendo_runs` | `id, app_id, trigger, status, record, started_at, finished_at` | automation run records |
30
+ | `vendo_secrets` | `name, ciphertext, created_at` | optional encrypted secret values |
31
+ | `vendo_mcp_clients` | `id, data, refs, created_at, updated_at` | door-owned MCP client state |
32
+ | `vendo_mcp_grants` | `id, data, refs, created_at, updated_at` | door-owned MCP grant state |
33
+ | `vendo_sessions` | `subject, touched_at` | ephemeral (anonymous) session registry: last-activity touch per session, read by the TTL sweep |
34
+
35
+ App storage uses `app:<appId>:<name>` by convention. App-scoped record and blob WRITES require an existing `vendo_apps` row and fail closed with `not-found` ("session may have expired") when there is none — the app never existed, or its ephemeral session was swept; reads on a missing app return empty. Except for the reserved names below, collection names remain opaque and use `vendo_records`; non-`app:`-prefixed collections and namespaces have no principal linkage.
36
+
37
+ Generic record collections and the two door-owned tables expose the optional
38
+ `RecordStore.claim` capability: one database statement compares the current
39
+ `data` and `refs`, then replaces or deletes the row. Exactly one concurrent
40
+ claimant receives `true`.
41
+
42
+ Ordinary record collections expose optional `records(collection).atomic` operations: `insertIfAbsent(record)` for one-winner claims and `compareAndSwap(record, expectedRevision)` for revision-guarded updates. Both PGlite and hosted Postgres use the same atomic SQL. The capability is optional at the core seam and reserved typed-table routes may omit it.
43
+
44
+ ## Reserved collections (block seam)
45
+
46
+ Blocks receive core's plain `StoreAdapter`, so these exact `records()` collection names route to their typed tables:
47
+
48
+ | Collection | Primary key | Data | Synthesized refs | Record timestamps |
49
+ | --- | --- | --- | --- | --- |
50
+ | `vendo_grants` | grant id | `PermissionGrant` | `subject`, `tool`, optional `app_id` | `grantedAt` / `revokedAt ?? grantedAt` |
51
+ | `vendo_approvals` | approval id | `{ request, status, decidedAt?, sessionId?, consumedAt? }` | `subject`, `status` | `request.createdAt` / `consumedAt ?? decidedAt ?? request.createdAt` |
52
+ | `vendo_audit` | audit event id | `AuditEvent` | `subject`, `kind`, optional `app_id`, optional `tool` | `at` / `at` |
53
+ | `vendo_threads` | thread id | `{ subject, messages }` | `subject` | table `created_at` / `updated_at` |
54
+ | `vendo_runs` | run id | `{ appId, trigger, status, record, startedAt, finishedAt? }` | `app_id`, `status` | `startedAt` / `finishedAt ?? startedAt` |
55
+ | `vendo_apps` | app id | `{ subject, enabled, doc }` | `subject` | table `created_at` / `updated_at` |
56
+ | `vendo_mcp_clients` | client id | block-internal JSON | caller-supplied, arbitrary keys | table `created_at` / `updated_at` |
57
+ | `vendo_mcp_grants` | grant id | block-internal JSON | caller-supplied, arbitrary keys | table `created_at` / `updated_at` |
58
+
59
+ Typed reserved writes validate their data, require embedded ids to match the record id, and upsert the typed row — with two enforced exceptions. `vendo_audit` is append-only: `put` on an existing id and `delete` are both refused; audit rows are erased only through the erase API below. `vendo_apps`, `vendo_grants`, and `vendo_threads` refuse cross-subject flips atomically: a put whose id already belongs to another subject fails with a conflict. The data is authoritative: caller-supplied `refs` are ignored on write and synthesized from typed columns on read. Their routed `list({ refs })` accepts only the refs shown above. The two door-owned collections use generic record semantics in dedicated tables: the store does not validate their block-internal payloads, and refs filters accept arbitrary keys. Generic and routed record lists are uniformly newest-first by `(createdAt, id)`.
60
+
61
+ Ephemeral principals take the SAME path as everyone else: their rows are ordinary disk rows under their subject. What makes a session ephemeral is its registration (`registerEphemeralSubject`); see the lifecycle section below.
62
+
63
+ ## Ephemeral session lifecycle
64
+
65
+ Ephemeral (anonymous) principals write ordinary disk rows under their subject; the session itself is one row in `vendo_sessions`. `registerEphemeralSubject(store, subject, now?)` (async) upserts the touch row — registration == touch — and the umbrella calls it on every ephemeral-principal request, so idle time is measured from the last request, reads included. Data written for a subject that is never registered behaves like durable data (nothing sweeps it); the erase API below remains the cleanup path for such compositions.
66
+
67
+ `sweepEphemeralSubjects(store, { idleMs, now? })` (async) erases every registered session idle for at least `idleMs` through the erase cascade — the subject's apps, records, blobs, state, threads, grants, approvals, audit, runs, and the session row itself — and returns the swept subjects so the caller can cascade further (the umbrella forwards them to `agent.evictSubject`). TTL policy is the caller's — the store stays config-free; `createVendo({ sessions })` owns the knobs and the sweep cadence.
68
+
69
+ Writes after a sweep fail closed. App-scoped (`app:<appId>:<name>`) record and blob writes require an existing `vendo_apps` row; a write against a missing app — one that never existed or whose session was swept — throws `not-found` ("session may have expired") instead of recreating rows no cascade would reach again. Reads on missing apps return empty.
70
+
71
+ Because the registry is a table in the same database, sessions survive restarts and are shared across instances — no sticky-session constraint. Anonymous audit events live in `vendo_audit` like any others and appear in `auditStore.export()` until their session is swept.
72
+
73
+ `adoptEphemeralSubject(store, from, to)` is the anonymous-to-signed-in merge: threads, apps (with their app-scoped records/blobs), and state move to the signed-in subject; grants, approvals, audit, and the adopted apps' run history are deleted — consent and history do not transfer. Idempotent; nothing is ever stolen from an existing durable row.
74
+
75
+ ## Encryption
76
+
77
+ `createStore({ encryption: { key } })` (base64 32-byte key) encrypts `vendo_secrets.ciphertext` with AES-256-GCM; everything else stays host-queryable plaintext by design. Encryption is production-owned: set `VENDO_STORE_ENCRYPTION_KEY` in the deploy environment and `createVendo` reads it when no store is passed; without a key, dev mode stores secrets unencrypted (`allowUnencryptedSecrets`, `plain@1:` envelope) and production secret writes fail closed. Ciphertext is bound to its secret name via AAD (`v2` envelope).
78
+
79
+ ## Retention and erasure
80
+
81
+ `eraseStore(store)` is the store-level erase API — `bySubject(subject)` for full erasure and `byApp(appId)` — cascading the matching rows across all 14 tables (ephemeral subjects included — their rows are ordinary disk rows) and returning per-table deleted counts. It is the only sanctioned deletion path for `vendo_audit` rows. It is also re-exported from `@vendoai/vendo/server`. Host SQL remains available for everything else.
@@ -0,0 +1,5 @@
1
+ import type { BlobStore } from "@vendoai/core";
2
+ import type { Db } from "./db.js";
3
+ /** 01-core §12 */
4
+ export declare function createBlobStore(db: Db, namespace: string): BlobStore;
5
+ //# sourceMappingURL=blobs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blobs.d.ts","sourceRoot":"","sources":["../src/blobs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAGlC,kBAAkB;AAClB,wBAAgB,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,CAiEpE"}
package/dist/blobs.js ADDED
@@ -0,0 +1,62 @@
1
+ import { appScopeId, escapeLike, requireKnownApp, text, unknownAppError } from "./helpers/utils.js";
2
+ /** 01-core §12 */
3
+ export function createBlobStore(db, namespace) {
4
+ const appId = appScopeId(namespace);
5
+ // STORE-1: mirrors records.ts — an app-scoped blob WRITE for an app with no
6
+ // vendo_apps row (never existed / session swept) fails closed rather than
7
+ // recreating rows no erase cascade would reach; reads come back empty. The
8
+ // row-creating put carries the gate IN the statement (structural, not
9
+ // ordering care); delete keeps the pre-check as its error signal.
10
+ const appGate = appId === undefined ? "" : "WHERE EXISTS (SELECT 1 FROM vendo_apps WHERE id = $6)";
11
+ const appGateParams = appId === undefined ? [] : [appId];
12
+ return {
13
+ async put(key, bytes, meta) {
14
+ const result = await db.query(`INSERT INTO vendo_blobs (namespace, key, bytes, content_type, created_at)
15
+ SELECT $1::text, $2::text, $3::bytea, $4::text, $5::timestamptz
16
+ ${appGate}
17
+ ON CONFLICT (namespace, key) DO UPDATE
18
+ SET bytes = EXCLUDED.bytes, content_type = EXCLUDED.content_type
19
+ RETURNING key`, [
20
+ namespace,
21
+ key,
22
+ // node-postgres only recognizes Buffer as bytea; a plain Uint8Array
23
+ // would be serialized as JSON. PGlite accepts Buffer (a Uint8Array).
24
+ Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength),
25
+ meta?.contentType ?? null,
26
+ new Date().toISOString(),
27
+ ...appGateParams,
28
+ ]);
29
+ // The upsert always returns a row unless the app-existence gate refused it.
30
+ if (result.rows[0] === undefined && appId !== undefined)
31
+ throw unknownAppError(appId);
32
+ },
33
+ async get(key) {
34
+ const result = await db.query("SELECT bytes, content_type FROM vendo_blobs WHERE namespace = $1 AND key = $2", [namespace, key]);
35
+ const row = result.rows[0];
36
+ if (!row)
37
+ return null;
38
+ const bytes = row["bytes"];
39
+ if (!(bytes instanceof Uint8Array))
40
+ throw new Error("Expected database bytea");
41
+ const contentType = row["content_type"];
42
+ return {
43
+ // Normalize to a plain Uint8Array so both drivers return one shape
44
+ // (pg hands back Buffer, PGlite hands back Uint8Array).
45
+ bytes: new Uint8Array(bytes),
46
+ ...(typeof contentType === "string" ? { contentType } : {}),
47
+ };
48
+ },
49
+ async delete(key) {
50
+ // DELETE never creates rows, so the pre-check is only the error signal.
51
+ await requireKnownApp(db, appId);
52
+ await db.query("DELETE FROM vendo_blobs WHERE namespace = $1 AND key = $2", [namespace, key]);
53
+ },
54
+ async list(prefix = "") {
55
+ const result = await db.query("SELECT key FROM vendo_blobs WHERE namespace = $1 AND key LIKE $2 ESCAPE '\\'", [namespace, `${escapeLike(prefix)}%`]);
56
+ // Sort in JS: SQL ORDER BY is collation-dependent (PGlite ships C,
57
+ // hosted Postgres usually a locale) — one deterministic order everywhere.
58
+ return result.rows.map((row) => text(row["key"])).sort();
59
+ },
60
+ };
61
+ }
62
+ //# sourceMappingURL=blobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blobs.js","sourceRoot":"","sources":["../src/blobs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /** 02-store §4 */
2
+ export declare function validateEncryptionKey(value: string): Buffer;
3
+ /** 02-store §4 — envelope version `v2`: AES-256-GCM with the secret NAME
4
+ * bound as AAD, so a ciphertext swapped between rows (or served for the
5
+ * wrong name) fails the auth tag instead of decrypting to another secret's
6
+ * value. */
7
+ export declare function encryptSecret(value: string, key: Buffer, name: string): string;
8
+ /** 02-store §4 */
9
+ export declare function decryptSecret(value: string, key: Buffer, name: string): string;
10
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAGA,kBAAkB;AAClB,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO3D;AAED;;;aAGa;AACb,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAO9E;AAED,kBAAkB;AAClB,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAmB9E"}
package/dist/crypto.js ADDED
@@ -0,0 +1,44 @@
1
+ import { VendoError } from "@vendoai/core";
2
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
3
+ /** 02-store §4 */
4
+ export function validateEncryptionKey(value) {
5
+ const validCharacters = /^[A-Za-z0-9+/]+={0,2}$/.test(value);
6
+ const decoded = Buffer.from(value, "base64");
7
+ if (!validCharacters || decoded.byteLength !== 32) {
8
+ throw new VendoError("validation", "encryption.key must be a base64-encoded 32-byte key");
9
+ }
10
+ return decoded;
11
+ }
12
+ /** 02-store §4 — envelope version `v2`: AES-256-GCM with the secret NAME
13
+ * bound as AAD, so a ciphertext swapped between rows (or served for the
14
+ * wrong name) fails the auth tag instead of decrypting to another secret's
15
+ * value. */
16
+ export function encryptSecret(value, key, name) {
17
+ const iv = randomBytes(12);
18
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
19
+ cipher.setAAD(Buffer.from(name, "utf8"));
20
+ const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
21
+ const tag = cipher.getAuthTag();
22
+ return `v2:${iv.toString("base64")}:${tag.toString("base64")}:${ciphertext.toString("base64")}`;
23
+ }
24
+ /** 02-store §4 */
25
+ export function decryptSecret(value, key, name) {
26
+ try {
27
+ const [version, ivValue, tagValue, ciphertextValue, extra] = value.split(":");
28
+ if (version !== "v2"
29
+ || !ivValue || !tagValue || ciphertextValue === undefined || extra !== undefined) {
30
+ throw new Error("invalid ciphertext envelope");
31
+ }
32
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivValue, "base64"));
33
+ decipher.setAAD(Buffer.from(name, "utf8"));
34
+ decipher.setAuthTag(Buffer.from(tagValue, "base64"));
35
+ return Buffer.concat([
36
+ decipher.update(Buffer.from(ciphertextValue, "base64")),
37
+ decipher.final(),
38
+ ]).toString("utf8");
39
+ }
40
+ catch {
41
+ throw new VendoError("validation", "Stored secret could not be decrypted");
42
+ }
43
+ }
44
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE5E,kBAAkB;AAClB,MAAM,UAAU,qBAAqB,CAAC,KAAa;IACjD,MAAM,eAAe,GAAG,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC7C,IAAI,CAAC,eAAe,IAAI,OAAO,CAAC,UAAU,KAAK,EAAE,EAAE,CAAC;QAClD,MAAM,IAAI,UAAU,CAAC,YAAY,EAAE,qDAAqD,CAAC,CAAC;IAC5F,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;aAGa;AACb,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,GAAW,EAAE,IAAY;IACpE,MAAM,EAAE,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IACzC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACjF,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAChC,OAAO,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AAClG,CAAC;AAED,kBAAkB;AAClB,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,GAAW,EAAE,IAAY;IACpE,IAAI,CAAC;QACH,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9E,IACE,OAAO,KAAK,IAAI;eACb,CAAC,OAAO,IAAI,CAAC,QAAQ,IAAI,eAAe,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,EAChF,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACjD,CAAC;QACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QACtF,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3C,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;QACrD,OAAO,MAAM,CAAC,MAAM,CAAC;YACnB,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;YACvD,QAAQ,CAAC,KAAK,EAAE;SACjB,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,UAAU,CAAC,YAAY,EAAE,sCAAsC,CAAC,CAAC;IAC7E,CAAC;AACH,CAAC"}
package/dist/db.d.ts CHANGED
@@ -1,28 +1,28 @@
1
- import { drizzle as drizzlePglite } from "drizzle-orm/pglite";
2
- import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
3
- import { Pool } from "pg";
4
- export interface VendoDatabaseConfig {
5
- connectionString?: string;
6
- pglite?: {
7
- dataDir: string;
1
+ /** 02-store §1 */
2
+ export interface StoreConfig {
3
+ url?: string;
4
+ dataDir?: string;
5
+ encryption?: {
6
+ key: string;
8
7
  };
8
+ /** 02-store §4 — dev-mode escape hatch: store secrets unencrypted when no
9
+ encryption key is configured. Never enable in production; secret writes
10
+ without a key fail closed there. */
11
+ allowUnencryptedSecrets?: boolean;
9
12
  }
10
- export type VendoDb = {
11
- kind: "pglite";
12
- db: ReturnType<typeof drizzlePglite>;
13
- cacheKey: string;
14
- } | {
15
- kind: "pg";
16
- db: ReturnType<typeof drizzlePg>;
17
- cacheKey: string;
18
- };
19
- /**
20
- * pg.Pool re-emits idle-client errors (backend restart, dropped connection)
21
- * on itself; with no 'error' listener Node escalates that to an uncaught
22
- * exception and kills the host process. The pool already discards the dead
23
- * client log and keep serving.
24
- */
25
- export declare function createPgPool(connectionString: string): Pool;
26
- export declare function createVendoDatabase(config?: VendoDatabaseConfig): Promise<VendoDb>;
27
- /** Idempotent, race-safe (advisory lock on real PG), memoized per handle. */
28
- export declare function migrateVendoDatabase(handle: VendoDb): Promise<void>;
13
+ /** 02-store §4 */
14
+ export interface Db {
15
+ kind: "pg" | "pglite";
16
+ query(text: string, params?: unknown[]): Promise<{
17
+ rows: Record<string, unknown>[];
18
+ }>;
19
+ close(): Promise<void>;
20
+ raw(): unknown;
21
+ }
22
+ type Query = Db["query"];
23
+ /** 02-store §4 */
24
+ export declare function createDb(config?: StoreConfig): Db;
25
+ /** 02-store §4 */
26
+ export declare function withSchemaLock<T>(db: Db, work: (query: Query) => Promise<T>): Promise<T>;
27
+ export {};
28
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAKA,kBAAkB;AAClB,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7B;;2CAEuC;IACvC,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAED,kBAAkB;AAClB,MAAM,WAAW,EAAE;IACjB,IAAI,EAAE,IAAI,GAAG,QAAQ,CAAC;IACtB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAC,CAAC;IACtF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,GAAG,IAAI,OAAO,CAAC;CAChB;AAGD,KAAK,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC;AAmPzB,kBAAkB;AAClB,wBAAgB,QAAQ,CAAC,MAAM,GAAE,WAAgB,GAAG,EAAE,CA2FrD;AAED,kBAAkB;AAClB,wBAAsB,cAAc,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAE9F"}