@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc

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 (230) hide show
  1. package/LICENSE-EXCEPTION +26 -0
  2. package/README.md +34 -0
  3. package/dist/chunk-3R6KEFOI.js +22 -0
  4. package/dist/chunk-67BDAMYX.js +376 -0
  5. package/dist/chunk-AITGS27R.js +139 -0
  6. package/dist/chunk-CMAEHQ4F.js +6 -0
  7. package/dist/chunk-MLKGABMK.js +9 -0
  8. package/dist/chunk-O3UIK34B.js +1430 -0
  9. package/dist/chunk-VHTUSZUX.js +176 -0
  10. package/dist/cli.js +9006 -0
  11. package/dist/docker.js +54 -0
  12. package/dist/index.js +1053 -0
  13. package/dist/ingress.js +436 -0
  14. package/dist/jsx-runtime.js +2 -0
  15. package/dist/preact.js +22 -0
  16. package/dist/service-url.js +11 -0
  17. package/dist/services.js +213 -0
  18. package/dist/test.js +5069 -0
  19. package/dist/ui.js +254 -0
  20. package/dist/worker-protocol.js +89 -0
  21. package/package.json +112 -0
  22. package/src/app-icon.ts +74 -0
  23. package/src/apply.ts +52 -0
  24. package/src/bundle-ui.ts +211 -0
  25. package/src/bundle.ts +546 -0
  26. package/src/cli/build-dev-landing.ts +84 -0
  27. package/src/cli/build-install-panel.ts +90 -0
  28. package/src/cli/build-runtime.ts +133 -0
  29. package/src/cli/build.ts +63 -0
  30. package/src/cli/dev.ts +107 -0
  31. package/src/cli/down.ts +60 -0
  32. package/src/cli/main.ts +56 -0
  33. package/src/cli/test.ts +61 -0
  34. package/src/cli/validate.ts +37 -0
  35. package/src/compile.ts +100 -0
  36. package/src/create-app.ts +982 -0
  37. package/src/deep-merge.ts +33 -0
  38. package/src/dev/fixture.ts +48 -0
  39. package/src/dev/install-handler.ts +383 -0
  40. package/src/dev/server.ts +968 -0
  41. package/src/dev/stack.ts +108 -0
  42. package/src/dev/state.ts +159 -0
  43. package/src/docker.ts +96 -0
  44. package/src/errors.ts +61 -0
  45. package/src/factory.ts +481 -0
  46. package/src/garage.ts +56 -0
  47. package/src/generator/hash.ts +6 -0
  48. package/src/generator/resolve-refs.ts +207 -0
  49. package/src/grants-builder.ts +100 -0
  50. package/src/grants.ts +260 -0
  51. package/src/home-api.ts +172 -0
  52. package/src/home-cookie.ts +10 -0
  53. package/src/home-registry.ts +120 -0
  54. package/src/home.ts +187 -0
  55. package/src/host-registry.ts +143 -0
  56. package/src/http-header.ts +20 -0
  57. package/src/identifiers.ts +49 -0
  58. package/src/index.ts +66 -0
  59. package/src/info.ts +64 -0
  60. package/src/ini.ts +50 -0
  61. package/src/json.ts +14 -0
  62. package/src/jsx-runtime/index.ts +5 -0
  63. package/src/keys.ts +29 -0
  64. package/src/mysql.ts +56 -0
  65. package/src/pack-build.ts +108 -0
  66. package/src/pack.ts +115 -0
  67. package/src/peer-registry.ts +121 -0
  68. package/src/peer-world.ts +117 -0
  69. package/src/permission-meta.ts +116 -0
  70. package/src/permissions.ts +108 -0
  71. package/src/pkg-root.ts +34 -0
  72. package/src/postgres.ts +79 -0
  73. package/src/preact/index.ts +27 -0
  74. package/src/redis.ts +43 -0
  75. package/src/ref.ts +271 -0
  76. package/src/remote-registry.ts +33 -0
  77. package/src/required-host-kinds.ts +110 -0
  78. package/src/resolve-context.ts +111 -0
  79. package/src/resolve.ts +471 -0
  80. package/src/runtime/docker/compose/compose-schema.ts +118 -0
  81. package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
  82. package/src/runtime/docker/compose/default-hardening.ts +21 -0
  83. package/src/runtime/docker/compose/effects.ts +107 -0
  84. package/src/runtime/docker/compose/index.ts +17 -0
  85. package/src/runtime/docker/compose/labels.ts +39 -0
  86. package/src/runtime/docker/compose/types.ts +78 -0
  87. package/src/runtime/docker/home-proxy-server/index.ts +177 -0
  88. package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
  89. package/src/runtime/docker/ingress/build-config.ts +819 -0
  90. package/src/runtime/docker/ingress/index.ts +16 -0
  91. package/src/runtime/docker/ingress/types.ts +182 -0
  92. package/src/runtime/docker/log-frames.ts +121 -0
  93. package/src/runtime/docker/logs-server/index.ts +366 -0
  94. package/src/runtime/docker/spawn-server/context-key.ts +82 -0
  95. package/src/runtime/docker/spawn-server/index.ts +492 -0
  96. package/src/runtime/docker/spawn-server/protocol.ts +70 -0
  97. package/src/runtime/docker/spawn-server/spec.ts +122 -0
  98. package/src/runtime/docker/stats-server/disks.ts +161 -0
  99. package/src/runtime/docker/stats-server/index.ts +322 -0
  100. package/src/runtime/home-client/index.ts +71 -0
  101. package/src/runtime/log-line.ts +24 -0
  102. package/src/runtime/logs-client/index.ts +175 -0
  103. package/src/runtime/spawn-client/index.ts +93 -0
  104. package/src/runtime/spawn-client/kaniko.ts +79 -0
  105. package/src/runtime/stats-client/index.ts +65 -0
  106. package/src/runtime/stats-sample.ts +95 -0
  107. package/src/safe-json.ts +37 -0
  108. package/src/sandbox.ts +1209 -0
  109. package/src/schema/build-apps.ts +38 -0
  110. package/src/schema/home.ts +217 -0
  111. package/src/schema/host.ts +792 -0
  112. package/src/schema/inputs.ts +26 -0
  113. package/src/schema/nodes.ts +75 -0
  114. package/src/schema/peer.ts +400 -0
  115. package/src/schema/public-domain.ts +116 -0
  116. package/src/schema/remote.ts +74 -0
  117. package/src/security.ts +594 -0
  118. package/src/serialize.ts +21 -0
  119. package/src/service-definition.ts +284 -0
  120. package/src/service-url.ts +64 -0
  121. package/src/service.ts +39 -0
  122. package/src/services/index.ts +5 -0
  123. package/src/shared.ts +65 -0
  124. package/src/speculative-silo.ts +96 -0
  125. package/src/stable-secret.ts +14 -0
  126. package/src/surfaces.ts +264 -0
  127. package/src/tailwind-bin.ts +56 -0
  128. package/src/test/boot.ts +192 -0
  129. package/src/test/compile.ts +501 -0
  130. package/src/test/describe.ts +117 -0
  131. package/src/test/index.ts +11 -0
  132. package/src/test/ingress-fixture.ts +37 -0
  133. package/src/test/types.ts +318 -0
  134. package/src/trusted-headers.ts +79 -0
  135. package/src/types.ts +126 -0
  136. package/src/ui/components/aside.tsx +96 -0
  137. package/src/ui/components/badge.tsx +42 -0
  138. package/src/ui/components/button.tsx +59 -0
  139. package/src/ui/components/card.tsx +106 -0
  140. package/src/ui/components/checkbox.tsx +37 -0
  141. package/src/ui/components/code-block.tsx +29 -0
  142. package/src/ui/components/combobox.tsx +119 -0
  143. package/src/ui/components/config-field.tsx +284 -0
  144. package/src/ui/components/dialog.tsx +137 -0
  145. package/src/ui/components/domain-editor.tsx +317 -0
  146. package/src/ui/components/domain-list.ts +205 -0
  147. package/src/ui/components/dropdown-menu.tsx +73 -0
  148. package/src/ui/components/empty-state.tsx +51 -0
  149. package/src/ui/components/highlight.ts +59 -0
  150. package/src/ui/components/icons.tsx +101 -0
  151. package/src/ui/components/image-posture.tsx +85 -0
  152. package/src/ui/components/input.tsx +28 -0
  153. package/src/ui/components/label.tsx +16 -0
  154. package/src/ui/components/line-chart.tsx +113 -0
  155. package/src/ui/components/page.tsx +39 -0
  156. package/src/ui/components/permission-row.tsx +262 -0
  157. package/src/ui/components/permission-section.tsx +51 -0
  158. package/src/ui/components/pie-chart.tsx +245 -0
  159. package/src/ui/components/schema-block.tsx +60 -0
  160. package/src/ui/components/separator.tsx +42 -0
  161. package/src/ui/components/sheet.tsx +130 -0
  162. package/src/ui/components/sidebar.tsx +650 -0
  163. package/src/ui/components/silo-picker.tsx +510 -0
  164. package/src/ui/components/skeleton.tsx +13 -0
  165. package/src/ui/components/sonner.tsx +33 -0
  166. package/src/ui/components/switch.tsx +58 -0
  167. package/src/ui/components/table-view.ts +219 -0
  168. package/src/ui/components/table.tsx +506 -0
  169. package/src/ui/components/tabs.tsx +59 -0
  170. package/src/ui/components/timeago.tsx +84 -0
  171. package/src/ui/components/tooltip.tsx +56 -0
  172. package/src/ui/components.json +21 -0
  173. package/src/ui/dev/dev-shell.tsx +145 -0
  174. package/src/ui/home/home-grid.tsx +177 -0
  175. package/src/ui/hooks/use-mobile.ts +19 -0
  176. package/src/ui/index.ts +189 -0
  177. package/src/ui/install/adapter.ts +193 -0
  178. package/src/ui/install/deploy-targets-picker.tsx +185 -0
  179. package/src/ui/install/install-app-panel.tsx +912 -0
  180. package/src/ui/install/install-error.tsx +62 -0
  181. package/src/ui/install/perms-inactive.ts +50 -0
  182. package/src/ui/install/required-fields.ts +37 -0
  183. package/src/ui/install/tabs-form.tsx +557 -0
  184. package/src/ui/tokens.css +279 -0
  185. package/src/ui/utils.ts +78 -0
  186. package/src/ui-error.ts +139 -0
  187. package/src/ui-runtime/dev-landing-entry.tsx +258 -0
  188. package/src/ui-runtime/index.ts +83 -0
  189. package/src/ui-runtime/install-entry.tsx +119 -0
  190. package/src/ui-runtime/mount.ts +367 -0
  191. package/src/ui-runtime/runtime.css +56 -0
  192. package/src/ui-surface.ts +451 -0
  193. package/src/valkey.ts +43 -0
  194. package/src/worker/extract-tar.ts +125 -0
  195. package/src/worker/identity.ts +183 -0
  196. package/src/worker/index.ts +16 -0
  197. package/src/worker/public-ip.ts +92 -0
  198. package/src/worker/queue.ts +54 -0
  199. package/src/worker/reconcile.ts +127 -0
  200. package/src/worker/start-worker.ts +227 -0
  201. package/src/worker/ui-proxy.ts +335 -0
  202. package/src/worker/uplink.ts +523 -0
  203. package/src/worker/validate.ts +34 -0
  204. package/src/worker/verify-relay.ts +102 -0
  205. package/src/worker-protocol/app-dirs.ts +94 -0
  206. package/src/worker-protocol/home-proxy-paths.ts +76 -0
  207. package/src/worker-protocol/index.ts +55 -0
  208. package/src/worker-protocol/ingress.ts +166 -0
  209. package/src/worker-protocol/logs-socket-path.ts +50 -0
  210. package/src/worker-protocol/networks.ts +39 -0
  211. package/src/worker-protocol/node-version.ts +70 -0
  212. package/src/worker-protocol/silo-paths.ts +114 -0
  213. package/src/worker-protocol/spawn-paths.ts +165 -0
  214. package/src/worker-protocol/stats-socket-path.ts +40 -0
  215. package/src/worker-protocol/update-files.ts +77 -0
  216. package/src/worker-protocol/uplink/constants.ts +86 -0
  217. package/src/worker-protocol/uplink/errors.ts +61 -0
  218. package/src/worker-protocol/uplink/frame.ts +114 -0
  219. package/src/worker-protocol/uplink/identity.ts +40 -0
  220. package/src/worker-protocol/uplink/index.ts +63 -0
  221. package/src/worker-protocol/uplink/methods.ts +462 -0
  222. package/src/worker-protocol/uplink/mux.ts +646 -0
  223. package/src/worker-protocol/uplink/protocol.ts +11 -0
  224. package/src/worker-protocol/uplink/rpc.ts +309 -0
  225. package/src/worker-protocol/uplink/session.ts +540 -0
  226. package/src/worker-protocol/uplink/test-support.ts +188 -0
  227. package/src/worker-protocol/worker-mount.ts +27 -0
  228. package/src/worker-protocol/worker-state.ts +190 -0
  229. package/src/xml.ts +49 -0
  230. package/src/yaml.ts +15 -0
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * The wire shape of one tapped container log line: transport metadata
5
+ * wrapping the raw, untouched line. Produced by the logs-server and read
6
+ * by the logs-client — the two ends of one unix socket, so this is the
7
+ * single source of truth for both.
8
+ *
9
+ * Distinct from apps/logger's `LogEnvelope`, which is the *stored* record
10
+ * a log-collector app derives from this (ISO timestamp, app-centric, with
11
+ * structured fields unwrapped). Don't conflate the transport line with a
12
+ * persisted record.
13
+ */
14
+ export const logLineSchema = z.object({
15
+ appId: z.string(),
16
+ service: z.string(),
17
+ containerId: z.string(),
18
+ // Unix milliseconds.
19
+ timestamp: z.number(),
20
+ stream: z.enum(["stdout", "stderr"]),
21
+ line: z.string(),
22
+ });
23
+
24
+ export type LogLine = z.infer<typeof logLineSchema>;
@@ -0,0 +1,175 @@
1
+ // Consumer-side helper for the worker's logs socket. Apps with the
2
+ // `host.logs` permission mount the socket and use this to read a stream
3
+ // of `LogLine` records.
4
+ //
5
+ // Two entry points:
6
+ // - `connectToLogsSocket` — single-shot connect; throws on disconnect.
7
+ // Use for tests, one-off scans, or callers that want full control.
8
+ // - `streamLogs` — auto-reconnecting wrapper. Tracks the latest yielded
9
+ // timestamp and resumes from it on drop with exponential backoff.
10
+ //
11
+ // Cursor persistence is left to the caller via `readCursor` /
12
+ // `writeCursor`. The consumer owns the at-least-once boundary and must
13
+ // only persist a cursor *after* it has successfully processed the line.
14
+
15
+ import { type LogLine, logLineSchema } from "../log-line.ts";
16
+
17
+ export type { LogLine };
18
+
19
+ export interface ConnectOpts {
20
+ /**
21
+ * Cursor in unix ms. Omit (or pass `0`) to replay everything Docker
22
+ * still has on disk for each managed container — bounded by the
23
+ * default per-container retention (`max-size: 10m`, `max-file: 3`),
24
+ * so a fresh consumer pays a one-time, capped backfill rather than
25
+ * starting blind. To start "from now", pass `Date.now()`.
26
+ */
27
+ since?: number;
28
+ /** Abort to close the socket and end iteration. */
29
+ signal?: AbortSignal;
30
+ }
31
+
32
+ export interface StreamOpts extends ConnectOpts {
33
+ /** First reconnect delay (default 100ms). */
34
+ initialBackoffMs?: number;
35
+ /** Max reconnect delay (default 5000ms). */
36
+ maxBackoffMs?: number;
37
+ /**
38
+ * Called before each reconnect attempt (after the first). `lastError`
39
+ * is `undefined` if the previous connection ended cleanly (server EOF
40
+ * with no thrown error).
41
+ */
42
+ onReconnect?(attempt: number, lastError: unknown): void;
43
+ }
44
+
45
+ const encoder = new TextEncoder();
46
+
47
+ export async function* connectToLogsSocket(
48
+ path: string,
49
+ opts: ConnectOpts = {},
50
+ ): AsyncGenerator<LogLine> {
51
+ const conn = await Deno.connect({ transport: "unix", path });
52
+ const onAbort = () => {
53
+ try {
54
+ conn.close();
55
+ } catch {}
56
+ };
57
+ opts.signal?.addEventListener("abort", onAbort);
58
+ // Per-instance, streaming decoder so a multi-byte UTF-8 sequence
59
+ // straddling two `conn.read()` calls survives the boundary.
60
+ const decoder = new TextDecoder("utf-8");
61
+ try {
62
+ const line = opts.since !== undefined ? `${JSON.stringify({ since: opts.since })}\n` : "\n";
63
+ await conn.write(encoder.encode(line));
64
+
65
+ const buf = new Uint8Array(8192);
66
+ let remainder = "";
67
+ while (true) {
68
+ const n = await conn.read(buf);
69
+ if (n === null) {
70
+ // Flush any bytes still buffered inside the decoder; partial UTF-8
71
+ // at EOF becomes the replacement char, which is fine — we only
72
+ // surface complete `\n`-terminated lines below.
73
+ remainder += decoder.decode();
74
+ return;
75
+ }
76
+ remainder += decoder.decode(buf.slice(0, n), { stream: true });
77
+ let nl: number;
78
+ while ((nl = remainder.indexOf("\n")) !== -1) {
79
+ const raw = remainder.slice(0, nl).trim();
80
+ remainder = remainder.slice(nl + 1);
81
+ if (!raw) continue;
82
+ try {
83
+ yield logLineSchema.parse(JSON.parse(raw));
84
+ } catch {
85
+ // Skip malformed lines; the server shouldn't emit them, but a
86
+ // partial buffer at the boundary shouldn't kill the stream.
87
+ }
88
+ }
89
+ }
90
+ } finally {
91
+ opts.signal?.removeEventListener("abort", onAbort);
92
+ try {
93
+ conn.close();
94
+ } catch {}
95
+ }
96
+ }
97
+
98
+ export async function* streamLogs(path: string, opts: StreamOpts = {}): AsyncGenerator<LogLine> {
99
+ const initial = opts.initialBackoffMs ?? 100;
100
+ const max = opts.maxBackoffMs ?? 5000;
101
+ let cursor = opts.since;
102
+ let backoff = initial;
103
+ let attempt = 0;
104
+ let lastError: unknown;
105
+
106
+ while (!opts.signal?.aborted) {
107
+ if (attempt > 0) opts.onReconnect?.(attempt, lastError);
108
+ try {
109
+ for await (const env of connectToLogsSocket(path, {
110
+ since: cursor,
111
+ signal: opts.signal,
112
+ })) {
113
+ cursor = env.timestamp;
114
+ backoff = initial;
115
+ attempt = 0;
116
+ yield env;
117
+ }
118
+ lastError = undefined;
119
+ } catch (err) {
120
+ lastError = err;
121
+ }
122
+
123
+ if (opts.signal?.aborted) return;
124
+ attempt++;
125
+ await sleep(backoff, opts.signal);
126
+ backoff = Math.min(backoff * 2, max);
127
+ }
128
+ }
129
+
130
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
131
+ return new Promise((resolve) => {
132
+ if (signal?.aborted) return resolve();
133
+ const timer = setTimeout(() => {
134
+ signal?.removeEventListener("abort", onAbort);
135
+ resolve();
136
+ }, ms);
137
+ const onAbort = () => {
138
+ clearTimeout(timer);
139
+ resolve();
140
+ };
141
+ signal?.addEventListener("abort", onAbort, { once: true });
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Read a persisted cursor from `path`. Returns `undefined` if the file
147
+ * doesn't exist or its contents aren't a finite number.
148
+ */
149
+ export async function readCursor(path: string): Promise<number | undefined> {
150
+ let raw: string;
151
+ try {
152
+ raw = await Deno.readTextFile(path);
153
+ } catch (err) {
154
+ if (err instanceof Deno.errors.NotFound) return undefined;
155
+ throw err;
156
+ }
157
+ const n = Number(raw.trim());
158
+ return Number.isFinite(n) ? n : undefined;
159
+ }
160
+
161
+ /**
162
+ * Write `ts` to `path` via tempfile + rename. A process crash mid-write
163
+ * can't truncate an existing cursor: either the old value or the new
164
+ * value is observable, never a partial write.
165
+ *
166
+ * Not durable across power loss — there's no fsync. Combined with the
167
+ * at-least-once semantics of `streamLogs` (resume re-delivers the
168
+ * boundary line) this is the right tradeoff: a lost cursor on power
169
+ * loss costs at most a small replay window.
170
+ */
171
+ export async function writeCursor(path: string, ts: number): Promise<void> {
172
+ const tmp = `${path}.tmp`;
173
+ await Deno.writeTextFile(tmp, String(ts));
174
+ await Deno.rename(tmp, path);
175
+ }
@@ -0,0 +1,93 @@
1
+ // Consumer-side helper for the worker's `spawn.sock` broker. Apps with
2
+ // the `host.spawn` permission mount the socket and call `buildImage` to
3
+ // run one disposable, fully-hardened box and stream its output.
4
+ //
5
+ // Mirrors `logs-client`: connect, write one request line, read the log
6
+ // lines (→ `onLog`) and the single terminal line, return.
7
+
8
+ import { z } from "zod";
9
+ import type { SpawnLogLine, SpawnRequest } from "../docker/spawn-server/protocol.ts";
10
+
11
+ export { DEFAULT_KANIKO_IMAGE, kanikoArgv, TAR_NAME } from "./kaniko.ts";
12
+
13
+ export interface BuildResult {
14
+ /** The box's exit code (-1 on a broker-side failure carried via `error`). */
15
+ exit: number;
16
+ /**
17
+ * Basename of the broker-owned output dir (`output-<boxId>`) when the
18
+ * request set `output`; otherwise absent. The caller reads its artifacts
19
+ * back from `${artifactsRoot}/${outputKey}` and removes the dir.
20
+ */
21
+ outputKey?: string;
22
+ }
23
+
24
+ export interface BuildOpts {
25
+ /** Called for each line of the box's output as it streams. */
26
+ onLog?(line: SpawnLogLine): void;
27
+ /** Abort to close the socket and stop waiting. */
28
+ signal?: AbortSignal;
29
+ }
30
+
31
+ const encoder = new TextEncoder();
32
+
33
+ /**
34
+ * Spawn one box for `req` and resolve once it exits. Rejects if the
35
+ * broker reports an `error`, or closes without a terminal line.
36
+ */
37
+ export async function buildImage(
38
+ socketPath: string,
39
+ req: SpawnRequest,
40
+ opts: BuildOpts = {},
41
+ ): Promise<BuildResult> {
42
+ const conn = await Deno.connect({ transport: "unix", path: socketPath });
43
+ const onAbort = () => {
44
+ try {
45
+ conn.close();
46
+ } catch {}
47
+ };
48
+ opts.signal?.addEventListener("abort", onAbort);
49
+ // Per-instance, streaming decoder so a multi-byte UTF-8 sequence
50
+ // straddling two reads survives the boundary.
51
+ const decoder = new TextDecoder("utf-8");
52
+ try {
53
+ await conn.write(encoder.encode(JSON.stringify(req) + "\n"));
54
+
55
+ const buf = new Uint8Array(8192);
56
+ let remainder = "";
57
+ let terminal: { exit: number; outputKey?: string; error?: string | null } | undefined;
58
+ while (true) {
59
+ const n = await conn.read(buf);
60
+ if (n === null) break;
61
+ remainder += decoder.decode(buf.slice(0, n), { stream: true });
62
+ let nl: number;
63
+ while ((nl = remainder.indexOf("\n")) !== -1) {
64
+ const raw = remainder.slice(0, nl).trim();
65
+ remainder = remainder.slice(nl + 1);
66
+ if (!raw) continue;
67
+ let parsed: Record<string, unknown>;
68
+ try {
69
+ parsed = z.record(z.string(), z.unknown()).parse(JSON.parse(raw));
70
+ } catch {
71
+ continue; // skip a malformed line; the boundary buffer recovers
72
+ }
73
+ if ("log" in parsed) {
74
+ opts.onLog?.({
75
+ log: String(parsed.log ?? ""),
76
+ stream: parsed.stream === "stderr" ? "stderr" : "stdout",
77
+ });
78
+ } else if ("exit" in parsed) {
79
+ terminal = parsed as { exit: number; outputKey?: string; error?: string | null };
80
+ }
81
+ }
82
+ }
83
+
84
+ if (!terminal) throw new Error("spawn broker closed without a terminal line");
85
+ if (terminal.error) throw new Error(terminal.error);
86
+ return { exit: terminal.exit, outputKey: terminal.outputKey };
87
+ } finally {
88
+ opts.signal?.removeEventListener("abort", onAbort);
89
+ try {
90
+ conn.close();
91
+ } catch {}
92
+ }
93
+ }
@@ -0,0 +1,79 @@
1
+ // Map a `build:` recipe to kaniko executor argv for the spawn broker.
2
+ //
3
+ // The broker is image-agnostic: the caller supplies the builder image +
4
+ // argv, and the broker runs one disposable hardened box. For a build, the
5
+ // image is kaniko (DEFAULT_KANIKO_IMAGE) and the argv is what `kanikoArgv`
6
+ // produces:
7
+ //
8
+ // buildImage(socket, { image: DEFAULT_KANIKO_IMAGE, context, argv: kanikoArgv(recipe) })
9
+ //
10
+ // The box NEVER pushes. It has no network path to a registry (it sits on
11
+ // the egress-only untrusted-net DMZ, with nothing internal reachable), so
12
+ // kaniko always runs `--no-push`. Two shapes:
13
+ //
14
+ // Build-only (no `output`): kaniko builds from the context the broker mounts
15
+ // read-only at /workspace and discards the result — just a compile check.
16
+ //
17
+ // Output (`output`): kaniko additionally writes the built image as an OCI
18
+ // tarball to the broker's RW output mount (`--tar-path`). The caller (in
19
+ // prod, Forge) reads that tarball back and pushes it to the registry itself,
20
+ // over its own trusted network — so the registry host never appears here and
21
+ // the box never touches it.
22
+
23
+ import type { BuildRecipe } from "../../service-definition.ts";
24
+
25
+ // Default builder image: canonical kaniko. It runs as root and never calls
26
+ // unshare(CLONE_NEWUSER), so it builds clean under the broker's envelope:
27
+ // HARDENING plus the file-extraction caps (FOWNER, DAC_OVERRIDE, SETFCAP,
28
+ // FSETID) spec.ts adds so base rootfs with non-root-owned files
29
+ // (node:22-alpine's /home/node) unpack faithfully — the property is
30
+ // kaniko's, not a specific distro's.
31
+ export const DEFAULT_KANIKO_IMAGE = "gcr.io/kaniko-project/executor:latest";
32
+
33
+ // Where the broker bind-mounts the staged context, read-only, in the box.
34
+ // Must match `CONTEXT_MOUNT` in spawn-server/spec.ts — kaniko.test.ts pins
35
+ // the two together (the client can't import spec.ts, which drags in
36
+ // `@local/docker`).
37
+ const CONTEXT_MOUNT = "/workspace";
38
+
39
+ // Where the broker bind-mounts its output dir, read-write, in the box.
40
+ // Must match `OUTPUT_MOUNT` in spawn-server/spec.ts — kaniko.test.ts pins
41
+ // the two together, same as `CONTEXT_MOUNT`.
42
+ const OUTPUT_MOUNT = "/output";
43
+
44
+ /**
45
+ * Basename of the OCI image tarball kaniko writes under the broker's output
46
+ * mount. The cross-layer contract: the box writes `${OUTPUT_MOUNT}/${TAR_NAME}`,
47
+ * and the caller reads the same basename back off the artifacts dir
48
+ * (`${artifactsRoot}/${outputKey}/${TAR_NAME}`) to push it.
49
+ */
50
+ export const TAR_NAME = "image.tar";
51
+
52
+ /**
53
+ * In-box path kaniko writes the OCI image tarball to (`/output/image.tar`).
54
+ * Lives in the broker's RW output mount; the caller reads it back off the
55
+ * artifacts dir. kaniko.test.ts pins this against spec.ts's OUTPUT_MOUNT so
56
+ * the local copy can't drift from the broker.
57
+ */
58
+ export const TAR_PATH = `${OUTPUT_MOUNT}/${TAR_NAME}`;
59
+
60
+ /**
61
+ * kaniko executor flags for `recipe`. The context is the broker's fixed
62
+ * `/workspace` mount; `recipe.context` selects a sub-path within it.
63
+ *
64
+ * Always `--no-push` — the box never reaches a registry. With `output` it
65
+ * also emits `--tar-path=${TAR_PATH}`, so kaniko writes the built image as an
66
+ * OCI tarball to the broker's RW output mount for the caller to read back and
67
+ * push.
68
+ */
69
+ export function kanikoArgv(recipe: BuildRecipe, output?: boolean): string[] {
70
+ const contextDir = recipe.context ? `${CONTEXT_MOUNT}/${recipe.context}` : CONTEXT_MOUNT;
71
+ // Build-only just compile-checks; an output run also tars the result for
72
+ // the caller to push. Either way the box never pushes (`--no-push`).
73
+ const argv = output ? ["--no-push", `--tar-path=${TAR_PATH}`] : ["--no-push"];
74
+ argv.push(`--context=dir://${contextDir}`, `--dockerfile=${recipe.dockerfile ?? "Dockerfile"}`);
75
+ for (const [key, value] of Object.entries(recipe.buildArgs ?? {})) {
76
+ argv.push(`--build-arg=${key}=${value}`);
77
+ }
78
+ return argv;
79
+ }
@@ -0,0 +1,65 @@
1
+ // Consumer-side helper for the worker's stats socket. Apps with the
2
+ // `host.stats` permission mount the socket dir and use this to read a
3
+ // point-in-time `StatsSample` of the host and every container on it.
4
+ //
5
+ // One entry point, because the socket is request/response: connect, ask,
6
+ // parse, close. There's no stream to resume and no cursor to persist — the
7
+ // contrast with logs-client is the whole design. Cadence is the caller's:
8
+ // the worker holds no timer and caches nothing, so a sample you don't store
9
+ // is gone.
10
+
11
+ import { type StatsSample, statsSampleSchema } from "../stats-sample.ts";
12
+
13
+ export type { ContainerSample, DiskSample, HostSample, StatsSample } from "../stats-sample.ts";
14
+ export { OTHER_APP } from "../stats-sample.ts";
15
+
16
+ export interface SampleOpts {
17
+ /** Abort to close the socket and fail the pending sample. */
18
+ signal?: AbortSignal;
19
+ }
20
+
21
+ const encoder = new TextEncoder();
22
+
23
+ /** Take one sample from the stats socket at `path` (the `stats.sock` inside the mounted dir). */
24
+ export async function sampleStats(path: string, opts: SampleOpts = {}): Promise<StatsSample> {
25
+ const conn = await Deno.connect({ transport: "unix", path });
26
+ const onAbort = () => {
27
+ try {
28
+ conn.close();
29
+ } catch {}
30
+ };
31
+ opts.signal?.addEventListener("abort", onAbort);
32
+ // Per-instance, streaming decoder so a multi-byte UTF-8 sequence straddling
33
+ // two `conn.read()` calls survives the boundary.
34
+ const decoder = new TextDecoder("utf-8");
35
+ try {
36
+ await conn.write(encoder.encode(JSON.stringify({ sample: true }) + "\n"));
37
+
38
+ const buf = new Uint8Array(65536);
39
+ let raw = "";
40
+ while (true) {
41
+ const n = await conn.read(buf);
42
+ if (n === null) {
43
+ raw += decoder.decode();
44
+ break;
45
+ }
46
+ raw += decoder.decode(buf.slice(0, n), { stream: true });
47
+ // The server writes one line and closes, but a sample large enough to
48
+ // straddle reads arrives in pieces — stop at the newline rather than
49
+ // waiting for EOF.
50
+ const nl = raw.indexOf("\n");
51
+ if (nl !== -1) {
52
+ raw = raw.slice(0, nl);
53
+ break;
54
+ }
55
+ }
56
+
57
+ if (!raw.trim()) throw new Error("stats socket closed without a sample");
58
+ return statsSampleSchema.parse(JSON.parse(raw));
59
+ } finally {
60
+ opts.signal?.removeEventListener("abort", onAbort);
61
+ try {
62
+ conn.close();
63
+ } catch {}
64
+ }
65
+ }
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * The wire shape of one point-in-time reading of a worker's host and every
5
+ * container on it. Produced by the stats-server and read by the stats-client —
6
+ * the two ends of one unix socket, so this is the single source of truth for
7
+ * both.
8
+ *
9
+ * Mirrors `log-line.ts`, with one deliberate difference: logs are a stream of
10
+ * events, stats are a request/response sample. The worker holds no timer, no
11
+ * history, and no cache — it reads the host when asked and answers.
12
+ *
13
+ * CPU, network, and block IO are CUMULATIVE COUNTERS, not rates. A consumer
14
+ * that wants a rate keeps the previous sample and divides by elapsed time.
15
+ * Counters average correctly over any interval; instantaneous gauges alias.
16
+ * Memory and pids are gauges because they're levels, with no counter to diff.
17
+ *
18
+ * @see https://yourrealm.eu/docs/realm-ts/host/stats/
19
+ */
20
+
21
+ /** One real filesystem mounted on the host. Keyed by mount point. */
22
+ export const diskSampleSchema = z.object({
23
+ path: z.string(),
24
+ total: z.number(),
25
+ available: z.number(),
26
+ });
27
+
28
+ export const hostSampleSchema = z.object({
29
+ kernelVersion: z.string(),
30
+ cores: z.number(),
31
+ /** 1, 5, 15 minute. Read against `cores`. */
32
+ loadAvg: z.tuple([z.number(), z.number(), z.number()]),
33
+ memoryTotal: z.number(),
34
+ memoryAvailable: z.number(),
35
+ swapTotal: z.number(),
36
+ swapUsed: z.number(),
37
+ /** Seconds since boot. */
38
+ uptime: z.number(),
39
+ disks: z.array(diskSampleSchema),
40
+ });
41
+
42
+ export const containerSampleSchema = z.object({
43
+ /** The `realm.app` label; {@link OTHER_APP} for non-Realm containers. */
44
+ app: z.string(),
45
+ service: z.string(),
46
+ /**
47
+ * Docker's own container name, always present. The only human-readable handle
48
+ * an {@link OTHER_APP} container has — `app` and `service` are both label-derived,
49
+ * so they say nothing about a container Realm didn't start.
50
+ */
51
+ name: z.string(),
52
+ containerId: z.string(),
53
+ /** running | exited | ... Counters read 0 unless this is "running". */
54
+ state: z.string(),
55
+ /** healthy | unhealthy | starting, if the container has a healthcheck. */
56
+ health: z.string().optional(),
57
+ startedAt: z.string(),
58
+ restartCount: z.number(),
59
+
60
+ // Cumulative counters.
61
+ cpuUsage: z.number(),
62
+ netRx: z.number(),
63
+ netTx: z.number(),
64
+ blockRead: z.number(),
65
+ blockWrite: z.number(),
66
+
67
+ // Gauges.
68
+ memoryUsage: z.number(),
69
+ memoryLimit: z.number(),
70
+ pids: z.number(),
71
+ });
72
+
73
+ export const statsSampleSchema = z.object({
74
+ /** ISO-8601, when the worker took the reading. */
75
+ ts: z.string(),
76
+ host: hostSampleSchema,
77
+ docker: z.object({
78
+ version: z.string(),
79
+ containersRunning: z.number(),
80
+ containersTotal: z.number(),
81
+ }),
82
+ containers: z.array(containerSampleSchema),
83
+ });
84
+
85
+ /**
86
+ * Reserved app name for containers on the host that Realm doesn't manage
87
+ * (no `realm.app` label). Underscore-prefixed so it can't collide with a real
88
+ * app id, which `nameSchema` constrains to hyphens.
89
+ */
90
+ export const OTHER_APP = "__other";
91
+
92
+ export type DiskSample = z.infer<typeof diskSampleSchema>;
93
+ export type HostSample = z.infer<typeof hostSampleSchema>;
94
+ export type ContainerSample = z.infer<typeof containerSampleSchema>;
95
+ export type StatsSample = z.infer<typeof statsSampleSchema>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `JSON.parse` variant that strips prototype-pollution vectors (and any
3
+ * additional caller-supplied keys) during parsing. The reviver runs
4
+ * before the parsed object is constructed, so a hostile input like
5
+ * `{"__proto__": {"polluted": true}}` cannot result in
6
+ * `({}).polluted === true` even if downstream code does
7
+ * `target[key] = parsed[key]` in a loop.
8
+ */
9
+
10
+ const DANGEROUS_KEYS: ReadonlySet<string> = new Set(["__proto__", "constructor", "prototype"]);
11
+
12
+ /**
13
+ * Parse JSON and drop dangerous keys. `extraStrip` lets the caller
14
+ * remove additional keys (e.g. timestamp aliases the consumer
15
+ * overwrites with its own value) at parse time, so the consumer doesn't
16
+ * need a separate skip-list during its own iteration.
17
+ *
18
+ * Keys the caller wants to *consume* (read the value, then place it
19
+ * elsewhere — e.g. `msg` → `envelope.msg`) must NOT be in `extraStrip`;
20
+ * the consumer reads them directly off the parsed object.
21
+ *
22
+ * @deprecated Prefer `schema.parse(JSON.parse(text))`. A Zod schema drops
23
+ * `__proto__` in every mode (object / record / passthrough), preserves
24
+ * legitimately-named `constructor` / `prototype` fields that this reviver
25
+ * wrongly strips, and validates the shape on top. Plain `JSON.parse` does
26
+ * not pollute `Object.prototype` on its own, so this function only earns
27
+ * its keep where attacker-controlled keys from an *open dictionary* are
28
+ * then assigned via `obj[key] = value` — a real pollution sink. The lone
29
+ * such caller is apps/logger's envelope builder.
30
+ */
31
+ export function hardenedJsonParse(text: string, extraStrip?: ReadonlySet<string>): unknown {
32
+ return JSON.parse(text, (key, value) => {
33
+ if (DANGEROUS_KEYS.has(key)) return undefined;
34
+ if (extraStrip?.has(key)) return undefined;
35
+ return value;
36
+ });
37
+ }