@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/resolve.ts
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { appIconSchema } from "./app-icon.ts";
|
|
3
|
+
import { nameSchema } from "./identifiers.ts";
|
|
4
|
+
import type { BuildRecipe, ServiceDefinition } from "./service-definition.ts";
|
|
5
|
+
import { buildRecipeSchema } from "./schema/build-apps.ts";
|
|
6
|
+
import { publicDomainValueSchema } from "./schema/public-domain.ts";
|
|
7
|
+
import { Service } from "./service.ts";
|
|
8
|
+
|
|
9
|
+
// Mirrors `ServiceDefinition` field-for-field. `Ref<string>` survives the
|
|
10
|
+
// sandbox boundary as a structured `{__realmRef: ...}` marker the Home
|
|
11
|
+
// walker resolves with a grant-scoped allow-map, so every ref-capable field
|
|
12
|
+
// accepts either a plain string or a marker; the walker replaces markers
|
|
13
|
+
// with strings before compose validation runs.
|
|
14
|
+
//
|
|
15
|
+
// Two structural invariants the schema enforces, so the walker doesn't have
|
|
16
|
+
// to defend against them at every step:
|
|
17
|
+
// 1. `concat` is flat — its parts are strings or non-concat markers.
|
|
18
|
+
// 2. `deferred.input` contains no `deferred` descendants, which makes
|
|
19
|
+
// single-batch `runDeferred` correct by construction.
|
|
20
|
+
const derivedBodySchema = z.object({
|
|
21
|
+
kind: z.literal("derived"),
|
|
22
|
+
appId: z.string(),
|
|
23
|
+
name: z.string(),
|
|
24
|
+
});
|
|
25
|
+
const sharedBodySchema = z.object({
|
|
26
|
+
kind: z.literal("shared"),
|
|
27
|
+
providerAppId: z.string(),
|
|
28
|
+
serviceKey: z.string(),
|
|
29
|
+
name: z.string(),
|
|
30
|
+
});
|
|
31
|
+
const secretBodySchema = z.object({ kind: z.literal("secret"), path: z.string() });
|
|
32
|
+
|
|
33
|
+
function hasDeferredDescendant(value: unknown): boolean {
|
|
34
|
+
if (value == null || typeof value !== "object") return false;
|
|
35
|
+
if (Array.isArray(value)) return value.some(hasDeferredDescendant);
|
|
36
|
+
if ("__realmRef" in value) {
|
|
37
|
+
const m = (value as { __realmRef: unknown }).__realmRef;
|
|
38
|
+
if (m && typeof m === "object") {
|
|
39
|
+
if ((m as { kind?: string }).kind === "deferred") return true;
|
|
40
|
+
return hasDeferredDescendant(m);
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
for (const v of Object.values(value as Record<string, unknown>)) {
|
|
45
|
+
if (hasDeferredDescendant(v)) return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const deferredBodySchema = z.object({
|
|
51
|
+
kind: z.literal("deferred"),
|
|
52
|
+
id: z.number().int().nonnegative(),
|
|
53
|
+
input: z.unknown().refine((v) => !hasDeferredDescendant(v), {
|
|
54
|
+
message: "deferred input must not contain another deferred marker",
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
// Concat parts are sync leaves only. `interpolate` rejects
|
|
58
|
+
// `Deferred<string>` at the type level and again with a runtime guard, so a
|
|
59
|
+
// deferred sub-part can't enter a concat through normal authoring; this
|
|
60
|
+
// schema is the last gate on the way out of the sandbox.
|
|
61
|
+
const concatLeafSchema = z.discriminatedUnion("kind", [
|
|
62
|
+
derivedBodySchema,
|
|
63
|
+
sharedBodySchema,
|
|
64
|
+
secretBodySchema,
|
|
65
|
+
]);
|
|
66
|
+
const concatBodySchema = z.object({
|
|
67
|
+
kind: z.literal("concat"),
|
|
68
|
+
parts: z.array(z.union([z.string(), concatLeafSchema])),
|
|
69
|
+
});
|
|
70
|
+
const markerBodySchema = z.discriminatedUnion("kind", [
|
|
71
|
+
derivedBodySchema,
|
|
72
|
+
sharedBodySchema,
|
|
73
|
+
secretBodySchema,
|
|
74
|
+
deferredBodySchema,
|
|
75
|
+
concatBodySchema,
|
|
76
|
+
]);
|
|
77
|
+
const refMarkerSchema = z.object({ __realmRef: markerBodySchema });
|
|
78
|
+
const stringOrRef = z.union([z.string(), refMarkerSchema]);
|
|
79
|
+
// hostPath stays a strict string. `interpolate` collapses to a literal when
|
|
80
|
+
// all parts resolve to strings (the Postgres/Valkey pattern), so a ref that
|
|
81
|
+
// survives here is rejected at schema parse - it would leak a marker object
|
|
82
|
+
// where a gate-checked path is expected.
|
|
83
|
+
const hostPathSchema = z
|
|
84
|
+
.string()
|
|
85
|
+
.refine((p) => (p.startsWith("./") || p.startsWith("/")) && !p.includes(".."), {
|
|
86
|
+
message: "hostPath must start with ./ or / and must not contain ..",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Realm internals + SSH. Short on purpose - users want 80, 443, 53, etc.
|
|
90
|
+
const RESERVED_PORTS = new Set([
|
|
91
|
+
22, // SSH on the worker host
|
|
92
|
+
3100, // Home
|
|
93
|
+
3101, // Home's dev uplink listener
|
|
94
|
+
6021, // The worker's verify relay
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const hostPortNumberSchema = z
|
|
98
|
+
.number()
|
|
99
|
+
.int()
|
|
100
|
+
.min(1, "hostPort must be >= 1")
|
|
101
|
+
.max(65535, "hostPort must be <= 65535")
|
|
102
|
+
.refine((p) => !RESERVED_PORTS.has(p), {
|
|
103
|
+
message: `hostPort must not be a Realm-reserved port (${[...RESERVED_PORTS].join(", ")})`,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Docker reference charset only (lowercase per OCI name grammar). Catches
|
|
107
|
+
// newline (YAML break), `$` (compose env interp), backtick / `;|&<>` (shell
|
|
108
|
+
// metachars), whitespace.
|
|
109
|
+
const imageReferenceSchema = z
|
|
110
|
+
.string()
|
|
111
|
+
.min(1, "image must not be empty")
|
|
112
|
+
.regex(
|
|
113
|
+
/^[a-z0-9_./\-:@]+$/,
|
|
114
|
+
"image must contain only Docker reference characters (a-z 0-9 _ . - / : @)",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Whole-second duration. Stays a number through resolve + the wire;
|
|
118
|
+
// the renderer formats `${n}s` when emitting compose, since the
|
|
119
|
+
// compose-spec duration parser rejects bare integers.
|
|
120
|
+
const duration = (defaultSeconds: number) => z.number().int().nonnegative().default(defaultSeconds);
|
|
121
|
+
|
|
122
|
+
const healthcheckSchema = z.object({
|
|
123
|
+
test: z.array(z.string()),
|
|
124
|
+
interval: duration(3),
|
|
125
|
+
timeout: duration(5),
|
|
126
|
+
retries: z.number().int().nonnegative().default(10),
|
|
127
|
+
startPeriod: duration(10),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const containerPortSchema = z.number().int().min(1).max(65535);
|
|
131
|
+
|
|
132
|
+
const routerSchema = z.object({
|
|
133
|
+
name: z.string().min(1).optional(),
|
|
134
|
+
icon: appIconSchema.optional(),
|
|
135
|
+
// Home-screen tile opt-out. Absent = shown; `false` keeps the gated route
|
|
136
|
+
// but drops the tile (machine-facing endpoints like webhooks/registries).
|
|
137
|
+
// Optional rather than `.default(true)` so only opted-out routers carry
|
|
138
|
+
// the field in resolved state.
|
|
139
|
+
tile: z.boolean().optional(),
|
|
140
|
+
containerPort: containerPortSchema,
|
|
141
|
+
// The whole resolved `r.home.publicDomain()` value, hosts and redirect mode
|
|
142
|
+
// together. No `stringOrRef` arm: that would let a bare author-supplied
|
|
143
|
+
// string in through the back door, which is what the permission closes.
|
|
144
|
+
// Absent when the app declares no such permission or none was granted.
|
|
145
|
+
publicDomain: publicDomainValueSchema.optional(),
|
|
146
|
+
pathPrefix: z
|
|
147
|
+
.string()
|
|
148
|
+
.regex(/^\/[a-zA-Z0-9/_-]*$/, "pathPrefix must start with / and contain only [a-zA-Z0-9/_-]")
|
|
149
|
+
.optional(),
|
|
150
|
+
compress: z.boolean().default(true),
|
|
151
|
+
maxRequestSizeMb: z.number().nullable().default(4),
|
|
152
|
+
rateLimit: z
|
|
153
|
+
.union([
|
|
154
|
+
z.number(),
|
|
155
|
+
z.object({
|
|
156
|
+
requests: z.number(),
|
|
157
|
+
window: z.string().regex(/^\d+(ms|s|m|h)$/, "window must be a duration like 10s, 1m, 1h"),
|
|
158
|
+
}),
|
|
159
|
+
])
|
|
160
|
+
.nullable()
|
|
161
|
+
// A runaway-client guard, not a browsing budget: one page load is ~80
|
|
162
|
+
// requests at the median and ~185 at p90 (HTTP Archive 2025), and grids
|
|
163
|
+
// like Immich or Plex fetch a few hundred thumbnails in a second. 100/s
|
|
164
|
+
// sustained from one address still catches a broken fetch loop; anything
|
|
165
|
+
// tighter (a public login form) is a per-route decision for the author.
|
|
166
|
+
.default({ requests: 1000, window: "10s" }),
|
|
167
|
+
headers: z.record(z.string(), z.string().nullable()).optional(),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const hostPortSchema = z.object({
|
|
171
|
+
containerPort: containerPortSchema,
|
|
172
|
+
hostPort: hostPortNumberSchema,
|
|
173
|
+
protocol: z.enum(["tcp", "udp"]).optional(),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const backupStrategySchema = z.discriminatedUnion("kind", [
|
|
177
|
+
z.object({ kind: z.literal("file-copy") }),
|
|
178
|
+
z.object({ kind: z.literal("sqlite"), file: z.string() }),
|
|
179
|
+
z.object({
|
|
180
|
+
kind: z.literal("pg-dump"),
|
|
181
|
+
url: stringOrRef,
|
|
182
|
+
version: z.number().int().positive(),
|
|
183
|
+
}),
|
|
184
|
+
z.object({ kind: z.literal("valkey-save"), url: stringOrRef }),
|
|
185
|
+
z.object({
|
|
186
|
+
kind: z.literal("mysql-dump"),
|
|
187
|
+
url: stringOrRef,
|
|
188
|
+
version: z.number().int().positive(),
|
|
189
|
+
}),
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
const absoluteContainerPath = z.string().startsWith("/", {
|
|
193
|
+
message: "containerPath must be absolute (start with '/')",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const homeServiceSchema = z
|
|
197
|
+
.object({
|
|
198
|
+
// Exactly one of `image` / `build` is present (enforced in the
|
|
199
|
+
// superRefine below). `image` is the prebuilt-pull path; `build` is the
|
|
200
|
+
// build-from-source path, which has no pinned digest until its first
|
|
201
|
+
// build, so Home leaves the app inert until one is set.
|
|
202
|
+
image: imageReferenceSchema.optional(),
|
|
203
|
+
// `build: true` is author sugar for the default recipe — normalize it to
|
|
204
|
+
// `{}` here so `HomeService.build` is always a `BuildRecipe` object.
|
|
205
|
+
// The image/build XOR (superRefine below) reads the transformed value, so
|
|
206
|
+
// `true`/`{}` both count as "build present".
|
|
207
|
+
build: z
|
|
208
|
+
.literal(true)
|
|
209
|
+
.or(buildRecipeSchema)
|
|
210
|
+
.transform((v): BuildRecipe => (v === true ? {} : v))
|
|
211
|
+
.optional(),
|
|
212
|
+
router: routerSchema.optional(),
|
|
213
|
+
hostPorts: z.array(hostPortSchema).optional(),
|
|
214
|
+
env: z.record(z.string(), z.union([stringOrRef, z.number()])).optional(),
|
|
215
|
+
files: z.record(absoluteContainerPath, stringOrRef).optional(),
|
|
216
|
+
command: z.array(stringOrRef).optional(),
|
|
217
|
+
volumes: z
|
|
218
|
+
.array(
|
|
219
|
+
z.object({
|
|
220
|
+
hostPath: hostPathSchema,
|
|
221
|
+
containerPath: absoluteContainerPath,
|
|
222
|
+
// Silo-owned group. Present only on a Silo's volume entry; the
|
|
223
|
+
// renderer derives `group_add` from it and the grant gate matches
|
|
224
|
+
// (hostPath, gid) against the Silos the app joined.
|
|
225
|
+
gid: z.number().int().positive().optional(),
|
|
226
|
+
readOnly: z.boolean().optional(),
|
|
227
|
+
backup: z.union([backupStrategySchema, z.literal(false)]).optional(),
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
230
|
+
.optional(),
|
|
231
|
+
gpu: z.boolean().optional(),
|
|
232
|
+
iptables: z.boolean().optional(),
|
|
233
|
+
joinNetworks: z.array(z.string()).optional(),
|
|
234
|
+
healthcheck: healthcheckSchema.optional(),
|
|
235
|
+
memory: z.string().optional(),
|
|
236
|
+
cpu: z.number().positive().max(128).optional(),
|
|
237
|
+
replicas: z.number().int().min(1).max(100).default(1),
|
|
238
|
+
labels: z.array(z.string()).optional(),
|
|
239
|
+
shmSizeMb: z.number().int().positive().optional(),
|
|
240
|
+
})
|
|
241
|
+
.strict()
|
|
242
|
+
.superRefine((svc, ctx) => {
|
|
243
|
+
// Exactly one of `image` / `build`. The type-level XOR on
|
|
244
|
+
// ServiceDefinition catches this at author time; this is the runtime
|
|
245
|
+
// backstop for sandbox-produced state, where the types are erased.
|
|
246
|
+
if (svc.image == null && svc.build == null) {
|
|
247
|
+
ctx.addIssue({
|
|
248
|
+
code: "custom",
|
|
249
|
+
path: ["image"],
|
|
250
|
+
message: "a service must set either `image` or `build`",
|
|
251
|
+
});
|
|
252
|
+
} else if (svc.image != null && svc.build != null) {
|
|
253
|
+
ctx.addIssue({
|
|
254
|
+
code: "custom",
|
|
255
|
+
path: ["build"],
|
|
256
|
+
message: "a service cannot set both `image` and `build`",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Two volumes on one containerPath hit the same "duplicate mount point"
|
|
261
|
+
// failure as the files/volumes collision below. Reachable by mounting one
|
|
262
|
+
// resolved value twice, or alongside a hand-written path matching it.
|
|
263
|
+
const seen = new Set<string>();
|
|
264
|
+
for (const [i, vol] of (svc.volumes ?? []).entries()) {
|
|
265
|
+
if (seen.has(vol.containerPath)) {
|
|
266
|
+
ctx.addIssue({
|
|
267
|
+
code: "custom",
|
|
268
|
+
path: ["volumes", i, "containerPath"],
|
|
269
|
+
message: `containerPath ${vol.containerPath} is mounted by two volumes`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
seen.add(vol.containerPath);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// `files:` and `volumes:` both emit bind mounts in the rendered
|
|
276
|
+
// compose. A collision on containerPath produces a "duplicate mount
|
|
277
|
+
// point" error at `docker compose up`, far from the source. Reject
|
|
278
|
+
// here so the author sees the conflict at validation time.
|
|
279
|
+
if (!svc.files || !svc.volumes) return;
|
|
280
|
+
const volumePaths = new Set(svc.volumes.map((v) => v.containerPath));
|
|
281
|
+
for (const fileKey of Object.keys(svc.files)) {
|
|
282
|
+
if (volumePaths.has(fileKey)) {
|
|
283
|
+
ctx.addIssue({
|
|
284
|
+
code: "custom",
|
|
285
|
+
path: ["files", fileKey],
|
|
286
|
+
message: `containerPath ${fileKey} is used by both files: and volumes:`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
export type HomeService = z.infer<typeof homeServiceSchema>;
|
|
293
|
+
|
|
294
|
+
// ui surface manifest. Output/input schemas are emitted as JSON Schema
|
|
295
|
+
// (z.toJSONSchema) so they survive the sandbox boundary; headers can carry
|
|
296
|
+
// refs (e.g. r.stableSecret) the walker resolves under the app's grant
|
|
297
|
+
// scope. `service`/`port` are the grant boundary (which container), so they
|
|
298
|
+
// are flat and static here; the per-call verb/path/query/body comes from the
|
|
299
|
+
// loader's `request(state, config)` fn in the iframe.
|
|
300
|
+
|
|
301
|
+
// The request descriptor a loader's `request(state, config)` returns. Not a
|
|
302
|
+
// manifest field — Home builds these live at prefill time (stitchLoaderRequests)
|
|
303
|
+
// and the iframe runtime builds one per render; both feed it to the proxy.
|
|
304
|
+
const requestManifestSchema = z.object({
|
|
305
|
+
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
306
|
+
path: z.string(),
|
|
307
|
+
query: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
308
|
+
body: z.unknown().optional(),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const loaderManifestSchema = z.object({
|
|
312
|
+
service: z.string(),
|
|
313
|
+
port: z.number().int().positive().max(65535),
|
|
314
|
+
headers: z.record(z.string(), stringOrRef).optional(),
|
|
315
|
+
outputSchema: z.unknown(),
|
|
316
|
+
merge: z.literal(true).optional(),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Actions are mutations — `outputSchema` is optional (authors often don't
|
|
320
|
+
// care about the response). The caller passes an explicit input at
|
|
321
|
+
// `mutate(input)` time, so actions keep an `inputSchema` where loaders read
|
|
322
|
+
// the widget's state. No `merge` knob: a system app resolves with the raw
|
|
323
|
+
// `{ results, failures }` envelope, a single app with the upstream body.
|
|
324
|
+
// Actions are never prefilled; the request is built at call time.
|
|
325
|
+
const actionManifestSchema = z.object({
|
|
326
|
+
service: z.string(),
|
|
327
|
+
port: z.number().int().positive().max(65535),
|
|
328
|
+
headers: z.record(z.string(), stringOrRef).optional(),
|
|
329
|
+
inputSchema: z.unknown(),
|
|
330
|
+
outputSchema: z.unknown().optional(),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const baseSurfaceManifestSchema = z.object({
|
|
334
|
+
title: z.string().min(1),
|
|
335
|
+
// Icon name from `@yourrealm/sdk/ui/components/icons.tsx` named exports.
|
|
336
|
+
// Runtime is just `string` (the TS-side `IconName` union catches
|
|
337
|
+
// typos at authoring time); the Home sidebar falls back to a default
|
|
338
|
+
// glyph if a value isn't a known export, so a typo doesn't 500.
|
|
339
|
+
icon: z.string().min(1),
|
|
340
|
+
// The surface's live UI state + settings schemas (JSON Schema form).
|
|
341
|
+
// `configSchema` (built from the config leaves' zod schemas) lets Home
|
|
342
|
+
// validate a submitted config; `stateSchema`'s `{}`-parse seeds the
|
|
343
|
+
// runtime's initial state.
|
|
344
|
+
stateSchema: z.unknown().optional(),
|
|
345
|
+
configSchema: z.unknown().optional(),
|
|
346
|
+
// The config form's render descriptor: one `ConfigFieldLeaf` per
|
|
347
|
+
// `r.input.*` leaf (type/label/placeholder/default/enum values/bounds),
|
|
348
|
+
// so the tile's config screen reuses the same field renderer the app
|
|
349
|
+
// install/edit form uses. Absent when the surface takes no config.
|
|
350
|
+
configFields: z.unknown().optional(),
|
|
351
|
+
// The surface's resolved config: each leaf's `.prefill()` default,
|
|
352
|
+
// shipped to the iframe as the base layer for every loader's
|
|
353
|
+
// request(state, config) (Home merges the placement's persisted config
|
|
354
|
+
// over it).
|
|
355
|
+
defaultConfig: z.unknown().optional(),
|
|
356
|
+
loaders: z.record(z.string(), loaderManifestSchema).optional(),
|
|
357
|
+
actions: z.record(z.string(), actionManifestSchema).optional(),
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Widget-only tile size in 8rem grid cells. Defaults materialize here
|
|
361
|
+
// so every persisted widget entry is self-describing — DB rows carry
|
|
362
|
+
// the actual `cols`/`rows`, no consumer-side `?? 2` fallbacks needed.
|
|
363
|
+
// Mobile collapse formula lives in `home-grid.tsx`.
|
|
364
|
+
const widgetManifestSchema = baseSurfaceManifestSchema.extend({
|
|
365
|
+
cols: z.number().int().min(1).default(2),
|
|
366
|
+
rows: z.number().int().min(1).default(2),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const uiManifestSchema = z.object({
|
|
370
|
+
pages: z.record(z.string(), baseSurfaceManifestSchema).optional(),
|
|
371
|
+
widgets: z.record(z.string(), widgetManifestSchema).optional(),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Wire-side TS types inferred from the schemas above; consumers outside the
|
|
375
|
+
// SDK reach them via the `/compile` barrel re-export. Author-facing types
|
|
376
|
+
// (`Page`, `Widget`, `Loader`, `UiConfig`) live in `ui-surface.ts` because
|
|
377
|
+
// they carry render fns and live Zod schemas that can't survive a JSON
|
|
378
|
+
// round-trip.
|
|
379
|
+
export type LoaderManifest = z.infer<typeof loaderManifestSchema>;
|
|
380
|
+
export type ActionManifest = z.infer<typeof actionManifestSchema>;
|
|
381
|
+
export type RequestManifest = z.infer<typeof requestManifestSchema>;
|
|
382
|
+
export type SurfaceManifest = z.infer<typeof baseSurfaceManifestSchema>;
|
|
383
|
+
export type WidgetManifest = z.infer<typeof widgetManifestSchema>;
|
|
384
|
+
export type UiManifest = z.infer<typeof uiManifestSchema>;
|
|
385
|
+
export type ManifestHeader = z.infer<typeof stringOrRef>;
|
|
386
|
+
|
|
387
|
+
// Header brokering manifest, sampled from `getTrustedHeaders` at eval time.
|
|
388
|
+
// `keys` is the static union of header names across roles (Caddy injects
|
|
389
|
+
// these and anti-spoof strips them). `byRole` maps each header name to its
|
|
390
|
+
// source: a `user` field, a literal string, or a `template` of literal +
|
|
391
|
+
// field segments (when the author interpolates, e.g. `i`Bearer
|
|
392
|
+
// ${user.username}``). The verify hop substitutes the live value.
|
|
393
|
+
const headerFieldSchema = z.object({ field: z.enum(["username", "email"]) });
|
|
394
|
+
const headerSegmentSchema = z.union([z.string(), headerFieldSchema]);
|
|
395
|
+
const authHeaderSourceSchema = z.union([
|
|
396
|
+
headerFieldSchema,
|
|
397
|
+
z.object({ literal: z.string() }),
|
|
398
|
+
z.object({ template: z.array(headerSegmentSchema) }),
|
|
399
|
+
]);
|
|
400
|
+
const trustedHeadersManifestSchema = z.object({
|
|
401
|
+
keys: z.array(z.string()),
|
|
402
|
+
byRole: z.record(z.enum(["admin", "member"]), z.record(z.string(), authHeaderSourceSchema)),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
export type TrustedHeaderSource = z.infer<typeof authHeaderSourceSchema>;
|
|
406
|
+
export type TrustedHeadersManifest = z.infer<typeof trustedHeadersManifestSchema>;
|
|
407
|
+
|
|
408
|
+
// The resolved form of an app's `DesiredApp` (what `getDesiredState`
|
|
409
|
+
// returns): services + egress + the ui / auth / routing manifests, after
|
|
410
|
+
// `resolveDesiredApp()` flattens Service instances to definitions and the
|
|
411
|
+
// Home walker bakes refs to literals. Home's internal per-app model — it
|
|
412
|
+
// lives as `AppEval.app` and is what `build-wire` projects the worker wire
|
|
413
|
+
// (`WorkerApp`) out of, dropping the Home-only fields (`ui`,
|
|
414
|
+
// `trustedHeaders`, `hasAnonymousGate`, `kind`).
|
|
415
|
+
export const homeAppSchema = z.object({
|
|
416
|
+
services: z.record(nameSchema, homeServiceSchema),
|
|
417
|
+
egress: z.literal(true).optional(),
|
|
418
|
+
ui: uiManifestSchema.optional(),
|
|
419
|
+
trustedHeaders: trustedHeadersManifestSchema.optional(),
|
|
420
|
+
// True when the app declares verifyAnonymousRequest. Home-side only (never
|
|
421
|
+
// crosses to the worker wire); recompute reads it to register the app's
|
|
422
|
+
// routed hosts in the anonymous-gate cache.
|
|
423
|
+
hasAnonymousGate: z.boolean().optional(),
|
|
424
|
+
// Single = run on one selected worker (default). System = fan out to
|
|
425
|
+
// every compatible worker. Drives the install panel's picker and the
|
|
426
|
+
// Home's UI fan-out behavior. See platform/docs realm-ts/workers.mdx.
|
|
427
|
+
kind: z.enum(["single", "system"]).default("single"),
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
export type HomeApp = z.infer<typeof homeAppSchema>;
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Scans the services map for Service instances and assigns service keys.
|
|
434
|
+
* After this, refs resolve (e.g. db.url becomes the real string).
|
|
435
|
+
*
|
|
436
|
+
* Returns the resolved desired state — Service instances are replaced
|
|
437
|
+
* with their input definitions for the Worker.
|
|
438
|
+
*
|
|
439
|
+
* `opts.appId`, when provided, namespaces each Service's key as
|
|
440
|
+
* `${appId}_${serviceKey}` so `host` refs resolve to the DNS name a
|
|
441
|
+
* sibling container will see in the merged worker compose project.
|
|
442
|
+
*/
|
|
443
|
+
export function resolveDesiredApp(
|
|
444
|
+
state: {
|
|
445
|
+
services?: Record<string, Service | ServiceDefinition>;
|
|
446
|
+
egress?: true;
|
|
447
|
+
ui?: UiManifest;
|
|
448
|
+
trustedHeaders?: TrustedHeadersManifest;
|
|
449
|
+
hasAnonymousGate?: boolean;
|
|
450
|
+
kind?: "single" | "system";
|
|
451
|
+
},
|
|
452
|
+
opts: { appId?: string } = {},
|
|
453
|
+
): HomeApp {
|
|
454
|
+
const resolvedServices: Record<string, unknown> = {};
|
|
455
|
+
|
|
456
|
+
for (const [key, service] of Object.entries(state.services ?? {})) {
|
|
457
|
+
if (service instanceof Service) {
|
|
458
|
+
const qualifiedKey = opts.appId ? `${opts.appId}_${key}` : key;
|
|
459
|
+
service._setServiceKey(qualifiedKey);
|
|
460
|
+
resolvedServices[key] = service.getServiceDefinition();
|
|
461
|
+
} else {
|
|
462
|
+
resolvedServices[key] = service;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Re-serialize to resolve all refs (`JSON.stringify` materializes ref
|
|
467
|
+
// proxies via their `toJSON`), then validate. Round-trip is a deep
|
|
468
|
+
// clone of data we just constructed; no untrusted input.
|
|
469
|
+
const serialized = JSON.parse(JSON.stringify({ ...state, services: resolvedServices }));
|
|
470
|
+
return homeAppSchema.parse(serialized);
|
|
471
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Validation schema for the docker-compose YAML the renderer produces.
|
|
2
|
+
// Used by Home integration tests to assert the generated output round-trips
|
|
3
|
+
// against the compose grammar Realm actually emits — strict so a stray
|
|
4
|
+
// field surfaces as a test failure rather than a silent passthrough.
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
const volumeSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
type: z.enum(["bind", "volume"]),
|
|
11
|
+
source: z.string(),
|
|
12
|
+
target: z.string(),
|
|
13
|
+
read_only: z.boolean().optional(),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
|
|
17
|
+
const portSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
target: z.number().int(),
|
|
20
|
+
published: z.number().int(),
|
|
21
|
+
protocol: z.enum(["tcp", "udp"]).optional(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
|
|
25
|
+
const healthcheckSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
test: z.array(z.string()),
|
|
28
|
+
interval: z.string().optional(),
|
|
29
|
+
timeout: z.string().optional(),
|
|
30
|
+
retries: z.number().int().optional(),
|
|
31
|
+
start_period: z.string().optional(),
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
|
|
35
|
+
const deploySchema = z
|
|
36
|
+
.object({
|
|
37
|
+
replicas: z.number().int().optional(),
|
|
38
|
+
resources: z
|
|
39
|
+
.object({
|
|
40
|
+
limits: z
|
|
41
|
+
.object({
|
|
42
|
+
memory: z.string().optional(),
|
|
43
|
+
cpus: z.string().optional(),
|
|
44
|
+
})
|
|
45
|
+
.strict()
|
|
46
|
+
.optional(),
|
|
47
|
+
})
|
|
48
|
+
.strict()
|
|
49
|
+
.optional(),
|
|
50
|
+
})
|
|
51
|
+
.strict();
|
|
52
|
+
|
|
53
|
+
const dependsOnEntrySchema = z
|
|
54
|
+
.object({
|
|
55
|
+
condition: z.enum(["service_healthy", "service_started", "service_completed_successfully"]),
|
|
56
|
+
restart: z.boolean().optional(),
|
|
57
|
+
})
|
|
58
|
+
.strict();
|
|
59
|
+
|
|
60
|
+
const loggingSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
driver: z.string(),
|
|
63
|
+
options: z.record(z.string(), z.string()).optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
|
|
67
|
+
const composeServiceSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
image: z.string(),
|
|
70
|
+
environment: z.record(z.string(), z.string()).optional(),
|
|
71
|
+
env_file: z.union([z.string(), z.array(z.string())]).optional(),
|
|
72
|
+
command: z.array(z.string()).optional(),
|
|
73
|
+
labels: z.record(z.string(), z.string()).optional(),
|
|
74
|
+
volumes: z.array(volumeSchema).optional(),
|
|
75
|
+
group_add: z.array(z.number().int().positive()).optional(),
|
|
76
|
+
ports: z.array(portSchema).optional(),
|
|
77
|
+
healthcheck: healthcheckSchema.optional(),
|
|
78
|
+
deploy: deploySchema.optional(),
|
|
79
|
+
cap_drop: z.array(z.string()).optional(),
|
|
80
|
+
cap_add: z.array(z.string()).optional(),
|
|
81
|
+
security_opt: z.array(z.string()).optional(),
|
|
82
|
+
pids_limit: z.number().int().optional(),
|
|
83
|
+
ulimits: z.record(z.string(), z.number().int()).optional(),
|
|
84
|
+
shm_size: z.string().optional(),
|
|
85
|
+
devices: z.array(z.string()).optional(),
|
|
86
|
+
network_mode: z.string().optional(),
|
|
87
|
+
restart: z.string().optional(),
|
|
88
|
+
stop_grace_period: z.string().optional(),
|
|
89
|
+
depends_on: z.record(z.string(), dependsOnEntrySchema).optional(),
|
|
90
|
+
networks: z
|
|
91
|
+
.record(
|
|
92
|
+
z.string(),
|
|
93
|
+
z
|
|
94
|
+
.object({
|
|
95
|
+
aliases: z.array(z.string()).optional(),
|
|
96
|
+
})
|
|
97
|
+
.strict(),
|
|
98
|
+
)
|
|
99
|
+
.optional(),
|
|
100
|
+
logging: loggingSchema.optional(),
|
|
101
|
+
})
|
|
102
|
+
.strict();
|
|
103
|
+
|
|
104
|
+
const composeNetworkSchema = z
|
|
105
|
+
.object({
|
|
106
|
+
name: z.string().optional(),
|
|
107
|
+
external: z.boolean().optional(),
|
|
108
|
+
internal: z.boolean().optional(),
|
|
109
|
+
driver: z.string().optional(),
|
|
110
|
+
})
|
|
111
|
+
.strict();
|
|
112
|
+
|
|
113
|
+
export const composeSchema = z
|
|
114
|
+
.object({
|
|
115
|
+
services: z.record(z.string(), composeServiceSchema),
|
|
116
|
+
networks: z.record(z.string(), composeNetworkSchema).optional(),
|
|
117
|
+
})
|
|
118
|
+
.strict();
|