@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,207 @@
|
|
|
1
|
+
import { RealmCompileError } from "../errors.ts";
|
|
2
|
+
import type { LeafRefMarker, RefMarker } from "../ref.ts";
|
|
3
|
+
import type { AllowedProvider } from "../peer-registry.ts";
|
|
4
|
+
import { deriveAppPassword } from "../keys.ts";
|
|
5
|
+
|
|
6
|
+
export interface ResolutionContext {
|
|
7
|
+
consumerAppId: string;
|
|
8
|
+
/**
|
|
9
|
+
* HMAC key used to derive per-app passwords. Null when the caller (a
|
|
10
|
+
* test fixture, typically) doesn't plan to exercise derived/shared
|
|
11
|
+
* markers — the walker throws if it meets one anyway.
|
|
12
|
+
*/
|
|
13
|
+
derivationSeed: CryptoKey | null;
|
|
14
|
+
secrets: Record<string, string>;
|
|
15
|
+
/** `providerAppId` -> set of `serviceKey`s the consumer holds grants for. */
|
|
16
|
+
allowedProviders: Map<string, Set<string>>;
|
|
17
|
+
derivedCache: Map<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Batched second-shot for `apply()` callbacks. The walker hands over
|
|
20
|
+
* every deferred marker it meets (with its `input` already resolved)
|
|
21
|
+
* in one call, gets back the returned strings in the same order, and
|
|
22
|
+
* splices them into the tree. Unset when the caller doesn't plan to
|
|
23
|
+
* exercise deferred markers — the walker throws if it meets one
|
|
24
|
+
* anyway.
|
|
25
|
+
*/
|
|
26
|
+
runDeferred?: (items: Array<{ id: number; input: unknown }>) => Promise<string[]>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build the per-consumer allow-map from the set of granted providers.
|
|
31
|
+
* Each `(providerAppId, serviceKey)` pair becomes an entry the walker
|
|
32
|
+
* will accept when it meets a `shared` marker.
|
|
33
|
+
*/
|
|
34
|
+
export function buildAllowMap(entries: AllowedProvider[]): Map<string, Set<string>> {
|
|
35
|
+
const map = new Map<string, Set<string>>();
|
|
36
|
+
for (const e of entries) {
|
|
37
|
+
let set = map.get(e.appId);
|
|
38
|
+
if (!set) {
|
|
39
|
+
set = new Set<string>();
|
|
40
|
+
map.set(e.appId, set);
|
|
41
|
+
}
|
|
42
|
+
set.add(e.serviceKey);
|
|
43
|
+
}
|
|
44
|
+
return map;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isMarker(value: unknown): value is { __realmRef: RefMarker } {
|
|
48
|
+
return (
|
|
49
|
+
typeof value === "object" &&
|
|
50
|
+
value !== null &&
|
|
51
|
+
!Array.isArray(value) &&
|
|
52
|
+
"__realmRef" in value &&
|
|
53
|
+
typeof (value as { __realmRef: unknown }).__realmRef === "object" &&
|
|
54
|
+
(value as { __realmRef: unknown }).__realmRef !== null
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function resolveDerived(
|
|
59
|
+
appId: string,
|
|
60
|
+
name: string,
|
|
61
|
+
ctx: ResolutionContext,
|
|
62
|
+
): Promise<string> {
|
|
63
|
+
const cacheKey = `derived:${appId}:${name}`;
|
|
64
|
+
const cached = ctx.derivedCache.get(cacheKey);
|
|
65
|
+
if (cached) return cached;
|
|
66
|
+
if (!ctx.derivationSeed) {
|
|
67
|
+
throw new RealmCompileError({ kind: "eval", message: "derivationSeed is not configured" });
|
|
68
|
+
}
|
|
69
|
+
const hash = await deriveAppPassword(ctx.derivationSeed, appId, name);
|
|
70
|
+
ctx.derivedCache.set(cacheKey, hash);
|
|
71
|
+
return hash;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a sync leaf marker (derived/shared/secret) to its string
|
|
76
|
+
* value. Concat is handled in `walk`; deferred goes through the
|
|
77
|
+
* placeholder mechanism in `resolveRefs`.
|
|
78
|
+
*/
|
|
79
|
+
async function resolveLeaf(marker: LeafRefMarker, ctx: ResolutionContext): Promise<string> {
|
|
80
|
+
if (marker.kind === "derived") {
|
|
81
|
+
// `derived` markers only ever carry `__self__` when emitted by the SDK —
|
|
82
|
+
// the resolver rewrites them to the consumer's own app id. A foreign
|
|
83
|
+
// `appId` can only come from a hand-crafted payload in a malicious
|
|
84
|
+
// realm.ts, which would otherwise let the walker hash a victim app's
|
|
85
|
+
// derived secret with no grant check — the cross-app leak `shared` closes.
|
|
86
|
+
if (marker.appId !== "__self__") {
|
|
87
|
+
throw new RealmCompileError({
|
|
88
|
+
kind: "eval",
|
|
89
|
+
message: "derived ref carried a foreign appId",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return resolveDerived(ctx.consumerAppId, marker.name, ctx);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (marker.kind === "shared") {
|
|
96
|
+
const { providerAppId, serviceKey, name } = marker;
|
|
97
|
+
const allowed = ctx.allowedProviders.get(providerAppId);
|
|
98
|
+
if (!allowed || !allowed.has(serviceKey)) {
|
|
99
|
+
const missing = `${providerAppId}.${serviceKey}`;
|
|
100
|
+
throw new RealmCompileError({ kind: "permission", missing });
|
|
101
|
+
}
|
|
102
|
+
// Hash with the provider's identity so both sides agree on the value.
|
|
103
|
+
return resolveDerived(providerAppId, name, ctx);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// secret
|
|
107
|
+
const value = ctx.secrets[marker.path];
|
|
108
|
+
if (value == null) {
|
|
109
|
+
throw new RealmCompileError({
|
|
110
|
+
kind: "validation",
|
|
111
|
+
path: marker.path.split("."),
|
|
112
|
+
message: "unknown secret",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface DeferredPlaceholder {
|
|
119
|
+
readonly __deferredPlaceholder__: true;
|
|
120
|
+
readonly index: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isPlaceholder(value: unknown): value is DeferredPlaceholder {
|
|
124
|
+
return (
|
|
125
|
+
typeof value === "object" &&
|
|
126
|
+
value !== null &&
|
|
127
|
+
(value as { __deferredPlaceholder__?: boolean }).__deferredPlaceholder__ === true
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Walk `node` recursively and replace every `{__realmRef: ...}` marker
|
|
133
|
+
* with its resolved value. Plain values pass through unchanged.
|
|
134
|
+
*
|
|
135
|
+
* Markers that can't be resolved — unknown kind, missing secret, or a
|
|
136
|
+
* `shared` ref that the consumer has no grant for — throw.
|
|
137
|
+
*
|
|
138
|
+
* Deferred markers force a second pass: the walker first resolves every
|
|
139
|
+
* other marker plus the `input` of each deferred marker (leaving
|
|
140
|
+
* placeholders in the tree), then invokes `ctx.runDeferred` once with
|
|
141
|
+
* the collected items, then splices the returned strings back in. The
|
|
142
|
+
* wire schema forbids deferred markers inside concat parts and inside
|
|
143
|
+
* other deferred inputs, so the walker's contract is one flat pass +
|
|
144
|
+
* one batch.
|
|
145
|
+
*/
|
|
146
|
+
export async function resolveRefs<T>(node: T, ctx: ResolutionContext): Promise<T> {
|
|
147
|
+
const items: Array<{ id: number; input: unknown }> = [];
|
|
148
|
+
|
|
149
|
+
async function walk(n: unknown): Promise<unknown> {
|
|
150
|
+
if (n == null) return n;
|
|
151
|
+
if (typeof n !== "object") return n;
|
|
152
|
+
if (isMarker(n)) {
|
|
153
|
+
const marker = n.__realmRef;
|
|
154
|
+
if (marker.kind === "deferred") {
|
|
155
|
+
if (!ctx.runDeferred) {
|
|
156
|
+
throw new RealmCompileError({ kind: "eval", message: "runDeferred is not configured" });
|
|
157
|
+
}
|
|
158
|
+
const resolvedInput = await walk(marker.input);
|
|
159
|
+
const index = items.length;
|
|
160
|
+
items.push({ id: marker.id, input: resolvedInput });
|
|
161
|
+
return { __deferredPlaceholder__: true, index } satisfies DeferredPlaceholder;
|
|
162
|
+
}
|
|
163
|
+
if (marker.kind === "concat") {
|
|
164
|
+
const parts: string[] = [];
|
|
165
|
+
for (const p of marker.parts) {
|
|
166
|
+
parts.push(typeof p === "string" ? p : await resolveLeaf(p, ctx));
|
|
167
|
+
}
|
|
168
|
+
return parts.join("");
|
|
169
|
+
}
|
|
170
|
+
return resolveLeaf(marker, ctx);
|
|
171
|
+
}
|
|
172
|
+
if (Array.isArray(n)) {
|
|
173
|
+
const out: unknown[] = [];
|
|
174
|
+
for (const item of n) out.push(await walk(item));
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
const out: Record<string, unknown> = {};
|
|
178
|
+
for (const [k, v] of Object.entries(n as Record<string, unknown>)) {
|
|
179
|
+
out[k] = await walk(v);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const walked = await walk(node);
|
|
185
|
+
|
|
186
|
+
if (items.length === 0) return walked as T;
|
|
187
|
+
|
|
188
|
+
// Any failure inside runDeferred (user fn throwing, non-string return,
|
|
189
|
+
// sandbox crash) bubbles as RealmCompileError from runInSandbox, which
|
|
190
|
+
// already logged the underlying cause. Don't catch-and-relog here —
|
|
191
|
+
// that just adds a second line with an empty message.
|
|
192
|
+
const results = await ctx.runDeferred!(items);
|
|
193
|
+
|
|
194
|
+
function replace(n: unknown): unknown {
|
|
195
|
+
if (n == null) return n;
|
|
196
|
+
if (typeof n !== "object") return n;
|
|
197
|
+
if (isPlaceholder(n)) return results[n.index];
|
|
198
|
+
if (Array.isArray(n)) return n.map(replace);
|
|
199
|
+
const out: Record<string, unknown> = {};
|
|
200
|
+
for (const [k, v] of Object.entries(n as Record<string, unknown>)) {
|
|
201
|
+
out[k] = replace(v);
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return replace(walked) as T;
|
|
207
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Pure grant-building helpers shared between the server install path
|
|
2
|
+
// (apps-logic.ts) and the dashboard islands (InstallAppPanel,
|
|
3
|
+
// EditAppPanel). Kept standalone so importing it from a client island
|
|
4
|
+
// doesn't pull drizzle / db / sandbox into the browser bundle.
|
|
5
|
+
|
|
6
|
+
import type { Grants } from "./home.ts";
|
|
7
|
+
import type { SerializablePermission } from "./sandbox.ts";
|
|
8
|
+
|
|
9
|
+
interface GrantSelection {
|
|
10
|
+
/**
|
|
11
|
+
* Dotted paths of permissions whose toggle is ON. Everything else is
|
|
12
|
+
* denied.
|
|
13
|
+
*/
|
|
14
|
+
enabled: Set<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Per-path admin override, keyed by dotted permission path. Sparse:
|
|
17
|
+
* an entry only appears when the admin actively edited the value.
|
|
18
|
+
* `descriptorsToGrants` falls back to the descriptor's `perm.data`
|
|
19
|
+
* when an entry is missing, so the form never has to prefill — the
|
|
20
|
+
* default *is* the descriptor.
|
|
21
|
+
*
|
|
22
|
+
* Entries for presence-only kinds (host.gpu/egress/management,
|
|
23
|
+
* peers.*, remote.*) are ignored: those grants are flag-shaped.
|
|
24
|
+
*/
|
|
25
|
+
grantValues: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a Grants object from the typed permission descriptors plus the
|
|
30
|
+
* admin's toggle state. Drops anything not in `selection.enabled`. For
|
|
31
|
+
* data-carrying host kinds, uses `grantValues[path] ?? perm.data` —
|
|
32
|
+
* untouched fields fall back to the descriptor's proposed value;
|
|
33
|
+
* `??` only triggers on null/undefined, so an admin-cleared empty
|
|
34
|
+
* string passes through verbatim and gets rejected by sanitizeGrants.
|
|
35
|
+
*
|
|
36
|
+
* Presence-only host kinds (host.gpu, host.egress, host.management —
|
|
37
|
+
* descriptor `perm.data === true`) and the peers kinds emit `data: true`.
|
|
38
|
+
* remote.* lands as flag-only `{ kind }`.
|
|
39
|
+
*
|
|
40
|
+
* `host.silo` and `home.publicDomain` are the exceptions to the fallback:
|
|
41
|
+
* their descriptor and their grant carry different shapes, so they emit the
|
|
42
|
+
* operator's value alone.
|
|
43
|
+
*/
|
|
44
|
+
export function descriptorsToGrants(
|
|
45
|
+
permsFlat: Record<string, SerializablePermission>,
|
|
46
|
+
selection: GrantSelection,
|
|
47
|
+
): Grants {
|
|
48
|
+
const out: Grants = {};
|
|
49
|
+
for (const [path, perm] of Object.entries(permsFlat)) {
|
|
50
|
+
if (!selection.enabled.has(path)) continue;
|
|
51
|
+
const kind = perm.kind;
|
|
52
|
+
|
|
53
|
+
// peers.* / remote.* are presence-only at grant time: the descriptor says
|
|
54
|
+
// *what* to consume (remote.fetchUrl.url) but the runtime resolver pulls
|
|
55
|
+
// real values from world state / fetch, so the grant blob is just a flag.
|
|
56
|
+
if (kind.startsWith("peers.") || kind.startsWith("remote.")) {
|
|
57
|
+
out[path] = { kind } as Grants[string];
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (kind.startsWith("host.")) {
|
|
62
|
+
// Presence-only host kinds (host.gpu, host.egress, host.management)
|
|
63
|
+
// resolve their `.default(true)` schema to `perm.data === true`.
|
|
64
|
+
if (perm.data === true) {
|
|
65
|
+
out[path] = { kind, data: true };
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// host.silo's descriptor data and grant data are different shapes: the
|
|
69
|
+
// descriptor carries the author's affordance flags (`{volume:
|
|
70
|
+
// "readwrite"}`), the grant the operator's chosen Silo id. There is no
|
|
71
|
+
// sensible default Silo, so an unpicked one emits `undefined` and
|
|
72
|
+
// sanitizeGrants rejects it.
|
|
73
|
+
if (kind === "host.silo") {
|
|
74
|
+
out[path] = { kind, data: selection.grantValues[path] };
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Data-carrying: prefer admin override, fall back to descriptor.
|
|
78
|
+
out[path] = { kind, data: selection.grantValues[path] ?? perm.data };
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (kind.startsWith("home.")) {
|
|
83
|
+
// home.publicDomain is the home-family twin of host.silo: the descriptor
|
|
84
|
+
// carries nothing but the author's intent (`true`), the grant the
|
|
85
|
+
// operator's hostnames. Falling back to `perm.data` would send
|
|
86
|
+
// `data: true` for an enabled-but-untouched toggle, which sanitizeGrants
|
|
87
|
+
// rejects and the install 400s on. `undefined` lands on the grantSchema's
|
|
88
|
+
// `{ hosts: [] }` default: permission granted, no domain yet, no route.
|
|
89
|
+
if (kind === "home.publicDomain") {
|
|
90
|
+
out[path] = { kind, data: selection.grantValues[path] };
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Every other home kind carries data (the granted scopes) with no
|
|
94
|
+
// presence-only form: admin override first, descriptor scopes second.
|
|
95
|
+
out[path] = { kind, data: selection.grantValues[path] ?? perm.data };
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
package/src/grants.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Grants: stripped permission records (kind + data, no meta) plus the
|
|
2
|
+
// primitives the Home-side gate builds on. A "use" has the same shape as a
|
|
3
|
+
// grant: `collectUses` emits one per elevated thing a resolved service does
|
|
4
|
+
// (mounts, port binds, network joins), and `checkAppGrants` in security.ts
|
|
5
|
+
// matches those against the admin's grants keyed by permission descriptor
|
|
6
|
+
// path — a use with no matching grant drops the app. `GrantsOf` carries the
|
|
7
|
+
// same contract at the type level, so `grants.X.data` flows from the schema's
|
|
8
|
+
// I. Per-kind host mechanics (iterateUses / toValue / grantSchema) are tagged
|
|
9
|
+
// onto each factory in schema/host.ts via `makeHostable`; this file walks that
|
|
10
|
+
// namespace import and dispatches through the resulting registry.
|
|
11
|
+
|
|
12
|
+
import type { HomeService } from "./resolve.ts";
|
|
13
|
+
import * as host from "./schema/host.ts";
|
|
14
|
+
import * as peers from "./schema/peer.ts";
|
|
15
|
+
import * as home from "./schema/home.ts";
|
|
16
|
+
import type { Artifact } from "./schema/remote.ts";
|
|
17
|
+
import type { SiloGrants, SiloValue } from "./schema/host.ts";
|
|
18
|
+
import type { ResolveContext } from "./resolve-context.ts";
|
|
19
|
+
import { deriveHostRegistry, type Hostable, type HostHooks } from "./host-registry.ts";
|
|
20
|
+
import { deriveHomeRegistry, type Homeable, type HomeHooks } from "./home-registry.ts";
|
|
21
|
+
import type { Peerable } from "./peer-registry.ts";
|
|
22
|
+
import type {
|
|
23
|
+
HomePermissionKind,
|
|
24
|
+
HostPermissionKind,
|
|
25
|
+
PeerPermissionKind,
|
|
26
|
+
PermissionKind,
|
|
27
|
+
RemotePermissionKind,
|
|
28
|
+
} from "./permissions.ts";
|
|
29
|
+
|
|
30
|
+
// ----- Grant / Use shapes --------------------------------------------------
|
|
31
|
+
//
|
|
32
|
+
// Two authorization mechanisms ride this one type. Host grants approve a
|
|
33
|
+
// specific value (the port number, the directory path), matched against each
|
|
34
|
+
// service's elevated uses by JSON-equal comparison. Peer/remote grants are
|
|
35
|
+
// presence-only: the grant existing at all unlocks the resolver, and
|
|
36
|
+
// authorization flows through resolved values into `allowedNetworks` /
|
|
37
|
+
// `allowedAbsoluteHostPaths`. Only `HostGrant` carries `data`.
|
|
38
|
+
|
|
39
|
+
export type { RemotePermissionKind };
|
|
40
|
+
|
|
41
|
+
export type HostGrant = { readonly kind: HostPermissionKind; readonly data: unknown };
|
|
42
|
+
export type PeerGrant = { readonly kind: PeerPermissionKind };
|
|
43
|
+
export type RemoteGrant = { readonly kind: RemotePermissionKind };
|
|
44
|
+
// Home grants carry `data` (the granted scopes) like host grants — the third
|
|
45
|
+
// authorization shape on this union. Resolved values flow into
|
|
46
|
+
// `allowedAbsoluteHostPaths` (the per-app socket), not through the grant blob.
|
|
47
|
+
export type HomeGrant = { readonly kind: HomePermissionKind; readonly data: unknown };
|
|
48
|
+
export type Grant = HostGrant | PeerGrant | RemoteGrant | HomeGrant;
|
|
49
|
+
|
|
50
|
+
export type GrantOf<P> = P extends { readonly kind: infer K }
|
|
51
|
+
? K extends HostPermissionKind | HomePermissionKind
|
|
52
|
+
? P extends { readonly data: infer D }
|
|
53
|
+
? { readonly kind: K; readonly data: D }
|
|
54
|
+
: never
|
|
55
|
+
: K extends PeerPermissionKind | RemotePermissionKind
|
|
56
|
+
? { readonly kind: K }
|
|
57
|
+
: never
|
|
58
|
+
: never;
|
|
59
|
+
|
|
60
|
+
export type GrantsOf<S> = {
|
|
61
|
+
readonly [K in keyof S]: GrantOf<S[K]>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type Use = { readonly kind: PermissionKind; readonly data: unknown };
|
|
65
|
+
|
|
66
|
+
/** Admin-supplied grants keyed by dotted permission path. */
|
|
67
|
+
export type Grants = Record<string, Grant>;
|
|
68
|
+
|
|
69
|
+
// ----- Host registry (derived from schema/host.ts) ------------------------
|
|
70
|
+
|
|
71
|
+
const HOST_REGISTRY = deriveHostRegistry(host as Record<string, unknown>);
|
|
72
|
+
|
|
73
|
+
export function hostHooks(kind: string): HostHooks | undefined {
|
|
74
|
+
return HOST_REGISTRY.get(kind as `host.${string}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ----- Home registry (derived from schema/home.ts) ------------------------
|
|
78
|
+
|
|
79
|
+
const HOME_REGISTRY = deriveHomeRegistry(home as Record<string, unknown>);
|
|
80
|
+
|
|
81
|
+
export function homeHooks(kind: string): HomeHooks | undefined {
|
|
82
|
+
return HOME_REGISTRY.get(kind as `home.${string}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ----- toGrant: strip meta from a permission record ----------------------
|
|
86
|
+
//
|
|
87
|
+
// host.* and home.* grants carry `data` (the value the admin approved, the
|
|
88
|
+
// granted scopes). For peer/remote kinds the descriptor's data is metadata the
|
|
89
|
+
// consumer needs (e.g. fetchUrl.url) but not part of the grant blob — those
|
|
90
|
+
// grants are presence-only, so the returned shape has no `data` field.
|
|
91
|
+
|
|
92
|
+
export function toGrant<P extends { readonly kind: string; readonly data: unknown }>(
|
|
93
|
+
p: P,
|
|
94
|
+
): GrantOf<P> {
|
|
95
|
+
if (p.kind.startsWith("host.") || p.kind.startsWith("home.")) {
|
|
96
|
+
return { kind: p.kind, data: p.data } as GrantOf<P>;
|
|
97
|
+
}
|
|
98
|
+
return { kind: p.kind } as GrantOf<P>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Union the granted `home.apiAccess` scopes across an app's grants blob,
|
|
103
|
+
* deduped. An app can declare `apiAccess` at more than one descriptor path
|
|
104
|
+
* (e.g. one per capability it needs); Home resolves the app's *effective*
|
|
105
|
+
* scope set by collecting them all. Returns `[]` when the app has no
|
|
106
|
+
* `home.apiAccess` grant. Home reads this to authorize an app's call against
|
|
107
|
+
* what it was granted (the keyless app identity carries no scopes itself).
|
|
108
|
+
*/
|
|
109
|
+
export function collectHomeApiScopes(grants: Grants): string[] {
|
|
110
|
+
const scopes = new Set<string>();
|
|
111
|
+
for (const g of Object.values(grants)) {
|
|
112
|
+
if (isHomeGrant(g) && g.kind === "home.apiAccess") {
|
|
113
|
+
for (const scope of g.data as string[]) scopes.add(scope);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [...scopes];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ----- grant matching primitives (used by security.ts checkAppGrants) -----
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* JSON-equal comparison of grant.data and use.data. Home-side path-aware
|
|
123
|
+
* gate (security.ts checkAppGrants) reuses this — same comparator, no
|
|
124
|
+
* duplication.
|
|
125
|
+
*/
|
|
126
|
+
export function dataMatches(grantData: unknown, useData: unknown): boolean {
|
|
127
|
+
return JSON.stringify(grantData) === JSON.stringify(useData);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isHostGrant(g: Grant): g is HostGrant {
|
|
131
|
+
return g.kind.startsWith("host.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isHomeGrant(g: Grant): g is HomeGrant {
|
|
135
|
+
return g.kind.startsWith("home.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ----- toValue: grant → runtime value -------------------------------------
|
|
139
|
+
//
|
|
140
|
+
// Each kind's runtime shape is declared via its makeHostable / makePeerable
|
|
141
|
+
// hook in schema/{host,peer}.ts; toValue dispatches through the host registry
|
|
142
|
+
// at runtime. peers.* / remote.* return undefined — peers resolve via
|
|
143
|
+
// WorldState in peer-world.ts, remote is fetched at runtime.
|
|
144
|
+
//
|
|
145
|
+
// `ValueOf<G>` walks the same namespace imports: each export is tagged
|
|
146
|
+
// `Hostable<K, V>` / `Peerable<K, V>` where V is the inferred return type of
|
|
147
|
+
// `toValue` (host) or `resolve` (peer). Adding a kind is one declaration in
|
|
148
|
+
// schema/{host,peer}.ts; remote falls through to `unknown`.
|
|
149
|
+
|
|
150
|
+
type HostValueLookup = {
|
|
151
|
+
[N in keyof typeof host as (typeof host)[N] extends Hostable<infer K, unknown>
|
|
152
|
+
? K
|
|
153
|
+
: never]: (typeof host)[N] extends Hostable<HostPermissionKind, infer V> ? V : never;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
type PeerValueLookup = {
|
|
157
|
+
[N in keyof typeof peers as (typeof peers)[N] extends Peerable<infer K, unknown>
|
|
158
|
+
? K
|
|
159
|
+
: never]: (typeof peers)[N] extends Peerable<PeerPermissionKind, infer V> ? Readonly<V> : never;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
type HomeValueLookup = {
|
|
163
|
+
[N in keyof typeof home as (typeof home)[N] extends Homeable<infer K, unknown>
|
|
164
|
+
? K
|
|
165
|
+
: never]: (typeof home)[N] extends Homeable<HomePermissionKind, infer V> ? V : never;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// `host.silo` is the one kind whose value depends on the grant's *data*, not
|
|
169
|
+
// just its kind: the affordances an author declared decide which fields
|
|
170
|
+
// `config.silo` has. The kind-keyed lookup above can't express that (no
|
|
171
|
+
// higher-kinded types to hang `SiloValue<_>` on), so it gets one branch ahead
|
|
172
|
+
// of the generic path. It fires only with the flags in hand — a stored grant,
|
|
173
|
+
// whose data is the operator's chosen Silo id, lands on the wide
|
|
174
|
+
// `SiloResolved`.
|
|
175
|
+
export type ValueOf<G extends Grant> = G extends {
|
|
176
|
+
kind: "host.silo";
|
|
177
|
+
data: infer D extends SiloGrants;
|
|
178
|
+
}
|
|
179
|
+
? SiloValue<D>
|
|
180
|
+
: G["kind"] extends keyof HostValueLookup
|
|
181
|
+
? HostValueLookup[G["kind"]]
|
|
182
|
+
: G["kind"] extends keyof HomeValueLookup
|
|
183
|
+
? HomeValueLookup[G["kind"]]
|
|
184
|
+
: G["kind"] extends keyof PeerValueLookup
|
|
185
|
+
? PeerValueLookup[G["kind"]]
|
|
186
|
+
: G["kind"] extends "remote.artifact"
|
|
187
|
+
? Artifact
|
|
188
|
+
: unknown;
|
|
189
|
+
|
|
190
|
+
export function toValue<G extends Grant>(grant: G): ValueOf<G> {
|
|
191
|
+
if (isHomeGrant(grant)) {
|
|
192
|
+
const hooks = HOME_REGISTRY.get(grant.kind);
|
|
193
|
+
if (!hooks) return undefined as ValueOf<G>;
|
|
194
|
+
return hooks.toValue(grant.data) as ValueOf<G>;
|
|
195
|
+
}
|
|
196
|
+
if (!isHostGrant(grant)) return undefined as ValueOf<G>;
|
|
197
|
+
const hooks = HOST_REGISTRY.get(grant.kind);
|
|
198
|
+
if (!hooks) return undefined as ValueOf<G>;
|
|
199
|
+
return hooks.toValue(grant.data) as ValueOf<G>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Resolve a grant against Home's eval-time world.
|
|
204
|
+
*
|
|
205
|
+
* Same job as `toValue`, with the context added for the kinds whose grant
|
|
206
|
+
* doesn't hold the answer on its own - a `host.silo` grant is a Silo id, which
|
|
207
|
+
* needs the Silo record to mean anything. A kind that declares a `resolve` hook
|
|
208
|
+
* gets it; every other kind falls through to `toValue`, which is most of them.
|
|
209
|
+
*
|
|
210
|
+
* This is the single dispatch point: Home's `resolvePermissions` and the SDK
|
|
211
|
+
* test harness both come through here, so an app's own tests and Home run the
|
|
212
|
+
* same resolver rather than two copies that drift.
|
|
213
|
+
*/
|
|
214
|
+
export function resolveValue<G extends Grant>(
|
|
215
|
+
grant: G,
|
|
216
|
+
args: { declared: unknown; path: string; ctx: ResolveContext },
|
|
217
|
+
): ValueOf<G> {
|
|
218
|
+
// Two branches rather than one lookup because the registries are keyed by
|
|
219
|
+
// their own kind shapes; the predicates are what narrow `grant.data`.
|
|
220
|
+
if (isHostGrant(grant)) return viaHooks(HOST_REGISTRY.get(grant.kind), grant.data, args);
|
|
221
|
+
if (isHomeGrant(grant)) return viaHooks(HOME_REGISTRY.get(grant.kind), grant.data, args);
|
|
222
|
+
return toValue(grant);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** `resolve` when the kind declares one, `toValue` otherwise. */
|
|
226
|
+
function viaHooks<V>(
|
|
227
|
+
hooks: HostHooks | HomeHooks | undefined,
|
|
228
|
+
data: unknown,
|
|
229
|
+
args: { declared: unknown; path: string; ctx: ResolveContext },
|
|
230
|
+
): V {
|
|
231
|
+
if (!hooks) return undefined as V;
|
|
232
|
+
return (hooks.resolve ? hooks.resolve({ grant: data, ...args }) : hooks.toValue(data)) as V;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ----- collectUses: service spec → uses[] ---------------------------------
|
|
236
|
+
//
|
|
237
|
+
// Walks a resolved service through every host and home kind's iterateUses hook
|
|
238
|
+
// and emits one Use per elevated thing it does; Home feeds the result into
|
|
239
|
+
// checkAppGrants (security.ts). Adding a kind is a single declaration in
|
|
240
|
+
// schema/{host,home}.ts. Most home kinds yield nothing — the grant alone
|
|
241
|
+
// authorizes them — except `home.publicDomain`, whose per-service hostnames
|
|
242
|
+
// have to be matched against what the operator granted.
|
|
243
|
+
|
|
244
|
+
export function collectUses(svc: HomeService): Use[] {
|
|
245
|
+
const uses: Use[] = [];
|
|
246
|
+
for (const [kind, hooks] of HOST_REGISTRY) {
|
|
247
|
+
for (const data of hooks.iterateUses(svc)) {
|
|
248
|
+
// HOST_REGISTRY's keys are typed as the open `host.${string}` shape;
|
|
249
|
+
// at runtime they're whatever the schema/host.ts factories declared,
|
|
250
|
+
// which are exactly HostPermissionKind. Narrow at the boundary.
|
|
251
|
+
uses.push({ kind: kind as HostPermissionKind, data });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const [kind, hooks] of HOME_REGISTRY) {
|
|
255
|
+
for (const data of hooks.iterateUses(svc)) {
|
|
256
|
+
uses.push({ kind: kind as HomePermissionKind, data });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return uses;
|
|
260
|
+
}
|