@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.
- package/LICENSE-EXCEPTION +26 -0
- package/README.md +34 -0
- package/dist/chunk-3R6KEFOI.js +22 -0
- package/dist/chunk-67BDAMYX.js +376 -0
- package/dist/chunk-AITGS27R.js +139 -0
- package/dist/chunk-CMAEHQ4F.js +6 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-O3UIK34B.js +1430 -0
- package/dist/chunk-VHTUSZUX.js +176 -0
- package/dist/cli.js +9006 -0
- package/dist/docker.js +54 -0
- package/dist/index.js +1053 -0
- package/dist/ingress.js +436 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/preact.js +22 -0
- package/dist/service-url.js +11 -0
- package/dist/services.js +213 -0
- package/dist/test.js +5069 -0
- package/dist/ui.js +254 -0
- package/dist/worker-protocol.js +89 -0
- package/package.json +112 -0
- package/src/app-icon.ts +74 -0
- package/src/apply.ts +52 -0
- package/src/bundle-ui.ts +211 -0
- package/src/bundle.ts +546 -0
- package/src/cli/build-dev-landing.ts +84 -0
- package/src/cli/build-install-panel.ts +90 -0
- package/src/cli/build-runtime.ts +133 -0
- package/src/cli/build.ts +63 -0
- package/src/cli/dev.ts +107 -0
- package/src/cli/down.ts +60 -0
- package/src/cli/main.ts +56 -0
- package/src/cli/test.ts +61 -0
- package/src/cli/validate.ts +37 -0
- package/src/compile.ts +100 -0
- package/src/create-app.ts +982 -0
- package/src/deep-merge.ts +33 -0
- package/src/dev/fixture.ts +48 -0
- package/src/dev/install-handler.ts +383 -0
- package/src/dev/server.ts +968 -0
- package/src/dev/stack.ts +108 -0
- package/src/dev/state.ts +159 -0
- package/src/docker.ts +96 -0
- package/src/errors.ts +61 -0
- package/src/factory.ts +481 -0
- package/src/garage.ts +56 -0
- package/src/generator/hash.ts +6 -0
- package/src/generator/resolve-refs.ts +207 -0
- package/src/grants-builder.ts +100 -0
- package/src/grants.ts +260 -0
- package/src/home-api.ts +172 -0
- package/src/home-cookie.ts +10 -0
- package/src/home-registry.ts +120 -0
- package/src/home.ts +187 -0
- package/src/host-registry.ts +143 -0
- package/src/http-header.ts +20 -0
- package/src/identifiers.ts +49 -0
- package/src/index.ts +66 -0
- package/src/info.ts +64 -0
- package/src/ini.ts +50 -0
- package/src/json.ts +14 -0
- package/src/jsx-runtime/index.ts +5 -0
- package/src/keys.ts +29 -0
- package/src/mysql.ts +56 -0
- package/src/pack-build.ts +108 -0
- package/src/pack.ts +115 -0
- package/src/peer-registry.ts +121 -0
- package/src/peer-world.ts +117 -0
- package/src/permission-meta.ts +116 -0
- package/src/permissions.ts +108 -0
- package/src/pkg-root.ts +34 -0
- package/src/postgres.ts +79 -0
- package/src/preact/index.ts +27 -0
- package/src/redis.ts +43 -0
- package/src/ref.ts +271 -0
- package/src/remote-registry.ts +33 -0
- package/src/required-host-kinds.ts +110 -0
- package/src/resolve-context.ts +111 -0
- package/src/resolve.ts +471 -0
- package/src/runtime/docker/compose/compose-schema.ts +118 -0
- package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
- package/src/runtime/docker/compose/default-hardening.ts +21 -0
- package/src/runtime/docker/compose/effects.ts +107 -0
- package/src/runtime/docker/compose/index.ts +17 -0
- package/src/runtime/docker/compose/labels.ts +39 -0
- package/src/runtime/docker/compose/types.ts +78 -0
- package/src/runtime/docker/home-proxy-server/index.ts +177 -0
- package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
- package/src/runtime/docker/ingress/build-config.ts +819 -0
- package/src/runtime/docker/ingress/index.ts +16 -0
- package/src/runtime/docker/ingress/types.ts +182 -0
- package/src/runtime/docker/log-frames.ts +121 -0
- package/src/runtime/docker/logs-server/index.ts +366 -0
- package/src/runtime/docker/spawn-server/context-key.ts +82 -0
- package/src/runtime/docker/spawn-server/index.ts +492 -0
- package/src/runtime/docker/spawn-server/protocol.ts +70 -0
- package/src/runtime/docker/spawn-server/spec.ts +122 -0
- package/src/runtime/docker/stats-server/disks.ts +161 -0
- package/src/runtime/docker/stats-server/index.ts +322 -0
- package/src/runtime/home-client/index.ts +71 -0
- package/src/runtime/log-line.ts +24 -0
- package/src/runtime/logs-client/index.ts +175 -0
- package/src/runtime/spawn-client/index.ts +93 -0
- package/src/runtime/spawn-client/kaniko.ts +79 -0
- package/src/runtime/stats-client/index.ts +65 -0
- package/src/runtime/stats-sample.ts +95 -0
- package/src/safe-json.ts +37 -0
- package/src/sandbox.ts +1209 -0
- package/src/schema/build-apps.ts +38 -0
- package/src/schema/home.ts +217 -0
- package/src/schema/host.ts +792 -0
- package/src/schema/inputs.ts +26 -0
- package/src/schema/nodes.ts +75 -0
- package/src/schema/peer.ts +400 -0
- package/src/schema/public-domain.ts +116 -0
- package/src/schema/remote.ts +74 -0
- package/src/security.ts +594 -0
- package/src/serialize.ts +21 -0
- package/src/service-definition.ts +284 -0
- package/src/service-url.ts +64 -0
- package/src/service.ts +39 -0
- package/src/services/index.ts +5 -0
- package/src/shared.ts +65 -0
- package/src/speculative-silo.ts +96 -0
- package/src/stable-secret.ts +14 -0
- package/src/surfaces.ts +264 -0
- package/src/tailwind-bin.ts +56 -0
- package/src/test/boot.ts +192 -0
- package/src/test/compile.ts +501 -0
- package/src/test/describe.ts +117 -0
- package/src/test/index.ts +11 -0
- package/src/test/ingress-fixture.ts +37 -0
- package/src/test/types.ts +318 -0
- package/src/trusted-headers.ts +79 -0
- package/src/types.ts +126 -0
- package/src/ui/components/aside.tsx +96 -0
- package/src/ui/components/badge.tsx +42 -0
- package/src/ui/components/button.tsx +59 -0
- package/src/ui/components/card.tsx +106 -0
- package/src/ui/components/checkbox.tsx +37 -0
- package/src/ui/components/code-block.tsx +29 -0
- package/src/ui/components/combobox.tsx +119 -0
- package/src/ui/components/config-field.tsx +284 -0
- package/src/ui/components/dialog.tsx +137 -0
- package/src/ui/components/domain-editor.tsx +317 -0
- package/src/ui/components/domain-list.ts +205 -0
- package/src/ui/components/dropdown-menu.tsx +73 -0
- package/src/ui/components/empty-state.tsx +51 -0
- package/src/ui/components/highlight.ts +59 -0
- package/src/ui/components/icons.tsx +101 -0
- package/src/ui/components/image-posture.tsx +85 -0
- package/src/ui/components/input.tsx +28 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/line-chart.tsx +113 -0
- package/src/ui/components/page.tsx +39 -0
- package/src/ui/components/permission-row.tsx +262 -0
- package/src/ui/components/permission-section.tsx +51 -0
- package/src/ui/components/pie-chart.tsx +245 -0
- package/src/ui/components/schema-block.tsx +60 -0
- package/src/ui/components/separator.tsx +42 -0
- package/src/ui/components/sheet.tsx +130 -0
- package/src/ui/components/sidebar.tsx +650 -0
- package/src/ui/components/silo-picker.tsx +510 -0
- package/src/ui/components/skeleton.tsx +13 -0
- package/src/ui/components/sonner.tsx +33 -0
- package/src/ui/components/switch.tsx +58 -0
- package/src/ui/components/table-view.ts +219 -0
- package/src/ui/components/table.tsx +506 -0
- package/src/ui/components/tabs.tsx +59 -0
- package/src/ui/components/timeago.tsx +84 -0
- package/src/ui/components/tooltip.tsx +56 -0
- package/src/ui/components.json +21 -0
- package/src/ui/dev/dev-shell.tsx +145 -0
- package/src/ui/home/home-grid.tsx +177 -0
- package/src/ui/hooks/use-mobile.ts +19 -0
- package/src/ui/index.ts +189 -0
- package/src/ui/install/adapter.ts +193 -0
- package/src/ui/install/deploy-targets-picker.tsx +185 -0
- package/src/ui/install/install-app-panel.tsx +912 -0
- package/src/ui/install/install-error.tsx +62 -0
- package/src/ui/install/perms-inactive.ts +50 -0
- package/src/ui/install/required-fields.ts +37 -0
- package/src/ui/install/tabs-form.tsx +557 -0
- package/src/ui/tokens.css +279 -0
- package/src/ui/utils.ts +78 -0
- package/src/ui-error.ts +139 -0
- package/src/ui-runtime/dev-landing-entry.tsx +258 -0
- package/src/ui-runtime/index.ts +83 -0
- package/src/ui-runtime/install-entry.tsx +119 -0
- package/src/ui-runtime/mount.ts +367 -0
- package/src/ui-runtime/runtime.css +56 -0
- package/src/ui-surface.ts +451 -0
- package/src/valkey.ts +43 -0
- package/src/worker/extract-tar.ts +125 -0
- package/src/worker/identity.ts +183 -0
- package/src/worker/index.ts +16 -0
- package/src/worker/public-ip.ts +92 -0
- package/src/worker/queue.ts +54 -0
- package/src/worker/reconcile.ts +127 -0
- package/src/worker/start-worker.ts +227 -0
- package/src/worker/ui-proxy.ts +335 -0
- package/src/worker/uplink.ts +523 -0
- package/src/worker/validate.ts +34 -0
- package/src/worker/verify-relay.ts +102 -0
- package/src/worker-protocol/app-dirs.ts +94 -0
- package/src/worker-protocol/home-proxy-paths.ts +76 -0
- package/src/worker-protocol/index.ts +55 -0
- package/src/worker-protocol/ingress.ts +166 -0
- package/src/worker-protocol/logs-socket-path.ts +50 -0
- package/src/worker-protocol/networks.ts +39 -0
- package/src/worker-protocol/node-version.ts +70 -0
- package/src/worker-protocol/silo-paths.ts +114 -0
- package/src/worker-protocol/spawn-paths.ts +165 -0
- package/src/worker-protocol/stats-socket-path.ts +40 -0
- package/src/worker-protocol/update-files.ts +77 -0
- package/src/worker-protocol/uplink/constants.ts +86 -0
- package/src/worker-protocol/uplink/errors.ts +61 -0
- package/src/worker-protocol/uplink/frame.ts +114 -0
- package/src/worker-protocol/uplink/identity.ts +40 -0
- package/src/worker-protocol/uplink/index.ts +63 -0
- package/src/worker-protocol/uplink/methods.ts +462 -0
- package/src/worker-protocol/uplink/mux.ts +646 -0
- package/src/worker-protocol/uplink/protocol.ts +11 -0
- package/src/worker-protocol/uplink/rpc.ts +309 -0
- package/src/worker-protocol/uplink/session.ts +540 -0
- package/src/worker-protocol/uplink/test-support.ts +188 -0
- package/src/worker-protocol/worker-mount.ts +27 -0
- package/src/worker-protocol/worker-state.ts +190 -0
- package/src/xml.ts +49 -0
- 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>;
|
package/src/safe-json.ts
ADDED
|
@@ -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
|
+
}
|