@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,31 +0,0 @@
1
- /**
2
- * Forward-only migrations of the shared `nylorun_streams` schema (Durable Streams §6): the
3
- * record of every Tenant's session events, the log heads, the relay's state and the
4
- * publication the stream relay reads. Unlike Tenant schemas, there is one for the database;
5
- * the Host migrates it at startup, before any Tenant opens, under an advisory lock.
6
- *
7
- * Adding a migration: append to `STREAMS_MIGRATIONS` with the next version. Never edit or
8
- * reorder an applied one.
9
- */
10
- import type { Sql } from "postgres";
11
- import { type Migration } from "../index.js";
12
- export declare const STREAMS_SCHEMA = "nylorun_streams";
13
- /** The publication the stream relay subscribes to. */
14
- export declare const STREAMS_PUBLICATION = "nylorun_stream_relay";
15
- export declare const STREAMS_MIGRATIONS: readonly Migration[];
16
- /** The shared schema version this Runtime writes and expects. */
17
- export declare const STREAMS_SCHEMA_VERSION: number;
18
- /**
19
- * Creates `nylorun_streams` when missing and applies every missing migration, in one
20
- * transaction under the schema's migration lock. Throws when the schema is newer than this
21
- * Runtime.
22
- */
23
- export declare function migrateStreamsSchema(sql: Sql, migrations?: readonly Migration[]): Promise<{
24
- from: number;
25
- to: number;
26
- }>;
27
- /**
28
- * Throws, naming the setting, when Postgres cannot run the stream relay: logical decoding
29
- * needs `wal_level = logical` (a restart) and a role allowed to replicate.
30
- */
31
- export declare function assertLogicalReplication(sql: Sql): Promise<void>;
@@ -1,92 +0,0 @@
1
- import { assertMigrations, lockSchema } from "../index.js";
2
- export const STREAMS_SCHEMA = "nylorun_streams";
3
- /** The publication the stream relay subscribes to. */
4
- export const STREAMS_PUBLICATION = "nylorun_stream_relay";
5
- export const STREAMS_MIGRATIONS = [
6
- {
7
- version: 1,
8
- name: "record",
9
- up: (s) => `
10
- CREATE TABLE ${s}.session_log_heads (
11
- tenant_id text COLLATE "C" NOT NULL,
12
- session_id text COLLATE "C" NOT NULL,
13
- -- The basin generation the session's events go to.
14
- generation integer NOT NULL,
15
- -- The next seq.
16
- head bigint NOT NULL DEFAULT 0,
17
- PRIMARY KEY (tenant_id, session_id)
18
- );
19
- CREATE TABLE ${s}.session_events (
20
- tenant_id text COLLATE "C" NOT NULL,
21
- session_id text COLLATE "C" NOT NULL,
22
- seq bigint NOT NULL,
23
- generation integer NOT NULL,
24
- type text NOT NULL,
25
- -- The nylorun.event/2 envelope, as written.
26
- body json NOT NULL,
27
- committed_at timestamptz NOT NULL DEFAULT now(),
28
- PRIMARY KEY (tenant_id, session_id, seq)
29
- );
30
- -- One row per replication slot: whether the record still has to be reconciled with S2
31
- -- after the slot was created (a crash during reconciliation leaves it pending).
32
- CREATE TABLE ${s}.relay_slots (
33
- slot_name text PRIMARY KEY,
34
- reconcile_pending boolean NOT NULL
35
- );
36
- CREATE PUBLICATION ${STREAMS_PUBLICATION}
37
- FOR TABLE ${s}.session_events WITH (publish = 'insert');
38
- `,
39
- },
40
- ];
41
- /** The shared schema version this Runtime writes and expects. */
42
- export const STREAMS_SCHEMA_VERSION = STREAMS_MIGRATIONS.at(-1).version;
43
- /**
44
- * Creates `nylorun_streams` when missing and applies every missing migration, in one
45
- * transaction under the schema's migration lock. Throws when the schema is newer than this
46
- * Runtime.
47
- */
48
- export async function migrateStreamsSchema(sql, migrations = STREAMS_MIGRATIONS) {
49
- assertMigrations(migrations);
50
- const result = await sql.begin(async (tx) => {
51
- await lockSchema(tx, STREAMS_SCHEMA);
52
- return { result: await migrateInTx(tx, migrations) };
53
- });
54
- return result.result;
55
- }
56
- async function migrateInTx(tx, migrations) {
57
- const s = STREAMS_SCHEMA;
58
- await tx.unsafe(`
59
- CREATE SCHEMA IF NOT EXISTS ${s};
60
- CREATE TABLE IF NOT EXISTS ${s}.schema_version (
61
- version integer PRIMARY KEY,
62
- name text NOT NULL,
63
- applied_at timestamptz NOT NULL DEFAULT now()
64
- );`);
65
- const [row] = await tx `
66
- SELECT max(version)::int AS version FROM ${tx(`${s}.schema_version`)}`;
67
- const from = row?.version ?? 0;
68
- const latest = migrations.length;
69
- if (from > latest)
70
- throw new Error(`The ${s} schema is at version ${from}, newer than this Runtime's ${latest}: upgrade the Runtime`);
71
- for (const migration of migrations.slice(from)) {
72
- await tx.unsafe(migration.up(s));
73
- await tx `INSERT INTO ${tx(`${s}.schema_version`)} (version, name)
74
- VALUES (${migration.version}, ${migration.name})`;
75
- }
76
- return { from, to: latest };
77
- }
78
- /**
79
- * Throws, naming the setting, when Postgres cannot run the stream relay: logical decoding
80
- * needs `wal_level = logical` (a restart) and a role allowed to replicate.
81
- */
82
- export async function assertLogicalReplication(sql) {
83
- const [row] = await sql `
84
- SELECT current_setting('wal_level') AS wal_level,
85
- (SELECT rolreplication OR rolsuper FROM pg_roles WHERE rolname = current_user) AS replicates`;
86
- if (row?.wal_level !== "logical")
87
- throw new Error(`Postgres has wal_level = ${row?.wal_level ?? "unknown"}; the stream relay needs logical replication. ` +
88
- "Set wal_level = logical and restart Postgres (`nylorun start` does this for the local stack; " +
89
- "on a managed Postgres, turn on its logical replication option). See DEPLOYMENT.md.");
90
- if (!row.replicates)
91
- throw new Error("The Runtime's Postgres role cannot replicate: grant it REPLICATION (ALTER ROLE … REPLICATION). See DEPLOYMENT.md.");
92
- }
@@ -1,11 +0,0 @@
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;
@@ -1,42 +0,0 @@
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
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * The Tenant catalog on one Postgres database (architecture §8.2).
3
- *
4
- * A Tenant exists when its schema `"tenant_<id>"` exists (see `names.ts`); the
5
- * Host keeps no other registry. The envelope is the one row of the schema's
6
- * `tenant` table, and its `schemaVersion` is the schema's migrated version.
7
- *
8
- * - `createTenant` runs in one transaction: create the schema, migrate it,
9
- * write the envelope and the bootstrap principals. A crash leaves nothing.
10
- * Creating an existing Tenant with the same bootstrap principals returns
11
- * `exists`; with different ones it throws `TenantConflictError`.
12
- * - `openTenant` migrates an older schema forward and returns its store. A
13
- * schema newer than this Runtime, a failed migration or an unreadable
14
- * envelope quarantines that Tenant only. A lost connection or an unavailable
15
- * server says nothing about the Tenant, so it is thrown, never a quarantine.
16
- * - `deleteTenant` drops the schema with everything in it.
17
- *
18
- * Create, migrate and delete of one schema are serialized by a
19
- * transaction-level advisory lock on the schema name.
20
- */
21
- import type { Sql } from "postgres";
22
- import { type TenantEnvelope } from "@nylorun/core/contracts";
23
- import type { BootstrapPrincipal, Quarantine } from "../../tenant/types.js";
24
- import type { SessionStore } from "../types.js";
25
- import { type Migration } from "./migrations/index.js";
26
- export interface PostgresTenantCatalogOptions {
27
- /** The shared pool. The catalog and its stores never end it. */
28
- sql: Sql;
29
- /** Clock for principal `createdAt`. Defaults to `() => new Date()`. */
30
- now?: () => Date;
31
- /** Passed to every opened store. */
32
- onError?: (error: unknown) => void;
33
- /** Tests only: the migrations this Runtime knows. */
34
- migrations?: readonly Migration[];
35
- }
36
- export interface TenantListing {
37
- id: string;
38
- /** Null when the envelope cannot be read; `quarantine` says why. */
39
- envelope: TenantEnvelope | null;
40
- quarantine?: Quarantine;
41
- }
42
- export type CreateTenantResult = {
43
- status: "created";
44
- envelope: TenantEnvelope;
45
- } | {
46
- status: "exists";
47
- envelope: TenantEnvelope;
48
- };
49
- export type OpenTenantResult = {
50
- status: "ok";
51
- store: SessionStore;
52
- envelope: TenantEnvelope;
53
- migrated: {
54
- from: number;
55
- to: number;
56
- };
57
- } | {
58
- status: "quarantined";
59
- reason: Quarantine;
60
- } | {
61
- status: "not-found";
62
- };
63
- export interface PostgresTenantCatalog {
64
- /** Every `tenant_*` schema that encodes a Tenant id, ordered by id. */
65
- listTenants(): Promise<TenantListing[]>;
66
- /** The ids of `listTenants`, without reading any envelope. */
67
- listTenantIds(): Promise<string[]>;
68
- /** Whether the Tenant's schema exists. */
69
- tenantExists(id: string): Promise<boolean>;
70
- /** Throws a `QuarantineError` (`envelope-invalid`) when unreadable. */
71
- readEnvelope(id: string): Promise<TenantEnvelope>;
72
- createTenant(input: {
73
- envelope: TenantEnvelope;
74
- principals: BootstrapPrincipal;
75
- }): Promise<CreateTenantResult>;
76
- /** Whether the stored bootstrap (and Studio) principals equal `bootstrap`. */
77
- bootstrapMatches(id: string, bootstrap: BootstrapPrincipal): Promise<boolean>;
78
- openTenant(id: string, options?: {
79
- now?: () => Date;
80
- onError?: (error: unknown) => void;
81
- }): Promise<OpenTenantResult>;
82
- /** Migrates an existing Tenant schema. Throws `schema-too-new` or the migration error. */
83
- migrateTenant(id: string): Promise<{
84
- from: number;
85
- to: number;
86
- }>;
87
- /** Drops the schema and everything in it. Returns false when it did not exist. */
88
- deleteTenant(id: string): Promise<boolean>;
89
- }
90
- export declare function createPostgresTenantCatalog(options: PostgresTenantCatalogOptions): PostgresTenantCatalog;
@@ -1,228 +0,0 @@
1
- import { isTenantId } from "@nylorun/core/compatibility";
2
- import { TenantEnvelopeSchema, } from "@nylorun/core/contracts";
3
- import { STUDIO_PRINCIPAL_ID } from "../../tenant/principals.js";
4
- import { TenantConflictError, asQuarantine, quarantine, } from "../../tenant/quarantine.js";
5
- import { MIGRATIONS, lockSchema, migrateSchemaInTx, readSchemaVersion, schemaExists, } from "./migrations/index.js";
6
- import { TENANT_SCHEMA_PREFIX, tenantIdFromSchema, tenantSchemaName } from "./names.js";
7
- import { createPostgresSessionStore } from "./store.js";
8
- import { STREAMS_SCHEMA, migrateStreamsSchema } from "./migrations/shared/index.js";
9
- /**
10
- * Whether Postgres rejected a statement (a SQLSTATE), rather than the connection or the
11
- * server failing (classes 08, 53, 57, and the driver's own connection errors). Only the
12
- * first says something about the Tenant's schema.
13
- */
14
- function isStatementError(error) {
15
- const code = error?.code;
16
- return (typeof code === "string" &&
17
- /^[0-9A-Z]{5}$/.test(code) &&
18
- !/^(08|53|57)/.test(code));
19
- }
20
- export function createPostgresTenantCatalog(options) {
21
- const { sql } = options;
22
- const now = options.now ?? (() => new Date());
23
- // The shared record (`nylorun_streams`) exists before any Tenant is created or opened.
24
- let streamsReady;
25
- const ensureStreams = () => (streamsReady ??= migrateStreamsSchema(sql).catch((error) => {
26
- streamsReady = undefined;
27
- throw error;
28
- }));
29
- const migrations = options.migrations ?? MIGRATIONS;
30
- const latest = migrations.length;
31
- async function readEnvelopeWith(q, id) {
32
- const schema = tenantSchemaName(id);
33
- const invalid = (message) => quarantine("envelope-invalid", message, { tenantId: id });
34
- let rows;
35
- try {
36
- rows = await q `
37
- SELECT id, name, created_at, updated_at, schema_version
38
- FROM ${q(`${schema}.tenant`)}`;
39
- }
40
- catch (error) {
41
- // A missing schema or `tenant` table is the Tenant's problem; anything else
42
- // (a lost connection) is not, and is thrown as it is.
43
- const code = error.code;
44
- if (code !== "42P01" && code !== "3F000")
45
- throw error;
46
- throw invalid(`Tenant envelope could not be read: ${error.message}`);
47
- }
48
- if (rows.length !== 1)
49
- throw invalid("Tenant envelope is missing");
50
- const row = rows[0];
51
- const parsed = TenantEnvelopeSchema.safeParse({
52
- id: row.id,
53
- name: row.name,
54
- createdAt: row.created_at,
55
- updatedAt: row.updated_at,
56
- schemaVersion: row.schema_version,
57
- });
58
- if (!parsed.success)
59
- throw invalid("Tenant envelope is not valid");
60
- if (parsed.data.id !== id)
61
- throw invalid(`Tenant envelope id ${parsed.data.id} does not match schema ${schema}`);
62
- return parsed.data;
63
- }
64
- async function bootstrapMatchesWith(q, id, bootstrap) {
65
- const schema = tenantSchemaName(id);
66
- const rows = await q `
67
- SELECT id, token_hash, idempotency_key FROM ${q(`${schema}.principals`)}
68
- WHERE role = 'application'`;
69
- const app = rows.find((row) => row.id === bootstrap.principalId);
70
- if (!app ||
71
- app.token_hash !== bootstrap.credentialHash ||
72
- app.idempotency_key !== bootstrap.idempotencyKey)
73
- return false;
74
- const studio = rows.find((row) => row.id === STUDIO_PRINCIPAL_ID);
75
- if (studio?.token_hash !== bootstrap.studioCredentialHash)
76
- return false;
77
- return (bootstrap.derivedPrincipals ?? []).every((principal) => rows.find((row) => row.id === principal.id)?.token_hash ===
78
- principal.credentialHash);
79
- }
80
- const catalog = {
81
- async listTenantIds() {
82
- const schemas = await sql `
83
- SELECT nspname FROM pg_namespace
84
- WHERE starts_with(nspname, ${TENANT_SCHEMA_PREFIX})
85
- ORDER BY nspname COLLATE "C"`;
86
- return schemas.flatMap(({ nspname }) => tenantIdFromSchema(nspname) ?? []);
87
- },
88
- async tenantExists(id) {
89
- return isTenantId(id) && schemaExists(sql, tenantSchemaName(id));
90
- },
91
- async listTenants() {
92
- const listings = [];
93
- for (const id of await catalog.listTenantIds()) {
94
- try {
95
- listings.push({ id, envelope: await readEnvelopeWith(sql, id) });
96
- }
97
- catch (error) {
98
- listings.push({
99
- id,
100
- envelope: null,
101
- quarantine: asQuarantine(error) ??
102
- quarantine("open-failed", "Tenant schema could not be read", {
103
- tenantId: id,
104
- }).toQuarantine(),
105
- });
106
- }
107
- }
108
- return listings;
109
- },
110
- readEnvelope(id) {
111
- return readEnvelopeWith(sql, id);
112
- },
113
- async createTenant({ envelope, principals }) {
114
- await ensureStreams();
115
- const input = TenantEnvelopeSchema.parse(envelope);
116
- const schema = tenantSchemaName(input.id);
117
- const outcome = await sql.begin(async (tx) => {
118
- await lockSchema(tx, schema);
119
- if (await schemaExists(tx, schema))
120
- return { created: false };
121
- await migrateSchemaInTx(tx, schema, migrations);
122
- const stored = { ...input, schemaVersion: latest };
123
- await tx `
124
- INSERT INTO ${tx(`${schema}.tenant`)} (id, name, created_at, updated_at, schema_version)
125
- VALUES (${stored.id}, ${stored.name}, ${stored.createdAt}, ${stored.updatedAt}, ${stored.schemaVersion})`;
126
- const createdAt = now().toISOString();
127
- await tx `
128
- INSERT INTO ${tx(`${schema}.principals`)} (id, role, token_hash, idempotency_key, created_at)
129
- VALUES (${principals.principalId}, 'application', ${principals.credentialHash},
130
- ${principals.idempotencyKey}, ${createdAt})`;
131
- if (principals.studioCredentialHash)
132
- await tx `
133
- INSERT INTO ${tx(`${schema}.principals`)} (id, role, token_hash, idempotency_key, created_at)
134
- VALUES (${STUDIO_PRINCIPAL_ID}, 'application', ${principals.studioCredentialHash},
135
- NULL, ${createdAt})`;
136
- for (const derived of principals.derivedPrincipals ?? [])
137
- await tx `
138
- INSERT INTO ${tx(`${schema}.principals`)} (id, role, token_hash, idempotency_key, created_at)
139
- VALUES (${derived.id}, 'application', ${derived.credentialHash}, NULL, ${createdAt})`;
140
- return { created: true, envelope: stored };
141
- });
142
- if (outcome.created)
143
- return { status: "created", envelope: outcome.envelope };
144
- if (!(await bootstrapMatchesWith(sql, input.id, principals)))
145
- throw new TenantConflictError();
146
- return { status: "exists", envelope: await readEnvelopeWith(sql, input.id) };
147
- },
148
- async bootstrapMatches(id, bootstrap) {
149
- if (!(await schemaExists(sql, tenantSchemaName(id))))
150
- return false;
151
- return bootstrapMatchesWith(sql, id, bootstrap);
152
- },
153
- async openTenant(id, storeOptions = {}) {
154
- if (!isTenantId(id))
155
- return { status: "not-found" };
156
- await ensureStreams();
157
- const schema = tenantSchemaName(id);
158
- const version = await readSchemaVersion(sql, schema);
159
- if (version === undefined)
160
- return { status: "not-found" };
161
- if (version > latest)
162
- return {
163
- status: "quarantined",
164
- reason: quarantine("schema-too-new", `Tenant schema version ${version} is newer than this Runtime's ${latest}`, { tenantId: id }).toQuarantine(),
165
- };
166
- let migrated = { from: version, to: version };
167
- if (version < latest) {
168
- try {
169
- migrated = await catalog.migrateTenant(id);
170
- }
171
- catch (error) {
172
- if (!asQuarantine(error) && !isStatementError(error))
173
- throw error;
174
- return {
175
- status: "quarantined",
176
- reason: asQuarantine(error) ??
177
- quarantine("migration-failed", `Tenant schema migration from version ${version} failed: ${error.message}`, { tenantId: id }).toQuarantine(),
178
- };
179
- }
180
- }
181
- let envelope;
182
- try {
183
- envelope = await readEnvelopeWith(sql, id);
184
- }
185
- catch (error) {
186
- const reason = asQuarantine(error);
187
- if (!reason)
188
- throw error;
189
- return { status: "quarantined", reason };
190
- }
191
- const store = createPostgresSessionStore({
192
- sql,
193
- tenantId: id,
194
- schema,
195
- schemaVersion: latest,
196
- now: storeOptions.now,
197
- onError: storeOptions.onError ?? options.onError,
198
- });
199
- return { status: "ok", store, envelope, migrated };
200
- },
201
- async migrateTenant(id) {
202
- const schema = tenantSchemaName(id);
203
- const result = await sql.begin(async (tx) => {
204
- await lockSchema(tx, schema);
205
- if (!(await schemaExists(tx, schema)))
206
- throw new Error(`Tenant ${id} does not exist`);
207
- return { result: await migrateSchemaInTx(tx, schema, migrations) };
208
- });
209
- return result.result;
210
- },
211
- async deleteTenant(id) {
212
- const schema = tenantSchemaName(id);
213
- return sql.begin(async (tx) => {
214
- await lockSchema(tx, schema);
215
- if (!(await schemaExists(tx, schema)))
216
- return false;
217
- await tx `DROP SCHEMA ${tx(schema)} CASCADE`;
218
- // The Tenant's share of the record goes with it (Durable Streams §15).
219
- if ((await schemaExists(tx, STREAMS_SCHEMA))) {
220
- await tx `DELETE FROM ${tx(`${STREAMS_SCHEMA}.session_events`)} WHERE tenant_id = ${id}`;
221
- await tx `DELETE FROM ${tx(`${STREAMS_SCHEMA}.session_log_heads`)} WHERE tenant_id = ${id}`;
222
- }
223
- return true;
224
- });
225
- },
226
- };
227
- return catalog;
228
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * An in-memory record with a change source that behaves like a logical replication slot:
3
- * committed transactions are kept until acknowledged and replayed to the next reader, one
4
- * reader holds the slot at a time, and a dropped slot starts fresh. For unit tests and the
5
- * in-memory Session Store. Not durable.
6
- */
7
- import type { ChangeHandlers, ChangeSource, CommittedTx, LogHead, RecordReader, RecordRow } from "./types.js";
8
- export declare class MemoryRecord implements RecordReader {
9
- /** `tenantId\0sessionId` → rows by seq. */
10
- private readonly rows;
11
- private readonly generations;
12
- /** Committed transactions since the slot's confirmed position. */
13
- private retained;
14
- private lsn;
15
- private slot;
16
- private reader;
17
- /** Commits rows as one transaction (the record is written by the Session Store). */
18
- commit(rows: readonly RecordRow[]): void;
19
- /** Sets a Tenant's basin generation (a reset moves it on). */
20
- setGeneration(tenantId: string, generation: number): void;
21
- /** Deletes a Tenant's rows, or one session's, as a reset or deletion does. */
22
- deleteRows(tenantId: string, sessionId?: string): void;
23
- /** Forgets a Tenant (deletion). */
24
- deleteTenant(tenantId: string): void;
25
- /** Drops the slot, as `max_slot_wal_keep_size` or an operator would. */
26
- dropSlot(): void;
27
- /** Ends the reader's connection but keeps the slot, as a network failure would. */
28
- disconnect(): void;
29
- /** A change source over this record; one at a time holds the slot. */
30
- source(): ChangeSource;
31
- readRange(tenantId: string, sessionId: string, from: number, to: number): Promise<RecordRow[]>;
32
- heads(after: {
33
- tenantId: string;
34
- sessionId: string;
35
- } | undefined, limit: number): Promise<LogHead[]>;
36
- generation(tenantId: string): Promise<number | undefined>;
37
- /** @internal */
38
- attach(reader: MemoryChangeSource): {
39
- fresh: boolean;
40
- replay: CommittedTx[];
41
- } | undefined;
42
- /** @internal */
43
- detach(reader: MemoryChangeSource): void;
44
- /** @internal */
45
- reconciled(): void;
46
- /** @internal */
47
- confirm(lsn: string): void;
48
- /** The slot's confirmed position (tests). */
49
- confirmed(): number | undefined;
50
- }
51
- declare class MemoryChangeSource implements ChangeSource {
52
- private readonly record;
53
- private handlers;
54
- private timer;
55
- private active;
56
- private stopped;
57
- constructor(record: MemoryRecord);
58
- start(handlers: ChangeHandlers): void;
59
- acknowledge(lsn: string): void;
60
- reconciled(): Promise<void>;
61
- stop(): Promise<void>;
62
- /** @internal */
63
- deliver(tx: CommittedTx): void;
64
- /** @internal */
65
- lost(error: Error): void;
66
- private tryAttach;
67
- private retry;
68
- }
69
- export {};