@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,61 @@
1
+ /**
2
+ * The Host's Durable Session Execution (architecture §6, §12.3, §14.8): one per process,
3
+ * shared by every Tenant the Host opens.
4
+ *
5
+ * - `tenantExecution` is what the Host passes to each Tenant it opens
6
+ * (`TenantOpenHooks.execution`): the execution, the `TenantWorkers` registry its handlers
7
+ * dispatch through (with `resolve` opening a Tenant on demand when an invocation arrives for
8
+ * one that is not open), and the Tenant status hook for stuck invocations.
9
+ * - `start` serves the Worker endpoint for `--role worker|all` (`infra/workers.ts`) and does
10
+ * nothing for `api`, whose wakes, timers and sweep arming go through Restate's ingress.
11
+ * - `armAll` re-arms every Tenant's sweep at startup. Arming is idempotent, so it is safe on
12
+ * every start, and it is what recovers from lost Restate state: the sweep re-wakes `runnable`
13
+ * sessions without a live owner (§14.8).
14
+ * - `disarm` stops a deleted Tenant's sweep.
15
+ *
16
+ * **Order at startup.** Call `start` before opening any Tenant. Opening a Tenant arms its
17
+ * sweep through Restate's ingress, which answers 404 until a Worker has registered the
18
+ * services, so the open would fail. An api-role process relies on a Worker having registered
19
+ * them first.
20
+ *
21
+ * **One Worker URL.** `start` registers the Worker endpoint (`NYLORUN_WORKER_URL`) with
22
+ * Restate once. Restate sends new invocations to the latest registered deployment, so
23
+ * several Workers that each register their own URL do not share load: the last one to start
24
+ * takes every new invocation. The V1 stack runs a single `runtime` container with
25
+ * `--role all`. A deployment with several Worker processes (Helm, Cloud) must advertise one
26
+ * load-balanced `NYLORUN_WORKER_URL` shared by all of them.
27
+ */
28
+ import type { DurableExecution } from "../execution/types.js";
29
+ import type { Logger } from "../tenant/types.js";
30
+ import { type TenantExecution, type TenantWorker } from "../tenant/worker.js";
31
+ import type { RuntimeRole } from "./stack-config.js";
32
+ export interface CreateHostExecutionOptions {
33
+ execution: DurableExecution;
34
+ role: RuntimeRole;
35
+ /** Finds or opens a Tenant an invocation arrived for that is not open on this process. */
36
+ resolve: (tenantId: string) => Promise<TenantWorker | undefined>;
37
+ logger?: Logger;
38
+ /**
39
+ * How long one advance may run before its signal aborts and it settles
40
+ * (`tenant/worker.ts`). Default 50 minutes, below Restate's timeouts.
41
+ */
42
+ advanceDeadlineMs?: number;
43
+ /** How long an aborted advance has to return before it is abandoned. Default 30 s. */
44
+ advanceGraceMs?: number;
45
+ }
46
+ export interface HostExecution {
47
+ /** Pass to every Tenant the Host opens (`TenantOpenHooks.execution`). */
48
+ readonly tenantExecution: TenantExecution;
49
+ /** Serves the Worker endpoint for the worker and all roles. Idempotent. */
50
+ start(): Promise<void>;
51
+ /** Stops serving: aborts running advances and waits for them. Idempotent. */
52
+ stop(): Promise<void>;
53
+ /**
54
+ * Arms every listed Tenant's sweep (idempotent). Tries them all; rejects with an
55
+ * `AggregateError` of the failures after logging each.
56
+ */
57
+ armAll(tenantIds: readonly string[]): Promise<void>;
58
+ /** Stops re-arming a deleted Tenant's sweep. */
59
+ disarm(tenantId: string): Promise<void>;
60
+ }
61
+ export declare function createHostExecution(options: CreateHostExecutionOptions): HostExecution;
@@ -0,0 +1,69 @@
1
+ import { startWorker, stopWorker } from "../infra/workers.js";
2
+ import { TenantWorkers, } from "../tenant/worker.js";
3
+ /** How many sweep arms `armAll` sends at once. */
4
+ const ARM_CONCURRENCY = 16;
5
+ export function createHostExecution(options) {
6
+ const { execution, role, logger } = options;
7
+ const workers = new TenantWorkers({
8
+ resolve: options.resolve,
9
+ ...(options.advanceDeadlineMs !== undefined
10
+ ? { advanceDeadlineMs: options.advanceDeadlineMs }
11
+ : {}),
12
+ ...(options.advanceGraceMs !== undefined
13
+ ? { advanceGraceMs: options.advanceGraceMs }
14
+ : {}),
15
+ ...(logger ? { logger } : {}),
16
+ });
17
+ const tenantExecution = {
18
+ execution,
19
+ workers,
20
+ ...(execution.stuckInvocations
21
+ ? {
22
+ stuckInvocations: (tenantId) => execution.stuckInvocations(tenantId),
23
+ }
24
+ : {}),
25
+ };
26
+ let started;
27
+ return {
28
+ tenantExecution,
29
+ async start() {
30
+ started ??= startWorker({ role, execution, handlers: workers.handlers });
31
+ try {
32
+ await started;
33
+ }
34
+ catch (error) {
35
+ started = undefined;
36
+ throw error;
37
+ }
38
+ },
39
+ async stop() {
40
+ const handle = await started?.catch(() => undefined);
41
+ if (handle)
42
+ await stopWorker(handle);
43
+ },
44
+ async armAll(tenantIds) {
45
+ const queue = [...new Set(tenantIds)];
46
+ const failures = [];
47
+ const next = async () => {
48
+ for (let id = queue.shift(); id !== undefined; id = queue.shift()) {
49
+ try {
50
+ await execution.armSweep(id);
51
+ }
52
+ catch (error) {
53
+ failures.push(error);
54
+ logger?.warn("tenant sweep arm failed", {
55
+ tenantId: id,
56
+ message: error instanceof Error ? error.message : String(error),
57
+ });
58
+ }
59
+ }
60
+ };
61
+ await Promise.all(Array.from({ length: Math.min(ARM_CONCURRENCY, queue.length) }, next));
62
+ if (failures.length > 0)
63
+ throw new AggregateError(failures, `Arming ${failures.length} Tenant sweep(s) failed`);
64
+ },
65
+ async disarm(tenantId) {
66
+ await execution.disarmSweep(tenantId);
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,39 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type ErrorCode, type ProtocolRange } from "@nylorun/core/compatibility";
3
+ export declare const OPAQUE_NOT_FOUND: {
4
+ status: "rejected";
5
+ code: "not_found";
6
+ message: string;
7
+ };
8
+ export declare const EXIT_PORT_IN_USE = 98;
9
+ export declare const EXIT_NON_LOOPBACK = 99;
10
+ export declare class HostListenError extends Error {
11
+ readonly exitCode: number;
12
+ readonly cause?: unknown;
13
+ constructor(message: string, exitCode: number, cause?: unknown);
14
+ }
15
+ export declare function sendJson(response: ServerResponse, status: number, body: unknown): void;
16
+ export declare function sendRejected(response: ServerResponse, status: number, code: ErrorCode, message: string, details?: unknown): void;
17
+ export declare function sendOpaqueNotFound(response: ServerResponse): void;
18
+ export declare function sendProtocolRejected(response: ServerResponse, protocol?: ProtocolRange): void;
19
+ /**
20
+ * D§11 Host allowlist: loopback forms with the listening port, plus the
21
+ * configured host when `allowNonLoopback` is set.
22
+ *
23
+ * An explicit `allowedHosts` list (container mode) replaces that rule: only
24
+ * those exact `name:port` values are accepted.
25
+ */
26
+ export declare function isAllowedRequestHost(hostHeader: string | undefined, options: {
27
+ port: number;
28
+ host: string;
29
+ allowNonLoopback?: boolean;
30
+ allowedHosts?: readonly string[];
31
+ }): boolean;
32
+ export declare function isJsonContentType(value: string | undefined): boolean;
33
+ /** True when the request carries a body (Content-Length > 0 or chunked). */
34
+ export declare function requestHasBody(request: IncomingMessage): boolean;
35
+ /** Redact path parameters after `/v1/sessions/` for Host logs. */
36
+ export declare function redactRoutePath(pathname: string): string;
37
+ export declare function readBearer(authorization: string | undefined): string | undefined;
38
+ export declare function readJsonBody(request: import("node:http").IncomingMessage, limit?: number): Promise<unknown>;
39
+ export declare function isLoopbackHost(host: string): boolean;
@@ -0,0 +1,142 @@
1
+ import { HOST_PROTOCOL, } from "@nylorun/core/compatibility";
2
+ export const OPAQUE_NOT_FOUND = {
3
+ status: "rejected",
4
+ code: "not_found",
5
+ message: "Not found",
6
+ };
7
+ export const EXIT_PORT_IN_USE = 98;
8
+ export const EXIT_NON_LOOPBACK = 99;
9
+ export class HostListenError extends Error {
10
+ exitCode;
11
+ cause;
12
+ constructor(message, exitCode, cause) {
13
+ super(message);
14
+ this.exitCode = exitCode;
15
+ this.cause = cause;
16
+ this.name = "HostListenError";
17
+ }
18
+ }
19
+ export function sendJson(response, status, body) {
20
+ const payload = JSON.stringify(body);
21
+ // No Access-Control-* headers (D§11).
22
+ response.writeHead(status, {
23
+ "content-type": "application/json",
24
+ "content-length": Buffer.byteLength(payload),
25
+ });
26
+ response.end(payload);
27
+ }
28
+ export function sendRejected(response, status, code, message, details) {
29
+ const body = { status: "rejected", code, message };
30
+ if (details !== undefined)
31
+ body.details = details;
32
+ sendJson(response, status, body);
33
+ }
34
+ export function sendOpaqueNotFound(response) {
35
+ sendJson(response, 404, OPAQUE_NOT_FOUND);
36
+ }
37
+ export function sendProtocolRejected(response, protocol = HOST_PROTOCOL) {
38
+ sendJson(response, 426, {
39
+ status: "rejected",
40
+ code: "protocol_unsupported",
41
+ protocol: {
42
+ min: protocol.min,
43
+ max: protocol.max,
44
+ features: [...protocol.features],
45
+ },
46
+ });
47
+ }
48
+ /**
49
+ * D§11 Host allowlist: loopback forms with the listening port, plus the
50
+ * configured host when `allowNonLoopback` is set.
51
+ *
52
+ * An explicit `allowedHosts` list (container mode) replaces that rule: only
53
+ * those exact `name:port` values are accepted.
54
+ */
55
+ export function isAllowedRequestHost(hostHeader, options) {
56
+ if (hostHeader === undefined)
57
+ return false;
58
+ const normalized = hostHeader.trim().toLowerCase();
59
+ if (options.allowedHosts !== undefined) {
60
+ return options.allowedHosts.some((entry) => entry.trim().toLowerCase() === normalized);
61
+ }
62
+ const port = String(options.port);
63
+ const allowed = new Set([
64
+ `127.0.0.1:${port}`,
65
+ `localhost:${port}`,
66
+ `[::1]:${port}`,
67
+ ]);
68
+ if (options.allowNonLoopback) {
69
+ const configured = options.host.trim().toLowerCase();
70
+ if (configured.includes(":") && !configured.startsWith("[")) {
71
+ allowed.add(`[${configured}]:${port}`);
72
+ }
73
+ else {
74
+ allowed.add(`${configured}:${port}`);
75
+ }
76
+ }
77
+ return allowed.has(normalized);
78
+ }
79
+ export function isJsonContentType(value) {
80
+ if (value === undefined)
81
+ return false;
82
+ const media = value.split(";")[0].trim().toLowerCase();
83
+ return media === "application/json";
84
+ }
85
+ /** True when the request carries a body (Content-Length > 0 or chunked). */
86
+ export function requestHasBody(request) {
87
+ const te = request.headers["transfer-encoding"];
88
+ const transfer = Array.isArray(te) ? te.join(",") : te;
89
+ if (transfer && transfer.toLowerCase() !== "identity")
90
+ return true;
91
+ const cl = request.headers["content-length"];
92
+ const raw = Array.isArray(cl) ? cl[0] : cl;
93
+ if (raw === undefined)
94
+ return false;
95
+ const n = Number(raw);
96
+ return Number.isFinite(n) && n > 0;
97
+ }
98
+ /** Redact path parameters after `/v1/sessions/` for Host logs. */
99
+ export function redactRoutePath(pathname) {
100
+ const parts = pathname.split("/");
101
+ // ["", "v1", "sessions", "<id>", ...]
102
+ if (parts[1] === "v1" && parts[2] === "sessions" && parts.length > 3) {
103
+ return `/v1/sessions/:id${parts.length > 4 ? "/…" : ""}`;
104
+ }
105
+ return pathname;
106
+ }
107
+ export function readBearer(authorization) {
108
+ if (!authorization)
109
+ return undefined;
110
+ const match = /^Bearer\s+(\S+)/i.exec(authorization.trim());
111
+ return match?.[1];
112
+ }
113
+ export async function readJsonBody(request, limit = 1024 * 1024) {
114
+ const chunks = [];
115
+ let size = 0;
116
+ for await (const chunk of request) {
117
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
118
+ size += buf.length;
119
+ if (size > limit) {
120
+ const error = new Error("Request too large");
121
+ error.status = 413;
122
+ throw error;
123
+ }
124
+ chunks.push(buf);
125
+ }
126
+ if (chunks.length === 0)
127
+ return undefined;
128
+ try {
129
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
130
+ }
131
+ catch {
132
+ const error = new Error("Invalid JSON");
133
+ error.status = 400;
134
+ throw error;
135
+ }
136
+ }
137
+ export function isLoopbackHost(host) {
138
+ const normalized = host.trim().toLowerCase();
139
+ return (normalized === "127.0.0.1" ||
140
+ normalized === "::1" ||
141
+ normalized === "localhost");
142
+ }
@@ -0,0 +1,8 @@
1
+ export type { HostConfigFile, HostCredentialsFile, } from "./config.js";
2
+ export { baselineEnvironment, hostProcessEnvironment, tenantChildEnvironment, } from "./environment.js";
3
+ export { createHost, adminKeyMatches, type CreateHostOptions, type HostServer } from "./create-host.js";
4
+ export { createHostLogger } from "./logger.js";
5
+ export { configForFactory } from "./config-for.js";
6
+ export { createHostExecution, type CreateHostExecutionOptions, type HostExecution, } from "./execution.js";
7
+ export { parseStackConfig, StackConfigError, type ContainerListen, type RuntimeRole, type StackConfig, type StackEndpoints, } from "./stack-config.js";
8
+ export { EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK, HostListenError, OPAQUE_NOT_FOUND, } from "./http.js";
@@ -0,0 +1,7 @@
1
+ export { baselineEnvironment, hostProcessEnvironment, tenantChildEnvironment, } from "./environment.js";
2
+ export { createHost, adminKeyMatches } from "./create-host.js";
3
+ export { createHostLogger } from "./logger.js";
4
+ export { configForFactory } from "./config-for.js";
5
+ export { createHostExecution, } from "./execution.js";
6
+ export { parseStackConfig, StackConfigError, } from "./stack-config.js";
7
+ export { EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK, HostListenError, OPAQUE_NOT_FOUND, } from "./http.js";
@@ -0,0 +1,3 @@
1
+ import type { Logger } from "../tenant/types.js";
2
+ /** Host logger: JSON lines to stdout (CLI redirects to `runtime.log`). */
3
+ export declare function createHostLogger(write?: (line: string) => void): Logger;
@@ -0,0 +1,18 @@
1
+ /** Host logger: JSON lines to stdout (CLI redirects to `runtime.log`). */
2
+ export function createHostLogger(write = (line) => {
3
+ process.stdout.write(`${line}\n`);
4
+ }) {
5
+ const emit = (level, message, fields) => {
6
+ write(JSON.stringify({
7
+ level,
8
+ message,
9
+ time: new Date().toISOString(),
10
+ ...fields,
11
+ }));
12
+ };
13
+ return {
14
+ info: (message, fields) => emit("info", message, fields),
15
+ warn: (message, fields) => emit("warn", message, fields),
16
+ error: (message, fields) => emit("error", message, fields),
17
+ };
18
+ }
@@ -0,0 +1,3 @@
1
+ import { EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK } from "./http.js";
2
+ export declare function main(): Promise<void>;
3
+ export { EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK };
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Runtime Host process entry (`@nylorun/runtime/server`).
3
+ *
4
+ * Reads `NYLORUN_HOME` (falls back to `~/.nylorun`), and hands an environment
5
+ * snapshot and argv to `parseStackConfig` (container listen mode, `--role`,
6
+ * stack endpoints). Absolute Host root is resolved once and passed down.
7
+ *
8
+ * Composition: `createInfra` builds the Postgres pool, Durable Session
9
+ * Execution and Durable Streams from the endpoints; `createHostExecution`
10
+ * shares one execution across the Tenants; the Tenant store is Postgres
11
+ * (`NYLORUN_DATABASE_URL`, required); `/ready` reports the infrastructure
12
+ * checks. See the startup order in `main()`. Tests compose a Host without this
13
+ * entry, with `createHost` and an injected Tenant module.
14
+ */
15
+ import { readFileSync, mkdirSync, existsSync } from "node:fs";
16
+ import { createRequire } from "node:module";
17
+ import { homedir } from "node:os";
18
+ import { resolve } from "node:path";
19
+ import { hostPaths, trashSqliteTenants } from "../tenant/paths.js";
20
+ import { createTenantModule } from "../tenant/module.js";
21
+ import { createPostgresTenantStore } from "../tenant/store-pg.js";
22
+ import { openTenantRuntime } from "../tenant/runtime.js";
23
+ import { createTenantStreams, deleteTenantStreams } from "../tenant/streams.js";
24
+ import { configForFactory } from "./config-for.js";
25
+ import { createHost } from "./create-host.js";
26
+ import { createHostExecution } from "./execution.js";
27
+ import { baselineEnvironment } from "./environment.js";
28
+ import { HostListenError, EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK, } from "./http.js";
29
+ import { createHostLogger } from "./logger.js";
30
+ import { describeEndpoints, parseStackConfig } from "./stack-config.js";
31
+ import { createExecution, createInfra } from "../infra/index.js";
32
+ const nodeRequire = createRequire(import.meta.url);
33
+ function coreVersion() {
34
+ try {
35
+ return nodeRequire("@nylorun/core/package.json").version;
36
+ }
37
+ catch {
38
+ return "unknown";
39
+ }
40
+ }
41
+ function loadJson(path) {
42
+ return JSON.parse(readFileSync(path, "utf8"));
43
+ }
44
+ function resolveHostRoot() {
45
+ const fromEnv = process.env.NYLORUN_HOME;
46
+ const root = fromEnv && fromEnv.length > 0 ? fromEnv : resolve(homedir(), ".nylorun");
47
+ return resolve(root);
48
+ }
49
+ export async function main() {
50
+ const stack = parseStackConfig(process.env, process.argv.slice(2));
51
+ const hostRoot = resolveHostRoot();
52
+ const paths = hostPaths(hostRoot);
53
+ mkdirSync(paths.home, { recursive: true });
54
+ mkdirSync(paths.tmp, { recursive: true });
55
+ mkdirSync(paths.tenants, { recursive: true });
56
+ if (!existsSync(paths.config)) {
57
+ throw new Error(`Missing host.json at ${paths.config}; run \`nylorun start\` first`);
58
+ }
59
+ if (!existsSync(paths.credentials)) {
60
+ throw new Error(`Missing host-credentials.json at ${paths.credentials}; run \`nylorun start\` first`);
61
+ }
62
+ // Tenants are Postgres schemas. Only a Host outside a container (a local
63
+ // development Host) may run without S2, on in-process streams.
64
+ if (!stack.endpoints.databaseUrl) {
65
+ throw new Error("NYLORUN_DATABASE_URL is required: the Postgres URL of the Session Store (`nylorun start` sets it)");
66
+ }
67
+ if (stack.listen && !stack.endpoints.s2Endpoint) {
68
+ throw new Error("NYLORUN_S2_ENDPOINT is required in container mode: the S2 endpoint of Durable Streams (`nylorun start` sets it)");
69
+ }
70
+ const config = loadJson(paths.config);
71
+ const credentials = loadJson(paths.credentials);
72
+ const logger = createHostLogger();
73
+ logger.info("host_stack_config", {
74
+ role: stack.role,
75
+ mode: stack.listen ? "container" : "local",
76
+ endpoints: describeEndpoints(stack.endpoints),
77
+ });
78
+ // Tenants from the SQLite Runtime are not migrated: move them out of the way.
79
+ trashSqliteTenants(hostRoot, logger);
80
+ const infra = createInfra(stack, { logger });
81
+ const database = infra.database;
82
+ if (!database)
83
+ throw new Error("NYLORUN_DATABASE_URL did not yield a Postgres pool");
84
+ const baseline = baselineEnvironment(process.env);
85
+ // A Tenant that should answer with the fixture model carries the Tenant
86
+ // setting (`tenant/model-setting.ts`); the Host has no fixture mode.
87
+ const configFor = configForFactory({
88
+ hostRoot,
89
+ hostConfig: config,
90
+ logger,
91
+ baseline,
92
+ });
93
+ // One Durable Session Execution for every Tenant this process opens: Restate
94
+ // when its endpoints are set, else the in-process memory execution. An
95
+ // invocation for a Tenant that is not open here opens it on demand.
96
+ const hostExecution = createHostExecution({
97
+ execution: infra.execution ?? createExecution(stack),
98
+ role: stack.role,
99
+ resolve: (tenantId) => module.worker(tenantId),
100
+ logger,
101
+ });
102
+ // Durable Streams: S2 when configured (always in container mode). Without
103
+ // them (a local Host only) each Tenant keeps in-process streams, whose history
104
+ // does not survive a restart.
105
+ const streams = infra.streams;
106
+ const store = createPostgresTenantStore({
107
+ hostRoot,
108
+ sql: database,
109
+ configFor,
110
+ logger,
111
+ openRuntime: (tenantConfig, opened) => openTenantRuntime(tenantConfig, {
112
+ execution: hostExecution.tenantExecution,
113
+ ...(streams ? { streams } : {}),
114
+ ...opened,
115
+ }),
116
+ });
117
+ const module = createTenantModule({
118
+ store,
119
+ logger,
120
+ // A new Tenant's basin is created with it (opening it repairs a failure);
121
+ // a deleted Tenant's sweep stops re-arming and its basin goes.
122
+ ...(streams
123
+ ? { onCreated: (tenantId) => createTenantStreams(streams, tenantId) }
124
+ : {}),
125
+ onDeleted: async (tenantId) => {
126
+ const failed = (await Promise.allSettled([
127
+ hostExecution.disarm(tenantId),
128
+ ...(streams ? [deleteTenantStreams(streams, tenantId)] : []),
129
+ ])).flatMap((result) => (result.status === "rejected" ? [result.reason] : []));
130
+ if (failed.length > 0)
131
+ throw new AggregateError(failed, "Deleted Tenant cleanup failed");
132
+ },
133
+ });
134
+ const options = {
135
+ hostRoot,
136
+ module,
137
+ config,
138
+ credentials,
139
+ logger,
140
+ coreVersion: coreVersion(),
141
+ ...(stack.listen ? { listen: stack.listen } : {}),
142
+ ...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
143
+ ...(infra.readiness ? { readiness: infra.readiness } : {}),
144
+ // SIGTERM and POST /v1/admin/host/shutdown both close the Host this way:
145
+ // stop the Worker, close the Tenants, then end the infrastructure clients.
146
+ shutdown: {
147
+ beforeTenants: () => hostExecution.stop(),
148
+ afterTenants: () => infra.close(),
149
+ },
150
+ };
151
+ const host = createHost(options);
152
+ // Startup order. The Worker starts first: opening a Tenant arms its sweep
153
+ // through Restate's ingress, which answers 404 until a Worker has registered
154
+ // the services, so no Tenant may open before `start` (the listener opens
155
+ // Tenants on demand). An api-role process serves no Worker endpoint, so it
156
+ // can open Tenants only once some Worker process has registered. Container
157
+ // mode runs one `--role all` process, which registers here. Then the
158
+ // listener starts (and marks discovery done), and every listed Tenant's
159
+ // sweep is re-armed, which recovers wakes lost with Restate's state (§14.8).
160
+ try {
161
+ await hostExecution.start();
162
+ await host.listen();
163
+ }
164
+ catch (error) {
165
+ await hostExecution.stop().catch(() => undefined);
166
+ await infra.close();
167
+ if (error instanceof HostListenError) {
168
+ logger.error("listen_failed", {
169
+ message: error.message,
170
+ exitCode: error.exitCode,
171
+ });
172
+ process.exitCode = error.exitCode;
173
+ return;
174
+ }
175
+ throw error;
176
+ }
177
+ const tenants = await module.list();
178
+ await hostExecution
179
+ .armAll(tenants.filter((t) => t.state === "open").map((t) => t.id))
180
+ .catch((error) => logger.warn("tenant_sweeps_not_armed", {
181
+ message: error instanceof Error ? error.message : String(error),
182
+ }));
183
+ logger.info("host_ready", {
184
+ url: host.url,
185
+ hostId: config.hostId,
186
+ tenants: tenants.length,
187
+ });
188
+ void host.closed.then(() => process.exit(0));
189
+ for (const signal of ["SIGINT", "SIGTERM"]) {
190
+ process.on(signal, () => void host.close());
191
+ }
192
+ }
193
+ void main().catch((error) => {
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ process.stderr.write(`${message}\n`);
196
+ if (error instanceof HostListenError) {
197
+ process.exit(error.exitCode);
198
+ }
199
+ process.exit(1);
200
+ });
201
+ export { EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK };
@@ -0,0 +1,72 @@
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 type RuntimeRole = "api" | "worker" | "all";
18
+ export declare const RUNTIME_ROLES: readonly RuntimeRole[];
19
+ export declare const DEFAULT_CONTAINER_LISTEN_HOST = "0.0.0.0";
20
+ export declare const DEFAULT_CONTAINER_LISTEN_PORT = 4000;
21
+ export interface ContainerListen {
22
+ /** Bind address, e.g. `0.0.0.0`. */
23
+ host: string;
24
+ port: number;
25
+ /**
26
+ * Exact `Host` header values accepted (lowercase `name:port`). Replaces the
27
+ * loopback-only rule.
28
+ */
29
+ allowedHosts: readonly string[];
30
+ }
31
+ export interface StackEndpoints {
32
+ databaseUrl?: string;
33
+ restateIngressUrl?: string;
34
+ restateAdminUrl?: string;
35
+ workerUrl?: string;
36
+ s2Endpoint?: string;
37
+ s2Token?: string;
38
+ workspaceStoreUrl?: string;
39
+ /**
40
+ * Restate request-identity public keys (`publickeyv1_...`) the Worker
41
+ * endpoint accepts, from `NYLORUN_RESTATE_IDENTITY_KEY` (comma-separated
42
+ * during a rotation). Unset means the endpoint accepts unsigned requests.
43
+ */
44
+ restateIdentityKeys?: string[];
45
+ }
46
+ export interface StackConfig {
47
+ role: RuntimeRole;
48
+ /** Present in container mode; absent means bind what host.json names. */
49
+ listen?: ContainerListen;
50
+ endpoints: StackEndpoints;
51
+ /**
52
+ * The URL clients use to reach this Host (`NYLORUN_PUBLIC_URL`), reported by
53
+ * `/v1/admin/status`. In container mode the bind address (`0.0.0.0:4000`)
54
+ * means nothing outside the container.
55
+ */
56
+ publicUrl?: string;
57
+ }
58
+ export declare class StackConfigError extends Error {
59
+ constructor(message: string);
60
+ }
61
+ type EnvSnapshot = Readonly<Record<string, string | undefined>>;
62
+ export declare function parseRole(argv: readonly string[]): RuntimeRole;
63
+ /**
64
+ * Normalize one `Host` header allowlist entry to lowercase `name:port`.
65
+ * IPv6 literals must be bracketed (`[::1]:4000`).
66
+ */
67
+ export declare function normalizeAllowedHost(name: string, entry: string): string;
68
+ /** Parse the stack configuration; throws `StackConfigError` naming the variable. */
69
+ export declare function parseStackConfig(env: EnvSnapshot, argv: readonly string[]): StackConfig;
70
+ /** Endpoint summary for logs: which endpoints are set, never their values. */
71
+ export declare function describeEndpoints(endpoints: StackEndpoints): string[];
72
+ export {};