@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,41 +1,27 @@
1
1
  import type { StreamRelayStatus } from "@nylorun/core/contracts";
2
- import { isQuarantineError, quarantine, TenantBusyError, TenantConflictError } from "./quarantine.js";
3
- import { TenantNotFoundError, TenantUnavailableError, type Logger, type OpenTenantRuntime, type TenantConfig, type TenantModule, type TenantStore } from "./types.js";
2
+ import { TenantUnavailableError, type Logger, type TenantHandle, type TenantModule, type TenantOpener } from "./types.js";
4
3
  export interface CreateTenantModuleOptions {
5
- store: TenantStore;
6
- /** Unused: the store derives paths. Accepted so callers can pass the store's options. */
7
- hostRoot?: string;
8
- /** Unused: the store opens Tenants. Accepted so callers can pass the store's options. */
9
- openRuntime?: OpenTenantRuntime;
10
- /** Unused; see `openRuntime`. */
11
- configFor?: (tenantId: string) => TenantConfig;
4
+ /** Opens the Host's Tenant (`store-pg.ts`). */
5
+ open: TenantOpener;
12
6
  logger: Logger;
13
- /** Bound on one Tenant open; a slower open quarantines it with `open-timeout`. */
7
+ /** Bound on opening the Tenant; a slower open fails with `open-timeout`. */
14
8
  openTimeoutMs?: number;
15
9
  /**
16
- * Runs after a Tenant is created in the store, before it opens: creates what lives outside
17
- * it (its Durable Streams basin). A failure is logged; opening the Tenant repairs it.
10
+ * Runs once the Tenant is open, at start or on a later retry: the Host starts what needs the
11
+ * Tenant's id (its stream relay). A failure is logged; the Tenant stays open.
18
12
  */
19
- onCreated?: (tenantId: string) => Promise<void>;
20
- /**
21
- * Runs after a Tenant is removed from the store: removes what lives outside it (its
22
- * Durable Streams basin, its armed sweep). A failure is logged; the Tenant stays deleted.
23
- */
24
- onDeleted?: (tenantId: string) => Promise<void>;
13
+ onOpen?: (handle: TenantHandle) => void | Promise<void>;
25
14
  /** This process's stream relay, for the Host aggregate (a Host with S2 runs one). */
26
15
  relayStatus?: () => Promise<StreamRelayStatus>;
27
16
  }
28
17
  /**
29
- * Deep Tenant module (§8): discovery, create, resolve, list, delete and summarize over an
30
- * injected `TenantStore`.
18
+ * Deep Tenant module (§8): the Host's one Tenant over an injected opener (tenancy.md §5).
31
19
  *
32
- * Handles open on demand and are cached (architecture §8.2). The first call that needs a
33
- * Tenant (`resolve`, `status`, `worker`, `delete`) opens it once, even when several ask at
34
- * the same time; a failed open is cached as a quarantine, and a missing Tenant is not
35
- * cached. A failure outside the Tenant (`TenantUnavailableError`: the database is down, its
36
- * sweep could not be armed) is thrown to the caller and not cached either, so the next use
37
- * tries again. A Tenant being deleted resolves as not found, so a late request or Worker
38
- * call cannot reopen it while its storage is removed.
20
+ * `start` opens it once, even when requests or Worker calls ask at the same time. An open that
21
+ * fails for a reason in the Tenant (a `TenantOpenError`, or a timeout) is kept as the Tenant's
22
+ * cause: the Host is not ready, `/v1/admin/status` names it, requests get the opaque 404.
23
+ * One that fails outside the Tenant (`TenantUnavailableError`) is retried in the background
24
+ * with backoff, and by the next request, which gets 503 meanwhile.
39
25
  */
40
26
  export declare function createTenantModule(options: CreateTenantModuleOptions): TenantModule;
41
- export { TenantBusyError, TenantConflictError, TenantNotFoundError, TenantUnavailableError, isQuarantineError, quarantine, };
27
+ export { TenantUnavailableError };
@@ -1,316 +1,176 @@
1
- import { isTenantId } from "@nylorun/core/compatibility";
2
- import { POSTGRES_SCHEMA_VERSION } from "../store/postgres/migrations/index.js";
3
- import { envelopeNow } from "./envelope.js";
1
+ import { openError, TenantOpenError } from "./cause.js";
4
2
  import { TimeoutError, withTimeout } from "./pool.js";
5
- import { asQuarantine, isQuarantineError, quarantine, TenantBusyError, TenantConflictError, } from "./quarantine.js";
6
- import { TenantNotFoundError, TenantUnavailableError, } from "./types.js";
3
+ import { TenantUnavailableError, } from "./types.js";
7
4
  const OPEN_TIMEOUT_MS = 30_000;
8
- function hasLiveWork(summary) {
9
- return (summary.runningSessions > 0 ||
10
- summary.inFlightDeliveries > 0 ||
11
- summary.pendingActions > 0);
12
- }
13
- function toAdmin(id, entry, envelope) {
14
- if (entry?.kind === "open") {
15
- return {
16
- id,
17
- name: entry.handle.envelope.name,
18
- state: "open",
19
- envelope: entry.handle.envelope,
20
- };
21
- }
22
- return {
23
- id,
24
- name: envelope?.name ?? null,
25
- state: entry ? "quarantined" : "open",
26
- envelope,
27
- };
28
- }
29
- /** The quarantine an open failure carries, from either quarantine error type. */
30
- function quarantineOf(error) {
31
- const carried = error?.quarantine;
32
- if (carried && typeof carried === "object" && "code" in carried)
33
- return carried;
34
- return asQuarantine(error);
35
- }
5
+ /** Retries of an open that failed outside the Tenant: from 1 s, doubling, at most 30 s apart. */
6
+ const RETRY_FIRST_MS = 1_000;
7
+ const RETRY_MAX_MS = 30_000;
36
8
  /**
37
- * Deep Tenant module (§8): discovery, create, resolve, list, delete and summarize over an
38
- * injected `TenantStore`.
9
+ * Deep Tenant module (§8): the Host's one Tenant over an injected opener (tenancy.md §5).
39
10
  *
40
- * Handles open on demand and are cached (architecture §8.2). The first call that needs a
41
- * Tenant (`resolve`, `status`, `worker`, `delete`) opens it once, even when several ask at
42
- * the same time; a failed open is cached as a quarantine, and a missing Tenant is not
43
- * cached. A failure outside the Tenant (`TenantUnavailableError`: the database is down, its
44
- * sweep could not be armed) is thrown to the caller and not cached either, so the next use
45
- * tries again. A Tenant being deleted resolves as not found, so a late request or Worker
46
- * call cannot reopen it while its storage is removed.
11
+ * `start` opens it once, even when requests or Worker calls ask at the same time. An open that
12
+ * fails for a reason in the Tenant (a `TenantOpenError`, or a timeout) is kept as the Tenant's
13
+ * cause: the Host is not ready, `/v1/admin/status` names it, requests get the opaque 404.
14
+ * One that fails outside the Tenant (`TenantUnavailableError`) is retried in the background
15
+ * with backoff, and by the next request, which gets 503 meanwhile.
47
16
  */
48
17
  export function createTenantModule(options) {
49
- const { store, logger, openTimeoutMs = OPEN_TIMEOUT_MS } = options;
50
- const entries = new Map();
51
- const opening = new Map();
52
- const deleting = new Set();
53
- let started = false;
54
- let closed = false;
55
- function rememberQuarantine(id, q) {
56
- const entry = { kind: "quarantined", quarantine: q };
57
- entries.set(id, entry);
58
- logger.warn("tenant quarantined", {
59
- tenantId: id,
60
- code: q.code,
61
- repair: q.repair,
62
- });
63
- return entry;
64
- }
65
- async function openEntry(id) {
18
+ const { logger, openTimeoutMs = OPEN_TIMEOUT_MS } = options;
19
+ let state = { kind: "idle" };
20
+ let opening;
21
+ let retry;
22
+ let nextDelayMs = RETRY_FIRST_MS;
23
+ const current = () => state;
24
+ async function openOnce() {
66
25
  let handle;
67
26
  try {
68
- handle = await withTimeout(store.open(id), openTimeoutMs, (late) => {
27
+ handle = await withTimeout(options.open(), openTimeoutMs, (late) => {
69
28
  void late.then((h) => h.close()).catch(() => undefined);
70
29
  });
71
30
  }
72
31
  catch (error) {
73
- if (error instanceof TenantNotFoundError)
74
- return undefined;
75
- if (closed || deleting.has(id))
76
- return undefined;
32
+ if (current().kind === "closed")
33
+ return;
77
34
  if (error instanceof TenantUnavailableError) {
78
- // Not the Tenant's fault: not cached, so the next use opens it again.
35
+ state = { kind: "unavailable", error };
79
36
  logger.warn("tenant unavailable", {
80
- tenantId: id,
81
37
  message: error.cause instanceof Error ? error.cause.message : String(error.cause),
38
+ retryMs: nextDelayMs,
82
39
  });
83
- throw error;
40
+ scheduleRetry();
41
+ return;
84
42
  }
85
- if (error instanceof TimeoutError)
86
- return rememberQuarantine(id, quarantine("open-timeout", `open timed out after ${openTimeoutMs}ms`, {
87
- tenantId: id,
88
- }).toQuarantine());
89
- return rememberQuarantine(id, quarantineOf(error) ??
90
- quarantine("open-failed", error instanceof Error ? error.message : "open failed", { tenantId: id }).toQuarantine());
43
+ const failure = error instanceof TenantOpenError
44
+ ? error
45
+ : error instanceof TimeoutError
46
+ ? openError("open-timeout", `Opening the Tenant timed out after ${openTimeoutMs}ms`)
47
+ : openError("open-failed", error instanceof Error ? error.message : "Opening the Tenant failed");
48
+ const failed = {
49
+ kind: "failed",
50
+ cause: failure.toCause(),
51
+ envelope: failure.envelope ?? null,
52
+ };
53
+ state = failed;
54
+ logger.error("tenant not opened", {
55
+ ...(failed.envelope ? { tenantId: failed.envelope.id } : {}),
56
+ code: failed.cause.code,
57
+ message: failed.cause.message,
58
+ repair: failed.cause.repair,
59
+ });
60
+ return;
91
61
  }
92
- if (closed || deleting.has(id)) {
62
+ if (current().kind === "closed") {
93
63
  await handle.close().catch(() => undefined);
94
- return undefined;
64
+ return;
95
65
  }
96
- const entry = { kind: "open", handle };
97
- entries.set(id, entry);
98
- return entry;
99
- }
100
- /** The cached entry, or the result of opening the Tenant once. */
101
- async function load(id) {
102
- if (deleting.has(id))
103
- return undefined;
104
- const cached = entries.get(id);
105
- if (cached)
106
- return cached;
107
- if (closed)
108
- return undefined;
109
- let pending = opening.get(id);
110
- if (!pending) {
111
- pending = openEntry(id).finally(() => opening.delete(id));
112
- opening.set(id, pending);
66
+ state = { kind: "open", handle };
67
+ nextDelayMs = RETRY_FIRST_MS;
68
+ logger.info("tenant opened", { tenantId: handle.envelope.id });
69
+ try {
70
+ await options.onOpen?.(handle);
113
71
  }
114
- return pending;
72
+ catch (error) {
73
+ logger.warn("tenant open step failed", {
74
+ tenantId: handle.envelope.id,
75
+ message: error instanceof Error ? error.message : String(error),
76
+ });
77
+ }
78
+ }
79
+ /** Opens the Tenant unless it is open, failed or closed; one open at a time. */
80
+ function load() {
81
+ const { kind } = current();
82
+ if (kind !== "idle" && kind !== "unavailable")
83
+ return Promise.resolve();
84
+ clearTimeout(retry);
85
+ retry = undefined;
86
+ opening ??= openOnce().finally(() => {
87
+ opening = undefined;
88
+ });
89
+ return opening;
90
+ }
91
+ function scheduleRetry() {
92
+ if (retry)
93
+ return;
94
+ const delayMs = nextDelayMs;
95
+ nextDelayMs = Math.min(delayMs * 2, RETRY_MAX_MS);
96
+ retry = setTimeout(() => {
97
+ retry = undefined;
98
+ void load();
99
+ }, delayMs);
100
+ retry.unref();
115
101
  }
116
- const openEntries = () => [...entries.values()].filter((e) => e.kind === "open");
117
102
  const module = {
118
- get started() {
119
- return started;
103
+ get ready() {
104
+ return current().kind === "open";
120
105
  },
121
106
  async start() {
122
- if (closed)
107
+ if (current().kind === "closed")
123
108
  throw new Error("Tenant module is closed");
124
- const ids = await store.enumerate();
125
- started = true;
126
- logger.info("tenant module started", { tenants: ids.length });
109
+ await load();
127
110
  },
128
- async resolve(id) {
129
- if (!isTenantId(id))
130
- return { kind: "not-found" };
131
- const entry = await load(id);
132
- if (!entry)
133
- return { kind: "not-found" };
134
- if (entry.kind === "open")
135
- return { kind: "open", handle: entry.handle };
136
- return { kind: "quarantined", quarantine: entry.quarantine };
111
+ async resolve() {
112
+ await load();
113
+ const now = current();
114
+ switch (now.kind) {
115
+ case "open":
116
+ return { kind: "open", handle: now.handle };
117
+ case "failed":
118
+ return { kind: "unavailable", cause: now.cause };
119
+ case "unavailable":
120
+ throw now.error;
121
+ default:
122
+ return { kind: "unavailable" };
123
+ }
137
124
  },
138
125
  async worker(id) {
139
- if (!isTenantId(id))
140
- return undefined;
141
- const entry = await load(id);
142
- return entry?.kind === "open" ? entry.handle.worker : undefined;
126
+ await load();
127
+ const now = current();
128
+ return now.kind === "open" && now.handle.envelope.id === id
129
+ ? now.handle.worker
130
+ : undefined;
143
131
  },
144
- async create(input) {
145
- if (!isTenantId(input.tenantId)) {
146
- throw new Error(`Invalid tenant id: ${input.tenantId}`);
147
- }
148
- const bootstrap = {
149
- principalId: input.principalId,
150
- credentialHash: input.credentialHash,
151
- idempotencyKey: input.idempotencyKey,
152
- ...(input.studioCredentialHash
153
- ? { studioCredentialHash: input.studioCredentialHash }
154
- : {}),
155
- ...(input.derivedPrincipals?.length
156
- ? { derivedPrincipals: input.derivedPrincipals }
157
- : {}),
158
- };
159
- // The store records its own schema version.
160
- const envelope = envelopeNow({
161
- id: input.tenantId,
162
- name: input.name,
163
- schemaVersion: POSTGRES_SCHEMA_VERSION,
164
- });
165
- let outcome;
166
- try {
167
- outcome = await store.create(envelope, bootstrap);
168
- }
169
- catch (error) {
170
- await store.removePartial(input.tenantId).catch(() => undefined);
171
- throw error;
172
- }
173
- if (outcome === "exists") {
174
- const matches = await store.bootstrapMatches(input.tenantId, bootstrap);
175
- if (!matches)
176
- throw new TenantConflictError();
177
- const existing = await store.readEnvelope(input.tenantId);
178
- await load(input.tenantId).catch(() => undefined);
179
- return { envelope: existing, created: false };
180
- }
181
- await options.onCreated?.(input.tenantId).catch((error) => logger.warn("tenant create step failed; open repairs it", {
182
- tenantId: input.tenantId,
183
- message: error instanceof Error ? error.message : String(error),
184
- }));
185
- const opened = await load(input.tenantId).catch(() => undefined);
186
- if (opened?.kind === "open") {
187
- return { envelope: opened.handle.envelope, created: true };
188
- }
189
- // Created but failed to open: report the stored envelope.
190
- return {
191
- envelope: await store
192
- .readEnvelope(input.tenantId)
193
- .catch(() => envelope),
194
- created: true,
195
- };
196
- },
197
- async list() {
198
- const ids = new Set(await store.enumerate());
199
- for (const id of entries.keys())
200
- ids.add(id);
201
- const result = [];
202
- for (const id of [...ids].sort()) {
203
- if (!isTenantId(id) || deleting.has(id))
204
- continue;
205
- let entry = entries.get(id);
206
- if (entry?.kind === "open") {
207
- result.push(toAdmin(id, entry, null));
208
- continue;
209
- }
210
- let envelope = null;
211
- try {
212
- envelope = await store.readEnvelope(id);
213
- if (envelope.id !== id) {
214
- entry = {
215
- kind: "quarantined",
216
- quarantine: quarantine("envelope-invalid", `Tenant envelope id ${envelope.id} does not match ${id}`, { tenantId: id }).toQuarantine(),
217
- };
218
- envelope = null;
219
- }
132
+ tenant() {
133
+ const now = current();
134
+ switch (now.kind) {
135
+ case "open": {
136
+ const { envelope } = now.handle;
137
+ return { id: envelope.id, name: envelope.name, state: "open", envelope };
220
138
  }
221
- catch (error) {
222
- // Deleted since it was enumerated.
223
- if (error instanceof TenantNotFoundError)
224
- continue;
225
- if (error instanceof TenantUnavailableError)
226
- throw error;
227
- // Listed as quarantined; only opening it caches a quarantine.
228
- entry ??= {
229
- kind: "quarantined",
230
- quarantine: quarantineOf(error) ??
231
- quarantine("envelope-invalid", "Tenant envelope is unreadable", {
232
- tenantId: id,
233
- }).toQuarantine(),
139
+ case "failed":
140
+ return {
141
+ id: now.envelope?.id ?? null,
142
+ name: now.envelope?.name ?? null,
143
+ state: "unavailable",
144
+ envelope: now.envelope,
145
+ cause: now.cause,
234
146
  };
235
- }
236
- result.push(toAdmin(id, entry, envelope));
237
- }
238
- return result;
239
- },
240
- async status(id) {
241
- if (!isTenantId(id))
242
- return undefined;
243
- const entry = await load(id);
244
- if (!entry)
245
- return undefined;
246
- if (entry.kind === "open")
247
- return toAdmin(id, entry, null);
248
- const envelope = await store.readEnvelope(id).catch(() => null);
249
- return { ...toAdmin(id, entry, envelope), quarantine: entry.quarantine };
250
- },
251
- async delete(id, activeWork) {
252
- if (!isTenantId(id))
253
- throw new Error(`Invalid tenant id: ${id}`);
254
- if (deleting.has(id))
255
- throw new TenantBusyError("Tenant is being deleted");
256
- const entry = await load(id);
257
- if (!entry)
258
- throw new TenantNotFoundError(id);
259
- if (entry.kind === "open") {
260
- const summary = await entry.handle.summary();
261
- if (activeWork === "refuse" && hasLiveWork(summary)) {
262
- throw new TenantBusyError();
263
- }
264
- if (activeWork === "drain" || activeWork === "cancel") {
265
- await entry.handle.drain(activeWork);
266
- }
267
- }
268
- deleting.add(id);
269
- try {
270
- entries.delete(id);
271
- // Sandbox prefix cleanup is owned by TenantHandle.close().
272
- if (entry.kind === "open")
273
- await entry.handle.close();
274
- await store.trash(id, new Date());
275
- }
276
- finally {
277
- deleting.delete(id);
147
+ default:
148
+ return { id: null, name: null, state: "unavailable", envelope: null };
278
149
  }
279
- logger.info("tenant deleted", { tenantId: id, activeWork });
280
- await options.onDeleted?.(id).catch((error) => logger.warn("tenant delete cleanup failed", {
281
- tenantId: id,
282
- message: error instanceof Error ? error.message : String(error),
283
- }));
284
150
  },
285
151
  async summarize() {
286
- let runningSessions = 0;
287
- let inFlightDeliveries = 0;
288
- let pendingActions = 0;
289
- let uncertainEffects = 0;
290
- for (const s of await Promise.all(openEntries().map((e) => e.handle.summary()))) {
291
- runningSessions += s.runningSessions;
292
- inFlightDeliveries += s.inFlightDeliveries;
293
- pendingActions += s.pendingActions;
294
- uncertainEffects += s.uncertainEffects;
295
- }
152
+ const now = current();
153
+ const summary = now.kind === "open" ? await now.handle.summary() : undefined;
296
154
  const relay = await options.relayStatus?.().catch(() => undefined);
297
155
  return {
298
- runningSessions,
299
- inFlightDeliveries,
300
- pendingActions,
301
- uncertainEffects,
156
+ runningSessions: summary?.runningSessions ?? 0,
157
+ inFlightDeliveries: summary?.inFlightDeliveries ?? 0,
158
+ pendingActions: summary?.pendingActions ?? 0,
159
+ uncertainEffects: summary?.uncertainEffects ?? 0,
302
160
  ...(relay ? { relay } : {}),
303
161
  };
304
162
  },
305
163
  async close() {
306
- closed = true;
307
- started = false;
308
- await Promise.allSettled([...opening.values()]);
309
- const open = openEntries();
310
- entries.clear();
311
- await Promise.all(open.map((e) => e.handle.close().catch(() => undefined)));
164
+ const previous = current();
165
+ state = { kind: "closed" };
166
+ clearTimeout(retry);
167
+ retry = undefined;
168
+ // An open in progress sees `closed` and closes what it opened.
169
+ await opening?.catch(() => undefined);
170
+ if (previous.kind === "open")
171
+ await previous.handle.close().catch(() => undefined);
312
172
  },
313
173
  };
314
174
  return module;
315
175
  }
316
- export { TenantBusyError, TenantConflictError, TenantNotFoundError, TenantUnavailableError, isQuarantineError, quarantine, };
176
+ export { TenantUnavailableError };
@@ -1,28 +1,23 @@
1
- import type { Logger, TenantPaths } from "./types.js";
1
+ import type { TenantPaths } from "./types.js";
2
2
  export interface HostPaths {
3
3
  root: string;
4
4
  config: string;
5
5
  credentials: string;
6
6
  home: string;
7
7
  tmp: string;
8
- tenants: string;
9
- trash: string;
8
+ /** The Tenant directory (`tenantPaths`). */
9
+ tenant: string;
10
10
  }
11
11
  /** Host layout under NYLORUN_HOME / ~/.nylorun. */
12
12
  export declare function hostPaths(hostRoot: string): HostPaths;
13
13
  /**
14
- * Absolute Tenant directory paths under `<hostRoot>/tenants/<tenantId>/`.
15
- * Validates the id and asserts containment after realpath.
14
+ * Absolute paths of the Tenant directory, `<hostRoot>/tenant/`: a Host serves one Tenant
15
+ * (tenancy.md §1), so the directory has no id segment. Asserts that it stays inside the Host
16
+ * root after realpath.
16
17
  *
17
- * The directory holds what stays on the Host (the Tenant's data is its Postgres schema):
18
- * `vault-kek`, `home/`, `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`.
18
+ * The directory holds what stays on the Host (the Tenant's data is its database): `home/`,
19
+ * `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`. The vault key is the one exception:
20
+ * it lives beside the Tenant directory, in `<hostRoot>/keys/vault-kek`, which the local stack
21
+ * mounts only into the gateway (F4.2), never into the runtime container.
19
22
  */
20
- export declare function tenantPaths(hostRoot: string, tenantId: string): TenantPaths;
21
- /**
22
- * Moves every Tenant directory that holds a `tenant.sqlite` to `trash/`, logging each.
23
- *
24
- * Those are beta Tenants from the SQLite Runtime. They are not migrated (recreate them); a
25
- * Postgres Tenant's directory never holds that file. Moving them at Host start keeps a new
26
- * Postgres Tenant with the same id from sharing the old directory. Returns the moved ids.
27
- */
28
- export declare function trashSqliteTenants(hostRoot: string, logger: Logger): string[];
23
+ export declare function tenantPaths(hostRoot: string): TenantPaths;