@nylorun/runtime 0.9.0-beta → 0.11.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.
- package/CHANGELOG.md +81 -0
- package/README.md +110 -37
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/media.d.ts +2 -2
- package/dist/adapters/media.js +2 -2
- package/dist/adapters/observe.d.ts +2 -1
- package/dist/adapters/observe.js +2 -3
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/assets.d.ts +2 -2
- package/dist/assets.js +5 -3
- package/dist/core/executors.d.ts +15 -3
- package/dist/core/executors.js +40 -5
- package/dist/core/flow-host.d.ts +225 -0
- package/dist/core/flow-host.js +518 -0
- package/dist/core/limits.d.ts +40 -0
- package/dist/core/limits.js +76 -0
- package/dist/core/runtime.d.ts +5 -90
- package/dist/core/runtime.js +3 -1447
- package/dist/core/sandbox-routes.d.ts +40 -0
- package/dist/core/sandbox-routes.js +106 -0
- package/dist/core/turn-manifest.d.ts +50 -0
- package/dist/core/turn-manifest.js +66 -0
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +17 -0
- package/dist/host/config-for.js +27 -0
- package/dist/host/config.d.ts +16 -0
- package/dist/host/create-host.d.ts +56 -0
- package/dist/host/create-host.js +420 -0
- package/dist/host/environment.d.ts +13 -0
- package/dist/host/environment.js +53 -0
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +39 -0
- package/dist/host/http.js +142 -0
- package/dist/host/index.d.ts +8 -0
- package/dist/host/index.js +7 -0
- package/dist/host/logger.d.ts +3 -0
- package/dist/host/logger.js +18 -0
- package/dist/host/main.d.ts +3 -0
- package/dist/host/main.js +201 -0
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -2
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/mcp/connect.d.ts +1 -0
- package/dist/mcp/connect.js +2 -0
- package/dist/mcp/pool.d.ts +3 -1
- package/dist/mcp/pool.js +2 -1
- package/dist/mcp/stdio.d.ts +2 -0
- package/dist/mcp/stdio.js +3 -1
- package/dist/model/auth-store.d.ts +1 -1
- package/dist/model/auth-store.js +2 -2
- package/dist/model/models.d.ts +4 -0
- package/dist/model/models.js +22 -26
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +6 -6
- package/dist/model/settings.d.ts +5 -1
- package/dist/model/settings.js +6 -2
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +5 -4
- package/dist/sandbox/select.js +18 -13
- package/dist/sandbox/share.d.ts +23 -0
- package/dist/sandbox/share.js +58 -0
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +643 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +75 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +790 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +207 -0
- package/dist/store/types.d.ts +440 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +11 -0
- package/dist/tenant/auth.js +34 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +384 -0
- package/dist/tenant/context.d.ts +140 -0
- package/dist/tenant/context.js +40 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +539 -0
- package/dist/tenant/envelope.d.ts +7 -0
- package/dist/tenant/envelope.js +22 -0
- package/dist/tenant/ephemeral.d.ts +42 -0
- package/dist/tenant/ephemeral.js +224 -0
- package/dist/tenant/http.d.ts +32 -0
- package/dist/tenant/http.js +51 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/logger.d.ts +9 -0
- package/dist/tenant/logger.js +25 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +38 -0
- package/dist/tenant/module.js +319 -0
- package/dist/tenant/paths.d.ts +28 -0
- package/dist/tenant/paths.js +91 -0
- package/dist/tenant/pool.d.ts +10 -0
- package/dist/tenant/pool.js +54 -0
- package/dist/tenant/principals.d.ts +13 -0
- package/dist/tenant/principals.js +37 -0
- package/dist/tenant/quarantine-error.d.ts +6 -0
- package/dist/tenant/quarantine-error.js +9 -0
- package/dist/tenant/quarantine.d.ts +27 -0
- package/dist/tenant/quarantine.js +72 -0
- package/dist/tenant/reset.d.ts +22 -0
- package/dist/tenant/reset.js +81 -0
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +126 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +142 -0
- package/dist/tenant/runtime.d.ts +81 -0
- package/dist/tenant/runtime.js +298 -0
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +34 -0
- package/dist/tenant/sessions.js +149 -0
- package/dist/tenant/status.d.ts +37 -0
- package/dist/tenant/status.js +130 -0
- package/dist/tenant/store-memory.d.ts +16 -0
- package/dist/tenant/store-memory.js +111 -0
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/types.d.ts +205 -0
- package/dist/tenant/types.js +23 -0
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/kek.d.ts +5 -2
- package/dist/vault/kek.js +7 -10
- package/dist/vault/service.d.ts +37 -31
- package/dist/vault/service.js +439 -359
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -11
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/main.js +0 -35
- package/dist/core/store.d.ts +0 -30
- package/dist/core/store.js +0 -202
- /package/dist/{core/main.d.ts → host/config.js} +0 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Postgres Session Store (architecture §12.2): one Tenant schema, one
|
|
3
|
+
* `SessionStore`. See `store/types.ts` for the invariants it keeps.
|
|
4
|
+
*
|
|
5
|
+
* ## Transactions
|
|
6
|
+
*
|
|
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.
|
|
14
|
+
*
|
|
15
|
+
* ## Lock ordering
|
|
16
|
+
*
|
|
17
|
+
* `lockSession`, `event`, `takeOwnership` and `assertEpoch` take the session
|
|
18
|
+
* row lock (`SELECT … FOR UPDATE` or `UPDATE`), held until the transaction
|
|
19
|
+
* ends. To stay deadlock-free:
|
|
20
|
+
*
|
|
21
|
+
* 1. A session-scoped transaction locks its session row before writing any
|
|
22
|
+
* other row of that session (effects, actions, checkpoints, outbox).
|
|
23
|
+
* 2. A transaction that touches several sessions locks them in ascending id
|
|
24
|
+
* order, with `lockSessions` (`./locking.ts`), before writing any of them.
|
|
25
|
+
*
|
|
26
|
+
* Serialization failures and deadlocks are not retried here: with the order
|
|
27
|
+
* above, READ COMMITTED transactions do not raise them. A `40P01` means a
|
|
28
|
+
* caller broke the order, and it surfaces as the rejection of `tx`.
|
|
29
|
+
*
|
|
30
|
+
* ## Values
|
|
31
|
+
*
|
|
32
|
+
* Document and outbox bodies are `json`, stored as the text `JSON.stringify`
|
|
33
|
+
* wrote, so every string round-trips, including U+0000 and unpaired
|
|
34
|
+
* surrogates that `jsonb` rejects (see `migrations/001_initial.ts`), and key
|
|
35
|
+
* order is kept. Ids and ISO timestamps compared as text use `COLLATE "C"`.
|
|
36
|
+
* Lease times in action bodies are compared as `timestamptz`.
|
|
37
|
+
*/
|
|
38
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
39
|
+
import { randomUUID } from "node:crypto";
|
|
40
|
+
import { encodeCursor } from "../cursor.js";
|
|
41
|
+
import { OwnershipLostError } from "../ownership.js";
|
|
42
|
+
import { POSTGRES_SCHEMA_VERSION, readSchemaVersion } from "./migrations/index.js";
|
|
43
|
+
import { assertIdentifier, tenantSchemaName } from "./names.js";
|
|
44
|
+
export function createPostgresSessionStore(options) {
|
|
45
|
+
return new PostgresSessionStore(options);
|
|
46
|
+
}
|
|
47
|
+
const OWNERSHIP_KEYS = new Set(["owner", "epoch", "ownerExpiresAt"]);
|
|
48
|
+
const OPEN_SESSION = ["running", "runnable"];
|
|
49
|
+
class PostgresSessionStore {
|
|
50
|
+
tenantId;
|
|
51
|
+
sql;
|
|
52
|
+
schema;
|
|
53
|
+
schemaVersion;
|
|
54
|
+
active = new AsyncLocalStorage();
|
|
55
|
+
listeners = new Set();
|
|
56
|
+
inflight = new Set();
|
|
57
|
+
now;
|
|
58
|
+
onError;
|
|
59
|
+
closed = false;
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.tenantId = options.tenantId;
|
|
62
|
+
this.sql = options.sql;
|
|
63
|
+
this.schema = options.schema ?? tenantSchemaName(options.tenantId);
|
|
64
|
+
assertIdentifier(this.schema);
|
|
65
|
+
this.schemaVersion = options.schemaVersion ?? POSTGRES_SCHEMA_VERSION;
|
|
66
|
+
this.now = options.now ?? (() => new Date());
|
|
67
|
+
this.onError =
|
|
68
|
+
options.onError ??
|
|
69
|
+
((error) => queueMicrotask(() => {
|
|
70
|
+
throw error;
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
async tx(fn) {
|
|
74
|
+
if (this.closed)
|
|
75
|
+
throw new Error("SessionStore is closed");
|
|
76
|
+
if (this.active.getStore() === this)
|
|
77
|
+
throw new Error("Nested SessionStore.tx is not allowed");
|
|
78
|
+
let t;
|
|
79
|
+
// Wrapped so `begin` does not treat an array result as queries to await.
|
|
80
|
+
const run = this.sql.begin("isolation level read committed", async (sql) => {
|
|
81
|
+
t = new PostgresTx(sql, this.schema, this.tenantId, this.now);
|
|
82
|
+
try {
|
|
83
|
+
return { value: await this.active.run(this, () => fn(t)) };
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
t.closed = true;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
this.inflight.add(run);
|
|
90
|
+
let result;
|
|
91
|
+
try {
|
|
92
|
+
result = (await run);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
this.inflight.delete(run);
|
|
96
|
+
}
|
|
97
|
+
if (t.events.length > 0 || t.workAvailable) {
|
|
98
|
+
const commit = {
|
|
99
|
+
events: t.events,
|
|
100
|
+
incarnations: t.incarnations,
|
|
101
|
+
workAvailable: t.workAvailable,
|
|
102
|
+
};
|
|
103
|
+
for (const listener of [...this.listeners]) {
|
|
104
|
+
try {
|
|
105
|
+
listener(commit);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
this.onError(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const callback of t.callbacks) {
|
|
113
|
+
try {
|
|
114
|
+
await callback();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this.onError(error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result.value;
|
|
121
|
+
}
|
|
122
|
+
onCommit(listener) {
|
|
123
|
+
this.listeners.add(listener);
|
|
124
|
+
return () => this.listeners.delete(listener);
|
|
125
|
+
}
|
|
126
|
+
async health() {
|
|
127
|
+
const expectedSchemaVersion = this.schemaVersion;
|
|
128
|
+
if (this.closed)
|
|
129
|
+
return { ok: false, schemaVersion: 0, expectedSchemaVersion };
|
|
130
|
+
try {
|
|
131
|
+
const schemaVersion = (await readSchemaVersion(this.sql, this.schema)) ?? 0;
|
|
132
|
+
return {
|
|
133
|
+
ok: schemaVersion === expectedSchemaVersion,
|
|
134
|
+
schemaVersion,
|
|
135
|
+
expectedSchemaVersion,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return { ok: false, schemaVersion: 0, expectedSchemaVersion };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Rejects new transactions and waits for running ones. Does not end the pool. */
|
|
143
|
+
async close() {
|
|
144
|
+
this.closed = true;
|
|
145
|
+
await Promise.allSettled([...this.inflight]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
function storedSession(row) {
|
|
150
|
+
return { ...row.body, ...ownership(row) };
|
|
151
|
+
}
|
|
152
|
+
function ownership(row) {
|
|
153
|
+
return {
|
|
154
|
+
owner: row.owner ?? null,
|
|
155
|
+
epoch: Number(row.epoch),
|
|
156
|
+
ownerExpiresAt: row.owner_expires_at
|
|
157
|
+
? new Date(row.owner_expires_at).toISOString()
|
|
158
|
+
: null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function toJson(value) {
|
|
162
|
+
const json = JSON.stringify(value);
|
|
163
|
+
if (json === undefined)
|
|
164
|
+
throw new Error("Document body must be JSON");
|
|
165
|
+
return json;
|
|
166
|
+
}
|
|
167
|
+
const bytes = (value) => Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
168
|
+
const fromBytes = (value) => new Uint8Array(value);
|
|
169
|
+
function executorRow(row) {
|
|
170
|
+
return {
|
|
171
|
+
agentId: row.agent_id,
|
|
172
|
+
tokenHash: row.token_hash,
|
|
173
|
+
implementationVersion: row.implementation_version,
|
|
174
|
+
...(row.manifest_hash !== null ? { manifestHash: row.manifest_hash } : {}),
|
|
175
|
+
...(row.principal_id !== null ? { principalId: row.principal_id } : {}),
|
|
176
|
+
createdAt: row.created_at,
|
|
177
|
+
updatedAt: row.updated_at,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function principalRow(row) {
|
|
181
|
+
return {
|
|
182
|
+
id: row.id,
|
|
183
|
+
role: row.role,
|
|
184
|
+
tokenHash: row.token_hash,
|
|
185
|
+
idempotencyKey: row.idempotency_key,
|
|
186
|
+
createdAt: row.created_at,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function vaultRow(row) {
|
|
190
|
+
return {
|
|
191
|
+
id: row.id,
|
|
192
|
+
name: row.name,
|
|
193
|
+
ownerUserId: row.owner_user_id,
|
|
194
|
+
metadataJson: row.metadata_json,
|
|
195
|
+
createdAt: row.created_at,
|
|
196
|
+
scope: row.scope,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function credentialRow(row) {
|
|
200
|
+
return {
|
|
201
|
+
id: row.id,
|
|
202
|
+
vaultId: row.vault_id,
|
|
203
|
+
name: row.name,
|
|
204
|
+
type: row.type,
|
|
205
|
+
bindingJson: row.binding_json,
|
|
206
|
+
expiresAt: row.expires_at,
|
|
207
|
+
createdAt: row.created_at,
|
|
208
|
+
rotatedAt: row.rotated_at,
|
|
209
|
+
kekId: row.kek_id,
|
|
210
|
+
nonce: fromBytes(row.nonce),
|
|
211
|
+
ciphertext: fromBytes(row.ciphertext),
|
|
212
|
+
wrappedDek: fromBytes(row.wrapped_dek),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function auditRow(row) {
|
|
216
|
+
return {
|
|
217
|
+
id: row.id,
|
|
218
|
+
at: row.at,
|
|
219
|
+
actor: row.actor,
|
|
220
|
+
action: row.action,
|
|
221
|
+
vaultId: row.vault_id,
|
|
222
|
+
credentialId: row.credential_id,
|
|
223
|
+
sessionId: row.session_id,
|
|
224
|
+
target: row.target,
|
|
225
|
+
outcome: row.outcome,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** `VaultCredentialPatch` field → column. */
|
|
229
|
+
const CREDENTIAL_COLUMNS = {
|
|
230
|
+
name: "name",
|
|
231
|
+
type: "type",
|
|
232
|
+
bindingJson: "binding_json",
|
|
233
|
+
expiresAt: "expires_at",
|
|
234
|
+
rotatedAt: "rotated_at",
|
|
235
|
+
kekId: "kek_id",
|
|
236
|
+
nonce: "nonce",
|
|
237
|
+
ciphertext: "ciphertext",
|
|
238
|
+
wrappedDek: "wrapped_dek",
|
|
239
|
+
};
|
|
240
|
+
const SESSION_TABLES = [
|
|
241
|
+
"sessions",
|
|
242
|
+
"commands",
|
|
243
|
+
"checkpoints",
|
|
244
|
+
"effects",
|
|
245
|
+
"actions",
|
|
246
|
+
"links",
|
|
247
|
+
"outbox",
|
|
248
|
+
];
|
|
249
|
+
class PostgresTx {
|
|
250
|
+
sql;
|
|
251
|
+
schema;
|
|
252
|
+
tenantId;
|
|
253
|
+
now;
|
|
254
|
+
closed = false;
|
|
255
|
+
events = [];
|
|
256
|
+
incarnations = [];
|
|
257
|
+
callbacks = [];
|
|
258
|
+
workAvailable = false;
|
|
259
|
+
constructor(sql, schema, tenantId, now) {
|
|
260
|
+
this.sql = sql;
|
|
261
|
+
this.schema = schema;
|
|
262
|
+
this.tenantId = tenantId;
|
|
263
|
+
this.now = now;
|
|
264
|
+
}
|
|
265
|
+
check() {
|
|
266
|
+
if (this.closed)
|
|
267
|
+
throw new Error("Tx used after its transaction ended");
|
|
268
|
+
}
|
|
269
|
+
/** The quoted, schema-qualified table. */
|
|
270
|
+
t(table) {
|
|
271
|
+
return this.sql(`${this.schema}.${table}`);
|
|
272
|
+
}
|
|
273
|
+
// --- documents -----------------------------------------------------------
|
|
274
|
+
async get(table, id) {
|
|
275
|
+
this.check();
|
|
276
|
+
const sql = this.sql;
|
|
277
|
+
if (table === "sessions") {
|
|
278
|
+
const [row] = await sql `
|
|
279
|
+
SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
|
|
280
|
+
WHERE id = ${id}`;
|
|
281
|
+
return row && storedSession(row);
|
|
282
|
+
}
|
|
283
|
+
const [row] = await sql `SELECT body FROM ${this.t(table)} WHERE id = ${id}`;
|
|
284
|
+
return row?.body;
|
|
285
|
+
}
|
|
286
|
+
async put(table, id, body) {
|
|
287
|
+
this.check();
|
|
288
|
+
let value = body;
|
|
289
|
+
if (table === "sessions" && body && typeof body === "object")
|
|
290
|
+
value = Object.fromEntries(Object.entries(body).filter(([key]) => !OWNERSHIP_KEYS.has(key)));
|
|
291
|
+
const json = toJson(value);
|
|
292
|
+
await this.sql `
|
|
293
|
+
INSERT INTO ${this.t(table)} (id, body) VALUES (${id}, ${json}::text::json)
|
|
294
|
+
ON CONFLICT (id) DO UPDATE SET body = excluded.body`;
|
|
295
|
+
}
|
|
296
|
+
async delete(table, id) {
|
|
297
|
+
this.check();
|
|
298
|
+
await this.sql `DELETE FROM ${this.t(table)} WHERE id = ${id}`;
|
|
299
|
+
// The session's stream is abandoned with it (a new incarnation starts at 0).
|
|
300
|
+
if (table === "sessions")
|
|
301
|
+
await this.sql `DELETE FROM ${this.t("outbox")} WHERE session_id = ${id}`;
|
|
302
|
+
}
|
|
303
|
+
async sessionRows(where, tail) {
|
|
304
|
+
const sql = this.sql;
|
|
305
|
+
const rows = await sql `
|
|
306
|
+
SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
|
|
307
|
+
${where ? sql `WHERE ${where}` : sql ``}
|
|
308
|
+
${tail ?? sql `ORDER BY id`}`;
|
|
309
|
+
return rows.map((row) => storedSession(row));
|
|
310
|
+
}
|
|
311
|
+
// --- ordering ------------------------------------------------------------
|
|
312
|
+
async lockSession(id) {
|
|
313
|
+
this.check();
|
|
314
|
+
const [row] = await this.sql `
|
|
315
|
+
SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
|
|
316
|
+
WHERE id = ${id} FOR UPDATE`;
|
|
317
|
+
return row && storedSession(row);
|
|
318
|
+
}
|
|
319
|
+
async event(sessionId, turnId, type, payload) {
|
|
320
|
+
this.check();
|
|
321
|
+
const sql = this.sql;
|
|
322
|
+
// The UPDATE takes the session row lock and allocates under it.
|
|
323
|
+
const [row] = await sql `
|
|
324
|
+
UPDATE ${this.t("sessions")} SET next_event_seq = next_event_seq + 1
|
|
325
|
+
WHERE id = ${sessionId}
|
|
326
|
+
RETURNING next_event_seq - 1 AS seq, stream_incarnation AS incarnation`;
|
|
327
|
+
if (!row)
|
|
328
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
329
|
+
const seq = Number(row.seq);
|
|
330
|
+
const event = JSON.parse(JSON.stringify({
|
|
331
|
+
eventId: randomUUID(),
|
|
332
|
+
sessionId,
|
|
333
|
+
tenantId: this.tenantId,
|
|
334
|
+
turnId,
|
|
335
|
+
cursor: encodeCursor(sessionId, seq),
|
|
336
|
+
createdAt: this.now().toISOString(),
|
|
337
|
+
type,
|
|
338
|
+
payload,
|
|
339
|
+
}));
|
|
340
|
+
await sql `
|
|
341
|
+
INSERT INTO ${this.t("outbox")} (session_id, seq, body)
|
|
342
|
+
VALUES (${sessionId}, ${seq}, ${JSON.stringify(event)}::text::json)`;
|
|
343
|
+
this.events.push(event);
|
|
344
|
+
this.incarnations.push(row.incarnation ?? null);
|
|
345
|
+
return structuredClone(event);
|
|
346
|
+
}
|
|
347
|
+
afterCommit(fn) {
|
|
348
|
+
this.check();
|
|
349
|
+
this.callbacks.push(fn);
|
|
350
|
+
}
|
|
351
|
+
signalWork() {
|
|
352
|
+
this.check();
|
|
353
|
+
this.workAvailable = true;
|
|
354
|
+
}
|
|
355
|
+
// --- ownership -----------------------------------------------------------
|
|
356
|
+
async takeOwnership(sessionId, claim) {
|
|
357
|
+
this.check();
|
|
358
|
+
const sql = this.sql;
|
|
359
|
+
const [row] = await sql `
|
|
360
|
+
SELECT owner, epoch, owner_expires_at FROM ${this.t("sessions")}
|
|
361
|
+
WHERE id = ${sessionId} FOR UPDATE`;
|
|
362
|
+
if (!row)
|
|
363
|
+
return { status: "missing" };
|
|
364
|
+
const previous = ownership(row);
|
|
365
|
+
if (previous.owner !== null &&
|
|
366
|
+
previous.ownerExpiresAt !== null &&
|
|
367
|
+
Date.parse(previous.ownerExpiresAt) > claim.now.getTime())
|
|
368
|
+
return {
|
|
369
|
+
status: "busy",
|
|
370
|
+
owner: previous.owner,
|
|
371
|
+
ownerExpiresAt: previous.ownerExpiresAt,
|
|
372
|
+
};
|
|
373
|
+
const expires = new Date(claim.now.getTime() + claim.leaseMs);
|
|
374
|
+
const [updated] = await sql `
|
|
375
|
+
UPDATE ${this.t("sessions")}
|
|
376
|
+
SET owner = ${claim.owner}, epoch = epoch + 1, owner_expires_at = ${expires}
|
|
377
|
+
WHERE id = ${sessionId} RETURNING epoch`;
|
|
378
|
+
return {
|
|
379
|
+
status: "owned",
|
|
380
|
+
epoch: Number(updated.epoch),
|
|
381
|
+
takeover: previous.owner !== null,
|
|
382
|
+
previous,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
async renewOwnership(sessionId, owner, epoch, until) {
|
|
386
|
+
this.check();
|
|
387
|
+
const result = await this.sql `
|
|
388
|
+
UPDATE ${this.t("sessions")} SET owner_expires_at = ${until}
|
|
389
|
+
WHERE id = ${sessionId} AND owner = ${owner} AND epoch = ${epoch}`;
|
|
390
|
+
return result.count > 0;
|
|
391
|
+
}
|
|
392
|
+
async releaseOwnership(sessionId, owner, epoch) {
|
|
393
|
+
this.check();
|
|
394
|
+
const result = await this.sql `
|
|
395
|
+
UPDATE ${this.t("sessions")} SET owner = NULL, owner_expires_at = NULL
|
|
396
|
+
WHERE id = ${sessionId} AND owner = ${owner} AND epoch = ${epoch}`;
|
|
397
|
+
return result.count > 0;
|
|
398
|
+
}
|
|
399
|
+
async assertEpoch(sessionId, epoch) {
|
|
400
|
+
const session = await this.lockSession(sessionId);
|
|
401
|
+
if (!session)
|
|
402
|
+
throw new OwnershipLostError(sessionId, epoch, undefined);
|
|
403
|
+
if (session.epoch !== epoch)
|
|
404
|
+
throw new OwnershipLostError(sessionId, epoch, session.epoch);
|
|
405
|
+
return session;
|
|
406
|
+
}
|
|
407
|
+
// --- typed queries -------------------------------------------------------
|
|
408
|
+
async sessionsWithStatus(statuses) {
|
|
409
|
+
this.check();
|
|
410
|
+
return this.sessionRows(this.sql `status = ANY(${[...statuses]})`);
|
|
411
|
+
}
|
|
412
|
+
async orphanedSessions(now, limit) {
|
|
413
|
+
this.check();
|
|
414
|
+
const sql = this.sql;
|
|
415
|
+
return this.sessionRows(sql `status = ANY(${OPEN_SESSION})
|
|
416
|
+
AND (owner IS NULL OR owner_expires_at IS NULL OR owner_expires_at <= ${now})`, sql `ORDER BY
|
|
417
|
+
CASE WHEN owner IS NULL THEN NULL ELSE owner_expires_at END ASC NULLS FIRST,
|
|
418
|
+
id
|
|
419
|
+
LIMIT ${limit}`);
|
|
420
|
+
}
|
|
421
|
+
async listSessions(filter = {}) {
|
|
422
|
+
this.check();
|
|
423
|
+
return this.sessionRows(filter.agentId === undefined
|
|
424
|
+
? undefined
|
|
425
|
+
: this.sql `agent_id = ${filter.agentId}`);
|
|
426
|
+
}
|
|
427
|
+
async bodies(table) {
|
|
428
|
+
const rows = await this.sql `SELECT body FROM ${this.t(table)} ORDER BY id`;
|
|
429
|
+
return rows.map((row) => row.body);
|
|
430
|
+
}
|
|
431
|
+
async listDefinitions() {
|
|
432
|
+
this.check();
|
|
433
|
+
return this.bodies("definitions");
|
|
434
|
+
}
|
|
435
|
+
async listSandboxes() {
|
|
436
|
+
this.check();
|
|
437
|
+
return this.bodies("sandboxes");
|
|
438
|
+
}
|
|
439
|
+
async expiredClaims(now, limit) {
|
|
440
|
+
this.check();
|
|
441
|
+
const rows = await this.sql `
|
|
442
|
+
SELECT body FROM ${this.t("actions")}
|
|
443
|
+
WHERE status = 'claimed' AND lease_expires_at IS NOT NULL
|
|
444
|
+
AND lease_expires_at::timestamptz <= ${now}
|
|
445
|
+
ORDER BY lease_expires_at::timestamptz, id
|
|
446
|
+
LIMIT ${limit}`;
|
|
447
|
+
return rows.map((row) => row.body);
|
|
448
|
+
}
|
|
449
|
+
async pendingActions(agentId) {
|
|
450
|
+
this.check();
|
|
451
|
+
const rows = await this.sql `
|
|
452
|
+
SELECT body FROM ${this.t("actions")}
|
|
453
|
+
WHERE agent_id = ${agentId} AND status = 'pending'
|
|
454
|
+
ORDER BY id`;
|
|
455
|
+
return rows.map((row) => row.body);
|
|
456
|
+
}
|
|
457
|
+
async actionsForSession(sessionId, filter = {}) {
|
|
458
|
+
this.check();
|
|
459
|
+
const sql = this.sql;
|
|
460
|
+
const rows = await sql `
|
|
461
|
+
SELECT body FROM ${this.t("actions")}
|
|
462
|
+
WHERE session_id = ${sessionId}
|
|
463
|
+
${filter.turnId === undefined ? sql `` : sql `AND turn_id = ${filter.turnId}`}
|
|
464
|
+
${filter.statuses === undefined ? sql `` : sql `AND status = ANY(${[...filter.statuses]})`}
|
|
465
|
+
ORDER BY id`;
|
|
466
|
+
return rows.map((row) => row.body);
|
|
467
|
+
}
|
|
468
|
+
async actionsWithStatus(statuses, filter = {}) {
|
|
469
|
+
this.check();
|
|
470
|
+
const sql = this.sql;
|
|
471
|
+
const rows = await sql `
|
|
472
|
+
SELECT body FROM ${this.t("actions")}
|
|
473
|
+
WHERE status = ANY(${[...statuses]})
|
|
474
|
+
${filter.kinds === undefined ? sql `` : sql `AND kind = ANY(${[...filter.kinds]})`}
|
|
475
|
+
ORDER BY id`;
|
|
476
|
+
return rows.map((row) => row.body);
|
|
477
|
+
}
|
|
478
|
+
async invokingEffects(sessionId) {
|
|
479
|
+
return this.effectsForSession(sessionId, { statuses: ["invoking"] });
|
|
480
|
+
}
|
|
481
|
+
async effectsForSession(sessionId, filter = {}) {
|
|
482
|
+
this.check();
|
|
483
|
+
const sql = this.sql;
|
|
484
|
+
const rows = await sql `
|
|
485
|
+
SELECT body FROM ${this.t("effects")}
|
|
486
|
+
WHERE session_id = ${sessionId}
|
|
487
|
+
${filter.turnId === undefined ? sql `` : sql `AND turn_id = ${filter.turnId}`}
|
|
488
|
+
${filter.statuses === undefined ? sql `` : sql `AND status = ANY(${[...filter.statuses]})`}
|
|
489
|
+
ORDER BY id`;
|
|
490
|
+
return rows.map((row) => row.body);
|
|
491
|
+
}
|
|
492
|
+
async effectsForTurn(sessionId, turnId, statuses) {
|
|
493
|
+
return this.effectsForSession(sessionId, { turnId, statuses });
|
|
494
|
+
}
|
|
495
|
+
async effectsWithStatus(statuses, filter = {}) {
|
|
496
|
+
this.check();
|
|
497
|
+
const sql = this.sql;
|
|
498
|
+
const rows = await sql `
|
|
499
|
+
SELECT body FROM ${this.t("effects")}
|
|
500
|
+
WHERE status = ANY(${[...statuses]})
|
|
501
|
+
${filter.kinds === undefined ? sql `` : sql `AND kind = ANY(${[...filter.kinds]})`}
|
|
502
|
+
ORDER BY id`;
|
|
503
|
+
return rows.map((row) => row.body);
|
|
504
|
+
}
|
|
505
|
+
async linkedSessions(workflowSessionId) {
|
|
506
|
+
this.check();
|
|
507
|
+
const rows = await this.sql `
|
|
508
|
+
SELECT l.id, l.body AS link, s.body, s.owner, s.epoch, s.owner_expires_at
|
|
509
|
+
FROM ${this.t("links")} l JOIN ${this.t("sessions")} s ON s.id = l.id
|
|
510
|
+
WHERE l.workflow_session_id = ${workflowSessionId}
|
|
511
|
+
ORDER BY l.id`;
|
|
512
|
+
return rows.map((row) => ({
|
|
513
|
+
agentSessionId: row.id,
|
|
514
|
+
link: row.link,
|
|
515
|
+
session: storedSession(row),
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
async counts() {
|
|
519
|
+
this.check();
|
|
520
|
+
const sql = this.sql;
|
|
521
|
+
const [row] = await sql `
|
|
522
|
+
SELECT
|
|
523
|
+
(SELECT count(*) FROM ${this.t("sessions")})::int AS sessions,
|
|
524
|
+
(SELECT count(*) FROM ${this.t("sessions")} WHERE status = ANY(${OPEN_SESSION}))::int AS running,
|
|
525
|
+
(SELECT count(*) FROM ${this.t("actions")} WHERE status IN ('pending', 'claimed'))::int AS actions,
|
|
526
|
+
(SELECT count(*) FROM ${this.t("effects")} WHERE status = 'uncertain')::int AS uncertain,
|
|
527
|
+
(SELECT count(*) FROM ${this.t("sandboxes")})::int AS sandboxes,
|
|
528
|
+
(SELECT count(*) FROM ${this.t("definitions")})::int AS definitions`;
|
|
529
|
+
return {
|
|
530
|
+
sessions: row.sessions,
|
|
531
|
+
runningSessions: row.running,
|
|
532
|
+
pendingActions: row.actions,
|
|
533
|
+
uncertainEffects: row.uncertain,
|
|
534
|
+
sandboxes: row.sandboxes,
|
|
535
|
+
definitions: row.definitions,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
// --- outbox --------------------------------------------------------------
|
|
539
|
+
async outbox(limit, filter = {}) {
|
|
540
|
+
this.check();
|
|
541
|
+
const sql = this.sql;
|
|
542
|
+
const rows = await sql `
|
|
543
|
+
SELECT session_id, seq, body FROM ${this.t("outbox")}
|
|
544
|
+
${filter.sessionId === undefined ? sql `` : sql `WHERE session_id = ${filter.sessionId}`}
|
|
545
|
+
ORDER BY session_id, seq
|
|
546
|
+
LIMIT ${limit}`;
|
|
547
|
+
return rows.map((row) => ({
|
|
548
|
+
sessionId: row.session_id,
|
|
549
|
+
seq: Number(row.seq),
|
|
550
|
+
event: row.body,
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
async deleteOutbox(sessionId, throughSeq, incarnation) {
|
|
554
|
+
this.check();
|
|
555
|
+
const sql = this.sql;
|
|
556
|
+
// One statement: the session and the rows are read from the same snapshot.
|
|
557
|
+
const result = await sql `
|
|
558
|
+
DELETE FROM ${this.t("outbox")}
|
|
559
|
+
WHERE session_id = ${sessionId} AND seq <= ${throughSeq}
|
|
560
|
+
${incarnation === undefined
|
|
561
|
+
? sql ``
|
|
562
|
+
: sql `AND EXISTS (
|
|
563
|
+
SELECT 1 FROM ${this.t("sessions")} WHERE id = ${sessionId}
|
|
564
|
+
AND stream_incarnation IS NOT DISTINCT FROM ${incarnation})`}`;
|
|
565
|
+
return result.count;
|
|
566
|
+
}
|
|
567
|
+
async outboxStats() {
|
|
568
|
+
this.check();
|
|
569
|
+
const [row] = await this.sql `
|
|
570
|
+
SELECT count(*)::int AS depth, min(created_at) AS oldest
|
|
571
|
+
FROM ${this.t("outbox")}`;
|
|
572
|
+
return {
|
|
573
|
+
depth: row.depth,
|
|
574
|
+
oldestCreatedAt: row.oldest ?? null,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
// --- executors -----------------------------------------------------------
|
|
578
|
+
async listExecutors() {
|
|
579
|
+
this.check();
|
|
580
|
+
const rows = await this.sql `SELECT * FROM ${this.t("executors")} ORDER BY agent_id`;
|
|
581
|
+
return rows.map(executorRow);
|
|
582
|
+
}
|
|
583
|
+
async getExecutor(agentId) {
|
|
584
|
+
this.check();
|
|
585
|
+
const [row] = await this.sql `
|
|
586
|
+
SELECT * FROM ${this.t("executors")} WHERE agent_id = ${agentId}`;
|
|
587
|
+
return row && executorRow(row);
|
|
588
|
+
}
|
|
589
|
+
async putExecutor(row) {
|
|
590
|
+
this.check();
|
|
591
|
+
await this.sql `
|
|
592
|
+
INSERT INTO ${this.t("executors")} (
|
|
593
|
+
agent_id, token_hash, implementation_version, manifest_hash,
|
|
594
|
+
principal_id, created_at, updated_at
|
|
595
|
+
) VALUES (
|
|
596
|
+
${row.agentId}, ${row.tokenHash}, ${row.implementationVersion},
|
|
597
|
+
${row.manifestHash ?? null}, ${row.principalId ?? null},
|
|
598
|
+
${row.updatedAt}, ${row.updatedAt}
|
|
599
|
+
)
|
|
600
|
+
ON CONFLICT (agent_id) DO UPDATE SET
|
|
601
|
+
token_hash = excluded.token_hash,
|
|
602
|
+
implementation_version = excluded.implementation_version,
|
|
603
|
+
manifest_hash = excluded.manifest_hash,
|
|
604
|
+
principal_id = excluded.principal_id,
|
|
605
|
+
updated_at = excluded.updated_at`;
|
|
606
|
+
}
|
|
607
|
+
async deleteExecutor(agentId) {
|
|
608
|
+
this.check();
|
|
609
|
+
await this.sql `DELETE FROM ${this.t("executors")} WHERE agent_id = ${agentId}`;
|
|
610
|
+
}
|
|
611
|
+
// --- principals ----------------------------------------------------------
|
|
612
|
+
async insertPrincipal(row) {
|
|
613
|
+
this.check();
|
|
614
|
+
await this.sql `
|
|
615
|
+
INSERT INTO ${this.t("principals")} (id, role, token_hash, idempotency_key, created_at)
|
|
616
|
+
VALUES (${row.id}, ${row.role}, ${row.tokenHash}, ${row.idempotencyKey}, ${row.createdAt})`;
|
|
617
|
+
}
|
|
618
|
+
async principalByTokenHash(tokenHash) {
|
|
619
|
+
this.check();
|
|
620
|
+
const [row] = await this.sql `
|
|
621
|
+
SELECT * FROM ${this.t("principals")} WHERE token_hash = ${tokenHash}`;
|
|
622
|
+
return row && principalRow(row);
|
|
623
|
+
}
|
|
624
|
+
async principalById(id) {
|
|
625
|
+
this.check();
|
|
626
|
+
const [row] = await this.sql `
|
|
627
|
+
SELECT * FROM ${this.t("principals")} WHERE id = ${id}`;
|
|
628
|
+
return row && principalRow(row);
|
|
629
|
+
}
|
|
630
|
+
async applicationTokenHashes() {
|
|
631
|
+
this.check();
|
|
632
|
+
const rows = await this.sql `
|
|
633
|
+
SELECT token_hash FROM ${this.t("principals")}
|
|
634
|
+
WHERE role = 'application' ORDER BY id`;
|
|
635
|
+
return rows.map((row) => row.token_hash);
|
|
636
|
+
}
|
|
637
|
+
// --- vault ---------------------------------------------------------------
|
|
638
|
+
async insertVault(row) {
|
|
639
|
+
this.check();
|
|
640
|
+
await this.sql `
|
|
641
|
+
INSERT INTO ${this.t("vaults")} (id, name, owner_user_id, metadata_json, created_at, scope)
|
|
642
|
+
VALUES (${row.id}, ${row.name}, ${row.ownerUserId}, ${row.metadataJson},
|
|
643
|
+
${row.createdAt}, ${row.scope})`;
|
|
644
|
+
}
|
|
645
|
+
async getVault(id) {
|
|
646
|
+
this.check();
|
|
647
|
+
const [row] = await this.sql `SELECT * FROM ${this.t("vaults")} WHERE id = ${id}`;
|
|
648
|
+
return row && vaultRow(row);
|
|
649
|
+
}
|
|
650
|
+
async vaultsByOwner(ownerUserId) {
|
|
651
|
+
this.check();
|
|
652
|
+
const rows = await this.sql `
|
|
653
|
+
SELECT * FROM ${this.t("vaults")}
|
|
654
|
+
WHERE owner_user_id = ${ownerUserId} AND scope = 'user'
|
|
655
|
+
ORDER BY created_at, id`;
|
|
656
|
+
return rows.map(vaultRow);
|
|
657
|
+
}
|
|
658
|
+
async updateVaultMetadata(id, metadataJson) {
|
|
659
|
+
this.check();
|
|
660
|
+
await this.sql `
|
|
661
|
+
UPDATE ${this.t("vaults")} SET metadata_json = ${metadataJson} WHERE id = ${id}`;
|
|
662
|
+
}
|
|
663
|
+
async deleteVault(id) {
|
|
664
|
+
this.check();
|
|
665
|
+
// Credentials go with it (ON DELETE CASCADE).
|
|
666
|
+
await this.sql `DELETE FROM ${this.t("vaults")} WHERE id = ${id}`;
|
|
667
|
+
}
|
|
668
|
+
async insertCredential(row) {
|
|
669
|
+
this.check();
|
|
670
|
+
await this.sql `
|
|
671
|
+
INSERT INTO ${this.t("vault_credentials")} (
|
|
672
|
+
id, vault_id, name, type, binding_json, expires_at, created_at,
|
|
673
|
+
rotated_at, kek_id, nonce, ciphertext, wrapped_dek
|
|
674
|
+
) VALUES (
|
|
675
|
+
${row.id}, ${row.vaultId}, ${row.name}, ${row.type}, ${row.bindingJson},
|
|
676
|
+
${row.expiresAt}, ${row.createdAt}, ${row.rotatedAt}, ${row.kekId},
|
|
677
|
+
${bytes(row.nonce)}, ${bytes(row.ciphertext)}, ${bytes(row.wrappedDek)}
|
|
678
|
+
)`;
|
|
679
|
+
}
|
|
680
|
+
async getCredential(id) {
|
|
681
|
+
this.check();
|
|
682
|
+
const [row] = await this.sql `
|
|
683
|
+
SELECT * FROM ${this.t("vault_credentials")} WHERE id = ${id}`;
|
|
684
|
+
return row && credentialRow(row);
|
|
685
|
+
}
|
|
686
|
+
async credentialsForVault(vaultId, filter = {}) {
|
|
687
|
+
this.check();
|
|
688
|
+
const sql = this.sql;
|
|
689
|
+
const rows = await sql `
|
|
690
|
+
SELECT * FROM ${this.t("vault_credentials")}
|
|
691
|
+
WHERE vault_id = ${vaultId}
|
|
692
|
+
${filter.type === undefined ? sql `` : sql `AND type = ${filter.type}`}
|
|
693
|
+
ORDER BY created_at, id`;
|
|
694
|
+
return rows.map(credentialRow);
|
|
695
|
+
}
|
|
696
|
+
async updateCredential(vaultId, id, patch) {
|
|
697
|
+
this.check();
|
|
698
|
+
const sql = this.sql;
|
|
699
|
+
const columns = {};
|
|
700
|
+
for (const [field, column] of Object.entries(CREDENTIAL_COLUMNS)) {
|
|
701
|
+
const value = patch[field];
|
|
702
|
+
if (value === undefined)
|
|
703
|
+
continue;
|
|
704
|
+
columns[column] = value instanceof Uint8Array ? bytes(value) : value;
|
|
705
|
+
}
|
|
706
|
+
if (Object.keys(columns).length === 0) {
|
|
707
|
+
const rows = await sql `
|
|
708
|
+
SELECT 1 FROM ${this.t("vault_credentials")}
|
|
709
|
+
WHERE vault_id = ${vaultId} AND id = ${id}`;
|
|
710
|
+
return rows.length > 0;
|
|
711
|
+
}
|
|
712
|
+
const result = await sql `
|
|
713
|
+
UPDATE ${this.t("vault_credentials")} SET ${sql(columns)}
|
|
714
|
+
WHERE vault_id = ${vaultId} AND id = ${id}`;
|
|
715
|
+
return result.count > 0;
|
|
716
|
+
}
|
|
717
|
+
async deleteCredential(vaultId, id) {
|
|
718
|
+
this.check();
|
|
719
|
+
const result = await this.sql `
|
|
720
|
+
DELETE FROM ${this.t("vault_credentials")}
|
|
721
|
+
WHERE vault_id = ${vaultId} AND id = ${id}`;
|
|
722
|
+
return result.count > 0;
|
|
723
|
+
}
|
|
724
|
+
async countCredentials() {
|
|
725
|
+
this.check();
|
|
726
|
+
const [row] = await this.sql `
|
|
727
|
+
SELECT count(*)::int AS n FROM ${this.t("vault_credentials")}`;
|
|
728
|
+
return row.n;
|
|
729
|
+
}
|
|
730
|
+
async insertVaultAudit(row) {
|
|
731
|
+
this.check();
|
|
732
|
+
await this.sql `
|
|
733
|
+
INSERT INTO ${this.t("vault_audit")} (
|
|
734
|
+
id, at, actor, action, vault_id, credential_id, session_id, target, outcome
|
|
735
|
+
) VALUES (
|
|
736
|
+
${row.id}, ${row.at}, ${row.actor}, ${row.action}, ${row.vaultId},
|
|
737
|
+
${row.credentialId}, ${row.sessionId}, ${row.target}, ${row.outcome}
|
|
738
|
+
)`;
|
|
739
|
+
}
|
|
740
|
+
async vaultAudit(filter = {}) {
|
|
741
|
+
this.check();
|
|
742
|
+
const sql = this.sql;
|
|
743
|
+
const rows = await sql `
|
|
744
|
+
SELECT * FROM ${this.t("vault_audit")}
|
|
745
|
+
${filter.vaultId === undefined ? sql `` : sql `WHERE vault_id = ${filter.vaultId}`}
|
|
746
|
+
ORDER BY ord
|
|
747
|
+
${filter.limit === undefined ? sql `` : sql `LIMIT ${filter.limit}`}`;
|
|
748
|
+
return rows.map(auditRow);
|
|
749
|
+
}
|
|
750
|
+
async getVaultIdempotency(id) {
|
|
751
|
+
this.check();
|
|
752
|
+
const [row] = await this.sql `
|
|
753
|
+
SELECT id, body_hash, response FROM ${this.t("vault_idempotency")} WHERE id = ${id}`;
|
|
754
|
+
return row && { id: row.id, bodyHash: row.body_hash, response: row.response };
|
|
755
|
+
}
|
|
756
|
+
async insertVaultIdempotency(row) {
|
|
757
|
+
this.check();
|
|
758
|
+
await this.sql `
|
|
759
|
+
INSERT INTO ${this.t("vault_idempotency")} (id, body_hash, response)
|
|
760
|
+
VALUES (${row.id}, ${row.bodyHash}, ${row.response})`;
|
|
761
|
+
}
|
|
762
|
+
// --- settings ------------------------------------------------------------
|
|
763
|
+
async getSetting(key) {
|
|
764
|
+
this.check();
|
|
765
|
+
const [row] = await this.sql `
|
|
766
|
+
SELECT value FROM ${this.t("tenant_settings")} WHERE key = ${key}`;
|
|
767
|
+
return row?.value;
|
|
768
|
+
}
|
|
769
|
+
async putSetting(key, value) {
|
|
770
|
+
this.check();
|
|
771
|
+
await this.sql `
|
|
772
|
+
INSERT INTO ${this.t("tenant_settings")} (key, value) VALUES (${key}, ${value})
|
|
773
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value`;
|
|
774
|
+
}
|
|
775
|
+
// --- reset ---------------------------------------------------------------
|
|
776
|
+
async reset(scope) {
|
|
777
|
+
this.check();
|
|
778
|
+
const sql = this.sql;
|
|
779
|
+
if (scope === "sessions" || scope === "all")
|
|
780
|
+
for (const table of SESSION_TABLES)
|
|
781
|
+
await sql `DELETE FROM ${this.t(table)}`;
|
|
782
|
+
if (scope === "sandboxes" || scope === "all")
|
|
783
|
+
await sql `DELETE FROM ${this.t("sandboxes")}`;
|
|
784
|
+
if (scope === "all") {
|
|
785
|
+
await sql `DELETE FROM ${this.t("definitions")}`;
|
|
786
|
+
await sql `DELETE FROM ${this.t("executors")}`;
|
|
787
|
+
await sql `DELETE FROM ${this.t("vaults")} WHERE scope <> 'host'`;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|