@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
package/src/ref.ts ADDED
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Brand symbols for the two ref categories. Distinct symbols (rather
3
+ * than a shared one) make `Ref<T>` and `Deferred<T>` non-assignable to
4
+ * each other at the type level — `interpolate` accepts the first but
5
+ * not the second, so the broken composition `interpolate\`${apply(...)}\``
6
+ * fails to typecheck instead of producing a malformed marker tree.
7
+ */
8
+ export const REF_SYMBOL = Symbol.for("realm.ref");
9
+ const DEFERRED_SYMBOL = Symbol.for("realm.deferred");
10
+
11
+ /**
12
+ * Sync leaf markers — single-step lookups the walker resolves with no
13
+ * user-code round-trip. Home maps marker → string against a grant-
14
+ * scoped allow-map.
15
+ */
16
+ export type LeafRefMarker =
17
+ | { kind: "derived"; appId: string; name: string }
18
+ | { kind: "shared"; providerAppId: string; serviceKey: string; name: string }
19
+ | { kind: "secret"; path: string };
20
+
21
+ /**
22
+ * Wire-shape markers `Ref<T>` carries. Either a single leaf or a flat
23
+ * concat of leaves. Two structural invariants the wire schema enforces:
24
+ *
25
+ * 1. concat parts contain only leaves — no nested concat (interpolate
26
+ * flattens at build time) and no deferred sub-parts (interpolate
27
+ * rejects `Deferred<T>` at the type level).
28
+ * 2. `Deferred<T>` is a separate brand entirely — never appears here.
29
+ *
30
+ * Together these mean the walker's concat handling is plain sync
31
+ * recursion over leaves, not a two-pass placeholder dance.
32
+ */
33
+ export type SyncRefMarker = LeafRefMarker | { kind: "concat"; parts: (string | LeafRefMarker)[] };
34
+
35
+ /**
36
+ * Wire shape for `apply()`. Distinct from `SyncRefMarker` because the
37
+ * walker has to register a placeholder and round-trip through a second
38
+ * sandbox eval to invoke the user fn.
39
+ */
40
+ export type DeferredRefMarker = { kind: "deferred"; id: number; input: unknown };
41
+
42
+ export type RefMarker = SyncRefMarker | DeferredRefMarker;
43
+
44
+ /**
45
+ * Sync ref — secrets, derived/shared passwords, concat templates. The
46
+ * walker resolves them in one pass with no user-code round-trip. Always
47
+ * the type accepted by `interpolate`.
48
+ */
49
+ export interface Ref<T> {
50
+ readonly [REF_SYMBOL]: true;
51
+ toJSON(): { __realmRef: SyncRefMarker } | T;
52
+ toString(): never;
53
+ }
54
+
55
+ /**
56
+ * Deferred ref — produced only by `apply()`. Resolution requires a
57
+ * second sandbox call to invoke the user fn, so deferred refs cannot
58
+ * compose into `interpolate` (a deferred sub-part inside a concat would
59
+ * force the walker to mix synchronous and second-pass resolution mid-
60
+ * concat, which the wire schema forbids). Wrap affix logic inside the
61
+ * deferred fn instead.
62
+ */
63
+ export interface Deferred<T> {
64
+ readonly [DEFERRED_SYMBOL]: true;
65
+ // Phantom `| T` parallels `Ref<T>` so `Deferred<string>` and
66
+ // `Deferred<number>` brand distinctly at the type level. Always the
67
+ // marker shape at runtime — the user fn's return value is spliced in
68
+ // by the walker, never produced by toJSON.
69
+ toJSON(): { __realmRef: DeferredRefMarker } | T;
70
+ toString(): never;
71
+ }
72
+
73
+ /**
74
+ * Anything that becomes a `T` at deploy time — sync ref or deferred.
75
+ * Use this for field types that accept either (env values, file
76
+ * contents, etc.). `interpolate` is the exception: it accepts `Ref<T>`
77
+ * only, since deferred values can't sit inside a concat template.
78
+ */
79
+ export type Resolvable<T> = Ref<T> | Deferred<T>;
80
+
81
+ function isRef(value: unknown): value is Ref<string> {
82
+ return typeof value === "object" && value !== null && REF_SYMBOL in value;
83
+ }
84
+
85
+ /**
86
+ * True if `value` holds a ref anywhere in its tree, in either form: a
87
+ * branded `Ref` (from author helpers like `r.stableSecret` / `interpolate`)
88
+ * or a bare `{__realmRef}` marker (what a config secret value already is by
89
+ * the time `getDesiredState` runs — symbols don't survive the sandbox's
90
+ * JSON boundary). The serializers (toJson/toYaml/toIni/toXml) use this to
91
+ * choose the eager path (no refs → plain string) over the deferred path
92
+ * (refs → resolve via apply(), then serialize).
93
+ */
94
+ export function containsRef(value: unknown): boolean {
95
+ if (isRef(value) || isRefMarker(value)) return true;
96
+ if (Array.isArray(value)) return value.some(containsRef);
97
+ if (typeof value === "object" && value !== null) {
98
+ for (const v of Object.values(value as Record<string, unknown>)) {
99
+ if (containsRef(v)) return true;
100
+ }
101
+ }
102
+ return false;
103
+ }
104
+
105
+ export function isRefMarker(value: unknown): value is { __realmRef: RefMarker } {
106
+ return (
107
+ typeof value === "object" &&
108
+ value !== null &&
109
+ "__realmRef" in value &&
110
+ typeof (value as { __realmRef: unknown }).__realmRef === "object"
111
+ );
112
+ }
113
+
114
+ function throwOnToString(): never {
115
+ throw new Error(
116
+ "Refs cannot be stringified directly. Use interpolate`...` for templates " +
117
+ "or pass the ref through as an env/command/volume value.",
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Marker for a `User` field read inside `getTrustedHeaders`. Unlike every
123
+ * other ref it never reaches the worker wire or the resolve-refs walker:
124
+ * `createApp`'s sampler reads it at deploy time to build the static
125
+ * header→field map, and the ForwardAuth verify hop substitutes the real
126
+ * value per request. Kept out of `SyncRefMarker` for exactly that reason —
127
+ * the wire schema and walker never have to know about it.
128
+ */
129
+ type IdentityRefMarker = { kind: "identity"; field: "username" | "email" };
130
+
131
+ /**
132
+ * Build the `Ref<string>` handed to `getTrustedHeaders` for `user.username` /
133
+ * `user.email`. At deploy time there is no real user, so the value is a
134
+ * deferred marker the verify hop fills in per request — the same "string
135
+ * not known until later" idiom as `db.url`. Composes through `interpolate`
136
+ * like any other ref (the sampler resolves the resulting concat).
137
+ */
138
+ export function identityRef(field: IdentityRefMarker["field"]): Ref<string> {
139
+ // The identity marker is deliberately outside `SyncRefMarker` (it never
140
+ // reaches the wire), so toJSON doesn't structurally match `Ref<string>` -
141
+ // cast through `unknown`. Runtime shape is identical to every other ref.
142
+ return {
143
+ [REF_SYMBOL]: true,
144
+ toJSON: () => ({ __realmRef: { kind: "identity", field } }),
145
+ toString: throwOnToString,
146
+ } as unknown as Ref<string>;
147
+ }
148
+
149
+ /**
150
+ * Build a sync ref that serializes as `{__realmRef: marker}`.
151
+ */
152
+ export function ref(marker: SyncRefMarker): Ref<string> {
153
+ return {
154
+ [REF_SYMBOL]: true,
155
+ toJSON: () => ({ __realmRef: marker }),
156
+ toString: throwOnToString,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Build a sync ref whose toJSON resolves to a plain string at
162
+ * serialization time. Used for values that don't need Home-side
163
+ * resolution (e.g. the service's DNS name — a literal known once
164
+ * `_setServiceKey` runs, which happens before `getServiceDefinition`).
165
+ */
166
+ export function stringRef(resolve: () => string): Ref<string> {
167
+ return {
168
+ [REF_SYMBOL]: true,
169
+ toJSON: resolve,
170
+ toString: throwOnToString,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Build a deferred ref. Internal — `apply()` in apply.ts is the public
176
+ * entry point; this helper centralizes the brand + toJSON shape.
177
+ */
178
+ export function deferredRef(marker: DeferredRefMarker): Deferred<string> {
179
+ return {
180
+ [DEFERRED_SYMBOL]: true,
181
+ toJSON: () => ({ __realmRef: marker }),
182
+ toString: throwOnToString,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Tagged template literal that handles refs.
188
+ *
189
+ * Literal parts and plain values fold in at serialization time. Sync
190
+ * refs keep their marker; string-producing refs fold to strings. The
191
+ * result is always a single concat marker with parts in source order
192
+ * (adjacent strings are not pre-joined — the walker collapses them).
193
+ *
194
+ * `Deferred<string>` is rejected at the type level: a deferred sub-part
195
+ * inside a concat would need to mix sync + second-pass resolution mid-
196
+ * template, and the wire schema forbids it. Move affix logic inside the
197
+ * apply callback: `apply(input, s => \`prefix-${s}\`)` instead of
198
+ * `interpolate\`prefix-${apply(input, s => s)}\``.
199
+ *
200
+ * Usage:
201
+ * interpolate`postgres://${db.user}:${db.password}@${db.host}:${db.port}/${db.database}`
202
+ */
203
+ function interpolate(
204
+ strings: TemplateStringsArray,
205
+ ...values: (string | Ref<string> | number)[]
206
+ ): Ref<string> {
207
+ return {
208
+ [REF_SYMBOL]: true,
209
+ toJSON() {
210
+ const parts: (string | LeafRefMarker)[] = [];
211
+ let allStrings = true;
212
+ for (let i = 0; i < strings.length; i++) {
213
+ const literal = strings[i]!;
214
+ if (literal.length > 0) parts.push(literal);
215
+ if (i < values.length) {
216
+ const val = values[i];
217
+ // Three runtime shapes: a `Ref<string>`, whose marker comes off
218
+ // `.toJSON()`; a plain `{__realmRef: ...}` object — what a Ref
219
+ // looks like after crossing the host→sandbox JSON boundary, which
220
+ // realm.ts authors hit interpolating an `r.input.secret(...)`
221
+ // field; or anything else, which coerces to a string.
222
+ let marker: SyncRefMarker | null = null;
223
+ if (isRef(val)) {
224
+ const resolved = val.toJSON();
225
+ if (typeof resolved === "object" && resolved !== null && "__realmRef" in resolved) {
226
+ marker = (resolved as { __realmRef: SyncRefMarker }).__realmRef;
227
+ } else {
228
+ parts.push(String(resolved));
229
+ }
230
+ } else if (isRefMarker(val)) {
231
+ // Runtime guard: a deferred marker can only land here via a
232
+ // hand-crafted `as unknown as string` cast (the type rejects
233
+ // proper `Deferred<string>`). Reject it loudly so the wire
234
+ // schema isn't the only line of defense.
235
+ const m = val.__realmRef;
236
+ if (m.kind === "deferred") {
237
+ throw new Error(
238
+ "interpolate cannot accept a deferred ref; move affix logic into apply()",
239
+ );
240
+ }
241
+ marker = m;
242
+ } else {
243
+ parts.push(String(val));
244
+ }
245
+ if (marker) {
246
+ // Flatten nested concats so the walker sees a single level.
247
+ // `interpolate` is the only path that produces a concat, so an
248
+ // already-concat sub-marker came from another one upstream.
249
+ if (marker.kind === "concat") {
250
+ for (const p of marker.parts) {
251
+ parts.push(p);
252
+ if (typeof p !== "string") allStrings = false;
253
+ }
254
+ } else {
255
+ parts.push(marker);
256
+ allStrings = false;
257
+ }
258
+ }
259
+ }
260
+ }
261
+ // When every part is already a plain string the template has no
262
+ // deferred resolution to do — collapse it to a literal so it can
263
+ // land in strict string-typed fields (e.g. volume hostPath).
264
+ if (allStrings) return (parts as string[]).join("");
265
+ return { __realmRef: { kind: "concat", parts } };
266
+ },
267
+ toString: throwOnToString,
268
+ };
269
+ }
270
+
271
+ export { interpolate };
@@ -0,0 +1,33 @@
1
+ // Per-kind tagging for remote.* permissions.
2
+ //
3
+ // Remote kinds carry no Home-side dispatch hooks (no iterateUses /
4
+ // extract) — Home handles fetchUrl + artifact through dedicated endpoints
5
+ // rather than by walking a registry. The factory tags only kind + meta,
6
+ // enough for derivePermissionMeta and permissions.ts's type-level kind
7
+ // deriver to pick them up.
8
+
9
+ import type { z } from "zod";
10
+ import type { Builder } from "./shared.ts";
11
+ import {
12
+ tagPermissionMeta,
13
+ type PermissionMeta,
14
+ type PermissionMetaTagged,
15
+ } from "./permission-meta.ts";
16
+
17
+ // Loose constraint used inside this module. The closed
18
+ // `RemotePermissionKind` is derived in permissions.ts.
19
+ type RemoteKindShape = `remote.${string}`;
20
+
21
+ /**
22
+ * Bind an inner Builder (basePermission or schemablePermission) and
23
+ * return a factory-builder that produces tagged remote-permission
24
+ * factories. Mirrors makeHostable / makePeerable.
25
+ */
26
+ export function makeRemotable<BaseR extends object, KindUnion extends RemoteKindShape>(
27
+ inner: Builder<BaseR, "perm", KindUnion>,
28
+ ) {
29
+ return <S extends z.ZodType, K extends KindUnion>(kind: K, schema: S, meta: PermissionMeta) => {
30
+ const factory = inner(kind, schema);
31
+ return tagPermissionMeta(factory, kind, meta) as typeof factory & PermissionMetaTagged<K>;
32
+ };
33
+ }
@@ -0,0 +1,110 @@
1
+ // Partition the host permission kinds an app declares into "required"
2
+ // vs "optional" sets. Required ⇒ the permission is at the root (or
3
+ // nested inside non-optional tabs/sections only) AND doesn't carry
4
+ // `.optional()` on its leaf meta. Anything else is optional.
5
+ //
6
+ // Two consumers: Home's validation gates (apps-logic / apps-recompute)
7
+ // drop workers whose advertised capabilities don't cover an app's required
8
+ // kinds, and the install panel grays out optional permissions whose kind no
9
+ // worker can satisfy. It lives in the SDK so server eval and dashboard
10
+ // islands consume the same rule.
11
+
12
+ import type { ConfigFieldTree, SerializablePermission } from "./sandbox.ts";
13
+ import type { HostPermissionKind } from "./permissions.ts";
14
+ import type { HomeApp } from "./resolve.ts";
15
+
16
+ function isHostKind(kind: string): kind is HostPermissionKind {
17
+ return kind.startsWith("host.");
18
+ }
19
+
20
+ /**
21
+ * Dotted paths of permission leaves that live inside an
22
+ * optional-section (or optional-tab) subtree. Schema-only — runtime
23
+ * `values` aren't consulted here, declaration-time optionality is what
24
+ * decides whether a permission counts as required for capability
25
+ * gating.
26
+ */
27
+ function pathsUnderOptionalAncestor(schema: ConfigFieldTree): Set<string> {
28
+ const optionalPrefixes: string[] = [];
29
+ function walk(fields: ConfigFieldTree, prefix: string) {
30
+ for (const [name, field] of Object.entries(fields)) {
31
+ const path = prefix ? `${prefix}.${name}` : name;
32
+ if (field.type === "section" || field.type === "tab") {
33
+ if (field.optional) {
34
+ optionalPrefixes.push(path);
35
+ // Don't recurse: every descendant is already covered by
36
+ // this prefix.
37
+ continue;
38
+ }
39
+ walk(field.fields, path);
40
+ }
41
+ }
42
+ }
43
+ walk(schema, "");
44
+ return new Set(optionalPrefixes);
45
+ }
46
+
47
+ function hasOptionalAncestor(path: string, optionalPrefixes: Set<string>): boolean {
48
+ for (const prefix of optionalPrefixes) {
49
+ if (path === prefix || path.startsWith(prefix + ".")) return true;
50
+ }
51
+ return false;
52
+ }
53
+
54
+ export interface PartitionedHostKinds {
55
+ required: HostPermissionKind[];
56
+ optional: HostPermissionKind[];
57
+ }
58
+
59
+ /**
60
+ * Given a permsFlat map plus the configFields tree, split the host
61
+ * kinds the app declares into required vs optional. A kind is required
62
+ * iff at least one declaration of that kind is *both* leaf-required
63
+ * (`meta.isOptional !== true`) AND has no optional ancestor section/tab
64
+ * along its dotted path. Otherwise the kind is optional.
65
+ *
66
+ * Both arrays are deduped and stable-sorted so the gate's missing-kinds
67
+ * messages render in a predictable order.
68
+ */
69
+ export function partitionHostKinds(
70
+ permissionsFlat: Record<string, SerializablePermission>,
71
+ configFields: ConfigFieldTree,
72
+ ): PartitionedHostKinds {
73
+ const optionalPrefixes = pathsUnderOptionalAncestor(configFields);
74
+ const required = new Set<HostPermissionKind>();
75
+ const optional = new Set<HostPermissionKind>();
76
+ for (const [path, perm] of Object.entries(permissionsFlat)) {
77
+ if (!isHostKind(perm.kind)) continue;
78
+ const leafOptional = (perm.meta as { isOptional?: boolean }).isOptional === true;
79
+ const ancestorOptional = hasOptionalAncestor(path, optionalPrefixes);
80
+ if (!leafOptional && !ancestorOptional) {
81
+ required.add(perm.kind);
82
+ } else {
83
+ optional.add(perm.kind);
84
+ }
85
+ }
86
+ // A kind that's required at one path and optional at another is
87
+ // required overall — required wins.
88
+ for (const k of required) optional.delete(k);
89
+ return {
90
+ required: [...required].sort(),
91
+ optional: [...optional].sort(),
92
+ };
93
+ }
94
+
95
+ /**
96
+ * State-based capability check: any service mounting a volume (relative
97
+ * or absolute) needs the worker to advertise the `host.volumes`
98
+ * capability — whether the worker can hold persistent files at all. A
99
+ * worker without it represents an ephemeral runtime (think sandbox VM,
100
+ * k8s emptyDir-only) and can't run any app that persists state.
101
+ *
102
+ * Returns true iff at least one service in the resolved state has a
103
+ * non-empty `volumes` array.
104
+ */
105
+ export function stateRequiresVolumes(state: HomeApp): boolean {
106
+ for (const svc of Object.values(state.services)) {
107
+ if (svc.volumes && svc.volumes.length > 0) return true;
108
+ }
109
+ return false;
110
+ }
@@ -0,0 +1,111 @@
1
+ // The world a permission resolver reads from.
2
+ //
3
+ // A resolver turns "the author declared this, the operator granted that"
4
+ // into the value the app reads out of `config`. For most kinds the grant
5
+ // already holds the answer and `toValue` is the whole resolver; the rest
6
+ // need facts only Home knows at eval time: which Silos exist, where the
7
+ // worker is, what the other members of a Silo publish.
8
+ //
9
+ // Everything here is plain data or a pure lookup closed over plain data,
10
+ // never a callback into Home - the resolvers live in the SDK, which cannot
11
+ // import Home. The caller resolves every database read, fetch and key
12
+ // derivation up front, which is what keeps resolvers synchronous.
13
+ //
14
+ // Home builds one per app in `evalApps`; the SDK test harness builds the
15
+ // same shape from fixtures, so both run the same resolvers.
16
+
17
+ /**
18
+ * A Silo as a resolver reads it. Deliberately structural: Home's own row
19
+ * (`server/silos.ts`) is assignable without the SDK knowing the row type,
20
+ * and the fields a resolver has no business reading (`saltId`,
21
+ * `setupAckAt`) stay off it.
22
+ */
23
+ export interface ResolveSilo {
24
+ readonly id: string;
25
+ readonly name: string;
26
+ /** The worker row id the Silo is pinned to - what placement is checked against. */
27
+ readonly workerId: string;
28
+ /** That worker's slug, so a refusal can name the box the operator knows. */
29
+ readonly workerSlug: string;
30
+ readonly hostPath: string;
31
+ readonly containerPath: string;
32
+ readonly gid: number;
33
+ }
34
+
35
+ /** One service name published into a Silo by another member. */
36
+ export interface SiloPublication {
37
+ readonly port: number;
38
+ readonly appId: string;
39
+ }
40
+
41
+ /**
42
+ * The Home identity `host.management` resolves from. Structural, like
43
+ * `ResolveSilo`: Home's own `HomeSettings` (read from installer env) is
44
+ * assignable without the SDK importing it.
45
+ */
46
+ export interface ResolveHomeSettings {
47
+ readonly baseDomain: string;
48
+ readonly homeSlug: string;
49
+ readonly acmeDns: {
50
+ readonly serverUrl: string;
51
+ readonly apiKey: string;
52
+ readonly ca: string;
53
+ readonly caRootPem: string;
54
+ } | null;
55
+ }
56
+
57
+ export interface ResolveContext {
58
+ /** The app being evaluated. */
59
+ readonly appId: string;
60
+ /**
61
+ * The worker this evaluation targets. `id` is the slug an author reads as
62
+ * `worker.id`; `internalId` is the row id, absent when there is no concrete
63
+ * worker (previews, sweeps, loader cold starts), which is why placement
64
+ * checks guard on it rather than assuming it.
65
+ */
66
+ readonly worker: { readonly id: string; readonly internalId?: string };
67
+ /**
68
+ * Home's own identity, from installer env. `host.management` resolves the
69
+ * dashboard URL pieces and the acme-dns credentials out of this.
70
+ */
71
+ readonly home: ResolveHomeSettings;
72
+ /** Every Silo the operator has defined. Read once per eval, not per app. */
73
+ readonly silos: readonly ResolveSilo[];
74
+ /**
75
+ * One Silo's merged service directory, with the evaluating app's own
76
+ * publications excluded. A function rather than a map because it is derived
77
+ * per app from state that grows as the app's peers evaluate.
78
+ */
79
+ readonly siloDirectory: (siloId: string) => ReadonlyMap<string, SiloPublication>;
80
+ /**
81
+ * Per-Silo API keys, derived up front (HKDF off REALM_SECRET) so a resolver
82
+ * that needs one stays synchronous.
83
+ */
84
+ readonly siloApiKeys: ReadonlyMap<string, string>;
85
+ /**
86
+ * Resolution mode, not a fact about the world like the rest of this context:
87
+ * the caller is asking what an app WOULD ship, with every permission granted
88
+ * sight-unseen and nothing actually chosen. Home's install preview is the
89
+ * only caller.
90
+ *
91
+ * The one resolver that reads it is `host.silo`; see `speculative-silo.ts`
92
+ * for why it is the only permission that needs a mode at all. Setting it
93
+ * anywhere an app can deploy would put a fabricated directory on the wire,
94
+ * so `evalApps` refuses it unless the eval is also `ephemeral`.
95
+ */
96
+ readonly speculative?: boolean;
97
+ }
98
+
99
+ /**
100
+ * What a `resolve` hook receives. `grant` is the operator's side (the same
101
+ * data `toValue` gets), `declared` is the author's side off the descriptor.
102
+ * Neither alone is enough for the kinds that need a resolver: the author
103
+ * says what they want, the operator says from where.
104
+ */
105
+ export interface ResolveArgs<TGrant = unknown> {
106
+ readonly grant: TGrant;
107
+ readonly declared: unknown;
108
+ /** Dotted config path of this permission. Error messages name it. */
109
+ readonly path: string;
110
+ readonly ctx: ResolveContext;
111
+ }