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