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