@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,367 @@
1
+ // Iframe runtime — mount one surface and proxy its loader/action calls
2
+ // to the parent window over postMessage. The iframe runs in an opaque
3
+ // origin (sandbox="allow-scripts allow-forms" without allow-same-origin)
4
+ // and never fetches Home directly; the parent makes authenticated
5
+ // requests on its behalf and posts results back.
6
+ //
7
+ // State is owned by tanstack-query: each mount() gets its own
8
+ // QueryClient. The render fn receives prebuilt handles — `loaders.X`
9
+ // (a UseQueryResult) and `actions.Y` (a UseMutationResult) — instead of
10
+ // calling hooks itself, plus the widget's own `state`/`setState`/`reset`
11
+ // and resolved `config`. A loader's bundled `request(state, config)`
12
+ // builds the call (or returns `null` for idle/lazy); Home prefills the
13
+ // eager ones and inlines the results as `initialData`, so those handles
14
+ // carry `data` on first paint.
15
+
16
+ import { type ComponentChildren, h, render } from "preact";
17
+ import { useState } from "preact/hooks";
18
+ import {
19
+ QueryClient,
20
+ QueryClientProvider,
21
+ useMutation,
22
+ type UseMutationResult,
23
+ useQuery,
24
+ type UseQueryResult,
25
+ } from "@tanstack/react-query";
26
+ import type { LoaderQueryOptions, RequestShape } from "../ui-surface.ts";
27
+ import {
28
+ isTransientUiError,
29
+ readUiFailure,
30
+ type UiErrorCode,
31
+ uiErrorFromFailure,
32
+ } from "../ui-error.ts";
33
+
34
+ interface SurfaceLoader {
35
+ // The bundled surface object retains the author's runtime `merge`
36
+ // callback (closured over the original surface). Home's manifest only
37
+ // sees `merge: true` and decides to fan out; the iframe still has the
38
+ // function and applies it to the `{ results: [...] }` envelope.
39
+ // deno-lint-ignore no-explicit-any
40
+ merge?: (results: any[]) => any;
41
+ // The bundled stitch fn: builds the call from the widget's state +
42
+ // config, or returns `null` when the state isn't ready (→ idle/lazy).
43
+ // Rides the bundle (never crosses QuickJS) so it keeps its closure.
44
+ request: (state: unknown, config: unknown) => RequestShape | null;
45
+ // Ships on the loader definition (rides the bundle like `merge`) so a
46
+ // *function* interval (e.g. `(q) => building ? 2000 : false`) survives
47
+ // — a function can't cross the JSON manifest / QuickJS boundary.
48
+ refetchInterval?: LoaderQueryOptions["refetchInterval"];
49
+ }
50
+
51
+ interface SurfaceAction {
52
+ // Builds the call from the caller's `mutate(input)` argument + config.
53
+ request: (input: unknown, config: unknown) => RequestShape;
54
+ }
55
+
56
+ interface RenderApi {
57
+ loaders: Record<string, UseQueryResult<unknown, Error>>;
58
+ actions: Record<string, UseMutationResult<unknown, Error, unknown>>;
59
+ // The widget's live UI state (read-only here; mutate via setState).
60
+ state: unknown;
61
+ setState: (patch: Record<string, unknown>) => void;
62
+ reset: () => void;
63
+ // The surface's resolved config (settings set outside the widget).
64
+ config: unknown;
65
+ queryClient: QueryClient;
66
+ }
67
+
68
+ interface SurfaceLike {
69
+ title: string;
70
+ // The live state schema rides the bundle; its `{}`-parse seeds the
71
+ // widget's initial state.
72
+ stateSchema?: { safeParse(v: unknown): { success: boolean; data?: unknown } };
73
+ loaders?: Record<string, SurfaceLoader>;
74
+ actions?: Record<string, SurfaceAction>;
75
+ render: (api: RenderApi) => ComponentChildren;
76
+ }
77
+
78
+ // No appId/kind/key: the parent stamped all three on this iframe's src and
79
+ // reads them back from there. A sandboxed surface can only send what
80
+ // genuinely originates inside it, so there is nothing here to forge.
81
+ interface MountOpts {
82
+ surface: SurfaceLike;
83
+ container: Element;
84
+ // The surface's resolved config, inlined by Home. A loader/action's
85
+ // `request(state, config)` reads it. Absent → `{}`.
86
+ config?: unknown;
87
+ // Server-prefilled loader results, keyed by loader name. Present for
88
+ // eager loaders Home could run (their `request` returned a shape at the
89
+ // default state); missing entries degrade to fetch-on-mount. Same shape
90
+ // as the loader's queryFn output *before* merge, so the runtime applies
91
+ // merge to it too — see `applyMerge`.
92
+ initialData?: Record<string, unknown>;
93
+ }
94
+
95
+ interface PendingHandler {
96
+ resolve: (data: unknown) => void;
97
+ reject: (err: Error) => void;
98
+ }
99
+
100
+ let nextCallId = 1;
101
+ const pending = new Map<number, PendingHandler>();
102
+ let listenerAttached = false;
103
+
104
+ function attachListener(): void {
105
+ if (listenerAttached) return;
106
+ listenerAttached = true;
107
+ globalThis.addEventListener("message", (ev: MessageEvent) => {
108
+ // Only the parent brokers ui-responses. Sibling iframes (and browser
109
+ // extensions injecting into them) can postMessage to us via
110
+ // `parent.frames[i]`, so without this check a forged response with
111
+ // a guessed `id` would resolve a pending loader with attacker data.
112
+ if (ev.source !== globalThis.parent) return;
113
+ const m = ev.data as {
114
+ realm?: string;
115
+ id?: number;
116
+ data?: unknown;
117
+ error?: string;
118
+ code?: UiErrorCode;
119
+ } | null;
120
+ if (!m || m.realm !== "ui-response" || typeof m.id !== "number") return;
121
+ const handler = pending.get(m.id);
122
+ if (!handler) return;
123
+ pending.delete(m.id);
124
+ if (typeof m.error === "string") {
125
+ // A coded failure rejects with a UiError so the surface (and the
126
+ // initial-load gate below) can branch on `code` — e.g. tell a
127
+ // transient boot window from a crashed container. `readUiFailure`
128
+ // validates the code against the known set rather than trusting the
129
+ // parent frame's string; an uncoded or unrecognized failure stays a
130
+ // plain Error and falls through to the author's branch.
131
+ const failure = readUiFailure({ error: m.error, code: m.code });
132
+ handler.reject(failure ? uiErrorFromFailure(failure) : new Error(m.error));
133
+ } else {
134
+ handler.resolve(m.data);
135
+ }
136
+ });
137
+ }
138
+
139
+ // Self-evict orphaned pending entries. Worker has a 30s upstream cap,
140
+ // Home→Worker has 35s, so a real reply always arrives within ~35s. Set
141
+ // the iframe-side timeout slightly higher so a slow round-trip wins
142
+ // over our local fallback when both fire close together.
143
+ const PENDING_TIMEOUT_MS = 40_000;
144
+
145
+ type Endpoint = "loader" | "action";
146
+
147
+ // Send the built request shape to the parent, which makes the
148
+ // authenticated call to Home and posts the result back. `request` is the
149
+ // `{ method, path, query, body }` a loader/action's stitch fn produced —
150
+ // Home layers on `service`/`port`/headers (the grant boundary + secrets)
151
+ // from the manifest and forwards to the worker.
152
+ function callParent(endpoint: Endpoint, name: string, request: RequestShape): Promise<unknown> {
153
+ const id = nextCallId++;
154
+ return new Promise<unknown>((resolve, reject) => {
155
+ const timer = setTimeout(() => {
156
+ if (pending.delete(id)) {
157
+ reject(
158
+ new Error(`ui-runtime: ${endpoint} "${name}" timed out after ${PENDING_TIMEOUT_MS}ms`),
159
+ );
160
+ }
161
+ }, PENDING_TIMEOUT_MS);
162
+ // Wrap the resolvers to clear the timer when a real reply arrives.
163
+ pending.set(id, {
164
+ resolve: (data) => {
165
+ clearTimeout(timer);
166
+ resolve(data);
167
+ },
168
+ reject: (err) => {
169
+ clearTimeout(timer);
170
+ reject(err);
171
+ },
172
+ });
173
+ const message = {
174
+ realm: "ui-call",
175
+ id,
176
+ endpoint,
177
+ name,
178
+ request,
179
+ };
180
+ // Parent's message listener validates `event.source` against the
181
+ // iframe's contentWindow so a "*" target here is fine — only the
182
+ // intended parent will accept the message.
183
+ (globalThis as unknown as { parent: Window }).parent.postMessage(message, "*");
184
+ });
185
+ }
186
+
187
+ // merge loaders return `{ results: [...] }` — one `{ data, workerId }` per
188
+ // worker that answered; the author's merge() reduces them to the declared
189
+ // output shape. Applied in both the queryFn (live fetch) and over
190
+ // `initialData` (server prefill) so the cached value is the same shape either
191
+ // way. Non-merge loaders and non-envelope values pass through untouched.
192
+ function applyMerge(loader: SurfaceLoader, raw: unknown): unknown {
193
+ if (raw && typeof raw === "object" && "results" in raw && typeof loader.merge === "function") {
194
+ return loader.merge((raw as { results: unknown[] }).results);
195
+ }
196
+ return raw;
197
+ }
198
+
199
+ export function mount(opts: MountOpts): void {
200
+ attachListener();
201
+ const queryClient = new QueryClient({
202
+ defaultOptions: {
203
+ queries: {
204
+ // The iframe is sandboxed and never visibly refocuses on its
205
+ // own; turn off focus refetch so behavior stays predictable
206
+ // regardless of how the host page swaps tabs.
207
+ refetchOnWindowFocus: false,
208
+ retry: false,
209
+ },
210
+ },
211
+ });
212
+
213
+ // The widget's config (resolved by Home) and initial state (the live
214
+ // stateSchema's `{}`-parse). `seedState` is a stable reference so
215
+ // `initialData` applies only while the widget is still at its seed: that
216
+ // gates the prefill to the query key Home actually ran to produce it
217
+ // (`request(seedState, config)`), so a re-keyed loader can't inherit it.
218
+ const config = opts.config ?? {};
219
+ const seedParse = opts.surface.stateSchema?.safeParse({});
220
+ const seedState: unknown = seedParse?.success ? seedParse.data : {};
221
+
222
+ // Capture the loader/action name sets ONCE, before the component
223
+ // renders. The render loop calls one useQuery per loader and one
224
+ // useMutation per action in this fixed order every render —
225
+ // rules-of-hooks safe precisely because the set never changes for a
226
+ // given mount. Never filter these lists conditionally.
227
+ const loaderSetup = Object.entries(opts.surface.loaders ?? {});
228
+ const actionSetup = Object.entries(opts.surface.actions ?? {});
229
+
230
+ function SurfaceRoot() {
231
+ // ONE state object for the whole widget — shared across its loaders.
232
+ const [state, setState] = useState<unknown>(seedState);
233
+
234
+ const loaders: Record<string, UseQueryResult<unknown, Error>> = {};
235
+ for (const [name, loader] of loaderSetup) {
236
+ // Build this loader's call from the current state + config. `null`
237
+ // → not ready (idle/lazy); a shape → run it, keyed on the shape so
238
+ // it re-fetches only when its own request changes. A throwing
239
+ // request() counts as idle — mirrors the server prefill (stitchSnippet
240
+ // catches → lazy), so one broken loader can't blank the mount.
241
+ let request: RequestShape | null;
242
+ try {
243
+ request = loader.request(state, config);
244
+ } catch (err) {
245
+ console.error(`[realm] loader "${name}" request() threw; treating as idle`, err);
246
+ request = null;
247
+ }
248
+ // Server-prefilled seed data is fresh — Home just ran this loader while
249
+ // building the shell — so staleTime Infinity keeps the query from
250
+ // re-fetching on mount and churning the result. A setState re-keys to a
251
+ // new query with no initialData (staleTime 0 → fetches normally);
252
+ // refetchInterval and an explicit .refetch() still override staleTime.
253
+ const prefilled =
254
+ state === seedState ? applyMerge(loader, opts.initialData?.[name]) : undefined;
255
+ loaders[name] = useQuery({
256
+ queryKey: ["loader", name, request],
257
+ enabled: request !== null,
258
+ initialData: prefilled,
259
+ staleTime: prefilled !== undefined ? Infinity : 0,
260
+ queryFn: async () =>
261
+ applyMerge(loader, await callParent("loader", name, request as RequestShape)),
262
+ refetchInterval: loader.refetchInterval,
263
+ });
264
+ }
265
+
266
+ const actions: Record<string, UseMutationResult<unknown, Error, unknown>> = {};
267
+ for (const [name, action] of actionSetup) {
268
+ // No queryKey for mutations — invalidation is the author's job (via
269
+ // a loader handle's `.refetch()` or `queryClient.invalidateQueries`).
270
+ // The stitch fn builds the call from the mutate argument + config.
271
+ actions[name] = useMutation({
272
+ mutationFn: (input: unknown) => callParent("action", name, action.request(input, config)),
273
+ });
274
+ }
275
+
276
+ // Initial-load boot state: while an eager loader has produced no data,
277
+ // render a centered boot state in place of the surface rather than let
278
+ // the author's render fn paint a half-empty tile (success branch with
279
+ // `data === undefined`) or a raw error string. `data === undefined` is
280
+ // what keeps this to the *initial* load — once a loader has succeeded its
281
+ // data stays set, so a later background refetch shows stale data rather
282
+ // than flipping back to a spinner. Two flavors:
283
+ // - first fetch in flight (`isFetching`, no data, no error) → "Loading…"
284
+ // - first fetch failed with a transient UiError (CONTAINER_STARTING) →
285
+ // "Container starting…"
286
+ // Hard codes (CONTAINER_CRASHED, UPSTREAM_*, …) and plain Errors fall
287
+ // through to the author's own error branch.
288
+ //
289
+ // `isFetching`, not `isPending`: `isPending` is true for ANY query with no
290
+ // cached data, including a disabled lazy loader that has never fetched, so
291
+ // gating on it would pin a lazy widget on "Loading…" forever.
292
+ //
293
+ // The `polls` guard is the "does it ever clear" condition for the ERROR
294
+ // case: a transient-error spinner clears only when the loader's poll timer
295
+ // re-fires and the tick that returns data re-renders the live surface. A
296
+ // loader with no active poll falls through to the author's branch instead
297
+ // of promising a recovery nothing delivers. (The in-flight case clears on
298
+ // the in-flight fetch itself, so it needs no poll.)
299
+ for (const [name, def] of loaderSetup) {
300
+ const loader = loaders[name]!;
301
+ if (loader.data !== undefined) continue;
302
+ const polls = def.refetchInterval !== undefined && def.refetchInterval !== false;
303
+ const inFlight = loader.isFetching;
304
+ const booting = polls && isTransientUiError(loader.error);
305
+ if (inFlight || booting) {
306
+ return h(
307
+ "div",
308
+ { class: "flex h-full flex-col items-center justify-center gap-2 p-3 bg-sidebar" },
309
+ h("span", { class: "realm-boot-spinner", "aria-hidden": "true" }),
310
+ h(
311
+ "p",
312
+ { class: "text-sm text-muted-foreground" },
313
+ booting ? "Container starting…" : "Loading…",
314
+ ),
315
+ );
316
+ }
317
+ }
318
+
319
+ return opts.surface.render({
320
+ loaders,
321
+ actions,
322
+ state,
323
+ setState: (patch: Record<string, unknown>) =>
324
+ setState((prev: unknown) => ({ ...(prev as Record<string, unknown>), ...patch })),
325
+ reset: () => setState(seedState),
326
+ config,
327
+ queryClient,
328
+ });
329
+ }
330
+
331
+ render(h(QueryClientProvider, { client: queryClient }, h(SurfaceRoot, null)), opts.container);
332
+ }
333
+
334
+ // Rendered in a widget iframe in place of the surface when its resolved config
335
+ // doesn't satisfy the widget's declared configSchema (a required field left
336
+ // unset, with no prefill). The author's render/loaders never run — their
337
+ // `config` is typed non-optional. The operator gets a Configure button that
338
+ // asks the parent to open this tile's config dialog; the parent maps the
339
+ // pid → placement.
340
+ export function mountNeedsConfig(opts: { container: Element; title: string; pid: string }): void {
341
+ function configure() {
342
+ (globalThis as unknown as { parent: Window }).parent.postMessage(
343
+ { realm: "ui-configure", pid: opts.pid },
344
+ "*",
345
+ );
346
+ }
347
+ render(
348
+ h(
349
+ "div",
350
+ {
351
+ class: "flex h-full flex-col items-center justify-center gap-2 bg-sidebar p-3 text-center",
352
+ },
353
+ h("p", { class: "text-xs text-muted-foreground" }, `${opts.title} needs setup`),
354
+ h(
355
+ "button",
356
+ {
357
+ type: "button",
358
+ onClick: configure,
359
+ class:
360
+ "cursor-pointer rounded-lg bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90",
361
+ },
362
+ "Configure",
363
+ ),
364
+ ),
365
+ opts.container,
366
+ );
367
+ }
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Iframe runtime CSS. Built by `realm-sdk build-runtime` via the
3
+ * tailwindcss CLI. Carries chrome only — clay tokens (from
4
+ * `@yourrealm/sdk/ui/tokens.css`), the SDK UI base layer, and every
5
+ * utility class a `src/ui/` component might emit (so the
6
+ * runtime ships <Button>/<Card>/<Dialog>/etc styled out-of-the-box).
7
+ *
8
+ * Per-surface utility classes live in a separate CSS file generated by
9
+ * `bundleUiSurfaces` and served at `/api/ui-bundles/<hash>.css`. The
10
+ * iframe shell loads runtime.css first, then the per-surface CSS — so
11
+ * the runtime stays small + cache-shared while each surface's own
12
+ * `class="..."` strings get whatever utilities they actually use,
13
+ * scoped to that surface alone.
14
+ */
15
+
16
+ @import "tailwindcss";
17
+ @import "@yourrealm/sdk/ui/tokens.css";
18
+
19
+ @source "../ui";
20
+
21
+ /* Iframe base: make html/body/#root fill the iframe viewport so surface
22
+ content can use `h-full` to vertically center inside its widget card.
23
+ Without this, the body sizes to content and any h-full child collapses
24
+ to zero, pinning content to the top-left of the iframe. */
25
+ html,
26
+ body,
27
+ #root {
28
+ height: 100%;
29
+ }
30
+ body {
31
+ margin: 0;
32
+ /* Match Home's _app.tsx body styling so the dev landing + install panel
33
+ share the same clay-theme background as the production dashboard. */
34
+ background: var(--background);
35
+ color: var(--foreground);
36
+ }
37
+
38
+ /* Boot-state spinner for the runtime's initial-load gate (mount.ts): a
39
+ container in its boot window renders a centered spinner instead of the
40
+ surface. Plain CSS so it needs no icon component and no per-surface
41
+ utility generation — the gate's markup lives in mount.ts, not a
42
+ bundled surface. */
43
+ .realm-boot-spinner {
44
+ width: 1.25rem;
45
+ height: 1.25rem;
46
+ border-radius: 9999px;
47
+ border: 2px solid var(--muted-foreground);
48
+ border-top-color: transparent;
49
+ opacity: 0.7;
50
+ animation: realm-boot-spin 0.7s linear infinite;
51
+ }
52
+ @keyframes realm-boot-spin {
53
+ to {
54
+ transform: rotate(360deg);
55
+ }
56
+ }