@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,451 @@
1
+ // Author-facing types for `ui:` on createApp. Authors declare pages and
2
+ // widgets here; renders are bundled separately for the Home iframe and
3
+ // loaders/actions become a JSON manifest Home can read after evaluate.
4
+
5
+ import type {
6
+ QueryClient,
7
+ UseMutationResult,
8
+ UseQueryOptions,
9
+ UseQueryResult,
10
+ } from "@tanstack/react-query";
11
+ import type { ZodType } from "zod";
12
+ import type { IconName } from "./ui/components/icons.tsx";
13
+ import type { Ref } from "./ref.ts";
14
+ import type { InferConfigSchema, SurfaceConfigSchema } from "./types.ts";
15
+
16
+ export type { IconName, SurfaceConfigSchema };
17
+
18
+ /**
19
+ * The resolved config object a surface's `configSchema` produces — each
20
+ * `r.input.*` leaf collapsed to its runtime value (text → string, number →
21
+ * number, boolean → boolean, enum → the union). Annotate a file-based loader's
22
+ * `request(state, config)` with `SurfaceConfig<typeof configSchema>` so `config`
23
+ * is typed the same way the inline `widget()`/`page()` wrappers infer it.
24
+ */
25
+ export type SurfaceConfig<T extends SurfaceConfigSchema> = InferConfigSchema<T>;
26
+
27
+ type InferInput<S> = S extends ZodType<infer T> ? T : never;
28
+ type InferOutput<S> = S extends ZodType<infer T> ? T : never;
29
+
30
+ /**
31
+ * What a loader/action's `request(state, config)` returns: the HTTP call
32
+ * to make against the surface's container. `service`/`port` are NOT here
33
+ * — those are the grant boundary (which container the app may reach), so
34
+ * they stay static on the loader and Home-controlled. This is only the
35
+ * *shape* of the call: verb, path, and where the input rides (query
36
+ * and/or body — a request can use both).
37
+ *
38
+ * A loader's `request` may also return `null`, meaning "not ready yet"
39
+ * (the state doesn't yet have what this call needs). `null` = lazy/idle;
40
+ * a shape = runnable. That's the whole eager-vs-lazy signal.
41
+ */
42
+ export type RequestMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
43
+
44
+ export interface RequestShape {
45
+ method: RequestMethod;
46
+ path: string;
47
+ /** Encoded onto the URL query string. Flat scalars. */
48
+ query?: Record<string, string | number | boolean>;
49
+ /** JSON body. Dropped on GET (which has none). */
50
+ body?: unknown;
51
+ }
52
+
53
+ /**
54
+ * An action mutates, so its request is never a GET: GET carries no body,
55
+ * and `mutate(input)`'s payload rides `body`. Narrowing the method here
56
+ * makes a GET action — which would silently drop its input at the worker
57
+ * proxy — a compile error at authoring time.
58
+ */
59
+ export interface ActionRequestShape extends Omit<RequestShape, "method"> {
60
+ method: Exclude<RequestMethod, "GET">;
61
+ }
62
+
63
+ /**
64
+ * One worker's answer to a fanned-out call, tagged with who gave it.
65
+ *
66
+ * On a system app a call goes to every worker the app runs on,
67
+ * so every response has an origin. The origin rides *on* the response
68
+ * rather than in a parallel array because a merge that filters or
69
+ * reorders would silently desync from one — and a mis-attributed row
70
+ * looks exactly like a correct one. Wrapping makes that unrepresentable.
71
+ *
72
+ * `data` is the container's response, already validated against the
73
+ * loader's `outputSchema`: the schema describes what the container
74
+ * returns, and never sees this envelope.
75
+ */
76
+ export interface WorkerResult<T> {
77
+ data: T;
78
+ /** Short slug of the worker that answered (`w1`, `w17`) — `worker.id` at eval. */
79
+ workerId: string;
80
+ }
81
+
82
+ /**
83
+ * The render fn's first arg. `loaders.{name}` / `actions.{name}` are
84
+ * prebuilt handles typed off the surface's loader/action keys, so
85
+ * authors get autocomplete matching what they declared. The render fn
86
+ * calls no hooks itself — the runtime builds every handle up front (and
87
+ * Home prefills the eager ones), so `loaders.X.data` is already present
88
+ * on first paint for a loader whose `request` returns a shape at mount.
89
+ *
90
+ * `state` / `setState` are the widget's own live UI state (shared across
91
+ * its loaders); `config` is the surface's resolved settings (set outside
92
+ * the widget). A loader's `request(state, config)` reads both.
93
+ */
94
+ export interface RenderApi<
95
+ // deno-lint-ignore no-explicit-any
96
+ TLoaders extends Record<string, Loader<any, any, any>> = Record<string, Loader>,
97
+ // deno-lint-ignore no-explicit-any
98
+ TActions extends Record<string, Action<any, any, any>> = Record<string, Action>,
99
+ TState = unknown,
100
+ TConfig = unknown,
101
+ > {
102
+ loaders: { [K in keyof TLoaders]: LoaderHandle<TLoaders[K]> };
103
+ actions: { [K in keyof TActions]: ActionHandle<TActions[K]> };
104
+ /** The widget's live UI state. Read-only here; mutate via `setState`. */
105
+ state: TState;
106
+ /** Patch the widget's state; re-keys every loader whose request depends on it. */
107
+ setState: (patch: Partial<TState>) => void;
108
+ /** Reset the widget's state to the `stateSchema` default (its seed). */
109
+ reset: () => void;
110
+ /** The surface's resolved config (settings set outside the widget). */
111
+ config: TConfig;
112
+ /**
113
+ * The surface's tanstack-query `QueryClient`. Use it for cross-loader
114
+ * cache work: `queryClient.invalidateQueries({ queryKey: ["loader", "stats"] })`
115
+ * to refetch every subscriber of a loader, prefetch a loader before
116
+ * rendering, or read a cached value out of band. For the common
117
+ * "refetch this one loader" case, the loader handle exposes `refetch`
118
+ * directly — reach for `queryClient` only when one handle isn't the
119
+ * right scope.
120
+ *
121
+ * Loader queries are keyed as `["loader", name, request]`.
122
+ */
123
+ queryClient: QueryClient;
124
+ }
125
+
126
+ /**
127
+ * The type of a surface's default-exported render component when a surface
128
+ * is authored as a standalone `pages/*.tsx` / `widgets/*.tsx` module rather
129
+ * than inline. `realm-sdk build` stitches each module's named exports
130
+ * (`title`, `icon`, `stateSchema`, `configSchema`, `loaders`, `actions`,
131
+ * `cols`/`rows`) plus this default render into the inline `ui:` block on
132
+ * `createApp`.
133
+ *
134
+ * Type the default export
135
+ * `Props<typeof loaders, typeof actions, typeof stateSchema, typeof configSchema>`
136
+ * so `loaders.{name}` / `actions.{name}` / `state` / `config` are typed off
137
+ * this file's own exports — the same inference the inline `page()` /
138
+ * `widget()` wrappers give.
139
+ */
140
+ export type Props<
141
+ // deno-lint-ignore no-explicit-any
142
+ TLoaders extends Record<string, Loader<any, any, any>> = Record<string, Loader>,
143
+ // deno-lint-ignore no-explicit-any
144
+ TActions extends Record<string, Action<any, any, any>> = Record<string, Action>,
145
+ TState extends ZodType = ZodType,
146
+ TConfig extends SurfaceConfigSchema = SurfaceConfigSchema,
147
+ // deno-lint-ignore no-explicit-any
148
+ > = (api: RenderApi<TLoaders, TActions, InferOutput<TState>, InferConfigSchema<TConfig>>) => any;
149
+
150
+ /**
151
+ * The `refetchInterval` knob authors set on a loader *definition* (it
152
+ * rides the bundle, so a function form survives — the JSON manifest and
153
+ * QuickJS can't carry a function). Picked 1:1 from tanstack-query's
154
+ * `UseQueryOptions` so authors can look up `refetchInterval` in tanstack
155
+ * docs and the type matches exactly.
156
+ */
157
+ export type LoaderQueryOptions = Pick<UseQueryOptions, "refetchInterval">;
158
+
159
+ /**
160
+ * A loader's `data` is the shape it hands the render fn. For a merge loader
161
+ * that's `merge`'s return type — which may differ from the per-worker
162
+ * `outputSchema` (folding N per-worker responses into one merged view). For a
163
+ * plain loader it's `InferOutput<outputSchema>`, which is also what `merge`'s
164
+ * type defaults to, so both resolve through the same read of `merge`'s return.
165
+ */
166
+ type LoaderData<
167
+ // deno-lint-ignore no-explicit-any
168
+ L extends Loader<any, any, any, any>,
169
+ // deno-lint-ignore no-explicit-any
170
+ > =
171
+ NonNullable<L["merge"]> extends (...args: any[]) => infer M ? M : InferOutput<L["outputSchema"]>;
172
+
173
+ /**
174
+ * A loader handle: the full tanstack-query `UseQueryResult` (so authors
175
+ * get `data` / `isLoading` / `error` / `refetch` / `isFetching` /
176
+ * `status` / `dataUpdatedAt`, everything tanstack docs cover).
177
+ *
178
+ * There's no per-loader `setState`/`reset` — a loader has no input of its
179
+ * own. It reads the *widget's* `state` via its `request`, so you drive it
180
+ * with the render API's `setState`/`reset`. An eager loader (its `request`
181
+ * returns a shape at mount) already carries `data` on first render; a lazy
182
+ * one (returns `null` until `state` supplies what it needs) is idle until
183
+ * a `setState` makes its `request` return a shape.
184
+ */
185
+ // deno-lint-ignore no-explicit-any
186
+ type LoaderHandle<L extends Loader<any, any, any, any>> = UseQueryResult<LoaderData<L>, Error>;
187
+
188
+ /**
189
+ * An action handle: tanstack's `UseMutationResult` so authors get
190
+ * `mutate`, `mutateAsync`, `isPending`, `reset`, etc. The output type
191
+ * narrows to `InferOutput<outputSchema>` when declared and falls back to
192
+ * `unknown` when omitted.
193
+ */
194
+ type ActionOutput<A> =
195
+ // deno-lint-ignore no-explicit-any
196
+ A extends Action<ZodType, any, infer Out>
197
+ ? Out extends ZodType
198
+ ? InferOutput<Out>
199
+ : unknown
200
+ : unknown;
201
+
202
+ // deno-lint-ignore no-explicit-any
203
+ type ActionHandle<A extends Action<any, any, any>> = UseMutationResult<
204
+ ActionOutput<A>,
205
+ Error,
206
+ InferInput<A["inputSchema"]>
207
+ >;
208
+
209
+ /**
210
+ * Read endpoint. Home forwards calls to a worker, which finds the named
211
+ * container by labels and dials it on the per-app `realm_<appId>` bridge.
212
+ *
213
+ * `service`/`port` name *which* container — the grant boundary, so they're
214
+ * static and Home-controlled. `request(state, config)` builds the actual
215
+ * call (`{ method, path, query?, body? }`) from the widget's state and
216
+ * config, or returns `null` for "not ready" (idle/lazy). Headers can be
217
+ * literal strings or refs (e.g. `r.stableSecret(...)`); Home resolves refs
218
+ * before the call so the iframe never sees the value.
219
+ *
220
+ * Generic on the widget's state/config types (so `request`'s params infer
221
+ * from the surface's `stateSchema`/`configSchema` when authored inline)
222
+ * and on the output Zod type (so `merge`'s `results` infers as
223
+ * `WorkerResult<InferOutput<TOut>>[]`). Wrap literals with `loader({...})`
224
+ * to pin them.
225
+ */
226
+ export interface Loader<
227
+ TState = unknown,
228
+ TConfig = unknown,
229
+ TOut extends ZodType = ZodType,
230
+ TMerged = InferOutput<TOut>,
231
+ > {
232
+ /** Service name within this app (the grant boundary). */
233
+ service: string;
234
+ /** Container port to forward to. */
235
+ port: number;
236
+ /** Static or ref headers (e.g. `r.stableSecret(...)`); Home resolves refs. */
237
+ headers?: Record<string, string | Ref<string>>;
238
+ outputSchema: TOut;
239
+ /**
240
+ * Build the call from the widget's state + config, or return `null`
241
+ * when the state isn't ready (→ idle/lazy). Runs in the iframe per
242
+ * state change; also run once at eval against the state/config defaults
243
+ * so Home can prefill the eager case.
244
+ */
245
+ request: (state: TState, config: TConfig) => RequestShape | null;
246
+ /**
247
+ * Fan out across every worker the app is deployed on, then reduce the
248
+ * per-worker responses to the shape this loader hands the render fn as
249
+ * `data`. That merged shape need not match `outputSchema`: `outputSchema`
250
+ * is the per-worker wire contract (validated once per response, never
251
+ * against the merged result), while `merge`'s return is the view — e.g.
252
+ * folding N single-host responses into one `{ hosts: [...] }` list, or
253
+ * tagging each with the worker that answered. Defaults to
254
+ * `InferOutput<outputSchema>` for the common flatten-in-place case.
255
+ */
256
+ merge?: (results: WorkerResult<InferOutput<TOut>>[]) => TMerged;
257
+ /**
258
+ * Poll interval for this loader (ms), or a function of the query that
259
+ * returns one (return `false` to stop). Lives on the definition — not
260
+ * the call site — so it rides the esbuild bundle; a function interval
261
+ * can't survive the JSON manifest / QuickJS boundary. Omitted = no
262
+ * polling.
263
+ */
264
+ refetchInterval?: LoaderQueryOptions["refetchInterval"];
265
+ }
266
+
267
+ /**
268
+ * Mutation endpoint. Like `Loader` but the caller passes an explicit
269
+ * `input` at call time (`mutateAsync(input)`) rather than reading the
270
+ * widget's state, and the runtime hook is a tanstack-query mutation
271
+ * (`useMutation`) — authors get `mutate` / `mutateAsync` / `isPending`
272
+ * instead of `data` / `refetch`. `request(input, config)` builds the call
273
+ * from that argument and the surface config.
274
+ *
275
+ * On a system app the action fans out to every reachable worker
276
+ * and `mutateAsync` resolves with `{ results, failures }`, where `results`
277
+ * is a {@link WorkerResult} per worker that answered. Actions don't have a
278
+ * `merge` knob — the caller decides what to do with the per-worker array
279
+ * (usually nothing; just invalidate a loader). On a single app the action
280
+ * targets one worker and resolves with the upstream body directly.
281
+ *
282
+ * Authors invalidate loader caches after a mutation either via the loader
283
+ * handle's `.refetch()` or
284
+ * `queryClient.invalidateQueries({ queryKey: ["loader", "name"] })`.
285
+ */
286
+ export interface Action<
287
+ TIn extends ZodType = ZodType,
288
+ TConfig = unknown,
289
+ TOut extends ZodType = ZodType,
290
+ > {
291
+ inputSchema: TIn;
292
+ /**
293
+ * Optional. If omitted, Home returns the upstream body unparsed
294
+ * (`unknown`). Tanstack's `mutateAsync` still resolves with that
295
+ * value; just no schema-checked narrowing.
296
+ */
297
+ outputSchema?: TOut;
298
+ /** Service name within this app (the grant boundary). */
299
+ service: string;
300
+ /** Container port to forward to. */
301
+ port: number;
302
+ /** Static or ref headers (e.g. `r.stableSecret(...)`); Home resolves refs. */
303
+ headers?: Record<string, string | Ref<string>>;
304
+ /** Build the call from the mutate argument + surface config. */
305
+ request: (input: InferInput<TIn>, config: TConfig) => ActionRequestShape;
306
+ }
307
+
308
+ /**
309
+ * A page mounts as a sidebar entry in Home. Author owns the whole
310
+ * route's content area; no tile chrome, no size hints.
311
+ */
312
+ export interface Page<
313
+ TState extends ZodType = ZodType,
314
+ TConfig extends SurfaceConfigSchema = SurfaceConfigSchema,
315
+ // deno-lint-ignore no-explicit-any
316
+ TLoaders extends Record<string, Loader<any, any, any>> = Record<string, Loader>,
317
+ // deno-lint-ignore no-explicit-any
318
+ TActions extends Record<string, Action<any, any, any>> = Record<string, Action>,
319
+ > {
320
+ title: string;
321
+ /**
322
+ * Icon name from `icons.tsx`, e.g. `"ListChecksIcon"` or `"CubeIcon"`.
323
+ * Drives the sidebar entry / tile glyph at first install; Home
324
+ * operator can override it later. Required so every surface is a
325
+ * deliberate choice — pick `"FadersIcon"` if you genuinely want the
326
+ * generic look.
327
+ */
328
+ icon: IconName;
329
+ /**
330
+ * The surface's live UI state, driven from inside the widget via
331
+ * `setState`. A loader's `request` reads it. Omit for a surface with no
332
+ * interactive state (every loader eager). Its `{}`-parse seeds the
333
+ * initial state.
334
+ */
335
+ stateSchema?: TState;
336
+ loaders?: TLoaders;
337
+ actions?: TActions;
338
+ // deno-lint-ignore no-explicit-any
339
+ render: (
340
+ api: RenderApi<TLoaders, TActions, InferOutput<TState>, InferConfigSchema<TConfig>>,
341
+ ) => any;
342
+ }
343
+
344
+ /**
345
+ * A widget mounts as a tile on the Home homepage grid. Adds size hints
346
+ * to the page shape: `cols`/`rows` count 8rem grid cells, both default
347
+ * to 2 (16rem square) when omitted.
348
+ *
349
+ * Below the `sm` breakpoint (640px) the grid collapses to two
350
+ * full-width cells: `cols` clamps to 2 and `rows` grows proportionally
351
+ * (`ceil(cols * rows / min(cols, 2))`) so the widget keeps its area
352
+ * for content to reflow into. A `rows === 1` widget renders without
353
+ * the title-bar chrome on every viewport — the author owns labeling
354
+ * inside the iframe — and that intent carries across the mobile
355
+ * collapse even when effective rows grow.
356
+ *
357
+ * `cols` and `rows` must be integers ≥ 1.
358
+ */
359
+ export interface Widget<
360
+ TState extends ZodType = ZodType,
361
+ TConfig extends SurfaceConfigSchema = SurfaceConfigSchema,
362
+ // deno-lint-ignore no-explicit-any
363
+ TLoaders extends Record<string, Loader<any, any, any>> = Record<string, Loader>,
364
+ // deno-lint-ignore no-explicit-any
365
+ TActions extends Record<string, Action<any, any, any>> = Record<string, Action>,
366
+ > extends Page<TState, TConfig, TLoaders, TActions> {
367
+ /**
368
+ * The widget's settings, set outside the widget on the tile's config screen
369
+ * and persisted per-placement by Home. A flat record of `r.input.*` leaves
370
+ * (text / number / boolean / enum — no secrets, no permissions); the config
371
+ * form renders from it and a loader's `request(state, config)` reads the
372
+ * resolved values. Widgets only — pages aren't configurable.
373
+ */
374
+ configSchema?: TConfig;
375
+ cols?: number;
376
+ rows?: number;
377
+ }
378
+
379
+ /** The `ui:` block on `createApp`. */
380
+ export interface UiConfig {
381
+ // deno-lint-ignore no-explicit-any
382
+ pages?: Record<string, Page<any, any, any, any>>;
383
+ // deno-lint-ignore no-explicit-any
384
+ widgets?: Record<string, Widget<any, any, any, any>>;
385
+ }
386
+
387
+ /**
388
+ * Capture per-call generics so `request`'s `(state, config)` params infer
389
+ * from the surface's `stateSchema`/`configSchema` (when authored inline)
390
+ * and `merge`'s `results` infers as `z.infer<outputSchema>[]`.
391
+ *
392
+ * Identity at runtime — TS-only ergonomics. Always wrap loader literals
393
+ * with this.
394
+ */
395
+ export function loader<
396
+ TOut extends ZodType,
397
+ TMerged = InferOutput<TOut>,
398
+ TState = unknown,
399
+ TConfig = unknown,
400
+ >(def: Loader<TState, TConfig, TOut, TMerged>): Loader<TState, TConfig, TOut, TMerged> {
401
+ return def;
402
+ }
403
+
404
+ /**
405
+ * Action counterpart to `loader()`. Identity at runtime; the wrapper lets
406
+ * TS pin the per-call generics on `inputSchema` / `outputSchema` so
407
+ * `actions.{name}.mutateAsync(input)` is typed against the exact shapes
408
+ * the author declared.
409
+ */
410
+ export function action<TIn extends ZodType, TConfig = unknown, TOut extends ZodType = ZodType>(
411
+ def: Action<TIn, TConfig, TOut>,
412
+ ): Action<TIn, TConfig, TOut> {
413
+ return def;
414
+ }
415
+
416
+ /**
417
+ * Capture a page's `stateSchema` / `loaders` / `actions` so the `render`
418
+ * function's handles + `state` are typed per-surface. Without this wrapper the
419
+ * outer `Record<string, Page>` constraint on `UiConfig.pages` widens them,
420
+ * losing inference. Pages take no config (only widgets are configurable), so
421
+ * the config type is pinned empty and a `configSchema` here is a type error.
422
+ */
423
+ export function page<
424
+ TState extends ZodType,
425
+ // deno-lint-ignore no-explicit-any
426
+ TLoaders extends Record<string, Loader<any, any, any>>,
427
+ // deno-lint-ignore no-explicit-any
428
+ TActions extends Record<string, Action<any, any, any>>,
429
+ >(
430
+ def: Page<TState, Record<string, never>, TLoaders, TActions>,
431
+ ): Page<TState, Record<string, never>, TLoaders, TActions> {
432
+ return def;
433
+ }
434
+
435
+ /** Widget counterpart to `page()` — same role, accepts `cols`/`rows`. */
436
+ export function widget<
437
+ TState extends ZodType,
438
+ TConfig extends SurfaceConfigSchema,
439
+ // deno-lint-ignore no-explicit-any
440
+ TLoaders extends Record<string, Loader<any, any, any>>,
441
+ // deno-lint-ignore no-explicit-any
442
+ TActions extends Record<string, Action<any, any, any>>,
443
+ >(def: Widget<TState, TConfig, TLoaders, TActions>): Widget<TState, TConfig, TLoaders, TActions> {
444
+ return def;
445
+ }
446
+
447
+ // Wire-side manifest types (`SurfaceManifest`, `WidgetManifest`,
448
+ // `UiManifest`, `LoaderManifest`, `ManifestHeader`) live in
449
+ // `resolve.ts` where the zod schemas they're inferred from are
450
+ // declared. Internal SDK code imports them from `./resolve.ts`;
451
+ // external consumers go through the `@yourrealm/sdk/compile` barrel.
package/src/valkey.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { Ref } from "./ref.ts";
2
+ import { interpolate as i } from "./ref.ts";
3
+ import { Service } from "./service.ts";
4
+
5
+ interface ValkeyOptions {
6
+ memory?: string;
7
+ }
8
+
9
+ export class Valkey extends Service {
10
+ readonly image: string;
11
+ readonly port = 6379;
12
+
13
+ constructor(_options?: ValkeyOptions) {
14
+ super();
15
+ this.image = "valkey/valkey:8";
16
+ }
17
+
18
+ get password(): Ref<string> {
19
+ return this.derived("valkey-password");
20
+ }
21
+
22
+ get url(): Ref<string> {
23
+ return i`redis://default:${this.password}@${this.host}:${this.port}/0`;
24
+ }
25
+
26
+ getServiceDefinition() {
27
+ return {
28
+ image: this.image,
29
+ volumes: [
30
+ {
31
+ hostPath: i`./data/${this.host}`,
32
+ containerPath: "/var/lib/valkey/data",
33
+ backup: { kind: "valkey-save" as const, url: this.url },
34
+ },
35
+ ],
36
+ command: ["--requirepass", this.password],
37
+ env: { REDISCLI_AUTH: this.password },
38
+ healthcheck: {
39
+ test: ["CMD", "valkey-cli", "PING"],
40
+ },
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,125 @@
1
+ import { dirname, resolve } from "@std/path";
2
+ import { log } from "@local/logger";
3
+ import { UntarStream } from "@std/tar";
4
+
5
+ // state.json carries every resolved env / API key / derived password
6
+ // for every app on this worker. It's captured in memory and parsed
7
+ // in-process, never landing on disk. Bind-mount artifacts (under
8
+ // files/) do hit disk, but with 0o600 perms.
9
+ const IN_MEMORY_TAR_ENTRY = "state.json";
10
+
11
+ // Entries at or under this land in one write(2); larger ones stream, and a
12
+ // stream can be observed mid-write. In practice only artifacts get this big,
13
+ // and artifacts are content-addressed — a rewrite puts back byte-identical
14
+ // bytes, so there's nothing to observe. Nothing caps an inline file's size
15
+ // though, so a big enough one gives up the single-syscall guarantee.
16
+ const BUFFERED_WRITE_MAX_BYTES = 8 * 1024 * 1024;
17
+
18
+ /** `write(2)` can come up short on a large buffer; loop until it all lands. */
19
+ async function writeAll(file: Deno.FsFile, buf: Uint8Array<ArrayBufferLike>): Promise<number> {
20
+ let offset = 0;
21
+ while (offset < buf.length) offset += await file.write(buf.subarray(offset));
22
+ return buf.length;
23
+ }
24
+
25
+ interface ExtractResult {
26
+ /** Plaintext state.json content captured in memory, never written to disk. */
27
+ stateJsonString: string | null;
28
+ }
29
+
30
+ /**
31
+ * Extract a gzipped tar stream under destDir. Rejects entries whose
32
+ * resolved path escapes destDir (path traversal via `..` or
33
+ * absolute paths). Only regular files and directories are written;
34
+ * symlinks, hardlinks, devices, FIFOs, etc. are skipped — Home
35
+ * emits only typeflags "0" (file) and "5" (directory), so anything
36
+ * else on the wire is either malformed or adversarial.
37
+ *
38
+ * The entry whose resolved path equals `destDir/state.json` is
39
+ * captured into the returned `ExtractResult` and never written to
40
+ * disk — state.json holds resolved secrets and is parsed in memory.
41
+ * The compare runs on the *resolved* dest so path variants like
42
+ * `./././state.json` can't bypass it. Everything else streams to
43
+ * disk with 0o600 (files) / 0o700 (dirs); we chmod after every
44
+ * write so a pre-existing inode with looser permissions gets
45
+ * retightened (Deno's `mode` arg only applies on creation).
46
+ *
47
+ * Files are rewritten **in place, on the existing inode**. Inline
48
+ * files are keyed by identity (sha of `appId:serviceKey:fileKey`),
49
+ * not content, so a content change reuses the same path — and that
50
+ * path is a single-file bind mount into containers that are already
51
+ * running. Docker attaches the inode, so the usual write-to-temp-
52
+ * then-rename recipe would rebind only the name and freeze every
53
+ * running container at the bytes it booted with, silently and
54
+ * forever. So: no `O_TRUNC` (the file is never observably empty),
55
+ * one `write(2)` for anything that fits in memory (a racing reader's
56
+ * window is a syscall, not the whole network transfer), and
57
+ * `truncate` last (a shrink reads as new-prefix + old-tail, which
58
+ * won't parse, rather than a valid bare prefix).
59
+ */
60
+ export async function extractTarToWorkerDir(
61
+ tarGz: ReadableStream<Uint8Array<ArrayBufferLike>>,
62
+ destDir: string,
63
+ ): Promise<ExtractResult> {
64
+ const root = resolve(destDir);
65
+ const rootPrefix = root + "/";
66
+ const stateDest = resolve(root, IN_MEMORY_TAR_ENTRY);
67
+ let stateJsonString: string | null = null;
68
+
69
+ const untarred = (tarGz as ReadableStream<Uint8Array<ArrayBuffer>>)
70
+ .pipeThrough(new DecompressionStream("gzip"))
71
+ .pipeThrough(new UntarStream());
72
+
73
+ for await (const entry of untarred) {
74
+ const dest = resolve(root, entry.path);
75
+ if (dest !== root && !dest.startsWith(rootPrefix)) {
76
+ await entry.readable?.cancel();
77
+ throw new Error(`tar entry escapes destination: ${entry.path}`);
78
+ }
79
+
80
+ const typeflag = entry.header.typeflag;
81
+ if (typeflag === "5") {
82
+ await Deno.mkdir(dest, { recursive: true, mode: 0o700 });
83
+ await Deno.chmod(dest, 0o700);
84
+ } else if (typeflag === "0" || typeflag === "") {
85
+ // The size field is `parseInt(octal)`, so a corrupt header yields NaN (or
86
+ // a negative), and UntarStream then hands back an empty body — which is
87
+ // indistinguishable from a legitimately empty entry right up until it
88
+ // truncates a live bind-mounted file to zero. Reject instead of writing.
89
+ const size = entry.header.size;
90
+ if (!Number.isFinite(size) || size < 0) {
91
+ await entry.readable?.cancel();
92
+ throw new Error(`tar entry has an invalid size header: ${entry.path}`);
93
+ }
94
+
95
+ if (dest === stateDest) {
96
+ const buf = new Uint8Array(await new Response(entry.readable!).arrayBuffer());
97
+ stateJsonString = new TextDecoder().decode(buf);
98
+ continue;
99
+ }
100
+ const parent = dirname(dest);
101
+ await Deno.mkdir(parent, { recursive: true, mode: 0o700 });
102
+ await Deno.chmod(parent, 0o700);
103
+
104
+ const buffered =
105
+ size <= BUFFERED_WRITE_MAX_BYTES
106
+ ? new Uint8Array(await new Response(entry.readable!).arrayBuffer())
107
+ : null;
108
+ const file = await Deno.open(dest, { write: true, create: true, mode: 0o600 });
109
+ try {
110
+ let written = 0;
111
+ if (buffered !== null) written = await writeAll(file, buffered);
112
+ else for await (const chunk of entry.readable!) written += await writeAll(file, chunk);
113
+ await file.truncate(written);
114
+ } finally {
115
+ file.close();
116
+ }
117
+ await Deno.chmod(dest, 0o600);
118
+ } else {
119
+ log.warn(`Skipping tar entry with unsupported typeflag "${typeflag}": ${entry.path}`);
120
+ await entry.readable?.cancel();
121
+ }
122
+ }
123
+
124
+ return { stateJsonString };
125
+ }