@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/dist/index.js
ADDED
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appIconSchema,
|
|
3
|
+
baseInput,
|
|
4
|
+
checkPermissionAppKinds,
|
|
5
|
+
checkSystemAppRoutes,
|
|
6
|
+
cloneAnd,
|
|
7
|
+
deepMergeConfig,
|
|
8
|
+
defineMethod,
|
|
9
|
+
home_exports,
|
|
10
|
+
host_exports,
|
|
11
|
+
isNetworkStrategy,
|
|
12
|
+
isVolumeStrategy,
|
|
13
|
+
numberInput,
|
|
14
|
+
peer_exports,
|
|
15
|
+
publicDomainValueSchema,
|
|
16
|
+
remote_exports
|
|
17
|
+
} from "./chunk-O3UIK34B.js";
|
|
18
|
+
import {
|
|
19
|
+
REALM_HOME_PROXY_CONTAINER_PATH,
|
|
20
|
+
nameSchema
|
|
21
|
+
} from "./chunk-VHTUSZUX.js";
|
|
22
|
+
import {
|
|
23
|
+
REF_SYMBOL,
|
|
24
|
+
SELF_APP_ID,
|
|
25
|
+
Service,
|
|
26
|
+
containsRef,
|
|
27
|
+
deferredRef,
|
|
28
|
+
identityRef,
|
|
29
|
+
interpolate,
|
|
30
|
+
ref
|
|
31
|
+
} from "./chunk-AITGS27R.js";
|
|
32
|
+
import {
|
|
33
|
+
HOME_COOKIE_PREFIX
|
|
34
|
+
} from "./chunk-CMAEHQ4F.js";
|
|
35
|
+
import "./chunk-MLKGABMK.js";
|
|
36
|
+
|
|
37
|
+
// src/create-app.ts
|
|
38
|
+
import { z as z3 } from "zod";
|
|
39
|
+
|
|
40
|
+
// src/resolve.ts
|
|
41
|
+
import { z as z2 } from "zod";
|
|
42
|
+
|
|
43
|
+
// src/schema/build-apps.ts
|
|
44
|
+
import { z } from "zod";
|
|
45
|
+
var buildRecipeSchema = z.object({
|
|
46
|
+
dockerfile: z.string().optional(),
|
|
47
|
+
buildArgs: z.record(z.string(), z.string()).optional(),
|
|
48
|
+
context: z.string().optional()
|
|
49
|
+
}).strict();
|
|
50
|
+
var buildAppsSchema = z.array(
|
|
51
|
+
z.object({
|
|
52
|
+
appId: z.string(),
|
|
53
|
+
service: z.string(),
|
|
54
|
+
recipe: buildRecipeSchema
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// src/resolve.ts
|
|
59
|
+
var derivedBodySchema = z2.object({
|
|
60
|
+
kind: z2.literal("derived"),
|
|
61
|
+
appId: z2.string(),
|
|
62
|
+
name: z2.string()
|
|
63
|
+
});
|
|
64
|
+
var sharedBodySchema = z2.object({
|
|
65
|
+
kind: z2.literal("shared"),
|
|
66
|
+
providerAppId: z2.string(),
|
|
67
|
+
serviceKey: z2.string(),
|
|
68
|
+
name: z2.string()
|
|
69
|
+
});
|
|
70
|
+
var secretBodySchema = z2.object({ kind: z2.literal("secret"), path: z2.string() });
|
|
71
|
+
function hasDeferredDescendant(value) {
|
|
72
|
+
if (value == null || typeof value !== "object") return false;
|
|
73
|
+
if (Array.isArray(value)) return value.some(hasDeferredDescendant);
|
|
74
|
+
if ("__realmRef" in value) {
|
|
75
|
+
const m = value.__realmRef;
|
|
76
|
+
if (m && typeof m === "object") {
|
|
77
|
+
if (m.kind === "deferred") return true;
|
|
78
|
+
return hasDeferredDescendant(m);
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
for (const v of Object.values(value)) {
|
|
83
|
+
if (hasDeferredDescendant(v)) return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
var deferredBodySchema = z2.object({
|
|
88
|
+
kind: z2.literal("deferred"),
|
|
89
|
+
id: z2.number().int().nonnegative(),
|
|
90
|
+
input: z2.unknown().refine((v) => !hasDeferredDescendant(v), {
|
|
91
|
+
message: "deferred input must not contain another deferred marker"
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
var concatLeafSchema = z2.discriminatedUnion("kind", [
|
|
95
|
+
derivedBodySchema,
|
|
96
|
+
sharedBodySchema,
|
|
97
|
+
secretBodySchema
|
|
98
|
+
]);
|
|
99
|
+
var concatBodySchema = z2.object({
|
|
100
|
+
kind: z2.literal("concat"),
|
|
101
|
+
parts: z2.array(z2.union([z2.string(), concatLeafSchema]))
|
|
102
|
+
});
|
|
103
|
+
var markerBodySchema = z2.discriminatedUnion("kind", [
|
|
104
|
+
derivedBodySchema,
|
|
105
|
+
sharedBodySchema,
|
|
106
|
+
secretBodySchema,
|
|
107
|
+
deferredBodySchema,
|
|
108
|
+
concatBodySchema
|
|
109
|
+
]);
|
|
110
|
+
var refMarkerSchema = z2.object({ __realmRef: markerBodySchema });
|
|
111
|
+
var stringOrRef = z2.union([z2.string(), refMarkerSchema]);
|
|
112
|
+
var hostPathSchema = z2.string().refine((p) => (p.startsWith("./") || p.startsWith("/")) && !p.includes(".."), {
|
|
113
|
+
message: "hostPath must start with ./ or / and must not contain .."
|
|
114
|
+
});
|
|
115
|
+
var RESERVED_PORTS = /* @__PURE__ */ new Set([
|
|
116
|
+
22,
|
|
117
|
+
// SSH on the worker host
|
|
118
|
+
3100,
|
|
119
|
+
// Home
|
|
120
|
+
3101,
|
|
121
|
+
// Home's dev uplink listener
|
|
122
|
+
6021
|
|
123
|
+
// The worker's verify relay
|
|
124
|
+
]);
|
|
125
|
+
var hostPortNumberSchema = z2.number().int().min(1, "hostPort must be >= 1").max(65535, "hostPort must be <= 65535").refine((p) => !RESERVED_PORTS.has(p), {
|
|
126
|
+
message: `hostPort must not be a Realm-reserved port (${[...RESERVED_PORTS].join(", ")})`
|
|
127
|
+
});
|
|
128
|
+
var imageReferenceSchema = z2.string().min(1, "image must not be empty").regex(
|
|
129
|
+
/^[a-z0-9_./\-:@]+$/,
|
|
130
|
+
"image must contain only Docker reference characters (a-z 0-9 _ . - / : @)"
|
|
131
|
+
);
|
|
132
|
+
var duration = (defaultSeconds) => z2.number().int().nonnegative().default(defaultSeconds);
|
|
133
|
+
var healthcheckSchema = z2.object({
|
|
134
|
+
test: z2.array(z2.string()),
|
|
135
|
+
interval: duration(3),
|
|
136
|
+
timeout: duration(5),
|
|
137
|
+
retries: z2.number().int().nonnegative().default(10),
|
|
138
|
+
startPeriod: duration(10)
|
|
139
|
+
});
|
|
140
|
+
var containerPortSchema = z2.number().int().min(1).max(65535);
|
|
141
|
+
var routerSchema = z2.object({
|
|
142
|
+
name: z2.string().min(1).optional(),
|
|
143
|
+
icon: appIconSchema.optional(),
|
|
144
|
+
// Home-screen tile opt-out. Absent = shown; `false` keeps the gated route
|
|
145
|
+
// but drops the tile (machine-facing endpoints like webhooks/registries).
|
|
146
|
+
// Optional rather than `.default(true)` so only opted-out routers carry
|
|
147
|
+
// the field in resolved state.
|
|
148
|
+
tile: z2.boolean().optional(),
|
|
149
|
+
containerPort: containerPortSchema,
|
|
150
|
+
// The whole resolved `r.home.publicDomain()` value, hosts and redirect mode
|
|
151
|
+
// together. No `stringOrRef` arm: that would let a bare author-supplied
|
|
152
|
+
// string in through the back door, which is what the permission closes.
|
|
153
|
+
// Absent when the app declares no such permission or none was granted.
|
|
154
|
+
publicDomain: publicDomainValueSchema.optional(),
|
|
155
|
+
pathPrefix: z2.string().regex(/^\/[a-zA-Z0-9/_-]*$/, "pathPrefix must start with / and contain only [a-zA-Z0-9/_-]").optional(),
|
|
156
|
+
compress: z2.boolean().default(true),
|
|
157
|
+
maxRequestSizeMb: z2.number().nullable().default(4),
|
|
158
|
+
rateLimit: z2.union([
|
|
159
|
+
z2.number(),
|
|
160
|
+
z2.object({
|
|
161
|
+
requests: z2.number(),
|
|
162
|
+
window: z2.string().regex(/^\d+(ms|s|m|h)$/, "window must be a duration like 10s, 1m, 1h")
|
|
163
|
+
})
|
|
164
|
+
]).nullable().default({ requests: 1e3, window: "10s" }),
|
|
165
|
+
headers: z2.record(z2.string(), z2.string().nullable()).optional()
|
|
166
|
+
});
|
|
167
|
+
var hostPortSchema = z2.object({
|
|
168
|
+
containerPort: containerPortSchema,
|
|
169
|
+
hostPort: hostPortNumberSchema,
|
|
170
|
+
protocol: z2.enum(["tcp", "udp"]).optional()
|
|
171
|
+
});
|
|
172
|
+
var backupStrategySchema = z2.discriminatedUnion("kind", [
|
|
173
|
+
z2.object({ kind: z2.literal("file-copy") }),
|
|
174
|
+
z2.object({ kind: z2.literal("sqlite"), file: z2.string() }),
|
|
175
|
+
z2.object({
|
|
176
|
+
kind: z2.literal("pg-dump"),
|
|
177
|
+
url: stringOrRef,
|
|
178
|
+
version: z2.number().int().positive()
|
|
179
|
+
}),
|
|
180
|
+
z2.object({ kind: z2.literal("valkey-save"), url: stringOrRef }),
|
|
181
|
+
z2.object({
|
|
182
|
+
kind: z2.literal("mysql-dump"),
|
|
183
|
+
url: stringOrRef,
|
|
184
|
+
version: z2.number().int().positive()
|
|
185
|
+
})
|
|
186
|
+
]);
|
|
187
|
+
var absoluteContainerPath = z2.string().startsWith("/", {
|
|
188
|
+
message: "containerPath must be absolute (start with '/')"
|
|
189
|
+
});
|
|
190
|
+
var homeServiceSchema = z2.object({
|
|
191
|
+
// Exactly one of `image` / `build` is present (enforced in the
|
|
192
|
+
// superRefine below). `image` is the prebuilt-pull path; `build` is the
|
|
193
|
+
// build-from-source path, which has no pinned digest until its first
|
|
194
|
+
// build, so Home leaves the app inert until one is set.
|
|
195
|
+
image: imageReferenceSchema.optional(),
|
|
196
|
+
// `build: true` is author sugar for the default recipe — normalize it to
|
|
197
|
+
// `{}` here so `HomeService.build` is always a `BuildRecipe` object.
|
|
198
|
+
// The image/build XOR (superRefine below) reads the transformed value, so
|
|
199
|
+
// `true`/`{}` both count as "build present".
|
|
200
|
+
build: z2.literal(true).or(buildRecipeSchema).transform((v) => v === true ? {} : v).optional(),
|
|
201
|
+
router: routerSchema.optional(),
|
|
202
|
+
hostPorts: z2.array(hostPortSchema).optional(),
|
|
203
|
+
env: z2.record(z2.string(), z2.union([stringOrRef, z2.number()])).optional(),
|
|
204
|
+
files: z2.record(absoluteContainerPath, stringOrRef).optional(),
|
|
205
|
+
command: z2.array(stringOrRef).optional(),
|
|
206
|
+
volumes: z2.array(
|
|
207
|
+
z2.object({
|
|
208
|
+
hostPath: hostPathSchema,
|
|
209
|
+
containerPath: absoluteContainerPath,
|
|
210
|
+
// Silo-owned group. Present only on a Silo's volume entry; the
|
|
211
|
+
// renderer derives `group_add` from it and the grant gate matches
|
|
212
|
+
// (hostPath, gid) against the Silos the app joined.
|
|
213
|
+
gid: z2.number().int().positive().optional(),
|
|
214
|
+
readOnly: z2.boolean().optional(),
|
|
215
|
+
backup: z2.union([backupStrategySchema, z2.literal(false)]).optional()
|
|
216
|
+
})
|
|
217
|
+
).optional(),
|
|
218
|
+
gpu: z2.boolean().optional(),
|
|
219
|
+
iptables: z2.boolean().optional(),
|
|
220
|
+
joinNetworks: z2.array(z2.string()).optional(),
|
|
221
|
+
healthcheck: healthcheckSchema.optional(),
|
|
222
|
+
memory: z2.string().optional(),
|
|
223
|
+
cpu: z2.number().positive().max(128).optional(),
|
|
224
|
+
replicas: z2.number().int().min(1).max(100).default(1),
|
|
225
|
+
labels: z2.array(z2.string()).optional(),
|
|
226
|
+
shmSizeMb: z2.number().int().positive().optional()
|
|
227
|
+
}).strict().superRefine((svc, ctx) => {
|
|
228
|
+
if (svc.image == null && svc.build == null) {
|
|
229
|
+
ctx.addIssue({
|
|
230
|
+
code: "custom",
|
|
231
|
+
path: ["image"],
|
|
232
|
+
message: "a service must set either `image` or `build`"
|
|
233
|
+
});
|
|
234
|
+
} else if (svc.image != null && svc.build != null) {
|
|
235
|
+
ctx.addIssue({
|
|
236
|
+
code: "custom",
|
|
237
|
+
path: ["build"],
|
|
238
|
+
message: "a service cannot set both `image` and `build`"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const seen = /* @__PURE__ */ new Set();
|
|
242
|
+
for (const [i, vol] of (svc.volumes ?? []).entries()) {
|
|
243
|
+
if (seen.has(vol.containerPath)) {
|
|
244
|
+
ctx.addIssue({
|
|
245
|
+
code: "custom",
|
|
246
|
+
path: ["volumes", i, "containerPath"],
|
|
247
|
+
message: `containerPath ${vol.containerPath} is mounted by two volumes`
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
seen.add(vol.containerPath);
|
|
251
|
+
}
|
|
252
|
+
if (!svc.files || !svc.volumes) return;
|
|
253
|
+
const volumePaths = new Set(svc.volumes.map((v) => v.containerPath));
|
|
254
|
+
for (const fileKey of Object.keys(svc.files)) {
|
|
255
|
+
if (volumePaths.has(fileKey)) {
|
|
256
|
+
ctx.addIssue({
|
|
257
|
+
code: "custom",
|
|
258
|
+
path: ["files", fileKey],
|
|
259
|
+
message: `containerPath ${fileKey} is used by both files: and volumes:`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
var requestManifestSchema = z2.object({
|
|
265
|
+
method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
266
|
+
path: z2.string(),
|
|
267
|
+
query: z2.record(z2.string(), z2.union([z2.string(), z2.number(), z2.boolean()])).optional(),
|
|
268
|
+
body: z2.unknown().optional()
|
|
269
|
+
});
|
|
270
|
+
var loaderManifestSchema = z2.object({
|
|
271
|
+
service: z2.string(),
|
|
272
|
+
port: z2.number().int().positive().max(65535),
|
|
273
|
+
headers: z2.record(z2.string(), stringOrRef).optional(),
|
|
274
|
+
outputSchema: z2.unknown(),
|
|
275
|
+
merge: z2.literal(true).optional()
|
|
276
|
+
});
|
|
277
|
+
var actionManifestSchema = z2.object({
|
|
278
|
+
service: z2.string(),
|
|
279
|
+
port: z2.number().int().positive().max(65535),
|
|
280
|
+
headers: z2.record(z2.string(), stringOrRef).optional(),
|
|
281
|
+
inputSchema: z2.unknown(),
|
|
282
|
+
outputSchema: z2.unknown().optional()
|
|
283
|
+
});
|
|
284
|
+
var baseSurfaceManifestSchema = z2.object({
|
|
285
|
+
title: z2.string().min(1),
|
|
286
|
+
// Icon name from `@yourrealm/sdk/ui/components/icons.tsx` named exports.
|
|
287
|
+
// Runtime is just `string` (the TS-side `IconName` union catches
|
|
288
|
+
// typos at authoring time); the Home sidebar falls back to a default
|
|
289
|
+
// glyph if a value isn't a known export, so a typo doesn't 500.
|
|
290
|
+
icon: z2.string().min(1),
|
|
291
|
+
// The surface's live UI state + settings schemas (JSON Schema form).
|
|
292
|
+
// `configSchema` (built from the config leaves' zod schemas) lets Home
|
|
293
|
+
// validate a submitted config; `stateSchema`'s `{}`-parse seeds the
|
|
294
|
+
// runtime's initial state.
|
|
295
|
+
stateSchema: z2.unknown().optional(),
|
|
296
|
+
configSchema: z2.unknown().optional(),
|
|
297
|
+
// The config form's render descriptor: one `ConfigFieldLeaf` per
|
|
298
|
+
// `r.input.*` leaf (type/label/placeholder/default/enum values/bounds),
|
|
299
|
+
// so the tile's config screen reuses the same field renderer the app
|
|
300
|
+
// install/edit form uses. Absent when the surface takes no config.
|
|
301
|
+
configFields: z2.unknown().optional(),
|
|
302
|
+
// The surface's resolved config: each leaf's `.prefill()` default,
|
|
303
|
+
// shipped to the iframe as the base layer for every loader's
|
|
304
|
+
// request(state, config) (Home merges the placement's persisted config
|
|
305
|
+
// over it).
|
|
306
|
+
defaultConfig: z2.unknown().optional(),
|
|
307
|
+
loaders: z2.record(z2.string(), loaderManifestSchema).optional(),
|
|
308
|
+
actions: z2.record(z2.string(), actionManifestSchema).optional()
|
|
309
|
+
});
|
|
310
|
+
var widgetManifestSchema = baseSurfaceManifestSchema.extend({
|
|
311
|
+
cols: z2.number().int().min(1).default(2),
|
|
312
|
+
rows: z2.number().int().min(1).default(2)
|
|
313
|
+
});
|
|
314
|
+
var uiManifestSchema = z2.object({
|
|
315
|
+
pages: z2.record(z2.string(), baseSurfaceManifestSchema).optional(),
|
|
316
|
+
widgets: z2.record(z2.string(), widgetManifestSchema).optional()
|
|
317
|
+
});
|
|
318
|
+
var headerFieldSchema = z2.object({ field: z2.enum(["username", "email"]) });
|
|
319
|
+
var headerSegmentSchema = z2.union([z2.string(), headerFieldSchema]);
|
|
320
|
+
var authHeaderSourceSchema = z2.union([
|
|
321
|
+
headerFieldSchema,
|
|
322
|
+
z2.object({ literal: z2.string() }),
|
|
323
|
+
z2.object({ template: z2.array(headerSegmentSchema) })
|
|
324
|
+
]);
|
|
325
|
+
var trustedHeadersManifestSchema = z2.object({
|
|
326
|
+
keys: z2.array(z2.string()),
|
|
327
|
+
byRole: z2.record(z2.enum(["admin", "member"]), z2.record(z2.string(), authHeaderSourceSchema))
|
|
328
|
+
});
|
|
329
|
+
var homeAppSchema = z2.object({
|
|
330
|
+
services: z2.record(nameSchema, homeServiceSchema),
|
|
331
|
+
egress: z2.literal(true).optional(),
|
|
332
|
+
ui: uiManifestSchema.optional(),
|
|
333
|
+
trustedHeaders: trustedHeadersManifestSchema.optional(),
|
|
334
|
+
// True when the app declares verifyAnonymousRequest. Home-side only (never
|
|
335
|
+
// crosses to the worker wire); recompute reads it to register the app's
|
|
336
|
+
// routed hosts in the anonymous-gate cache.
|
|
337
|
+
hasAnonymousGate: z2.boolean().optional(),
|
|
338
|
+
// Single = run on one selected worker (default). System = fan out to
|
|
339
|
+
// every compatible worker. Drives the install panel's picker and the
|
|
340
|
+
// Home's UI fan-out behavior. See platform/docs realm-ts/workers.mdx.
|
|
341
|
+
kind: z2.enum(["single", "system"]).default("single")
|
|
342
|
+
});
|
|
343
|
+
function resolveDesiredApp(state, opts = {}) {
|
|
344
|
+
const resolvedServices = {};
|
|
345
|
+
for (const [key, service] of Object.entries(state.services ?? {})) {
|
|
346
|
+
if (service instanceof Service) {
|
|
347
|
+
const qualifiedKey = opts.appId ? `${opts.appId}_${key}` : key;
|
|
348
|
+
service._setServiceKey(qualifiedKey);
|
|
349
|
+
resolvedServices[key] = service.getServiceDefinition();
|
|
350
|
+
} else {
|
|
351
|
+
resolvedServices[key] = service;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const serialized = JSON.parse(JSON.stringify({ ...state, services: resolvedServices }));
|
|
355
|
+
return homeAppSchema.parse(serialized);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/http-header.ts
|
|
359
|
+
var HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$/;
|
|
360
|
+
var HEADER_VALUE_RE = /^[\x20-\x7e]*$/;
|
|
361
|
+
|
|
362
|
+
// src/types.ts
|
|
363
|
+
function isTab(node) {
|
|
364
|
+
return node.kind === "node.tab";
|
|
365
|
+
}
|
|
366
|
+
function isSection(node) {
|
|
367
|
+
return node.kind === "node.section";
|
|
368
|
+
}
|
|
369
|
+
function isInput(node) {
|
|
370
|
+
return typeof node.kind === "string" && node.kind.startsWith("input.");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/create-app.ts
|
|
374
|
+
var IDENT_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
375
|
+
function assertValidKeys(schema, prefix = "") {
|
|
376
|
+
for (const [key, node] of Object.entries(schema)) {
|
|
377
|
+
if (!IDENT_RE.test(key)) {
|
|
378
|
+
const where = prefix ? `at "${prefix}.${key}"` : `at "${key}"`;
|
|
379
|
+
throw new Error(
|
|
380
|
+
`configSchema key must match /^[a-zA-Z_][a-zA-Z0-9_]*$/ ${where} - use identifier-shaped names (letters, digits, underscore)`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (isTab(node) || isSection(node)) {
|
|
384
|
+
assertValidKeys(node.children, prefix ? `${prefix}.${key}` : key);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function assertSchemaHomogeneous(schema) {
|
|
389
|
+
const keys = Object.keys(schema);
|
|
390
|
+
if (keys.length === 0) return;
|
|
391
|
+
const tabs = keys.filter((k) => isTab(schema[k]));
|
|
392
|
+
if (tabs.length > 0 && tabs.length !== keys.length) {
|
|
393
|
+
const first = tabs[0];
|
|
394
|
+
const other = keys.find((k) => !isTab(schema[k]));
|
|
395
|
+
throw new Error(
|
|
396
|
+
`configSchema must be either all r.tab(...) or none - "${first}" is a tab, "${other}" is not`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function collectPermissionNodes(schema, out = {}, prefix = "") {
|
|
401
|
+
for (const [key, node] of Object.entries(schema)) {
|
|
402
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
403
|
+
if (isTab(node) || isSection(node)) {
|
|
404
|
+
collectPermissionNodes(node.children, out, path);
|
|
405
|
+
} else if (!isInput(node)) {
|
|
406
|
+
out[path] = node;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
var OMIT = /* @__PURE__ */ Symbol("omit");
|
|
412
|
+
function buildConfigShape(schema) {
|
|
413
|
+
const out = {};
|
|
414
|
+
for (const [key, node] of Object.entries(schema)) {
|
|
415
|
+
const built = buildNodeShape(node);
|
|
416
|
+
if (built !== OMIT) out[key] = built;
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
function buildNodeShape(node) {
|
|
421
|
+
if (isTab(node)) {
|
|
422
|
+
const sub = {};
|
|
423
|
+
for (const [k, child] of Object.entries(node.children)) {
|
|
424
|
+
const built = buildNodeShape(child);
|
|
425
|
+
if (built !== OMIT) sub[k] = built;
|
|
426
|
+
}
|
|
427
|
+
return sub;
|
|
428
|
+
}
|
|
429
|
+
if (isSection(node)) {
|
|
430
|
+
if (node.meta.isOptional) return OMIT;
|
|
431
|
+
const sub = {};
|
|
432
|
+
for (const [k, child] of Object.entries(node.children)) {
|
|
433
|
+
const built = buildNodeShape(child);
|
|
434
|
+
if (built !== OMIT) sub[k] = built;
|
|
435
|
+
}
|
|
436
|
+
return sub;
|
|
437
|
+
}
|
|
438
|
+
if (isInput(node)) {
|
|
439
|
+
const value = node.meta.prefill;
|
|
440
|
+
if (value !== void 0) return value;
|
|
441
|
+
return OMIT;
|
|
442
|
+
}
|
|
443
|
+
return OMIT;
|
|
444
|
+
}
|
|
445
|
+
var AUTH_ROLES = ["admin", "member"];
|
|
446
|
+
function sampleTrustedHeaders(fn) {
|
|
447
|
+
const keys = /* @__PURE__ */ new Set();
|
|
448
|
+
const byRole = {};
|
|
449
|
+
for (const role of AUTH_ROLES) {
|
|
450
|
+
const out = fn({ username: identityRef("username"), email: identityRef("email"), role });
|
|
451
|
+
const map = {};
|
|
452
|
+
for (const [key, value] of Object.entries(out)) {
|
|
453
|
+
if (!HEADER_NAME_RE.test(key)) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
`getTrustedHeaders["${key}"]: not a valid HTTP header name (letters, digits, and !#$%&'*+-.^_\`|~). Every call into the app carries these headers, and a malformed name is rejected outright`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (key.toLowerCase().startsWith("x-realm-")) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`getTrustedHeaders["${key}"]: the X-Realm-* namespace is reserved for the platform. Home and the worker stamp headers there and clients are stripped of them, so a container can trust them; pick a name outside it`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
keys.add(key);
|
|
464
|
+
map[key] = toTrustedHeaderSource(key, value);
|
|
465
|
+
}
|
|
466
|
+
byRole[role] = map;
|
|
467
|
+
}
|
|
468
|
+
return { keys: [...keys], byRole };
|
|
469
|
+
}
|
|
470
|
+
function assertHeaderLiteral(key, text2) {
|
|
471
|
+
if (HEADER_VALUE_RE.test(text2)) return;
|
|
472
|
+
throw new Error(
|
|
473
|
+
`getTrustedHeaders["${key}"]: value must be printable ASCII. A character outside it is rejected on the way into the container, failing every call the app makes`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
function toTrustedHeaderSource(key, value) {
|
|
477
|
+
if (typeof value === "string") {
|
|
478
|
+
assertHeaderLiteral(key, value);
|
|
479
|
+
return { literal: value };
|
|
480
|
+
}
|
|
481
|
+
if (typeof value === "object" && value !== null && REF_SYMBOL in value) {
|
|
482
|
+
const json = value.toJSON();
|
|
483
|
+
if (typeof json === "string") {
|
|
484
|
+
assertHeaderLiteral(key, json);
|
|
485
|
+
return { literal: json };
|
|
486
|
+
}
|
|
487
|
+
const marker = json.__realmRef;
|
|
488
|
+
if (marker.kind === "identity") return { field: identityField(key, marker.field) };
|
|
489
|
+
if (marker.kind === "concat") {
|
|
490
|
+
return { template: (marker.parts ?? []).map((part) => headerSegment(key, part)) };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
throw new Error(`getTrustedHeaders["${key}"]: value must be a user field or a literal string`);
|
|
494
|
+
}
|
|
495
|
+
function headerSegment(key, part) {
|
|
496
|
+
if (typeof part === "string") {
|
|
497
|
+
assertHeaderLiteral(key, part);
|
|
498
|
+
return part;
|
|
499
|
+
}
|
|
500
|
+
const m = part;
|
|
501
|
+
if (m?.kind === "identity") return { field: identityField(key, m.field) };
|
|
502
|
+
throw new Error(
|
|
503
|
+
`getTrustedHeaders["${key}"]: only user fields (user.username / user.email) and literal text can compose a header value`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
function identityField(key, field) {
|
|
507
|
+
if (field === "username" || field === "email") return field;
|
|
508
|
+
throw new Error(`getTrustedHeaders["${key}"]: unknown user field "${field}"`);
|
|
509
|
+
}
|
|
510
|
+
function assertBaseSurface(surface, where) {
|
|
511
|
+
if (typeof surface.title !== "string" || surface.title.length === 0) {
|
|
512
|
+
throw new Error(`${where}.title must be a non-empty string`);
|
|
513
|
+
}
|
|
514
|
+
if (typeof surface.icon !== "string" || surface.icon.length === 0) {
|
|
515
|
+
throw new Error(`${where}.icon must be a non-empty string from the icon registry`);
|
|
516
|
+
}
|
|
517
|
+
if (typeof surface.render !== "function") {
|
|
518
|
+
throw new Error(`${where}.render must be a function`);
|
|
519
|
+
}
|
|
520
|
+
if (surface.loaders) {
|
|
521
|
+
for (const [lk, l] of Object.entries(surface.loaders)) {
|
|
522
|
+
if (!IDENT_RE.test(lk)) {
|
|
523
|
+
throw new Error(`${where}.loaders key "${lk}" must be an identifier`);
|
|
524
|
+
}
|
|
525
|
+
if (typeof l.service !== "string" || typeof l.port !== "number") {
|
|
526
|
+
throw new Error(`${where}.loaders.${lk} needs a string service and a number port`);
|
|
527
|
+
}
|
|
528
|
+
if (typeof l.request !== "function") {
|
|
529
|
+
throw new Error(`${where}.loaders.${lk}.request must be a (state, config) => request fn`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (surface.actions) {
|
|
534
|
+
for (const [ak, a] of Object.entries(surface.actions)) {
|
|
535
|
+
if (!IDENT_RE.test(ak)) {
|
|
536
|
+
throw new Error(`${where}.actions key "${ak}" must be an identifier`);
|
|
537
|
+
}
|
|
538
|
+
if (typeof a.service !== "string" || typeof a.port !== "number") {
|
|
539
|
+
throw new Error(`${where}.actions.${ak} needs a string service and a number port`);
|
|
540
|
+
}
|
|
541
|
+
if (typeof a.request !== "function") {
|
|
542
|
+
throw new Error(`${where}.actions.${ak}.request must be an (input, config) => request fn`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function assertPageMap(pages) {
|
|
548
|
+
for (const [key, p] of Object.entries(pages)) {
|
|
549
|
+
if (!IDENT_RE.test(key)) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`ui.pages key must match /^[a-zA-Z_][a-zA-Z0-9_]*$/ at "${key}" - use identifier-shaped names (letters, digits, underscore)`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
assertBaseSurface(p, `ui.pages.${key}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function assertWidgetMap(widgets) {
|
|
558
|
+
for (const [key, w] of Object.entries(widgets)) {
|
|
559
|
+
if (!IDENT_RE.test(key)) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
`ui.widgets key must match /^[a-zA-Z_][a-zA-Z0-9_]*$/ at "${key}" - use identifier-shaped names (letters, digits, underscore)`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
assertBaseSurface(w, `ui.widgets.${key}`);
|
|
565
|
+
for (const dim of ["cols", "rows"]) {
|
|
566
|
+
const v = w[dim];
|
|
567
|
+
if (v === void 0) continue;
|
|
568
|
+
if (!Number.isInteger(v) || v < 1) {
|
|
569
|
+
throw new Error(`ui.widgets.${key}.${dim} must be an integer \u2265 1`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function assertValidUi(ui) {
|
|
575
|
+
if (ui.pages) assertPageMap(ui.pages);
|
|
576
|
+
if (ui.widgets) assertWidgetMap(ui.widgets);
|
|
577
|
+
}
|
|
578
|
+
function assertMergeMatchesKind(ui, kind) {
|
|
579
|
+
const visit = (where, surface) => {
|
|
580
|
+
if (!surface.loaders) return;
|
|
581
|
+
for (const [name, l] of Object.entries(surface.loaders)) {
|
|
582
|
+
const hasMerge = typeof l.merge === "function";
|
|
583
|
+
if (kind === "system" && !hasMerge) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`${where}.loaders.${name}: createSystemApp requires merge(results) on every loader`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (kind === "single" && hasMerge) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`${where}.loaders.${name}: createApp forbids merge \u2014 only one worker responds`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
if (ui.pages) for (const [k, p] of Object.entries(ui.pages)) visit(`ui.pages.${k}`, p);
|
|
596
|
+
if (ui.widgets) for (const [k, w] of Object.entries(ui.widgets)) visit(`ui.widgets.${k}`, w);
|
|
597
|
+
}
|
|
598
|
+
function headerToManifest(value) {
|
|
599
|
+
if (typeof value === "string") return value;
|
|
600
|
+
if (value && typeof value === "object" && REF_SYMBOL in value) {
|
|
601
|
+
const json = value.toJSON();
|
|
602
|
+
return json;
|
|
603
|
+
}
|
|
604
|
+
throw new Error("ui proxy headers must be strings or refs (e.g. r.stableSecret(...))");
|
|
605
|
+
}
|
|
606
|
+
function headersToManifest(headers) {
|
|
607
|
+
if (!headers) return void 0;
|
|
608
|
+
const out = {};
|
|
609
|
+
for (const [k, v] of Object.entries(headers)) out[k] = headerToManifest(v);
|
|
610
|
+
return out;
|
|
611
|
+
}
|
|
612
|
+
function toJsonSchemaSafe(schema) {
|
|
613
|
+
try {
|
|
614
|
+
return z3.toJSONSchema(schema);
|
|
615
|
+
} catch (e) {
|
|
616
|
+
if (e instanceof Error && e.message.includes("cannot be represented in JSON Schema")) {
|
|
617
|
+
return {};
|
|
618
|
+
}
|
|
619
|
+
throw e;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function loaderToManifest(loader) {
|
|
623
|
+
const manifest = {
|
|
624
|
+
outputSchema: toJsonSchemaSafe(loader.outputSchema),
|
|
625
|
+
service: loader.service,
|
|
626
|
+
port: loader.port,
|
|
627
|
+
headers: headersToManifest(loader.headers)
|
|
628
|
+
};
|
|
629
|
+
if (loader.merge) manifest.merge = true;
|
|
630
|
+
if (manifest.headers === void 0) delete manifest.headers;
|
|
631
|
+
return manifest;
|
|
632
|
+
}
|
|
633
|
+
function actionToManifest(action) {
|
|
634
|
+
const manifest = {
|
|
635
|
+
inputSchema: toJsonSchemaSafe(action.inputSchema),
|
|
636
|
+
service: action.service,
|
|
637
|
+
port: action.port,
|
|
638
|
+
headers: headersToManifest(action.headers)
|
|
639
|
+
};
|
|
640
|
+
if (action.outputSchema) manifest.outputSchema = toJsonSchemaSafe(action.outputSchema);
|
|
641
|
+
if (manifest.headers === void 0) delete manifest.headers;
|
|
642
|
+
return manifest;
|
|
643
|
+
}
|
|
644
|
+
function describeSurfaceConfig(schema) {
|
|
645
|
+
const fields = {};
|
|
646
|
+
const shape = {};
|
|
647
|
+
const defaultConfig = {};
|
|
648
|
+
for (const [key, node] of Object.entries(schema)) {
|
|
649
|
+
if (!isInput(node)) {
|
|
650
|
+
throw new Error(
|
|
651
|
+
`ui surface configSchema "${key}": only r.input.* leaves are allowed (no permissions, tabs, or sections).`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
if (node.kind === "input.secret") {
|
|
655
|
+
throw new Error(
|
|
656
|
+
`ui surface configSchema "${key}": r.input.secret() isn't supported in a surface config - a widget config holds display settings, not credentials. Declare the secret in the app's top-level configSchema and pass its ref to a loader header.`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
const specific = node.kind.slice("input.".length);
|
|
660
|
+
const meta = node.meta ?? {};
|
|
661
|
+
const leaf = { type: specific };
|
|
662
|
+
if (meta.title) leaf.label = meta.title;
|
|
663
|
+
if (meta.placeholder !== void 0) leaf.placeholder = meta.placeholder;
|
|
664
|
+
if (meta.prefill !== void 0) {
|
|
665
|
+
leaf.default = meta.prefill;
|
|
666
|
+
defaultConfig[key] = meta.prefill;
|
|
667
|
+
}
|
|
668
|
+
if (meta.description) leaf.description = meta.description;
|
|
669
|
+
if (meta.isOptional) leaf.optional = true;
|
|
670
|
+
if (specific === "enum" && "values" in node) {
|
|
671
|
+
leaf.values = [...node.values];
|
|
672
|
+
}
|
|
673
|
+
if (specific === "number") {
|
|
674
|
+
if (meta.int) leaf.step = 1;
|
|
675
|
+
if (meta.min !== void 0) leaf.min = meta.min;
|
|
676
|
+
if (meta.max !== void 0) leaf.max = meta.max;
|
|
677
|
+
}
|
|
678
|
+
fields[key] = leaf;
|
|
679
|
+
shape[key] = meta.isOptional || meta.prefill !== void 0 ? node.schema.optional() : node.schema;
|
|
680
|
+
}
|
|
681
|
+
return { fields, defaultConfig, validation: z3.object(shape) };
|
|
682
|
+
}
|
|
683
|
+
function surfaceToManifest(s) {
|
|
684
|
+
const manifest = { title: s.title, icon: s.icon };
|
|
685
|
+
if (s.stateSchema) manifest.stateSchema = toJsonSchemaSafe(s.stateSchema);
|
|
686
|
+
if (s.loaders) {
|
|
687
|
+
manifest.loaders = {};
|
|
688
|
+
for (const [k, l] of Object.entries(s.loaders)) {
|
|
689
|
+
manifest.loaders[k] = loaderToManifest(l);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (s.actions) {
|
|
693
|
+
manifest.actions = {};
|
|
694
|
+
for (const [k, a] of Object.entries(s.actions)) {
|
|
695
|
+
manifest.actions[k] = actionToManifest(a);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return manifest;
|
|
699
|
+
}
|
|
700
|
+
function widgetToManifest(w) {
|
|
701
|
+
const manifest = {
|
|
702
|
+
...surfaceToManifest(w),
|
|
703
|
+
cols: w.cols ?? 2,
|
|
704
|
+
rows: w.rows ?? 2
|
|
705
|
+
};
|
|
706
|
+
if (w.configSchema) {
|
|
707
|
+
const { fields, defaultConfig, validation } = describeSurfaceConfig(
|
|
708
|
+
w.configSchema
|
|
709
|
+
);
|
|
710
|
+
manifest.configFields = fields;
|
|
711
|
+
manifest.configSchema = toJsonSchemaSafe(validation);
|
|
712
|
+
manifest.defaultConfig = defaultConfig;
|
|
713
|
+
}
|
|
714
|
+
return manifest;
|
|
715
|
+
}
|
|
716
|
+
function buildUiManifest(ui) {
|
|
717
|
+
const manifest = {};
|
|
718
|
+
if (ui.pages) {
|
|
719
|
+
manifest.pages = {};
|
|
720
|
+
for (const [k, p] of Object.entries(ui.pages)) {
|
|
721
|
+
if (p.configSchema) {
|
|
722
|
+
throw new Error(
|
|
723
|
+
`ui page "${k}": only widgets can be configured \u2014 move the configSchema to a widget or drop it.`
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
manifest.pages[k] = surfaceToManifest(p);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (ui.widgets) {
|
|
730
|
+
manifest.widgets = {};
|
|
731
|
+
for (const [k, w] of Object.entries(ui.widgets)) {
|
|
732
|
+
manifest.widgets[k] = widgetToManifest(w);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return manifest;
|
|
736
|
+
}
|
|
737
|
+
function buildApp(kind, options) {
|
|
738
|
+
const schema = options.configSchema;
|
|
739
|
+
if (schema) {
|
|
740
|
+
assertValidKeys(schema);
|
|
741
|
+
assertSchemaHomogeneous(schema);
|
|
742
|
+
const scopeViolations = checkPermissionAppKinds(kind, collectPermissionNodes(schema));
|
|
743
|
+
if (scopeViolations.length > 0) {
|
|
744
|
+
const factory = kind === "system" ? "createSystemApp" : "createApp";
|
|
745
|
+
throw new Error(`${factory}: ${scopeViolations.map((v) => v.description).join("; ")}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (options.info && kind === "system") {
|
|
749
|
+
throw new Error(
|
|
750
|
+
`createSystemApp: \`info\` is not supported \u2014 a fanned-out service has no single URL for services.<name> to resolve to`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
if (options.ui) {
|
|
754
|
+
assertValidUi(options.ui);
|
|
755
|
+
assertMergeMatchesKind(options.ui, kind);
|
|
756
|
+
}
|
|
757
|
+
let uiManifest = options.ui ? buildUiManifest(options.ui) : void 0;
|
|
758
|
+
const app = {
|
|
759
|
+
name: options.name,
|
|
760
|
+
description: options.description,
|
|
761
|
+
kind,
|
|
762
|
+
verifyAnonymousRequest: options.verifyAnonymousRequest,
|
|
763
|
+
configSchema: options.configSchema,
|
|
764
|
+
ui: options.ui,
|
|
765
|
+
info: options.info,
|
|
766
|
+
attachSurfaces(ui) {
|
|
767
|
+
if (app.ui) {
|
|
768
|
+
throw new Error(
|
|
769
|
+
"createApp: `ui` is already set \u2014 remove the inline `ui:` block to use file-based surfaces (pages/*, widgets/*), or delete the surface files."
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
assertValidUi(ui);
|
|
773
|
+
assertMergeMatchesKind(ui, kind);
|
|
774
|
+
app.ui = ui;
|
|
775
|
+
uiManifest = buildUiManifest(ui);
|
|
776
|
+
},
|
|
777
|
+
evaluate({ config, appId, preview }) {
|
|
778
|
+
const shaped = schema ? deepMergeConfig(
|
|
779
|
+
buildConfigShape(schema),
|
|
780
|
+
config ?? {}
|
|
781
|
+
) : config;
|
|
782
|
+
const state = options.getDesiredState({ config: shaped, preview: preview ?? null });
|
|
783
|
+
const withExtras = {
|
|
784
|
+
...state,
|
|
785
|
+
...options.getTrustedHeaders ? { trustedHeaders: sampleTrustedHeaders(options.getTrustedHeaders) } : {},
|
|
786
|
+
...options.verifyAnonymousRequest ? { hasAnonymousGate: true } : {},
|
|
787
|
+
...uiManifest ? { ui: uiManifest } : {}
|
|
788
|
+
};
|
|
789
|
+
const resolved = resolveDesiredApp({ ...withExtras, kind }, { appId });
|
|
790
|
+
if (kind === "system") {
|
|
791
|
+
const violations = checkSystemAppRoutes(resolved);
|
|
792
|
+
if (violations.length > 0) {
|
|
793
|
+
throw new Error(violations.map((v) => v.description).join("; "));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return resolved;
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
return app;
|
|
800
|
+
}
|
|
801
|
+
function createApp(options) {
|
|
802
|
+
return buildApp("single", options);
|
|
803
|
+
}
|
|
804
|
+
function createSystemApp(options) {
|
|
805
|
+
if (options.getTrustedHeaders) {
|
|
806
|
+
throw new Error(
|
|
807
|
+
"createSystemApp: `getTrustedHeaders` is not supported \u2014 header brokering happens on an authenticated subdomain, and a system app has no routes"
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
if (options.verifyAnonymousRequest) {
|
|
811
|
+
throw new Error(
|
|
812
|
+
"createSystemApp: `verifyAnonymousRequest` is not supported \u2014 anonymous gates admit callers to an app's routes, and a system app has none"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return buildApp("system", options);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/index.ts
|
|
819
|
+
import { z as z6 } from "zod";
|
|
820
|
+
|
|
821
|
+
// src/schema/inputs.ts
|
|
822
|
+
import { z as z4 } from "zod";
|
|
823
|
+
var text = baseInput("input.text", z4.string());
|
|
824
|
+
var secret = baseInput("input.secret", z4.string());
|
|
825
|
+
var number = numberInput("input.number", z4.number());
|
|
826
|
+
var boolean = baseInput("input.boolean", z4.boolean());
|
|
827
|
+
function enum_(values) {
|
|
828
|
+
const record = baseInput("input.enum", z4.enum(values))();
|
|
829
|
+
return cloneAnd(record, { values });
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/schema/nodes.ts
|
|
833
|
+
function section(children) {
|
|
834
|
+
const record = {
|
|
835
|
+
kind: "node.section",
|
|
836
|
+
children,
|
|
837
|
+
meta: { isOptional: false }
|
|
838
|
+
};
|
|
839
|
+
defineMethod(record, "title", function(text2) {
|
|
840
|
+
return cloneAnd(this, { meta: { ...this.meta, title: text2 } });
|
|
841
|
+
});
|
|
842
|
+
defineMethod(record, "describe", function(text2) {
|
|
843
|
+
return cloneAnd(this, { meta: { ...this.meta, description: text2 } });
|
|
844
|
+
});
|
|
845
|
+
defineMethod(record, "optional", function() {
|
|
846
|
+
return cloneAnd(this, { meta: { ...this.meta, isOptional: true } });
|
|
847
|
+
});
|
|
848
|
+
return record;
|
|
849
|
+
}
|
|
850
|
+
function tab(children) {
|
|
851
|
+
const record = {
|
|
852
|
+
kind: "node.tab",
|
|
853
|
+
children,
|
|
854
|
+
meta: {}
|
|
855
|
+
};
|
|
856
|
+
defineMethod(record, "title", function(text2) {
|
|
857
|
+
return cloneAnd(this, { meta: { ...this.meta, title: text2 } });
|
|
858
|
+
});
|
|
859
|
+
defineMethod(record, "describe", function(text2) {
|
|
860
|
+
return cloneAnd(this, { meta: { ...this.meta, description: text2 } });
|
|
861
|
+
});
|
|
862
|
+
return record;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/stable-secret.ts
|
|
866
|
+
function stableSecret(name) {
|
|
867
|
+
return ref({ kind: "derived", appId: SELF_APP_ID, name });
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/runtime/docker/home-proxy-server/protocol.ts
|
|
871
|
+
import { z as z5 } from "zod";
|
|
872
|
+
var MAX_PATH = 2048;
|
|
873
|
+
var SAFE_SUBPATH = /^\/[A-Za-z0-9._~/?=&%-]*$/;
|
|
874
|
+
function isSafeSubPath(p) {
|
|
875
|
+
if (!SAFE_SUBPATH.test(p)) return false;
|
|
876
|
+
if (p.includes("..")) return false;
|
|
877
|
+
if (p.includes("//")) return false;
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
var homeProxyRequestSchema = z5.object({
|
|
881
|
+
method: z5.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
882
|
+
path: z5.string().min(1).max(MAX_PATH).refine(isSafeSubPath, { message: "unsafe path" }),
|
|
883
|
+
body: z5.unknown().optional()
|
|
884
|
+
}).strict();
|
|
885
|
+
var homeProxyResponseSchema = z5.object({
|
|
886
|
+
status: z5.number().int(),
|
|
887
|
+
body: z5.unknown()
|
|
888
|
+
}).strict();
|
|
889
|
+
|
|
890
|
+
// src/runtime/home-client/index.ts
|
|
891
|
+
async function callHome(method, path, body, opts = {}) {
|
|
892
|
+
const socketPath = opts.socketPath ?? REALM_HOME_PROXY_CONTAINER_PATH;
|
|
893
|
+
const conn = await Deno.connect({ transport: "unix", path: socketPath });
|
|
894
|
+
const encoder = new TextEncoder();
|
|
895
|
+
const decoder = new TextDecoder("utf-8");
|
|
896
|
+
try {
|
|
897
|
+
const line = JSON.stringify(body === void 0 ? { method, path } : { method, path, body });
|
|
898
|
+
await conn.write(encoder.encode(line + "\n"));
|
|
899
|
+
const buf = new Uint8Array(8192);
|
|
900
|
+
let remainder = "";
|
|
901
|
+
while (true) {
|
|
902
|
+
const n = await conn.read(buf);
|
|
903
|
+
if (n === null) break;
|
|
904
|
+
remainder += decoder.decode(buf.slice(0, n), { stream: true });
|
|
905
|
+
const nl = remainder.indexOf("\n");
|
|
906
|
+
if (nl !== -1) {
|
|
907
|
+
const raw = remainder.slice(0, nl).trim();
|
|
908
|
+
const parsed = homeProxyResponseSchema.parse(JSON.parse(raw));
|
|
909
|
+
return { status: parsed.status, body: parsed.body };
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
throw new Error("home proxy closed without a response line");
|
|
913
|
+
} finally {
|
|
914
|
+
try {
|
|
915
|
+
conn.close();
|
|
916
|
+
} catch {
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/apply.ts
|
|
922
|
+
function registry() {
|
|
923
|
+
const g = globalThis;
|
|
924
|
+
let fns = g.__realm_deferred_fns;
|
|
925
|
+
if (!fns) {
|
|
926
|
+
fns = [];
|
|
927
|
+
g.__realm_deferred_fns = fns;
|
|
928
|
+
}
|
|
929
|
+
return fns;
|
|
930
|
+
}
|
|
931
|
+
function apply(input, fn) {
|
|
932
|
+
const fns = registry();
|
|
933
|
+
const id = fns.length;
|
|
934
|
+
fns.push(fn);
|
|
935
|
+
return deferredRef({ kind: "deferred", id, input });
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/yaml.ts
|
|
939
|
+
import { stringify } from "yaml";
|
|
940
|
+
|
|
941
|
+
// src/serialize.ts
|
|
942
|
+
function serializeRefs(value, serialize) {
|
|
943
|
+
if (!containsRef(value)) return serialize(value);
|
|
944
|
+
return apply(value, serialize);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/yaml.ts
|
|
948
|
+
function toYaml(value) {
|
|
949
|
+
return serializeRefs(value, (resolved) => stringify(resolved));
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/ini.ts
|
|
953
|
+
function scalar(value) {
|
|
954
|
+
return typeof value === "boolean" ? value ? "true" : "false" : String(value);
|
|
955
|
+
}
|
|
956
|
+
function emitIni(value) {
|
|
957
|
+
const orphans = [];
|
|
958
|
+
const sections = [];
|
|
959
|
+
for (const [k, v] of Object.entries(value)) {
|
|
960
|
+
if (typeof v === "object" && v !== null) {
|
|
961
|
+
sections.push([k, v]);
|
|
962
|
+
} else {
|
|
963
|
+
orphans.push([k, v]);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
let out = "";
|
|
967
|
+
for (const [k, v] of orphans) out += `${k} = ${scalar(v)}
|
|
968
|
+
`;
|
|
969
|
+
for (const [section2, body] of sections) {
|
|
970
|
+
if (out.length > 0) out += "\n";
|
|
971
|
+
out += `[${section2}]
|
|
972
|
+
`;
|
|
973
|
+
for (const [k, v] of Object.entries(body)) {
|
|
974
|
+
out += `${k} = ${scalar(v)}
|
|
975
|
+
`;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return out;
|
|
979
|
+
}
|
|
980
|
+
function toIni(value) {
|
|
981
|
+
return serializeRefs(value, emitIni);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/json.ts
|
|
985
|
+
function toJson(value) {
|
|
986
|
+
return serializeRefs(value, (resolved) => JSON.stringify(resolved));
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/xml.ts
|
|
990
|
+
function escapeXmlText(s) {
|
|
991
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
992
|
+
}
|
|
993
|
+
function emit(parts, indent, key, value) {
|
|
994
|
+
parts.push(`${indent}<${key}>`);
|
|
995
|
+
if (typeof value === "string") {
|
|
996
|
+
parts.push(escapeXmlText(value));
|
|
997
|
+
} else if (typeof value === "number") {
|
|
998
|
+
parts.push(String(value));
|
|
999
|
+
} else {
|
|
1000
|
+
parts.push("\n");
|
|
1001
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1002
|
+
emit(parts, indent + " ", k, v);
|
|
1003
|
+
}
|
|
1004
|
+
parts.push(indent);
|
|
1005
|
+
}
|
|
1006
|
+
parts.push(`</${key}>
|
|
1007
|
+
`);
|
|
1008
|
+
}
|
|
1009
|
+
function toXml(value) {
|
|
1010
|
+
return serializeRefs(value, (resolved) => {
|
|
1011
|
+
const parts = [];
|
|
1012
|
+
for (const [k, v] of Object.entries(resolved)) {
|
|
1013
|
+
emit(parts, "", k, v);
|
|
1014
|
+
}
|
|
1015
|
+
return parts.join("");
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/index.ts
|
|
1020
|
+
var r = {
|
|
1021
|
+
host: host_exports,
|
|
1022
|
+
peers: peer_exports,
|
|
1023
|
+
remote: remote_exports,
|
|
1024
|
+
home: home_exports,
|
|
1025
|
+
input: {
|
|
1026
|
+
text,
|
|
1027
|
+
secret,
|
|
1028
|
+
number,
|
|
1029
|
+
boolean,
|
|
1030
|
+
enum: enum_
|
|
1031
|
+
},
|
|
1032
|
+
section,
|
|
1033
|
+
tab,
|
|
1034
|
+
stableSecret
|
|
1035
|
+
};
|
|
1036
|
+
export {
|
|
1037
|
+
HOME_COOKIE_PREFIX,
|
|
1038
|
+
apply,
|
|
1039
|
+
callHome,
|
|
1040
|
+
createApp,
|
|
1041
|
+
createSystemApp,
|
|
1042
|
+
interpolate,
|
|
1043
|
+
isNetworkStrategy,
|
|
1044
|
+
isVolumeStrategy,
|
|
1045
|
+
nameSchema,
|
|
1046
|
+
r,
|
|
1047
|
+
ref,
|
|
1048
|
+
toIni,
|
|
1049
|
+
toJson,
|
|
1050
|
+
toXml,
|
|
1051
|
+
toYaml,
|
|
1052
|
+
z6 as z
|
|
1053
|
+
};
|