@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,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
|
+
}
|