@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,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
|
+
}
|