@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,94 @@
1
+ // Wire constants for an app's *own* directories — where a relative `volumes`
2
+ // hostPath lands, and the group that makes it writable.
3
+ //
4
+ // The Silo answer to "several apps share one tree" is a group per Silo
5
+ // (silo-paths.ts). This is the smaller sibling question: one app, one
6
+ // directory, and an image that doesn't run as root. Docker creates a missing
7
+ // bind source as root-owned 0755, so a container running as a baked-in uid gets
8
+ // EACCES on its own config directory and dies at boot, and the image can't fix
9
+ // that from inside: it has no root phase to chown from.
10
+ //
11
+ // So the worker prepares the directory the same way it prepares a Silo's —
12
+ // group ownership, setgid, group rwx — and the renderer puts the container in
13
+ // that group. Nothing reaches the author: a relative volume is an app's private
14
+ // storage with no permission and no operator decision, and it stays that way.
15
+ //
16
+ // Pure constants and string math, no runtime imports, so this stays QuickJS-safe
17
+ // like its siblings.
18
+
19
+ /**
20
+ * Group owning every directory Realm creates for an app's own relative volumes.
21
+ *
22
+ * One number for every app rather than one per app, because the isolation is
23
+ * already carried by the *path*: bind sources are scoped per app by
24
+ * `scopeBindSource`, an app only ever mounts its own, and the enclosing
25
+ * `apps/<id>/` tree is root-owned 0700 so nothing on the host box can walk in.
26
+ * A per-app gid would buy nothing over that and cost an allocator, a wire
27
+ * field, and a row to keep in sync.
28
+ *
29
+ * Sits directly below the Silo range (`home_state.next_gid` counts up from
30
+ * 1_000_000), so the two allocations can never meet.
31
+ */
32
+ export const REALM_APP_DIR_GID = 999_999;
33
+
34
+ /**
35
+ * Whether this volume mounts a directory Realm prepares for the app itself -
36
+ * the one question both halves of the mechanism must answer identically. The
37
+ * worker prepares exactly what this returns true for; the renderer grants
38
+ * membership for exactly the same set. Split into two implementations they
39
+ * drift, and each direction is its own bug: a prepared directory nothing
40
+ * mounts, or a mount whose container was never put in the group.
41
+ *
42
+ * Four things are *not* an app's own directory:
43
+ *
44
+ * - a volume carrying a `gid` - that's a Silo's, prepared by prepareSiloDirs
45
+ * against a path that may be the operator's own;
46
+ * - an exempt path (`allowedAbsoluteHostPaths`) - a peer's directory reached
47
+ * through a grant. It's relative and it looks just like private storage,
48
+ * which is exactly why this check has to live in one place: preparing it
49
+ * would build a hollow mirror of the peer's layout under the wrong app;
50
+ * - an absolute path - a Silo's or the operator's;
51
+ * - the app's own root (`./` and friends), which the worker keeps at 0700
52
+ * for the compose file and secrets it writes there. Claiming it would be
53
+ * undone later in the same reconcile.
54
+ */
55
+ export function isPreparedAppDir(
56
+ appId: string,
57
+ volume: { readonly hostPath: string; readonly gid?: number | undefined },
58
+ exemptHostPaths: ReadonlySet<string>,
59
+ ): boolean {
60
+ if (volume.gid !== undefined) return false;
61
+ if (exemptHostPaths.has(volume.hostPath)) return false;
62
+ if (!volume.hostPath.startsWith("./")) return false;
63
+ return scopeBindSource(volume.hostPath, appId) !== `./apps/${appId}`;
64
+ }
65
+
66
+ /**
67
+ * Resolve a bind source against the app's own subtree.
68
+ *
69
+ * Relative paths (`./data`) are private storage and land under
70
+ * `apps/<appId>/`; absolute ones belong to a Silo or the operator and pass
71
+ * through untouched. Shared by the renderer (which writes the compose source)
72
+ * and the worker (which prepares the directory), because a preparation that
73
+ * lands on a different inode than the mount is worse than no preparation at
74
+ * all - it looks done and shares nothing.
75
+ *
76
+ * Home's input validator rejects `..` segments before the wire is built, but
77
+ * this re-checks: a buggy Home commit must not let a path escape the per-app
78
+ * subtree.
79
+ */
80
+ export function scopeBindSource(source: string, appId: string): string {
81
+ if (!source.startsWith("./")) return source;
82
+ const scoped = `./apps/${appId}/${source.slice(2)}`;
83
+ const segments = scoped.split("/");
84
+ const resolved: string[] = [];
85
+ for (const seg of segments) {
86
+ if (seg === "..") resolved.pop();
87
+ else if (seg !== "." && seg !== "") resolved.push(seg);
88
+ }
89
+ const normalized = "./" + resolved.join("/");
90
+ if (!normalized.startsWith(`./apps/${appId}/`) && normalized !== `./apps/${appId}`) {
91
+ throw new Error(`bind source escapes app directory: ${source}`);
92
+ }
93
+ return normalized;
94
+ }
@@ -0,0 +1,76 @@
1
+ // Wire constants for the worker's app→Home proxy brokers (per-app
2
+ // `home-proxy/<appId>/home-proxy.sock`). An app container mounts its own
3
+ // per-app DIRECTORY and calls `callHome` over the socket inside it; the worker
4
+ // turns that into one `apiAccess` call on its uplink, which Home dispatches to
5
+ // `/api-access/*`. An app container holds no uplink — only the worker does —
6
+ // so this socket is the only path an app has to reach the control plane.
7
+ //
8
+ // One socket per granted app, not a single shared one: the worker attaches
9
+ // `X-Realm-App-Id` based on *which* socket a request arrived on, so the app
10
+ // never asserts — or can spoof — its own id. Each app's socket lives ALONE in
11
+ // its own per-app directory: the app mounts the directory (a file bind-mount of
12
+ // a unix socket doesn't wire across containers on macOS Docker), and because
13
+ // the socket is alone in it, mounting the dir never exposes a sibling's socket.
14
+ //
15
+ // Pure constants + string-join builders, no runtime imports (the
16
+ // `WorkerHostPath` import is type-only and erases), so the QuickJS-safe
17
+ // `callHome` helper can pull the container path without dragging Deno deps.
18
+ // Mirrors `spawn-paths.ts`.
19
+
20
+ import type { WorkerHostPath } from "./logs-socket-path.ts";
21
+ import type { WorkerMount } from "./worker-mount.ts";
22
+
23
+ /**
24
+ * Host directory the worker keeps every app's per-app proxy socket dir under.
25
+ * Lives beneath `/opt/realm/worker/` — the one directory the worker container
26
+ * already shares with the host (mirrors `REALM_SPAWN_DIR`). The worker mkdirs
27
+ * this once; each per-app subdir under it lives as long as its app is on the
28
+ * wire, because a running container holds it by inode.
29
+ */
30
+ export const REALM_HOME_PROXY_DIR = "/opt/realm/worker/home-proxy";
31
+
32
+ /**
33
+ * Host directory holding one app's proxy socket
34
+ * (`/opt/realm/worker/home-proxy/<appId>`). The socket sits alone inside it, so
35
+ * this is the path the app bind-mounts. `home.apiAccess`'s resolver builds its
36
+ * `{ socket }` from this per-app dir at eval time, and `collectAllowed` exempts
37
+ * it from the absolute-mount gate.
38
+ */
39
+ export function homeProxyAppDir(appId: string): WorkerHostPath {
40
+ return `${REALM_HOME_PROXY_DIR}/${appId}` as WorkerHostPath;
41
+ }
42
+
43
+ /**
44
+ * Host path of one app's proxy socket, inside {@link homeProxyAppDir}. The
45
+ * worker binds this in `reconcileHomeProxySockets` and carries it on the wire;
46
+ * the app connects to it inside the mounted per-app dir.
47
+ */
48
+ export function homeProxySocketPath(appId: string): string {
49
+ return `${homeProxyAppDir(appId)}/home-proxy.sock`;
50
+ }
51
+
52
+ /**
53
+ * Mount target inside the app container — the DIRECTORY the grant bind-mounts
54
+ * the per-app proxy dir at. Under `/run` so it doesn't collide with an app's
55
+ * own filesystem.
56
+ */
57
+ const REALM_HOME_PROXY_CONTAINER_DIR = "/run/realm/home-proxy";
58
+
59
+ /**
60
+ * In-container path `callHome` connects to by default — the socket inside the
61
+ * mounted `REALM_HOME_PROXY_CONTAINER_DIR`.
62
+ */
63
+ export const REALM_HOME_PROXY_CONTAINER_PATH = `${REALM_HOME_PROXY_CONTAINER_DIR}/home-proxy.sock`;
64
+
65
+ /**
66
+ * The mount `home.apiAccess` resolves `{ socket }` to for one app. Read-only:
67
+ * `callHome` connects to the socket inside this directory and never writes to
68
+ * the directory itself.
69
+ */
70
+ export function homeProxyMount(appId: string): WorkerMount {
71
+ return {
72
+ hostPath: homeProxyAppDir(appId),
73
+ containerPath: REALM_HOME_PROXY_CONTAINER_DIR,
74
+ readOnly: true,
75
+ };
76
+ }
@@ -0,0 +1,55 @@
1
+ // @yourrealm/sdk/worker-protocol — Home ↔ Worker wire contracts.
2
+ //
3
+ // Schemas + types both live here so Home and Worker validate against the
4
+ // same source. Adding a new wire surface (log-tap, etc.) means a new
5
+ // file next to worker-state.ts with a Zod schema + an inferred type.
6
+
7
+ export {
8
+ dependsOnEdgeSchema,
9
+ wireServiceSchema,
10
+ workerAppSchema,
11
+ workerStateSchema,
12
+ } from "./worker-state.ts";
13
+ export type { DependsOnEdge, WireService, WorkerApp, WorkerState } from "./worker-state.ts";
14
+ export { ingressInputSchema, PLATFORM_INGRESS_PORTS } from "./ingress.ts";
15
+ export type { IngressInput } from "./ingress.ts";
16
+ export { nodeVersionSchema, readNodeVersion, shortNodeVersion } from "./node-version.ts";
17
+ export type { NodeVersion } from "./node-version.ts";
18
+ export { readUpdateFiles } from "./update-files.ts";
19
+ export type { UpdateFiles } from "./update-files.ts";
20
+ export { REALM_LOGS_DIR, REALM_LOGS_MOUNT, REALM_LOGS_SOCKET } from "./logs-socket-path.ts";
21
+ export { REALM_STATS_DIR, REALM_STATS_MOUNT, REALM_STATS_SOCKET } from "./stats-socket-path.ts";
22
+ export type { WorkerMount } from "./worker-mount.ts";
23
+ export {
24
+ MANAGEMENT_NET,
25
+ SILO_NET_PREFIX,
26
+ UNTRUSTED_NET,
27
+ isSiloNetwork,
28
+ siloNetwork,
29
+ } from "./networks.ts";
30
+ export { REALM_APP_DIR_GID, isPreparedAppDir, scopeBindSource } from "./app-dirs.ts";
31
+ export {
32
+ REALM_SILO_DIR,
33
+ isManagedSiloPath,
34
+ managedSiloPath,
35
+ siloReclaimCommand,
36
+ siloSetupCommand,
37
+ siloWidenCommand,
38
+ } from "./silo-paths.ts";
39
+ export type { SiloOrphan } from "./silo-paths.ts";
40
+ export {
41
+ REALM_SPAWN_DIR,
42
+ spawnMounts,
43
+ spawnSocketDir,
44
+ spawnSocketPath,
45
+ spawnScratchPath,
46
+ spawnArtifactsPath,
47
+ } from "./spawn-paths.ts";
48
+ export type { SpawnValue } from "./spawn-paths.ts";
49
+ export {
50
+ REALM_HOME_PROXY_CONTAINER_PATH,
51
+ REALM_HOME_PROXY_DIR,
52
+ homeProxyAppDir,
53
+ homeProxyMount,
54
+ homeProxySocketPath,
55
+ } from "./home-proxy-paths.ts";
@@ -0,0 +1,166 @@
1
+ // The ingress half of the Home → Worker wire contract.
2
+ //
3
+ // `WorkerState.ingress` says *where traffic goes*, the way `WorkerState.apps`
4
+ // says *what runs*. Both are runtime-neutral: an implementation picks a
5
+ // renderer for each and satisfies them however it likes. The Docker worker
6
+ // pairs `apps` with `convertToDockerCompose` and `ingress` with
7
+ // `buildCaddyConfig` (`runtime/docker/ingress/`); a Fargate worker would emit
8
+ // ECS task definitions and ALB listener rules from these same two fields. So
9
+ // the schema lives here, in the protocol, while the renderer that consumes it
10
+ // lives under `runtime/docker/` - that split is what lets a second worker
11
+ // implementation read the same field.
12
+ //
13
+ // Two things are deliberately NOT here:
14
+ // - Caddy's config document. Shipping the rendered blob would lock the wire
15
+ // to one proxy and validate nothing (`workerStateSchema.parse` cannot see
16
+ // into an opaque string).
17
+ // - Docker network names. The routed entries carry no `network`: it is
18
+ // always `realm_${appId}`, and the Docker renderer derives it.
19
+ //
20
+ // Install-time fields (`baseDomain`, `homeSlug`, the acme-dns credentials) ride
21
+ // along even though the installer could seed them on the box: they carry
22
+ // secrets, so they need the per-worker encrypted channel rather than a plain
23
+ // file, and one delivery path beats two that can disagree.
24
+
25
+ import { z } from "zod";
26
+
27
+ /**
28
+ * One ACME DNS-01 credential set: the acme-dns shim's base URL plus the label
29
+ * this caller owns under it.
30
+ *
31
+ * `ca` is an ACME directory URL override (staging / a test CA); empty means
32
+ * the renderer's default endpoint. `caRootPem` is the PEM to trust for that
33
+ * endpoint - the BYTES, not a path. A path would have to be agreed between
34
+ * whoever writes the file and whoever references it, which are two different
35
+ * modules; with the bytes on the wire the renderer owns both ends and the two
36
+ * cannot disagree.
37
+ */
38
+ const acmeIssuerSchema = z.object({
39
+ serverUrl: z.string(),
40
+ username: z.string(),
41
+ apiKey: z.string(),
42
+ ca: z.string(),
43
+ caRootPem: z.string(),
44
+ });
45
+
46
+ /**
47
+ * A routed service's router, projected down to what a renderer reads.
48
+ *
49
+ * NOT a copy of `RoutedRouter` (`schema/peer.ts`): that type also carries
50
+ * `name`, `icon` and `tile`, which are Home's dashboard presentation
51
+ * and have no business on a worker's wire. Home enumerates the fields below
52
+ * explicitly when it derives this, so a router field added later stays off the
53
+ * wire until someone puts it here on purpose.
54
+ */
55
+ const ingressRouterSchema = z.object({
56
+ containerPort: z.number().int().min(1).max(65535),
57
+ /** Operator-granted hostnames for the additional ungated public route. */
58
+ publicDomain: z.union([z.string(), z.array(z.string())]).optional(),
59
+ /** With several granted hosts: serve them all, or serve one and 301 the rest. */
60
+ domainMode: z.enum(["redirect", "all"]),
61
+ pathPrefix: z
62
+ .string()
63
+ .regex(/^\/[a-zA-Z0-9/_-]*$/, "pathPrefix must start with / and contain only [a-zA-Z0-9/_-]")
64
+ .optional(),
65
+ compress: z.boolean(),
66
+ maxRequestSizeMb: z.number().nullable(),
67
+ rateLimit: z
68
+ .union([
69
+ z.number(),
70
+ z.object({
71
+ requests: z.number(),
72
+ window: z.string().regex(/^\d+(ms|s|m|h)$/, "window must be a duration like 10s, 1m, 1h"),
73
+ }),
74
+ ])
75
+ .nullable(),
76
+ /** Response-header overrides; `null` deletes the renderer's default. */
77
+ headers: z.record(z.string(), z.string().nullable()).optional(),
78
+ });
79
+
80
+ const ingressRoutedAppSchema = z.object({
81
+ appId: z.string(),
82
+ services: z.array(z.object({ serviceId: z.string(), router: ingressRouterSchema })),
83
+ /**
84
+ * Header-brokering names the app declared via `getTrustedHeaders`. The
85
+ * renderer injects these on the authenticated route (lifted off the
86
+ * forward-auth response) and strips them everywhere else, so identity can't
87
+ * be spoofed.
88
+ */
89
+ trustedHeaderKeys: z.array(z.string()).optional(),
90
+ });
91
+
92
+ export const ingressInputSchema = z.object({
93
+ /** Every routed service on this worker, ordered canonically by `appId`. */
94
+ routedApps: z.array(ingressRoutedAppSchema),
95
+ /**
96
+ * The Home dashboard's own host, `{homeSlug}.{workerId}.{baseDomain}` - one
97
+ * label under the worker wildcard, which a renderer has to match ahead of
98
+ * that wildcard so the not-routed holding page never shadows it.
99
+ *
100
+ * Null on a box that does not run Home. The name resolves realm-wide (it
101
+ * sits under the realm's wildcard), so emitting the route on a worker-only
102
+ * box would answer a hit there with a page claiming the control plane is
103
+ * down - and, under BYO public providers, cost a real per-host certificate
104
+ * on every worker. Home knows which box is which from `home_state
105
+ * .home_worker_id`.
106
+ */
107
+ dashboardHost: z.string().nullable(),
108
+ /**
109
+ * This worker's short slug: the `{workerId}` level of every app subdomain,
110
+ * and the label it owns under acme-dns.
111
+ */
112
+ workerId: z.string(),
113
+ /**
114
+ * The realm's domain base (`{slug}.yourrealm.me`, or the operator's own).
115
+ * With `workerId` it forms every structured authenticated subdomain. Empty
116
+ * on the placeholder identity, before an install is configured.
117
+ */
118
+ baseDomain: z.string(),
119
+ /** DNS-01 inputs for the worker wildcard, or null (dev / e2e / first boot). */
120
+ acmeDns: acmeIssuerSchema.extend({ baseDomain: z.string() }).nullable(),
121
+ /**
122
+ * BYO public-DNS providers, in issue order. `apiToken` is a plain string:
123
+ * a `Ref` cannot ride the wire, and by this point Home has resolved it.
124
+ */
125
+ publicProviders: z.array(
126
+ z.object({ name: z.enum(["cloudflare", "hetzner"]), apiToken: z.string() }),
127
+ ),
128
+ /**
129
+ * CDNs in front of this realm, whose edge addresses ingress trusts when
130
+ * resolving the real client IP. The addresses themselves are not here - the
131
+ * renderer's runtime fetches and refreshes them.
132
+ */
133
+ cdnProviders: z.array(z.string()),
134
+ /**
135
+ * The signing-certificate fingerprints in the Digital Asset Links statement
136
+ * the ingress serves on every host - the statement's
137
+ * `sha256_cert_fingerprints`, which Chrome matches the companion APK's own
138
+ * signature against before it drops the Trusted Web Activity's toolbar.
139
+ *
140
+ * Operator-set rather than a constant, because which certificate signs the
141
+ * installed APK is a property of how that operator got it: Play App Signing
142
+ * re-signs with Google's key, direct distribution keeps the release
143
+ * keystore's, and a locally built debug APK carries a third.
144
+ *
145
+ * The shape is pinned here because the renderer inlines these strings into a
146
+ * JSON body Caddy's `ReplaceKnown` walks: uppercase hex bytes and colons can
147
+ * never form a `{placeholder}`, so a value that parses cannot reach into the
148
+ * replacer.
149
+ */
150
+ companionFingerprints: z.array(
151
+ z
152
+ .string()
153
+ .regex(/^[0-9A-F]{2}(:[0-9A-F]{2}){31}$/, "expected 32 colon-separated uppercase hex bytes"),
154
+ ),
155
+ });
156
+
157
+ export type IngressInput = z.infer<typeof ingressInputSchema>;
158
+
159
+ /**
160
+ * The ports the box ingress binds, for the life of the box.
161
+ *
162
+ * Constants rather than grants: the ingress is placed by the installer as a
163
+ * platform compose project, so no app declares them and no operator can refuse
164
+ * them - a realm whose 443 is closed is a realm nobody can reach.
165
+ */
166
+ export const PLATFORM_INGRESS_PORTS = [80, 443] as const;
@@ -0,0 +1,50 @@
1
+ import type { WorkerMount } from "./worker-mount.ts";
2
+
3
+ /**
4
+ * Branded host-absolute path supplied by the worker (e.g. the host.logs
5
+ * broker socket dir). Mirrors `PeerHostPath`: lets a worker-managed path
6
+ * flow into `volumes.hostPath` without widening the literal-path type
7
+ * to allow arbitrary `string` host paths.
8
+ */
9
+ export type WorkerHostPath = string & { readonly __workerHostPath: unique symbol };
10
+
11
+ /**
12
+ * Host directory the worker keeps the shared logs socket inside. This is the
13
+ * path an app mounts (the DIRECTORY, not the socket file): a file bind-mount
14
+ * of a unix socket does NOT wire to the live listener across containers on
15
+ * macOS Docker (connect succeeds, no bytes flow), so every broker socket lives
16
+ * alone in its own directory and the app mounts that directory. The value
17
+ * `r.host.logs()` resolves to (and the path the resolver exempts from the
18
+ * absolute-mount gate).
19
+ *
20
+ * Lives under `/opt/realm/worker/` because that directory is the one volume
21
+ * the worker container already shares with the host (see `install-script.ts`'s
22
+ * `-v /opt/realm/worker:/opt/realm/worker`), so the socket the worker writes
23
+ * is the same inode an app container can bind-mount through the host docker
24
+ * daemon.
25
+ *
26
+ * Lives in worker-protocol because it's a Home↔Worker contract: Home encodes
27
+ * the path into WorkerState via `host.logs.toValue`; Worker binds the socket
28
+ * inside it on startup. Pure constants, no runtime imports, so `schema/host.ts`
29
+ * (loaded in QuickJS) and any realm.ts consumer can pull it without dragging
30
+ * Deno deps.
31
+ */
32
+ export const REALM_LOGS_DIR = "/opt/realm/worker/logs" as WorkerHostPath;
33
+
34
+ /**
35
+ * Host path of the logs socket, inside {@link REALM_LOGS_DIR}. The worker's
36
+ * logs-server binds this; a consumer mounts {@link REALM_LOGS_DIR} and connects
37
+ * to the socket at the corresponding path inside the mounted directory.
38
+ */
39
+ export const REALM_LOGS_SOCKET = `${REALM_LOGS_DIR}/logs.sock`;
40
+
41
+ /**
42
+ * What `r.host.logs()` resolves to: the mount, not the bare path. Read-only
43
+ * because {@link REALM_LOGS_DIR} is shared by every app holding the grant -
44
+ * see {@link WorkerMount}.
45
+ */
46
+ export const REALM_LOGS_MOUNT: WorkerMount = {
47
+ hostPath: REALM_LOGS_DIR,
48
+ containerPath: "/run/realm/logs",
49
+ readOnly: true,
50
+ };
@@ -0,0 +1,39 @@
1
+ // Worker-owned external networks - shared Docker bridges created out of band
2
+ // (`management-net` by the installer, `untrusted-net` by the spawn broker) and
3
+ // joined via `host.*` permissions. Both surface in `build-wire`'s
4
+ // `externalNetworks` and land on each service's `joinNetworks` post-up. Branded
5
+ // `Network` so a raw name can't stand in for a `joinNetworks` entry.
6
+ //
7
+ // Co-located because they're the same kind of thing: `host.management`
8
+ // (schema/host.ts) reads `MANAGEMENT_NET`, the spawn broker reads
9
+ // `UNTRUSTED_NET`, and `build-wire` matches both. Pure constants, type-only
10
+ // import, so the QuickJS-loaded `schema/host.ts` can pull them.
11
+
12
+ import type { Network } from "../identifiers.ts";
13
+
14
+ /**
15
+ * The Home's management bridge: dashboard URL plumbing + Home identity,
16
+ * joined by system-app proxies/sidecars via `host.management()`.
17
+ */
18
+ export const MANAGEMENT_NET = "management-net" as Network;
19
+
20
+ /**
21
+ * The untrusted-zone ("DMZ") bridge a spawned box attaches to at create time.
22
+ * The broker creates it and joins the box; no app service does.
23
+ */
24
+ export const UNTRUSTED_NET = "untrusted-net" as Network;
25
+
26
+ /**
27
+ * Prefix of every Silo bridge. App ids never contain `_`, so
28
+ * `realm_silo_<siloId>` can't collide with an app's own `realm_<appId>`.
29
+ */
30
+ export const SILO_NET_PREFIX = "realm_silo_";
31
+
32
+ /** The shared bridge of one Silo, created `Internal: true` by the worker pre-up. */
33
+ export function siloNetwork(siloId: string): Network {
34
+ return `${SILO_NET_PREFIX}${siloId}` as Network;
35
+ }
36
+
37
+ export function isSiloNetwork(name: string): boolean {
38
+ return name.startsWith(SILO_NET_PREFIX);
39
+ }
@@ -0,0 +1,70 @@
1
+ // What a node knows about its own version: the image it was started from
2
+ // and the commit that built it. Home and Worker answer the question the same
3
+ // way, so the contract lives here instead of being written twice.
4
+ //
5
+ // Split by who supplies what. `REALM_GIT_HASH` / `REALM_BUILD_DATE` are baked
6
+ // into the image at build time. `REALM_IMAGE_REF` cannot be - a container has
7
+ // no way to read its own digest - so the install script's compose injects it
8
+ // from the `.env` pin, which makes that pin the single source of truth for both
9
+ // what runs and what gets reported.
10
+ //
11
+ // Every field is nullable rather than defaulted, and the build-args default to
12
+ // empty rather than to a placeholder like "dev". A node can legitimately know
13
+ // only part of this - a hand-built image reports its digest but no commit - and
14
+ // "unknown" must never be mistakable for a real answer, because the
15
+ // self-updater's applied signal compares these strings.
16
+
17
+ import { z } from "zod";
18
+
19
+ export const nodeVersionSchema = z.object({
20
+ // The pinned ref this container runs: `benjick/realm-worker@sha256:…` for a
21
+ // pulled image, a bare image ID for a locally-loaded one (see the installer's
22
+ // pin_image). This is the string an `applied` audit entry matches against.
23
+ imageRef: z.string().nullable(),
24
+ // The commit the image was built from - a full SHA from the publish workflow,
25
+ // null for anything built outside it.
26
+ gitHash: z.string().nullable(),
27
+ // ISO-8601 build timestamp, read back off the image's OCI `created` label by
28
+ // the publish workflow so the reported date and the image's own can't drift.
29
+ buildDate: z.string().nullable(),
30
+ });
31
+
32
+ export type NodeVersion = z.infer<typeof nodeVersionSchema>;
33
+
34
+ // An unset build-arg reaches the container as an empty ENV, not an absent one,
35
+ // so empty normalizes to null alongside undefined.
36
+ function readEnv(name: string): string | null {
37
+ const value = Deno.env.get(name);
38
+ return value === undefined || value === "" ? null : value;
39
+ }
40
+
41
+ /**
42
+ * Read this container's version identity from the environment.
43
+ *
44
+ * Reads lazily on purpose. `bundle.ts`'s allowlist is prefix-based
45
+ * (`ALLOWED_IMPORT_PREFIXES = ["@yourrealm/sdk", ...]`), so an author's
46
+ * `realm.ts` may legally import this entry and have it bundled into QuickJS,
47
+ * where `Deno` does not exist. Module scope must stay clean; calling this in
48
+ * there is the author's own error and fails at the call, not at load.
49
+ *
50
+ * Enforced by `quickjs-safe.test.ts` next to this file: hoisting the read to
51
+ * module scope fails it with `ReferenceError: 'Deno' is not defined`.
52
+ */
53
+ export function readNodeVersion(): NodeVersion {
54
+ return {
55
+ imageRef: readEnv("REALM_IMAGE_REF"),
56
+ gitHash: readEnv("REALM_GIT_HASH"),
57
+ buildDate: readEnv("REALM_BUILD_DATE"),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Short human label for a node's version, for UI and logs. Prefers the commit
63
+ * (a person can look it up); falls back to the digest's leading hex so a node
64
+ * built outside CI still reads as something. `null` when neither is known.
65
+ */
66
+ export function shortNodeVersion(version: NodeVersion): string | null {
67
+ if (version.gitHash) return version.gitHash.slice(0, 7);
68
+ const digest = version.imageRef?.split("sha256:")[1];
69
+ return digest ? digest.slice(0, 12) : null;
70
+ }
@@ -0,0 +1,114 @@
1
+ // Wire constants for `host.silo()` — the shared directory a Silo's members
2
+ // mount. Home stores the concrete path on the Silo row (never this template),
3
+ // so changing the root here can't re-point an existing Silo at bytes that
4
+ // didn't move; the builder is what Home calls when it mints a *new* managed
5
+ // Silo, and what the worker's docs point at when explaining where they live.
6
+ //
7
+ // Pure string constants, no runtime imports, so this stays QuickJS-safe like
8
+ // its siblings.
9
+
10
+ /**
11
+ * Host directory holding every Realm-managed Silo, one per id.
12
+ *
13
+ * Beneath `/opt/realm/worker/` for the same reason the broker sockets are: it's
14
+ * the one directory the worker container already shares read-write with the
15
+ * host, so the tree the worker prepares (group, setgid, group rwx) is the same
16
+ * inode a member bind-mounts through the host docker daemon. A path the worker
17
+ * can't reach is a path it can't `chgrp`, and an unprepared Silo directory is
18
+ * just a root-owned folder every member gets EACCES from.
19
+ *
20
+ * It sits beside `apps/`, where each app's own `./data` already resolves, so
21
+ * all of a worker's persistent state stays under one root.
22
+ */
23
+ export const REALM_SILO_DIR = "/opt/realm/worker/silo";
24
+
25
+ /** Host path of one Realm-managed Silo's shared directory. */
26
+ export function managedSiloPath(id: string): string {
27
+ return `${REALM_SILO_DIR}/${id}`;
28
+ }
29
+
30
+ /**
31
+ * Whether Realm made this directory, and so may prepare it.
32
+ *
33
+ * The worker's guard and Home's "is this Silo adopted" test are the same
34
+ * question, so they're the same function: a Silo Home believes it adopted but
35
+ * the worker believes it manages would get its group rewritten on every
36
+ * reconcile, which is the one thing adoption promises never happens.
37
+ */
38
+ export function isManagedSiloPath(hostPath: string): boolean {
39
+ return hostPath.startsWith(`${REALM_SILO_DIR}/`);
40
+ }
41
+
42
+ /**
43
+ * Single-quote a path for the shell. Always, never conditionally: a real folder
44
+ * name can hold a space, an `&`, or brackets, and an unquoted `/mnt/my media`
45
+ * is two operands - so `chgrp -R` walks one tree the operator didn't name and
46
+ * `chmod` misses the one they did. `'\''` is the only escape a single-quoted
47
+ * string needs.
48
+ */
49
+ function shellQuote(path: string): string {
50
+ return `'${path.replaceAll("'", "'\\''")}'`;
51
+ }
52
+
53
+ /**
54
+ * The command that makes an adopted directory shareable, for the operator to
55
+ * run on the box that holds it.
56
+ *
57
+ * Realm allocates the group and hands over the one command that applies it,
58
+ * rather than reaching into a directory it didn't create. `chgrp -R` because a
59
+ * member has to be able to reach what's already in there; `g+rwxs` because
60
+ * group-write plus setgid is what makes a file *one* member writes editable by
61
+ * the *others* (setgid is the half no image can supply - it's a property of the
62
+ * directory).
63
+ *
64
+ * The `chmod` is deliberately NOT recursive: `-R` would rewrite the permission
65
+ * bits of every file the operator already has, and `g+...s` on a regular file is
66
+ * a setgid binary. Directories that already exist keep their modes, which is
67
+ * what the operator is told - widening that is their call, not ours.
68
+ *
69
+ * One definition, because the Silos page, the install picker and the docs all
70
+ * show this string and an operator who runs a stale one gets a Silo that looks
71
+ * ready and shares nothing.
72
+ */
73
+ export function siloSetupCommand(silo: { hostPath: string; gid: number }): string {
74
+ const path = shellQuote(silo.hostPath);
75
+ return `chgrp -R ${silo.gid} ${path}\nchmod g+rwxs ${path}`;
76
+ }
77
+
78
+ /**
79
+ * The widening for an adopted directory that already had folders in it: setup
80
+ * leaves their modes alone, so members can read them and generally can't write
81
+ * into them. Optional, and the operator's call - which is why it's a second
82
+ * command they choose to run rather than a third line of the first one.
83
+ *
84
+ * Rendered here rather than by whichever surface displays it, for the same
85
+ * reason as `siloSetupCommand`: it's a path going into a root shell, and the one
86
+ * place that knows to quote it should be the only place that builds it.
87
+ */
88
+ export function siloWidenCommand(silo: { hostPath: string }): string {
89
+ return `find ${shellQuote(silo.hostPath)} -type d -exec chmod g+rwxs {} +`;
90
+ }
91
+
92
+ /**
93
+ * A managed directory left on disk after its Silo row was deleted. The worker
94
+ * reports these (name + last change + size); Home diffs against live ids and
95
+ * shows the survivors with the reclaim command.
96
+ */
97
+ export interface SiloOrphan {
98
+ /** Directory name under REALM_SILO_DIR — the dead Silo's id. */
99
+ id: string;
100
+ /** ISO timestamp of the directory's last modification. */
101
+ mtime: string | null;
102
+ /** Total bytes inside, or null when the tree was too large to walk in time. */
103
+ sizeBytes: number | null;
104
+ }
105
+
106
+ /**
107
+ * The command that removes an orphaned directory, for the operator to run on
108
+ * the box that holds it. Deleting a Silo never deletes its bytes and no Realm
109
+ * code path ever will - reclaiming is the operator's act, so what Realm hands
110
+ * over is instructions, exactly as adoption hands over the `chgrp`.
111
+ */
112
+ export function siloReclaimCommand(orphan: { workerIp: string; hostPath: string }): string {
113
+ return `ssh root@${orphan.workerIp}\nrm -rf ${shellQuote(orphan.hostPath)}`;
114
+ }