@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,501 @@
1
+ // Single-app realm.ts → HomeApp orchestrator for tests.
2
+ //
3
+ // Mirrors the slice of Home's eval pipeline that doesn't depend on a
4
+ // multi-app world: extractSchema → resolvePermissions → merge config +
5
+ // perms → execute in sandbox → resolveRefs → checkAppGrants. The world
6
+ // is whatever the fixture supplies in `peers` (empty by default); a
7
+ // `peers.*` permission without a grant resolves to `[]`, same as Home.
8
+ // remote.* is unfetched, so a realm.ts consuming one surfaces a
9
+ // validation error.
10
+ //
11
+ // `compile` never throws: a pipeline error becomes
12
+ // `{error: true, issues: [...]}`. An eval crash surfaces as an `eval`
13
+ // issue, an elevated *use* without a backing grant as a `permission`
14
+ // issue from `checkAppGrants`. Declared-but-unused ungranted
15
+ // permissions stay silent, same as production.
16
+
17
+ import { execute, executeDeferred, executeInfo, extractSchema, nestByPaths } from "../sandbox.ts";
18
+ import { buildAllowMap, resolveRefs, type ResolutionContext } from "../generator/resolve-refs.ts";
19
+ import { firstPublicDomain, serviceUrl } from "../service-url.ts";
20
+ import { normalizeInfo, type ResolvedInfoEntry } from "../info.ts";
21
+ import { replaceSecrets } from "../compile.ts";
22
+ import { sha256Hex } from "../generator/hash.ts";
23
+ import { deepMergeConfig } from "../deep-merge.ts";
24
+ import { RealmCompileError, type CompileIssue, formatIssue } from "../errors.ts";
25
+ import {
26
+ checkAppGrants,
27
+ grantsSchema,
28
+ sanitizeGrants,
29
+ siloMountKey,
30
+ type Violation,
31
+ } from "../security.ts";
32
+ import { type SiloGrants, type SiloResolved } from "../schema/host.ts";
33
+ import { isHomeGrant, isHostGrant, resolveValue } from "../grants.ts";
34
+ import type { ResolveContext } from "../resolve-context.ts";
35
+ import { peerHooks, resolvePeerValue, type WorldState } from "../peer-world.ts";
36
+ import type { AllowedProvider } from "../peer-registry.ts";
37
+ import type { PeerPermissionKind } from "../permissions.ts";
38
+ import type { Grants } from "../grants.ts";
39
+ import type { Network } from "../identifiers.ts";
40
+ import { homeProxyMount } from "../worker-protocol/home-proxy-paths.ts";
41
+ import { spawnMounts } from "../worker-protocol/spawn-paths.ts";
42
+ import type { WorkerHostPath } from "../worker-protocol/logs-socket-path.ts";
43
+ import type { WorkerMount } from "../worker-protocol/worker-mount.ts";
44
+ import type { HomeApp } from "../resolve.ts";
45
+ import type { SerializablePermission } from "../sandbox.ts";
46
+ import type { CompileFixture, CompileResult } from "./types.ts";
47
+
48
+ interface CompileInputs<App> {
49
+ bundledCode: string;
50
+ appName: string;
51
+ fixture: CompileFixture<App>;
52
+ }
53
+
54
+ export async function compile<App>(inputs: CompileInputs<App>): Promise<CompileResult> {
55
+ const { bundledCode, appName, fixture } = inputs;
56
+ const appId = fixture.appId ?? appName;
57
+ const issues: CompileIssue[] = [];
58
+ const grantViolations: Violation[] = [];
59
+
60
+ let permissionsFlat: Record<string, SerializablePermission>;
61
+ let secretPaths: string[];
62
+ try {
63
+ const extracted = await extractSchema(bundledCode);
64
+ permissionsFlat = extracted.permissionsFlat;
65
+ secretPaths = extracted.secretPaths;
66
+ } catch (err) {
67
+ return errorResult([...issues, ...issuesFromError(err)], grantViolations);
68
+ }
69
+
70
+ const { sanitized: config, secrets } = replaceSecrets(
71
+ fixture.config as Record<string, unknown>,
72
+ secretPaths,
73
+ );
74
+
75
+ let grants: Grants;
76
+ try {
77
+ const parsed = grantsSchema.parse((fixture.grants ?? {}) as unknown);
78
+ grants = sanitizeGrants(permissionsFlat, parsed).sanitized;
79
+ } catch (err) {
80
+ return errorResult([...issues, ...issuesFromError(err)], grantViolations);
81
+ }
82
+
83
+ const world: WorldState = fixture.peers ?? [];
84
+ let resolved: ResolvedPerms;
85
+ try {
86
+ resolved = await resolvePermissions(
87
+ permissionsFlat,
88
+ grants,
89
+ world,
90
+ appId,
91
+ fixture.management,
92
+ fixture.homeApi,
93
+ fixture.spawn,
94
+ fixture.silo,
95
+ );
96
+ } catch (err) {
97
+ // An unmet silo require-map (or a fixture-usage slip) lands as a
98
+ // CompileResult issue rather than escaping compile(), mirroring how Home
99
+ // turns the same throw into a drop.
100
+ return errorResult([...issues, ...issuesFromError(err)], grantViolations);
101
+ }
102
+ const nestedPermissions = nestByPaths(resolved.values);
103
+ const data = deepMergeConfig(config, nestedPermissions);
104
+
105
+ const preview = fixture.preview ?? null;
106
+ let state: HomeApp;
107
+ try {
108
+ // No cache key: a fixture run compiles one source against many configs, so
109
+ // every call would miss `execute`'s per-app slot anyway.
110
+ state = await execute(bundledCode, data, appId, null, preview);
111
+ } catch (err) {
112
+ return errorResult([...issues, ...issuesFromError(err)], grantViolations);
113
+ }
114
+
115
+ let walked: HomeApp;
116
+ let infoEntries: ResolvedInfoEntry[] = [];
117
+ try {
118
+ const seed = fixture.seed ?? "test";
119
+ const derivationSeed = await crypto.subtle.importKey(
120
+ "raw",
121
+ new TextEncoder().encode(seed),
122
+ { name: "HMAC", hash: "SHA-256" },
123
+ false,
124
+ ["sign"],
125
+ );
126
+ const resolveCtx: ResolutionContext = {
127
+ consumerAppId: appId,
128
+ derivationSeed,
129
+ secrets,
130
+ allowedProviders: buildAllowMap(resolved.allowedProviders),
131
+ derivedCache: new Map(),
132
+ runDeferred: (items) => executeDeferred(bundledCode, data, appId, items, preview),
133
+ };
134
+ const services: Record<string, unknown> = { ...state.services };
135
+ // Pass `ui` through the walker too — loader.headers may carry ref
136
+ // markers the dev server and boot tests need resolved. Omit when
137
+ // absent so apps without a `ui:` block don't acquire a spurious
138
+ // `ui: undefined` key on the resolved state.
139
+ const walkInput: Record<string, unknown> = { services };
140
+ if (state.ui !== undefined) walkInput.ui = state.ui;
141
+ if (state.egress) walkInput.egress = state.egress;
142
+ // trustedHeaders is plain sampled data (no ref markers) but result.state
143
+ // consumers (and the Caddy peer extract) read it, so carry it through.
144
+ if (state.trustedHeaders !== undefined) walkInput.trustedHeaders = state.trustedHeaders;
145
+ walked = (await resolveRefs(walkInput as unknown as HomeApp, resolveCtx)) as HomeApp;
146
+ // Defense-in-depth: a marker landing in a field the walker doesn't
147
+ // recognize passes through unresolved, which in production renders
148
+ // `{__realmRef: ...}` into compose YAML. Throw so the test fails
149
+ // loud. Mirrors `build-wire.ts`.
150
+ assertNoRefMarkers(walked, ["state"]);
151
+
152
+ // Resolve `info` on the same separate path Home's summary page uses.
153
+ infoEntries = await resolveCompileInfo(
154
+ bundledCode,
155
+ walked,
156
+ appId,
157
+ fixture.baseDomain ?? null,
158
+ fixture.slug ?? null,
159
+ resolveCtx,
160
+ );
161
+ } catch (err) {
162
+ return errorResult([...issues, ...issuesFromError(err)], grantViolations);
163
+ }
164
+
165
+ const violations = checkAppGrants(walked, grants, permissionsFlat, {
166
+ allowedNetworks: resolved.networks,
167
+ allowedAbsoluteHostPaths: resolved.absoluteHostPaths,
168
+ readOnlyHostPaths: resolved.readOnlyHostPaths,
169
+ allowedSiloMounts: resolved.siloMounts,
170
+ });
171
+ for (const v of violations) {
172
+ grantViolations.push(v);
173
+ issues.push(violationToIssue(v));
174
+ }
175
+
176
+ if (issues.length > 0) return errorResult(issues, grantViolations);
177
+ return { error: false, app: walked, info: infoEntries };
178
+ }
179
+
180
+ // Build the `services` URL map + run `app.info`, resolving its refs with the
181
+ // already-built walk context. Own try/catch so a broken `info` never fails an
182
+ // otherwise-good compile (matches Home's leniency).
183
+ async function resolveCompileInfo(
184
+ bundledCode: string,
185
+ state: HomeApp,
186
+ appId: string,
187
+ baseDomain: string | null,
188
+ slug: string | null,
189
+ ctx: ResolutionContext,
190
+ ): Promise<ResolvedInfoEntry[]> {
191
+ try {
192
+ const services: Record<string, string | null> = {};
193
+ for (const [name, svc] of Object.entries(state.services)) {
194
+ services[name] = serviceUrl({
195
+ appId,
196
+ serviceId: name,
197
+ slug,
198
+ baseDomain,
199
+ publicDomain: firstPublicDomain(svc.router?.publicDomain),
200
+ hasRouter: svc.router != null,
201
+ });
202
+ }
203
+ const raw = await executeInfo(bundledCode, services);
204
+ if (raw == null) return [];
205
+ const resolved = (await resolveRefs(raw, ctx)) as Record<string, unknown>;
206
+ return normalizeInfo(resolved);
207
+ } catch {
208
+ return [];
209
+ }
210
+ }
211
+
212
+ function errorResult(issues: CompileIssue[], violations: Violation[]): CompileResult {
213
+ return {
214
+ error: true,
215
+ message: issues.length === 1 ? formatIssue(issues[0]!) : `${issues.length} compile errors`,
216
+ issues,
217
+ violations,
218
+ };
219
+ }
220
+
221
+ function issuesFromError(err: unknown): CompileIssue[] {
222
+ if (err instanceof RealmCompileError) return [...err.issues];
223
+ return [{ kind: "eval", message: err instanceof Error ? err.message : String(err) }];
224
+ }
225
+
226
+ function violationToIssue(v: Violation): CompileIssue {
227
+ if (v.kind === "eval") return { kind: "eval", message: v.message };
228
+ if (v.kind === "worker-capability") {
229
+ return {
230
+ kind: "permission",
231
+ missing: `${v.missing.join(", ")} (worker ${v.workerId} can't satisfy)`,
232
+ };
233
+ }
234
+ if (v.kind === "app-kind") {
235
+ return { kind: "validation", path: ["kind"], message: v.description };
236
+ }
237
+ if (v.kind === "routing") {
238
+ return { kind: "validation", path: ["router", "publicDomain"], message: v.description };
239
+ }
240
+ const where = v.serviceName
241
+ ? ` (used by service "${v.serviceName}" without a matching grant)`
242
+ : " (no matching grant)";
243
+ return {
244
+ kind: "permission",
245
+ missing: `${v.permissionKind}${where}`,
246
+ };
247
+ }
248
+
249
+ function assertNoRefMarkers(value: unknown, path: string[]): void {
250
+ if (value == null || typeof value !== "object") return;
251
+ if (Array.isArray(value)) {
252
+ for (let i = 0; i < value.length; i++) {
253
+ assertNoRefMarkers(value[i], [...path, String(i)]);
254
+ }
255
+ return;
256
+ }
257
+ if ("__realmRef" in (value as Record<string, unknown>)) {
258
+ throw new RealmCompileError({
259
+ kind: "eval",
260
+ message: `unresolved ref marker leaked into resolved state at ${path.join(".")}`,
261
+ });
262
+ }
263
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
264
+ assertNoRefMarkers(v, [...path, k]);
265
+ }
266
+ }
267
+
268
+ interface ResolvedPerms {
269
+ values: Record<string, unknown>;
270
+ networks: Set<Network>;
271
+ absoluteHostPaths: Set<string>;
272
+ readOnlyHostPaths: Map<string, string>;
273
+ siloMounts: Set<string>;
274
+ allowedProviders: AllowedProvider[];
275
+ }
276
+
277
+ async function resolvePermissions(
278
+ permsFlat: Record<string, SerializablePermission>,
279
+ grants: Grants,
280
+ world: WorldState,
281
+ appId: string,
282
+ managementOverride: CompileFixture<unknown>["management"],
283
+ homeApiOverride: CompileFixture<unknown>["homeApi"],
284
+ spawnOverride: CompileFixture<unknown>["spawn"],
285
+ siloOverride: CompileFixture<unknown>["silo"],
286
+ ): Promise<ResolvedPerms> {
287
+ const values: Record<string, unknown> = {};
288
+ const networks = new Set<Network>();
289
+ const absoluteHostPaths = new Set<string>();
290
+ const readOnlyHostPaths = new Map<string, string>();
291
+ const siloMounts = new Set<string>();
292
+ const allowedProviders: AllowedProvider[] = [];
293
+
294
+ // Mirrors graph.ts collectAllowed: exempt the mount from the absolute-mount
295
+ // gate, and remember which paths the grant only covers read-only (against the
296
+ // kind that granted them, so the violation names the right permission).
297
+ //
298
+ // Writable wins, same as graph.ts: two permissions can resolve to the same
299
+ // path with different modes, and the map is keyed by path alone, so the
300
+ // second pass below clears the read-only mark a writable mount also earned.
301
+ const writableHostPaths = new Set<string>();
302
+ const allowMount = (mount: { hostPath: string; readOnly: boolean } | undefined, kind: string) => {
303
+ if (!mount?.hostPath) return;
304
+ absoluteHostPaths.add(mount.hostPath);
305
+ if (mount.readOnly) readOnlyHostPaths.set(mount.hostPath, kind);
306
+ else writableHostPaths.add(mount.hostPath);
307
+ };
308
+
309
+ // The eval-time world, from fixture data instead of Home's database. Home
310
+ // builds the same shape in graph.ts, so the resolvers can't tell the
311
+ // difference.
312
+ const siloFixtures = siloOverride ?? {};
313
+ // Fixture-usage checks, per granted declaration. Home reads these fields off a
314
+ // Silo row that always has them, so a missing one is a test-authoring problem
315
+ // rather than a resolution failure an operator would ever see - worth its own
316
+ // message instead of the resolver's.
317
+ //
318
+ // Only granted paths are checked: an optional silo compiled WITHOUT its grant
319
+ // never reaches the resolver, so it asks nothing of the fixture either.
320
+ for (const [path, perm] of Object.entries(permsFlat)) {
321
+ if (perm.kind !== "host.silo") continue;
322
+ const grant = grants[path];
323
+ if (grant?.kind !== "host.silo" || typeof grant.data !== "string") continue;
324
+ const siloId = grant.data;
325
+ const fixture = siloFixtures[siloId];
326
+ if (fixture === undefined) {
327
+ throw new Error(
328
+ `${path}: granted Silo "${siloId}" has no fixture — add \`silo: { ${siloId}: {…} }\``,
329
+ );
330
+ }
331
+ if ((perm.data as SiloGrants).volume === undefined) continue;
332
+ if (fixture.hostPath === undefined || fixture.gid === undefined) {
333
+ throw new Error(
334
+ `${path}: the ask includes \`volume\` — set \`silo: { ${siloId}: { hostPath, gid } }\``,
335
+ );
336
+ }
337
+ }
338
+ const ctx: ResolveContext = {
339
+ appId,
340
+ // No worker identity in a compile-tier test, so `internalId` stays absent
341
+ // and silo's placement check doesn't fire (there is no wrong worker to be
342
+ // on).
343
+ worker: { id: managementOverride?.workerId ?? "" },
344
+ home: {
345
+ baseDomain: managementOverride?.baseDomain ?? "",
346
+ homeSlug: managementOverride?.homeSlug ?? "",
347
+ // The fixture supplies the whole acmeDns block including `username`,
348
+ // which the real resolver derives from baseDomain - so a fixture that
349
+ // sets one keeps its own value by overriding after resolution below.
350
+ acmeDns: managementOverride?.acmeDns ?? null,
351
+ },
352
+ // One record per fixture entry, keyed by the id the grant carries. Two
353
+ // entries means two Silos with their own directories, exactly as Home would
354
+ // read two rows.
355
+ silos: Object.entries(siloFixtures).map(([id, f]) => ({
356
+ id,
357
+ // Home names the Silo in require-map failures; a fixture has no name, so
358
+ // the id stands in.
359
+ name: id,
360
+ workerId: "",
361
+ workerSlug: "",
362
+ hostPath: f.hostPath ?? "",
363
+ containerPath: f.containerPath ?? "/silo",
364
+ gid: f.gid ?? 0,
365
+ })),
366
+ // Per Silo, so a require-map against one never sees another's publications.
367
+ siloDirectory: (siloId) =>
368
+ new Map(
369
+ Object.entries(siloFixtures[siloId]?.services ?? {}).map(([name, port]) => [
370
+ name,
371
+ { port, appId: "fixture-peer" },
372
+ ]),
373
+ ),
374
+ // Production *shape* (64-char hex) without REALM_SECRET; stable per Silo id
375
+ // so two members of one fixture Silo read the same key.
376
+ siloApiKeys: new Map(
377
+ await Promise.all(
378
+ Object.entries(siloFixtures).map(
379
+ async ([id, f]) => [id, f.apiKey ?? (await sha256Hex(`test-silo-apikey-${id}`))] as const,
380
+ ),
381
+ ),
382
+ ),
383
+ };
384
+
385
+ for (const [path, perm] of Object.entries(permsFlat)) {
386
+ const kind = perm.kind;
387
+
388
+ if (kind.startsWith("peers.")) {
389
+ // Mirror Home: ungranted peer permissions resolve to []; granted
390
+ // ones run through the registry against the supplied world. Each
391
+ // resolved entry also contributes networks / absolute host paths /
392
+ // shared providers to the security check.
393
+ const g = grants[path];
394
+ const granted = g != null && g.kind === kind;
395
+ const value = granted
396
+ ? (resolvePeerValue(kind as PeerPermissionKind, perm.data, world) as unknown[])
397
+ : [];
398
+ values[path] = value;
399
+ if (granted && Array.isArray(value)) {
400
+ const hooks = peerHooks(kind);
401
+ if (hooks) {
402
+ for (const entry of value) {
403
+ if (!entry || typeof entry !== "object") continue;
404
+ const net = hooks.extractNetworks?.(entry);
405
+ if (net) networks.add(net);
406
+ const vol = hooks.extractPeerVolumes?.(entry);
407
+ if (vol) absoluteHostPaths.add(vol);
408
+ const provider = hooks.extractAllowedProvider?.(entry);
409
+ if (provider) allowedProviders.push(provider);
410
+ }
411
+ }
412
+ }
413
+ continue;
414
+ }
415
+
416
+ // host.* and home.* run through the SAME registry dispatch Home uses
417
+ // (resolve-permissions.ts) against a context built from the fixture, so an
418
+ // app's own tests can't green-light something Home would reject.
419
+ //
420
+ // remote.* + unknown: no value here. A realm.ts that reads one will trip a
421
+ // downstream validation error.
422
+ const grant = grants[path];
423
+ if (!grant || grant.kind !== kind || !(isHostGrant(grant) || isHomeGrant(grant))) {
424
+ values[path] = undefined;
425
+ continue;
426
+ }
427
+ values[path] = resolveValue(grant, { declared: perm.data, path, ctx });
428
+ }
429
+
430
+ // The collectAllowed half of the sweep, mirroring graph.ts: fold resolved
431
+ // networks in and exempt resolved mounts from the absolute-mount gate. Reads
432
+ // the resolved value, not grant.data — these grants carry presence-only
433
+ // (`true`) data while the real value came out of the resolver above.
434
+ for (const [path, perm] of Object.entries(permsFlat)) {
435
+ const value = values[path];
436
+ if (perm.kind === "host.management" && value && typeof value === "object") {
437
+ const mgmt = value as {
438
+ network: Network;
439
+ baseDomain: string;
440
+ homeSlug: string;
441
+ workerId: string;
442
+ acmeDns: {
443
+ serverUrl: string;
444
+ username: string;
445
+ apiKey: string;
446
+ ca: string;
447
+ caRootPem: string;
448
+ } | null;
449
+ };
450
+ networks.add(mgmt.network);
451
+ // `acmeDns` is the one field not taken straight off the context: the
452
+ // fixture's whole block, `username` included, wins over the derived one.
453
+ if (managementOverride?.acmeDns !== undefined) {
454
+ values[path] = { ...mgmt, acmeDns: managementOverride.acmeDns };
455
+ }
456
+ } else if (perm.kind === "host.logs" || perm.kind === "host.stats") {
457
+ allowMount(value as WorkerMount | undefined, perm.kind);
458
+ } else if (perm.kind === "host.silo" && value && typeof value === "object") {
459
+ // Already resolved above through the real resolver. What's left is the
460
+ // collectAllowed half: the shared directory is exempted from the
461
+ // absolute-mount gate and recorded as a (hostPath, gid) pair, and the
462
+ // silo network folds into allowedNetworks.
463
+ const resolvedSilo = value as SiloResolved;
464
+ if (resolvedSilo.network) networks.add(resolvedSilo.network);
465
+ if (resolvedSilo.volume) {
466
+ allowMount(resolvedSilo.volume, perm.kind);
467
+ siloMounts.add(siloMountKey(resolvedSilo.volume.hostPath, resolvedSilo.volume.gid));
468
+ }
469
+ } else if (perm.kind === "host.spawn" && value && typeof value === "object") {
470
+ // host.spawn resolves to `{socket, scratch, artifacts}` mounts, built
471
+ // per-app by its resolver; the fixture overrides only the host paths. All
472
+ // three are exempted from the absolute-mount gate, so mounting needs only
473
+ // the grant (mirrors graph.ts collectAllowed). No network exemption: the
474
+ // box joins the DMZ broker-side, and no app service may sit on it.
475
+ const spawn = spawnMounts(appId);
476
+ const socket = withHostPath(spawn.socket, spawnOverride?.socket);
477
+ const scratch = withHostPath(spawn.scratch, spawnOverride?.scratch);
478
+ const artifacts = withHostPath(spawn.artifacts, spawnOverride?.artifacts);
479
+ values[path] = { socket, scratch, artifacts };
480
+ allowMount(socket, perm.kind);
481
+ allowMount(scratch, perm.kind);
482
+ allowMount(artifacts, perm.kind);
483
+ } else if (perm.kind === "home.apiAccess" && value && typeof value === "object") {
484
+ // home.apiAccess resolves to `{socket}`, the per-app socket mount its
485
+ // resolver builds; the fixture overrides only its host path. It is exempt
486
+ // from the absolute-mount gate (mirrors graph.ts collectAllowed).
487
+ const socket = withHostPath(homeProxyMount(appId), homeApiOverride?.socket);
488
+ values[path] = { socket };
489
+ allowMount(socket, perm.kind);
490
+ }
491
+ }
492
+
493
+ for (const p of writableHostPaths) readOnlyHostPaths.delete(p);
494
+
495
+ return { values, networks, absoluteHostPaths, readOnlyHostPaths, siloMounts, allowedProviders };
496
+ }
497
+
498
+ /** Swap a resolved mount's host path for the fixture's, keeping the rest. */
499
+ function withHostPath(mount: WorkerMount, hostPath: string | undefined): WorkerMount {
500
+ return hostPath === undefined ? mount : { ...mount, hostPath: hostPath as WorkerHostPath };
501
+ }
@@ -0,0 +1,117 @@
1
+ // Realm.describe: the test-harness entry point. Wraps Deno.test so each
2
+ // test in the block sees a typed `t` whose `compile` and `boot` are
3
+ // constrained by the imported app.
4
+ //
5
+ // Bundling is shared across tests and lazy: the first `t.compile` (or
6
+ // `t.boot`) call drives the bundle, subsequent calls await the same
7
+ // Promise, and a run with every test filtered out bundles nothing.
8
+ //
9
+ // `test.e2e(...)` is a sibling of `test(...)` reserved for tier-2 e2e
10
+ // tests. It registers via `Deno.test.ignore` unless `REALM_TEST_E2E=1`
11
+ // is set, so the default `realm-sdk test` loop stays compile-only.
12
+ // `e2e` is the test tier; `t.boot(...)` inside the body is the action
13
+ // that renders compose, runs `docker compose up`, and connects Playwright.
14
+
15
+ import { basename, dirname, fromFileUrl } from "@std/path";
16
+ import { bundle } from "../bundle.ts";
17
+ import type { AppDefinition } from "../create-app.ts";
18
+ import { mergeSurfaces } from "../surfaces.ts";
19
+ import type { ConfigSchema } from "../types.ts";
20
+ import { compile as runCompile } from "./compile.ts";
21
+ import { boot as runBoot, type RealmBootHandle } from "./boot.ts";
22
+ import type { CompileFixture, CompileResult } from "./types.ts";
23
+
24
+ // `t` inside a `Realm.describe` test is a Deno `TestContext` with two
25
+ // extra methods: `compile` (single-app pipeline → resolved state) and
26
+ // `boot` (full docker stack + Playwright sidecar). It extends rather
27
+ // than wraps because authors pass the same `t` to `assertSnapshot` /
28
+ // `t.step`, which want a real TestContext.
29
+ export type RealmTestContext<App> = Deno.TestContext & {
30
+ compile(fixture: CompileFixture<App>): Promise<CompileResult>;
31
+ boot(fixture: CompileFixture<App>): Promise<RealmBootHandle>;
32
+ };
33
+
34
+ interface TestFn<App> {
35
+ (name: string, fn: (t: RealmTestContext<App>) => void | Promise<void>): void;
36
+ /**
37
+ * Register a tier-2 e2e test. Auto-skipped unless `REALM_TEST_E2E=1`
38
+ * is set (the `realm-sdk test --e2e` flag does this for you).
39
+ * Inside the body, call `t.boot(fixture)` to bring the stack up.
40
+ */
41
+ e2e(name: string, fn: (t: RealmTestContext<App>) => void | Promise<void>): void;
42
+ }
43
+
44
+ interface DescribeArgs<App> {
45
+ test: TestFn<App>;
46
+ }
47
+
48
+ function describe<TSchema extends ConfigSchema>(
49
+ app: AppDefinition<TSchema>,
50
+ sourceUrl: string,
51
+ body: (args: DescribeArgs<AppDefinition<TSchema>>) => void,
52
+ ): void {
53
+ type App = AppDefinition<TSchema>;
54
+ let bundledCodePromise: Promise<string> | undefined;
55
+ const ensureBundle = (): Promise<string> => {
56
+ if (!bundledCodePromise) {
57
+ bundledCodePromise = (async () => {
58
+ const path = sourceUrl.startsWith("file://") ? fromFileUrl(sourceUrl) : sourceUrl;
59
+ // Merge the app dir rather than reading the entry alone: a multi-file
60
+ // app's entry can import relative helpers, which have to be inlined
61
+ // before bundle() sees them (it blocks relative specifiers). Same merge
62
+ // dev and Home's install run, so a test compiles what Home ships.
63
+ const source = (await mergeSurfaces(dirname(path), basename(path))).source;
64
+ return bundle(source);
65
+ })();
66
+ }
67
+ return bundledCodePromise;
68
+ };
69
+
70
+ // Drives the test lifecycle: a per-test cleanup queue that each `t.boot`
71
+ // stack registers against, drained LIFO once the body resolves or throws.
72
+ // Authors never call `teardown()` themselves.
73
+ const runWithCleanup = async (
74
+ denoCtx: Deno.TestContext,
75
+ fn: (t: RealmTestContext<App>) => void | Promise<void>,
76
+ ): Promise<void> => {
77
+ const cleanups: Array<() => Promise<void>> = [];
78
+ const ctx = denoCtx as RealmTestContext<App>;
79
+ ctx.compile = async (fixture) => {
80
+ const bundledCode = await ensureBundle();
81
+ return runCompile<App>({ bundledCode, appName: app.name, fixture });
82
+ };
83
+ ctx.boot = async (fixture) => {
84
+ const bundledCode = await ensureBundle();
85
+ const { handle, cleanup } = await runBoot<App>({
86
+ bundledCode,
87
+ appName: app.name,
88
+ fixture,
89
+ });
90
+ cleanups.push(cleanup);
91
+ return handle;
92
+ };
93
+ try {
94
+ await fn(ctx);
95
+ } finally {
96
+ // LIFO so a test that booted A then B sees B torn down first.
97
+ // Errors during cleanup are swallowed so a flaky teardown can't
98
+ // mask the real test failure.
99
+ for (let i = cleanups.length - 1; i >= 0; i--) {
100
+ await cleanups[i]!().catch(() => {});
101
+ }
102
+ }
103
+ };
104
+
105
+ const test = ((testName, fn) => {
106
+ Deno.test(testName, (denoCtx) => runWithCleanup(denoCtx, fn));
107
+ }) as TestFn<App>;
108
+
109
+ test.e2e = (testName, fn) => {
110
+ const register = Deno.env.get("REALM_TEST_E2E") === "1" ? Deno.test : Deno.test.ignore;
111
+ register(testName, (denoCtx) => runWithCleanup(denoCtx, fn));
112
+ };
113
+
114
+ body({ test });
115
+ }
116
+
117
+ export const Realm = { describe };
@@ -0,0 +1,11 @@
1
+ // `@yourrealm/sdk/test` — author-facing test harness.
2
+ //
3
+ // Authors import `Realm` and the inferred `ConfigOf` / `GrantsOf` types
4
+ // here. Home-side primitives (sanitizeGrants, the eval graph, etc.) stay
5
+ // on `/home` — this entry only carries what a realm.ts test needs.
6
+
7
+ export { Realm } from "./describe.ts";
8
+ export type { RealmTestContext } from "./describe.ts";
9
+ export type { RealmBootHandle } from "./boot.ts";
10
+ export type { CompileFixture, CompileResult, ConfigOf, GrantsOf, CompileIssue } from "./types.ts";
11
+ export type { WorldEntry, WorldState } from "../peer-world.ts";
@@ -0,0 +1,37 @@
1
+ // The `WorkerState.ingress` a harness-rendered box gets.
2
+ //
3
+ // `realm-sdk test --e2e` and `realm-sdk dev` both build a one-app WorkerState
4
+ // in process and hand it to a renderer. Neither has a Home, so the values come
5
+ // from the compile fixture's `management` block - the same Home identity
6
+ // `host.management()` resolves from, so a fixture that sets `baseDomain`
7
+ // describes a box whose ingress knows its own domain.
8
+ //
9
+ // `routedApps` is always empty: the harness boots one app and no proxy, so
10
+ // there is nothing routing to anything.
11
+
12
+ import type { IngressInput } from "../worker-protocol/ingress.ts";
13
+ import type { CompileFixture } from "./types.ts";
14
+
15
+ export function fixtureIngressInput(
16
+ management: CompileFixture<unknown>["management"],
17
+ ): IngressInput {
18
+ const baseDomain = management?.baseDomain ?? "";
19
+ const workerId = management?.workerId ?? "";
20
+ const homeSlug = management?.homeSlug ?? "";
21
+ const acmeDns = management?.acmeDns ?? null;
22
+ return {
23
+ routedApps: [],
24
+ // Null unless the fixture describes a full Home identity: `dashboardHost`
25
+ // names the box that runs Home, and a fixture with blank identity fields
26
+ // describes one that does not.
27
+ dashboardHost:
28
+ homeSlug && workerId && baseDomain ? `${homeSlug}.${workerId}.${baseDomain}` : null,
29
+ workerId,
30
+ baseDomain,
31
+ acmeDns: acmeDns ? { baseDomain, ...acmeDns } : null,
32
+ publicProviders: [],
33
+ cdnProviders: [],
34
+ // No operator to set them: the harness's statement matches no app.
35
+ companionFingerprints: [],
36
+ };
37
+ }