@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,108 @@
1
+ // Shared compose-stack lifecycle: write a compose file, run
2
+ // `docker compose up -d --wait`, and tear down deterministically. Both
3
+ // `realm-sdk test --e2e` (test/boot.ts) and `realm-sdk dev` (dev/server.ts) go
4
+ // through it. The activeStacks registry and beforeunload hook are module-level
5
+ // so a process driving both code paths drains every stack on exit.
6
+
7
+ import { join } from "@std/path";
8
+ import { stringify as yamlStringify } from "yaml";
9
+ import type { ComposeConfig } from "../runtime/docker/compose/types.ts";
10
+
11
+ interface StackInputs {
12
+ /** Pre-built docker-compose object (services, networks, etc.). */
13
+ compose: ComposeConfig;
14
+ /**
15
+ * Compose project name. Docker-compose-validates to a-z 0-9; pick
16
+ * something unique enough to avoid collisions across concurrent runs.
17
+ */
18
+ project: string;
19
+ /** Working directory for the compose file. Caller owns its lifetime. */
20
+ tmpDir: string;
21
+ }
22
+
23
+ interface StackHandle {
24
+ project: string;
25
+ composeFile: string;
26
+ /**
27
+ * Take down the stack and remove its volumes. Idempotent — calling
28
+ * twice (e.g. once from a test's finally + again from the exit hook)
29
+ * is safe.
30
+ */
31
+ cleanup: () => Promise<void>;
32
+ }
33
+
34
+ interface ActiveStack {
35
+ project: string;
36
+ composeFile: string;
37
+ }
38
+
39
+ const activeStacks = new Set<ActiveStack>();
40
+ let exitHookRegistered = false;
41
+
42
+ function registerExitHook(): void {
43
+ if (exitHookRegistered) return;
44
+ exitHookRegistered = true;
45
+ // Deno emits `beforeunload` on a clean exit; we attach there so any
46
+ // stack a caller forgot still goes down before the process leaves.
47
+ // Ctrl-C / uncaught throws don't fire this — `realm-sdk down` is the
48
+ // recovery path for those.
49
+ globalThis.addEventListener("beforeunload", () => {
50
+ for (const stack of activeStacks) {
51
+ // Sync best-effort: we can't await inside beforeunload, but
52
+ // `Deno.Command.outputSync` blocks the event loop until done.
53
+ try {
54
+ new Deno.Command("docker", {
55
+ args: ["compose", "-p", stack.project, "-f", stack.composeFile, "down", "-v"],
56
+ stdout: "null",
57
+ stderr: "null",
58
+ }).outputSync();
59
+ } catch {
60
+ // Ignore — best effort.
61
+ }
62
+ }
63
+ activeStacks.clear();
64
+ });
65
+ }
66
+
67
+ async function downStack(project: string, composeFile: string): Promise<void> {
68
+ const cmd = new Deno.Command("docker", {
69
+ args: ["compose", "-p", project, "-f", composeFile, "down", "-v"],
70
+ stdout: "null",
71
+ stderr: "null",
72
+ });
73
+ await cmd.output().catch(() => {});
74
+ }
75
+
76
+ export async function bringUpStack(inputs: StackInputs): Promise<StackHandle> {
77
+ const { compose, project, tmpDir } = inputs;
78
+ const composeFile = join(tmpDir, "docker-compose.yml");
79
+ await Deno.writeTextFile(composeFile, yamlStringify(compose as unknown));
80
+
81
+ registerExitHook();
82
+
83
+ const upCmd = new Deno.Command("docker", {
84
+ args: ["compose", "-p", project, "-f", composeFile, "up", "-d", "--wait"],
85
+ stdout: "piped",
86
+ stderr: "piped",
87
+ });
88
+ const upOut = await upCmd.output();
89
+ if (upOut.code !== 0) {
90
+ const stderr = new TextDecoder().decode(upOut.stderr);
91
+ const stdout = new TextDecoder().decode(upOut.stdout);
92
+ // Best-effort cleanup before throwing — leaving the stack up after
93
+ // a failure makes the next run fight over names.
94
+ await downStack(project, composeFile);
95
+ throw new Error(`docker compose up failed (exit ${upOut.code})\n${stdout}\n${stderr}`);
96
+ }
97
+
98
+ const stack: ActiveStack = { project, composeFile };
99
+ activeStacks.add(stack);
100
+
101
+ const cleanup = async () => {
102
+ if (!activeStacks.has(stack)) return;
103
+ activeStacks.delete(stack);
104
+ await downStack(project, composeFile);
105
+ };
106
+
107
+ return { project, composeFile, cleanup };
108
+ }
@@ -0,0 +1,159 @@
1
+ // Shared compile + bring-up plumbing for `realm-sdk dev`. The HTTP server
2
+ // (dev/server.ts) and the in-process install handler (dev/install-handler.ts)
3
+ // both go through it, so they can't drift on how a realm.ts becomes a bundled,
4
+ // compiled, compose-running stack: `compileAndBundle` returns a `DevState` and
5
+ // `bringUp` runs `docker compose up -d --wait` against the resolved state.
6
+
7
+ import type { Grants } from "../home.ts";
8
+ import { bundle } from "../bundle.ts";
9
+ import { bundleUiSurfaces, type UiSurfaceBundle } from "../bundle-ui.ts";
10
+ import { mergeSurfaces } from "../surfaces.ts";
11
+ import { compile } from "../test/compile.ts";
12
+ import { convertToDockerCompose } from "../runtime/docker/compose/index.ts";
13
+ import { extractAppName } from "../sandbox.ts";
14
+ import { type CompileIssue, formatIssue } from "../errors.ts";
15
+ import type { HomeApp } from "../resolve.ts";
16
+ import type { Violation } from "../security.ts";
17
+ import type { IngressInput, WorkerApp, WorkerState } from "../worker-protocol/index.ts";
18
+ import { fixtureIngressInput } from "../test/ingress-fixture.ts";
19
+ import { bringUpStack } from "./stack.ts";
20
+ import { loadDevFixture, stableDevSeed } from "./fixture.ts";
21
+
22
+ /**
23
+ * Compile failure thrown by `compileAndBundle`. Carries the original
24
+ * issue list (for the human-readable stderr summary on dev boot / file
25
+ * watcher reloads) and the raw violations (so the install panel can
26
+ * render the structured per-permission list instead of a flat blob).
27
+ */
28
+ export class DevCompileError extends Error {
29
+ constructor(
30
+ message: string,
31
+ public readonly issues: readonly CompileIssue[],
32
+ public readonly violations: readonly Violation[],
33
+ ) {
34
+ super(message);
35
+ this.name = "DevCompileError";
36
+ }
37
+ }
38
+
39
+ export interface DevState {
40
+ appId: string;
41
+ appName: string;
42
+ source: string;
43
+ resolvedState: HomeApp;
44
+ bundles: Map<string, UiSurfaceBundle>;
45
+ /** Map of `${kind}:${key}` → hash. */
46
+ surfaceHashes: Map<string, string>;
47
+ /**
48
+ * This box's ingress, off the dev fixture's `management` block. Carried on
49
+ * the state because `bringUp` needs it for the wire and the fixture is read
50
+ * here - it is the one piece of the wire dev cannot reconstruct downstream.
51
+ */
52
+ ingress: IngressInput;
53
+ }
54
+
55
+ interface CompileAndBundleOpts {
56
+ cwd: string;
57
+ realmFile: string;
58
+ /**
59
+ * In-flight install POST from the panel. When set, `source` is the packed
60
+ * artifact the panel pre-filled (and the operator may have edited) — used
61
+ * verbatim, not re-read from disk — and `config` / `grants` replace the
62
+ * on-disk fixture's values for this one compile. `handleInstall` persists
63
+ * them to `realm-sdk.dev.ts` immediately after, so subsequent reloads pick
64
+ * them up from disk normally. `peers` + `seed` always come from the disk
65
+ * fixture so peer-world resolution and password derivation match the
66
+ * file-watcher path.
67
+ */
68
+ override?: {
69
+ source: string;
70
+ config: Record<string, unknown>;
71
+ grants: Grants;
72
+ };
73
+ }
74
+
75
+ export async function compileAndBundle(opts: CompileAndBundleOpts): Promise<DevState> {
76
+ // The install panel POSTs the packed artifact it pre-filled (what actually
77
+ // ships), so Preview and Install read the exact same bytes. The file-watcher
78
+ // path has no override and merges the app's live files off disk instead. Both
79
+ // feed bundle(), which unpacks + merges a packed source and passes a plain
80
+ // one straight through.
81
+ const source = opts.override ? opts.override.source : (await mergeSurfaces(opts.cwd)).source;
82
+ const bundledCode = await bundle(source);
83
+
84
+ const { fixture: rawFixture, source: fixtureSource } = await loadDevFixture(opts.cwd);
85
+ const fixture = {
86
+ ...rawFixture,
87
+ seed: rawFixture.seed ?? stableDevSeed(opts.cwd),
88
+ ...(opts.override
89
+ ? {
90
+ config: opts.override.config as never,
91
+ grants: opts.override.grants as never,
92
+ }
93
+ : {}),
94
+ };
95
+
96
+ const appName = await extractAppName(bundledCode);
97
+ const appId = fixture.appId ?? appName;
98
+
99
+ const result = await compile({ bundledCode, appName, fixture });
100
+ if (result.error) {
101
+ const lines = [
102
+ `compile failed${fixtureSource ? ` (using ${fixtureSource})` : " (no realm-sdk.dev.ts found — using empty fixture)"}:`,
103
+ ...result.issues.map((i) => ` - ${formatIssue(i)}`),
104
+ ];
105
+ throw new DevCompileError(lines.join("\n"), result.issues, result.violations);
106
+ }
107
+
108
+ const surfaces = await bundleUiSurfaces(source, bundledCode);
109
+ const bundles = new Map<string, UiSurfaceBundle>();
110
+ const surfaceHashes = new Map<string, string>();
111
+ for (const b of surfaces) {
112
+ bundles.set(b.hash, b);
113
+ surfaceHashes.set(`${b.surface.kind}:${b.surface.key}`, b.hash);
114
+ }
115
+
116
+ return {
117
+ appId,
118
+ appName,
119
+ source,
120
+ resolvedState: result.app,
121
+ bundles,
122
+ surfaceHashes,
123
+ ingress: fixtureIngressInput(fixture.management),
124
+ };
125
+ }
126
+
127
+ export function bringUp(state: DevState, tmpDir: string) {
128
+ const workerApp: WorkerApp = {
129
+ appId: state.appId,
130
+ // Dev renders in-process and never serializes to the wire, and the
131
+ // renderer reads only the fields it knows, so the resolve model's extra
132
+ // Home-only fields are simply ignored — cast straight to the wire shape.
133
+ services: state.resolvedState.services as unknown as WorkerApp["services"],
134
+ ...(state.resolvedState.egress ? { egress: true } : {}),
135
+ externalNetworks: [],
136
+ dependsOn: {},
137
+ inlineFiles: {},
138
+ allowedAbsoluteHostPaths: [],
139
+ };
140
+ const wireState: WorkerState = {
141
+ apps: [workerApp],
142
+ artifactHashes: [],
143
+ workerNetworks: [],
144
+ ingress: state.ingress,
145
+ };
146
+ // Single-app harness: the renderer returns one project per app, and
147
+ // dev only ever has one.
148
+ const compose = convertToDockerCompose(wireState)[0]!.compose;
149
+ // Mark every service so `realm-sdk down` (com.realm.test=true) won't
150
+ // sweep them, and so an operator can grep their docker ps. Also inject
151
+ // REALM_SDK_DEV=1 so apps can opt into dev-only behavior (synthetic
152
+ // fixtures, skipping connections that need a real worker, etc.).
153
+ for (const svc of Object.values(compose.services)) {
154
+ svc.labels = { ...svc.labels, "com.realm.dev": "true" };
155
+ svc.environment = { ...svc.environment, REALM_SDK_DEV: "1" };
156
+ }
157
+ const project = `realm-dev-${state.appId}`;
158
+ return bringUpStack({ compose, project, tmpDir });
159
+ }
package/src/docker.ts ADDED
@@ -0,0 +1,96 @@
1
+ // Docker registry v2 gate helpers — for use inside `verifyAnonymousRequest`
2
+ // when an app fronts an OCI registry (e.g. Forge's Zot). A `docker pull`
3
+ // answers a 401-with-Basic-challenge with `Authorization: Basic
4
+ // base64(username:password)`; `checkDockerBasic` compares that password to the
5
+ // app's `stableSecret(...)`, and `dockerChallenge` is the 401 that triggers it.
6
+ //
7
+ // These run in the QuickJS gate sandbox, which shims only
8
+ // Request/Response/Headers/URL — there is no `atob` and no Web Crypto — so the
9
+ // code is pure JS: base64 is decoded by hand and the compare is a plain loop.
10
+ // Keep it that way (no `Deno`/`atob`/`TextEncoder`) or the gate bundle breaks
11
+ // at load under QuickJS.
12
+
13
+ /** Standard-base64 alphabet (RFC 4648). */
14
+ const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
15
+
16
+ /**
17
+ * Decode standard base64 to a Latin-1 string, or `null` on an invalid
18
+ * character. Pure JS (no `atob`) so it runs in the gate sandbox. Exact for
19
+ * well-formed base64; looser than `atob` on a malformed *length* — a dangling
20
+ * final partial group is dropped rather than rejected. That only ever drops a
21
+ * trailing group (never alters earlier bytes), so it's safe for this
22
+ * fail-closed credential check: a bad credential still can't match the secret.
23
+ * Registry credentials are ASCII (`username:hexsecret`), so the byte-as-charcode
24
+ * string round-trips them exactly.
25
+ */
26
+ function decodeBase64(input: string): string | null {
27
+ let s = input;
28
+ // Drop CR/LF and trailing `=` padding; the bit accumulator handles the rest.
29
+ s = s.replace(/[\r\n]/g, "");
30
+ while (s.length > 0 && s[s.length - 1] === "=") s = s.slice(0, -1);
31
+
32
+ let out = "";
33
+ let acc = 0;
34
+ let bits = 0;
35
+ for (let i = 0; i < s.length; i++) {
36
+ const idx = B64.indexOf(s[i]!);
37
+ if (idx < 0) return null;
38
+ acc = (acc << 6) | idx;
39
+ bits += 6;
40
+ if (bits >= 8) {
41
+ bits -= 8;
42
+ out += String.fromCharCode((acc >> bits) & 0xff);
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+
48
+ /** Length-then-XOR compare; no early return on the first differing byte. */
49
+ function constantTimeEqual(a: string, b: string): boolean {
50
+ if (a.length !== b.length) return false;
51
+ let diff = 0;
52
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
53
+ return diff === 0;
54
+ }
55
+
56
+ /**
57
+ * `true` when the request carries an `Authorization: Basic` header whose
58
+ * password half equals `expectedSecret`. The username half is ignored — the
59
+ * single shared `stableSecret(...)` is the whole credential, so any username
60
+ * with the right password passes. Anything malformed (no header, wrong scheme,
61
+ * bad base64, no colon) is a plain `false`, so the caller falls through to
62
+ * {@link dockerChallenge}.
63
+ *
64
+ * ```ts
65
+ * verifyAnonymousRequest: (request, stableSecret) =>
66
+ * checkDockerBasic(request, stableSecret("registry")) || dockerChallenge();
67
+ * ```
68
+ */
69
+ export function checkDockerBasic(request: Request, expectedSecret: string): boolean {
70
+ const header = request.headers.get("authorization");
71
+ if (!header) return false;
72
+ const space = header.indexOf(" ");
73
+ if (space < 0) return false;
74
+ if (header.slice(0, space).toLowerCase() !== "basic") return false;
75
+
76
+ const decoded = decodeBase64(header.slice(space + 1).trim());
77
+ if (decoded == null) return false;
78
+ const colon = decoded.indexOf(":");
79
+ if (colon < 0) return false;
80
+ return constantTimeEqual(decoded.slice(colon + 1), expectedSecret);
81
+ }
82
+
83
+ /**
84
+ * The `401` that makes a `docker pull` retry with Basic credentials: an empty
85
+ * body plus `WWW-Authenticate: Basic`. Return it from `verifyAnonymousRequest`
86
+ * for an unauthenticated (or wrong-credential) registry request.
87
+ */
88
+ export function dockerChallenge(realm = "Realm registry"): Response {
89
+ return new Response(null, {
90
+ status: 401,
91
+ headers: {
92
+ "WWW-Authenticate": `Basic realm="${realm}"`,
93
+ "Docker-Distribution-Api-Version": "registry/2.0",
94
+ },
95
+ });
96
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Error contract for the realm.ts → state compile pipeline.
3
+ *
4
+ * Modeled on Zod: one class with a list of issues, where each issue is a
5
+ * discriminated union keyed on `kind`. Single-issue throw sites pass one
6
+ * issue; accumulating boundaries (Phase 2 `compile()`, multi-violation
7
+ * grant checks) pass a list. The list is always the public surface — a
8
+ * one-issue error is just a one-element list.
9
+ *
10
+ * No `publicMessage` on the throw site. The HTTP boundary decides what's
11
+ * safe to render per-kind in one place; throw sites just describe what
12
+ * happened.
13
+ */
14
+ export type CompileIssue =
15
+ | { kind: "eval"; message: string; line?: number; column?: number }
16
+ | { kind: "validation"; path: string[]; message: string }
17
+ | { kind: "permission"; missing: string };
18
+
19
+ export class RealmCompileError extends Error {
20
+ readonly issues: readonly CompileIssue[];
21
+
22
+ constructor(input: CompileIssue | CompileIssue[]) {
23
+ const list = Array.isArray(input) ? input : [input];
24
+ super(formatHeader(list));
25
+ this.issues = Object.freeze([...list]) as readonly CompileIssue[];
26
+ this.name = "RealmCompileError";
27
+ }
28
+ }
29
+
30
+ /**
31
+ * A `host.silo` require-map the Silo doesn't meet yet. Its own subclass so
32
+ * the eval pass structure can tell it apart from a permanent failure: a
33
+ * provider that evaluates later may satisfy it, so this is the one drop the
34
+ * fixpoint loop retries.
35
+ *
36
+ * Lives here rather than in Home because the thrower is `host.silo`'s
37
+ * resolver, which lives in the SDK.
38
+ */
39
+ export class SiloRequiresError extends RealmCompileError {}
40
+
41
+ function formatHeader(issues: readonly CompileIssue[]): string {
42
+ if (issues.length === 1) return formatIssue(issues[0]!);
43
+ return `${issues.length} compile errors`;
44
+ }
45
+
46
+ /**
47
+ * Render a single issue as a human-readable string. Used for the
48
+ * Error.message header (stack traces, logs) - NOT for HTTP responses,
49
+ * which redact eval issues to a generic string. The HTTP boundary
50
+ * lives in Home's `parseRealmError`.
51
+ */
52
+ export function formatIssue(issue: CompileIssue): string {
53
+ switch (issue.kind) {
54
+ case "eval":
55
+ return issue.message;
56
+ case "validation":
57
+ return `${issue.path.join(".")}: ${issue.message}`;
58
+ case "permission":
59
+ return `missing grant: ${issue.missing}`;
60
+ }
61
+ }