@nylorun/runtime 0.10.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.
Files changed (190) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +53 -51
  3. package/dist/adapters/execution/restate.d.ts +130 -0
  4. package/dist/adapters/execution/restate.js +442 -0
  5. package/dist/adapters/sandbox/virtual.js +1 -1
  6. package/dist/adapters/streams/s2.d.ts +18 -0
  7. package/dist/adapters/streams/s2.js +361 -0
  8. package/dist/core/executors.d.ts +4 -1
  9. package/dist/core/executors.js +20 -1
  10. package/dist/core/flow-host.d.ts +93 -132
  11. package/dist/core/flow-host.js +282 -547
  12. package/dist/core/sandbox-routes.d.ts +8 -3
  13. package/dist/core/sandbox-routes.js +10 -10
  14. package/dist/execution/memory.d.ts +54 -0
  15. package/dist/execution/memory.js +217 -0
  16. package/dist/execution/types.d.ts +125 -0
  17. package/dist/execution/types.js +52 -0
  18. package/dist/host/config-for.d.ts +2 -1
  19. package/dist/host/config-for.js +3 -21
  20. package/dist/host/config.d.ts +0 -8
  21. package/dist/host/create-host.d.ts +36 -1
  22. package/dist/host/create-host.js +48 -24
  23. package/dist/host/execution.d.ts +61 -0
  24. package/dist/host/execution.js +69 -0
  25. package/dist/host/http.d.ts +4 -0
  26. package/dist/host/http.js +6 -0
  27. package/dist/host/index.d.ts +3 -1
  28. package/dist/host/index.js +2 -0
  29. package/dist/host/main.js +106 -47
  30. package/dist/host/stack-config.d.ts +72 -0
  31. package/dist/host/stack-config.js +193 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/infra/database.d.ts +16 -0
  34. package/dist/infra/database.js +31 -0
  35. package/dist/infra/execution.d.ts +39 -0
  36. package/dist/infra/execution.js +68 -0
  37. package/dist/infra/index.d.ts +42 -0
  38. package/dist/infra/index.js +56 -0
  39. package/dist/infra/readiness.d.ts +33 -0
  40. package/dist/infra/readiness.js +53 -0
  41. package/dist/infra/streams.d.ts +17 -0
  42. package/dist/infra/streams.js +33 -0
  43. package/dist/infra/workers.d.ts +39 -0
  44. package/dist/infra/workers.js +31 -0
  45. package/dist/model/pi-model.d.ts +2 -2
  46. package/dist/model/pi-model.js +5 -5
  47. package/dist/sandbox/manager.d.ts +26 -8
  48. package/dist/sandbox/manager.js +95 -47
  49. package/dist/sandbox/select.d.ts +1 -1
  50. package/dist/sandbox/select.js +4 -5
  51. package/dist/sandbox/types.d.ts +2 -2
  52. package/dist/store/canonical.d.ts +5 -0
  53. package/dist/store/canonical.js +17 -0
  54. package/dist/store/cursor.d.ts +9 -0
  55. package/dist/store/cursor.js +18 -0
  56. package/dist/store/memory.d.ts +57 -0
  57. package/dist/store/memory.js +643 -0
  58. package/dist/store/ownership.d.ts +19 -0
  59. package/dist/store/ownership.js +33 -0
  60. package/dist/store/postgres/connect.d.ts +25 -0
  61. package/dist/store/postgres/connect.js +25 -0
  62. package/dist/store/postgres/locking.d.ts +16 -0
  63. package/dist/store/postgres/locking.js +22 -0
  64. package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
  65. package/dist/store/postgres/migrations/001_initial.js +196 -0
  66. package/dist/store/postgres/migrations/index.d.ts +49 -0
  67. package/dist/store/postgres/migrations/index.js +75 -0
  68. package/dist/store/postgres/names.d.ts +11 -0
  69. package/dist/store/postgres/names.js +42 -0
  70. package/dist/store/postgres/store.d.ts +11 -0
  71. package/dist/store/postgres/store.js +790 -0
  72. package/dist/store/postgres/tenants.d.ts +90 -0
  73. package/dist/store/postgres/tenants.js +207 -0
  74. package/dist/store/types.d.ts +440 -0
  75. package/dist/store/types.js +10 -0
  76. package/dist/streams/basin.d.ts +23 -0
  77. package/dist/streams/basin.js +51 -0
  78. package/dist/streams/memory.d.ts +25 -0
  79. package/dist/streams/memory.js +124 -0
  80. package/dist/streams/relay.d.ts +37 -0
  81. package/dist/streams/relay.js +254 -0
  82. package/dist/streams/types.d.ts +115 -0
  83. package/dist/streams/types.js +86 -0
  84. package/dist/tenant/actions.d.ts +153 -0
  85. package/dist/tenant/actions.js +191 -0
  86. package/dist/tenant/advance.d.ts +15 -0
  87. package/dist/tenant/advance.js +439 -0
  88. package/dist/tenant/auth.d.ts +11 -0
  89. package/dist/tenant/auth.js +34 -0
  90. package/dist/tenant/commands.d.ts +3 -0
  91. package/dist/tenant/commands.js +384 -0
  92. package/dist/tenant/context.d.ts +140 -0
  93. package/dist/tenant/context.js +40 -0
  94. package/dist/tenant/effects.d.ts +20 -0
  95. package/dist/tenant/effects.js +539 -0
  96. package/dist/tenant/envelope.d.ts +0 -3
  97. package/dist/tenant/envelope.js +2 -34
  98. package/dist/tenant/ephemeral.d.ts +15 -4
  99. package/dist/tenant/ephemeral.js +106 -38
  100. package/dist/tenant/http.d.ts +32 -0
  101. package/dist/tenant/http.js +51 -0
  102. package/dist/tenant/live.d.ts +92 -0
  103. package/dist/tenant/live.js +290 -0
  104. package/dist/tenant/model-setting.d.ts +16 -0
  105. package/dist/tenant/model-setting.js +12 -0
  106. package/dist/tenant/module.d.ts +29 -8
  107. package/dist/tenant/module.js +162 -158
  108. package/dist/tenant/paths.d.ts +12 -8
  109. package/dist/tenant/paths.js +38 -12
  110. package/dist/tenant/principals.d.ts +9 -13
  111. package/dist/tenant/principals.js +31 -38
  112. package/dist/tenant/quarantine-error.d.ts +1 -1
  113. package/dist/tenant/quarantine-error.js +2 -8
  114. package/dist/tenant/quarantine.d.ts +0 -8
  115. package/dist/tenant/quarantine.js +8 -30
  116. package/dist/tenant/reset.d.ts +7 -6
  117. package/dist/tenant/reset.js +7 -51
  118. package/dist/tenant/routes-tenant.d.ts +12 -0
  119. package/dist/tenant/routes-tenant.js +126 -0
  120. package/dist/tenant/routes.d.ts +9 -0
  121. package/dist/tenant/routes.js +142 -0
  122. package/dist/tenant/runtime.d.ts +59 -58
  123. package/dist/tenant/runtime.js +243 -2201
  124. package/dist/tenant/scheduler.d.ts +25 -0
  125. package/dist/tenant/scheduler.js +84 -0
  126. package/dist/tenant/session.d.ts +60 -0
  127. package/dist/tenant/session.js +94 -0
  128. package/dist/tenant/sessions.d.ts +34 -0
  129. package/dist/tenant/sessions.js +149 -0
  130. package/dist/tenant/status.d.ts +13 -4
  131. package/dist/tenant/status.js +76 -41
  132. package/dist/tenant/store-memory.d.ts +9 -12
  133. package/dist/tenant/store-memory.js +79 -68
  134. package/dist/tenant/store-pg.d.ts +15 -0
  135. package/dist/tenant/store-pg.js +140 -0
  136. package/dist/tenant/streams.d.ts +126 -0
  137. package/dist/tenant/streams.js +419 -0
  138. package/dist/tenant/sweep.d.ts +15 -0
  139. package/dist/tenant/sweep.js +122 -0
  140. package/dist/tenant/types.d.ts +95 -15
  141. package/dist/tenant/types.js +23 -1
  142. package/dist/tenant/worker.d.ts +84 -0
  143. package/dist/tenant/worker.js +189 -0
  144. package/dist/vault/service.d.ts +37 -31
  145. package/dist/vault/service.js +439 -359
  146. package/dist/version.d.ts +1 -1
  147. package/dist/version.js +1 -1
  148. package/package.json +13 -13
  149. package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
  150. package/dist/adapters/sandbox/microsandbox.js +0 -238
  151. package/dist/core/store.d.ts +0 -32
  152. package/dist/core/store.js +0 -143
  153. package/dist/launcher/commands.d.ts +0 -24
  154. package/dist/launcher/commands.js +0 -260
  155. package/dist/launcher/ensure-config.d.ts +0 -16
  156. package/dist/launcher/ensure-config.js +0 -79
  157. package/dist/launcher/errors.d.ts +0 -12
  158. package/dist/launcher/errors.js +0 -22
  159. package/dist/launcher/host-config.d.ts +0 -17
  160. package/dist/launcher/host-config.js +0 -64
  161. package/dist/launcher/host-state.d.ts +0 -17
  162. package/dist/launcher/host-state.js +0 -61
  163. package/dist/launcher/lifecycle.d.ts +0 -66
  164. package/dist/launcher/lifecycle.js +0 -407
  165. package/dist/launcher/locks.d.ts +0 -26
  166. package/dist/launcher/locks.js +0 -117
  167. package/dist/launcher/logs.d.ts +0 -18
  168. package/dist/launcher/logs.js +0 -117
  169. package/dist/launcher/main.d.ts +0 -2
  170. package/dist/launcher/main.js +0 -66
  171. package/dist/launcher/output.d.ts +0 -11
  172. package/dist/launcher/output.js +0 -68
  173. package/dist/launcher/paths.d.ts +0 -20
  174. package/dist/launcher/paths.js +0 -36
  175. package/dist/launcher/probe.d.ts +0 -23
  176. package/dist/launcher/probe.js +0 -72
  177. package/dist/launcher/protocol.d.ts +0 -62
  178. package/dist/launcher/protocol.js +0 -1
  179. package/dist/launcher/schema-guard.d.ts +0 -6
  180. package/dist/launcher/schema-guard.js +0 -36
  181. package/dist/launcher/spawn.d.ts +0 -22
  182. package/dist/launcher/spawn.js +0 -85
  183. package/dist/launcher/status.d.ts +0 -6
  184. package/dist/launcher/status.js +0 -65
  185. package/dist/tenant/migration.d.ts +0 -16
  186. package/dist/tenant/migration.js +0 -116
  187. package/dist/tenant/schema.d.ts +0 -23
  188. package/dist/tenant/schema.js +0 -151
  189. package/dist/tenant/store-fs.d.ts +0 -16
  190. package/dist/tenant/store-fs.js +0 -205
@@ -0,0 +1,25 @@
1
+ import { type Sql } from "postgres";
2
+ export type PostgresClient = Sql;
3
+ export interface PostgresClientOptions {
4
+ /** Pool size. Default 10. */
5
+ max?: number;
6
+ /** Seconds before an idle connection closes. Default 30. */
7
+ idleTimeoutSeconds?: number;
8
+ /** Seconds to wait for a connection. Default 10. */
9
+ connectTimeoutSeconds?: number;
10
+ /** `application_name` on every connection. Default `nylorun-runtime`. */
11
+ applicationName?: string;
12
+ }
13
+ /**
14
+ * Opens a connection pool for Session Stores and the Tenant catalog. One pool
15
+ * serves every Tenant schema of a Host; stores never end it, so the caller
16
+ * calls `client.end()` at shutdown. This module and its siblings are the only
17
+ * code that imports the Postgres driver (seam rule 3).
18
+ *
19
+ * Statements are not prepared (`prepare: false`). Every statement names its
20
+ * Tenant's schema, so the same query is a different statement per Tenant, and
21
+ * named prepared statements would pile up on every pooled connection with the
22
+ * number of Tenants a Host serves. Unnamed statements still go in one round
23
+ * trip; the queries are simple enough that re-planning them costs little.
24
+ */
25
+ export declare function createPostgresClient(url: string, options?: PostgresClientOptions): PostgresClient;
@@ -0,0 +1,25 @@
1
+ import postgres, {} from "postgres";
2
+ /**
3
+ * Opens a connection pool for Session Stores and the Tenant catalog. One pool
4
+ * serves every Tenant schema of a Host; stores never end it, so the caller
5
+ * calls `client.end()` at shutdown. This module and its siblings are the only
6
+ * code that imports the Postgres driver (seam rule 3).
7
+ *
8
+ * Statements are not prepared (`prepare: false`). Every statement names its
9
+ * Tenant's schema, so the same query is a different statement per Tenant, and
10
+ * named prepared statements would pile up on every pooled connection with the
11
+ * number of Tenants a Host serves. Unnamed statements still go in one round
12
+ * trip; the queries are simple enough that re-planning them costs little.
13
+ */
14
+ export function createPostgresClient(url, options = {}) {
15
+ return postgres(url, {
16
+ max: options.max ?? 10,
17
+ idle_timeout: options.idleTimeoutSeconds ?? 30,
18
+ connect_timeout: options.connectTimeoutSeconds ?? 10,
19
+ prepare: false,
20
+ onnotice: () => { },
21
+ connection: {
22
+ application_name: options.applicationName ?? "nylorun-runtime",
23
+ },
24
+ });
25
+ }
@@ -0,0 +1,16 @@
1
+ import type { SessionDoc, StoredSession, Tx } from "../types.js";
2
+ /**
3
+ * Locks several sessions in one transaction without risking a deadlock.
4
+ *
5
+ * Postgres row locks are taken in the order statements run. Two transactions
6
+ * that lock sessions `a` then `b` and `b` then `a` can deadlock, and Postgres
7
+ * aborts one with `40P01`. Every transaction that touches more than one
8
+ * session therefore locks all of them first, in ascending id order (UTF-16
9
+ * code unit order, the same order `lockSessions` uses everywhere), and only
10
+ * then writes. Duplicate ids are locked once.
11
+ *
12
+ * Returns each id's session, or undefined when it does not exist.
13
+ */
14
+ export declare function lockSessions<T extends SessionDoc = SessionDoc>(t: Tx, ids: Iterable<string>): Promise<Map<string, StoredSession<T> | undefined>>;
15
+ /** The order `lockSessions` locks in: unique ids, ascending. */
16
+ export declare function lockOrder(ids: Iterable<string>): string[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Locks several sessions in one transaction without risking a deadlock.
3
+ *
4
+ * Postgres row locks are taken in the order statements run. Two transactions
5
+ * that lock sessions `a` then `b` and `b` then `a` can deadlock, and Postgres
6
+ * aborts one with `40P01`. Every transaction that touches more than one
7
+ * session therefore locks all of them first, in ascending id order (UTF-16
8
+ * code unit order, the same order `lockSessions` uses everywhere), and only
9
+ * then writes. Duplicate ids are locked once.
10
+ *
11
+ * Returns each id's session, or undefined when it does not exist.
12
+ */
13
+ export async function lockSessions(t, ids) {
14
+ const locked = new Map();
15
+ for (const id of lockOrder(ids))
16
+ locked.set(id, await t.lockSession(id));
17
+ return locked;
18
+ }
19
+ /** The order `lockSessions` locks in: unique ids, ascending. */
20
+ export function lockOrder(ids) {
21
+ return [...new Set(ids)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
22
+ }
@@ -0,0 +1,27 @@
1
+ import type { Migration } from "./index.js";
2
+ /**
3
+ * The first Postgres Tenant schema.
4
+ *
5
+ * - `tenant` holds the Tenant envelope (one row).
6
+ * - Document tables keep `id text primary key, body json`. The fields typed
7
+ * queries filter on are stored generated columns with indexes. Ids and
8
+ * timestamps compared as text use `COLLATE "C"`, so ordering is by code
9
+ * point, like the in-memory fake, whatever the database collation.
10
+ * - Bodies are `json`, not `jsonb`, so every string round-trips exactly as
11
+ * `JSON.stringify` wrote it. `jsonb` rejects the escapes `\u0000` (`22P05`)
12
+ * and unpaired surrogates (`\ud800`), which tool output can contain and
13
+ * which the in-memory store accepts. `json` stores the text
14
+ * verbatim and checks only its syntax. The generated columns read the body
15
+ * through `doc(body)`, which casts to `jsonb` after replacing those two
16
+ * escapes with `\ufffd`: the stored body is untouched, and only an indexed
17
+ * field that itself contained one (never an id, status or timestamp) would
18
+ * see U+FFFD. Bodies with no `\u` escape at all take the plain cast. Queries
19
+ * never apply `->`/`->>` to a `json` body (Postgres de-escapes the whole text
20
+ * and fails on the same escapes); they read generated columns.
21
+ * - `sessions` carries the store-managed `next_event_seq` and ownership
22
+ * columns (`owner`, `epoch`, `owner_expires_at`), which are never in `body`.
23
+ * - `outbox(session_id, seq, body)` holds committed, unrelayed events.
24
+ * - Executors, principals, vault and settings keep typed columns. Vault
25
+ * ciphertext is `bytea` columns, never inside a JSON body.
26
+ */
27
+ export declare const initial: Migration;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The first Postgres Tenant schema.
3
+ *
4
+ * - `tenant` holds the Tenant envelope (one row).
5
+ * - Document tables keep `id text primary key, body json`. The fields typed
6
+ * queries filter on are stored generated columns with indexes. Ids and
7
+ * timestamps compared as text use `COLLATE "C"`, so ordering is by code
8
+ * point, like the in-memory fake, whatever the database collation.
9
+ * - Bodies are `json`, not `jsonb`, so every string round-trips exactly as
10
+ * `JSON.stringify` wrote it. `jsonb` rejects the escapes `\u0000` (`22P05`)
11
+ * and unpaired surrogates (`\ud800`), which tool output can contain and
12
+ * which the in-memory store accepts. `json` stores the text
13
+ * verbatim and checks only its syntax. The generated columns read the body
14
+ * through `doc(body)`, which casts to `jsonb` after replacing those two
15
+ * escapes with `\ufffd`: the stored body is untouched, and only an indexed
16
+ * field that itself contained one (never an id, status or timestamp) would
17
+ * see U+FFFD. Bodies with no `\u` escape at all take the plain cast. Queries
18
+ * never apply `->`/`->>` to a `json` body (Postgres de-escapes the whole text
19
+ * and fails on the same escapes); they read generated columns.
20
+ * - `sessions` carries the store-managed `next_event_seq` and ownership
21
+ * columns (`owner`, `epoch`, `owner_expires_at`), which are never in `body`.
22
+ * - `outbox(session_id, seq, body)` holds committed, unrelayed events.
23
+ * - Executors, principals, vault and settings keep typed columns. Vault
24
+ * ciphertext is `bytea` columns, never inside a JSON body.
25
+ */
26
+ export const initial = {
27
+ version: 1,
28
+ name: "initial",
29
+ up: (s) => `
30
+ CREATE FUNCTION ${s}.doc(body json) RETURNS jsonb
31
+ LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS $$
32
+ SELECT CASE WHEN strpos(body::text, '\\u') = 0 THEN body::jsonb
33
+ ELSE regexp_replace(
34
+ body::text,
35
+ '(?<!\\\\)((?:\\\\\\\\)*)\\\\u(0000|[dD][89a-fA-F][0-9a-fA-F]{2})',
36
+ '\\1\\\\ufffd',
37
+ 'g'
38
+ )::jsonb
39
+ END
40
+ $$;
41
+
42
+ CREATE TABLE ${s}.tenant (
43
+ id text COLLATE "C" PRIMARY KEY,
44
+ name text NOT NULL,
45
+ created_at text NOT NULL,
46
+ updated_at text NOT NULL,
47
+ schema_version integer NOT NULL,
48
+ singleton boolean NOT NULL DEFAULT true UNIQUE CHECK (singleton)
49
+ );
50
+
51
+ CREATE TABLE ${s}.definitions (
52
+ id text COLLATE "C" PRIMARY KEY,
53
+ body json NOT NULL
54
+ );
55
+
56
+ CREATE TABLE ${s}.sessions (
57
+ id text COLLATE "C" PRIMARY KEY,
58
+ body json NOT NULL,
59
+ status text GENERATED ALWAYS AS (${s}.doc(body)->>'status') STORED,
60
+ agent_id text GENERATED ALWAYS AS (${s}.doc(body)->>'agentId') STORED,
61
+ stream_incarnation text GENERATED ALWAYS AS (${s}.doc(body)->>'streamIncarnation') STORED,
62
+ next_event_seq bigint NOT NULL DEFAULT 0,
63
+ owner text,
64
+ epoch bigint NOT NULL DEFAULT 0,
65
+ owner_expires_at timestamptz
66
+ );
67
+ CREATE INDEX sessions_status ON ${s}.sessions (status, owner_expires_at);
68
+ CREATE INDEX sessions_agent ON ${s}.sessions (agent_id);
69
+
70
+ CREATE TABLE ${s}.commands (
71
+ id text COLLATE "C" PRIMARY KEY,
72
+ body json NOT NULL
73
+ );
74
+
75
+ CREATE TABLE ${s}.checkpoints (
76
+ id text COLLATE "C" PRIMARY KEY,
77
+ body json NOT NULL
78
+ );
79
+
80
+ CREATE TABLE ${s}.effects (
81
+ id text COLLATE "C" PRIMARY KEY,
82
+ body json NOT NULL,
83
+ session_id text GENERATED ALWAYS AS (${s}.doc(body)->'request'->>'sessionId') STORED,
84
+ turn_id text GENERATED ALWAYS AS (${s}.doc(body)->'request'->>'turnId') STORED,
85
+ kind text GENERATED ALWAYS AS (${s}.doc(body)->'request'->>'kind') STORED,
86
+ status text GENERATED ALWAYS AS (${s}.doc(body)->>'status') STORED
87
+ );
88
+ CREATE INDEX effects_session ON ${s}.effects (session_id, turn_id, status);
89
+ CREATE INDEX effects_status ON ${s}.effects (status, kind);
90
+
91
+ CREATE TABLE ${s}.actions (
92
+ id text COLLATE "C" PRIMARY KEY,
93
+ body json NOT NULL,
94
+ session_id text GENERATED ALWAYS AS (${s}.doc(body)->>'sessionId') STORED,
95
+ turn_id text GENERATED ALWAYS AS (${s}.doc(body)->>'turnId') STORED,
96
+ agent_id text GENERATED ALWAYS AS (${s}.doc(body)->>'agentId') STORED,
97
+ status text GENERATED ALWAYS AS (${s}.doc(body)->>'status') STORED,
98
+ kind text GENERATED ALWAYS AS (${s}.doc(body)->>'kind') STORED,
99
+ lease_expires_at text GENERATED ALWAYS AS (${s}.doc(body)->>'leaseExpiresAt') STORED
100
+ );
101
+ CREATE INDEX actions_session ON ${s}.actions (session_id, turn_id, status);
102
+ CREATE INDEX actions_agent ON ${s}.actions (agent_id, status);
103
+ CREATE INDEX actions_status ON ${s}.actions (status, kind);
104
+ CREATE INDEX actions_lease ON ${s}.actions (status, lease_expires_at);
105
+
106
+ CREATE TABLE ${s}.sandboxes (
107
+ id text COLLATE "C" PRIMARY KEY,
108
+ body json NOT NULL
109
+ );
110
+
111
+ CREATE TABLE ${s}.links (
112
+ id text COLLATE "C" PRIMARY KEY,
113
+ body json NOT NULL,
114
+ workflow_session_id text GENERATED ALWAYS AS (${s}.doc(body)->>'workflowSessionId') STORED
115
+ );
116
+ CREATE INDEX links_workflow ON ${s}.links (workflow_session_id);
117
+
118
+ CREATE TABLE ${s}.outbox (
119
+ session_id text COLLATE "C" NOT NULL,
120
+ seq bigint NOT NULL,
121
+ body json NOT NULL,
122
+ created_at text COLLATE "C" GENERATED ALWAYS AS (${s}.doc(body)->>'createdAt') STORED,
123
+ PRIMARY KEY (session_id, seq)
124
+ );
125
+
126
+ CREATE TABLE ${s}.executors (
127
+ agent_id text COLLATE "C" PRIMARY KEY,
128
+ token_hash text NOT NULL UNIQUE,
129
+ implementation_version text NOT NULL,
130
+ manifest_hash text,
131
+ principal_id text,
132
+ created_at text NOT NULL,
133
+ updated_at text NOT NULL
134
+ );
135
+
136
+ CREATE TABLE ${s}.principals (
137
+ id text COLLATE "C" PRIMARY KEY,
138
+ role text NOT NULL,
139
+ token_hash text NOT NULL UNIQUE,
140
+ idempotency_key text,
141
+ created_at text NOT NULL
142
+ );
143
+
144
+ CREATE TABLE ${s}.vaults (
145
+ id text COLLATE "C" PRIMARY KEY,
146
+ name text NOT NULL,
147
+ owner_user_id text NOT NULL,
148
+ metadata_json text,
149
+ created_at text COLLATE "C" NOT NULL,
150
+ scope text NOT NULL DEFAULT 'user' CHECK (scope IN ('user', 'host'))
151
+ );
152
+ CREATE INDEX vaults_owner ON ${s}.vaults (owner_user_id, created_at, id);
153
+ CREATE UNIQUE INDEX vaults_one_host ON ${s}.vaults (scope) WHERE scope = 'host';
154
+
155
+ CREATE TABLE ${s}.vault_credentials (
156
+ id text COLLATE "C" PRIMARY KEY,
157
+ vault_id text NOT NULL REFERENCES ${s}.vaults (id) ON DELETE CASCADE,
158
+ name text NOT NULL,
159
+ type text NOT NULL,
160
+ binding_json text NOT NULL,
161
+ expires_at text,
162
+ created_at text COLLATE "C" NOT NULL,
163
+ rotated_at text,
164
+ kek_id text NOT NULL,
165
+ nonce bytea NOT NULL,
166
+ ciphertext bytea NOT NULL,
167
+ wrapped_dek bytea NOT NULL
168
+ );
169
+ CREATE INDEX vault_credentials_vault ON ${s}.vault_credentials (vault_id, created_at, id);
170
+
171
+ CREATE TABLE ${s}.vault_audit (
172
+ ord bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
173
+ id text NOT NULL UNIQUE,
174
+ at text NOT NULL,
175
+ actor text NOT NULL,
176
+ action text NOT NULL,
177
+ vault_id text,
178
+ credential_id text,
179
+ session_id text,
180
+ target text,
181
+ outcome text NOT NULL
182
+ );
183
+ CREATE INDEX vault_audit_vault ON ${s}.vault_audit (vault_id, ord);
184
+
185
+ CREATE TABLE ${s}.vault_idempotency (
186
+ id text PRIMARY KEY,
187
+ body_hash text NOT NULL,
188
+ response text NOT NULL
189
+ );
190
+
191
+ CREATE TABLE ${s}.tenant_settings (
192
+ key text PRIMARY KEY,
193
+ value text NOT NULL
194
+ );
195
+ `,
196
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Forward-only, numbered migrations of one Tenant schema (architecture §14.6).
3
+ *
4
+ * Each schema records what it has applied in `<schema>.schema_version`, one
5
+ * row per migration; its version is the highest row. Migrating runs every
6
+ * missing step in order, in the caller's transaction, under a transaction-level
7
+ * advisory lock on the schema so two processes never migrate the same schema
8
+ * at once. A schema newer than the Runtime is never touched: migrating it
9
+ * throws a `schema-too-new` quarantine and the Tenant is quarantined.
10
+ *
11
+ * Adding a migration: append `NNN_name.ts` with the next version to
12
+ * `MIGRATIONS`. Never edit or reorder an applied migration.
13
+ */
14
+ import type { Sql, TransactionSql } from "postgres";
15
+ export interface Migration {
16
+ /** 1, 2, 3, … without gaps. */
17
+ version: number;
18
+ name: string;
19
+ /** DDL for the quoted schema name `s` (for example `"tenant_tn_…"`). */
20
+ up(s: string): string;
21
+ }
22
+ export declare const MIGRATIONS: readonly Migration[];
23
+ /** The schema version this Runtime writes and expects. */
24
+ export declare const POSTGRES_SCHEMA_VERSION: number;
25
+ /** Checks that versions run 1, 2, 3, … Throws otherwise. */
26
+ export declare function assertMigrations(migrations: readonly Migration[]): void;
27
+ /** Takes the schema's migration lock until the transaction ends. */
28
+ export declare function lockSchema(tx: TransactionSql, schema: string): Promise<void>;
29
+ export declare function schemaExists(sql: Sql | TransactionSql, schema: string): Promise<boolean>;
30
+ /**
31
+ * The schema's version: 0 when it exists without a `schema_version` table,
32
+ * undefined when the schema does not exist.
33
+ */
34
+ export declare function readSchemaVersion(sql: Sql | TransactionSql, schema: string): Promise<number | undefined>;
35
+ /**
36
+ * Creates the schema when missing and applies every missing migration, inside
37
+ * `tx`. Call `lockSchema` first when other processes may migrate concurrently
38
+ * (`migrateSchema` does). Throws a `schema-too-new` `QuarantineError` when the
39
+ * schema is ahead of `migrations`.
40
+ */
41
+ export declare function migrateSchemaInTx(tx: TransactionSql, schema: string, migrations?: readonly Migration[]): Promise<{
42
+ from: number;
43
+ to: number;
44
+ }>;
45
+ /** `migrateSchemaInTx` in its own transaction, under the schema's migration lock. */
46
+ export declare function migrateSchema(sql: Sql, schema: string, migrations?: readonly Migration[]): Promise<{
47
+ from: number;
48
+ to: number;
49
+ }>;
@@ -0,0 +1,75 @@
1
+ import { quarantine } from "../../../tenant/quarantine.js";
2
+ import { quoteIdentifier, tenantIdFromSchema } from "../names.js";
3
+ import { initial } from "./001_initial.js";
4
+ export const MIGRATIONS = [initial];
5
+ /** The schema version this Runtime writes and expects. */
6
+ export const POSTGRES_SCHEMA_VERSION = MIGRATIONS.at(-1).version;
7
+ /** Checks that versions run 1, 2, 3, … Throws otherwise. */
8
+ export function assertMigrations(migrations) {
9
+ migrations.forEach((migration, index) => {
10
+ if (migration.version !== index + 1)
11
+ throw new Error(`Migration ${migration.name} has version ${migration.version}, expected ${index + 1}`);
12
+ });
13
+ }
14
+ /** Takes the schema's migration lock until the transaction ends. */
15
+ export async function lockSchema(tx, schema) {
16
+ await tx `SELECT pg_advisory_xact_lock(hashtextextended(${`nylorun.schema:${schema}`}, 0))`;
17
+ }
18
+ export async function schemaExists(sql, schema) {
19
+ const rows = await sql `SELECT 1 FROM pg_namespace WHERE nspname = ${schema}`;
20
+ return rows.length > 0;
21
+ }
22
+ /**
23
+ * The schema's version: 0 when it exists without a `schema_version` table,
24
+ * undefined when the schema does not exist.
25
+ */
26
+ export async function readSchemaVersion(sql, schema) {
27
+ const [row] = await sql `
28
+ SELECT
29
+ EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = ${schema}) AS has_schema,
30
+ to_regclass(${`${quoteIdentifier(schema)}.schema_version`}) IS NOT NULL AS has_table`;
31
+ if (!row.has_schema)
32
+ return undefined;
33
+ if (!row.has_table)
34
+ return 0;
35
+ const [version] = await sql `
36
+ SELECT max(version)::int AS version FROM ${sql(`${schema}.schema_version`)}`;
37
+ return version?.version ?? 0;
38
+ }
39
+ /**
40
+ * Creates the schema when missing and applies every missing migration, inside
41
+ * `tx`. Call `lockSchema` first when other processes may migrate concurrently
42
+ * (`migrateSchema` does). Throws a `schema-too-new` `QuarantineError` when the
43
+ * schema is ahead of `migrations`.
44
+ */
45
+ export async function migrateSchemaInTx(tx, schema, migrations = MIGRATIONS) {
46
+ assertMigrations(migrations);
47
+ const s = quoteIdentifier(schema);
48
+ const latest = migrations.length;
49
+ await tx.unsafe(`
50
+ CREATE SCHEMA IF NOT EXISTS ${s};
51
+ CREATE TABLE IF NOT EXISTS ${s}.schema_version (
52
+ version integer PRIMARY KEY,
53
+ name text NOT NULL,
54
+ applied_at timestamptz NOT NULL DEFAULT now()
55
+ );`);
56
+ const from = (await readSchemaVersion(tx, schema)) ?? 0;
57
+ if (from > latest)
58
+ throw quarantine("schema-too-new", `Tenant schema version ${from} is newer than this Runtime's ${latest}`, { tenantId: tenantIdFromSchema(schema) ?? schema });
59
+ for (const migration of migrations.slice(from)) {
60
+ await tx.unsafe(migration.up(s));
61
+ await tx `INSERT INTO ${tx(`${schema}.schema_version`)} (version, name)
62
+ VALUES (${migration.version}, ${migration.name})`;
63
+ }
64
+ if (from < latest)
65
+ await tx `UPDATE ${tx(`${schema}.tenant`)} SET schema_version = ${latest}`;
66
+ return { from, to: latest };
67
+ }
68
+ /** `migrateSchemaInTx` in its own transaction, under the schema's migration lock. */
69
+ export async function migrateSchema(sql, schema, migrations = MIGRATIONS) {
70
+ const result = await sql.begin(async (tx) => {
71
+ await lockSchema(tx, schema);
72
+ return { result: await migrateSchemaInTx(tx, schema, migrations) };
73
+ });
74
+ return result.result;
75
+ }
@@ -0,0 +1,11 @@
1
+ export declare const TENANT_SCHEMA_PREFIX = "tenant_";
2
+ /** Postgres truncates identifiers longer than NAMEDATALEN - 1 bytes. */
3
+ export declare const MAX_IDENTIFIER_BYTES = 63;
4
+ /** The schema holding `tenantId`'s Session Store. Throws on an invalid id. */
5
+ export declare function tenantSchemaName(tenantId: string): string;
6
+ /** The Tenant id a schema name encodes, or undefined when it is not a Tenant schema. */
7
+ export declare function tenantIdFromSchema(schema: string): string | undefined;
8
+ /** Rejects names Postgres would truncate or that could not be quoted safely. */
9
+ export declare function assertIdentifier(name: string): void;
10
+ /** `"name"`, for SQL text built outside a tagged template (migrations). */
11
+ export declare function quoteIdentifier(name: string): string;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tenant id ↔ Postgres schema name (architecture §8.1, §12.2).
3
+ *
4
+ * A Tenant's Session Store is the schema `"tenant_<id>"`, for example
5
+ * `tn_01k8z3…` ↦ `"tenant_tn_01k8z3…"`. Tenant ids match
6
+ * `^tn_[0-9a-hjkmnp-tv-z]{26}$` (lowercase ASCII), so the name is 36 bytes,
7
+ * under Postgres' 63-byte identifier limit, and needs no case folding. It is
8
+ * always written quoted. The mapping is total and reversible: a schema that
9
+ * starts with `tenant_` but does not decode to a valid Tenant id is not a
10
+ * Tenant.
11
+ */
12
+ import { isTenantId } from "@nylorun/core/compatibility";
13
+ export const TENANT_SCHEMA_PREFIX = "tenant_";
14
+ /** Postgres truncates identifiers longer than NAMEDATALEN - 1 bytes. */
15
+ export const MAX_IDENTIFIER_BYTES = 63;
16
+ /** The schema holding `tenantId`'s Session Store. Throws on an invalid id. */
17
+ export function tenantSchemaName(tenantId) {
18
+ if (!isTenantId(tenantId))
19
+ throw new Error(`Invalid tenant id: ${tenantId}`);
20
+ const schema = `${TENANT_SCHEMA_PREFIX}${tenantId}`;
21
+ assertIdentifier(schema);
22
+ return schema;
23
+ }
24
+ /** The Tenant id a schema name encodes, or undefined when it is not a Tenant schema. */
25
+ export function tenantIdFromSchema(schema) {
26
+ if (!schema.startsWith(TENANT_SCHEMA_PREFIX))
27
+ return undefined;
28
+ const id = schema.slice(TENANT_SCHEMA_PREFIX.length);
29
+ return isTenantId(id) ? id : undefined;
30
+ }
31
+ /** Rejects names Postgres would truncate or that could not be quoted safely. */
32
+ export function assertIdentifier(name) {
33
+ if (name.length === 0 ||
34
+ Buffer.byteLength(name, "utf8") > MAX_IDENTIFIER_BYTES ||
35
+ !/^[a-z_][a-z0-9_]*$/.test(name))
36
+ throw new Error(`Invalid Postgres identifier: ${name}`);
37
+ }
38
+ /** `"name"`, for SQL text built outside a tagged template (migrations). */
39
+ export function quoteIdentifier(name) {
40
+ assertIdentifier(name);
41
+ return `"${name}"`;
42
+ }
@@ -0,0 +1,11 @@
1
+ import type { Sql } from "postgres";
2
+ import type { SessionStore, SessionStoreOptions } from "../types.js";
3
+ export interface PostgresSessionStoreOptions extends SessionStoreOptions {
4
+ /** The shared pool. The store never ends it. */
5
+ sql: Sql;
6
+ /** The Tenant schema. Defaults to `tenantSchemaName(tenantId)`. */
7
+ schema?: string;
8
+ /** The version `health()` expects. Defaults to `POSTGRES_SCHEMA_VERSION`. */
9
+ schemaVersion?: number;
10
+ }
11
+ export declare function createPostgresSessionStore(options: PostgresSessionStoreOptions): SessionStore;