@nylorun/runtime 0.9.0-beta → 0.11.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +110 -37
  3. package/dist/adapters/execution/restate.d.ts +130 -0
  4. package/dist/adapters/execution/restate.js +442 -0
  5. package/dist/adapters/media.d.ts +2 -2
  6. package/dist/adapters/media.js +2 -2
  7. package/dist/adapters/observe.d.ts +2 -1
  8. package/dist/adapters/observe.js +2 -3
  9. package/dist/adapters/sandbox/virtual.js +1 -1
  10. package/dist/adapters/streams/s2.d.ts +18 -0
  11. package/dist/adapters/streams/s2.js +361 -0
  12. package/dist/assets.d.ts +2 -2
  13. package/dist/assets.js +5 -3
  14. package/dist/core/executors.d.ts +15 -3
  15. package/dist/core/executors.js +40 -5
  16. package/dist/core/flow-host.d.ts +225 -0
  17. package/dist/core/flow-host.js +518 -0
  18. package/dist/core/limits.d.ts +40 -0
  19. package/dist/core/limits.js +76 -0
  20. package/dist/core/runtime.d.ts +5 -90
  21. package/dist/core/runtime.js +3 -1447
  22. package/dist/core/sandbox-routes.d.ts +40 -0
  23. package/dist/core/sandbox-routes.js +106 -0
  24. package/dist/core/turn-manifest.d.ts +50 -0
  25. package/dist/core/turn-manifest.js +66 -0
  26. package/dist/execution/memory.d.ts +54 -0
  27. package/dist/execution/memory.js +217 -0
  28. package/dist/execution/types.d.ts +125 -0
  29. package/dist/execution/types.js +52 -0
  30. package/dist/host/config-for.d.ts +17 -0
  31. package/dist/host/config-for.js +27 -0
  32. package/dist/host/config.d.ts +16 -0
  33. package/dist/host/create-host.d.ts +56 -0
  34. package/dist/host/create-host.js +420 -0
  35. package/dist/host/environment.d.ts +13 -0
  36. package/dist/host/environment.js +53 -0
  37. package/dist/host/execution.d.ts +61 -0
  38. package/dist/host/execution.js +69 -0
  39. package/dist/host/http.d.ts +39 -0
  40. package/dist/host/http.js +142 -0
  41. package/dist/host/index.d.ts +8 -0
  42. package/dist/host/index.js +7 -0
  43. package/dist/host/logger.d.ts +3 -0
  44. package/dist/host/logger.js +18 -0
  45. package/dist/host/main.d.ts +3 -0
  46. package/dist/host/main.js +201 -0
  47. package/dist/host/stack-config.d.ts +72 -0
  48. package/dist/host/stack-config.js +193 -0
  49. package/dist/index.d.ts +7 -2
  50. package/dist/index.js +4 -2
  51. package/dist/infra/database.d.ts +16 -0
  52. package/dist/infra/database.js +31 -0
  53. package/dist/infra/execution.d.ts +39 -0
  54. package/dist/infra/execution.js +68 -0
  55. package/dist/infra/index.d.ts +42 -0
  56. package/dist/infra/index.js +56 -0
  57. package/dist/infra/readiness.d.ts +33 -0
  58. package/dist/infra/readiness.js +53 -0
  59. package/dist/infra/streams.d.ts +17 -0
  60. package/dist/infra/streams.js +33 -0
  61. package/dist/infra/workers.d.ts +39 -0
  62. package/dist/infra/workers.js +31 -0
  63. package/dist/mcp/connect.d.ts +1 -0
  64. package/dist/mcp/connect.js +2 -0
  65. package/dist/mcp/pool.d.ts +3 -1
  66. package/dist/mcp/pool.js +2 -1
  67. package/dist/mcp/stdio.d.ts +2 -0
  68. package/dist/mcp/stdio.js +3 -1
  69. package/dist/model/auth-store.d.ts +1 -1
  70. package/dist/model/auth-store.js +2 -2
  71. package/dist/model/models.d.ts +4 -0
  72. package/dist/model/models.js +22 -26
  73. package/dist/model/pi-model.d.ts +2 -2
  74. package/dist/model/pi-model.js +6 -6
  75. package/dist/model/settings.d.ts +5 -1
  76. package/dist/model/settings.js +6 -2
  77. package/dist/sandbox/manager.d.ts +26 -8
  78. package/dist/sandbox/manager.js +95 -47
  79. package/dist/sandbox/select.d.ts +5 -4
  80. package/dist/sandbox/select.js +18 -13
  81. package/dist/sandbox/share.d.ts +23 -0
  82. package/dist/sandbox/share.js +58 -0
  83. package/dist/sandbox/types.d.ts +2 -2
  84. package/dist/store/canonical.d.ts +5 -0
  85. package/dist/store/canonical.js +17 -0
  86. package/dist/store/cursor.d.ts +9 -0
  87. package/dist/store/cursor.js +18 -0
  88. package/dist/store/memory.d.ts +57 -0
  89. package/dist/store/memory.js +643 -0
  90. package/dist/store/ownership.d.ts +19 -0
  91. package/dist/store/ownership.js +33 -0
  92. package/dist/store/postgres/connect.d.ts +25 -0
  93. package/dist/store/postgres/connect.js +25 -0
  94. package/dist/store/postgres/locking.d.ts +16 -0
  95. package/dist/store/postgres/locking.js +22 -0
  96. package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
  97. package/dist/store/postgres/migrations/001_initial.js +196 -0
  98. package/dist/store/postgres/migrations/index.d.ts +49 -0
  99. package/dist/store/postgres/migrations/index.js +75 -0
  100. package/dist/store/postgres/names.d.ts +11 -0
  101. package/dist/store/postgres/names.js +42 -0
  102. package/dist/store/postgres/store.d.ts +11 -0
  103. package/dist/store/postgres/store.js +790 -0
  104. package/dist/store/postgres/tenants.d.ts +90 -0
  105. package/dist/store/postgres/tenants.js +207 -0
  106. package/dist/store/types.d.ts +440 -0
  107. package/dist/store/types.js +10 -0
  108. package/dist/streams/basin.d.ts +23 -0
  109. package/dist/streams/basin.js +51 -0
  110. package/dist/streams/memory.d.ts +25 -0
  111. package/dist/streams/memory.js +124 -0
  112. package/dist/streams/relay.d.ts +37 -0
  113. package/dist/streams/relay.js +254 -0
  114. package/dist/streams/types.d.ts +115 -0
  115. package/dist/streams/types.js +86 -0
  116. package/dist/tenant/actions.d.ts +153 -0
  117. package/dist/tenant/actions.js +191 -0
  118. package/dist/tenant/advance.d.ts +15 -0
  119. package/dist/tenant/advance.js +439 -0
  120. package/dist/tenant/auth.d.ts +11 -0
  121. package/dist/tenant/auth.js +34 -0
  122. package/dist/tenant/commands.d.ts +3 -0
  123. package/dist/tenant/commands.js +384 -0
  124. package/dist/tenant/context.d.ts +140 -0
  125. package/dist/tenant/context.js +40 -0
  126. package/dist/tenant/effects.d.ts +20 -0
  127. package/dist/tenant/effects.js +539 -0
  128. package/dist/tenant/envelope.d.ts +7 -0
  129. package/dist/tenant/envelope.js +22 -0
  130. package/dist/tenant/ephemeral.d.ts +42 -0
  131. package/dist/tenant/ephemeral.js +224 -0
  132. package/dist/tenant/http.d.ts +32 -0
  133. package/dist/tenant/http.js +51 -0
  134. package/dist/tenant/live.d.ts +92 -0
  135. package/dist/tenant/live.js +290 -0
  136. package/dist/tenant/logger.d.ts +9 -0
  137. package/dist/tenant/logger.js +25 -0
  138. package/dist/tenant/model-setting.d.ts +16 -0
  139. package/dist/tenant/model-setting.js +12 -0
  140. package/dist/tenant/module.d.ts +38 -0
  141. package/dist/tenant/module.js +319 -0
  142. package/dist/tenant/paths.d.ts +28 -0
  143. package/dist/tenant/paths.js +91 -0
  144. package/dist/tenant/pool.d.ts +10 -0
  145. package/dist/tenant/pool.js +54 -0
  146. package/dist/tenant/principals.d.ts +13 -0
  147. package/dist/tenant/principals.js +37 -0
  148. package/dist/tenant/quarantine-error.d.ts +6 -0
  149. package/dist/tenant/quarantine-error.js +9 -0
  150. package/dist/tenant/quarantine.d.ts +27 -0
  151. package/dist/tenant/quarantine.js +72 -0
  152. package/dist/tenant/reset.d.ts +22 -0
  153. package/dist/tenant/reset.js +81 -0
  154. package/dist/tenant/routes-tenant.d.ts +12 -0
  155. package/dist/tenant/routes-tenant.js +126 -0
  156. package/dist/tenant/routes.d.ts +9 -0
  157. package/dist/tenant/routes.js +142 -0
  158. package/dist/tenant/runtime.d.ts +81 -0
  159. package/dist/tenant/runtime.js +298 -0
  160. package/dist/tenant/scheduler.d.ts +25 -0
  161. package/dist/tenant/scheduler.js +84 -0
  162. package/dist/tenant/session.d.ts +60 -0
  163. package/dist/tenant/session.js +94 -0
  164. package/dist/tenant/sessions.d.ts +34 -0
  165. package/dist/tenant/sessions.js +149 -0
  166. package/dist/tenant/status.d.ts +37 -0
  167. package/dist/tenant/status.js +130 -0
  168. package/dist/tenant/store-memory.d.ts +16 -0
  169. package/dist/tenant/store-memory.js +111 -0
  170. package/dist/tenant/store-pg.d.ts +15 -0
  171. package/dist/tenant/store-pg.js +140 -0
  172. package/dist/tenant/streams.d.ts +126 -0
  173. package/dist/tenant/streams.js +419 -0
  174. package/dist/tenant/sweep.d.ts +15 -0
  175. package/dist/tenant/sweep.js +122 -0
  176. package/dist/tenant/types.d.ts +205 -0
  177. package/dist/tenant/types.js +23 -0
  178. package/dist/tenant/worker.d.ts +84 -0
  179. package/dist/tenant/worker.js +189 -0
  180. package/dist/vault/kek.d.ts +5 -2
  181. package/dist/vault/kek.js +7 -10
  182. package/dist/vault/service.d.ts +37 -31
  183. package/dist/vault/service.js +439 -359
  184. package/dist/version.d.ts +1 -1
  185. package/dist/version.js +1 -1
  186. package/package.json +14 -11
  187. package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
  188. package/dist/adapters/sandbox/microsandbox.js +0 -238
  189. package/dist/core/main.js +0 -35
  190. package/dist/core/store.d.ts +0 -30
  191. package/dist/core/store.js +0 -202
  192. /package/dist/{core/main.d.ts → host/config.js} +0 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Stack configuration for the Runtime process, parsed from an environment
3
+ * snapshot and argv. Pure: `host/main.ts` passes the process environment and
4
+ * arguments; nothing here reads ambient state.
5
+ *
6
+ * Two listen modes:
7
+ * - **local** (no `NYLORUN_LISTEN_*` / `NYLORUN_ALLOWED_HOSTS`): the Host binds
8
+ * what `host.json` names, loopback only unless `allowNonLoopback`.
9
+ * - **container**: the Host binds `NYLORUN_LISTEN_HOST:NYLORUN_LISTEN_PORT`
10
+ * (default `0.0.0.0:4000`) and accepts only the `Host` headers in
11
+ * `NYLORUN_ALLOWED_HOSTS`, plus the loopback forms of the listen port for
12
+ * probes from inside the container.
13
+ *
14
+ * The Postgres, Restate and S2 endpoints are parsed and validated here;
15
+ * `infra/*` builds the clients from them.
16
+ */
17
+ export const RUNTIME_ROLES = ["api", "worker", "all"];
18
+ export const DEFAULT_CONTAINER_LISTEN_HOST = "0.0.0.0";
19
+ export const DEFAULT_CONTAINER_LISTEN_PORT = 4000;
20
+ export class StackConfigError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = "StackConfigError";
24
+ }
25
+ }
26
+ function read(env, name) {
27
+ const value = env[name];
28
+ if (value === undefined)
29
+ return undefined;
30
+ const trimmed = value.trim();
31
+ return trimmed === "" ? undefined : trimmed;
32
+ }
33
+ export function parseRole(argv) {
34
+ let role;
35
+ for (let index = 0; index < argv.length; index += 1) {
36
+ const arg = argv[index];
37
+ let value;
38
+ if (arg === "--role") {
39
+ value = argv[index + 1];
40
+ index += 1;
41
+ if (value === undefined || value.startsWith("-"))
42
+ throw new StackConfigError("--role requires a value: api, worker or all");
43
+ }
44
+ else if (arg.startsWith("--role=")) {
45
+ value = arg.slice("--role=".length);
46
+ }
47
+ else {
48
+ throw new StackConfigError(`Unknown argument ${arg}. Usage: main.js [--role api|worker|all]`);
49
+ }
50
+ if (role !== undefined)
51
+ throw new StackConfigError("--role may only be supplied once");
52
+ if (!RUNTIME_ROLES.includes(value))
53
+ throw new StackConfigError(`Invalid --role ${value}; expected api, worker or all`);
54
+ role = value;
55
+ }
56
+ return role ?? "all";
57
+ }
58
+ function parsePort(name, raw) {
59
+ if (!/^\d+$/.test(raw))
60
+ throw new StackConfigError(`${name} must be a port number; got ${raw}`);
61
+ const port = Number(raw);
62
+ if (port < 1 || port > 65535)
63
+ throw new StackConfigError(`${name} must be between 1 and 65535; got ${raw}`);
64
+ return port;
65
+ }
66
+ /**
67
+ * Normalize one `Host` header allowlist entry to lowercase `name:port`.
68
+ * IPv6 literals must be bracketed (`[::1]:4000`).
69
+ */
70
+ export function normalizeAllowedHost(name, entry) {
71
+ const value = entry.trim().toLowerCase();
72
+ const match = /^(\[[0-9a-f:.]+\]|[a-z0-9]([a-z0-9.-]*[a-z0-9])?):(\d+)$/.exec(value);
73
+ if (!match)
74
+ throw new StackConfigError(`${name} entry "${entry}" must be host:port (IPv6 in brackets)`);
75
+ parsePort(name, match[3]);
76
+ return value;
77
+ }
78
+ function loopbackForms(port) {
79
+ return [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`];
80
+ }
81
+ function isLoopbackAddress(host) {
82
+ const value = host.toLowerCase();
83
+ return value === "127.0.0.1" || value === "::1" || value === "localhost";
84
+ }
85
+ function parseUrl(env, name, protocols) {
86
+ const raw = read(env, name);
87
+ if (raw === undefined)
88
+ return undefined;
89
+ let url;
90
+ try {
91
+ url = new URL(raw);
92
+ }
93
+ catch {
94
+ throw new StackConfigError(`${name} is not a valid URL`);
95
+ }
96
+ if (!protocols.includes(url.protocol))
97
+ throw new StackConfigError(`${name} must use ${protocols.map((p) => p.replace(/:$/, "")).join(" or ")}; got ${url.protocol.replace(/:$/, "")}`);
98
+ return raw;
99
+ }
100
+ function parseListen(env) {
101
+ const rawHost = read(env, "NYLORUN_LISTEN_HOST");
102
+ const rawPort = read(env, "NYLORUN_LISTEN_PORT");
103
+ const rawAllowed = read(env, "NYLORUN_ALLOWED_HOSTS");
104
+ if (rawHost === undefined && rawPort === undefined && rawAllowed === undefined)
105
+ return undefined;
106
+ const host = rawHost ?? DEFAULT_CONTAINER_LISTEN_HOST;
107
+ if (/\s|\//.test(host))
108
+ throw new StackConfigError(`NYLORUN_LISTEN_HOST is not an address: ${host}`);
109
+ const port = rawPort === undefined
110
+ ? DEFAULT_CONTAINER_LISTEN_PORT
111
+ : parsePort("NYLORUN_LISTEN_PORT", rawPort);
112
+ const explicit = rawAllowed === undefined
113
+ ? []
114
+ : rawAllowed
115
+ .split(",")
116
+ .map((entry) => entry.trim())
117
+ .filter((entry) => entry !== "")
118
+ .map((entry) => normalizeAllowedHost("NYLORUN_ALLOWED_HOSTS", entry));
119
+ if (explicit.length === 0 && !isLoopbackAddress(host))
120
+ throw new StackConfigError(`NYLORUN_ALLOWED_HOSTS is required when NYLORUN_LISTEN_HOST is ${host}: list the Host headers clients send, e.g. runtime:${port},localhost:<published port>`);
121
+ // Loopback forms of the listen port serve probes from inside the container
122
+ // (health checks). A browser on the Docker host never sends them: it sends
123
+ // the published port.
124
+ const allowedHosts = [...new Set([...explicit, ...loopbackForms(port)])];
125
+ return { host, port, allowedHosts };
126
+ }
127
+ const IDENTITY_KEY = /^publickeyv1_[1-9A-HJ-NP-Za-km-z]{32,64}$/;
128
+ function parseIdentityKeys(env) {
129
+ const raw = read(env, "NYLORUN_RESTATE_IDENTITY_KEY");
130
+ if (raw === undefined)
131
+ return undefined;
132
+ const keys = raw
133
+ .split(",")
134
+ .map((key) => key.trim())
135
+ .filter((key) => key !== "");
136
+ for (const key of keys)
137
+ if (!IDENTITY_KEY.test(key))
138
+ throw new StackConfigError("NYLORUN_RESTATE_IDENTITY_KEY must be publickeyv1_<base58 Ed25519 public key>, comma-separated");
139
+ return keys.length > 0 ? keys : undefined;
140
+ }
141
+ /** Parse the stack configuration; throws `StackConfigError` naming the variable. */
142
+ export function parseStackConfig(env, argv) {
143
+ const role = parseRole(argv);
144
+ const listen = parseListen(env);
145
+ const http = ["http:", "https:"];
146
+ const endpoints = {};
147
+ const databaseUrl = parseUrl(env, "NYLORUN_DATABASE_URL", [
148
+ "postgres:",
149
+ "postgresql:",
150
+ ]);
151
+ if (databaseUrl)
152
+ endpoints.databaseUrl = databaseUrl;
153
+ const restateIngressUrl = parseUrl(env, "NYLORUN_RESTATE_INGRESS_URL", http);
154
+ if (restateIngressUrl)
155
+ endpoints.restateIngressUrl = restateIngressUrl;
156
+ const restateAdminUrl = parseUrl(env, "NYLORUN_RESTATE_ADMIN_URL", http);
157
+ if (restateAdminUrl)
158
+ endpoints.restateAdminUrl = restateAdminUrl;
159
+ const workerUrl = parseUrl(env, "NYLORUN_WORKER_URL", http);
160
+ if (workerUrl)
161
+ endpoints.workerUrl = workerUrl;
162
+ const s2Endpoint = parseUrl(env, "NYLORUN_S2_ENDPOINT", http);
163
+ if (s2Endpoint)
164
+ endpoints.s2Endpoint = s2Endpoint;
165
+ const s2Token = read(env, "NYLORUN_S2_TOKEN");
166
+ if (s2Token)
167
+ endpoints.s2Token = s2Token;
168
+ const workspaceStoreUrl = parseUrl(env, "NYLORUN_WORKSPACE_STORE_URL", [
169
+ "file:",
170
+ "s3:",
171
+ "http:",
172
+ "https:",
173
+ ]);
174
+ if (workspaceStoreUrl)
175
+ endpoints.workspaceStoreUrl = workspaceStoreUrl;
176
+ const restateIdentityKeys = parseIdentityKeys(env);
177
+ if (restateIdentityKeys)
178
+ endpoints.restateIdentityKeys = restateIdentityKeys;
179
+ const publicUrl = parseUrl(env, "NYLORUN_PUBLIC_URL", http)?.replace(/\/+$/, "");
180
+ return {
181
+ role,
182
+ ...(listen ? { listen } : {}),
183
+ endpoints,
184
+ ...(publicUrl ? { publicUrl } : {}),
185
+ };
186
+ }
187
+ /** Endpoint summary for logs: which endpoints are set, never their values. */
188
+ export function describeEndpoints(endpoints) {
189
+ return Object.entries(endpoints)
190
+ .filter(([, value]) => value !== undefined)
191
+ .map(([key]) => key)
192
+ .sort();
193
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,10 @@
1
- export { CoreRuntime, createRuntime, startRuntime, type RuntimeOptions } from "./core/runtime.js";
1
+ export { openTenantRuntime, type TenantOpenHooks, } from "./tenant/runtime.js";
2
+ export { startEphemeralRuntime, type StartEphemeralRuntimeOptions, type EphemeralRuntime, } from "./tenant/ephemeral.js";
3
+ export type { TenantConfig, TenantHandle, TenantPaths, Logger, OpenTenantRuntime, } from "./tenant/types.js";
2
4
  export { scriptedModel, gatewayModel, type ModelProvider } from "./core/provider.js";
3
5
  export type { RuntimeAgent } from "./contracts.js";
4
- export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
6
+ export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./media.js";
5
7
  export type { RuntimeModelAdapter } from "./contracts.js";
8
+ export { baselineEnvironment, hostProcessEnvironment, tenantChildEnvironment, } from "./host/environment.js";
9
+ export type { HostProcessPathInputs } from "./host/environment.js";
10
+ export type { HostConfigFile, HostCredentialsFile, } from "./host/config.js";
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
- export { CoreRuntime, createRuntime, startRuntime } from "./core/runtime.js";
1
+ export { openTenantRuntime, } from "./tenant/runtime.js";
2
+ export { startEphemeralRuntime, } from "./tenant/ephemeral.js";
2
3
  export { scriptedModel, gatewayModel } from "./core/provider.js";
3
- export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
4
+ export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./media.js";
5
+ export { baselineEnvironment, hostProcessEnvironment, tenantChildEnvironment, } from "./host/environment.js";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Builds the Host's Postgres connection pool from the stack configuration
3
+ * (architecture §12.2, §14.5). One pool serves the Tenant catalog and every
4
+ * Tenant schema; the Host ends it at shutdown. The driver stays behind
5
+ * `store/postgres/connect.ts`.
6
+ */
7
+ import type { StackConfig } from "../host/stack-config.js";
8
+ import { type PostgresClient, type PostgresClientOptions } from "../store/postgres/connect.js";
9
+ export type { PostgresClient };
10
+ /** Throws when `NYLORUN_DATABASE_URL` is unset: there is no in-memory Session Store. */
11
+ export declare function createDatabase(config: Pick<StackConfig, "endpoints">, options?: PostgresClientOptions): PostgresClient;
12
+ /**
13
+ * Readiness probe: resolves when `select 1` answers within `signal`'s
14
+ * lifetime. An aborted probe cancels its query.
15
+ */
16
+ export declare function probeDatabase(client: PostgresClient, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import { createPostgresClient, } from "../store/postgres/connect.js";
2
+ /** Throws when `NYLORUN_DATABASE_URL` is unset: there is no in-memory Session Store. */
3
+ export function createDatabase(config, options = {}) {
4
+ const url = config.endpoints.databaseUrl;
5
+ if (!url)
6
+ throw new Error("NYLORUN_DATABASE_URL is required: the Postgres URL of the Session Store");
7
+ return createPostgresClient(url, options);
8
+ }
9
+ /**
10
+ * Readiness probe: resolves when `select 1` answers within `signal`'s
11
+ * lifetime. An aborted probe cancels its query.
12
+ */
13
+ export async function probeDatabase(client, signal) {
14
+ signal.throwIfAborted();
15
+ const query = client `select 1 as ok`;
16
+ const cancel = () => void query.cancel();
17
+ signal.addEventListener("abort", cancel, { once: true });
18
+ try {
19
+ await Promise.race([
20
+ query,
21
+ new Promise((_, reject) => {
22
+ if (signal.aborted)
23
+ reject(signal.reason);
24
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
25
+ }),
26
+ ]);
27
+ }
28
+ finally {
29
+ signal.removeEventListener("abort", cancel);
30
+ }
31
+ }
@@ -0,0 +1,39 @@
1
+ import type { DurableExecution } from "../execution/types.js";
2
+ import type { RuntimeRole, StackConfig } from "../host/stack-config.js";
3
+ /** Where a Worker serves the Restate endpoint (architecture §14.3). */
4
+ export declare const WORKER_LISTEN: {
5
+ readonly host: "0.0.0.0";
6
+ readonly port: 9080;
7
+ };
8
+ export interface CreateExecutionOptions {
9
+ /**
10
+ * Prepended to Restate service names, so several Runtimes or test runs can
11
+ * share one Restate server. Letters, digits and `_`. Default "".
12
+ */
13
+ servicePrefix?: string;
14
+ /** Replaces `WORKER_LISTEN` (tests serving several Workers on one machine). */
15
+ workerListen?: {
16
+ host: string;
17
+ port: number;
18
+ };
19
+ /** Receives the Restate SDK's log lines instead of the console. */
20
+ logger?: (level: string, message: string) => void;
21
+ }
22
+ /** Which implementation `createExecution` chooses for a configuration. */
23
+ export declare function executionKind(config: Pick<StackConfig, "endpoints">): "restate" | "memory";
24
+ /**
25
+ * Throws naming the missing variable when the Restate endpoints are
26
+ * incomplete for `role`. The api role never serves the Worker endpoint, so it
27
+ * needs no `NYLORUN_WORKER_URL`.
28
+ */
29
+ export declare function validateExecutionConfig(config: Pick<StackConfig, "endpoints"> & {
30
+ role?: RuntimeRole;
31
+ }): void;
32
+ export declare function createExecution(config: Pick<StackConfig, "endpoints"> & {
33
+ role?: RuntimeRole;
34
+ }, options?: CreateExecutionOptions): DurableExecution;
35
+ /**
36
+ * Readiness probe: resolves when Restate's admin API and ingress answer
37
+ * within `signal`'s lifetime. The memory execution is always ready.
38
+ */
39
+ export declare function probeExecution(execution: DurableExecution, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Builds the Host's Durable Session Execution from the stack configuration
3
+ * (architecture §6, §12.3, §14.5).
4
+ *
5
+ * With `NYLORUN_RESTATE_INGRESS_URL` and `NYLORUN_RESTATE_ADMIN_URL` the Host
6
+ * uses Restate:
7
+ *
8
+ * - wakes, timers and sweep arming go through the ingress, so every role can
9
+ * call them;
10
+ * - a Worker (`--role worker|all`) serves the Restate endpoint on
11
+ * `0.0.0.0:9080` and registers `NYLORUN_WORKER_URL` with the admin API when
12
+ * `infra/workers.ts` starts it;
13
+ * - with `NYLORUN_RESTATE_IDENTITY_KEY` the endpoint accepts only requests
14
+ * signed by the Restate server holding the matching private key.
15
+ *
16
+ * Without Restate endpoints it returns the in-process memory execution, which
17
+ * only unit tests and a local development Host use.
18
+ */
19
+ import { createRestateExecution } from "../adapters/execution/restate.js";
20
+ import { MemoryExecution } from "../execution/memory.js";
21
+ /** Where a Worker serves the Restate endpoint (architecture §14.3). */
22
+ export const WORKER_LISTEN = { host: "0.0.0.0", port: 9080 };
23
+ /** Which implementation `createExecution` chooses for a configuration. */
24
+ export function executionKind(config) {
25
+ const { restateIngressUrl, restateAdminUrl } = config.endpoints;
26
+ return restateIngressUrl || restateAdminUrl ? "restate" : "memory";
27
+ }
28
+ /**
29
+ * Throws naming the missing variable when the Restate endpoints are
30
+ * incomplete for `role`. The api role never serves the Worker endpoint, so it
31
+ * needs no `NYLORUN_WORKER_URL`.
32
+ */
33
+ export function validateExecutionConfig(config) {
34
+ const { restateIngressUrl, restateAdminUrl, workerUrl, restateIdentityKeys } = config.endpoints;
35
+ if (executionKind(config) === "memory") {
36
+ if (workerUrl || restateIdentityKeys?.length)
37
+ throw new Error(`${workerUrl ? "NYLORUN_WORKER_URL" : "NYLORUN_RESTATE_IDENTITY_KEY"} is set without the Restate endpoints; set NYLORUN_RESTATE_INGRESS_URL and NYLORUN_RESTATE_ADMIN_URL`);
38
+ return;
39
+ }
40
+ if (!restateIngressUrl)
41
+ throw new Error("NYLORUN_RESTATE_INGRESS_URL is required with NYLORUN_RESTATE_ADMIN_URL");
42
+ if (!restateAdminUrl)
43
+ throw new Error("NYLORUN_RESTATE_ADMIN_URL is required with NYLORUN_RESTATE_INGRESS_URL");
44
+ if ((config.role ?? "all") !== "api" && !workerUrl)
45
+ throw new Error(`NYLORUN_WORKER_URL is required for --role ${config.role ?? "all"}: the Worker endpoint URL Restate calls`);
46
+ }
47
+ export function createExecution(config, options = {}) {
48
+ validateExecutionConfig(config);
49
+ const { restateIngressUrl, restateAdminUrl, workerUrl, restateIdentityKeys } = config.endpoints;
50
+ if (!restateIngressUrl || !restateAdminUrl)
51
+ return new MemoryExecution();
52
+ return createRestateExecution({
53
+ ingressUrl: restateIngressUrl,
54
+ adminUrl: restateAdminUrl,
55
+ workerListen: options.workerListen ?? { ...WORKER_LISTEN },
56
+ ...(workerUrl ? { workerAdvertisedUrl: workerUrl } : {}),
57
+ ...(restateIdentityKeys?.length ? { identityKeys: [...restateIdentityKeys] } : {}),
58
+ ...(options.servicePrefix ? { servicePrefix: options.servicePrefix } : {}),
59
+ ...(options.logger ? { logger: options.logger } : {}),
60
+ });
61
+ }
62
+ /**
63
+ * Readiness probe: resolves when Restate's admin API and ingress answer
64
+ * within `signal`'s lifetime. The memory execution is always ready.
65
+ */
66
+ export async function probeExecution(execution, signal) {
67
+ await execution.probe?.(signal);
68
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The Host's infrastructure clients, built from the stack configuration: the
3
+ * Postgres pool, Durable Session Execution and Durable Streams, each only when
4
+ * its endpoints are configured, plus the readiness that covers them.
5
+ *
6
+ * Building a client connects to nothing: the pool connects on first query,
7
+ * Restate and S2 on first call. Nothing here starts a Worker (see
8
+ * `workers.ts`).
9
+ */
10
+ import type { DurableExecution } from "../execution/types.js";
11
+ import type { StackConfig } from "../host/stack-config.js";
12
+ import type { DurableStreams } from "../streams/types.js";
13
+ import type { Logger } from "../tenant/types.js";
14
+ import { type PostgresClient } from "./database.js";
15
+ import { type CreateExecutionOptions } from "./execution.js";
16
+ import { type Readiness } from "./readiness.js";
17
+ import { type CreateStreamsOptions } from "./streams.js";
18
+ export { createDatabase, probeDatabase } from "./database.js";
19
+ export { createExecution, probeExecution, WORKER_LISTEN } from "./execution.js";
20
+ export { createReadiness, infraProbes } from "./readiness.js";
21
+ export type { Probe, Readiness, ReadinessReport } from "./readiness.js";
22
+ export { createStreams, probeStreams } from "./streams.js";
23
+ export { servesWorker, startWorker, stopWorker } from "./workers.js";
24
+ export type { WorkerHandle } from "./workers.js";
25
+ export interface Infra {
26
+ database?: PostgresClient;
27
+ execution?: DurableExecution;
28
+ streams?: DurableStreams;
29
+ /** Present when at least one client was built. */
30
+ readiness?: Readiness;
31
+ /** Ends the pool and closes the streams. Does not stop a Worker. */
32
+ close(): Promise<void>;
33
+ }
34
+ /**
35
+ * Builds the configured clients. Throws naming the variable when endpoints are
36
+ * incomplete (e.g. a Restate ingress without an admin URL).
37
+ */
38
+ export declare function createInfra(config: Pick<StackConfig, "role" | "endpoints">, options?: {
39
+ logger?: Logger;
40
+ execution?: CreateExecutionOptions;
41
+ streams?: CreateStreamsOptions;
42
+ }): Infra;
@@ -0,0 +1,56 @@
1
+ import { createDatabase } from "./database.js";
2
+ import { createExecution, executionKind } from "./execution.js";
3
+ import { createReadiness, infraProbes } from "./readiness.js";
4
+ import { createStreams, streamsKind } from "./streams.js";
5
+ export { createDatabase, probeDatabase } from "./database.js";
6
+ export { createExecution, probeExecution, WORKER_LISTEN } from "./execution.js";
7
+ export { createReadiness, infraProbes } from "./readiness.js";
8
+ export { createStreams, probeStreams } from "./streams.js";
9
+ export { servesWorker, startWorker, stopWorker } from "./workers.js";
10
+ /**
11
+ * Builds the configured clients. Throws naming the variable when endpoints are
12
+ * incomplete (e.g. a Restate ingress without an admin URL).
13
+ */
14
+ export function createInfra(config, options = {}) {
15
+ const database = config.endpoints.databaseUrl ? createDatabase(config) : undefined;
16
+ const execution = executionKind(config) === "restate"
17
+ ? createExecution(config, options.execution)
18
+ : undefined;
19
+ const streams = streamsKind(config) === "s2" ? createStreams(config, options.streams) : undefined;
20
+ const probes = infraProbes({
21
+ ...(database ? { database } : {}),
22
+ ...(execution ? { execution } : {}),
23
+ ...(streams ? { streams } : {}),
24
+ });
25
+ const readiness = Object.keys(probes).length > 0
26
+ ? logTransitions(createReadiness(probes), options.logger)
27
+ : undefined;
28
+ return {
29
+ ...(database ? { database } : {}),
30
+ ...(execution ? { execution } : {}),
31
+ ...(streams ? { streams } : {}),
32
+ ...(readiness ? { readiness } : {}),
33
+ async close() {
34
+ await Promise.allSettled([database?.end({ timeout: 5 }), streams?.close()]);
35
+ },
36
+ };
37
+ }
38
+ /** Logs a check when it starts failing and when it recovers, not on every probe. */
39
+ function logTransitions(readiness, logger) {
40
+ if (!logger)
41
+ return readiness;
42
+ const failing = new Set();
43
+ return async () => {
44
+ const report = await readiness();
45
+ for (const [name, ok] of Object.entries(report.checks)) {
46
+ if (!ok && !failing.has(name)) {
47
+ failing.add(name);
48
+ logger.warn("ready_check_failed", { check: name, error: report.errors[name] });
49
+ }
50
+ else if (ok && failing.delete(name)) {
51
+ logger.info("ready_check_recovered", { check: name });
52
+ }
53
+ }
54
+ return report;
55
+ };
56
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Aggregate readiness of the Host's infrastructure (architecture §13, §14.3):
3
+ * the Runtime's `/ready` covers Postgres, Restate and S2. s2-lite's image has
4
+ * no health check of its own, so this is the only place its health shows.
5
+ *
6
+ * Every probe runs on each call, in parallel, bounded by one timeout. A probe
7
+ * that throws or outlives the timeout marks its check `false`. Only check
8
+ * names and booleans are reported; the errors stay in the report for logs.
9
+ */
10
+ import type { DurableExecution } from "../execution/types.js";
11
+ import type { DurableStreams } from "../streams/types.js";
12
+ import { type PostgresClient } from "./database.js";
13
+ /** Resolves when the dependency answers; rejects otherwise. Must honour `signal`. */
14
+ export type Probe = (signal: AbortSignal) => Promise<void>;
15
+ export interface ReadinessReport {
16
+ ok: boolean;
17
+ /** One entry per configured dependency, e.g. `{ postgres, restate, s2 }`. */
18
+ checks: Record<string, boolean>;
19
+ /** Why each failing check failed. Not for unauthenticated responses. */
20
+ errors: Record<string, string>;
21
+ }
22
+ export type Readiness = () => Promise<ReadinessReport>;
23
+ /** Default bound on one readiness pass. Compose probes `/ready` every 2 s with a 5 s timeout. */
24
+ export declare const READINESS_TIMEOUT_MS = 2000;
25
+ export declare function createReadiness(probes: Readonly<Record<string, Probe>>, options?: {
26
+ timeoutMs?: number;
27
+ }): Readiness;
28
+ /** The standard checks for whichever infrastructure the Host was built with. */
29
+ export declare function infraProbes(infra: {
30
+ database?: PostgresClient;
31
+ execution?: DurableExecution;
32
+ streams?: DurableStreams;
33
+ }): Record<string, Probe>;
@@ -0,0 +1,53 @@
1
+ import { probeDatabase } from "./database.js";
2
+ import { probeExecution } from "./execution.js";
3
+ import { probeStreams } from "./streams.js";
4
+ /** Default bound on one readiness pass. Compose probes `/ready` every 2 s with a 5 s timeout. */
5
+ export const READINESS_TIMEOUT_MS = 2000;
6
+ export function createReadiness(probes, options = {}) {
7
+ const timeoutMs = options.timeoutMs ?? READINESS_TIMEOUT_MS;
8
+ return async () => {
9
+ const entries = Object.entries(probes);
10
+ const results = await Promise.all(entries.map(async ([name, probe]) => [name, await runProbe(probe, timeoutMs)]));
11
+ const checks = {};
12
+ const errors = {};
13
+ for (const [name, error] of results) {
14
+ checks[name] = error === undefined;
15
+ if (error !== undefined)
16
+ errors[name] = error;
17
+ }
18
+ return { ok: Object.values(checks).every(Boolean), checks, errors };
19
+ };
20
+ }
21
+ /** The standard checks for whichever infrastructure the Host was built with. */
22
+ export function infraProbes(infra) {
23
+ const probes = {};
24
+ const { database, execution, streams } = infra;
25
+ if (database)
26
+ probes.postgres = (signal) => probeDatabase(database, signal);
27
+ if (execution)
28
+ probes.restate = (signal) => probeExecution(execution, signal);
29
+ if (streams)
30
+ probes.s2 = (signal) => probeStreams(streams, signal);
31
+ return probes;
32
+ }
33
+ /** Runs one probe under a timeout; returns its failure message, or undefined when ready. */
34
+ async function runProbe(probe, timeoutMs) {
35
+ const controller = new AbortController();
36
+ let timer;
37
+ const timedOut = new Promise((resolve) => {
38
+ timer = setTimeout(() => {
39
+ controller.abort(new Error(`timed out after ${timeoutMs} ms`));
40
+ resolve(`timed out after ${timeoutMs} ms`);
41
+ }, timeoutMs);
42
+ });
43
+ try {
44
+ return await Promise.race([
45
+ probe(controller.signal).then(() => undefined, (error) => (error instanceof Error ? error.message : String(error))),
46
+ timedOut,
47
+ ]);
48
+ }
49
+ finally {
50
+ clearTimeout(timer);
51
+ controller.abort(new Error("readiness pass finished"));
52
+ }
53
+ }
@@ -0,0 +1,17 @@
1
+ import type { StackConfig } from "../host/stack-config.js";
2
+ import type { DurableStreams } from "../streams/types.js";
3
+ export interface CreateStreamsOptions {
4
+ /**
5
+ * Prepended to every basin name, so several Hosts or test runs can share one
6
+ * S2 account. See `streams/basin.ts`. Default "".
7
+ */
8
+ basinPrefix?: string;
9
+ }
10
+ /** Which implementation `createStreams` chooses for a configuration. */
11
+ export declare function streamsKind(config: Pick<StackConfig, "endpoints">): "s2" | "memory";
12
+ export declare function createStreams(config: Pick<StackConfig, "endpoints">, options?: CreateStreamsOptions): DurableStreams;
13
+ /**
14
+ * Readiness probe: resolves when the streams' service answers within
15
+ * `signal`'s lifetime. In-memory streams are always ready.
16
+ */
17
+ export declare function probeStreams(streams: DurableStreams, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Builds the Host's Durable Streams from the stack configuration
3
+ * (architecture §12.4, §14.5). Only endpoints are configurable, never the kind:
4
+ * with `NYLORUN_S2_ENDPOINT` the Host uses S2 (s2-lite on a developer machine,
5
+ * or the S2 service); without it, the in-memory implementation, which only
6
+ * unit tests and a local development Host use.
7
+ */
8
+ import { createS2Streams } from "../adapters/streams/s2.js";
9
+ import { MemoryStreams } from "../streams/memory.js";
10
+ /** Which implementation `createStreams` chooses for a configuration. */
11
+ export function streamsKind(config) {
12
+ return config.endpoints.s2Endpoint ? "s2" : "memory";
13
+ }
14
+ export function createStreams(config, options = {}) {
15
+ const { s2Endpoint, s2Token } = config.endpoints;
16
+ if (!s2Endpoint) {
17
+ if (s2Token)
18
+ throw new Error("NYLORUN_S2_TOKEN is set without NYLORUN_S2_ENDPOINT; set the endpoint too");
19
+ return new MemoryStreams();
20
+ }
21
+ return createS2Streams({
22
+ endpoint: s2Endpoint,
23
+ ...(s2Token ? { token: s2Token } : {}),
24
+ ...(options.basinPrefix ? { basinPrefix: options.basinPrefix } : {}),
25
+ });
26
+ }
27
+ /**
28
+ * Readiness probe: resolves when the streams' service answers within
29
+ * `signal`'s lifetime. In-memory streams are always ready.
30
+ */
31
+ export async function probeStreams(streams, signal) {
32
+ await streams.probe?.(signal);
33
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Worker lifecycle by role (architecture §6).
3
+ *
4
+ * | Role | Serves | `execution.start` |
5
+ * | --- | --- | --- |
6
+ * | `api` | Tenant API, Admin API, SSE | **never**: wakes, timers and sweep arming go through Restate's ingress, which needs no Worker endpoint |
7
+ * | `worker` | the Restate endpoint (`NylorunSession.advance`, `NylorunTenant.sweep`, timers) | yes |
8
+ * | `all` | both (the local stack) | yes |
9
+ *
10
+ * An API node that called `start` would register its own endpoint with
11
+ * Restate, and Restate would send new invocations to it (it routes to the
12
+ * latest registered deployment), so the rule is enforced here rather than left
13
+ * to callers: `startWorker` with role `api` returns a handle that never
14
+ * started anything.
15
+ */
16
+ import type { DurableExecution, WorkerHandlers } from "../execution/types.js";
17
+ import type { RuntimeRole } from "../host/stack-config.js";
18
+ export interface WorkerHandle {
19
+ readonly role: RuntimeRole;
20
+ /** True when this process serves the Worker endpoint. */
21
+ readonly serving: boolean;
22
+ }
23
+ /** Whether a process in `role` serves the Worker endpoint. */
24
+ export declare function servesWorker(role: RuntimeRole): boolean;
25
+ /**
26
+ * Starts delivering Restate invocations to `handlers` when `role` is `worker`
27
+ * or `all` (serving and registering the Worker endpoint, for Restate). For
28
+ * `api` it does nothing. Rejects if the endpoint cannot listen or register.
29
+ */
30
+ export declare function startWorker(input: {
31
+ role: RuntimeRole;
32
+ execution: DurableExecution;
33
+ handlers: WorkerHandlers;
34
+ }): Promise<WorkerHandle>;
35
+ /**
36
+ * Stops a Worker started by `startWorker`: aborts running advances' signals
37
+ * and waits for them. Idempotent; a no-op for the api role.
38
+ */
39
+ export declare function stopWorker(handle: WorkerHandle): Promise<void>;