@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAGlC;;;;;;;;;;4CAU4C;AAC5C,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,kBAAkB;AAClB,eAAO,MAAM,GAAG,wqHAqEN,CAAC;AAgIX,kBAAkB;AAClB,wBAAsB,YAAY,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAExD"}
package/dist/schema.js CHANGED
@@ -1,114 +1,206 @@
1
- import { pgSchema, text, integer, boolean, jsonb, timestamp, primaryKey, uniqueIndex, index, bigserial } from "drizzle-orm/pg-core";
2
- export const vendo = pgSchema("vendo");
3
- export const automations = vendo.table("automations", {
4
- id: text("id").primaryKey(),
5
- tenantId: text("tenant_id").notNull(),
6
- subject: text("subject").notNull(),
7
- name: text("name").notNull(),
8
- status: text("status").notNull(),
9
- disabledReason: text("disabled_reason"),
10
- spec: jsonb("spec").notNull(),
11
- currentVersion: integer("current_version").notNull(),
12
- triggerKind: text("trigger_kind").notNull(),
13
- triggerKey: text("trigger_key"),
14
- counters: jsonb("counters").notNull(),
15
- createdFromThreadId: text("created_from_thread_id"),
16
- createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).notNull(),
17
- updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).notNull(),
18
- }, (t) => [index("automations_scope_idx").on(t.tenantId, t.subject), index("automations_trigger_idx").on(t.triggerKind, t.triggerKey)]);
19
- export const automationVersions = vendo.table("automation_versions", {
20
- automationId: text("automation_id").notNull(),
21
- version: integer("version").notNull(),
22
- spec: jsonb("spec").notNull(),
23
- dslVersion: integer("dsl_version").notNull(),
24
- manifestHash: text("manifest_hash"),
25
- grants: jsonb("grants").notNull(),
26
- createdBy: text("created_by").notNull(),
27
- createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).notNull(),
28
- }, (t) => [primaryKey({ columns: [t.automationId, t.version] })]);
29
- export const automationRuns = vendo.table("automation_runs", {
30
- id: text("id").primaryKey(), // firingRunId DB-level double-fire dedup
31
- automationId: text("automation_id").notNull(),
32
- tenantId: text("tenant_id").notNull(),
33
- subject: text("subject").notNull(),
34
- version: integer("version").notNull(),
35
- manifestHash: text("manifest_hash"),
36
- status: text("status").notNull(),
37
- outcome: text("outcome"),
38
- trigger: jsonb("trigger").notNull(),
39
- steps: jsonb("steps").notNull(),
40
- pendingApproval: jsonb("pending_approval"),
41
- error: text("error"),
42
- isTest: boolean("is_test").notNull(),
43
- /** ENG-193 §4.6 cumulative count of parked actions this run created. */
44
- parkedCount: integer("parked_count").notNull().default(0),
45
- startedAt: timestamp("started_at", { withTimezone: true, mode: "string" }).notNull(),
46
- finishedAt: timestamp("finished_at", { withTimezone: true, mode: "string" }),
47
- }, (t) => [index("runs_automation_idx").on(t.automationId, t.tenantId, t.subject)]);
48
- /** ENG-193 §4.6 parked automation drafts: an action a for_each iteration or
49
- * agent step couldn't run unattended, resolved standalone later. Indexed
50
- * columns cover the list filters; the full ParkedAction payload (frozen
51
- * input, guard bindings, tier, hashes, resolution detail) lives in `record`
52
- * jsonb, mirrored the way `saved_vendos.record` already is. */
53
- export const parkedActions = vendo.table("parked_actions", {
54
- id: text("id").primaryKey(),
55
- tenantId: text("tenant_id").notNull(),
56
- subject: text("subject").notNull(),
57
- automationId: text("automation_id").notNull(),
58
- runId: text("run_id").notNull(),
59
- resolution: text("resolution"),
60
- requestedAt: timestamp("requested_at", { withTimezone: true, mode: "string" }).notNull(),
61
- record: jsonb("record").notNull(),
62
- }, (t) => [index("parked_actions_scope_idx").on(t.tenantId, t.subject, t.automationId, t.runId)]);
63
- export const decisions = vendo.table("decisions", {
64
- tenantId: text("tenant_id").notNull(),
65
- subject: text("subject").notNull(),
66
- canonicalKey: text("canonical_key").notNull(),
67
- decision: jsonb("decision").notNull(),
68
- createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).notNull(),
69
- }, (t) => [primaryKey({ columns: [t.tenantId, t.subject, t.canonicalKey] })]);
70
- export const threads = vendo.table("threads", {
71
- id: text("id").notNull(),
72
- tenantId: text("tenant_id").notNull(),
73
- subject: text("subject").notNull(),
74
- title: text("title"),
75
- nextSeq: integer("next_seq").notNull().default(0),
76
- createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).notNull(),
77
- updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).notNull(),
78
- }, (t) => [primaryKey({ columns: [t.tenantId, t.subject, t.id] })]);
79
- export const threadMessages = vendo.table("thread_messages", {
80
- rowId: bigserial("row_id", { mode: "number" }).primaryKey(),
81
- tenantId: text("tenant_id").notNull(),
82
- subject: text("subject").notNull(),
83
- threadId: text("thread_id").notNull(),
84
- messageId: text("message_id").notNull(),
85
- seq: integer("seq").notNull(),
86
- message: jsonb("message").notNull(),
87
- }, (t) => [
88
- uniqueIndex("thread_messages_id_uq").on(t.tenantId, t.subject, t.threadId, t.messageId),
89
- uniqueIndex("thread_messages_seq_uq").on(t.tenantId, t.subject, t.threadId, t.seq),
90
- ]);
91
- export const savedVendos = vendo.table("saved_vendos", {
92
- id: text("id").notNull(),
93
- tenantId: text("tenant_id").notNull(),
94
- subject: text("subject").notNull(),
95
- record: jsonb("record").notNull(),
96
- updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).notNull(),
97
- }, (t) => [primaryKey({ columns: [t.tenantId, t.subject, t.id] })]);
98
- export const connections = vendo.table("connections", {
99
- toolkit: text("toolkit").notNull(),
100
- tenantId: text("tenant_id").notNull(),
101
- subject: text("subject").notNull(),
102
- connectedAccountId: text("connected_account_id"),
103
- status: text("status").notNull(),
104
- createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).notNull(),
105
- }, (t) => [
106
- primaryKey({ columns: [t.tenantId, t.subject, t.toolkit] }),
107
- index("connections_account_idx").on(t.connectedAccountId),
108
- ]);
109
- /** Tiny operational KV (scheduler heartbeat, future flags). NOT for domain data. */
110
- export const meta = vendo.table("meta", {
111
- key: text("key").primaryKey(),
112
- value: jsonb("value").notNull(),
113
- updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).notNull(),
114
- });
1
+ import { VendoError } from "@vendoai/core";
2
+ import { randomUUID } from "node:crypto";
3
+ import { withSchemaLock } from "./db.js";
4
+ /** 02-store §4. v3 (block-actions design §C, ENG-263) historically added the
5
+ Vendo-owned org tables (`vendo_orgs` + `vendo_org_members`); those tables
6
+ are cut under the simplify-v2 kill-list (docs/superpowers/specs/2026-07-16
7
+ -simplify-v2-kill-list-design.md §A5) — orgs live on the Vendo-hosted side
8
+ now. Existing dev databases that already have `vendo_orgs`/
9
+ `vendo_org_members` keep those orphaned tables — erasing them is not
10
+ required and this migration does not attempt it.
11
+
12
+ v4 (kill-list §B3) adds `vendo_sessions`: the ephemeral in-memory overlay is
13
+ gone, anonymous rows are ordinary disk rows, and this table is the session
14
+ registry the TTL sweep reads (02 §4). */
15
+ export const SCHEMA_VERSION = 4;
16
+ /** 02-store §2 */
17
+ export const DDL = [
18
+ `CREATE TABLE IF NOT EXISTS vendo_apps (
19
+ id text PRIMARY KEY, subject text NOT NULL, enabled boolean NOT NULL DEFAULT true,
20
+ doc jsonb NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
21
+ )`,
22
+ "CREATE INDEX IF NOT EXISTS vendo_apps_subject_idx ON vendo_apps (subject)",
23
+ `CREATE TABLE IF NOT EXISTS vendo_records (
24
+ collection text NOT NULL, id text NOT NULL, data jsonb NOT NULL, refs jsonb,
25
+ created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL,
26
+ revision bigint NOT NULL DEFAULT 1,
27
+ PRIMARY KEY (collection, id)
28
+ )`,
29
+ "CREATE INDEX IF NOT EXISTS vendo_records_refs_idx ON vendo_records USING GIN (refs jsonb_path_ops)",
30
+ `CREATE TABLE IF NOT EXISTS vendo_blobs (
31
+ namespace text NOT NULL, key text NOT NULL, bytes bytea NOT NULL, content_type text,
32
+ created_at timestamptz NOT NULL, PRIMARY KEY (namespace, key)
33
+ )`,
34
+ `CREATE TABLE IF NOT EXISTS vendo_state (
35
+ app_id text NOT NULL, subject text NOT NULL, data jsonb NOT NULL,
36
+ updated_at timestamptz NOT NULL, PRIMARY KEY (app_id, subject)
37
+ )`,
38
+ `CREATE TABLE IF NOT EXISTS vendo_threads (
39
+ id text PRIMARY KEY, subject text NOT NULL, messages jsonb NOT NULL,
40
+ created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
41
+ )`,
42
+ "CREATE INDEX IF NOT EXISTS vendo_threads_subject_idx ON vendo_threads (subject)",
43
+ `CREATE TABLE IF NOT EXISTS vendo_grants (
44
+ id text PRIMARY KEY, subject text NOT NULL, tool text NOT NULL, descriptor_hash text NOT NULL,
45
+ scope jsonb NOT NULL, duration text NOT NULL, context_key text, app_id text, source text NOT NULL,
46
+ granted_at timestamptz NOT NULL, expires_at timestamptz, revoked_at timestamptz
47
+ )`,
48
+ "CREATE INDEX IF NOT EXISTS vendo_grants_subject_tool_idx ON vendo_grants (subject, tool)",
49
+ `CREATE TABLE IF NOT EXISTS vendo_approvals (
50
+ id text PRIMARY KEY, subject text NOT NULL, request jsonb NOT NULL,
51
+ status text NOT NULL DEFAULT 'pending', decided_at timestamptz, session_id text,
52
+ consumed_at timestamptz, created_at timestamptz NOT NULL
53
+ )`,
54
+ "CREATE INDEX IF NOT EXISTS vendo_approvals_subject_status_idx ON vendo_approvals (subject, status)",
55
+ `CREATE TABLE IF NOT EXISTS vendo_audit (
56
+ id text PRIMARY KEY, at timestamptz NOT NULL, kind text NOT NULL, subject text NOT NULL,
57
+ venue text NOT NULL, presence text NOT NULL, app_id text, tool text, event jsonb NOT NULL
58
+ )`,
59
+ "CREATE INDEX IF NOT EXISTS vendo_audit_subject_at_idx ON vendo_audit (subject, at)",
60
+ "CREATE INDEX IF NOT EXISTS vendo_audit_at_idx ON vendo_audit (at)",
61
+ `CREATE TABLE IF NOT EXISTS vendo_runs (
62
+ id text PRIMARY KEY, app_id text NOT NULL, trigger jsonb NOT NULL, status text NOT NULL,
63
+ record jsonb NOT NULL, started_at timestamptz NOT NULL, finished_at timestamptz
64
+ )`,
65
+ "CREATE INDEX IF NOT EXISTS vendo_runs_app_started_idx ON vendo_runs (app_id, started_at)",
66
+ `CREATE TABLE IF NOT EXISTS vendo_secrets (
67
+ name text PRIMARY KEY, ciphertext text NOT NULL, created_at timestamptz NOT NULL,
68
+ updated_at timestamptz
69
+ )`,
70
+ `CREATE TABLE IF NOT EXISTS vendo_mcp_clients (
71
+ id text PRIMARY KEY, data jsonb NOT NULL, refs jsonb,
72
+ created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
73
+ )`,
74
+ "CREATE INDEX IF NOT EXISTS vendo_mcp_clients_refs_idx ON vendo_mcp_clients USING GIN (refs jsonb_path_ops)",
75
+ `CREATE TABLE IF NOT EXISTS vendo_mcp_grants (
76
+ id text PRIMARY KEY, data jsonb NOT NULL, refs jsonb,
77
+ created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
78
+ )`,
79
+ "CREATE INDEX IF NOT EXISTS vendo_mcp_grants_refs_idx ON vendo_mcp_grants USING GIN (refs jsonb_path_ops)",
80
+ // 02-store §4 (kill-list B3): the ephemeral-session registry. One row per
81
+ // live anonymous session; touched_at is the last-activity stamp the TTL
82
+ // sweep compares against. Registration == touch (upsert).
83
+ `CREATE TABLE IF NOT EXISTS vendo_sessions (
84
+ subject text PRIMARY KEY, touched_at timestamptz NOT NULL
85
+ )`,
86
+ ];
87
+ // Additive columns stay compatible with same-version development databases (02 §2
88
+ // allows additive columns within the version train; key columns are untouched).
89
+ // vendo_state gains a stable record id (generated from the app_id:subject PK, so
90
+ // point lookups hit an index instead of seq-scanning) and its own created_at, so
91
+ // the seam can expose a creation timestamp that survives updates.
92
+ const ADDITIVE_DDL = [
93
+ "ALTER TABLE vendo_records ADD COLUMN IF NOT EXISTS revision bigint NOT NULL DEFAULT 1",
94
+ "ALTER TABLE vendo_approvals ADD COLUMN IF NOT EXISTS session_id text",
95
+ "ALTER TABLE vendo_approvals ADD COLUMN IF NOT EXISTS consumed_at timestamptz",
96
+ "ALTER TABLE vendo_state ADD COLUMN IF NOT EXISTS id text GENERATED ALWAYS AS (app_id || ':' || subject) STORED",
97
+ // created_at is the pagination cursor column, so it must never be NULL. DEFAULT now()
98
+ // fills the column for any direct INSERT that omits it (the table map is public); our
99
+ // own write paths always populate it explicitly.
100
+ "ALTER TABLE vendo_state ADD COLUMN IF NOT EXISTS created_at timestamptz DEFAULT now()",
101
+ // ADD COLUMN IF NOT EXISTS SKIPS when the column already exists, so databases that
102
+ // booted before the DEFAULT was introduced would keep a default-less column forever.
103
+ // SET DEFAULT is idempotent, so it runs every boot like the rest of this block.
104
+ "ALTER TABLE vendo_state ALTER COLUMN created_at SET DEFAULT now()",
105
+ "CREATE INDEX IF NOT EXISTS vendo_state_id_idx ON vendo_state (id)",
106
+ // Keyset pagination lists order by (created_at, id) DESC — compared at millisecond
107
+ // precision via date_trunc (helpers/utils.ts cursorMs; cursors round-trip through JS
108
+ // Dates) — with a matching `<` tuple predicate (records.ts / routing.ts). These btree
109
+ // indexes serve the equality/filter legs (the truncated sort itself is a top-N over the
110
+ // filtered set); a dropped index here is exactly the order-of-magnitude regression the
111
+ // perf gate exists to catch.
112
+ "CREATE INDEX IF NOT EXISTS vendo_records_collection_created_idx ON vendo_records (collection, created_at DESC, id DESC)",
113
+ "CREATE INDEX IF NOT EXISTS vendo_mcp_clients_created_idx ON vendo_mcp_clients (created_at DESC, id DESC)",
114
+ "CREATE INDEX IF NOT EXISTS vendo_mcp_grants_created_idx ON vendo_mcp_grants (created_at DESC, id DESC)",
115
+ // The automations tick and vendo.emit fetch apps by trigger kind (schedule / host-event).
116
+ // A STORED generated column projects doc->trigger->on->kind into an indexable value so
117
+ // those paths query only the matching apps instead of scanning every app for every subject.
118
+ // ADD COLUMN ... GENERATED ALWAYS AS ... STORED backfills existing rows on ALTER, so no
119
+ // separate data migration is needed (mirrors the vendo_state.id generated column above).
120
+ "ALTER TABLE vendo_apps ADD COLUMN IF NOT EXISTS trigger_kind text GENERATED ALWAYS AS (doc->'trigger'->'on'->>'kind') STORED",
121
+ "CREATE INDEX IF NOT EXISTS vendo_apps_subject_trigger_idx ON vendo_apps (subject, trigger_kind)",
122
+ // Thread listing derives a title without loading the full messages array (routing.ts uses a
123
+ // messages-less listSelect once a row has a stored title). NULLable; populated on next write.
124
+ "ALTER TABLE vendo_threads ADD COLUMN IF NOT EXISTS title text",
125
+ // ENG-310: revision counter backing the routed vendo_threads atomic capability
126
+ // (01 §12 — insertIfAbsent / compareAndSwap), so concurrent turns on one thread
127
+ // can do guarded read-merge-write instead of last-write-wins. DEFAULT backfills
128
+ // existing rows on ALTER; every write path bumps it.
129
+ "ALTER TABLE vendo_threads ADD COLUMN IF NOT EXISTS revision bigint NOT NULL DEFAULT 1",
130
+ // Wave 7: the same counter for vendo_apps, so the machine lifecycle and the
131
+ // schedule engine's fire claims (updateAppRow's read-mutate-CAS) stop
132
+ // degrading to read-then-put on the dev store — a multi-process dev host
133
+ // could double-fire a schedule or clobber a concurrent lifecycle write.
134
+ "ALTER TABLE vendo_apps ADD COLUMN IF NOT EXISTS revision bigint NOT NULL DEFAULT 1",
135
+ // Tracks the secret's last rewrite (rotation) separately from created_at;
136
+ // set() stamps it. NULL on legacy rows means created_at IS the last write.
137
+ "ALTER TABLE vendo_secrets ADD COLUMN IF NOT EXISTS updated_at timestamptz",
138
+ // The TTL sweep's stale scan and the host-driven claim both predicate on
139
+ // touched_at (sessions.ts); without this a busy anonymous host seq-scans
140
+ // the registry on every sweep interval.
141
+ "CREATE INDEX IF NOT EXISTS vendo_sessions_touched_idx ON vendo_sessions (touched_at)",
142
+ ];
143
+ // v2 backfill (runs once, only when upgrading from a version < 2 — 02 §4 keys
144
+ // migrations by vendo_meta.schema_version, forward-only). Three moves:
145
+ // 1. Relocate legacy vendo_state singletons that a pre-fix deployment wrote into
146
+ // vendo_records (collection 'vendo_state', id `${app_id}:${subject}`) into the
147
+ // dedicated table. App ids are colon-free and non-empty (`^app_[^:]+$`), so the
148
+ // FIRST colon splits id into app_id + subject unambiguously; the
149
+ // `id ~ '^app_[^:]+:.'` predicate relocates only rows whose leading segment is a
150
+ // real app id AND whose subject is non-empty — the SAME shape the state door
151
+ // (splitStateId) enforces. Anything else (colon-less rows, ids whose first
152
+ // segment is not app-shaped, or empty-subject ids like 'app_x:') SURVIVES in
153
+ // vendo_records rather than being silently destroyed or misrouted.
154
+ // 2. The DELETE is scoped to the identical predicate as the INSERT — only the rows
155
+ // actually relocated are removed.
156
+ // 3. Both write doors were live pre-fix (stateStore wrote the dedicated table, the
157
+ // seam wrote vendo_records), so a legacy row can be NEWER than an existing
158
+ // dedicated row. Resolve by timestamp (`WHERE vendo_state.updated_at <
159
+ // EXCLUDED.updated_at`) so the newer write wins instead of DO NOTHING dropping it.
160
+ // 4. Relocated rows set created_at = updated_at on insert (the column now DEFAULTs to
161
+ // now(), so it must be given the legacy timestamp explicitly); the trailing UPDATE
162
+ // still backfills created_at for any pre-existing row that predates the column.
163
+ const DATA_BACKFILL = [
164
+ `INSERT INTO vendo_state (app_id, subject, data, updated_at, created_at)
165
+ SELECT split_part(id, ':', 1), substring(id FROM position(':' IN id) + 1), data, updated_at, updated_at
166
+ FROM vendo_records WHERE collection = 'vendo_state' AND id ~ '^app_[^:]+:.'
167
+ ON CONFLICT (app_id, subject) DO UPDATE SET data = EXCLUDED.data, updated_at = EXCLUDED.updated_at
168
+ WHERE vendo_state.updated_at < EXCLUDED.updated_at`,
169
+ "DELETE FROM vendo_records WHERE collection = 'vendo_state' AND id ~ '^app_[^:]+:.'",
170
+ "UPDATE vendo_state SET created_at = updated_at WHERE created_at IS NULL",
171
+ ];
172
+ async function migrate(query) {
173
+ await query("CREATE TABLE IF NOT EXISTS vendo_meta (key text PRIMARY KEY, value jsonb NOT NULL)");
174
+ const result = await query("SELECT value FROM vendo_meta WHERE key = 'schema_version'");
175
+ const value = result.rows[0]?.["value"];
176
+ const version = typeof value === "number" ? value : undefined;
177
+ if (version !== undefined && version > SCHEMA_VERSION) {
178
+ throw new VendoError("conflict", `Store schema version ${version} is newer than supported version ${SCHEMA_VERSION}`);
179
+ }
180
+ const upgrading = version === undefined || version < SCHEMA_VERSION;
181
+ if (upgrading) {
182
+ for (const statement of DDL)
183
+ await query(statement);
184
+ await query(`INSERT INTO vendo_meta (key, value) VALUES ('schema_version', $1::jsonb)
185
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`, [JSON.stringify(SCHEMA_VERSION)]);
186
+ }
187
+ // Additive columns are safe to re-apply every run (IF NOT EXISTS); they keep
188
+ // same-version development databases compatible without a version bump.
189
+ for (const statement of ADDITIVE_DDL)
190
+ await query(statement);
191
+ // The v2 backfill is destructive-adjacent (it DELETEs from vendo_records), so it
192
+ // runs ONLY while upgrading past ITS version (< 2) — never unconditionally, or a
193
+ // newer vendo_records write in a mixed-version deploy would be repeatedly
194
+ // relocated/lost, and never on later bumps (v2→v3 adds tables only).
195
+ if (version === undefined || version < 2) {
196
+ for (const statement of DATA_BACKFILL)
197
+ await query(statement);
198
+ }
199
+ await query(`INSERT INTO vendo_meta (key, value) VALUES ('boot_id', $1::jsonb)
200
+ ON CONFLICT (key) DO NOTHING`, [JSON.stringify(randomUUID())]);
201
+ }
202
+ /** 02-store §4 */
203
+ export async function ensureSchema(db) {
204
+ await withSchemaLock(db, migrate);
205
+ }
206
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { type SecretsProvider } from "@vendoai/core";
2
+ import { type VendoStore } from "./store.js";
3
+ /** 02-store §1 */
4
+ export declare function envSecrets(prefix?: string): SecretsProvider;
5
+ /** 02-store §1 */
6
+ export declare function storeSecrets(store: VendoStore): SecretsProvider;
7
+ /** 02-store §3 */
8
+ export declare function secretStore(store: VendoStore): {
9
+ set(name: string, value: string): Promise<void>;
10
+ delete(name: string): Promise<void>;
11
+ list(): Promise<string[]>;
12
+ };
13
+ //# sourceMappingURL=secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAGtE,kBAAkB;AAClB,wBAAgB,UAAU,CAAC,MAAM,SAAK,GAAG,eAAe,CAMvD;AAiCD,kBAAkB;AAClB,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,eAAe,CAkB/D;AAED,kBAAkB;AAClB,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG;IAC9C,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC3B,CA0BA"}
@@ -0,0 +1,86 @@
1
+ import { VendoError } from "@vendoai/core";
2
+ import { decryptSecret, encryptSecret } from "./crypto.js";
3
+ import { dbFor, secretsConfigFor } from "./store.js";
4
+ import { text } from "./helpers/utils.js";
5
+ /** 02-store §1 */
6
+ export function envSecrets(prefix = "") {
7
+ return {
8
+ async get(name) {
9
+ return process.env[`${prefix}${name}`];
10
+ },
11
+ };
12
+ }
13
+ /** 02-store §4 — dev-mode plaintext envelope. Version-prefixed like the
14
+ * encrypted `v2:` envelope so a store that later gains a key still reads
15
+ * rows written before it. */
16
+ const PLAINTEXT_PREFIX = "plain@1:";
17
+ /** One loud line the first time a plaintext secret is actually stored — the
18
+ * guard against a deploy that forgot both NODE_ENV=production and the key
19
+ * (Devin review on the NODE_ENV-only gate). */
20
+ let plaintextWarned = false;
21
+ function warnPlaintextOnce() {
22
+ if (plaintextWarned)
23
+ return;
24
+ plaintextWarned = true;
25
+ console.warn("[vendo] Storing secrets UNENCRYPTED (dev mode). If this is a real deployment, set NODE_ENV=production "
26
+ + "and VENDO_STORE_ENCRYPTION_KEY (base64 32-byte) — production refuses plaintext secrets.");
27
+ }
28
+ /** The key when configured; null when plaintext is allowed (dev mode);
29
+ * fail-closed otherwise — production stores secrets encrypted or not at all. */
30
+ function keyFor(store) {
31
+ const { encryptionKey, allowPlaintextSecrets } = secretsConfigFor(store);
32
+ if (encryptionKey)
33
+ return encryptionKey;
34
+ if (allowPlaintextSecrets)
35
+ return null;
36
+ throw new VendoError("not-implemented", "Stored secrets require an encryption key in production: set VENDO_STORE_ENCRYPTION_KEY "
37
+ + "(base64 32-byte) or pass createStore({ encryption: { key } }).");
38
+ }
39
+ /** 02-store §1 */
40
+ export function storeSecrets(store) {
41
+ const db = dbFor(store);
42
+ return {
43
+ async get(name) {
44
+ const key = keyFor(store);
45
+ const result = await db.query("SELECT ciphertext FROM vendo_secrets WHERE name = $1", [name]);
46
+ const row = result.rows[0];
47
+ if (!row)
48
+ return undefined;
49
+ const stored = text(row["ciphertext"]);
50
+ if (stored.startsWith(PLAINTEXT_PREFIX)) {
51
+ return Buffer.from(stored.slice(PLAINTEXT_PREFIX.length), "base64").toString("utf8");
52
+ }
53
+ if (key === null) {
54
+ throw new VendoError("validation", "Stored secret is encrypted but no encryption key is configured");
55
+ }
56
+ return decryptSecret(stored, key, name);
57
+ },
58
+ };
59
+ }
60
+ /** 02-store §3 */
61
+ export function secretStore(store) {
62
+ const db = dbFor(store);
63
+ return {
64
+ async set(name, value) {
65
+ const key = keyFor(store);
66
+ if (key === null)
67
+ warnPlaintextOnce();
68
+ const ciphertext = key === null
69
+ ? `${PLAINTEXT_PREFIX}${Buffer.from(value, "utf8").toString("base64")}`
70
+ : encryptSecret(value, key, name);
71
+ // updated_at marks the last write (rotation), distinct from created_at.
72
+ await db.query(`INSERT INTO vendo_secrets (name, ciphertext, created_at, updated_at) VALUES ($1, $2, $3, $3)
73
+ ON CONFLICT (name) DO UPDATE SET ciphertext = EXCLUDED.ciphertext, updated_at = EXCLUDED.updated_at`, [name, ciphertext, new Date().toISOString()]);
74
+ },
75
+ async delete(name) {
76
+ await db.query("DELETE FROM vendo_secrets WHERE name = $1", [name]);
77
+ },
78
+ async list() {
79
+ const result = await db.query("SELECT name FROM vendo_secrets");
80
+ // Sort in JS: SQL ORDER BY is collation-dependent (PGlite ships C,
81
+ // hosted Postgres usually a locale) — one deterministic order everywhere.
82
+ return result.rows.map((row) => text(row["name"])).sort();
83
+ },
84
+ };
85
+ }
86
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import { type VendoStore } from "./store.js";
2
+ /** 02-store §4 (kill-list B3) — ephemeral (anonymous) sessions on disk.
3
+ *
4
+ * Anonymous principals write ORDINARY rows under their subject; what makes a
5
+ * session ephemeral is its registration in `vendo_sessions`, and what ends it
6
+ * is the TTL sweep (or adoption on sign-in — helpers/subjects). Registration
7
+ * is the caller's job: the umbrella registers the subject on every
8
+ * ephemeral-principal request (registration == touch), so idle time is
9
+ * measured from the last request, reads included. The store itself stays
10
+ * config-free — TTL policy arrives as an argument. */
11
+ /** Touch (or create) an ephemeral session: stamps `touched_at` for the TTL
12
+ sweep. `now` is a millisecond clock reading; the umbrella passes its own
13
+ session clock so tests are deterministic. */
14
+ export declare function registerEphemeralSubject(store: VendoStore, subject: string, now?: number): Promise<void>;
15
+ /** The stale-session candidates for a HOST-DRIVEN sweep (the hosted-store
16
+ service exposes this so the umbrella's hosted sweep can list → claim →
17
+ erase over the wire, ending in the erase cascade — 2026-07-18 hosted-store
18
+ one-pager). sweepEphemeralSubjects lists through this too; reading a
19
+ candidate confers no ownership — winning claimEphemeralSubject does. */
20
+ export declare function listStaleEphemeralSubjects(store: VendoStore, opts: {
21
+ idleMs: number;
22
+ now?: number;
23
+ }): Promise<string[]>;
24
+ /** The claim leg of a sweep (host-driven or sweepEphemeralSubjects): deleting
25
+ the session row is the mutual-exclusion point (the idleness predicate is
26
+ repeated in the claim — a re-touch after the stale listing defeats it, so
27
+ a live session is never erased out from under its visitor). The winner
28
+ owns the subject and MUST follow with the erase cascade. */
29
+ export declare function claimEphemeralSubject(store: VendoStore, subject: string, opts: {
30
+ idleMs: number;
31
+ now?: number;
32
+ }): Promise<boolean>;
33
+ /** Whether the subject currently has a registered ephemeral session. */
34
+ export declare function isEphemeralSubject(store: VendoStore, subject: string): Promise<boolean>;
35
+ /** The TTL sweep: erase every registered session idle for at least `idleMs`
36
+ (now - touched_at >= idleMs), cascading through the erase API (02 §5) so a
37
+ swept subject leaves zero rows anywhere. Returns the swept subjects so the
38
+ caller can cascade further (the umbrella forwards them to
39
+ `agent.evictSubject`, 03 §1).
40
+
41
+ Claim-first serialization with adoption (helpers/subjects): the sweep owns
42
+ a subject only after winning `claimEphemeralSubject` — a subject whose
43
+ session row an interleaved adopt already claimed is SKIPPED, so the erase
44
+ cascade can never chase app ids the adopt just moved to the signed-in
45
+ user. The claim repeats the idleness predicate, so a re-touch landing
46
+ after the stale listing (the window is wide — a full erase cascade runs
47
+ between candidates) also defeats it: a live session is never erased out
48
+ from under its visitor. */
49
+ export declare function sweepEphemeralSubjects(store: VendoStore, opts: {
50
+ idleMs: number;
51
+ now?: number;
52
+ }): Promise<string[]>;
53
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;;;sDAQsD;AAEtD;;gDAEgD;AAChD,wBAAsB,wBAAwB,CAC5C,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;2EAI2E;AAC3E,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACrC,OAAO,CAAC,MAAM,EAAE,CAAC,CAOnB;AAED;;;;+DAI+D;AAC/D,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACrC,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED,wEAAwE;AACxE,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAM7F;AAED;;;;;;;;;;;;;8BAa8B;AAC9B,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACrC,OAAO,CAAC,MAAM,EAAE,CAAC,CA2BnB"}
@@ -0,0 +1,85 @@
1
+ import { eraseStore } from "./erase.js";
2
+ import { dbFor } from "./store.js";
3
+ /** 02-store §4 (kill-list B3) — ephemeral (anonymous) sessions on disk.
4
+ *
5
+ * Anonymous principals write ORDINARY rows under their subject; what makes a
6
+ * session ephemeral is its registration in `vendo_sessions`, and what ends it
7
+ * is the TTL sweep (or adoption on sign-in — helpers/subjects). Registration
8
+ * is the caller's job: the umbrella registers the subject on every
9
+ * ephemeral-principal request (registration == touch), so idle time is
10
+ * measured from the last request, reads included. The store itself stays
11
+ * config-free — TTL policy arrives as an argument. */
12
+ /** Touch (or create) an ephemeral session: stamps `touched_at` for the TTL
13
+ sweep. `now` is a millisecond clock reading; the umbrella passes its own
14
+ session clock so tests are deterministic. */
15
+ export async function registerEphemeralSubject(store, subject, now = Date.now()) {
16
+ await dbFor(store).query(`INSERT INTO vendo_sessions (subject, touched_at) VALUES ($1, $2)
17
+ ON CONFLICT (subject) DO UPDATE SET touched_at = EXCLUDED.touched_at`, [subject, new Date(now).toISOString()]);
18
+ }
19
+ /** The stale-session candidates for a HOST-DRIVEN sweep (the hosted-store
20
+ service exposes this so the umbrella's hosted sweep can list → claim →
21
+ erase over the wire, ending in the erase cascade — 2026-07-18 hosted-store
22
+ one-pager). sweepEphemeralSubjects lists through this too; reading a
23
+ candidate confers no ownership — winning claimEphemeralSubject does. */
24
+ export async function listStaleEphemeralSubjects(store, opts) {
25
+ const cutoff = new Date((opts.now ?? Date.now()) - opts.idleMs).toISOString();
26
+ const result = await dbFor(store).query("SELECT subject FROM vendo_sessions WHERE touched_at <= $1 ORDER BY touched_at ASC", [cutoff]);
27
+ return result.rows.map((row) => String(row["subject"]));
28
+ }
29
+ /** The claim leg of a sweep (host-driven or sweepEphemeralSubjects): deleting
30
+ the session row is the mutual-exclusion point (the idleness predicate is
31
+ repeated in the claim — a re-touch after the stale listing defeats it, so
32
+ a live session is never erased out from under its visitor). The winner
33
+ owns the subject and MUST follow with the erase cascade. */
34
+ export async function claimEphemeralSubject(store, subject, opts) {
35
+ const cutoff = new Date((opts.now ?? Date.now()) - opts.idleMs).toISOString();
36
+ const result = await dbFor(store).query("DELETE FROM vendo_sessions WHERE subject = $1 AND touched_at <= $2 RETURNING 1", [subject, cutoff]);
37
+ return result.rows[0] !== undefined;
38
+ }
39
+ /** Whether the subject currently has a registered ephemeral session. */
40
+ export async function isEphemeralSubject(store, subject) {
41
+ const result = await dbFor(store).query("SELECT 1 FROM vendo_sessions WHERE subject = $1", [subject]);
42
+ return result.rows.length > 0;
43
+ }
44
+ /** The TTL sweep: erase every registered session idle for at least `idleMs`
45
+ (now - touched_at >= idleMs), cascading through the erase API (02 §5) so a
46
+ swept subject leaves zero rows anywhere. Returns the swept subjects so the
47
+ caller can cascade further (the umbrella forwards them to
48
+ `agent.evictSubject`, 03 §1).
49
+
50
+ Claim-first serialization with adoption (helpers/subjects): the sweep owns
51
+ a subject only after winning `claimEphemeralSubject` — a subject whose
52
+ session row an interleaved adopt already claimed is SKIPPED, so the erase
53
+ cascade can never chase app ids the adopt just moved to the signed-in
54
+ user. The claim repeats the idleness predicate, so a re-touch landing
55
+ after the stale listing (the window is wide — a full erase cascade runs
56
+ between candidates) also defeats it: a live session is never erased out
57
+ from under its visitor. */
58
+ export async function sweepEphemeralSubjects(store, opts) {
59
+ const now = opts.now ?? Date.now();
60
+ const stale = await listStaleEphemeralSubjects(store, { idleMs: opts.idleMs, now });
61
+ const erase = eraseStore(store);
62
+ const evicted = [];
63
+ for (const subject of stale) {
64
+ if (!(await claimEphemeralSubject(store, subject, { idleMs: opts.idleMs, now })))
65
+ continue; // adopted or re-touched mid-sweep — not ours
66
+ try {
67
+ await erase.bySubject(subject);
68
+ }
69
+ catch (error) {
70
+ // The claim deleted the session row; a failed cascade must give it back
71
+ // or no later sweep can ever see this subject again and its remaining
72
+ // rows are stranded (the callers log "will retry next interval" — make
73
+ // that true). Restore at the cutoff so it stays sweep-eligible; DO
74
+ // NOTHING keeps a visitor who re-registered mid-erase live. Best
75
+ // effort: if the restore itself fails, the original error still
76
+ // propagates.
77
+ await dbFor(store).query(`INSERT INTO vendo_sessions (subject, touched_at) VALUES ($1, $2)
78
+ ON CONFLICT (subject) DO NOTHING`, [subject, new Date(now - opts.idleMs).toISOString()]).catch(() => undefined);
79
+ throw error;
80
+ }
81
+ evicted.push(subject);
82
+ }
83
+ return evicted;
84
+ }
85
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}