@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,188 @@
1
+ // Test-only plumbing. A `Wire` is two cross-wired in-process byte streams
2
+ // standing in for the plaintext side of a Noise transport, taps every frame
3
+ // that crosses it, and can hold bytes back until the test releases them so
4
+ // backpressure and frame priority are observable.
5
+
6
+ import { type Frame, FrameParser, frameTypeName } from "./frame.ts";
7
+ import { type ByteDuplex, type Limits, Mux, type Stream } from "./mux.ts";
8
+
9
+ interface WireFrame extends Frame {
10
+ /** "ab" is a→b. */
11
+ dir: "ab" | "ba";
12
+ name: string;
13
+ }
14
+
15
+ interface Wire {
16
+ a: ByteDuplex;
17
+ b: ByteDuplex;
18
+ /** Every frame that has actually crossed, both directions, in order. */
19
+ seen: WireFrame[];
20
+ /** Lets `n` more chunks through a→b. Only meaningful when gated. */
21
+ release(n: number): void;
22
+ }
23
+
24
+ /** `gated` holds every a→b chunk until `release` is called. */
25
+ export function wire(opts: { gated?: boolean } = {}): Wire {
26
+ const seen: WireFrame[] = [];
27
+ const permits = new Permits();
28
+ const ab = channel(seen, "ab", opts.gated === true ? permits : undefined);
29
+ const ba = channel(seen, "ba", undefined);
30
+ return {
31
+ a: { readable: ba.readable, writable: ab.writable },
32
+ b: { readable: ab.readable, writable: ba.writable },
33
+ seen,
34
+ release: (n: number) => permits.grant(n),
35
+ };
36
+ }
37
+
38
+ interface MuxPair {
39
+ a: Mux;
40
+ b: Mux;
41
+ seen: WireFrame[];
42
+ release(n: number): void;
43
+ close(): Promise<void>;
44
+ }
45
+
46
+ export function muxPair(limits: Partial<Limits> = {}, opts: { gated?: boolean } = {}): MuxPair {
47
+ const w = wire(opts);
48
+ const a = new Mux(w.a, { side: "initiator", ...limits });
49
+ const b = new Mux(w.b, { side: "responder", ...limits });
50
+ return {
51
+ a,
52
+ b,
53
+ seen: w.seen,
54
+ release: w.release,
55
+ close: async () => {
56
+ // A gated wire holds an in-flight write; abort would wait on it.
57
+ w.release(1_000_000);
58
+ a.close("test over");
59
+ b.close("test over");
60
+ await Promise.all([a.closed, b.closed]);
61
+ },
62
+ };
63
+ }
64
+
65
+ interface Accepted {
66
+ stream: Stream;
67
+ header: Uint8Array;
68
+ }
69
+
70
+ /** Turns `onOpen` into a queue the test pulls from. */
71
+ export function acceptor(mux: Mux): () => Promise<Accepted> {
72
+ const queue: Accepted[] = [];
73
+ const waiting: Array<(accepted: Accepted) => void> = [];
74
+ mux.onOpen((stream, header) => {
75
+ const next = waiting.shift();
76
+ if (next === undefined) queue.push({ stream, header });
77
+ else next({ stream, header });
78
+ });
79
+ return () => {
80
+ const next = queue.shift();
81
+ if (next !== undefined) return Promise.resolve(next);
82
+ return new Promise<Accepted>((resolve) => {
83
+ waiting.push(resolve);
84
+ });
85
+ };
86
+ }
87
+
88
+ export function dataBytes(seen: WireFrame[], dir: "ab" | "ba"): number {
89
+ return seen
90
+ .filter((f) => f.dir === dir && f.name === "DATA")
91
+ .reduce((sum, f) => sum + f.payload.length, 0);
92
+ }
93
+
94
+ export function named(seen: WireFrame[], name: string, dir?: "ab" | "ba"): WireFrame[] {
95
+ return seen.filter((f) => f.name === name && (dir === undefined || f.dir === dir));
96
+ }
97
+
98
+ /**
99
+ * Reads and discards. A raw peer must do this or the tapped channel toward
100
+ * it stalls on backpressure and nothing is ever observed.
101
+ */
102
+ export function sink(readable: ReadableStream<Uint8Array>): Promise<void> {
103
+ return readable.pipeTo(new WritableStream()).catch(() => {});
104
+ }
105
+
106
+ export async function collect(readable: ReadableStream<Uint8Array>): Promise<Uint8Array> {
107
+ const chunks: Uint8Array[] = [];
108
+ let size = 0;
109
+ for await (const chunk of readable) {
110
+ chunks.push(chunk);
111
+ size += chunk.length;
112
+ }
113
+ const out = new Uint8Array(size);
114
+ let offset = 0;
115
+ for (const chunk of chunks) {
116
+ out.set(chunk, offset);
117
+ offset += chunk.length;
118
+ }
119
+ return out;
120
+ }
121
+
122
+ /** Deterministic filler so a round trip can be compared byte for byte. */
123
+ export function pattern(size: number, seed = 1): Uint8Array {
124
+ const out = new Uint8Array(size);
125
+ let x = seed >>> 0;
126
+ for (let i = 0; i < size; i++) {
127
+ x = (x * 1664525 + 1013904223) >>> 0;
128
+ out[i] = x >>> 24;
129
+ }
130
+ return out;
131
+ }
132
+
133
+ /** Lets every queued stream job run. Macrotask ticks, so nothing is left mid-pipe. */
134
+ export async function settle(rounds = 6): Promise<void> {
135
+ for (let i = 0; i < rounds; i++) {
136
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
137
+ }
138
+ }
139
+
140
+ export async function waitFor(predicate: () => boolean, what = "condition"): Promise<void> {
141
+ for (let i = 0; i < 500; i++) {
142
+ if (predicate()) return;
143
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
144
+ }
145
+ throw new Error(`timed out waiting for ${what}`);
146
+ }
147
+
148
+ class Permits {
149
+ #available = 0;
150
+ #resolve: (() => void) | undefined;
151
+
152
+ async take(): Promise<void> {
153
+ while (this.#available <= 0) {
154
+ await new Promise<void>((resolve) => {
155
+ this.#resolve = resolve;
156
+ });
157
+ }
158
+ this.#available--;
159
+ }
160
+
161
+ grant(n: number): void {
162
+ this.#available += n;
163
+ const resolve = this.#resolve;
164
+ this.#resolve = undefined;
165
+ resolve?.();
166
+ }
167
+ }
168
+
169
+ function channel(
170
+ seen: WireFrame[],
171
+ dir: "ab" | "ba",
172
+ permits: Permits | undefined,
173
+ ): TransformStream<Uint8Array, Uint8Array> {
174
+ const parser = new FrameParser();
175
+ return new TransformStream<Uint8Array, Uint8Array>({
176
+ transform: async (chunk, controller) => {
177
+ if (permits !== undefined) await permits.take();
178
+ try {
179
+ for (const frame of parser.push(chunk)) {
180
+ seen.push({ ...frame, dir, name: frameTypeName(frame.type) });
181
+ }
182
+ } catch {
183
+ // Tests that inject garbage break the tap, not the transport.
184
+ }
185
+ controller.enqueue(chunk);
186
+ },
187
+ });
188
+ }
@@ -0,0 +1,27 @@
1
+ import type { WorkerHostPath } from "./logs-socket-path.ts";
2
+
3
+ /**
4
+ * A mount the worker dictates: where it comes from on the host, where it
5
+ * lands in the container by default, and whether the app may write to it.
6
+ *
7
+ * Spread it into a service's `volumes`:
8
+ *
9
+ * ```ts
10
+ * volumes: [config.logs] // default mount point
11
+ * volumes: [{ ...config.logs, containerPath: "/x" }] // somewhere else
12
+ * ```
13
+ *
14
+ * `containerPath` is the app's to change - it only has to agree with whatever
15
+ * the app's own code opens inside the container. `readOnly` is not: it says
16
+ * what the *permission* covers. Connecting to a broker socket needs no write
17
+ * access to the directory holding it, so those mounts resolve `readOnly: true`
18
+ * and overriding that to `false` is a grant violation rather than a
19
+ * preference (`readOnlyMountViolations` in `security.ts`). Without that gate
20
+ * every app granted `host.logs` could unlink the shared socket and bind its
21
+ * own listener in its place.
22
+ */
23
+ export interface WorkerMount {
24
+ readonly hostPath: WorkerHostPath;
25
+ readonly containerPath: string;
26
+ readonly readOnly: boolean;
27
+ }
@@ -0,0 +1,190 @@
1
+ // The Home → Worker wire contract.
2
+ //
3
+ // `WorkerState` is the whole payload Home ships to a worker (read from
4
+ // `state.json`, encrypted at rest, plaintext over WireGuard) and hands to
5
+ // runtime-specific renderers - today `convertToDockerCompose` for `apps` and
6
+ // `buildCaddyConfig` for `ingress`, both injected by `platform/worker`.
7
+ //
8
+ // This is its OWN schema, deliberately ignorant of the resolve model
9
+ // (`homeAppSchema` in `resolve.ts`), which is Home's internal working state and
10
+ // mirrors `getDesiredState` (refs, `ui`, `trustedHeaders`, per-service
11
+ // `router`/`serviceKind`/`output`). The wire carries only what a keyless,
12
+ // app-isolated worker needs to run the workloads and route traffic to them.
13
+ //
14
+ // The wire is post-resolution, and `build-wire.ts` proves it: it walks every
15
+ // `{__realmRef:...}` marker out (`resolveRefs`) and asserts none survive
16
+ // (`assertNoRefMarkers`). So every field here is a plain literal - no
17
+ // `stringOrRef` unions, and a marker that escaped resolution fails this parse.
18
+
19
+ import { z } from "zod";
20
+ import { hostKey, nameSchema } from "../identifiers.ts";
21
+ import { ingressInputSchema } from "./ingress.ts";
22
+ import { SPECULATIVE_SILO } from "../speculative-silo.ts";
23
+
24
+ /**
25
+ * Per-service depends_on edge, runtime-neutral. Home computes these during
26
+ * stitching from the peer permissions registry; the renderer emits the
27
+ * runtime-specific form (compose `service_healthy`/`service_started`).
28
+ */
29
+ export const dependsOnEdgeSchema = z.object({
30
+ providerAppId: z.string(),
31
+ providerServiceName: z.string(),
32
+ condition: z.enum(["started", "healthy"]),
33
+ });
34
+ export type DependsOnEdge = z.infer<typeof dependsOnEdgeSchema>;
35
+
36
+ // Whole-second duration. The renderer formats `${n}s` when emitting
37
+ // compose. Defaulted upstream in the resolve model, so on the wire it's
38
+ // always a concrete number.
39
+ const duration = z.number().int().nonnegative();
40
+ const containerPort = z.number().int().min(1).max(65535);
41
+
42
+ const healthcheckSchema = z.object({
43
+ test: z.array(z.string()),
44
+ interval: duration,
45
+ timeout: duration,
46
+ retries: z.number().int().nonnegative(),
47
+ startPeriod: duration,
48
+ });
49
+
50
+ const hostPortSchema = z.object({
51
+ containerPort,
52
+ hostPort: z.number().int().min(1).max(65535),
53
+ protocol: z.enum(["tcp", "udp"]).optional(),
54
+ });
55
+
56
+ // Resolved on the wire: the `pg-dump`/`valkey-save`/`mysql-dump` URLs are
57
+ // `stringOrRef` in the resolve model; the walker has baked them to strings.
58
+ const backupStrategySchema = z.discriminatedUnion("kind", [
59
+ z.object({ kind: z.literal("file-copy") }),
60
+ z.object({ kind: z.literal("sqlite"), file: z.string() }),
61
+ z.object({ kind: z.literal("pg-dump"), url: z.string(), version: z.number().int().positive() }),
62
+ z.object({ kind: z.literal("valkey-save"), url: z.string() }),
63
+ z.object({
64
+ kind: z.literal("mysql-dump"),
65
+ url: z.string(),
66
+ version: z.number().int().positive(),
67
+ }),
68
+ ]);
69
+
70
+ const volumeSchema = z.object({
71
+ // Refused by name: the wire is where "this was fully resolved" stops being a
72
+ // promise and becomes a check. A speculative eval fabricates a Silo so an app
73
+ // can be previewed before the operator picks one (`speculative-silo.ts`), and
74
+ // that placeholder is otherwise an ordinary absolute path - shipping it binds
75
+ // an empty directory in place of the operator's library, silently. `evalApps`
76
+ // refuses that combination up front; this is the half that cannot be bypassed.
77
+ hostPath: z.string().refine((p) => p !== SPECULATIVE_SILO.hostPath, {
78
+ message: "the speculative Silo placeholder must never reach the wire",
79
+ }),
80
+ containerPath: z.string(),
81
+ // The Silo group that owns this directory. The renderer turns it into
82
+ // `group_add` on every service that mounts the volume, so directory
83
+ // preparation and group membership are driven by the same field and can
84
+ // never disagree.
85
+ gid: z.number().int().positive().optional(),
86
+ readOnly: z.boolean().optional(),
87
+ backup: z.union([backupStrategySchema, z.literal(false)]).optional(),
88
+ });
89
+
90
+ /**
91
+ * One service as the renderer sees it - renderer fields only. The resolve
92
+ * model carries extra Home-side fields (`router`, `serviceKind`, `output`,
93
+ * `build`); none belong on the wire. This is a plain (non-strict) object, so
94
+ * parsing against it drops any such field automatically - that parse is what
95
+ * keeps Home-only data off the wire. (`output` is the one field a producer
96
+ * still removes by hand, earlier, for an unrelated reason - see build-wire.)
97
+ *
98
+ * No `build`: recompute guarantees every service is image-pinned before the
99
+ * wire is built, so `build` has already become an `image`. The wire is always
100
+ * image-based.
101
+ */
102
+ export const wireServiceSchema = z.object({
103
+ image: z.string().min(1),
104
+ hostPorts: z.array(hostPortSchema).optional(),
105
+ env: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
106
+ // containerPath -> 64-char hex digest the renderer resolves to bytes. The two
107
+ // sources hash different things: artifact refs collapse to their bare content
108
+ // sha, while inline content is keyed by *identity* — sha256 of
109
+ // `appId:serviceKey:fileKey` (plaintext rides in `WorkerApp.inlineFiles`). An
110
+ // inline file therefore keeps the same digest and worker path across content
111
+ // changes, so `--watch` consumers reload without recreating the container.
112
+ files: z.record(z.string().startsWith("/"), z.string().regex(/^[a-f0-9]{64}$/)).optional(),
113
+ command: z.array(z.string()).optional(),
114
+ volumes: z.array(volumeSchema).optional(),
115
+ gpu: z.boolean().optional(),
116
+ iptables: z.boolean().optional(),
117
+ joinNetworks: z.array(z.string()).optional(),
118
+ healthcheck: healthcheckSchema.optional(),
119
+ memory: z.string().optional(),
120
+ cpu: z.number().positive().max(128).optional(),
121
+ // Defaulted to 1 upstream, so always present on the wire.
122
+ replicas: z.number().int().min(1).max(100),
123
+ labels: z.array(z.string()).optional(),
124
+ shmSizeMb: z.number().int().positive().optional(),
125
+ });
126
+ export type WireService = z.infer<typeof wireServiceSchema>;
127
+
128
+ export const workerAppSchema = z.object({
129
+ appId: z.string(),
130
+ // Flattened: in the resolve model these live under `.state` (matching
131
+ // `getDesiredState`'s shape), but on the wire only these two of that
132
+ // object's fields survive, so they're hoisted onto the app directly.
133
+ services: z.record(nameSchema, wireServiceSchema),
134
+ /** `true` makes the per-app network `internal: true` (no egress). */
135
+ egress: z.literal(true).optional(),
136
+ /** Worker-owned external nets this app's services join (e.g. `management-net`). */
137
+ externalNetworks: z.array(z.string()),
138
+ /**
139
+ * Service-name (unprefixed) -> producer edges. The renderer namespaces
140
+ * `providerServiceName` with `providerAppId` to form the final compose key.
141
+ */
142
+ dependsOn: z.record(z.string(), z.array(dependsOnEdgeSchema)),
143
+ /** Plaintext for inline `files` entries, keyed by their identity digest. */
144
+ inlineFiles: z.record(z.string(), z.string()),
145
+ /**
146
+ * Absolute host paths exempted from the absolute-mount gate and
147
+ * bound verbatim by the renderer (peer backup volumes, the logs socket).
148
+ */
149
+ allowedAbsoluteHostPaths: z.array(z.string()),
150
+ /**
151
+ * Host path the worker binds this app's `home-proxy.sock` at. Present only
152
+ * when the app holds a `home.apiAccess` grant.
153
+ */
154
+ homeProxySocketPath: z.string().optional(),
155
+ /**
156
+ * Host path the worker binds this app's spawn broker socket at. Present
157
+ * only when the app holds a `host.spawn` grant.
158
+ */
159
+ spawnSocketPath: z.string().optional(),
160
+ /**
161
+ * This app's registry credentials, keyed by registry host. The worker
162
+ * renders a per-app docker `config.json` from these so private images pull.
163
+ */
164
+ registryAuth: z
165
+ .record(hostKey, z.object({ username: z.string(), password: z.string() }))
166
+ .optional(),
167
+ });
168
+ export type WorkerApp = z.infer<typeof workerAppSchema>;
169
+
170
+ export const workerStateSchema = z.object({
171
+ apps: z.array(workerAppSchema),
172
+ /** SHAs of every artifact the worker keeps in `files/` after a reconcile. */
173
+ artifactHashes: z.array(z.string()),
174
+ /** Docker networks the worker container itself joins this reconcile. */
175
+ workerNetworks: z.array(z.string()),
176
+ /**
177
+ * Where traffic goes on this box, as `apps` says what runs on it. Runtime-
178
+ * neutral: the Docker worker renders it with `buildCaddyConfig`
179
+ * (`runtime/docker/ingress`) exactly as it renders `apps` with
180
+ * `convertToDockerCompose`.
181
+ *
182
+ * Required, not optional. Every producer can state an honest value - an
183
+ * unconfigured box gets the empty-identity placeholder, which is a real
184
+ * answer ("no domain yet") rather than a missing one - and a box with no
185
+ * schedulable app still needs a front door, so absence must never be a legal
186
+ * way to say "no ingress here".
187
+ */
188
+ ingress: ingressInputSchema,
189
+ });
190
+ export type WorkerState = z.infer<typeof workerStateSchema>;
package/src/xml.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { Deferred, Ref } from "./ref.ts";
2
+ import { serializeRefs } from "./serialize.ts";
3
+
4
+ /**
5
+ * Element-only XML values. Attributes aren't expressible — every node is
6
+ * either a leaf with text content or a parent with element children.
7
+ * Covers config files like Sonarr/Radarr `config.xml` where each setting
8
+ * is a `<Key>value</Key>` line.
9
+ */
10
+ type XmlValue = string | number | Ref<string> | { [key: string]: XmlValue };
11
+
12
+ function escapeXmlText(s: string): string {
13
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
14
+ }
15
+
16
+ // Emit a resolved (ref-free) tree. String leaves — including values that
17
+ // were refs before resolution — go through escapeXmlText, so a resolved
18
+ // secret containing `&`/`<`/`>` can't break the document.
19
+ function emit(parts: string[], indent: string, key: string, value: unknown): void {
20
+ parts.push(`${indent}<${key}>`);
21
+ if (typeof value === "string") {
22
+ parts.push(escapeXmlText(value));
23
+ } else if (typeof value === "number") {
24
+ parts.push(String(value));
25
+ } else {
26
+ parts.push("\n");
27
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
28
+ emit(parts, indent + " ", k, v);
29
+ }
30
+ parts.push(indent);
31
+ }
32
+ parts.push(`</${key}>\n`);
33
+ }
34
+
35
+ /**
36
+ * Serialize a tree of objects to an element-only XML string. Refs in leaf
37
+ * positions are resolved at deploy time (via `apply()`) before the text is
38
+ * XML-escaped, so a resolved secret is escaped rather than spliced raw. A
39
+ * ref-free tree collapses to a plain string at build time.
40
+ */
41
+ export function toXml(value: Record<string, XmlValue>): string | Deferred<string> {
42
+ return serializeRefs(value, (resolved) => {
43
+ const parts: string[] = [];
44
+ for (const [k, v] of Object.entries(resolved as Record<string, unknown>)) {
45
+ emit(parts, "", k, v);
46
+ }
47
+ return parts.join("");
48
+ });
49
+ }
package/src/yaml.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { stringify } from "yaml";
2
+ import type { Deferred } from "./ref.ts";
3
+ import { serializeRefs } from "./serialize.ts";
4
+
5
+ /**
6
+ * Serialize a value to a YAML string. Refs anywhere in the tree are
7
+ * resolved at deploy time (via `apply()`) before serialization, so YAML's
8
+ * value-dependent quoting sees the real string. A ref-free value collapses
9
+ * to a plain string at build time.
10
+ *
11
+ * files: { "/etc/app/config.yml": toYaml({ database: db.url }) }
12
+ */
13
+ export function toYaml<T>(value: T): string | Deferred<string> {
14
+ return serializeRefs(value, (resolved) => stringify(resolved));
15
+ }