@nylorun/runtime 0.14.0-beta → 0.15.0-beta

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 (198) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +75 -33
  3. package/dist/adapters/execution/restate.js +2 -2
  4. package/dist/adapters/replication/pgoutput.d.ts +5 -0
  5. package/dist/adapters/replication/pgoutput.js +4 -3
  6. package/dist/admin-openapi.json +184 -685
  7. package/dist/api/components.d.ts +308 -68
  8. package/dist/api/components.js +4 -5
  9. package/dist/api/gate/keys.d.ts +14 -0
  10. package/dist/api/gate/keys.js +59 -0
  11. package/dist/api/gate/routes.d.ts +34 -0
  12. package/dist/api/gate/routes.js +235 -0
  13. package/dist/api/http/define.js +0 -8
  14. package/dist/api/http/routes/access.js +25 -18
  15. package/dist/api/http/routes/tenant.d.ts +2 -1
  16. package/dist/api/http/routes/tenant.js +77 -5
  17. package/dist/api/http/routes/vaults.js +2 -2
  18. package/dist/api/openapi.js +4 -5
  19. package/dist/gates/contract.d.ts +51 -0
  20. package/dist/gates/contract.js +37 -0
  21. package/dist/gates/handler.d.ts +21 -0
  22. package/dist/gates/handler.js +55 -0
  23. package/dist/gates/http-client.d.ts +12 -0
  24. package/dist/gates/http-client.js +160 -0
  25. package/dist/gates/in-process.d.ts +32 -0
  26. package/dist/gates/in-process.js +42 -0
  27. package/dist/gates/inflight.d.ts +39 -0
  28. package/dist/gates/inflight.js +70 -0
  29. package/dist/gates/mcp-handler.d.ts +35 -0
  30. package/dist/gates/mcp-handler.js +142 -0
  31. package/dist/gates/meter.d.ts +24 -0
  32. package/dist/gates/meter.js +165 -0
  33. package/dist/gates/model-gate.d.ts +45 -0
  34. package/dist/gates/model-gate.js +1 -0
  35. package/dist/gates/tenant-vaults.d.ts +64 -0
  36. package/dist/gates/tenant-vaults.js +94 -0
  37. package/dist/gates/tool-calls.d.ts +51 -0
  38. package/dist/gates/tool-calls.js +64 -0
  39. package/dist/gates/tool-client.d.ts +12 -0
  40. package/dist/gates/tool-client.js +201 -0
  41. package/dist/gates/tool-contract.d.ts +116 -0
  42. package/dist/gates/tool-contract.js +64 -0
  43. package/dist/gates/tool-gate.d.ts +45 -0
  44. package/dist/gates/tool-gate.js +10 -0
  45. package/dist/host/admin-api.d.ts +7 -6
  46. package/dist/host/admin-api.js +9 -113
  47. package/dist/host/app.d.ts +1 -1
  48. package/dist/host/app.js +20 -24
  49. package/dist/host/config-for.d.ts +1 -1
  50. package/dist/host/config-for.js +2 -2
  51. package/dist/host/create-host.d.ts +5 -5
  52. package/dist/host/create-host.js +5 -15
  53. package/dist/host/execution.d.ts +20 -18
  54. package/dist/host/execution.js +2 -2
  55. package/dist/host/gates.d.ts +58 -0
  56. package/dist/host/gates.js +127 -0
  57. package/dist/host/http.d.ts +6 -1
  58. package/dist/host/http.js +17 -0
  59. package/dist/host/index.d.ts +1 -1
  60. package/dist/host/main.js +151 -71
  61. package/dist/host/stack-config.d.ts +71 -19
  62. package/dist/host/stack-config.js +192 -18
  63. package/dist/infra/database.d.ts +2 -2
  64. package/dist/infra/execution.d.ts +5 -5
  65. package/dist/infra/execution.js +8 -6
  66. package/dist/infra/index.d.ts +1 -1
  67. package/dist/infra/workers.d.ts +17 -17
  68. package/dist/infra/workers.js +9 -9
  69. package/dist/keys/client.d.ts +9 -0
  70. package/dist/keys/client.js +70 -0
  71. package/dist/keys/contract.d.ts +24 -0
  72. package/dist/keys/contract.js +12 -0
  73. package/dist/keys/keys.d.ts +41 -0
  74. package/dist/keys/keys.js +50 -0
  75. package/dist/mcp/connect.d.ts +43 -3
  76. package/dist/mcp/connect.js +12 -5
  77. package/dist/mcp/pool.d.ts +21 -3
  78. package/dist/mcp/pool.js +15 -2
  79. package/dist/openapi.json +887 -694
  80. package/dist/record/append.d.ts +51 -0
  81. package/dist/record/append.js +20 -0
  82. package/dist/{store/event.js → record/envelope.js} +1 -1
  83. package/dist/record/index.d.ts +11 -0
  84. package/dist/record/index.js +11 -0
  85. package/dist/store/postgres/connect.d.ts +15 -12
  86. package/dist/store/postgres/connect.js +24 -12
  87. package/dist/store/postgres/db.d.ts +16 -0
  88. package/dist/store/postgres/db.js +28 -0
  89. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  90. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  91. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  92. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  93. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  94. package/dist/store/postgres/migrate.d.ts +68 -0
  95. package/dist/store/postgres/migrate.js +183 -0
  96. package/dist/store/postgres/record-writer.d.ts +4 -0
  97. package/dist/store/postgres/record-writer.js +34 -0
  98. package/dist/store/postgres/record.d.ts +2 -8
  99. package/dist/store/postgres/record.js +50 -54
  100. package/dist/store/postgres/schema.d.ts +3096 -0
  101. package/dist/store/postgres/schema.js +363 -0
  102. package/dist/store/postgres/store.d.ts +2 -4
  103. package/dist/store/postgres/store.js +481 -567
  104. package/dist/store/postgres/tenant.d.ts +56 -0
  105. package/dist/store/postgres/tenant.js +154 -0
  106. package/dist/store/types.d.ts +43 -87
  107. package/dist/store/types.js +0 -1
  108. package/dist/streams/control.d.ts +1 -1
  109. package/dist/streams/control.js +6 -2
  110. package/dist/streams/relay/types.d.ts +2 -1
  111. package/dist/streams/relay/types.js +2 -1
  112. package/dist/streams/types.d.ts +5 -0
  113. package/dist/tenant/advance.d.ts +7 -2
  114. package/dist/tenant/advance.js +95 -16
  115. package/dist/tenant/cause.d.ts +25 -0
  116. package/dist/tenant/cause.js +45 -0
  117. package/dist/tenant/commands.js +4 -2
  118. package/dist/tenant/context.d.ts +26 -3
  119. package/dist/tenant/delivery-token.js +17 -18
  120. package/dist/tenant/delivery.d.ts +1 -1
  121. package/dist/tenant/delivery.js +18 -13
  122. package/dist/tenant/effects.d.ts +15 -1
  123. package/dist/tenant/effects.js +93 -17
  124. package/dist/tenant/ephemeral.d.ts +16 -7
  125. package/dist/tenant/ephemeral.js +45 -111
  126. package/dist/tenant/history.d.ts +64 -0
  127. package/dist/tenant/history.js +120 -0
  128. package/dist/tenant/model-setting.d.ts +2 -2
  129. package/dist/tenant/module.d.ts +14 -28
  130. package/dist/tenant/module.js +131 -271
  131. package/dist/tenant/paths.d.ts +11 -16
  132. package/dist/tenant/paths.js +24 -68
  133. package/dist/tenant/pool.d.ts +1 -1
  134. package/dist/tenant/pool.js +1 -1
  135. package/dist/tenant/principals.d.ts +23 -11
  136. package/dist/tenant/principals.js +61 -49
  137. package/dist/tenant/runtime.d.ts +19 -1
  138. package/dist/tenant/runtime.js +40 -26
  139. package/dist/tenant/scheduler.d.ts +4 -2
  140. package/dist/tenant/scheduler.js +4 -4
  141. package/dist/tenant/session-streams.d.ts +2 -0
  142. package/dist/tenant/session-streams.js +8 -3
  143. package/dist/tenant/status.d.ts +3 -0
  144. package/dist/tenant/status.js +1 -1
  145. package/dist/tenant/store-pg.d.ts +9 -6
  146. package/dist/tenant/store-pg.js +60 -125
  147. package/dist/tenant/streams.d.ts +9 -25
  148. package/dist/tenant/streams.js +7 -22
  149. package/dist/tenant/tokens.js +16 -14
  150. package/dist/tenant/types.d.ts +41 -92
  151. package/dist/tenant/types.js +4 -15
  152. package/dist/tenant/worker.js +4 -4
  153. package/dist/vault/host-model.d.ts +31 -0
  154. package/dist/vault/host-model.js +57 -0
  155. package/dist/vault/service.d.ts +26 -15
  156. package/dist/vault/service.js +70 -109
  157. package/dist/version.d.ts +1 -1
  158. package/dist/version.js +1 -1
  159. package/package.json +9 -5
  160. package/dist/store/memory.d.ts +0 -69
  161. package/dist/store/memory.js +0 -810
  162. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  163. package/dist/store/postgres/migrations/001_initial.js +0 -196
  164. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  165. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  166. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
  167. package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
  168. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
  169. package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
  170. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
  171. package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
  172. package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
  173. package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
  174. package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
  175. package/dist/store/postgres/migrations/007_event_time.js +0 -14
  176. package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
  177. package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
  178. package/dist/store/postgres/migrations/index.d.ts +0 -53
  179. package/dist/store/postgres/migrations/index.js +0 -96
  180. package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
  181. package/dist/store/postgres/migrations/shared/index.js +0 -92
  182. package/dist/store/postgres/names.d.ts +0 -11
  183. package/dist/store/postgres/names.js +0 -42
  184. package/dist/store/postgres/tenants.d.ts +0 -90
  185. package/dist/store/postgres/tenants.js +0 -228
  186. package/dist/streams/relay/memory.d.ts +0 -69
  187. package/dist/streams/relay/memory.js +0 -176
  188. package/dist/tenant/envelope.d.ts +0 -7
  189. package/dist/tenant/envelope.js +0 -22
  190. package/dist/tenant/quarantine-error.d.ts +0 -6
  191. package/dist/tenant/quarantine-error.js +0 -9
  192. package/dist/tenant/quarantine.d.ts +0 -27
  193. package/dist/tenant/quarantine.js +0 -72
  194. package/dist/tenant/store-memory.d.ts +0 -16
  195. package/dist/tenant/store-memory.js +0 -111
  196. /package/dist/{store → record}/cursor.d.ts +0 -0
  197. /package/dist/{store → record}/cursor.js +0 -0
  198. /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
@@ -1,16 +1,25 @@
1
1
  /**
2
- * The Postgres Session Store (architecture §12.2): one Tenant schema, one
3
- * `SessionStore`. See `store/types.ts` for the invariants it keeps.
2
+ * The Postgres Session Store (architecture §12.2, session-store.md §2–§3): the `SessionStore`
3
+ * of the one Tenant a database holds, its state in the schema `nylorun` and its record in
4
+ * `nylorun_streams`, queried through Drizzle (`schema.ts`). See `store/types.ts` for the
5
+ * invariants it keeps.
6
+ *
7
+ * ## Queries
8
+ *
9
+ * Plain reads and writes use Drizzle's query builder: `.set(patch)` and `and(...)` skip
10
+ * `undefined`, rows come back in the camelCase of `schema.ts`. Statements whose logic is SQL
11
+ * stay `sql` fragments inside it: the endpoint upsert's `CASE` arms, the log-head upsert,
12
+ * `orphanedSessions`' ordering, `counts()`, advisory locks and array updates.
4
13
  *
5
14
  * ## Transactions
6
15
  *
7
- * Every `tx` is one READ COMMITTED transaction on a pooled connection. Tables
8
- * are addressed by fully qualified, quoted names (`"tenant_<id>"."sessions"`),
9
- * never through `search_path`, so a statement cannot reach another schema.
10
- * Commit listeners and `afterCommit` callbacks run after `COMMIT` returns and
11
- * never reject the committed `tx`. Nested `tx` calls are detected with
12
- * `AsyncLocalStorage` and rejected, and a `Tx` rejects every call once its
13
- * callback settles.
16
+ * Every `tx` is one READ COMMITTED transaction on a pooled connection (Drizzle's
17
+ * `transaction` on postgres.js `begin`). Tables are addressed by schema-qualified names,
18
+ * never through `search_path`. Commit listeners and `afterCommit` callbacks run after `COMMIT`
19
+ * returns and never reject the committed `tx`. Nested `tx` calls are detected with
20
+ * `AsyncLocalStorage` and rejected, and a `Tx` rejects every call once its callback settles.
21
+ * A failed statement rejects `tx` with the driver's error (`driverError`), not Drizzle's
22
+ * wrapper, so its SQLSTATE is `code`.
14
23
  *
15
24
  * ## Lock ordering
16
25
  *
@@ -19,8 +28,8 @@
19
28
  * ends. To stay deadlock-free:
20
29
  *
21
30
  * 1. A session-scoped transaction locks its session row before writing any
22
- * other row of that session (effects, actions, checkpoints, its record rows
23
- * and log head).
31
+ * other row of that session (effects, actions, its record rows and log
32
+ * head).
24
33
  * 2. A transaction that touches several sessions locks them in ascending id
25
34
  * order, with `lockSessions` (`./locking.ts`), before writing any of them.
26
35
  *
@@ -31,32 +40,44 @@
31
40
  * ## Values
32
41
  *
33
42
  * Document and event bodies are `json`, stored as the text `JSON.stringify`
34
- * wrote, so every string round-trips, including U+0000 and unpaired
35
- * surrogates that `jsonb` rejects (see `migrations/001_initial.ts`), and key
36
- * order is kept. Ids and ISO timestamps compared as text use `COLLATE "C"`.
37
- * Lease times in action bodies are compared as `timestamptz`.
43
+ * wrote (`jsonText` in `schema.ts`), so every string round-trips, including U+0000 and
44
+ * unpaired surrogates that `jsonb` rejects, and key order is kept. Ids and ISO timestamps
45
+ * compared as text are `COLLATE "C"`. Delivery deadlines in action bodies are compared as
46
+ * `timestamptz`.
38
47
  */
39
48
  import { AsyncLocalStorage } from "node:async_hooks";
40
- import { randomUUID } from "node:crypto";
41
- import { buildEvent } from "../event.js";
42
- import { encodeCursor } from "../cursor.js";
49
+ import { and, count, eq, getTableColumns, gte, inArray, isNotNull, isNull, lt, lte, ne, or, sql, } from "drizzle-orm";
50
+ import { appendEvent } from "../../record/index.js";
43
51
  import { OwnershipLostError } from "../ownership.js";
44
- import { POSTGRES_SCHEMA_VERSION, readSchemaVersion } from "./migrations/index.js";
45
- import { assertIdentifier, tenantSchemaName } from "./names.js";
46
- import { STREAMS_SCHEMA } from "./migrations/shared/index.js";
52
+ import { database, driverError } from "./db.js";
53
+ import { expectedSchemaVersion, readSchemaVersion } from "./migrate.js";
47
54
  import { createPostgresRecordReader } from "./record.js";
48
- /** The shared record (Durable Streams §6): every Tenant's events and log heads. */
49
- const SESSION_EVENTS = `${STREAMS_SCHEMA}.session_events`;
50
- const LOG_HEADS = `${STREAMS_SCHEMA}.session_log_heads`;
55
+ import { postgresRecordWriter } from "./record-writer.js";
56
+ import { actions, commands, definitions, effects, endpoints, links, modelBudgets, modelUsage, principals, publishableKeys, sandboxes, sessionEvents, sessionLogHeads, sessions, toolCrossings, signingKeys, subjectEpochs, subjectUsage, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, } from "./schema.js";
51
57
  export function createPostgresSessionStore(options) {
52
58
  return new PostgresSessionStore(options);
53
59
  }
54
- const OWNERSHIP_KEYS = new Set(["owner", "epoch", "ownerExpiresAt"]);
55
60
  const OPEN_SESSION = ["running", "runnable"];
61
+ const DOCUMENTS = {
62
+ definitions,
63
+ sessions: sessions,
64
+ commands,
65
+ effects: effects,
66
+ actions: actions,
67
+ sandboxes,
68
+ links: links,
69
+ };
70
+ /** A session row as `StoredSession` reads it: the body and the ownership columns. */
71
+ const SESSION = {
72
+ body: sessions.body,
73
+ owner: sessions.owner,
74
+ epoch: sessions.epoch,
75
+ ownerExpiresAt: sessions.ownerExpiresAt,
76
+ };
56
77
  class PostgresSessionStore {
57
78
  tenantId;
58
79
  sql;
59
- schema;
80
+ db;
60
81
  schemaVersion;
61
82
  active = new AsyncLocalStorage();
62
83
  listeners = new Set();
@@ -67,9 +88,8 @@ class PostgresSessionStore {
67
88
  constructor(options) {
68
89
  this.tenantId = options.tenantId;
69
90
  this.sql = options.sql;
70
- this.schema = options.schema ?? tenantSchemaName(options.tenantId);
71
- assertIdentifier(this.schema);
72
- this.schemaVersion = options.schemaVersion ?? POSTGRES_SCHEMA_VERSION;
91
+ this.db = database(options.sql);
92
+ this.schemaVersion = options.schemaVersion ?? expectedSchemaVersion();
73
93
  this.now = options.now ?? (() => new Date());
74
94
  this.onError =
75
95
  options.onError ??
@@ -84,19 +104,22 @@ class PostgresSessionStore {
84
104
  throw new Error("Nested SessionStore.tx is not allowed");
85
105
  let t;
86
106
  // Wrapped so `begin` does not treat an array result as queries to await.
87
- const run = this.sql.begin("isolation level read committed", async (sql) => {
88
- t = new PostgresTx(sql, this.schema, this.tenantId, this.now);
107
+ const run = this.db.transaction(async (db) => {
108
+ t = new PostgresTx(db, this.tenantId, this.now);
89
109
  try {
90
110
  return { value: await this.active.run(this, () => fn(t)) };
91
111
  }
92
112
  finally {
93
113
  t.closed = true;
94
114
  }
95
- });
115
+ }, { isolationLevel: "read committed" });
96
116
  this.inflight.add(run);
97
117
  let result;
98
118
  try {
99
- result = (await run);
119
+ result = await run;
120
+ }
121
+ catch (error) {
122
+ throw driverError(error);
100
123
  }
101
124
  finally {
102
125
  this.inflight.delete(run);
@@ -137,7 +160,7 @@ class PostgresSessionStore {
137
160
  if (this.closed)
138
161
  return { ok: false, schemaVersion: 0, expectedSchemaVersion };
139
162
  try {
140
- const schemaVersion = (await readSchemaVersion(this.sql, this.schema)) ?? 0;
163
+ const schemaVersion = (await readSchemaVersion(this.db)) ?? 0;
141
164
  return {
142
165
  ok: schemaVersion === expectedSchemaVersion,
143
166
  schemaVersion,
@@ -154,160 +177,43 @@ class PostgresSessionStore {
154
177
  await Promise.allSettled([...this.inflight]);
155
178
  }
156
179
  }
157
- // ---------------------------------------------------------------------------
158
180
  function storedSession(row) {
159
181
  return { ...row.body, ...ownership(row) };
160
182
  }
161
183
  function ownership(row) {
162
184
  return {
163
- owner: row.owner ?? null,
164
- epoch: Number(row.epoch),
165
- ownerExpiresAt: row.owner_expires_at
166
- ? new Date(row.owner_expires_at).toISOString()
167
- : null,
185
+ owner: row.owner,
186
+ epoch: row.epoch,
187
+ ownerExpiresAt: row.ownerExpiresAt?.toISOString() ?? null,
168
188
  };
169
189
  }
170
- function toJson(value) {
171
- const json = JSON.stringify(value);
172
- if (json === undefined)
173
- throw new Error("Document body must be JSON");
174
- return json;
175
- }
176
- const bytes = (value) => Buffer.from(value.buffer, value.byteOffset, value.byteLength);
177
- const fromBytes = (value) => new Uint8Array(value);
190
+ /** An endpoint row with its unset (null) columns left out, as `EndpointRow` has them. */
178
191
  function endpointRow(row) {
179
- const optional = (key, value) => value === null ? {} : { [key]: value };
180
- return {
181
- agentId: row.agent_id,
182
- url: row.url,
183
- implementationVersion: row.implementation_version,
184
- ...optional("manifestHash", row.manifest_hash),
185
- timeoutMs: row.timeout_ms,
186
- maxConcurrent: row.max_concurrent,
187
- ...optional("principalId", row.principal_id),
188
- ...optional("lastDeliveryAt", row.last_delivery_at),
189
- ...optional("lastSuccessAt", row.last_success_at),
190
- ...optional("lastErrorCode", row.last_error_code),
191
- ...optional("lastErrorMessage", row.last_error_message),
192
- consecutiveFailures: row.consecutive_failures,
193
- ...optional("servedImplementationVersion", row.served_implementation_version),
194
- ...optional("servedManifestHash", row.served_manifest_hash),
195
- createdAt: row.created_at,
196
- updatedAt: row.updated_at,
197
- };
198
- }
199
- function principalRow(row) {
200
- return {
201
- id: row.id,
202
- role: row.role,
203
- tokenHash: row.token_hash,
204
- idempotencyKey: row.idempotency_key,
205
- createdAt: row.created_at,
206
- };
207
- }
208
- function signingKeyRow(row) {
209
- return {
210
- id: row.id,
211
- state: row.state,
212
- alg: row.alg,
213
- publicJwk: row.public_jwk,
214
- createdAt: row.created_at,
215
- activatedAt: row.activated_at,
216
- retiredAt: row.retired_at,
217
- revokedAt: row.revoked_at,
218
- kekId: row.kek_id,
219
- nonce: fromBytes(row.nonce),
220
- ciphertext: fromBytes(row.ciphertext),
221
- wrappedDek: fromBytes(row.wrapped_dek),
222
- };
223
- }
224
- function publishableKeyRow(row) {
225
- return {
226
- id: row.id,
227
- key: row.key,
228
- name: row.name,
229
- originsJson: row.origins_json,
230
- createdAt: row.created_at,
231
- revokedAt: row.revoked_at,
232
- };
192
+ return Object.fromEntries(Object.entries(row).filter(([, value]) => value !== null));
233
193
  }
234
194
  /** The time column a signing key's new state stamps. */
235
195
  const SIGNING_KEY_STAMP = {
236
- current: "activated_at",
237
- previous: "retired_at",
238
- revoked: "revoked_at",
196
+ current: "activatedAt",
197
+ previous: "retiredAt",
198
+ revoked: "revokedAt",
239
199
  };
240
- function vaultRow(row) {
241
- return {
242
- id: row.id,
243
- name: row.name,
244
- ownerUserId: row.owner_user_id,
245
- metadataJson: row.metadata_json,
246
- createdAt: row.created_at,
247
- scope: row.scope,
248
- };
249
- }
250
- function credentialRow(row) {
251
- return {
252
- id: row.id,
253
- vaultId: row.vault_id,
254
- name: row.name,
255
- type: row.type,
256
- bindingJson: row.binding_json,
257
- expiresAt: row.expires_at,
258
- createdAt: row.created_at,
259
- rotatedAt: row.rotated_at,
260
- kekId: row.kek_id,
261
- nonce: fromBytes(row.nonce),
262
- ciphertext: fromBytes(row.ciphertext),
263
- wrappedDek: fromBytes(row.wrapped_dek),
264
- };
265
- }
266
- function auditRow(row) {
267
- return {
268
- id: row.id,
269
- at: row.at,
270
- actor: row.actor,
271
- action: row.action,
272
- vaultId: row.vault_id,
273
- credentialId: row.credential_id,
274
- sessionId: row.session_id,
275
- target: row.target,
276
- outcome: row.outcome,
277
- };
278
- }
279
- /** `VaultCredentialPatch` field → column. */
280
- const CREDENTIAL_COLUMNS = {
281
- name: "name",
282
- type: "type",
283
- bindingJson: "binding_json",
284
- expiresAt: "expires_at",
285
- rotatedAt: "rotated_at",
286
- kekId: "kek_id",
287
- nonce: "nonce",
288
- ciphertext: "ciphertext",
289
- wrappedDek: "wrapped_dek",
290
- };
291
- const SESSION_TABLES = [
292
- "sessions",
293
- "commands",
294
- "checkpoints",
295
- "effects",
296
- "actions",
297
- "links",
298
- ];
200
+ /** The audit columns a row has (`ord` only orders them). */
201
+ const { ord: _ord, ...AUDIT } = getTableColumns(vaultAudit);
202
+ const SESSION_TABLES = [sessions, commands, effects, actions, links];
203
+ /** The inserted row's `column` (`ON CONFLICT … DO UPDATE`). */
204
+ const excluded = (column) => sql.raw(`excluded.${column}`);
205
+ /** Keeps an endpoint's health `column` when its URL stays; a new URL starts with `fresh`. */
206
+ const sameUrl = (column, fresh = sql `NULL`) => sql `CASE WHEN ${endpoints.url} = excluded.url THEN ${column} ELSE ${fresh} END`;
299
207
  class PostgresTx {
300
- sql;
301
- schema;
208
+ db;
302
209
  tenantId;
303
210
  now;
304
211
  closed = false;
305
212
  events = [];
306
213
  generations = [];
307
214
  callbacks = [];
308
- constructor(sql, schema, tenantId, now) {
309
- this.sql = sql;
310
- this.schema = schema;
215
+ constructor(db, tenantId, now) {
216
+ this.db = db;
311
217
  this.tenantId = tenantId;
312
218
  this.now = now;
313
219
  }
@@ -315,87 +221,67 @@ class PostgresTx {
315
221
  if (this.closed)
316
222
  throw new Error("Tx used after its transaction ended");
317
223
  }
318
- /** The quoted, schema-qualified table. */
319
- t(table) {
320
- return this.sql(`${this.schema}.${table}`);
321
- }
322
224
  // --- documents -----------------------------------------------------------
323
225
  async get(table, id) {
324
226
  this.check();
325
- const sql = this.sql;
326
227
  if (table === "sessions") {
327
- const [row] = await sql `
328
- SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
329
- WHERE id = ${id}`;
228
+ const [row] = await this.sessions().where(eq(sessions.id, id));
330
229
  return row && storedSession(row);
331
230
  }
332
- const [row] = await sql `SELECT body FROM ${this.t(table)} WHERE id = ${id}`;
231
+ const t = DOCUMENTS[table];
232
+ const [row] = await this.db.select({ body: t.body }).from(t).where(eq(t.id, id));
333
233
  return row?.body;
334
234
  }
335
235
  async put(table, id, body) {
336
236
  this.check();
337
237
  let value = body;
338
- if (table === "sessions" && body && typeof body === "object")
339
- value = Object.fromEntries(Object.entries(body).filter(([key]) => !OWNERSHIP_KEYS.has(key)));
340
- const json = toJson(value);
341
- await this.sql `
342
- INSERT INTO ${this.t(table)} (id, body) VALUES (${id}, ${json}::text::json)
343
- ON CONFLICT (id) DO UPDATE SET body = excluded.body`;
238
+ if (table === "sessions" && body && typeof body === "object") {
239
+ // Ownership is store-managed: never in the body.
240
+ const { owner: _owner, epoch: _epoch, ownerExpiresAt: _expires, ...rest } = body;
241
+ value = rest;
242
+ }
243
+ const t = DOCUMENTS[table];
244
+ await this.db
245
+ .insert(t)
246
+ .values({ id, body: value })
247
+ .onConflictDoUpdate({ target: t.id, set: { body: excluded("body") } });
344
248
  }
345
249
  async delete(table, id) {
346
250
  this.check();
347
251
  // A session's record rows and log head stay: a session created again with this id
348
252
  // continues its log (per-session record deletion is deferred, Durable Streams §15).
349
- await this.sql `DELETE FROM ${this.t(table)} WHERE id = ${id}`;
350
- }
351
- async sessionRows(where, tail) {
352
- const sql = this.sql;
353
- const rows = await sql `
354
- SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
355
- ${where ? sql `WHERE ${where}` : sql ``}
356
- ${tail ?? sql `ORDER BY id`}`;
357
- return rows.map((row) => storedSession(row));
253
+ const t = DOCUMENTS[table];
254
+ await this.db.delete(t).where(eq(t.id, id));
255
+ }
256
+ sessions() {
257
+ return this.db.select(SESSION).from(sessions).$dynamic();
358
258
  }
359
259
  // --- ordering ------------------------------------------------------------
360
260
  async lockSession(id) {
361
261
  this.check();
362
- const [row] = await this.sql `
363
- SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
364
- WHERE id = ${id} FOR UPDATE`;
262
+ const [row] = await this.sessions().where(eq(sessions.id, id)).for("update");
365
263
  return row && storedSession(row);
366
264
  }
367
265
  async event(sessionId, turnId, type, payload) {
368
266
  this.check();
369
- const sql = this.sql;
267
+ const db = this.db;
370
268
  // The session row lock orders the session's events; the log head allocates under it.
371
- const [session] = await sql `
372
- SELECT epoch FROM ${this.t("sessions")} WHERE id = ${sessionId} FOR UPDATE`;
269
+ const [session] = await db
270
+ .select({ epoch: sessions.epoch })
271
+ .from(sessions)
272
+ .where(eq(sessions.id, sessionId))
273
+ .for("update");
373
274
  if (!session)
374
275
  throw new Error(`Session ${sessionId} not found`);
375
- const [row] = await sql `
376
- INSERT INTO ${sql(LOG_HEADS)} (tenant_id, session_id, generation, head)
377
- VALUES (
378
- ${this.tenantId}, ${sessionId},
379
- coalesce((SELECT basin_generation FROM ${this.t("tenant")}), 0), 1)
380
- ON CONFLICT (tenant_id, session_id)
381
- DO UPDATE SET head = ${sql(LOG_HEADS)}.head + 1
382
- RETURNING head - 1 AS seq, generation`;
383
- const seq = Number(row.seq);
384
- const generation = Number(row.generation);
385
- const event = buildEvent({
276
+ const { event, generation } = await appendEvent(postgresRecordWriter(db), {
386
277
  tenantId: this.tenantId,
387
278
  sessionId,
388
279
  turnId,
389
- seq,
390
- epoch: Number(session.epoch),
280
+ epoch: session.epoch,
391
281
  time: this.now(),
392
282
  type,
393
283
  payload,
394
284
  });
395
- await sql `
396
- INSERT INTO ${sql(SESSION_EVENTS)} (tenant_id, session_id, seq, generation, type, body)
397
- VALUES (${this.tenantId}, ${sessionId}, ${seq}, ${generation}, ${type},
398
- ${JSON.stringify(event)}::text::json)`;
399
285
  this.events.push(event);
400
286
  this.generations.push(generation);
401
287
  return structuredClone(event);
@@ -407,10 +293,7 @@ class PostgresTx {
407
293
  // --- ownership -----------------------------------------------------------
408
294
  async takeOwnership(sessionId, claim) {
409
295
  this.check();
410
- const sql = this.sql;
411
- const [row] = await sql `
412
- SELECT owner, epoch, owner_expires_at FROM ${this.t("sessions")}
413
- WHERE id = ${sessionId} FOR UPDATE`;
296
+ const [row] = await this.sessions().where(eq(sessions.id, sessionId)).for("update");
414
297
  if (!row)
415
298
  return { status: "missing" };
416
299
  const previous = ownership(row);
@@ -422,30 +305,36 @@ class PostgresTx {
422
305
  owner: previous.owner,
423
306
  ownerExpiresAt: previous.ownerExpiresAt,
424
307
  };
425
- const expires = new Date(claim.now.getTime() + claim.leaseMs);
426
- const [updated] = await sql `
427
- UPDATE ${this.t("sessions")}
428
- SET owner = ${claim.owner}, epoch = epoch + 1, owner_expires_at = ${expires}
429
- WHERE id = ${sessionId} RETURNING epoch`;
308
+ const [updated] = await this.db
309
+ .update(sessions)
310
+ .set({
311
+ owner: claim.owner,
312
+ epoch: sql `${sessions.epoch} + 1`,
313
+ ownerExpiresAt: new Date(claim.now.getTime() + claim.leaseMs),
314
+ })
315
+ .where(eq(sessions.id, sessionId))
316
+ .returning({ epoch: sessions.epoch });
430
317
  return {
431
318
  status: "owned",
432
- epoch: Number(updated.epoch),
319
+ epoch: updated.epoch,
433
320
  takeover: previous.owner !== null,
434
321
  previous,
435
322
  };
436
323
  }
437
324
  async renewOwnership(sessionId, owner, epoch, until) {
438
325
  this.check();
439
- const result = await this.sql `
440
- UPDATE ${this.t("sessions")} SET owner_expires_at = ${until}
441
- WHERE id = ${sessionId} AND owner = ${owner} AND epoch = ${epoch}`;
326
+ const result = await this.db
327
+ .update(sessions)
328
+ .set({ ownerExpiresAt: until })
329
+ .where(and(eq(sessions.id, sessionId), eq(sessions.owner, owner), eq(sessions.epoch, epoch)));
442
330
  return result.count > 0;
443
331
  }
444
332
  async releaseOwnership(sessionId, owner, epoch) {
445
333
  this.check();
446
- const result = await this.sql `
447
- UPDATE ${this.t("sessions")} SET owner = NULL, owner_expires_at = NULL
448
- WHERE id = ${sessionId} AND owner = ${owner} AND epoch = ${epoch}`;
334
+ const result = await this.db
335
+ .update(sessions)
336
+ .set({ owner: null, ownerExpiresAt: null })
337
+ .where(and(eq(sessions.id, sessionId), eq(sessions.owner, owner), eq(sessions.epoch, epoch)));
449
338
  return result.count > 0;
450
339
  }
451
340
  async assertEpoch(sessionId, epoch) {
@@ -459,96 +348,97 @@ class PostgresTx {
459
348
  // --- typed queries -------------------------------------------------------
460
349
  async sessionsWithStatus(statuses) {
461
350
  this.check();
462
- return this.sessionRows(this.sql `status = ANY(${[...statuses]})`);
351
+ const rows = await this.sessions()
352
+ .where(inArray(sessions.status, statuses))
353
+ .orderBy(sessions.id);
354
+ return rows.map((row) => storedSession(row));
463
355
  }
464
356
  async orphanedSessions(now, limit) {
465
357
  this.check();
466
- const sql = this.sql;
467
- return this.sessionRows(sql `status = ANY(${OPEN_SESSION})
468
- AND (owner IS NULL OR owner_expires_at IS NULL OR owner_expires_at <= ${now})`, sql `ORDER BY
469
- CASE WHEN owner IS NULL THEN NULL ELSE owner_expires_at END ASC NULLS FIRST,
470
- id
471
- LIMIT ${limit}`);
358
+ const rows = await this.sessions()
359
+ .where(and(inArray(sessions.status, OPEN_SESSION), or(isNull(sessions.owner), isNull(sessions.ownerExpiresAt), lte(sessions.ownerExpiresAt, now))))
360
+ // Never-owned sessions first, then the longest expired.
361
+ .orderBy(sql `CASE WHEN ${sessions.owner} IS NULL THEN NULL ELSE ${sessions.ownerExpiresAt} END ASC NULLS FIRST`, sessions.id)
362
+ .limit(limit);
363
+ return rows.map((row) => storedSession(row));
472
364
  }
473
365
  async listSessions(filter = {}) {
474
366
  this.check();
475
- const sql = this.sql;
476
- const conditions = [
477
- ...(filter.agentId === undefined ? [] : [sql `agent_id = ${filter.agentId}`]),
478
- ...(filter.ownerUserId === undefined
479
- ? []
480
- : [sql `owner_user_id = ${filter.ownerUserId}`]),
481
- ];
482
- return this.sessionRows(conditions.length === 0
367
+ const rows = await this.sessions()
368
+ .where(and(filter.agentId === undefined ? undefined : eq(sessions.agentId, filter.agentId), filter.ownerUserId === undefined
483
369
  ? undefined
484
- : conditions.reduce((all, next) => sql `${all} AND ${next}`));
370
+ : eq(sessions.ownerUserId, filter.ownerUserId)))
371
+ .orderBy(sessions.id);
372
+ return rows.map((row) => storedSession(row));
485
373
  }
486
374
  async bodies(table) {
487
- const rows = await this.sql `SELECT body FROM ${this.t(table)} ORDER BY id`;
375
+ const rows = await this.db.select({ body: table.body }).from(table).orderBy(table.id);
488
376
  return rows.map((row) => row.body);
489
377
  }
490
378
  async listDefinitions() {
491
379
  this.check();
492
- return this.bodies("definitions");
380
+ return this.bodies(definitions);
493
381
  }
494
382
  async listSandboxes() {
495
383
  this.check();
496
- return this.bodies("sandboxes");
384
+ return this.bodies(sandboxes);
385
+ }
386
+ async actionBodies(where) {
387
+ const rows = await this.db
388
+ .select({ body: actions.body })
389
+ .from(actions)
390
+ .where(where)
391
+ .orderBy(actions.id);
392
+ return rows.map((row) => row.body);
497
393
  }
498
394
  async pendingActions(agentId) {
499
395
  this.check();
500
- const rows = await this.sql `
501
- SELECT body FROM ${this.t("actions")}
502
- WHERE agent_id = ${agentId} AND status = 'pending'
503
- ORDER BY id`;
504
- return rows.map((row) => row.body);
396
+ return this.actionBodies(and(eq(actions.agentId, agentId), eq(actions.status, "pending")));
505
397
  }
506
398
  async deliveringCount(agentId) {
507
399
  this.check();
508
- const [row] = await this.sql `
509
- SELECT count(*)::int AS n FROM ${this.t("actions")}
510
- WHERE agent_id = ${agentId} AND status = 'delivering'`;
400
+ const [row] = await this.db
401
+ .select({ n: count() })
402
+ .from(actions)
403
+ .where(and(eq(actions.agentId, agentId), eq(actions.status, "delivering")));
511
404
  return row.n;
512
405
  }
513
406
  async pendingActionsWithEndpoint(limit) {
514
407
  this.check();
515
- const rows = await this.sql `
516
- SELECT a.body FROM ${this.t("actions")} a
517
- JOIN ${this.t("endpoints")} e ON e.agent_id = a.agent_id
518
- WHERE a.status = 'pending'
519
- ORDER BY a.id
520
- LIMIT ${limit}`;
408
+ const rows = await this.db
409
+ .select({ body: actions.body })
410
+ .from(actions)
411
+ .innerJoin(endpoints, eq(endpoints.agentId, actions.agentId))
412
+ .where(eq(actions.status, "pending"))
413
+ .orderBy(actions.id)
414
+ .limit(limit);
521
415
  return rows.map((row) => row.body);
522
416
  }
523
417
  async expiredDeliveries(now, limit) {
524
418
  this.check();
525
- const rows = await this.sql `
526
- SELECT body FROM ${this.t("actions")}
527
- WHERE status = 'delivering' AND deadline_at IS NOT NULL
528
- AND deadline_at::timestamptz <= ${now}
529
- ORDER BY deadline_at::timestamptz, id
530
- LIMIT ${limit}`;
419
+ const deadline = sql `${actions.deadlineAt}::timestamptz`;
420
+ const rows = await this.db
421
+ .select({ body: actions.body })
422
+ .from(actions)
423
+ .where(and(eq(actions.status, "delivering"), isNotNull(actions.deadlineAt), sql `${deadline} <= ${now.toISOString()}::timestamptz`))
424
+ .orderBy(deadline, actions.id)
425
+ .limit(limit);
531
426
  return rows.map((row) => row.body);
532
427
  }
533
428
  async actionsForSession(sessionId, filter = {}) {
534
429
  this.check();
535
- const sql = this.sql;
536
- const rows = await sql `
537
- SELECT body FROM ${this.t("actions")}
538
- WHERE session_id = ${sessionId}
539
- ${filter.turnId === undefined ? sql `` : sql `AND turn_id = ${filter.turnId}`}
540
- ${filter.statuses === undefined ? sql `` : sql `AND status = ANY(${[...filter.statuses]})`}
541
- ORDER BY id`;
542
- return rows.map((row) => row.body);
430
+ return this.actionBodies(and(eq(actions.sessionId, sessionId), filter.turnId === undefined ? undefined : eq(actions.turnId, filter.turnId), filter.statuses === undefined ? undefined : inArray(actions.status, filter.statuses)));
543
431
  }
544
432
  async actionsWithStatus(statuses, filter = {}) {
545
433
  this.check();
546
- const sql = this.sql;
547
- const rows = await sql `
548
- SELECT body FROM ${this.t("actions")}
549
- WHERE status = ANY(${[...statuses]})
550
- ${filter.kinds === undefined ? sql `` : sql `AND kind = ANY(${[...filter.kinds]})`}
551
- ORDER BY id`;
434
+ return this.actionBodies(and(inArray(actions.status, statuses), filter.kinds === undefined ? undefined : inArray(actions.kind, filter.kinds)));
435
+ }
436
+ async effectBodies(where) {
437
+ const rows = await this.db
438
+ .select({ body: effects.body })
439
+ .from(effects)
440
+ .where(where)
441
+ .orderBy(effects.id);
552
442
  return rows.map((row) => row.body);
553
443
  }
554
444
  async invokingEffects(sessionId) {
@@ -556,35 +446,23 @@ class PostgresTx {
556
446
  }
557
447
  async effectsForSession(sessionId, filter = {}) {
558
448
  this.check();
559
- const sql = this.sql;
560
- const rows = await sql `
561
- SELECT body FROM ${this.t("effects")}
562
- WHERE session_id = ${sessionId}
563
- ${filter.turnId === undefined ? sql `` : sql `AND turn_id = ${filter.turnId}`}
564
- ${filter.statuses === undefined ? sql `` : sql `AND status = ANY(${[...filter.statuses]})`}
565
- ORDER BY id`;
566
- return rows.map((row) => row.body);
449
+ return this.effectBodies(and(eq(effects.sessionId, sessionId), filter.turnId === undefined ? undefined : eq(effects.turnId, filter.turnId), filter.statuses === undefined ? undefined : inArray(effects.status, filter.statuses)));
567
450
  }
568
451
  async effectsForTurn(sessionId, turnId, statuses) {
569
452
  return this.effectsForSession(sessionId, { turnId, statuses });
570
453
  }
571
454
  async effectsWithStatus(statuses, filter = {}) {
572
455
  this.check();
573
- const sql = this.sql;
574
- const rows = await sql `
575
- SELECT body FROM ${this.t("effects")}
576
- WHERE status = ANY(${[...statuses]})
577
- ${filter.kinds === undefined ? sql `` : sql `AND kind = ANY(${[...filter.kinds]})`}
578
- ORDER BY id`;
579
- return rows.map((row) => row.body);
456
+ return this.effectBodies(and(inArray(effects.status, statuses), filter.kinds === undefined ? undefined : inArray(effects.kind, filter.kinds)));
580
457
  }
581
458
  async linkedSessions(workflowSessionId) {
582
459
  this.check();
583
- const rows = await this.sql `
584
- SELECT l.id, l.body AS link, s.body, s.owner, s.epoch, s.owner_expires_at
585
- FROM ${this.t("links")} l JOIN ${this.t("sessions")} s ON s.id = l.id
586
- WHERE l.workflow_session_id = ${workflowSessionId}
587
- ORDER BY l.id`;
460
+ const rows = await this.db
461
+ .select({ id: links.id, link: links.body, ...SESSION })
462
+ .from(links)
463
+ .innerJoin(sessions, eq(sessions.id, links.id))
464
+ .where(eq(links.workflowSessionId, workflowSessionId))
465
+ .orderBy(links.id);
588
466
  return rows.map((row) => ({
589
467
  agentSessionId: row.id,
590
468
  link: row.link,
@@ -593,15 +471,16 @@ class PostgresTx {
593
471
  }
594
472
  async counts() {
595
473
  this.check();
596
- const sql = this.sql;
597
- const [row] = await sql `
474
+ const [row] = await this.db.execute(sql `
598
475
  SELECT
599
- (SELECT count(*) FROM ${this.t("sessions")})::int AS sessions,
600
- (SELECT count(*) FROM ${this.t("sessions")} WHERE status = ANY(${OPEN_SESSION}))::int AS running,
601
- (SELECT count(*) FROM ${this.t("actions")} WHERE status IN ('pending', 'delivering'))::int AS actions,
602
- (SELECT count(*) FROM ${this.t("effects")} WHERE status = 'uncertain')::int AS uncertain,
603
- (SELECT count(*) FROM ${this.t("sandboxes")})::int AS sandboxes,
604
- (SELECT count(*) FROM ${this.t("definitions")})::int AS definitions`;
476
+ (SELECT count(*) FROM ${sessions})::int AS sessions,
477
+ (SELECT count(*) FROM ${sessions}
478
+ WHERE ${sessions.status} IN ('running', 'runnable'))::int AS running,
479
+ (SELECT count(*) FROM ${actions}
480
+ WHERE ${actions.status} IN ('pending', 'delivering'))::int AS actions,
481
+ (SELECT count(*) FROM ${effects} WHERE ${effects.status} = 'uncertain')::int AS uncertain,
482
+ (SELECT count(*) FROM ${sandboxes})::int AS sandboxes,
483
+ (SELECT count(*) FROM ${definitions})::int AS definitions`);
605
484
  return {
606
485
  sessions: row.sessions,
607
486
  runningSessions: row.running,
@@ -614,426 +493,461 @@ class PostgresTx {
614
493
  // --- basin generations --------------------------------------------------
615
494
  async basinGenerations() {
616
495
  this.check();
617
- const [row] = await this.sql `
618
- SELECT basin_generation, retired_generations FROM ${this.t("tenant")}`;
619
- // A schema without its Tenant row (store tests) is at generation 0.
620
- if (!row)
621
- return { current: 0, retired: [] };
622
- return {
623
- current: Number(row.basin_generation),
624
- retired: row.retired_generations.map(Number),
625
- };
496
+ const [row] = await this.db
497
+ .select({ current: tenant.basinGeneration, retired: tenant.retiredGenerations })
498
+ .from(tenant);
499
+ // A database whose Tenant row is not written yet is at generation 0.
500
+ return row ?? { current: 0, retired: [] };
626
501
  }
627
502
  async forgetRetiredGeneration(generation) {
628
503
  this.check();
629
- await this.sql `
630
- UPDATE ${this.t("tenant")}
631
- SET retired_generations = array_remove(retired_generations, ${generation}::int)`;
504
+ await this.db.update(tenant).set({
505
+ retiredGenerations: sql `array_remove(${tenant.retiredGenerations}, ${generation}::int)`,
506
+ });
632
507
  }
633
508
  // --- Action endpoints -----------------------------------------------------
634
509
  async listEndpoints() {
635
510
  this.check();
636
- const rows = await this.sql `SELECT * FROM ${this.t("endpoints")} ORDER BY agent_id`;
511
+ const rows = await this.db.select().from(endpoints).orderBy(endpoints.agentId);
637
512
  return rows.map(endpointRow);
638
513
  }
639
514
  async getEndpoint(agentId) {
640
515
  this.check();
641
- const [row] = await this.sql `
642
- SELECT * FROM ${this.t("endpoints")} WHERE agent_id = ${agentId}`;
516
+ const [row] = await this.db.select().from(endpoints).where(eq(endpoints.agentId, agentId));
643
517
  return row && endpointRow(row);
644
518
  }
645
519
  async putEndpoint(row) {
646
520
  this.check();
647
521
  // Health belongs to a URL: a new URL starts with none.
648
- await this.sql `
649
- INSERT INTO ${this.t("endpoints")} AS e (
650
- agent_id, url, implementation_version, manifest_hash, timeout_ms,
651
- max_concurrent, principal_id, consecutive_failures, created_at, updated_at
652
- ) VALUES (
653
- ${row.agentId}, ${row.url}, ${row.implementationVersion}, ${row.manifestHash ?? null},
654
- ${row.timeoutMs}, ${row.maxConcurrent}, ${row.principalId ?? null}, 0,
655
- ${row.updatedAt}, ${row.updatedAt}
656
- )
657
- ON CONFLICT (agent_id) DO UPDATE SET
658
- url = excluded.url,
659
- implementation_version = excluded.implementation_version,
660
- manifest_hash = excluded.manifest_hash,
661
- timeout_ms = excluded.timeout_ms,
662
- max_concurrent = excluded.max_concurrent,
663
- principal_id = excluded.principal_id,
664
- updated_at = excluded.updated_at,
665
- last_delivery_at = CASE WHEN e.url = excluded.url THEN e.last_delivery_at END,
666
- last_success_at = CASE WHEN e.url = excluded.url THEN e.last_success_at END,
667
- last_error_code = CASE WHEN e.url = excluded.url THEN e.last_error_code END,
668
- last_error_message = CASE WHEN e.url = excluded.url THEN e.last_error_message END,
669
- consecutive_failures =
670
- CASE WHEN e.url = excluded.url THEN e.consecutive_failures ELSE 0 END,
671
- served_implementation_version =
672
- CASE WHEN e.url = excluded.url THEN e.served_implementation_version END,
673
- served_manifest_hash =
674
- CASE WHEN e.url = excluded.url THEN e.served_manifest_hash END`;
522
+ await this.db
523
+ .insert(endpoints)
524
+ .values({
525
+ agentId: row.agentId,
526
+ url: row.url,
527
+ implementationVersion: row.implementationVersion,
528
+ manifestHash: row.manifestHash ?? null,
529
+ timeoutMs: row.timeoutMs,
530
+ maxConcurrent: row.maxConcurrent,
531
+ principalId: row.principalId ?? null,
532
+ consecutiveFailures: 0,
533
+ createdAt: row.updatedAt,
534
+ updatedAt: row.updatedAt,
535
+ })
536
+ .onConflictDoUpdate({
537
+ target: endpoints.agentId,
538
+ set: {
539
+ url: excluded("url"),
540
+ implementationVersion: excluded("implementation_version"),
541
+ manifestHash: excluded("manifest_hash"),
542
+ timeoutMs: excluded("timeout_ms"),
543
+ maxConcurrent: excluded("max_concurrent"),
544
+ principalId: excluded("principal_id"),
545
+ updatedAt: excluded("updated_at"),
546
+ lastDeliveryAt: sameUrl(sql `${endpoints.lastDeliveryAt}`),
547
+ lastSuccessAt: sameUrl(sql `${endpoints.lastSuccessAt}`),
548
+ lastErrorCode: sameUrl(sql `${endpoints.lastErrorCode}`),
549
+ lastErrorMessage: sameUrl(sql `${endpoints.lastErrorMessage}`),
550
+ consecutiveFailures: sameUrl(sql `${endpoints.consecutiveFailures}`, sql `0`),
551
+ servedImplementationVersion: sameUrl(sql `${endpoints.servedImplementationVersion}`),
552
+ servedManifestHash: sameUrl(sql `${endpoints.servedManifestHash}`),
553
+ },
554
+ });
675
555
  }
676
556
  async deleteEndpoint(agentId) {
677
557
  this.check();
678
- await this.sql `DELETE FROM ${this.t("endpoints")} WHERE agent_id = ${agentId}`;
558
+ await this.db.delete(endpoints).where(eq(endpoints.agentId, agentId));
679
559
  }
680
560
  async recordEndpointHealth(agentId, update) {
681
561
  this.check();
682
- const table = this.t("endpoints");
683
- switch (update.kind) {
684
- case "success":
685
- await this.sql `
686
- UPDATE ${table} SET last_delivery_at = ${update.at}, last_success_at = ${update.at},
687
- consecutive_failures = 0, last_error_code = NULL, last_error_message = NULL
688
- WHERE agent_id = ${agentId}`;
689
- return;
690
- case "failure":
691
- await this.sql `
692
- UPDATE ${table} SET last_delivery_at = ${update.at}, last_error_code = ${update.code},
693
- last_error_message = ${update.message},
694
- consecutive_failures = consecutive_failures + 1
695
- WHERE agent_id = ${agentId}`;
696
- return;
697
- case "served":
698
- await this.sql `
699
- UPDATE ${table} SET served_implementation_version = ${update.implementationVersion},
700
- served_manifest_hash = ${update.manifestHash ?? null}
701
- WHERE agent_id = ${agentId}`;
702
- return;
703
- }
562
+ const set = update.kind === "success"
563
+ ? {
564
+ lastDeliveryAt: update.at,
565
+ lastSuccessAt: update.at,
566
+ consecutiveFailures: 0,
567
+ lastErrorCode: null,
568
+ lastErrorMessage: null,
569
+ }
570
+ : update.kind === "failure"
571
+ ? {
572
+ lastDeliveryAt: update.at,
573
+ lastErrorCode: update.code,
574
+ lastErrorMessage: update.message,
575
+ consecutiveFailures: sql `${endpoints.consecutiveFailures} + 1`,
576
+ }
577
+ : {
578
+ servedImplementationVersion: update.implementationVersion,
579
+ servedManifestHash: update.manifestHash ?? null,
580
+ };
581
+ await this.db.update(endpoints).set(set).where(eq(endpoints.agentId, agentId));
704
582
  }
705
583
  // --- principals ----------------------------------------------------------
706
584
  async insertPrincipal(row) {
707
585
  this.check();
708
- await this.sql `
709
- INSERT INTO ${this.t("principals")} (id, role, token_hash, idempotency_key, created_at)
710
- VALUES (${row.id}, ${row.role}, ${row.tokenHash}, ${row.idempotencyKey}, ${row.createdAt})`;
586
+ await this.db.insert(principals).values(row);
711
587
  }
712
588
  async principalByTokenHash(tokenHash) {
713
589
  this.check();
714
- const [row] = await this.sql `
715
- SELECT * FROM ${this.t("principals")} WHERE token_hash = ${tokenHash}`;
716
- return row && principalRow(row);
590
+ const [row] = await this.db
591
+ .select()
592
+ .from(principals)
593
+ .where(eq(principals.tokenHash, tokenHash));
594
+ return row;
717
595
  }
718
596
  async principalById(id) {
719
597
  this.check();
720
- const [row] = await this.sql `
721
- SELECT * FROM ${this.t("principals")} WHERE id = ${id}`;
722
- return row && principalRow(row);
598
+ const [row] = await this.db.select().from(principals).where(eq(principals.id, id));
599
+ return row;
723
600
  }
724
601
  async applicationTokenHashes() {
725
602
  this.check();
726
- const rows = await this.sql `
727
- SELECT token_hash FROM ${this.t("principals")}
728
- WHERE role = 'application' ORDER BY id`;
729
- return rows.map((row) => row.token_hash);
603
+ const rows = await this.db
604
+ .select({ tokenHash: principals.tokenHash })
605
+ .from(principals)
606
+ .where(eq(principals.role, "application"))
607
+ .orderBy(principals.id);
608
+ return rows.map((row) => row.tokenHash);
730
609
  }
731
610
  // --- vault ---------------------------------------------------------------
732
611
  async insertVault(row) {
733
612
  this.check();
734
- await this.sql `
735
- INSERT INTO ${this.t("vaults")} (id, name, owner_user_id, metadata_json, created_at, scope)
736
- VALUES (${row.id}, ${row.name}, ${row.ownerUserId}, ${row.metadataJson},
737
- ${row.createdAt}, ${row.scope})`;
613
+ await this.db.insert(vaults).values(row);
738
614
  }
739
615
  async getVault(id) {
740
616
  this.check();
741
- const [row] = await this.sql `SELECT * FROM ${this.t("vaults")} WHERE id = ${id}`;
742
- return row && vaultRow(row);
617
+ const [row] = await this.db.select().from(vaults).where(eq(vaults.id, id));
618
+ return row;
743
619
  }
744
620
  async vaultsByOwner(ownerUserId) {
745
621
  this.check();
746
- const rows = await this.sql `
747
- SELECT * FROM ${this.t("vaults")}
748
- WHERE owner_user_id = ${ownerUserId} AND scope = 'user'
749
- ORDER BY created_at, id`;
750
- return rows.map(vaultRow);
622
+ return this.db
623
+ .select()
624
+ .from(vaults)
625
+ .where(and(eq(vaults.ownerUserId, ownerUserId), eq(vaults.scope, "user")))
626
+ .orderBy(vaults.createdAt, vaults.id);
751
627
  }
752
628
  async updateVaultMetadata(id, metadataJson) {
753
629
  this.check();
754
- await this.sql `
755
- UPDATE ${this.t("vaults")} SET metadata_json = ${metadataJson} WHERE id = ${id}`;
630
+ await this.db.update(vaults).set({ metadataJson }).where(eq(vaults.id, id));
756
631
  }
757
632
  async deleteVault(id) {
758
633
  this.check();
759
634
  // Credentials go with it (ON DELETE CASCADE).
760
- await this.sql `DELETE FROM ${this.t("vaults")} WHERE id = ${id}`;
635
+ await this.db.delete(vaults).where(eq(vaults.id, id));
761
636
  }
762
637
  async insertCredential(row) {
763
638
  this.check();
764
- await this.sql `
765
- INSERT INTO ${this.t("vault_credentials")} (
766
- id, vault_id, name, type, binding_json, expires_at, created_at,
767
- rotated_at, kek_id, nonce, ciphertext, wrapped_dek
768
- ) VALUES (
769
- ${row.id}, ${row.vaultId}, ${row.name}, ${row.type}, ${row.bindingJson},
770
- ${row.expiresAt}, ${row.createdAt}, ${row.rotatedAt}, ${row.kekId},
771
- ${bytes(row.nonce)}, ${bytes(row.ciphertext)}, ${bytes(row.wrappedDek)}
772
- )`;
639
+ await this.db.insert(vaultCredentials).values(row);
773
640
  }
774
641
  async getCredential(id) {
775
642
  this.check();
776
- const [row] = await this.sql `
777
- SELECT * FROM ${this.t("vault_credentials")} WHERE id = ${id}`;
778
- return row && credentialRow(row);
643
+ const [row] = await this.db
644
+ .select()
645
+ .from(vaultCredentials)
646
+ .where(eq(vaultCredentials.id, id));
647
+ return row;
779
648
  }
780
649
  async credentialsForVault(vaultId, filter = {}) {
781
650
  this.check();
782
- const sql = this.sql;
783
- const rows = await sql `
784
- SELECT * FROM ${this.t("vault_credentials")}
785
- WHERE vault_id = ${vaultId}
786
- ${filter.type === undefined ? sql `` : sql `AND type = ${filter.type}`}
787
- ORDER BY created_at, id`;
788
- return rows.map(credentialRow);
651
+ return this.db
652
+ .select()
653
+ .from(vaultCredentials)
654
+ .where(and(eq(vaultCredentials.vaultId, vaultId), filter.type === undefined ? undefined : eq(vaultCredentials.type, filter.type)))
655
+ .orderBy(vaultCredentials.createdAt, vaultCredentials.id);
789
656
  }
790
657
  async updateCredential(vaultId, id, patch) {
791
658
  this.check();
792
- const sql = this.sql;
793
- const columns = {};
794
- for (const [field, column] of Object.entries(CREDENTIAL_COLUMNS)) {
795
- const value = patch[field];
796
- if (value === undefined)
797
- continue;
798
- columns[column] = value instanceof Uint8Array ? bytes(value) : value;
799
- }
800
- if (Object.keys(columns).length === 0) {
801
- const rows = await sql `
802
- SELECT 1 FROM ${this.t("vault_credentials")}
803
- WHERE vault_id = ${vaultId} AND id = ${id}`;
659
+ const where = and(eq(vaultCredentials.vaultId, vaultId), eq(vaultCredentials.id, id));
660
+ if (Object.values(patch).every((value) => value === undefined)) {
661
+ const rows = await this.db
662
+ .select({ id: vaultCredentials.id })
663
+ .from(vaultCredentials)
664
+ .where(where);
804
665
  return rows.length > 0;
805
666
  }
806
- const result = await sql `
807
- UPDATE ${this.t("vault_credentials")} SET ${sql(columns)}
808
- WHERE vault_id = ${vaultId} AND id = ${id}`;
667
+ const result = await this.db.update(vaultCredentials).set(patch).where(where);
809
668
  return result.count > 0;
810
669
  }
811
670
  async deleteCredential(vaultId, id) {
812
671
  this.check();
813
- const result = await this.sql `
814
- DELETE FROM ${this.t("vault_credentials")}
815
- WHERE vault_id = ${vaultId} AND id = ${id}`;
672
+ const result = await this.db
673
+ .delete(vaultCredentials)
674
+ .where(and(eq(vaultCredentials.vaultId, vaultId), eq(vaultCredentials.id, id)));
816
675
  return result.count > 0;
817
676
  }
818
677
  async countCredentials() {
819
678
  this.check();
820
- const [row] = await this.sql `
821
- SELECT count(*)::int AS n FROM ${this.t("vault_credentials")}`;
679
+ const [row] = await this.db.select({ n: count() }).from(vaultCredentials);
822
680
  return row.n;
823
681
  }
824
682
  async insertVaultAudit(row) {
825
683
  this.check();
826
- await this.sql `
827
- INSERT INTO ${this.t("vault_audit")} (
828
- id, at, actor, action, vault_id, credential_id, session_id, target, outcome
829
- ) VALUES (
830
- ${row.id}, ${row.at}, ${row.actor}, ${row.action}, ${row.vaultId},
831
- ${row.credentialId}, ${row.sessionId}, ${row.target}, ${row.outcome}
832
- )`;
684
+ await this.db.insert(vaultAudit).values(row);
833
685
  }
834
686
  async vaultAudit(filter = {}) {
835
687
  this.check();
836
- const sql = this.sql;
837
- const rows = await sql `
838
- SELECT * FROM ${this.t("vault_audit")}
839
- ${filter.vaultId === undefined ? sql `` : sql `WHERE vault_id = ${filter.vaultId}`}
840
- ORDER BY ord
841
- ${filter.limit === undefined ? sql `` : sql `LIMIT ${filter.limit}`}`;
842
- return rows.map(auditRow);
688
+ const query = this.db
689
+ .select(AUDIT)
690
+ .from(vaultAudit)
691
+ .where(filter.vaultId === undefined ? undefined : eq(vaultAudit.vaultId, filter.vaultId))
692
+ .orderBy(vaultAudit.ord)
693
+ .$dynamic();
694
+ return filter.limit === undefined ? query : query.limit(filter.limit);
843
695
  }
844
696
  async getVaultIdempotency(id) {
845
697
  this.check();
846
- const [row] = await this.sql `
847
- SELECT id, body_hash, response FROM ${this.t("vault_idempotency")} WHERE id = ${id}`;
848
- return row && { id: row.id, bodyHash: row.body_hash, response: row.response };
698
+ const [row] = await this.db
699
+ .select()
700
+ .from(vaultIdempotency)
701
+ .where(eq(vaultIdempotency.id, id));
702
+ return row;
849
703
  }
850
704
  async insertVaultIdempotency(row) {
851
705
  this.check();
852
- await this.sql `
853
- INSERT INTO ${this.t("vault_idempotency")} (id, body_hash, response)
854
- VALUES (${row.id}, ${row.bodyHash}, ${row.response})`;
706
+ await this.db.insert(vaultIdempotency).values(row);
855
707
  }
856
708
  // --- subject tokens ------------------------------------------------------
857
709
  async lockSigningKeys() {
858
710
  this.check();
859
- const key = `${this.schema}.signing_keys`;
860
- await this.sql `SELECT pg_advisory_xact_lock(hashtext(${key}))`;
711
+ await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.signing_keys'))`);
861
712
  }
862
713
  async insertSigningKey(row) {
863
714
  this.check();
864
- await this.sql `
865
- INSERT INTO ${this.t("signing_keys")} (
866
- id, state, alg, public_jwk, kek_id, nonce, ciphertext, wrapped_dek,
867
- created_at, activated_at, retired_at, revoked_at
868
- ) VALUES (
869
- ${row.id}, ${row.state}, ${row.alg}, ${row.publicJwk}, ${row.kekId},
870
- ${bytes(row.nonce)}, ${bytes(row.ciphertext)}, ${bytes(row.wrappedDek)},
871
- ${row.createdAt}, ${row.activatedAt}, ${row.retiredAt}, ${row.revokedAt}
872
- )`;
715
+ await this.db.insert(signingKeys).values(row);
873
716
  }
874
717
  async signingKey(id) {
875
718
  this.check();
876
- const [row] = await this.sql `
877
- SELECT * FROM ${this.t("signing_keys")} WHERE id = ${id}`;
878
- return row && signingKeyRow(row);
719
+ const [row] = await this.db.select().from(signingKeys).where(eq(signingKeys.id, id));
720
+ return row;
879
721
  }
880
722
  async signingKeys(states) {
881
723
  this.check();
882
- const sql = this.sql;
883
- const rows = await sql `
884
- SELECT * FROM ${this.t("signing_keys")}
885
- ${states === undefined ? sql `` : sql `WHERE state = ANY(${states})`}
886
- ORDER BY created_at, id`;
887
- return rows.map(signingKeyRow);
724
+ return this.db
725
+ .select()
726
+ .from(signingKeys)
727
+ .where(states === undefined ? undefined : inArray(signingKeys.state, states))
728
+ .orderBy(signingKeys.createdAt, signingKeys.id);
888
729
  }
889
730
  async setSigningKeyState(id, from, to, at) {
890
731
  this.check();
891
- const sql = this.sql;
892
- const column = SIGNING_KEY_STAMP[to];
893
- const rows = await sql `
894
- UPDATE ${this.t("signing_keys")}
895
- SET state = ${to}${column ? sql `, ${sql(column)} = ${at}` : sql ``}
896
- WHERE id = ${id} AND state = ${from}
897
- RETURNING id`;
732
+ const stamp = SIGNING_KEY_STAMP[to];
733
+ const rows = await this.db
734
+ .update(signingKeys)
735
+ .set({ state: to, ...(stamp ? { [stamp]: at } : {}) })
736
+ .where(and(eq(signingKeys.id, id), eq(signingKeys.state, from)))
737
+ .returning({ id: signingKeys.id });
898
738
  return rows.length === 1;
899
739
  }
900
740
  async countSigningKeys() {
901
741
  this.check();
902
- const [row] = await this.sql `
903
- SELECT count(*)::int AS n FROM ${this.t("signing_keys")}`;
742
+ const [row] = await this.db.select({ n: count() }).from(signingKeys);
904
743
  return row.n;
905
744
  }
906
745
  async subjectEpoch(subject) {
907
746
  this.check();
908
- const [row] = await this.sql `
909
- SELECT epoch FROM ${this.t("subject_epochs")} WHERE subject = ${subject}`;
910
- return row ? Number(row.epoch) : 0;
747
+ const [row] = await this.db
748
+ .select({ epoch: subjectEpochs.epoch })
749
+ .from(subjectEpochs)
750
+ .where(eq(subjectEpochs.subject, subject));
751
+ return row?.epoch ?? 0;
911
752
  }
912
753
  async subjectEpochs(subjects) {
913
754
  this.check();
914
755
  if (subjects.length === 0)
915
756
  return new Map();
916
- const rows = await this.sql `
917
- SELECT subject, epoch FROM ${this.t("subject_epochs")}
918
- WHERE subject = ANY(${subjects})`;
919
- return new Map(rows.map((row) => [row.subject, Number(row.epoch)]));
757
+ const rows = await this.db
758
+ .select({ subject: subjectEpochs.subject, epoch: subjectEpochs.epoch })
759
+ .from(subjectEpochs)
760
+ .where(inArray(subjectEpochs.subject, subjects));
761
+ return new Map(rows.map((row) => [row.subject, row.epoch]));
920
762
  }
921
763
  async bumpSubjectEpoch(subject, at) {
922
764
  this.check();
923
- const [row] = await this.sql `
924
- INSERT INTO ${this.t("subject_epochs")} (subject, epoch, revoked_at)
925
- VALUES (${subject}, 1, ${at})
926
- ON CONFLICT (subject) DO UPDATE
927
- SET epoch = ${this.t("subject_epochs")}.epoch + 1, revoked_at = excluded.revoked_at
928
- RETURNING epoch`;
929
- return Number(row.epoch);
765
+ const [row] = await this.db
766
+ .insert(subjectEpochs)
767
+ .values({ subject, epoch: 1, revokedAt: at })
768
+ .onConflictDoUpdate({
769
+ target: subjectEpochs.subject,
770
+ set: { epoch: sql `${subjectEpochs.epoch} + 1`, revokedAt: excluded("revoked_at") },
771
+ })
772
+ .returning({ epoch: subjectEpochs.epoch });
773
+ return row.epoch;
930
774
  }
931
775
  async lockSubjectUsage(initial) {
932
776
  this.check();
933
- await this.sql `
934
- INSERT INTO ${this.t("subject_usage")} (subject, turn_tokens, refilled_at)
935
- VALUES (${initial.subject}, ${initial.turnTokens}, ${initial.refilledAt})
936
- ON CONFLICT (subject) DO NOTHING`;
937
- const [row] = await this.sql `
938
- SELECT subject, turn_tokens, refilled_at FROM ${this.t("subject_usage")}
939
- WHERE subject = ${initial.subject} FOR UPDATE`;
940
- return {
941
- subject: row.subject,
942
- turnTokens: Number(row.turn_tokens),
943
- refilledAt: row.refilled_at,
944
- };
777
+ await this.db.insert(subjectUsage).values(initial).onConflictDoNothing();
778
+ const [row] = await this.db
779
+ .select()
780
+ .from(subjectUsage)
781
+ .where(eq(subjectUsage.subject, initial.subject))
782
+ .for("update");
783
+ return row;
945
784
  }
946
785
  async putSubjectUsage(row) {
947
786
  this.check();
948
- await this.sql `
949
- INSERT INTO ${this.t("subject_usage")} (subject, turn_tokens, refilled_at)
950
- VALUES (${row.subject}, ${row.turnTokens}, ${row.refilledAt})
951
- ON CONFLICT (subject) DO UPDATE
952
- SET turn_tokens = excluded.turn_tokens, refilled_at = excluded.refilled_at`;
787
+ await this.db
788
+ .insert(subjectUsage)
789
+ .values(row)
790
+ .onConflictDoUpdate({
791
+ target: subjectUsage.subject,
792
+ set: { turnTokens: excluded("turn_tokens"), refilledAt: excluded("refilled_at") },
793
+ });
953
794
  }
954
795
  async countOwnerSessions(ownerUserId, statuses) {
955
796
  this.check();
956
- const [row] = await this.sql `
957
- SELECT count(*)::int AS n FROM ${this.t("sessions")}
958
- WHERE owner_user_id = ${ownerUserId} AND status = ANY(${statuses})`;
797
+ const [row] = await this.db
798
+ .select({ n: count() })
799
+ .from(sessions)
800
+ .where(and(eq(sessions.ownerUserId, ownerUserId), inArray(sessions.status, statuses)));
959
801
  return row.n;
960
802
  }
961
803
  // --- publishable keys ----------------------------------------------------
962
804
  async insertPublishableKey(row) {
963
805
  this.check();
964
- await this.sql `
965
- INSERT INTO ${this.t("publishable_keys")} (id, key, name, origins_json, created_at, revoked_at)
966
- VALUES (${row.id}, ${row.key}, ${row.name}, ${row.originsJson}, ${row.createdAt}, ${row.revokedAt})`;
806
+ await this.db.insert(publishableKeys).values(row);
967
807
  }
968
808
  async publishableKeyByKey(key) {
969
809
  this.check();
970
- const [row] = await this.sql `
971
- SELECT * FROM ${this.t("publishable_keys")} WHERE key = ${key}`;
972
- return row && publishableKeyRow(row);
810
+ const [row] = await this.db
811
+ .select()
812
+ .from(publishableKeys)
813
+ .where(eq(publishableKeys.key, key));
814
+ return row;
973
815
  }
974
816
  async publishableKey(id) {
975
817
  this.check();
976
- const [row] = await this.sql `
977
- SELECT * FROM ${this.t("publishable_keys")} WHERE id = ${id}`;
978
- return row && publishableKeyRow(row);
818
+ const [row] = await this.db
819
+ .select()
820
+ .from(publishableKeys)
821
+ .where(eq(publishableKeys.id, id));
822
+ return row;
979
823
  }
980
824
  async publishableKeys() {
981
825
  this.check();
982
- const rows = await this.sql `
983
- SELECT * FROM ${this.t("publishable_keys")} ORDER BY created_at, id`;
984
- return rows.map(publishableKeyRow);
826
+ return this.db
827
+ .select()
828
+ .from(publishableKeys)
829
+ .orderBy(publishableKeys.createdAt, publishableKeys.id);
985
830
  }
986
831
  async updatePublishableKey(id, patch) {
987
832
  this.check();
988
- const sql = this.sql;
989
- const sets = [
990
- ...(patch.originsJson === undefined ? [] : [sql `origins_json = ${patch.originsJson}`]),
991
- ...(patch.revokedAt === undefined ? [] : [sql `revoked_at = ${patch.revokedAt}`]),
992
- ];
993
- if (sets.length === 0)
833
+ if (patch.originsJson === undefined && patch.revokedAt === undefined)
994
834
  return (await this.publishableKey(id)) !== undefined;
995
- const rows = await sql `
996
- UPDATE ${this.t("publishable_keys")}
997
- SET ${sets.reduce((all, next) => sql `${all}, ${next}`)}
998
- WHERE id = ${id} RETURNING id`;
835
+ const rows = await this.db
836
+ .update(publishableKeys)
837
+ .set(patch)
838
+ .where(eq(publishableKeys.id, id))
839
+ .returning({ id: publishableKeys.id });
999
840
  return rows.length === 1;
1000
841
  }
842
+ // --- model usage ---------------------------------------------------------
843
+ async recordModelUsage(row) {
844
+ this.check();
845
+ const [inserted] = await this.db
846
+ .insert(modelUsage)
847
+ .values({
848
+ ...row,
849
+ duplicate: sql `EXISTS (SELECT 1 FROM ${modelUsage} WHERE ${modelUsage.effectKey} = ${row.effectKey})`,
850
+ })
851
+ .returning({ duplicate: modelUsage.duplicate });
852
+ return { ...row, duplicate: inserted.duplicate };
853
+ }
854
+ async modelUsageTotals(query) {
855
+ this.check();
856
+ const [row] = await this.db
857
+ .select({
858
+ calls: count(),
859
+ tokens: sql `coalesce(sum(${modelUsage.totalTokens}), 0)::bigint`.mapWith(Number),
860
+ costUsd: sql `coalesce(sum(${modelUsage.costUsd}), 0)::double precision`.mapWith(Number),
861
+ })
862
+ .from(modelUsage)
863
+ .where(and(query.scope === "agent" ? eq(modelUsage.agentId, query.id ?? "") : undefined, query.scope === "turn" ? eq(modelUsage.turnId, query.id ?? "") : undefined, query.since !== undefined ? gte(modelUsage.createdAt, query.since) : undefined));
864
+ return row;
865
+ }
866
+ async listModelBudgets() {
867
+ this.check();
868
+ return this.db
869
+ .select()
870
+ .from(modelBudgets)
871
+ .orderBy(sql `${modelBudgets.scope} COLLATE "C"`, modelBudgets.scopeId);
872
+ }
873
+ async putModelBudgets(rows) {
874
+ this.check();
875
+ await this.db.delete(modelBudgets);
876
+ if (rows.length > 0)
877
+ await this.db.insert(modelBudgets).values([...rows]);
878
+ }
879
+ // --- tool crossings --------------------------------------------------------
880
+ async toolCrossing(key) {
881
+ this.check();
882
+ const [row] = await this.db.select().from(toolCrossings).where(eq(toolCrossings.key, key));
883
+ return row;
884
+ }
885
+ async startToolCrossing(row) {
886
+ this.check();
887
+ const inserted = await this.db
888
+ .insert(toolCrossings)
889
+ .values({ ...row, settledAt: null, answer: null })
890
+ .onConflictDoNothing()
891
+ .returning({ key: toolCrossings.key });
892
+ return inserted.length === 1;
893
+ }
894
+ async settleToolCrossing(key, answer, settledAt) {
895
+ this.check();
896
+ await this.db
897
+ .update(toolCrossings)
898
+ .set({ answer, settledAt })
899
+ .where(eq(toolCrossings.key, key));
900
+ }
901
+ async pruneToolCrossings(before) {
902
+ this.check();
903
+ const deleted = await this.db
904
+ .delete(toolCrossings)
905
+ .where(lt(toolCrossings.settledAt, before))
906
+ .returning({ key: toolCrossings.key });
907
+ return deleted.length;
908
+ }
1001
909
  // --- settings ------------------------------------------------------------
1002
910
  async getSetting(key) {
1003
911
  this.check();
1004
- const [row] = await this.sql `
1005
- SELECT value FROM ${this.t("tenant_settings")} WHERE key = ${key}`;
912
+ const [row] = await this.db
913
+ .select({ value: tenantSettings.value })
914
+ .from(tenantSettings)
915
+ .where(eq(tenantSettings.key, key));
1006
916
  return row?.value;
1007
917
  }
1008
918
  async putSetting(key, value) {
1009
919
  this.check();
1010
- await this.sql `
1011
- INSERT INTO ${this.t("tenant_settings")} (key, value) VALUES (${key}, ${value})
1012
- ON CONFLICT (key) DO UPDATE SET value = excluded.value`;
920
+ await this.db
921
+ .insert(tenantSettings)
922
+ .values({ key, value })
923
+ .onConflictDoUpdate({ target: tenantSettings.key, set: { value: excluded("value") } });
1013
924
  }
1014
925
  // --- reset ---------------------------------------------------------------
1015
926
  async reset(scope) {
1016
927
  this.check();
1017
- const sql = this.sql;
928
+ const db = this.db;
1018
929
  if (scope === "sessions" || scope === "all") {
1019
930
  for (const table of SESSION_TABLES)
1020
- await sql `DELETE FROM ${this.t(table)}`;
1021
- await sql `DELETE FROM ${this.t("subject_usage")}`;
931
+ await db.delete(table);
932
+ await db.delete(subjectUsage);
933
+ await db.delete(toolCrossings);
1022
934
  // The record goes with the sessions, and the Tenant moves to a new basin: the ids it
1023
935
  // frees start again in an empty one (Durable Streams §8.1).
1024
- await sql `DELETE FROM ${sql(SESSION_EVENTS)} WHERE tenant_id = ${this.tenantId}`;
1025
- await sql `DELETE FROM ${sql(LOG_HEADS)} WHERE tenant_id = ${this.tenantId}`;
1026
- await sql `
1027
- UPDATE ${this.t("tenant")}
1028
- SET retired_generations = array_append(retired_generations, basin_generation),
1029
- basin_generation = basin_generation + 1`;
936
+ await db.delete(sessionEvents);
937
+ await db.delete(sessionLogHeads);
938
+ await db.update(tenant).set({
939
+ retiredGenerations: sql `array_append(${tenant.retiredGenerations}, ${tenant.basinGeneration})`,
940
+ basinGeneration: sql `${tenant.basinGeneration} + 1`,
941
+ });
1030
942
  }
1031
943
  if (scope === "sandboxes" || scope === "all")
1032
- await sql `DELETE FROM ${this.t("sandboxes")}`;
944
+ await db.delete(sandboxes);
1033
945
  if (scope === "all") {
1034
- await sql `DELETE FROM ${this.t("definitions")}`;
1035
- await sql `DELETE FROM ${this.t("endpoints")}`;
1036
- await sql `DELETE FROM ${this.t("vaults")} WHERE scope <> 'host'`;
946
+ await db.delete(definitions);
947
+ await db.delete(endpoints);
948
+ await db.delete(modelUsage);
949
+ await db.delete(modelBudgets);
950
+ await db.delete(vaults).where(ne(vaults.scope, "host"));
1037
951
  }
1038
952
  }
1039
953
  }