@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
package/src/ref.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand symbols for the two ref categories. Distinct symbols (rather
|
|
3
|
+
* than a shared one) make `Ref<T>` and `Deferred<T>` non-assignable to
|
|
4
|
+
* each other at the type level — `interpolate` accepts the first but
|
|
5
|
+
* not the second, so the broken composition `interpolate\`${apply(...)}\``
|
|
6
|
+
* fails to typecheck instead of producing a malformed marker tree.
|
|
7
|
+
*/
|
|
8
|
+
export const REF_SYMBOL = Symbol.for("realm.ref");
|
|
9
|
+
const DEFERRED_SYMBOL = Symbol.for("realm.deferred");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sync leaf markers — single-step lookups the walker resolves with no
|
|
13
|
+
* user-code round-trip. Home maps marker → string against a grant-
|
|
14
|
+
* scoped allow-map.
|
|
15
|
+
*/
|
|
16
|
+
export type LeafRefMarker =
|
|
17
|
+
| { kind: "derived"; appId: string; name: string }
|
|
18
|
+
| { kind: "shared"; providerAppId: string; serviceKey: string; name: string }
|
|
19
|
+
| { kind: "secret"; path: string };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wire-shape markers `Ref<T>` carries. Either a single leaf or a flat
|
|
23
|
+
* concat of leaves. Two structural invariants the wire schema enforces:
|
|
24
|
+
*
|
|
25
|
+
* 1. concat parts contain only leaves — no nested concat (interpolate
|
|
26
|
+
* flattens at build time) and no deferred sub-parts (interpolate
|
|
27
|
+
* rejects `Deferred<T>` at the type level).
|
|
28
|
+
* 2. `Deferred<T>` is a separate brand entirely — never appears here.
|
|
29
|
+
*
|
|
30
|
+
* Together these mean the walker's concat handling is plain sync
|
|
31
|
+
* recursion over leaves, not a two-pass placeholder dance.
|
|
32
|
+
*/
|
|
33
|
+
export type SyncRefMarker = LeafRefMarker | { kind: "concat"; parts: (string | LeafRefMarker)[] };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wire shape for `apply()`. Distinct from `SyncRefMarker` because the
|
|
37
|
+
* walker has to register a placeholder and round-trip through a second
|
|
38
|
+
* sandbox eval to invoke the user fn.
|
|
39
|
+
*/
|
|
40
|
+
export type DeferredRefMarker = { kind: "deferred"; id: number; input: unknown };
|
|
41
|
+
|
|
42
|
+
export type RefMarker = SyncRefMarker | DeferredRefMarker;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Sync ref — secrets, derived/shared passwords, concat templates. The
|
|
46
|
+
* walker resolves them in one pass with no user-code round-trip. Always
|
|
47
|
+
* the type accepted by `interpolate`.
|
|
48
|
+
*/
|
|
49
|
+
export interface Ref<T> {
|
|
50
|
+
readonly [REF_SYMBOL]: true;
|
|
51
|
+
toJSON(): { __realmRef: SyncRefMarker } | T;
|
|
52
|
+
toString(): never;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Deferred ref — produced only by `apply()`. Resolution requires a
|
|
57
|
+
* second sandbox call to invoke the user fn, so deferred refs cannot
|
|
58
|
+
* compose into `interpolate` (a deferred sub-part inside a concat would
|
|
59
|
+
* force the walker to mix synchronous and second-pass resolution mid-
|
|
60
|
+
* concat, which the wire schema forbids). Wrap affix logic inside the
|
|
61
|
+
* deferred fn instead.
|
|
62
|
+
*/
|
|
63
|
+
export interface Deferred<T> {
|
|
64
|
+
readonly [DEFERRED_SYMBOL]: true;
|
|
65
|
+
// Phantom `| T` parallels `Ref<T>` so `Deferred<string>` and
|
|
66
|
+
// `Deferred<number>` brand distinctly at the type level. Always the
|
|
67
|
+
// marker shape at runtime — the user fn's return value is spliced in
|
|
68
|
+
// by the walker, never produced by toJSON.
|
|
69
|
+
toJSON(): { __realmRef: DeferredRefMarker } | T;
|
|
70
|
+
toString(): never;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Anything that becomes a `T` at deploy time — sync ref or deferred.
|
|
75
|
+
* Use this for field types that accept either (env values, file
|
|
76
|
+
* contents, etc.). `interpolate` is the exception: it accepts `Ref<T>`
|
|
77
|
+
* only, since deferred values can't sit inside a concat template.
|
|
78
|
+
*/
|
|
79
|
+
export type Resolvable<T> = Ref<T> | Deferred<T>;
|
|
80
|
+
|
|
81
|
+
function isRef(value: unknown): value is Ref<string> {
|
|
82
|
+
return typeof value === "object" && value !== null && REF_SYMBOL in value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True if `value` holds a ref anywhere in its tree, in either form: a
|
|
87
|
+
* branded `Ref` (from author helpers like `r.stableSecret` / `interpolate`)
|
|
88
|
+
* or a bare `{__realmRef}` marker (what a config secret value already is by
|
|
89
|
+
* the time `getDesiredState` runs — symbols don't survive the sandbox's
|
|
90
|
+
* JSON boundary). The serializers (toJson/toYaml/toIni/toXml) use this to
|
|
91
|
+
* choose the eager path (no refs → plain string) over the deferred path
|
|
92
|
+
* (refs → resolve via apply(), then serialize).
|
|
93
|
+
*/
|
|
94
|
+
export function containsRef(value: unknown): boolean {
|
|
95
|
+
if (isRef(value) || isRefMarker(value)) return true;
|
|
96
|
+
if (Array.isArray(value)) return value.some(containsRef);
|
|
97
|
+
if (typeof value === "object" && value !== null) {
|
|
98
|
+
for (const v of Object.values(value as Record<string, unknown>)) {
|
|
99
|
+
if (containsRef(v)) return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isRefMarker(value: unknown): value is { __realmRef: RefMarker } {
|
|
106
|
+
return (
|
|
107
|
+
typeof value === "object" &&
|
|
108
|
+
value !== null &&
|
|
109
|
+
"__realmRef" in value &&
|
|
110
|
+
typeof (value as { __realmRef: unknown }).__realmRef === "object"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function throwOnToString(): never {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"Refs cannot be stringified directly. Use interpolate`...` for templates " +
|
|
117
|
+
"or pass the ref through as an env/command/volume value.",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Marker for a `User` field read inside `getTrustedHeaders`. Unlike every
|
|
123
|
+
* other ref it never reaches the worker wire or the resolve-refs walker:
|
|
124
|
+
* `createApp`'s sampler reads it at deploy time to build the static
|
|
125
|
+
* header→field map, and the ForwardAuth verify hop substitutes the real
|
|
126
|
+
* value per request. Kept out of `SyncRefMarker` for exactly that reason —
|
|
127
|
+
* the wire schema and walker never have to know about it.
|
|
128
|
+
*/
|
|
129
|
+
type IdentityRefMarker = { kind: "identity"; field: "username" | "email" };
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build the `Ref<string>` handed to `getTrustedHeaders` for `user.username` /
|
|
133
|
+
* `user.email`. At deploy time there is no real user, so the value is a
|
|
134
|
+
* deferred marker the verify hop fills in per request — the same "string
|
|
135
|
+
* not known until later" idiom as `db.url`. Composes through `interpolate`
|
|
136
|
+
* like any other ref (the sampler resolves the resulting concat).
|
|
137
|
+
*/
|
|
138
|
+
export function identityRef(field: IdentityRefMarker["field"]): Ref<string> {
|
|
139
|
+
// The identity marker is deliberately outside `SyncRefMarker` (it never
|
|
140
|
+
// reaches the wire), so toJSON doesn't structurally match `Ref<string>` -
|
|
141
|
+
// cast through `unknown`. Runtime shape is identical to every other ref.
|
|
142
|
+
return {
|
|
143
|
+
[REF_SYMBOL]: true,
|
|
144
|
+
toJSON: () => ({ __realmRef: { kind: "identity", field } }),
|
|
145
|
+
toString: throwOnToString,
|
|
146
|
+
} as unknown as Ref<string>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build a sync ref that serializes as `{__realmRef: marker}`.
|
|
151
|
+
*/
|
|
152
|
+
export function ref(marker: SyncRefMarker): Ref<string> {
|
|
153
|
+
return {
|
|
154
|
+
[REF_SYMBOL]: true,
|
|
155
|
+
toJSON: () => ({ __realmRef: marker }),
|
|
156
|
+
toString: throwOnToString,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build a sync ref whose toJSON resolves to a plain string at
|
|
162
|
+
* serialization time. Used for values that don't need Home-side
|
|
163
|
+
* resolution (e.g. the service's DNS name — a literal known once
|
|
164
|
+
* `_setServiceKey` runs, which happens before `getServiceDefinition`).
|
|
165
|
+
*/
|
|
166
|
+
export function stringRef(resolve: () => string): Ref<string> {
|
|
167
|
+
return {
|
|
168
|
+
[REF_SYMBOL]: true,
|
|
169
|
+
toJSON: resolve,
|
|
170
|
+
toString: throwOnToString,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build a deferred ref. Internal — `apply()` in apply.ts is the public
|
|
176
|
+
* entry point; this helper centralizes the brand + toJSON shape.
|
|
177
|
+
*/
|
|
178
|
+
export function deferredRef(marker: DeferredRefMarker): Deferred<string> {
|
|
179
|
+
return {
|
|
180
|
+
[DEFERRED_SYMBOL]: true,
|
|
181
|
+
toJSON: () => ({ __realmRef: marker }),
|
|
182
|
+
toString: throwOnToString,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Tagged template literal that handles refs.
|
|
188
|
+
*
|
|
189
|
+
* Literal parts and plain values fold in at serialization time. Sync
|
|
190
|
+
* refs keep their marker; string-producing refs fold to strings. The
|
|
191
|
+
* result is always a single concat marker with parts in source order
|
|
192
|
+
* (adjacent strings are not pre-joined — the walker collapses them).
|
|
193
|
+
*
|
|
194
|
+
* `Deferred<string>` is rejected at the type level: a deferred sub-part
|
|
195
|
+
* inside a concat would need to mix sync + second-pass resolution mid-
|
|
196
|
+
* template, and the wire schema forbids it. Move affix logic inside the
|
|
197
|
+
* apply callback: `apply(input, s => \`prefix-${s}\`)` instead of
|
|
198
|
+
* `interpolate\`prefix-${apply(input, s => s)}\``.
|
|
199
|
+
*
|
|
200
|
+
* Usage:
|
|
201
|
+
* interpolate`postgres://${db.user}:${db.password}@${db.host}:${db.port}/${db.database}`
|
|
202
|
+
*/
|
|
203
|
+
function interpolate(
|
|
204
|
+
strings: TemplateStringsArray,
|
|
205
|
+
...values: (string | Ref<string> | number)[]
|
|
206
|
+
): Ref<string> {
|
|
207
|
+
return {
|
|
208
|
+
[REF_SYMBOL]: true,
|
|
209
|
+
toJSON() {
|
|
210
|
+
const parts: (string | LeafRefMarker)[] = [];
|
|
211
|
+
let allStrings = true;
|
|
212
|
+
for (let i = 0; i < strings.length; i++) {
|
|
213
|
+
const literal = strings[i]!;
|
|
214
|
+
if (literal.length > 0) parts.push(literal);
|
|
215
|
+
if (i < values.length) {
|
|
216
|
+
const val = values[i];
|
|
217
|
+
// Three runtime shapes: a `Ref<string>`, whose marker comes off
|
|
218
|
+
// `.toJSON()`; a plain `{__realmRef: ...}` object — what a Ref
|
|
219
|
+
// looks like after crossing the host→sandbox JSON boundary, which
|
|
220
|
+
// realm.ts authors hit interpolating an `r.input.secret(...)`
|
|
221
|
+
// field; or anything else, which coerces to a string.
|
|
222
|
+
let marker: SyncRefMarker | null = null;
|
|
223
|
+
if (isRef(val)) {
|
|
224
|
+
const resolved = val.toJSON();
|
|
225
|
+
if (typeof resolved === "object" && resolved !== null && "__realmRef" in resolved) {
|
|
226
|
+
marker = (resolved as { __realmRef: SyncRefMarker }).__realmRef;
|
|
227
|
+
} else {
|
|
228
|
+
parts.push(String(resolved));
|
|
229
|
+
}
|
|
230
|
+
} else if (isRefMarker(val)) {
|
|
231
|
+
// Runtime guard: a deferred marker can only land here via a
|
|
232
|
+
// hand-crafted `as unknown as string` cast (the type rejects
|
|
233
|
+
// proper `Deferred<string>`). Reject it loudly so the wire
|
|
234
|
+
// schema isn't the only line of defense.
|
|
235
|
+
const m = val.__realmRef;
|
|
236
|
+
if (m.kind === "deferred") {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"interpolate cannot accept a deferred ref; move affix logic into apply()",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
marker = m;
|
|
242
|
+
} else {
|
|
243
|
+
parts.push(String(val));
|
|
244
|
+
}
|
|
245
|
+
if (marker) {
|
|
246
|
+
// Flatten nested concats so the walker sees a single level.
|
|
247
|
+
// `interpolate` is the only path that produces a concat, so an
|
|
248
|
+
// already-concat sub-marker came from another one upstream.
|
|
249
|
+
if (marker.kind === "concat") {
|
|
250
|
+
for (const p of marker.parts) {
|
|
251
|
+
parts.push(p);
|
|
252
|
+
if (typeof p !== "string") allStrings = false;
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
parts.push(marker);
|
|
256
|
+
allStrings = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// When every part is already a plain string the template has no
|
|
262
|
+
// deferred resolution to do — collapse it to a literal so it can
|
|
263
|
+
// land in strict string-typed fields (e.g. volume hostPath).
|
|
264
|
+
if (allStrings) return (parts as string[]).join("");
|
|
265
|
+
return { __realmRef: { kind: "concat", parts } };
|
|
266
|
+
},
|
|
267
|
+
toString: throwOnToString,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export { interpolate };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Per-kind tagging for remote.* permissions.
|
|
2
|
+
//
|
|
3
|
+
// Remote kinds carry no Home-side dispatch hooks (no iterateUses /
|
|
4
|
+
// extract) — Home handles fetchUrl + artifact through dedicated endpoints
|
|
5
|
+
// rather than by walking a registry. The factory tags only kind + meta,
|
|
6
|
+
// enough for derivePermissionMeta and permissions.ts's type-level kind
|
|
7
|
+
// deriver to pick them up.
|
|
8
|
+
|
|
9
|
+
import type { z } from "zod";
|
|
10
|
+
import type { Builder } from "./shared.ts";
|
|
11
|
+
import {
|
|
12
|
+
tagPermissionMeta,
|
|
13
|
+
type PermissionMeta,
|
|
14
|
+
type PermissionMetaTagged,
|
|
15
|
+
} from "./permission-meta.ts";
|
|
16
|
+
|
|
17
|
+
// Loose constraint used inside this module. The closed
|
|
18
|
+
// `RemotePermissionKind` is derived in permissions.ts.
|
|
19
|
+
type RemoteKindShape = `remote.${string}`;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bind an inner Builder (basePermission or schemablePermission) and
|
|
23
|
+
* return a factory-builder that produces tagged remote-permission
|
|
24
|
+
* factories. Mirrors makeHostable / makePeerable.
|
|
25
|
+
*/
|
|
26
|
+
export function makeRemotable<BaseR extends object, KindUnion extends RemoteKindShape>(
|
|
27
|
+
inner: Builder<BaseR, "perm", KindUnion>,
|
|
28
|
+
) {
|
|
29
|
+
return <S extends z.ZodType, K extends KindUnion>(kind: K, schema: S, meta: PermissionMeta) => {
|
|
30
|
+
const factory = inner(kind, schema);
|
|
31
|
+
return tagPermissionMeta(factory, kind, meta) as typeof factory & PermissionMetaTagged<K>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Partition the host permission kinds an app declares into "required"
|
|
2
|
+
// vs "optional" sets. Required ⇒ the permission is at the root (or
|
|
3
|
+
// nested inside non-optional tabs/sections only) AND doesn't carry
|
|
4
|
+
// `.optional()` on its leaf meta. Anything else is optional.
|
|
5
|
+
//
|
|
6
|
+
// Two consumers: Home's validation gates (apps-logic / apps-recompute)
|
|
7
|
+
// drop workers whose advertised capabilities don't cover an app's required
|
|
8
|
+
// kinds, and the install panel grays out optional permissions whose kind no
|
|
9
|
+
// worker can satisfy. It lives in the SDK so server eval and dashboard
|
|
10
|
+
// islands consume the same rule.
|
|
11
|
+
|
|
12
|
+
import type { ConfigFieldTree, SerializablePermission } from "./sandbox.ts";
|
|
13
|
+
import type { HostPermissionKind } from "./permissions.ts";
|
|
14
|
+
import type { HomeApp } from "./resolve.ts";
|
|
15
|
+
|
|
16
|
+
function isHostKind(kind: string): kind is HostPermissionKind {
|
|
17
|
+
return kind.startsWith("host.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Dotted paths of permission leaves that live inside an
|
|
22
|
+
* optional-section (or optional-tab) subtree. Schema-only — runtime
|
|
23
|
+
* `values` aren't consulted here, declaration-time optionality is what
|
|
24
|
+
* decides whether a permission counts as required for capability
|
|
25
|
+
* gating.
|
|
26
|
+
*/
|
|
27
|
+
function pathsUnderOptionalAncestor(schema: ConfigFieldTree): Set<string> {
|
|
28
|
+
const optionalPrefixes: string[] = [];
|
|
29
|
+
function walk(fields: ConfigFieldTree, prefix: string) {
|
|
30
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
31
|
+
const path = prefix ? `${prefix}.${name}` : name;
|
|
32
|
+
if (field.type === "section" || field.type === "tab") {
|
|
33
|
+
if (field.optional) {
|
|
34
|
+
optionalPrefixes.push(path);
|
|
35
|
+
// Don't recurse: every descendant is already covered by
|
|
36
|
+
// this prefix.
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
walk(field.fields, path);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
walk(schema, "");
|
|
44
|
+
return new Set(optionalPrefixes);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasOptionalAncestor(path: string, optionalPrefixes: Set<string>): boolean {
|
|
48
|
+
for (const prefix of optionalPrefixes) {
|
|
49
|
+
if (path === prefix || path.startsWith(prefix + ".")) return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PartitionedHostKinds {
|
|
55
|
+
required: HostPermissionKind[];
|
|
56
|
+
optional: HostPermissionKind[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Given a permsFlat map plus the configFields tree, split the host
|
|
61
|
+
* kinds the app declares into required vs optional. A kind is required
|
|
62
|
+
* iff at least one declaration of that kind is *both* leaf-required
|
|
63
|
+
* (`meta.isOptional !== true`) AND has no optional ancestor section/tab
|
|
64
|
+
* along its dotted path. Otherwise the kind is optional.
|
|
65
|
+
*
|
|
66
|
+
* Both arrays are deduped and stable-sorted so the gate's missing-kinds
|
|
67
|
+
* messages render in a predictable order.
|
|
68
|
+
*/
|
|
69
|
+
export function partitionHostKinds(
|
|
70
|
+
permissionsFlat: Record<string, SerializablePermission>,
|
|
71
|
+
configFields: ConfigFieldTree,
|
|
72
|
+
): PartitionedHostKinds {
|
|
73
|
+
const optionalPrefixes = pathsUnderOptionalAncestor(configFields);
|
|
74
|
+
const required = new Set<HostPermissionKind>();
|
|
75
|
+
const optional = new Set<HostPermissionKind>();
|
|
76
|
+
for (const [path, perm] of Object.entries(permissionsFlat)) {
|
|
77
|
+
if (!isHostKind(perm.kind)) continue;
|
|
78
|
+
const leafOptional = (perm.meta as { isOptional?: boolean }).isOptional === true;
|
|
79
|
+
const ancestorOptional = hasOptionalAncestor(path, optionalPrefixes);
|
|
80
|
+
if (!leafOptional && !ancestorOptional) {
|
|
81
|
+
required.add(perm.kind);
|
|
82
|
+
} else {
|
|
83
|
+
optional.add(perm.kind);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// A kind that's required at one path and optional at another is
|
|
87
|
+
// required overall — required wins.
|
|
88
|
+
for (const k of required) optional.delete(k);
|
|
89
|
+
return {
|
|
90
|
+
required: [...required].sort(),
|
|
91
|
+
optional: [...optional].sort(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* State-based capability check: any service mounting a volume (relative
|
|
97
|
+
* or absolute) needs the worker to advertise the `host.volumes`
|
|
98
|
+
* capability — whether the worker can hold persistent files at all. A
|
|
99
|
+
* worker without it represents an ephemeral runtime (think sandbox VM,
|
|
100
|
+
* k8s emptyDir-only) and can't run any app that persists state.
|
|
101
|
+
*
|
|
102
|
+
* Returns true iff at least one service in the resolved state has a
|
|
103
|
+
* non-empty `volumes` array.
|
|
104
|
+
*/
|
|
105
|
+
export function stateRequiresVolumes(state: HomeApp): boolean {
|
|
106
|
+
for (const svc of Object.values(state.services)) {
|
|
107
|
+
if (svc.volumes && svc.volumes.length > 0) return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The world a permission resolver reads from.
|
|
2
|
+
//
|
|
3
|
+
// A resolver turns "the author declared this, the operator granted that"
|
|
4
|
+
// into the value the app reads out of `config`. For most kinds the grant
|
|
5
|
+
// already holds the answer and `toValue` is the whole resolver; the rest
|
|
6
|
+
// need facts only Home knows at eval time: which Silos exist, where the
|
|
7
|
+
// worker is, what the other members of a Silo publish.
|
|
8
|
+
//
|
|
9
|
+
// Everything here is plain data or a pure lookup closed over plain data,
|
|
10
|
+
// never a callback into Home - the resolvers live in the SDK, which cannot
|
|
11
|
+
// import Home. The caller resolves every database read, fetch and key
|
|
12
|
+
// derivation up front, which is what keeps resolvers synchronous.
|
|
13
|
+
//
|
|
14
|
+
// Home builds one per app in `evalApps`; the SDK test harness builds the
|
|
15
|
+
// same shape from fixtures, so both run the same resolvers.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A Silo as a resolver reads it. Deliberately structural: Home's own row
|
|
19
|
+
* (`server/silos.ts`) is assignable without the SDK knowing the row type,
|
|
20
|
+
* and the fields a resolver has no business reading (`saltId`,
|
|
21
|
+
* `setupAckAt`) stay off it.
|
|
22
|
+
*/
|
|
23
|
+
export interface ResolveSilo {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/** The worker row id the Silo is pinned to - what placement is checked against. */
|
|
27
|
+
readonly workerId: string;
|
|
28
|
+
/** That worker's slug, so a refusal can name the box the operator knows. */
|
|
29
|
+
readonly workerSlug: string;
|
|
30
|
+
readonly hostPath: string;
|
|
31
|
+
readonly containerPath: string;
|
|
32
|
+
readonly gid: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One service name published into a Silo by another member. */
|
|
36
|
+
export interface SiloPublication {
|
|
37
|
+
readonly port: number;
|
|
38
|
+
readonly appId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The Home identity `host.management` resolves from. Structural, like
|
|
43
|
+
* `ResolveSilo`: Home's own `HomeSettings` (read from installer env) is
|
|
44
|
+
* assignable without the SDK importing it.
|
|
45
|
+
*/
|
|
46
|
+
export interface ResolveHomeSettings {
|
|
47
|
+
readonly baseDomain: string;
|
|
48
|
+
readonly homeSlug: string;
|
|
49
|
+
readonly acmeDns: {
|
|
50
|
+
readonly serverUrl: string;
|
|
51
|
+
readonly apiKey: string;
|
|
52
|
+
readonly ca: string;
|
|
53
|
+
readonly caRootPem: string;
|
|
54
|
+
} | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ResolveContext {
|
|
58
|
+
/** The app being evaluated. */
|
|
59
|
+
readonly appId: string;
|
|
60
|
+
/**
|
|
61
|
+
* The worker this evaluation targets. `id` is the slug an author reads as
|
|
62
|
+
* `worker.id`; `internalId` is the row id, absent when there is no concrete
|
|
63
|
+
* worker (previews, sweeps, loader cold starts), which is why placement
|
|
64
|
+
* checks guard on it rather than assuming it.
|
|
65
|
+
*/
|
|
66
|
+
readonly worker: { readonly id: string; readonly internalId?: string };
|
|
67
|
+
/**
|
|
68
|
+
* Home's own identity, from installer env. `host.management` resolves the
|
|
69
|
+
* dashboard URL pieces and the acme-dns credentials out of this.
|
|
70
|
+
*/
|
|
71
|
+
readonly home: ResolveHomeSettings;
|
|
72
|
+
/** Every Silo the operator has defined. Read once per eval, not per app. */
|
|
73
|
+
readonly silos: readonly ResolveSilo[];
|
|
74
|
+
/**
|
|
75
|
+
* One Silo's merged service directory, with the evaluating app's own
|
|
76
|
+
* publications excluded. A function rather than a map because it is derived
|
|
77
|
+
* per app from state that grows as the app's peers evaluate.
|
|
78
|
+
*/
|
|
79
|
+
readonly siloDirectory: (siloId: string) => ReadonlyMap<string, SiloPublication>;
|
|
80
|
+
/**
|
|
81
|
+
* Per-Silo API keys, derived up front (HKDF off REALM_SECRET) so a resolver
|
|
82
|
+
* that needs one stays synchronous.
|
|
83
|
+
*/
|
|
84
|
+
readonly siloApiKeys: ReadonlyMap<string, string>;
|
|
85
|
+
/**
|
|
86
|
+
* Resolution mode, not a fact about the world like the rest of this context:
|
|
87
|
+
* the caller is asking what an app WOULD ship, with every permission granted
|
|
88
|
+
* sight-unseen and nothing actually chosen. Home's install preview is the
|
|
89
|
+
* only caller.
|
|
90
|
+
*
|
|
91
|
+
* The one resolver that reads it is `host.silo`; see `speculative-silo.ts`
|
|
92
|
+
* for why it is the only permission that needs a mode at all. Setting it
|
|
93
|
+
* anywhere an app can deploy would put a fabricated directory on the wire,
|
|
94
|
+
* so `evalApps` refuses it unless the eval is also `ephemeral`.
|
|
95
|
+
*/
|
|
96
|
+
readonly speculative?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What a `resolve` hook receives. `grant` is the operator's side (the same
|
|
101
|
+
* data `toValue` gets), `declared` is the author's side off the descriptor.
|
|
102
|
+
* Neither alone is enough for the kinds that need a resolver: the author
|
|
103
|
+
* says what they want, the operator says from where.
|
|
104
|
+
*/
|
|
105
|
+
export interface ResolveArgs<TGrant = unknown> {
|
|
106
|
+
readonly grant: TGrant;
|
|
107
|
+
readonly declared: unknown;
|
|
108
|
+
/** Dotted config path of this permission. Error messages name it. */
|
|
109
|
+
readonly path: string;
|
|
110
|
+
readonly ctx: ResolveContext;
|
|
111
|
+
}
|