@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-EXCEPTION +26 -0
- package/README.md +34 -0
- package/dist/chunk-3R6KEFOI.js +22 -0
- package/dist/chunk-67BDAMYX.js +376 -0
- package/dist/chunk-AITGS27R.js +139 -0
- package/dist/chunk-CMAEHQ4F.js +6 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-O3UIK34B.js +1430 -0
- package/dist/chunk-VHTUSZUX.js +176 -0
- package/dist/cli.js +9006 -0
- package/dist/docker.js +54 -0
- package/dist/index.js +1053 -0
- package/dist/ingress.js +436 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/preact.js +22 -0
- package/dist/service-url.js +11 -0
- package/dist/services.js +213 -0
- package/dist/test.js +5069 -0
- package/dist/ui.js +254 -0
- package/dist/worker-protocol.js +89 -0
- package/package.json +112 -0
- package/src/app-icon.ts +74 -0
- package/src/apply.ts +52 -0
- package/src/bundle-ui.ts +211 -0
- package/src/bundle.ts +546 -0
- package/src/cli/build-dev-landing.ts +84 -0
- package/src/cli/build-install-panel.ts +90 -0
- package/src/cli/build-runtime.ts +133 -0
- package/src/cli/build.ts +63 -0
- package/src/cli/dev.ts +107 -0
- package/src/cli/down.ts +60 -0
- package/src/cli/main.ts +56 -0
- package/src/cli/test.ts +61 -0
- package/src/cli/validate.ts +37 -0
- package/src/compile.ts +100 -0
- package/src/create-app.ts +982 -0
- package/src/deep-merge.ts +33 -0
- package/src/dev/fixture.ts +48 -0
- package/src/dev/install-handler.ts +383 -0
- package/src/dev/server.ts +968 -0
- package/src/dev/stack.ts +108 -0
- package/src/dev/state.ts +159 -0
- package/src/docker.ts +96 -0
- package/src/errors.ts +61 -0
- package/src/factory.ts +481 -0
- package/src/garage.ts +56 -0
- package/src/generator/hash.ts +6 -0
- package/src/generator/resolve-refs.ts +207 -0
- package/src/grants-builder.ts +100 -0
- package/src/grants.ts +260 -0
- package/src/home-api.ts +172 -0
- package/src/home-cookie.ts +10 -0
- package/src/home-registry.ts +120 -0
- package/src/home.ts +187 -0
- package/src/host-registry.ts +143 -0
- package/src/http-header.ts +20 -0
- package/src/identifiers.ts +49 -0
- package/src/index.ts +66 -0
- package/src/info.ts +64 -0
- package/src/ini.ts +50 -0
- package/src/json.ts +14 -0
- package/src/jsx-runtime/index.ts +5 -0
- package/src/keys.ts +29 -0
- package/src/mysql.ts +56 -0
- package/src/pack-build.ts +108 -0
- package/src/pack.ts +115 -0
- package/src/peer-registry.ts +121 -0
- package/src/peer-world.ts +117 -0
- package/src/permission-meta.ts +116 -0
- package/src/permissions.ts +108 -0
- package/src/pkg-root.ts +34 -0
- package/src/postgres.ts +79 -0
- package/src/preact/index.ts +27 -0
- package/src/redis.ts +43 -0
- package/src/ref.ts +271 -0
- package/src/remote-registry.ts +33 -0
- package/src/required-host-kinds.ts +110 -0
- package/src/resolve-context.ts +111 -0
- package/src/resolve.ts +471 -0
- package/src/runtime/docker/compose/compose-schema.ts +118 -0
- package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
- package/src/runtime/docker/compose/default-hardening.ts +21 -0
- package/src/runtime/docker/compose/effects.ts +107 -0
- package/src/runtime/docker/compose/index.ts +17 -0
- package/src/runtime/docker/compose/labels.ts +39 -0
- package/src/runtime/docker/compose/types.ts +78 -0
- package/src/runtime/docker/home-proxy-server/index.ts +177 -0
- package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
- package/src/runtime/docker/ingress/build-config.ts +819 -0
- package/src/runtime/docker/ingress/index.ts +16 -0
- package/src/runtime/docker/ingress/types.ts +182 -0
- package/src/runtime/docker/log-frames.ts +121 -0
- package/src/runtime/docker/logs-server/index.ts +366 -0
- package/src/runtime/docker/spawn-server/context-key.ts +82 -0
- package/src/runtime/docker/spawn-server/index.ts +492 -0
- package/src/runtime/docker/spawn-server/protocol.ts +70 -0
- package/src/runtime/docker/spawn-server/spec.ts +122 -0
- package/src/runtime/docker/stats-server/disks.ts +161 -0
- package/src/runtime/docker/stats-server/index.ts +322 -0
- package/src/runtime/home-client/index.ts +71 -0
- package/src/runtime/log-line.ts +24 -0
- package/src/runtime/logs-client/index.ts +175 -0
- package/src/runtime/spawn-client/index.ts +93 -0
- package/src/runtime/spawn-client/kaniko.ts +79 -0
- package/src/runtime/stats-client/index.ts +65 -0
- package/src/runtime/stats-sample.ts +95 -0
- package/src/safe-json.ts +37 -0
- package/src/sandbox.ts +1209 -0
- package/src/schema/build-apps.ts +38 -0
- package/src/schema/home.ts +217 -0
- package/src/schema/host.ts +792 -0
- package/src/schema/inputs.ts +26 -0
- package/src/schema/nodes.ts +75 -0
- package/src/schema/peer.ts +400 -0
- package/src/schema/public-domain.ts +116 -0
- package/src/schema/remote.ts +74 -0
- package/src/security.ts +594 -0
- package/src/serialize.ts +21 -0
- package/src/service-definition.ts +284 -0
- package/src/service-url.ts +64 -0
- package/src/service.ts +39 -0
- package/src/services/index.ts +5 -0
- package/src/shared.ts +65 -0
- package/src/speculative-silo.ts +96 -0
- package/src/stable-secret.ts +14 -0
- package/src/surfaces.ts +264 -0
- package/src/tailwind-bin.ts +56 -0
- package/src/test/boot.ts +192 -0
- package/src/test/compile.ts +501 -0
- package/src/test/describe.ts +117 -0
- package/src/test/index.ts +11 -0
- package/src/test/ingress-fixture.ts +37 -0
- package/src/test/types.ts +318 -0
- package/src/trusted-headers.ts +79 -0
- package/src/types.ts +126 -0
- package/src/ui/components/aside.tsx +96 -0
- package/src/ui/components/badge.tsx +42 -0
- package/src/ui/components/button.tsx +59 -0
- package/src/ui/components/card.tsx +106 -0
- package/src/ui/components/checkbox.tsx +37 -0
- package/src/ui/components/code-block.tsx +29 -0
- package/src/ui/components/combobox.tsx +119 -0
- package/src/ui/components/config-field.tsx +284 -0
- package/src/ui/components/dialog.tsx +137 -0
- package/src/ui/components/domain-editor.tsx +317 -0
- package/src/ui/components/domain-list.ts +205 -0
- package/src/ui/components/dropdown-menu.tsx +73 -0
- package/src/ui/components/empty-state.tsx +51 -0
- package/src/ui/components/highlight.ts +59 -0
- package/src/ui/components/icons.tsx +101 -0
- package/src/ui/components/image-posture.tsx +85 -0
- package/src/ui/components/input.tsx +28 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/line-chart.tsx +113 -0
- package/src/ui/components/page.tsx +39 -0
- package/src/ui/components/permission-row.tsx +262 -0
- package/src/ui/components/permission-section.tsx +51 -0
- package/src/ui/components/pie-chart.tsx +245 -0
- package/src/ui/components/schema-block.tsx +60 -0
- package/src/ui/components/separator.tsx +42 -0
- package/src/ui/components/sheet.tsx +130 -0
- package/src/ui/components/sidebar.tsx +650 -0
- package/src/ui/components/silo-picker.tsx +510 -0
- package/src/ui/components/skeleton.tsx +13 -0
- package/src/ui/components/sonner.tsx +33 -0
- package/src/ui/components/switch.tsx +58 -0
- package/src/ui/components/table-view.ts +219 -0
- package/src/ui/components/table.tsx +506 -0
- package/src/ui/components/tabs.tsx +59 -0
- package/src/ui/components/timeago.tsx +84 -0
- package/src/ui/components/tooltip.tsx +56 -0
- package/src/ui/components.json +21 -0
- package/src/ui/dev/dev-shell.tsx +145 -0
- package/src/ui/home/home-grid.tsx +177 -0
- package/src/ui/hooks/use-mobile.ts +19 -0
- package/src/ui/index.ts +189 -0
- package/src/ui/install/adapter.ts +193 -0
- package/src/ui/install/deploy-targets-picker.tsx +185 -0
- package/src/ui/install/install-app-panel.tsx +912 -0
- package/src/ui/install/install-error.tsx +62 -0
- package/src/ui/install/perms-inactive.ts +50 -0
- package/src/ui/install/required-fields.ts +37 -0
- package/src/ui/install/tabs-form.tsx +557 -0
- package/src/ui/tokens.css +279 -0
- package/src/ui/utils.ts +78 -0
- package/src/ui-error.ts +139 -0
- package/src/ui-runtime/dev-landing-entry.tsx +258 -0
- package/src/ui-runtime/index.ts +83 -0
- package/src/ui-runtime/install-entry.tsx +119 -0
- package/src/ui-runtime/mount.ts +367 -0
- package/src/ui-runtime/runtime.css +56 -0
- package/src/ui-surface.ts +451 -0
- package/src/valkey.ts +43 -0
- package/src/worker/extract-tar.ts +125 -0
- package/src/worker/identity.ts +183 -0
- package/src/worker/index.ts +16 -0
- package/src/worker/public-ip.ts +92 -0
- package/src/worker/queue.ts +54 -0
- package/src/worker/reconcile.ts +127 -0
- package/src/worker/start-worker.ts +227 -0
- package/src/worker/ui-proxy.ts +335 -0
- package/src/worker/uplink.ts +523 -0
- package/src/worker/validate.ts +34 -0
- package/src/worker/verify-relay.ts +102 -0
- package/src/worker-protocol/app-dirs.ts +94 -0
- package/src/worker-protocol/home-proxy-paths.ts +76 -0
- package/src/worker-protocol/index.ts +55 -0
- package/src/worker-protocol/ingress.ts +166 -0
- package/src/worker-protocol/logs-socket-path.ts +50 -0
- package/src/worker-protocol/networks.ts +39 -0
- package/src/worker-protocol/node-version.ts +70 -0
- package/src/worker-protocol/silo-paths.ts +114 -0
- package/src/worker-protocol/spawn-paths.ts +165 -0
- package/src/worker-protocol/stats-socket-path.ts +40 -0
- package/src/worker-protocol/update-files.ts +77 -0
- package/src/worker-protocol/uplink/constants.ts +86 -0
- package/src/worker-protocol/uplink/errors.ts +61 -0
- package/src/worker-protocol/uplink/frame.ts +114 -0
- package/src/worker-protocol/uplink/identity.ts +40 -0
- package/src/worker-protocol/uplink/index.ts +63 -0
- package/src/worker-protocol/uplink/methods.ts +462 -0
- package/src/worker-protocol/uplink/mux.ts +646 -0
- package/src/worker-protocol/uplink/protocol.ts +11 -0
- package/src/worker-protocol/uplink/rpc.ts +309 -0
- package/src/worker-protocol/uplink/session.ts +540 -0
- package/src/worker-protocol/uplink/test-support.ts +188 -0
- package/src/worker-protocol/worker-mount.ts +27 -0
- package/src/worker-protocol/worker-state.ts +190 -0
- package/src/xml.ts +49 -0
- package/src/yaml.ts +15 -0
|
@@ -0,0 +1,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
|
+
}
|