@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,318 @@
1
+ // Fixture and result types for the realm.ts test harness.
2
+ //
3
+ // `ConfigOf<App>` walks the app's configSchema and emits the shape an
4
+ // author writes in tests: every input leaf, with secrets typed as plain
5
+ // `string` (the harness wraps them into ref markers before the sandbox
6
+ // runs). Permission leaves are excluded — those go in `grants`.
7
+ //
8
+ // `GrantsOf<App>` mirrors the runtime `Grants` shape: a flat record keyed
9
+ // by the schema paths that declared a permission, valued `{kind, data}` of
10
+ // that permission's kind. `r.X().optional()` permissions become optional
11
+ // keys in the record.
12
+
13
+ import type { z } from "zod";
14
+ import type { CompileIssue } from "../errors.ts";
15
+ import type { CompleteInput, CompletePermission } from "../factory.ts";
16
+ import type { AppDefinition, PreviewContext } from "../create-app.ts";
17
+ import type { ConfigSchema } from "../types.ts";
18
+ import type { TabRecord, SectionRecord } from "../schema/nodes.ts";
19
+ import type { HomeApp } from "../resolve.ts";
20
+ import type { ResolvedInfoEntry } from "../info.ts";
21
+ import type { GrantOf } from "../grants.ts";
22
+ import type { PublicDomainGrantData } from "../schema/public-domain.ts";
23
+ import type { WorldState } from "../peer-world.ts";
24
+ import type { Violation } from "../security.ts";
25
+
26
+ type FixtureInputValueOf<I> = I extends { readonly kind: "input.text" }
27
+ ? string
28
+ : I extends { readonly kind: "input.secret" }
29
+ ? string
30
+ : I extends { readonly kind: "input.number" }
31
+ ? number
32
+ : I extends { readonly kind: "input.boolean" }
33
+ ? boolean
34
+ : I extends { readonly kind: "input.enum"; readonly schema: z.ZodType<infer V> }
35
+ ? V
36
+ : never;
37
+
38
+ type IsOptional<N> = N extends { readonly meta: { readonly isOptional: true } } ? true : false;
39
+
40
+ type FixtureValueOf<N> = N extends CompletePermission
41
+ ? never
42
+ : N extends CompleteInput
43
+ ? FixtureInputValueOf<N>
44
+ : N extends TabRecord<infer C>
45
+ ? FixtureTree<C>
46
+ : N extends SectionRecord<infer C>
47
+ ? FixtureTree<C>
48
+ : never;
49
+
50
+ type IsPermission<N> = N extends CompletePermission ? true : false;
51
+
52
+ type FixtureTree<C> = {
53
+ readonly [K in keyof C as IsPermission<C[K]> extends true
54
+ ? never
55
+ : IsOptional<C[K]> extends true
56
+ ? never
57
+ : K]: FixtureValueOf<C[K]>;
58
+ } & {
59
+ readonly [K in keyof C as IsPermission<C[K]> extends true
60
+ ? never
61
+ : IsOptional<C[K]> extends true
62
+ ? K
63
+ : never]?: FixtureValueOf<C[K]>;
64
+ };
65
+
66
+ export type ConfigOf<App> =
67
+ App extends AppDefinition<infer S extends ConfigSchema> ? FixtureTree<S> : never;
68
+
69
+ // ----- GrantsOf -----------------------------------------------------------
70
+ //
71
+ // Walks the schema and collects every CompletePermission leaf, keyed by
72
+ // dotted path. Each value is `GrantOf<Leaf>` — `kind` narrowed to that
73
+ // permission's literal kind, `data` to the kind's declared data shape.
74
+
75
+ type PermissionPaths<S, Prefix extends string = ""> =
76
+ S extends Record<string, unknown>
77
+ ? {
78
+ [K in keyof S & string]: S[K] extends CompletePermission
79
+ ? `${Prefix}${K}`
80
+ : S[K] extends TabRecord<infer C>
81
+ ? PermissionPaths<C, `${Prefix}${K}.`>
82
+ : S[K] extends SectionRecord<infer C>
83
+ ? PermissionPaths<C, `${Prefix}${K}.`>
84
+ : never;
85
+ }[keyof S & string]
86
+ : never;
87
+
88
+ type PermissionAtPath<S, P extends string> = P extends `${infer Head}.${infer Rest}`
89
+ ? Head extends keyof S
90
+ ? S[Head] extends TabRecord<infer C>
91
+ ? PermissionAtPath<C, Rest>
92
+ : S[Head] extends SectionRecord<infer C>
93
+ ? PermissionAtPath<C, Rest>
94
+ : never
95
+ : never
96
+ : P extends keyof S
97
+ ? S[P] extends CompletePermission
98
+ ? S[P]
99
+ : never
100
+ : never;
101
+
102
+ type RequiredPermissionPaths<S> = {
103
+ [P in PermissionPaths<S>]: PermissionAtPath<S, P> extends infer Leaf
104
+ ? Leaf extends CompletePermission
105
+ ? IsOptional<Leaf> extends true
106
+ ? never
107
+ : P
108
+ : never
109
+ : never;
110
+ }[PermissionPaths<S>];
111
+
112
+ type OptionalPermissionPaths<S> = {
113
+ [P in PermissionPaths<S>]: PermissionAtPath<S, P> extends infer Leaf
114
+ ? Leaf extends CompletePermission
115
+ ? IsOptional<Leaf> extends true
116
+ ? P
117
+ : never
118
+ : never
119
+ : never;
120
+ }[PermissionPaths<S>];
121
+
122
+ // The stored-grant shape for a leaf. Identical to `GrantOf` for every kind
123
+ // whose descriptor data and grant data are the same shape. Two diverge, both
124
+ // because the operator supplies the elevated value, so the descriptor carries
125
+ // only the author's ask:
126
+ //
127
+ // - `host.silo`: the record's data is the author's asked flags (they drive
128
+ // the `config.silo` narrowing), the stored grant is the Silo's id.
129
+ // - `home.publicDomain`: the record's data is `true`, the stored grant is
130
+ // the operator's hostnames plus their redirect choice.
131
+ //
132
+ // Without these branches a fixture's `grants` entry types as `data: true`.
133
+ type StoredGrantOf<P> =
134
+ GrantOf<P> extends { readonly kind: "host.silo" }
135
+ ? { readonly kind: "host.silo"; readonly data: string }
136
+ : GrantOf<P> extends { readonly kind: "home.publicDomain" }
137
+ ? { readonly kind: "home.publicDomain"; readonly data: PublicDomainGrantData }
138
+ : GrantOf<P>;
139
+
140
+ type FlatGrants<S> = {
141
+ readonly [P in RequiredPermissionPaths<S>]: StoredGrantOf<PermissionAtPath<S, P>>;
142
+ } & {
143
+ readonly [P in OptionalPermissionPaths<S>]?: StoredGrantOf<PermissionAtPath<S, P>>;
144
+ };
145
+
146
+ export type GrantsOf<App> =
147
+ App extends AppDefinition<infer S extends ConfigSchema> ? FlatGrants<S> : never;
148
+
149
+ /**
150
+ * One Silo as a compile-tier fixture describes it - the stand-in for the row
151
+ * the operator would have created. Keyed by Silo id in `CompileFixture.silo`,
152
+ * so the id itself isn't a field here.
153
+ */
154
+ export interface SiloFixture {
155
+ /** The shared directory. Required when the ask includes `volume`. */
156
+ hostPath?: string;
157
+ /** Where members mount it. Defaults to `/silo`, as a real Silo does. */
158
+ containerPath?: string;
159
+ /** The Silo's group. Required when the ask includes `volume`. */
160
+ gid?: number;
161
+ /**
162
+ * What this Silo's other members publish (name → port) - the directory a
163
+ * require-map resolves against. Defaults to `{}`, so any require-map without
164
+ * it fails with the same guided message Home emits. Per Silo: a require
165
+ * against one Silo never sees another's publications, same as Home.
166
+ */
167
+ services?: Record<string, number>;
168
+ /**
169
+ * The shared credential. Defaults to a stable 64-hex string derived from
170
+ * `id` - production *shape* without needing REALM_SECRET.
171
+ */
172
+ apiKey?: string;
173
+ }
174
+
175
+ export interface CompileFixture<App> {
176
+ config: ConfigOf<App>;
177
+ grants?: GrantsOf<App>;
178
+ /** Optional override; defaults to the app's `name`. */
179
+ appId?: string;
180
+ /**
181
+ * HMAC seed used to derive per-app passwords. Defaults to `"test"` so
182
+ * compiles are deterministic across runs - two `t.compile(...)` calls
183
+ * with the same fixture produce the same `derived` secrets, which lets
184
+ * tests assert on resolved env values directly.
185
+ */
186
+ seed?: string;
187
+ /**
188
+ * Peer-world entries to expose to this app's `peers.*` permissions.
189
+ * Each entry is `{kind, data}` for one of the registered peer kinds
190
+ * (routedApps, hostPorts, backupVolumes, buildApps). Defaults to
191
+ * `[]`. A `peers.*` permission still requires a matching grant - same
192
+ * as Home - so unset grants resolve to `[]` regardless of `peers`.
193
+ */
194
+ peers?: WorldState;
195
+ /**
196
+ * Override for the bundled `host.management()` resolved value. At
197
+ * runtime Home fills `baseDomain` / `homeSlug` from `home_state` and
198
+ * `workerId` from the running worker; tests supply whatever subset
199
+ * they need. Fields left unset stay as the SDK placeholder
200
+ * (empty string), matching a fresh install before configuration.
201
+ * Apps that don't grant `host.management()` ignore this entirely.
202
+ */
203
+ management?: {
204
+ baseDomain?: string;
205
+ homeSlug?: string;
206
+ workerId?: string;
207
+ acmeDns?: {
208
+ serverUrl: string;
209
+ username: string;
210
+ apiKey: string;
211
+ ca: string;
212
+ caRootPem: string;
213
+ } | null;
214
+ };
215
+ /**
216
+ * Override for the bundled `home.apiAccess()` resolved value. At runtime
217
+ * Home fills `socket` with the app's per-app proxy DIRECTORY
218
+ * (`/opt/realm/worker/home-proxy/<appId>`, the dir the app mounts); tests
219
+ * supply whatever path they want to mount/assert on. The supplied path is
220
+ * also exempted from the absolute-mount gate (mirrors Home's
221
+ * `collectAllowed`). Apps that don't grant `home.apiAccess()` ignore this
222
+ * entirely.
223
+ */
224
+ homeApi?: {
225
+ socket: string;
226
+ };
227
+ /**
228
+ * Override for the bundled `host.spawn()` resolved value. At runtime Home
229
+ * fills `socket` + `scratch` + `artifacts` with the app's per-app paths
230
+ * (`/opt/realm/worker/spawn/sock/<appId>` — the socket DIRECTORY the app
231
+ * mounts — + `data/<appId>/scratch` + `data/<appId>/artifacts`); tests supply
232
+ * whatever paths they want to mount/assert on. All three supplied paths are
233
+ * exempted from the absolute-mount gate (mirrors Home's
234
+ * `collectAllowed`). Apps that don't grant `host.spawn()` ignore this
235
+ * entirely.
236
+ */
237
+ spawn?: {
238
+ socket: string;
239
+ scratch: string;
240
+ artifacts: string;
241
+ };
242
+ /**
243
+ * The Silo (or Silos) a granted `host.silo()` resolves against. At runtime
244
+ * these are rows the operator created and bound the app to; there's no such
245
+ * record in a test, so the fixture supplies them. The declared affordances
246
+ * still come from the app's own `r.host.silo({...})` - this only says which
247
+ * Silo, the same split as the real install (the author asks, the operator
248
+ * points).
249
+ *
250
+ * Keyed by Silo id - the same id the grant carries, so a `host.silo` grant of
251
+ * `"k3f9x2"` resolves against the `k3f9x2` entry:
252
+ *
253
+ * ```ts
254
+ * grants: { silo: { kind: "host.silo", data: "k3f9x2" } },
255
+ * silo: { k3f9x2: { hostPath: "/mnt/media", gid: 1_000_000 } },
256
+ * ```
257
+ *
258
+ * An app binding two Silos gets two entries, and each declaration resolves
259
+ * against its own directory and its own `services` - the same separation Home
260
+ * gets from two rows.
261
+ *
262
+ * Only the fields the ask actually uses are needed: `hostPath` + `gid` when
263
+ * the ask includes `volume`, `services` when it carries a require-map. The
264
+ * volume's `hostPath` is exempted from the absolute-mount gate, and
265
+ * the (hostPath, gid) pair is what the mount gate matches against - so a
266
+ * fixture that hand-writes a different gid gets the same violation a real
267
+ * app would. Apps that don't grant `host.silo()` ignore this entirely.
268
+ */
269
+ silo?: Record<string, SiloFixture>;
270
+ /**
271
+ * Per-install preview context handed to `getDesiredState` as `preview`.
272
+ * Defaults to `null` (a persistent install). Set `{ previewKey }` to compile
273
+ * the app as a template's preview child and assert on its preview-branched
274
+ * state (e.g. a per-PR public domain).
275
+ */
276
+ preview?: PreviewContext | null;
277
+ /**
278
+ * Home's mesh base domain, used to resolve `info`'s `services.<name>` URLs
279
+ * (`{appId}-{service}.{slug}.{baseDomain}`). Defaults to unset, so service
280
+ * URLs resolve to `null` (an unplaced install) and `info` entries that lean
281
+ * on them drop. Mirrors Home's `home_state` baseDomain.
282
+ */
283
+ baseDomain?: string;
284
+ /**
285
+ * Placement worker slug, the other half of the structured service host used
286
+ * to resolve `info`'s `services.<name>` URLs. Defaults to unset (`null`).
287
+ */
288
+ slug?: string;
289
+ }
290
+
291
+ export type CompileResult =
292
+ | {
293
+ error: false;
294
+ app: HomeApp;
295
+ /**
296
+ * The app's `info` entries, resolved on the same separate path Home's
297
+ * summary page uses. A sibling of `app` (not folded into it) because
298
+ * `info` is not part of the resolved worker-bound state - keeping it
299
+ * off `app` leaves resolved-state snapshots untouched.
300
+ */
301
+ info: ResolvedInfoEntry[];
302
+ }
303
+ | {
304
+ error: true;
305
+ message: string;
306
+ issues: readonly CompileIssue[];
307
+ /**
308
+ * Raw violation records from `checkAppGrants` for the subset of
309
+ * issues that came from grant checking. Carries the structured
310
+ * shape Home returns from POST /api/apps so the dev install panel
311
+ * can render the per-permission violations list rather than a
312
+ * flat error message. Empty for non-grant failures (eval throws,
313
+ * bundle errors, validation, etc).
314
+ */
315
+ violations: readonly Violation[];
316
+ };
317
+
318
+ export type { CompileIssue };
@@ -0,0 +1,79 @@
1
+ // Resolving a sampled `getTrustedHeaders` manifest against a live viewer.
2
+ //
3
+ // The manifest is static (header names must be knowable without a request,
4
+ // so Caddy can list them for copy_headers / anti-spoof strip); the values
5
+ // are per-request. This module is the single place that turns one into the
6
+ // other, for both doors: the ForwardAuth verify hop (`access-gate.ts`, the
7
+ // browser reaching an authenticated subdomain) and the UI surface hop
8
+ // (`run-loader.ts`, a page or widget loader/action going iframe → Home →
9
+ // worker → container).
10
+ //
11
+ // Plain data in, plain data out — no `Deno`, no Web globals.
12
+
13
+ import type { TrustedHeaderSource, TrustedHeadersManifest } from "./resolve.ts";
14
+
15
+ type ByRole = TrustedHeadersManifest["byRole"];
16
+
17
+ /**
18
+ * The live Home user a sampled mapping resolves against. Structurally the
19
+ * runtime half of `User` in create-app.ts, where `username` / `email` are
20
+ * refs because there is no real user at sampling time.
21
+ */
22
+ export interface TrustedHeaderViewer {
23
+ username: string;
24
+ email: string;
25
+ role: "admin" | "member";
26
+ }
27
+
28
+ /**
29
+ * Apply an app's sampled per-role mapping to the viewer. Returns an empty
30
+ * map when the app brokers no headers (no manifest) or declares none for
31
+ * this viewer's role.
32
+ */
33
+ export function resolveTrustedHeaders(
34
+ byRole: ByRole | undefined,
35
+ viewer: TrustedHeaderViewer,
36
+ ): Record<string, string> {
37
+ const map = byRole?.[viewer.role];
38
+ if (!map) return {};
39
+ const out: Record<string, string> = {};
40
+ for (const [key, source] of Object.entries(map)) {
41
+ out[key] = resolveSource(source, viewer);
42
+ }
43
+ return out;
44
+ }
45
+
46
+ // Resolve a header source against the live viewer: a literal passes through,
47
+ // a `{field}` reads the session value, a `{template}` joins literal + field
48
+ // segments in order.
49
+ function resolveSource(source: TrustedHeaderSource, viewer: TrustedHeaderViewer): string {
50
+ if ("literal" in source) return source.literal;
51
+ if ("field" in source) return viewer[source.field];
52
+ return source.template.map((seg) => (typeof seg === "string" ? seg : viewer[seg.field])).join("");
53
+ }
54
+
55
+ /**
56
+ * Layer Home-stamped headers over the ones the author declared on a
57
+ * loader/action call, dropping any declared key that collides with a
58
+ * stamped name in *any* casing.
59
+ *
60
+ * The case-insensitivity is the point. A plain spread only overrides an
61
+ * exact-case duplicate, so an author-declared `x-realm-widget-pid` survives
62
+ * beside Home's `X-Realm-Widget-PID` as a second entry and both reach the
63
+ * container comma-joined — the app then reads a value it authored itself.
64
+ * Returns `undefined` when there is nothing to send.
65
+ */
66
+ export function stampHeaders(
67
+ declared: Record<string, string> | undefined,
68
+ stamped: Record<string, string>,
69
+ ): Record<string, string> | undefined {
70
+ const stampedKeys = Object.keys(stamped);
71
+ if (stampedKeys.length === 0) return declared;
72
+ const reserved = new Set(stampedKeys.map((k) => k.toLowerCase()));
73
+ const out: Record<string, string> = {};
74
+ for (const [k, v] of Object.entries(declared ?? {})) {
75
+ if (reserved.has(k.toLowerCase())) continue;
76
+ out[k] = v;
77
+ }
78
+ return { ...out, ...stamped };
79
+ }
package/src/types.ts ADDED
@@ -0,0 +1,126 @@
1
+ // Type-level wiring for createApp's `getDesiredState` parameter.
2
+ //
3
+ // A configSchema is a tree of typed records (permissions, inputs, sections,
4
+ // tabs). At eval time it collapses into a `config` object whose shape
5
+ // depends on each node:
6
+ //
7
+ // r.host.port(9090).reason(...) → { port: number }
8
+ // r.input.text() → string
9
+ // r.input.secret() → Ref<string> (resolved at deploy time)
10
+ // r.section({ a: r.input.text() }) → { a: string }
11
+ // r.tab({ s: r.section(...) }) → { s: { ... } }
12
+ //
13
+ // Optional nodes flip from required keys to optional keys in the resolved
14
+ // tree.
15
+
16
+ import type { z } from "zod";
17
+ import type { Ref } from "./ref.ts";
18
+ import type { CompleteInput, CompletePermission, CompleteSchemablePermission } from "./factory.ts";
19
+ import type { Grant, GrantOf, ValueOf } from "./grants.ts";
20
+ import type { TabChild, TabRecord, SectionChild, SectionRecord } from "./schema/nodes.ts";
21
+ import type { InputKind } from "./shared.ts";
22
+
23
+ // ----- Node hierarchy -----------------------------------------------------
24
+
25
+ // CompleteSchemablePermission is the post-`.schema()` shape of a fetchUrl
26
+ // factory; it lands here so r.remote.fetchUrl(...).schema(z).reason("...")
27
+ // is assignable to a configSchema slot. The pre-`.schema()` factory
28
+ // output is missing `responseSchema`, which CompleteSchemablePermission
29
+ // requires — so an unschema'd fetchUrl fails to typecheck in configSchema.
30
+ export type Leaf = CompletePermission | CompleteSchemablePermission | CompleteInput;
31
+
32
+ export type Section = SectionRecord<Record<string, SectionChild>>;
33
+ export type Tab = TabRecord<Record<string, TabChild>>;
34
+
35
+ export type Node = Leaf | Section | Tab;
36
+
37
+ /**
38
+ * A configSchema is either a flat record of leaves and sections, or a
39
+ * record of tabs. `createApp`'s homogeneity check enforces "all tabs or
40
+ * no tabs" at runtime; sections fall on the no-tabs side and can sit
41
+ * alongside leaves at the top level.
42
+ */
43
+ export type ConfigSchema = Record<string, Leaf | Section> | Record<string, Tab>;
44
+
45
+ /**
46
+ * A ui surface's `configSchema` — the per-mount settings a user fills in on
47
+ * the tile's config screen. Narrower than an app's `ConfigSchema` on purpose:
48
+ *
49
+ * - **Inputs only, no permissions.** Permissions are app-level grants, not
50
+ * per-user per-placement settings; they have no meaning on a widget tile.
51
+ * - **Flat, no tabs/sections.** A tile's settings are a short list, not a
52
+ * multi-page form.
53
+ * - **No `r.input.secret()`.** A secret leaf can't ride the in-iframe
54
+ * `request(state, config)` (the iframe never sees it), so it needs a
55
+ * server-side delivery channel that doesn't exist yet. Excluding
56
+ * `input.secret` at the type level makes a secret leaf a compile error
57
+ * instead of one that would silently ship to the iframe. (`create-app`'s
58
+ * manifest builder also throws on one, for the packed/JS path that skips
59
+ * the type check.)
60
+ */
61
+ export type NonSecretInput = CompleteInput & {
62
+ readonly kind: Exclude<InputKind, "input.secret">;
63
+ };
64
+
65
+ export type SurfaceConfigSchema = Record<string, NonSecretInput>;
66
+
67
+ // ----- Per-input runtime value --------------------------------------------
68
+
69
+ type InputValueOf<I> = I extends { readonly kind: "input.text" }
70
+ ? string
71
+ : I extends { readonly kind: "input.secret" }
72
+ ? Ref<string>
73
+ : I extends { readonly kind: "input.number" }
74
+ ? number
75
+ : I extends { readonly kind: "input.boolean" }
76
+ ? boolean
77
+ : I extends { readonly kind: "input.enum"; readonly schema: z.ZodType<infer V> }
78
+ ? V
79
+ : never;
80
+
81
+ // ----- ResolvedTree -------------------------------------------------------
82
+
83
+ type IsOptionalNode<N> = N extends { readonly meta: { readonly isOptional: true } } ? true : false;
84
+
85
+ type ResolvedNode<N> =
86
+ // CompleteSchemablePermission (fetchUrl after .schema()) is checked
87
+ // before CompletePermission because it carries a live schema that lets
88
+ // the resolved value type narrow to the schema's output — authors get
89
+ // typed access to the validated JSON.
90
+ N extends CompleteSchemablePermission
91
+ ? N extends { readonly responseSchema: z.ZodType<infer V> }
92
+ ? V
93
+ : unknown
94
+ : N extends CompletePermission
95
+ ? // toGrant strips meta to {kind, data}; ValueOf maps that to the runtime shape.
96
+ ValueOf<Extract<GrantOf<N>, Grant>>
97
+ : N extends CompleteInput
98
+ ? InputValueOf<N>
99
+ : N extends TabRecord<infer C>
100
+ ? ResolvedTree<C>
101
+ : N extends SectionRecord<infer C>
102
+ ? ResolvedTree<C>
103
+ : never;
104
+
105
+ export type ResolvedTree<C> = {
106
+ readonly [K in keyof C as IsOptionalNode<C[K]> extends true ? never : K]: ResolvedNode<C[K]>;
107
+ } & {
108
+ readonly [K in keyof C as IsOptionalNode<C[K]> extends true ? K : never]?: ResolvedNode<C[K]>;
109
+ };
110
+
111
+ export type InferConfigSchema<T> =
112
+ T extends Record<string, Node> ? ResolvedTree<T> : Record<string, never>;
113
+
114
+ // ----- Runtime narrowers --------------------------------------------------
115
+
116
+ export function isTab(node: Node): node is Tab {
117
+ return node.kind === "node.tab";
118
+ }
119
+
120
+ export function isSection(node: Node): node is Section {
121
+ return node.kind === "node.section";
122
+ }
123
+
124
+ export function isInput(node: Node): node is CompleteInput {
125
+ return typeof node.kind === "string" && node.kind.startsWith("input.");
126
+ }
@@ -0,0 +1,96 @@
1
+ import type { ComponentChildren } from "preact";
2
+ import { cn } from "../utils.ts";
3
+ import {
4
+ InfoIcon,
5
+ LightbulbIcon,
6
+ RocketLaunchIcon,
7
+ WarningIcon,
8
+ WarningOctagonIcon,
9
+ } from "./icons.tsx";
10
+
11
+ // The docs' callout anatomy - a coloured clay panel with a 24px icon, an
12
+ // uppercase title and one short body - available to the dashboard. The Updates
13
+ // page uses it for the consequence statement above the primary button, which
14
+ // is why the spacing is the caller's: it sits inside a flex column there and
15
+ // under a paragraph in the docs.
16
+
17
+ type AsideVariant = "note" | "tip" | "caution" | "danger" | "roadmap";
18
+
19
+ interface VariantStyle {
20
+ bg: string;
21
+ shadow: string;
22
+ text: string;
23
+ Icon: typeof InfoIcon;
24
+ defaultTitle: string;
25
+ }
26
+
27
+ const VARIANTS: Record<AsideVariant, VariantStyle> = {
28
+ note: {
29
+ bg: "bg-trust-bg",
30
+ shadow: "shadow-clay-blue",
31
+ text: "text-trust",
32
+ Icon: InfoIcon,
33
+ defaultTitle: "Note",
34
+ },
35
+ tip: {
36
+ bg: "bg-surface-light",
37
+ shadow: "shadow-clay-primary",
38
+ text: "text-primary",
39
+ Icon: LightbulbIcon,
40
+ defaultTitle: "Tip",
41
+ },
42
+ caution: {
43
+ bg: "bg-perm-bg",
44
+ shadow: "shadow-clay-amber",
45
+ text: "text-perm",
46
+ Icon: WarningIcon,
47
+ defaultTitle: "Caution",
48
+ },
49
+ danger: {
50
+ bg: "bg-danger-bg",
51
+ shadow: "shadow-clay-danger",
52
+ text: "text-danger",
53
+ Icon: WarningOctagonIcon,
54
+ defaultTitle: "Danger",
55
+ },
56
+ roadmap: {
57
+ bg: "bg-roadmap-bg",
58
+ shadow: "shadow-clay-roadmap",
59
+ text: "text-roadmap",
60
+ Icon: RocketLaunchIcon,
61
+ defaultTitle: "Roadmap",
62
+ },
63
+ };
64
+
65
+ function Aside({
66
+ type = "note",
67
+ title,
68
+ className,
69
+ children,
70
+ }: {
71
+ type?: AsideVariant;
72
+ title?: string;
73
+ className?: string;
74
+ children: ComponentChildren;
75
+ }) {
76
+ const v = VARIANTS[type];
77
+ const { Icon } = v;
78
+ return (
79
+ <aside
80
+ data-slot="aside"
81
+ className={cn(v.bg, v.shadow, "flex items-start gap-3 rounded-3xl p-4", className)}
82
+ >
83
+ <Icon className={cn(v.text, "mt-0.5 h-6 w-6 shrink-0")} />
84
+ <div className="flex min-w-0 flex-1 flex-col gap-1.5">
85
+ <p className={cn(v.text, "m-0 text-sm font-extrabold uppercase tracking-wide")}>
86
+ {title ?? v.defaultTitle}
87
+ </p>
88
+ <div className="text-sm leading-relaxed text-foreground [&_code]:rounded [&_code]:bg-background [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em] [&_p]:m-0 [&_p+p]:mt-2">
89
+ {children}
90
+ </div>
91
+ </div>
92
+ </aside>
93
+ );
94
+ }
95
+
96
+ export { Aside, type AsideVariant };
@@ -0,0 +1,42 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ import { cn, type ElementProps } from "../utils.ts";
4
+
5
+ // Small status pill (`data-slot="badge"`). Radix's `Slot`/`asChild`
6
+ // polymorphism is dropped — same as this repo's Button/Card, since
7
+ // preact/compat can't take Radix — so a Badge is always a <span>. That also
8
+ // drops shadcn's `ghost`/`link` variants, whose styling is entirely
9
+ // anchor-gated (`[a&]:hover:…`) and inert on a span.
10
+ const badgeVariants = cva(
11
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] [&>svg]:pointer-events-none [&>svg]:size-3",
12
+ {
13
+ variants: {
14
+ variant: {
15
+ default: "border-transparent bg-primary text-primary-foreground",
16
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
17
+ destructive: "border-transparent bg-destructive text-white",
18
+ outline: "border-border text-foreground",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: "default",
23
+ },
24
+ },
25
+ );
26
+
27
+ function Badge({
28
+ className,
29
+ variant = "default",
30
+ ...props
31
+ }: ElementProps<"span"> & VariantProps<typeof badgeVariants>) {
32
+ return (
33
+ <span
34
+ data-slot="badge"
35
+ data-variant={variant}
36
+ className={cn(badgeVariants({ variant, className }))}
37
+ {...props}
38
+ />
39
+ );
40
+ }
41
+
42
+ export { Badge };