@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-EXCEPTION +26 -0
- package/README.md +34 -0
- package/dist/chunk-3R6KEFOI.js +22 -0
- package/dist/chunk-67BDAMYX.js +376 -0
- package/dist/chunk-AITGS27R.js +139 -0
- package/dist/chunk-CMAEHQ4F.js +6 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-O3UIK34B.js +1430 -0
- package/dist/chunk-VHTUSZUX.js +176 -0
- package/dist/cli.js +9006 -0
- package/dist/docker.js +54 -0
- package/dist/index.js +1053 -0
- package/dist/ingress.js +436 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/preact.js +22 -0
- package/dist/service-url.js +11 -0
- package/dist/services.js +213 -0
- package/dist/test.js +5069 -0
- package/dist/ui.js +254 -0
- package/dist/worker-protocol.js +89 -0
- package/package.json +112 -0
- package/src/app-icon.ts +74 -0
- package/src/apply.ts +52 -0
- package/src/bundle-ui.ts +211 -0
- package/src/bundle.ts +546 -0
- package/src/cli/build-dev-landing.ts +84 -0
- package/src/cli/build-install-panel.ts +90 -0
- package/src/cli/build-runtime.ts +133 -0
- package/src/cli/build.ts +63 -0
- package/src/cli/dev.ts +107 -0
- package/src/cli/down.ts +60 -0
- package/src/cli/main.ts +56 -0
- package/src/cli/test.ts +61 -0
- package/src/cli/validate.ts +37 -0
- package/src/compile.ts +100 -0
- package/src/create-app.ts +982 -0
- package/src/deep-merge.ts +33 -0
- package/src/dev/fixture.ts +48 -0
- package/src/dev/install-handler.ts +383 -0
- package/src/dev/server.ts +968 -0
- package/src/dev/stack.ts +108 -0
- package/src/dev/state.ts +159 -0
- package/src/docker.ts +96 -0
- package/src/errors.ts +61 -0
- package/src/factory.ts +481 -0
- package/src/garage.ts +56 -0
- package/src/generator/hash.ts +6 -0
- package/src/generator/resolve-refs.ts +207 -0
- package/src/grants-builder.ts +100 -0
- package/src/grants.ts +260 -0
- package/src/home-api.ts +172 -0
- package/src/home-cookie.ts +10 -0
- package/src/home-registry.ts +120 -0
- package/src/home.ts +187 -0
- package/src/host-registry.ts +143 -0
- package/src/http-header.ts +20 -0
- package/src/identifiers.ts +49 -0
- package/src/index.ts +66 -0
- package/src/info.ts +64 -0
- package/src/ini.ts +50 -0
- package/src/json.ts +14 -0
- package/src/jsx-runtime/index.ts +5 -0
- package/src/keys.ts +29 -0
- package/src/mysql.ts +56 -0
- package/src/pack-build.ts +108 -0
- package/src/pack.ts +115 -0
- package/src/peer-registry.ts +121 -0
- package/src/peer-world.ts +117 -0
- package/src/permission-meta.ts +116 -0
- package/src/permissions.ts +108 -0
- package/src/pkg-root.ts +34 -0
- package/src/postgres.ts +79 -0
- package/src/preact/index.ts +27 -0
- package/src/redis.ts +43 -0
- package/src/ref.ts +271 -0
- package/src/remote-registry.ts +33 -0
- package/src/required-host-kinds.ts +110 -0
- package/src/resolve-context.ts +111 -0
- package/src/resolve.ts +471 -0
- package/src/runtime/docker/compose/compose-schema.ts +118 -0
- package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
- package/src/runtime/docker/compose/default-hardening.ts +21 -0
- package/src/runtime/docker/compose/effects.ts +107 -0
- package/src/runtime/docker/compose/index.ts +17 -0
- package/src/runtime/docker/compose/labels.ts +39 -0
- package/src/runtime/docker/compose/types.ts +78 -0
- package/src/runtime/docker/home-proxy-server/index.ts +177 -0
- package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
- package/src/runtime/docker/ingress/build-config.ts +819 -0
- package/src/runtime/docker/ingress/index.ts +16 -0
- package/src/runtime/docker/ingress/types.ts +182 -0
- package/src/runtime/docker/log-frames.ts +121 -0
- package/src/runtime/docker/logs-server/index.ts +366 -0
- package/src/runtime/docker/spawn-server/context-key.ts +82 -0
- package/src/runtime/docker/spawn-server/index.ts +492 -0
- package/src/runtime/docker/spawn-server/protocol.ts +70 -0
- package/src/runtime/docker/spawn-server/spec.ts +122 -0
- package/src/runtime/docker/stats-server/disks.ts +161 -0
- package/src/runtime/docker/stats-server/index.ts +322 -0
- package/src/runtime/home-client/index.ts +71 -0
- package/src/runtime/log-line.ts +24 -0
- package/src/runtime/logs-client/index.ts +175 -0
- package/src/runtime/spawn-client/index.ts +93 -0
- package/src/runtime/spawn-client/kaniko.ts +79 -0
- package/src/runtime/stats-client/index.ts +65 -0
- package/src/runtime/stats-sample.ts +95 -0
- package/src/safe-json.ts +37 -0
- package/src/sandbox.ts +1209 -0
- package/src/schema/build-apps.ts +38 -0
- package/src/schema/home.ts +217 -0
- package/src/schema/host.ts +792 -0
- package/src/schema/inputs.ts +26 -0
- package/src/schema/nodes.ts +75 -0
- package/src/schema/peer.ts +400 -0
- package/src/schema/public-domain.ts +116 -0
- package/src/schema/remote.ts +74 -0
- package/src/security.ts +594 -0
- package/src/serialize.ts +21 -0
- package/src/service-definition.ts +284 -0
- package/src/service-url.ts +64 -0
- package/src/service.ts +39 -0
- package/src/services/index.ts +5 -0
- package/src/shared.ts +65 -0
- package/src/speculative-silo.ts +96 -0
- package/src/stable-secret.ts +14 -0
- package/src/surfaces.ts +264 -0
- package/src/tailwind-bin.ts +56 -0
- package/src/test/boot.ts +192 -0
- package/src/test/compile.ts +501 -0
- package/src/test/describe.ts +117 -0
- package/src/test/index.ts +11 -0
- package/src/test/ingress-fixture.ts +37 -0
- package/src/test/types.ts +318 -0
- package/src/trusted-headers.ts +79 -0
- package/src/types.ts +126 -0
- package/src/ui/components/aside.tsx +96 -0
- package/src/ui/components/badge.tsx +42 -0
- package/src/ui/components/button.tsx +59 -0
- package/src/ui/components/card.tsx +106 -0
- package/src/ui/components/checkbox.tsx +37 -0
- package/src/ui/components/code-block.tsx +29 -0
- package/src/ui/components/combobox.tsx +119 -0
- package/src/ui/components/config-field.tsx +284 -0
- package/src/ui/components/dialog.tsx +137 -0
- package/src/ui/components/domain-editor.tsx +317 -0
- package/src/ui/components/domain-list.ts +205 -0
- package/src/ui/components/dropdown-menu.tsx +73 -0
- package/src/ui/components/empty-state.tsx +51 -0
- package/src/ui/components/highlight.ts +59 -0
- package/src/ui/components/icons.tsx +101 -0
- package/src/ui/components/image-posture.tsx +85 -0
- package/src/ui/components/input.tsx +28 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/line-chart.tsx +113 -0
- package/src/ui/components/page.tsx +39 -0
- package/src/ui/components/permission-row.tsx +262 -0
- package/src/ui/components/permission-section.tsx +51 -0
- package/src/ui/components/pie-chart.tsx +245 -0
- package/src/ui/components/schema-block.tsx +60 -0
- package/src/ui/components/separator.tsx +42 -0
- package/src/ui/components/sheet.tsx +130 -0
- package/src/ui/components/sidebar.tsx +650 -0
- package/src/ui/components/silo-picker.tsx +510 -0
- package/src/ui/components/skeleton.tsx +13 -0
- package/src/ui/components/sonner.tsx +33 -0
- package/src/ui/components/switch.tsx +58 -0
- package/src/ui/components/table-view.ts +219 -0
- package/src/ui/components/table.tsx +506 -0
- package/src/ui/components/tabs.tsx +59 -0
- package/src/ui/components/timeago.tsx +84 -0
- package/src/ui/components/tooltip.tsx +56 -0
- package/src/ui/components.json +21 -0
- package/src/ui/dev/dev-shell.tsx +145 -0
- package/src/ui/home/home-grid.tsx +177 -0
- package/src/ui/hooks/use-mobile.ts +19 -0
- package/src/ui/index.ts +189 -0
- package/src/ui/install/adapter.ts +193 -0
- package/src/ui/install/deploy-targets-picker.tsx +185 -0
- package/src/ui/install/install-app-panel.tsx +912 -0
- package/src/ui/install/install-error.tsx +62 -0
- package/src/ui/install/perms-inactive.ts +50 -0
- package/src/ui/install/required-fields.ts +37 -0
- package/src/ui/install/tabs-form.tsx +557 -0
- package/src/ui/tokens.css +279 -0
- package/src/ui/utils.ts +78 -0
- package/src/ui-error.ts +139 -0
- package/src/ui-runtime/dev-landing-entry.tsx +258 -0
- package/src/ui-runtime/index.ts +83 -0
- package/src/ui-runtime/install-entry.tsx +119 -0
- package/src/ui-runtime/mount.ts +367 -0
- package/src/ui-runtime/runtime.css +56 -0
- package/src/ui-surface.ts +451 -0
- package/src/valkey.ts +43 -0
- package/src/worker/extract-tar.ts +125 -0
- package/src/worker/identity.ts +183 -0
- package/src/worker/index.ts +16 -0
- package/src/worker/public-ip.ts +92 -0
- package/src/worker/queue.ts +54 -0
- package/src/worker/reconcile.ts +127 -0
- package/src/worker/start-worker.ts +227 -0
- package/src/worker/ui-proxy.ts +335 -0
- package/src/worker/uplink.ts +523 -0
- package/src/worker/validate.ts +34 -0
- package/src/worker/verify-relay.ts +102 -0
- package/src/worker-protocol/app-dirs.ts +94 -0
- package/src/worker-protocol/home-proxy-paths.ts +76 -0
- package/src/worker-protocol/index.ts +55 -0
- package/src/worker-protocol/ingress.ts +166 -0
- package/src/worker-protocol/logs-socket-path.ts +50 -0
- package/src/worker-protocol/networks.ts +39 -0
- package/src/worker-protocol/node-version.ts +70 -0
- package/src/worker-protocol/silo-paths.ts +114 -0
- package/src/worker-protocol/spawn-paths.ts +165 -0
- package/src/worker-protocol/stats-socket-path.ts +40 -0
- package/src/worker-protocol/update-files.ts +77 -0
- package/src/worker-protocol/uplink/constants.ts +86 -0
- package/src/worker-protocol/uplink/errors.ts +61 -0
- package/src/worker-protocol/uplink/frame.ts +114 -0
- package/src/worker-protocol/uplink/identity.ts +40 -0
- package/src/worker-protocol/uplink/index.ts +63 -0
- package/src/worker-protocol/uplink/methods.ts +462 -0
- package/src/worker-protocol/uplink/mux.ts +646 -0
- package/src/worker-protocol/uplink/protocol.ts +11 -0
- package/src/worker-protocol/uplink/rpc.ts +309 -0
- package/src/worker-protocol/uplink/session.ts +540 -0
- package/src/worker-protocol/uplink/test-support.ts +188 -0
- package/src/worker-protocol/worker-mount.ts +27 -0
- package/src/worker-protocol/worker-state.ts +190 -0
- package/src/xml.ts +49 -0
- package/src/yaml.ts +15 -0
|
@@ -0,0 +1,1430 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MANAGEMENT_NET,
|
|
3
|
+
REALM_LOGS_MOUNT,
|
|
4
|
+
REALM_STATS_MOUNT,
|
|
5
|
+
SPAWN_PLACEHOLDER,
|
|
6
|
+
homeProxyMount,
|
|
7
|
+
siloNetwork,
|
|
8
|
+
spawnMounts,
|
|
9
|
+
speculativeSilo
|
|
10
|
+
} from "./chunk-VHTUSZUX.js";
|
|
11
|
+
import {
|
|
12
|
+
__export
|
|
13
|
+
} from "./chunk-MLKGABMK.js";
|
|
14
|
+
|
|
15
|
+
// src/schema/host.ts
|
|
16
|
+
var host_exports = {};
|
|
17
|
+
__export(host_exports, {
|
|
18
|
+
HOST_DIRECTORY_BLOCKLIST: () => HOST_DIRECTORY_BLOCKLIST,
|
|
19
|
+
egress: () => egress,
|
|
20
|
+
gpu: () => gpu,
|
|
21
|
+
iptables: () => iptables,
|
|
22
|
+
isValidHostDirectoryPath: () => isValidHostDirectoryPath,
|
|
23
|
+
logs: () => logs,
|
|
24
|
+
management: () => management,
|
|
25
|
+
normalizeHostPath: () => normalizeHostPath,
|
|
26
|
+
pathsOverlap: () => pathsOverlap,
|
|
27
|
+
port: () => port,
|
|
28
|
+
rocketLaunch: () => rocketLaunch,
|
|
29
|
+
silo: () => silo,
|
|
30
|
+
siloRequiresUnmetMessage: () => siloRequiresUnmetMessage,
|
|
31
|
+
spawn: () => spawn,
|
|
32
|
+
stats: () => stats
|
|
33
|
+
});
|
|
34
|
+
import { z } from "zod";
|
|
35
|
+
|
|
36
|
+
// src/factory.ts
|
|
37
|
+
function defineMethod(target, name, fn) {
|
|
38
|
+
Object.defineProperty(target, name, {
|
|
39
|
+
value: fn,
|
|
40
|
+
enumerable: false,
|
|
41
|
+
writable: false,
|
|
42
|
+
configurable: true
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function cloneAnd(source, patch) {
|
|
46
|
+
const c = Object.create(
|
|
47
|
+
Object.getPrototypeOf(source),
|
|
48
|
+
Object.getOwnPropertyDescriptors(source)
|
|
49
|
+
);
|
|
50
|
+
Object.assign(c, patch);
|
|
51
|
+
return c;
|
|
52
|
+
}
|
|
53
|
+
var makePermRecord = ((kind, schema) => {
|
|
54
|
+
return (input) => ({
|
|
55
|
+
kind,
|
|
56
|
+
data: schema.parse(input),
|
|
57
|
+
meta: {}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
var makeInputRecord = (kind, schema) => {
|
|
61
|
+
return () => ({
|
|
62
|
+
kind,
|
|
63
|
+
schema,
|
|
64
|
+
meta: {}
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
function makeOptional(inner, ..._gate) {
|
|
68
|
+
return ((kind, schema) => {
|
|
69
|
+
const factory = inner(kind, schema);
|
|
70
|
+
return ((...args) => {
|
|
71
|
+
const innerRecord = factory(...args);
|
|
72
|
+
const seeded = cloneAnd(innerRecord, {
|
|
73
|
+
meta: { ...innerRecord.meta, isOptional: false }
|
|
74
|
+
});
|
|
75
|
+
defineMethod(seeded, "optional", function() {
|
|
76
|
+
return cloneAnd(this, { meta: { ...this.meta, isOptional: true } });
|
|
77
|
+
});
|
|
78
|
+
return seeded;
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function makeDescribable(inner, ..._gate) {
|
|
83
|
+
return ((kind, schema) => {
|
|
84
|
+
const factory = inner(kind, schema);
|
|
85
|
+
return ((...args) => {
|
|
86
|
+
const record = factory(...args);
|
|
87
|
+
defineMethod(record, "describe", function(text) {
|
|
88
|
+
return cloneAnd(this, { meta: { ...this.meta, description: text } });
|
|
89
|
+
});
|
|
90
|
+
return record;
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function makeReasonable(inner, ..._gate) {
|
|
95
|
+
return ((kind, schema) => {
|
|
96
|
+
const factory = inner(kind, schema);
|
|
97
|
+
return ((input) => {
|
|
98
|
+
const record = factory(input);
|
|
99
|
+
defineMethod(record, "reason", function(text) {
|
|
100
|
+
return cloneAnd(this, { meta: { ...this.meta, reason: text } });
|
|
101
|
+
});
|
|
102
|
+
return record;
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function makeSchemable(inner, ..._gate) {
|
|
107
|
+
return ((kind, schema) => {
|
|
108
|
+
const factory = inner(kind, schema);
|
|
109
|
+
return ((input) => {
|
|
110
|
+
const record = factory(input);
|
|
111
|
+
defineMethod(record, "schema", function(s) {
|
|
112
|
+
const cloned = cloneAnd(this, {});
|
|
113
|
+
Object.defineProperty(cloned, "responseSchema", {
|
|
114
|
+
value: s,
|
|
115
|
+
enumerable: false,
|
|
116
|
+
writable: false,
|
|
117
|
+
configurable: true
|
|
118
|
+
});
|
|
119
|
+
return cloned;
|
|
120
|
+
});
|
|
121
|
+
return record;
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function makeTitleable(inner, ..._gate) {
|
|
126
|
+
return ((kind, schema) => {
|
|
127
|
+
const factory = inner(kind, schema);
|
|
128
|
+
return (() => {
|
|
129
|
+
const record = factory();
|
|
130
|
+
defineMethod(record, "title", function(text) {
|
|
131
|
+
return cloneAnd(this, { meta: { ...this.meta, title: text } });
|
|
132
|
+
});
|
|
133
|
+
return record;
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function makePrefillable(inner, ..._gate) {
|
|
138
|
+
return ((kind, schema) => {
|
|
139
|
+
const factory = inner(kind, schema);
|
|
140
|
+
return (() => {
|
|
141
|
+
const record = factory();
|
|
142
|
+
defineMethod(record, "prefill", function(value) {
|
|
143
|
+
return cloneAnd(this, { meta: { ...this.meta, prefill: value } });
|
|
144
|
+
});
|
|
145
|
+
return record;
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function makePlaceholdable(inner, ..._gate) {
|
|
150
|
+
return ((kind, schema) => {
|
|
151
|
+
const factory = inner(kind, schema);
|
|
152
|
+
return (() => {
|
|
153
|
+
const record = factory();
|
|
154
|
+
defineMethod(record, "placeholder", function(text) {
|
|
155
|
+
return cloneAnd(this, { meta: { ...this.meta, placeholder: text } });
|
|
156
|
+
});
|
|
157
|
+
return record;
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function makeNumericConstraints(inner) {
|
|
162
|
+
return ((kind, schema) => {
|
|
163
|
+
const factory = inner(kind, schema);
|
|
164
|
+
return (() => {
|
|
165
|
+
const record = factory();
|
|
166
|
+
defineMethod(record, "int", function() {
|
|
167
|
+
return cloneAnd(this, {
|
|
168
|
+
meta: { ...this.meta, int: true },
|
|
169
|
+
schema: this.schema.int()
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
defineMethod(record, "min", function(value) {
|
|
173
|
+
return cloneAnd(this, {
|
|
174
|
+
meta: { ...this.meta, min: value },
|
|
175
|
+
schema: this.schema.min(value)
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
defineMethod(record, "max", function(value) {
|
|
179
|
+
return cloneAnd(this, {
|
|
180
|
+
meta: { ...this.meta, max: value },
|
|
181
|
+
schema: this.schema.max(value)
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
return record;
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
var basePermission = makeOptional(makeDescribable(makeReasonable(makePermRecord)));
|
|
189
|
+
var schemablePermission = makeSchemable(basePermission);
|
|
190
|
+
var baseInput = makeOptional(
|
|
191
|
+
makeDescribable(makeTitleable(makePrefillable(makePlaceholdable(makeInputRecord))))
|
|
192
|
+
);
|
|
193
|
+
var numberInput = makeNumericConstraints(baseInput);
|
|
194
|
+
|
|
195
|
+
// src/permission-meta.ts
|
|
196
|
+
function scopeAllows(scope, appKind) {
|
|
197
|
+
return scope === "both" || scope === appKind;
|
|
198
|
+
}
|
|
199
|
+
var PERMISSION_META_TAG = /* @__PURE__ */ Symbol("realm.permission.meta");
|
|
200
|
+
function tagPermissionMeta(target, kind, meta) {
|
|
201
|
+
Object.defineProperty(target, PERMISSION_META_TAG, {
|
|
202
|
+
value: { kind, meta },
|
|
203
|
+
enumerable: false
|
|
204
|
+
});
|
|
205
|
+
return target;
|
|
206
|
+
}
|
|
207
|
+
function isMetaTagged(value) {
|
|
208
|
+
return (typeof value === "function" || typeof value === "object" && value !== null) && PERMISSION_META_TAG in value;
|
|
209
|
+
}
|
|
210
|
+
function derivePermissionMeta(...modules) {
|
|
211
|
+
const out = {};
|
|
212
|
+
for (const mod of modules) {
|
|
213
|
+
for (const value of Object.values(mod)) {
|
|
214
|
+
if (isMetaTagged(value)) {
|
|
215
|
+
const tag = value[PERMISSION_META_TAG];
|
|
216
|
+
out[tag.kind] = tag.meta;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/host-registry.ts
|
|
224
|
+
var HOST_KIND = /* @__PURE__ */ Symbol("realm.host.kind");
|
|
225
|
+
var HOST_HOOKS = /* @__PURE__ */ Symbol("realm.host.hooks");
|
|
226
|
+
function isHostable(value) {
|
|
227
|
+
return (typeof value === "function" || typeof value === "object" && value !== null) && HOST_KIND in value && HOST_HOOKS in value;
|
|
228
|
+
}
|
|
229
|
+
function tagHostable(target, kind, hooks, meta) {
|
|
230
|
+
Object.defineProperty(target, HOST_KIND, { value: kind, enumerable: false });
|
|
231
|
+
Object.defineProperty(target, HOST_HOOKS, { value: hooks, enumerable: false });
|
|
232
|
+
return tagPermissionMeta(target, kind, meta);
|
|
233
|
+
}
|
|
234
|
+
function makeHostable(inner) {
|
|
235
|
+
return (kind, schema, hooks, meta) => {
|
|
236
|
+
const factory = inner(kind, schema);
|
|
237
|
+
const enriched = {
|
|
238
|
+
...hooks,
|
|
239
|
+
grantSchema: hooks.grantSchema ?? schema
|
|
240
|
+
};
|
|
241
|
+
return tagHostable(factory, kind, enriched, meta);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function deriveHostRegistry(mod) {
|
|
245
|
+
const reg = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const value of Object.values(mod)) {
|
|
247
|
+
if (isHostable(value)) reg.set(value[HOST_KIND], value[HOST_HOOKS]);
|
|
248
|
+
}
|
|
249
|
+
return reg;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/errors.ts
|
|
253
|
+
var RealmCompileError = class extends Error {
|
|
254
|
+
issues;
|
|
255
|
+
constructor(input) {
|
|
256
|
+
const list = Array.isArray(input) ? input : [input];
|
|
257
|
+
super(formatHeader(list));
|
|
258
|
+
this.issues = Object.freeze([...list]);
|
|
259
|
+
this.name = "RealmCompileError";
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var SiloRequiresError = class extends RealmCompileError {
|
|
263
|
+
};
|
|
264
|
+
function formatHeader(issues) {
|
|
265
|
+
if (issues.length === 1) return formatIssue(issues[0]);
|
|
266
|
+
return `${issues.length} compile errors`;
|
|
267
|
+
}
|
|
268
|
+
function formatIssue(issue) {
|
|
269
|
+
switch (issue.kind) {
|
|
270
|
+
case "eval":
|
|
271
|
+
return issue.message;
|
|
272
|
+
case "validation":
|
|
273
|
+
return `${issue.path.join(".")}: ${issue.message}`;
|
|
274
|
+
case "permission":
|
|
275
|
+
return `missing grant: ${issue.missing}`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/schema/host.ts
|
|
280
|
+
var hostable = makeHostable(basePermission);
|
|
281
|
+
var MANAGEMENT_PLACEHOLDER = {
|
|
282
|
+
network: MANAGEMENT_NET,
|
|
283
|
+
baseDomain: "",
|
|
284
|
+
homeSlug: "",
|
|
285
|
+
workerId: "",
|
|
286
|
+
acmeDns: null
|
|
287
|
+
};
|
|
288
|
+
var HOST_DIRECTORY_BLOCKLIST = [
|
|
289
|
+
"/",
|
|
290
|
+
"/proc",
|
|
291
|
+
"/sys",
|
|
292
|
+
"/dev",
|
|
293
|
+
"/run",
|
|
294
|
+
"/var/run",
|
|
295
|
+
"/var/lib/docker",
|
|
296
|
+
"/etc",
|
|
297
|
+
"/root",
|
|
298
|
+
"/home",
|
|
299
|
+
"/opt/realm",
|
|
300
|
+
"/boot"
|
|
301
|
+
];
|
|
302
|
+
function normalizeHostPath(raw) {
|
|
303
|
+
if (!raw.startsWith("/")) return null;
|
|
304
|
+
const segments = [];
|
|
305
|
+
for (const segment of raw.split("/")) {
|
|
306
|
+
if (segment === "" || segment === ".") continue;
|
|
307
|
+
if (segment === "..") return null;
|
|
308
|
+
segments.push(segment);
|
|
309
|
+
}
|
|
310
|
+
return `/${segments.join("/")}`;
|
|
311
|
+
}
|
|
312
|
+
function pathsOverlap(a, b) {
|
|
313
|
+
if (a === b) return true;
|
|
314
|
+
const contains = (parent, child) => child.startsWith(parent === "/" ? "/" : `${parent}/`);
|
|
315
|
+
return contains(a, b) || contains(b, a);
|
|
316
|
+
}
|
|
317
|
+
function isValidHostDirectoryPath(raw) {
|
|
318
|
+
if (typeof raw !== "string") return false;
|
|
319
|
+
const normalized = normalizeHostPath(raw);
|
|
320
|
+
if (normalized === null) return false;
|
|
321
|
+
for (const blocked of HOST_DIRECTORY_BLOCKLIST) {
|
|
322
|
+
if (normalized === blocked) return false;
|
|
323
|
+
if (blocked === "/") continue;
|
|
324
|
+
if (normalized.startsWith(blocked + "/")) return false;
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
var gpu = hostable(
|
|
329
|
+
"host.gpu",
|
|
330
|
+
z.literal(true).default(true),
|
|
331
|
+
{
|
|
332
|
+
iterateUses: (svc) => svc.gpu === true ? [true] : [],
|
|
333
|
+
toValue: () => true
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "GPU passthrough",
|
|
337
|
+
phrase: "GPU passthrough (/dev/dri)",
|
|
338
|
+
helpText: "This app gets access to your graphics card.",
|
|
339
|
+
icon: "GraphicsCardIcon",
|
|
340
|
+
appKinds: "both"
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
var port = hostable(
|
|
344
|
+
"host.port",
|
|
345
|
+
z.number().int().min(1).max(65535),
|
|
346
|
+
{
|
|
347
|
+
iterateUses: (svc) => (svc.hostPorts ?? []).map((hp) => hp.hostPort),
|
|
348
|
+
toValue: (data) => ({ port: data }),
|
|
349
|
+
// Admin-grant input arrives as a raw string from the form (we don't
|
|
350
|
+
// coerce in the UI so an empty input stays empty). Coerce here, then
|
|
351
|
+
// re-validate the int range.
|
|
352
|
+
grantSchema: z.coerce.number().int().min(1).max(65535)
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "Port",
|
|
356
|
+
phrase: "bind a host port",
|
|
357
|
+
helpText: "This port will be reachable from the internet. You can change the port number before approving.",
|
|
358
|
+
icon: "PlugIcon",
|
|
359
|
+
appKinds: "both"
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
function siloRequiresUnmetMessage(siloName, unmet) {
|
|
363
|
+
const parts = unmet.map(
|
|
364
|
+
(u) => u.offeredPort === void 0 ? `doesn't offer "${u.name}" on port ${u.port}` : `offers "${u.name}" on ${u.offeredPort}, not ${u.port}`
|
|
365
|
+
);
|
|
366
|
+
return `Silo "${siloName}" ${parts.join("; ")}`;
|
|
367
|
+
}
|
|
368
|
+
var SILO_PLACEHOLDER = {};
|
|
369
|
+
function resolveSilo({ grant, declared, path, ctx }) {
|
|
370
|
+
const fail = (message) => {
|
|
371
|
+
throw new RealmCompileError({ kind: "validation", path: path.split("."), message });
|
|
372
|
+
};
|
|
373
|
+
if (typeof grant !== "string") {
|
|
374
|
+
if (ctx.speculative) return speculativeSilo(declared);
|
|
375
|
+
fail("no Silo was picked for this permission");
|
|
376
|
+
}
|
|
377
|
+
const silo2 = ctx.silos.find((s) => s.id === grant);
|
|
378
|
+
if (!silo2) fail(`Silo "${grant}" no longer exists`);
|
|
379
|
+
if (ctx.worker.internalId !== void 0 && silo2.workerId !== ctx.worker.internalId) {
|
|
380
|
+
fail(`Silo "${silo2.name}" is on worker ${silo2.workerSlug}, so this app must run there too`);
|
|
381
|
+
}
|
|
382
|
+
const asked = declared;
|
|
383
|
+
const resolved = {};
|
|
384
|
+
if (asked.network !== void 0) {
|
|
385
|
+
resolved.network = siloNetwork(silo2.id);
|
|
386
|
+
if (asked.network !== true) {
|
|
387
|
+
const offered = ctx.siloDirectory(silo2.id);
|
|
388
|
+
const unmet = [];
|
|
389
|
+
for (const [name, port2] of Object.entries(asked.network)) {
|
|
390
|
+
const entry = offered.get(name);
|
|
391
|
+
if (entry === void 0) unmet.push({ name, port: port2 });
|
|
392
|
+
else if (entry.port !== port2) unmet.push({ name, port: port2, offeredPort: entry.port });
|
|
393
|
+
}
|
|
394
|
+
if (unmet.length > 0) {
|
|
395
|
+
throw new SiloRequiresError({
|
|
396
|
+
kind: "validation",
|
|
397
|
+
path: path.split("."),
|
|
398
|
+
message: siloRequiresUnmetMessage(silo2.name, unmet)
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
const services = {};
|
|
402
|
+
for (const [name, port2] of Object.entries(asked.network)) {
|
|
403
|
+
services[name] = { host: name, port: port2 };
|
|
404
|
+
}
|
|
405
|
+
resolved.services = services;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (asked.volume !== void 0) {
|
|
409
|
+
resolved.volume = {
|
|
410
|
+
hostPath: silo2.hostPath,
|
|
411
|
+
containerPath: silo2.containerPath,
|
|
412
|
+
gid: silo2.gid,
|
|
413
|
+
// The author declared the mode and the operator approved it; a member
|
|
414
|
+
// that only asked to read gets a mount it cannot write through.
|
|
415
|
+
readOnly: asked.volume === "read",
|
|
416
|
+
backup: false
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (asked.apiKey !== void 0) {
|
|
420
|
+
resolved.apiKey = ctx.siloApiKeys.get(silo2.id);
|
|
421
|
+
}
|
|
422
|
+
return resolved;
|
|
423
|
+
}
|
|
424
|
+
var siloRequiresSchema = z.record(z.string(), z.number().int().min(1).max(65535));
|
|
425
|
+
var siloGrantsSchema = z.object({
|
|
426
|
+
network: z.union([z.literal(true), siloRequiresSchema]).optional(),
|
|
427
|
+
volume: z.enum(["read", "readwrite"]).optional(),
|
|
428
|
+
apiKey: z.literal(true).optional()
|
|
429
|
+
}).refine((g) => g.network !== void 0 || g.volume !== void 0 || g.apiKey !== void 0, {
|
|
430
|
+
message: "silo() needs at least one of network, volume, apiKey"
|
|
431
|
+
});
|
|
432
|
+
var siloFactory = hostable(
|
|
433
|
+
"host.silo",
|
|
434
|
+
siloGrantsSchema,
|
|
435
|
+
{
|
|
436
|
+
// The elevated use is the communal volume: a mount carrying a gid reads
|
|
437
|
+
// (or writes) another trust domain's bytes and joins its group. All three
|
|
438
|
+
// of hostPath, gid and writability have to match a Silo the operator bound
|
|
439
|
+
// this app to - a hand-written gid, a Silo path without the join, or a
|
|
440
|
+
// writable mount from an app that only asked to read, all fail the gate.
|
|
441
|
+
// Network membership and the apiKey have no per-service use; they fall out
|
|
442
|
+
// of the grant being in place.
|
|
443
|
+
iterateUses: function* (svc) {
|
|
444
|
+
for (const v of svc.volumes ?? []) {
|
|
445
|
+
if (v.gid !== void 0) {
|
|
446
|
+
yield { hostPath: v.hostPath, gid: v.gid, readOnly: v.readOnly === true };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
// Type-level only. The grant carries a Silo id, which means nothing
|
|
451
|
+
// without the Silo record, so every real value comes from `resolve`
|
|
452
|
+
// below; this exists so `ValueOf` can read the resolved shape.
|
|
453
|
+
toValue: () => SILO_PLACEHOLDER,
|
|
454
|
+
resolve: resolveSilo,
|
|
455
|
+
// The author declares which affordances they want; the grant carries
|
|
456
|
+
// which Silo the operator bound (its id).
|
|
457
|
+
grantSchema: z.string().min(1)
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: "Silo",
|
|
461
|
+
phrase: "join a Silo",
|
|
462
|
+
helpText: "This app joins a Silo you pick - an operator-owned network, shared directory, and shared credential. Everything in a Silo can see everything else in it, so only put apps together that you mean to share.",
|
|
463
|
+
icon: "StackIcon",
|
|
464
|
+
// Single-only: a Silo is a directory on one box, so the grant means
|
|
465
|
+
// nothing spread across a fleet.
|
|
466
|
+
appKinds: "single"
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
var silo = siloFactory;
|
|
470
|
+
var management = hostable(
|
|
471
|
+
"host.management",
|
|
472
|
+
z.literal(true).default(true),
|
|
473
|
+
{
|
|
474
|
+
// The "use" of management is implicit — joining the network and
|
|
475
|
+
// reading the URL pieces falls out of the grant being in place;
|
|
476
|
+
// nothing in the resolved service spec distinguishes a user from a
|
|
477
|
+
// non-user.
|
|
478
|
+
iterateUses: () => [],
|
|
479
|
+
// Type-level only. The network is the SDK-side constant, but every other
|
|
480
|
+
// field is Home's, so the real value comes from `resolve`.
|
|
481
|
+
toValue: () => MANAGEMENT_PLACEHOLDER,
|
|
482
|
+
resolve: ({ ctx }) => ({
|
|
483
|
+
network: MANAGEMENT_NET,
|
|
484
|
+
baseDomain: ctx.home.baseDomain,
|
|
485
|
+
homeSlug: ctx.home.homeSlug,
|
|
486
|
+
workerId: ctx.worker.id,
|
|
487
|
+
// acme-dns username is the domain slug = the first label of baseDomain.
|
|
488
|
+
acmeDns: ctx.home.acmeDns ? {
|
|
489
|
+
serverUrl: ctx.home.acmeDns.serverUrl,
|
|
490
|
+
username: ctx.home.baseDomain.split(".")[0] ?? "",
|
|
491
|
+
apiKey: ctx.home.acmeDns.apiKey,
|
|
492
|
+
ca: ctx.home.acmeDns.ca,
|
|
493
|
+
caRootPem: ctx.home.acmeDns.caRootPem
|
|
494
|
+
} : null
|
|
495
|
+
})
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "Management bridge + Home identity",
|
|
499
|
+
phrase: "join the management bridge network and read Home identity",
|
|
500
|
+
helpText: "This grants access to the management bridge network and the Home's dashboard URL pieces.",
|
|
501
|
+
icon: "ShareNetworkIcon",
|
|
502
|
+
danger: true,
|
|
503
|
+
appKinds: "both"
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
var iptables = hostable(
|
|
507
|
+
"host.iptables",
|
|
508
|
+
z.literal(true).default(true),
|
|
509
|
+
{
|
|
510
|
+
iterateUses: (svc) => svc.iptables === true ? [true] : [],
|
|
511
|
+
toValue: () => true
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "Host iptables",
|
|
515
|
+
phrase: "install host iptables and ip6tables rules",
|
|
516
|
+
helpText: "This app installs firewall rules on the host. Required for the firewall app to lock down which ports are reachable.",
|
|
517
|
+
icon: "ShieldIcon",
|
|
518
|
+
danger: true,
|
|
519
|
+
appKinds: "both"
|
|
520
|
+
}
|
|
521
|
+
);
|
|
522
|
+
var egress = hostable(
|
|
523
|
+
"host.egress",
|
|
524
|
+
z.literal(true).default(true),
|
|
525
|
+
{
|
|
526
|
+
iterateUses: () => [],
|
|
527
|
+
toValue: () => true
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: "Internet access",
|
|
531
|
+
phrase: "outbound internet access",
|
|
532
|
+
helpText: "Without this, the app can only communicate with other apps on your server. Granting this allows it to make outbound connections to the internet.",
|
|
533
|
+
icon: "GlobeIcon",
|
|
534
|
+
appKinds: "both"
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
var rocketLaunch = hostable(
|
|
538
|
+
"host.rocketLaunch",
|
|
539
|
+
z.literal(true).default(true),
|
|
540
|
+
{
|
|
541
|
+
iterateUses: () => [],
|
|
542
|
+
toValue: () => true
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "Rocket launch",
|
|
546
|
+
phrase: "launch a rocket",
|
|
547
|
+
helpText: "Permission with no worker that can satisfy it. Used to exercise the unsupported-permission UX.",
|
|
548
|
+
icon: "RocketLaunchIcon",
|
|
549
|
+
appKinds: "both"
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
var logs = hostable(
|
|
553
|
+
"host.logs",
|
|
554
|
+
z.literal(true).default(true),
|
|
555
|
+
{
|
|
556
|
+
// The "use" of host.logs is implicit — mounting the socket dir falls out
|
|
557
|
+
// of the grant being in place; the resolved hostPath is exempted from the
|
|
558
|
+
// absolute-mount check via `collectAllowed` in the Home eval graph. That
|
|
559
|
+
// exemption carries the read-only requirement with it, so an app that
|
|
560
|
+
// mounts the shared dir writable is rejected there.
|
|
561
|
+
iterateUses: () => [],
|
|
562
|
+
toValue: () => REALM_LOGS_MOUNT
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
name: "Container logs",
|
|
566
|
+
phrase: "stream container logs from every app",
|
|
567
|
+
helpText: "This app receives the stdout and stderr of every container running on this Worker. Useful for log forwarders and audit collectors.",
|
|
568
|
+
icon: "TerminalWindowIcon",
|
|
569
|
+
danger: true,
|
|
570
|
+
appKinds: "both"
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
var stats = hostable(
|
|
574
|
+
"host.stats",
|
|
575
|
+
z.literal(true).default(true),
|
|
576
|
+
{
|
|
577
|
+
// The "use" of host.stats is implicit — mounting the socket dir falls out
|
|
578
|
+
// of the grant being in place; the resolved hostPath is exempted from the
|
|
579
|
+
// absolute-mount check via `collectAllowed` in the Home eval graph. That
|
|
580
|
+
// exemption carries the read-only requirement with it, so an app that
|
|
581
|
+
// mounts the shared dir writable is rejected there.
|
|
582
|
+
iterateUses: () => [],
|
|
583
|
+
toValue: () => REALM_STATS_MOUNT
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: "Container stats",
|
|
587
|
+
phrase: "read resource usage for every app's containers",
|
|
588
|
+
helpText: "This app can sample the resource usage of every container running on this Worker, plus the host's own load, memory, and disks. Read-only: it can see your fleet, but can't touch it.",
|
|
589
|
+
icon: "PulseIcon",
|
|
590
|
+
danger: true,
|
|
591
|
+
appKinds: "both"
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
var spawn = hostable(
|
|
595
|
+
"host.spawn",
|
|
596
|
+
z.literal(true).default(true),
|
|
597
|
+
{
|
|
598
|
+
// The "use" of host.spawn is implicit — mounting the socket/scratch/
|
|
599
|
+
// artifacts dirs falls out of the grant being in place; the resolved
|
|
600
|
+
// paths are exempted from the absolute-mount gate via `collectAllowed`
|
|
601
|
+
// in the Home eval graph.
|
|
602
|
+
iterateUses: () => [],
|
|
603
|
+
// Type-level only; every real value comes from `resolve`, since all three
|
|
604
|
+
// paths are per-app and the grant carries only presence.
|
|
605
|
+
toValue: () => SPAWN_PLACEHOLDER,
|
|
606
|
+
// `socket` is the per-app DIRECTORY the app mounts (connect to
|
|
607
|
+
// `spawn.sock` inside); the wire carries the socket file inside it (the
|
|
608
|
+
// path the worker binds). scratch/artifacts are dir mounts.
|
|
609
|
+
resolve: ({ ctx }) => spawnMounts(ctx.appId)
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: "Spawn disposable containers",
|
|
613
|
+
phrase: "spawn disposable hardened containers on the worker",
|
|
614
|
+
helpText: "This app can start one-shot, fully-hardened containers on your server (e.g. to run builds). The worker forces the hardening, network, and lifecycle; the app never gets the Docker socket.",
|
|
615
|
+
icon: "CubeIcon",
|
|
616
|
+
danger: true,
|
|
617
|
+
appKinds: "both"
|
|
618
|
+
}
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
// src/schema/home.ts
|
|
622
|
+
var home_exports = {};
|
|
623
|
+
__export(home_exports, {
|
|
624
|
+
apiAccess: () => apiAccess,
|
|
625
|
+
publicDomain: () => publicDomain
|
|
626
|
+
});
|
|
627
|
+
import { z as z3 } from "zod";
|
|
628
|
+
|
|
629
|
+
// src/home-registry.ts
|
|
630
|
+
var HOME_KIND = /* @__PURE__ */ Symbol("realm.home.kind");
|
|
631
|
+
var HOME_HOOKS = /* @__PURE__ */ Symbol("realm.home.hooks");
|
|
632
|
+
function isHomeable(value) {
|
|
633
|
+
return (typeof value === "function" || typeof value === "object" && value !== null) && HOME_KIND in value && HOME_HOOKS in value;
|
|
634
|
+
}
|
|
635
|
+
function tagHomeable(target, kind, hooks, meta) {
|
|
636
|
+
Object.defineProperty(target, HOME_KIND, { value: kind, enumerable: false });
|
|
637
|
+
Object.defineProperty(target, HOME_HOOKS, { value: hooks, enumerable: false });
|
|
638
|
+
return tagPermissionMeta(target, kind, meta);
|
|
639
|
+
}
|
|
640
|
+
function makeHomeable(inner) {
|
|
641
|
+
return (kind, schema, hooks, meta) => {
|
|
642
|
+
const factory = inner(kind, schema);
|
|
643
|
+
const enriched = {
|
|
644
|
+
...hooks,
|
|
645
|
+
grantSchema: hooks.grantSchema ?? schema
|
|
646
|
+
};
|
|
647
|
+
return tagHomeable(factory, kind, enriched, meta);
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function deriveHomeRegistry(mod) {
|
|
651
|
+
const reg = /* @__PURE__ */ new Map();
|
|
652
|
+
for (const value of Object.values(mod)) {
|
|
653
|
+
if (isHomeable(value)) reg.set(value[HOME_KIND], value[HOME_HOOKS]);
|
|
654
|
+
}
|
|
655
|
+
return reg;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/schema/public-domain.ts
|
|
659
|
+
import { z as z2 } from "zod";
|
|
660
|
+
var LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
|
|
661
|
+
var HOSTNAME = new RegExp(`^${LABEL}(?:\\.${LABEL})+$`);
|
|
662
|
+
function normalizeHost(host) {
|
|
663
|
+
return host.trim().replace(/\.$/, "").toLowerCase();
|
|
664
|
+
}
|
|
665
|
+
var publicDomainHostSchema = z2.string().transform(normalizeHost).refine((h) => h.length <= 253 && HOSTNAME.test(h), {
|
|
666
|
+
message: "must be a hostname of at least two labels using letters, digits and hyphens (e.g. photos.example.com)"
|
|
667
|
+
});
|
|
668
|
+
function dedupeHosts(hosts) {
|
|
669
|
+
const seen = /* @__PURE__ */ new Set();
|
|
670
|
+
const out = [];
|
|
671
|
+
for (const host of hosts) {
|
|
672
|
+
if (seen.has(host)) continue;
|
|
673
|
+
seen.add(host);
|
|
674
|
+
out.push(host);
|
|
675
|
+
}
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
var publicDomainValueSchema = z2.object({
|
|
679
|
+
hosts: z2.array(publicDomainHostSchema).transform(dedupeHosts),
|
|
680
|
+
domainMode: z2.enum(["redirect", "all"])
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// src/schema/home.ts
|
|
684
|
+
var homeable = makeHomeable(basePermission);
|
|
685
|
+
var HOME_API_ACCESS_SCOPES = ["builder", "whoami"];
|
|
686
|
+
var HOME_API_PLACEHOLDER = {
|
|
687
|
+
socket: { ...homeProxyMount(""), hostPath: "" }
|
|
688
|
+
};
|
|
689
|
+
var apiAccess = homeable(
|
|
690
|
+
"home.apiAccess",
|
|
691
|
+
z3.array(z3.enum(HOME_API_ACCESS_SCOPES)).min(1),
|
|
692
|
+
{
|
|
693
|
+
// No elevated *use* to collect — mounting the socket falls out of the
|
|
694
|
+
// grant being in place; the resolved socket is exempted from the
|
|
695
|
+
// absolute-mount gate via `collectAllowed` in the Home eval graph.
|
|
696
|
+
iterateUses: () => [],
|
|
697
|
+
// Type-level only; every real value comes from `resolve`, since the
|
|
698
|
+
// socket is per-app and the grant carries only scopes.
|
|
699
|
+
toValue: () => HOME_API_PLACEHOLDER,
|
|
700
|
+
// The value the app mounts is the per-app DIRECTORY; the wire carries the
|
|
701
|
+
// socket file inside it (the path the worker binds).
|
|
702
|
+
resolve: ({ ctx }) => ({ socket: homeProxyMount(ctx.appId) })
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: "Home API access",
|
|
706
|
+
phrase: "call the Home control-plane API",
|
|
707
|
+
helpText: "This app can call your Home's control-plane API (over the worker) to use the capabilities you grant \u2014 e.g. pinning the images it builds. It holds no key; the worker proves which app it is.",
|
|
708
|
+
icon: "GearIcon",
|
|
709
|
+
danger: true,
|
|
710
|
+
// Single-only: a fleet-wide app holding a Home API socket on every
|
|
711
|
+
// box is a materially larger surface than one holding a single
|
|
712
|
+
// socket, and `builder` (the only scope with reach) is a singleton.
|
|
713
|
+
appKinds: "single"
|
|
714
|
+
}
|
|
715
|
+
);
|
|
716
|
+
var NO_PUBLIC_DOMAIN = { hosts: [], domainMode: "redirect" };
|
|
717
|
+
var publicDomain = homeable(
|
|
718
|
+
"home.publicDomain",
|
|
719
|
+
// Empty author data. `z.literal(true)` rather than `z.void()` leaves the
|
|
720
|
+
// slot for `publicDomain({ wildcard: true })` without changing any call
|
|
721
|
+
// site: the author declares cardinality, the operator only ever chooses
|
|
722
|
+
// which domain.
|
|
723
|
+
z3.literal(true).default(true),
|
|
724
|
+
{
|
|
725
|
+
// Unlike the rest of `home.*` this kind has a real per-service use: the
|
|
726
|
+
// hosts a router actually serves on. `checkAppGrants` matches them against
|
|
727
|
+
// the granted set, so a packed bundle can't hand the VM a router carrying
|
|
728
|
+
// a host the operator never approved. Routers with no hosts emit nothing —
|
|
729
|
+
// there is no elevated thing being done.
|
|
730
|
+
iterateUses: function* (svc) {
|
|
731
|
+
const value = svc.router?.publicDomain;
|
|
732
|
+
if (value && value.hosts.length > 0) yield value;
|
|
733
|
+
},
|
|
734
|
+
// The grant holds the whole answer (the operator's hosts + their redirect
|
|
735
|
+
// choice), so there is no `resolve` hook — branding is all that separates
|
|
736
|
+
// stored data from the resolved value.
|
|
737
|
+
//
|
|
738
|
+
// Total on purpose: the speculative-eval paths build grants straight off
|
|
739
|
+
// the descriptors with an empty `grantValues` map, and this kind has no
|
|
740
|
+
// `perm.data` to fall back to, so throwing on `undefined` would drop the
|
|
741
|
+
// image cards off the install screen of every app that declares it.
|
|
742
|
+
toValue: (data) => data == null ? NO_PUBLIC_DOMAIN : { hosts: data.hosts, domainMode: data.domainMode },
|
|
743
|
+
// Descriptor data and grant data diverge, the `host.silo` shape: the
|
|
744
|
+
// author declares nothing but intent, the operator supplies the hosts.
|
|
745
|
+
// The default is what an enabled-but-untouched toggle stores, and it is
|
|
746
|
+
// the "no route" value rather than an error, because adding the domain
|
|
747
|
+
// later is a normal install-then-configure flow.
|
|
748
|
+
grantSchema: publicDomainValueSchema.default({ hosts: [], domainMode: "redirect" })
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
name: "Public domain",
|
|
752
|
+
phrase: "serve this app on a domain you own",
|
|
753
|
+
helpText: "This app is served on a hostname you supply, outside your Realm domain and without Home's login in front of it. Anyone who can reach the name reaches the app, so only grant it to an app whose own authentication you trust.",
|
|
754
|
+
icon: "GlobeIcon",
|
|
755
|
+
// Single-only: a hostname points at one box. Fanning the same name
|
|
756
|
+
// across every worker would have them all claim it.
|
|
757
|
+
appKinds: "single"
|
|
758
|
+
}
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
// src/schema/peer.ts
|
|
762
|
+
var peer_exports = {};
|
|
763
|
+
__export(peer_exports, {
|
|
764
|
+
backupVolumes: () => backupVolumes,
|
|
765
|
+
buildApps: () => buildApps,
|
|
766
|
+
hostPorts: () => hostPorts,
|
|
767
|
+
isNetworkStrategy: () => isNetworkStrategy,
|
|
768
|
+
isVolumeStrategy: () => isVolumeStrategy,
|
|
769
|
+
routedApps: () => routedApps
|
|
770
|
+
});
|
|
771
|
+
import { z as z4 } from "zod";
|
|
772
|
+
|
|
773
|
+
// src/peer-registry.ts
|
|
774
|
+
var PEER_KIND = /* @__PURE__ */ Symbol("realm.peer.kind");
|
|
775
|
+
var PEER_HOOKS = /* @__PURE__ */ Symbol("realm.peer.hooks");
|
|
776
|
+
function isPeerable(value) {
|
|
777
|
+
return (typeof value === "function" || typeof value === "object" && value !== null) && PEER_KIND in value && PEER_HOOKS in value;
|
|
778
|
+
}
|
|
779
|
+
function tagPeerable(target, kind, hooks, meta) {
|
|
780
|
+
Object.defineProperty(target, PEER_KIND, { value: kind, enumerable: false });
|
|
781
|
+
Object.defineProperty(target, PEER_HOOKS, { value: hooks, enumerable: false });
|
|
782
|
+
return tagPermissionMeta(target, kind, meta);
|
|
783
|
+
}
|
|
784
|
+
function makePeerable(inner) {
|
|
785
|
+
return (kind, schema, hooks, meta) => {
|
|
786
|
+
const factory = inner(kind, schema);
|
|
787
|
+
return tagPeerable(factory, kind, hooks, meta);
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function derivePeerRegistry(mod) {
|
|
791
|
+
const reg = /* @__PURE__ */ new Map();
|
|
792
|
+
for (const value of Object.values(mod)) {
|
|
793
|
+
if (isPeerable(value)) reg.set(value[PEER_KIND], value[PEER_HOOKS]);
|
|
794
|
+
}
|
|
795
|
+
return reg;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/schema/peer.ts
|
|
799
|
+
var peerable = makePeerable(basePermission);
|
|
800
|
+
function isNetworkStrategy(s) {
|
|
801
|
+
return "network" in s;
|
|
802
|
+
}
|
|
803
|
+
function isVolumeStrategy(s) {
|
|
804
|
+
return !("network" in s);
|
|
805
|
+
}
|
|
806
|
+
function appNetwork(appId) {
|
|
807
|
+
return `realm_${appId}`;
|
|
808
|
+
}
|
|
809
|
+
function rewriteSelfDerivedToShared(value, providerAppId, serviceKey) {
|
|
810
|
+
if (value == null || typeof value !== "object") return value;
|
|
811
|
+
if (Array.isArray(value)) {
|
|
812
|
+
return value.map((v) => rewriteSelfDerivedToShared(v, providerAppId, serviceKey));
|
|
813
|
+
}
|
|
814
|
+
if ("__realmRef" in value) {
|
|
815
|
+
const inner = value.__realmRef;
|
|
816
|
+
return { __realmRef: rewriteMarker(inner, providerAppId, serviceKey) };
|
|
817
|
+
}
|
|
818
|
+
const out = {};
|
|
819
|
+
for (const [k, v] of Object.entries(value)) {
|
|
820
|
+
out[k] = rewriteSelfDerivedToShared(v, providerAppId, serviceKey);
|
|
821
|
+
}
|
|
822
|
+
return out;
|
|
823
|
+
}
|
|
824
|
+
function rewriteMarker(marker, providerAppId, serviceKey) {
|
|
825
|
+
if (marker == null || typeof marker !== "object") return marker;
|
|
826
|
+
const m = marker;
|
|
827
|
+
if (m.kind === "derived" && m.appId === "__self__" && typeof m.name === "string") {
|
|
828
|
+
return { kind: "shared", providerAppId, serviceKey, name: m.name };
|
|
829
|
+
}
|
|
830
|
+
if (m.kind === "concat" && Array.isArray(m.parts)) {
|
|
831
|
+
return {
|
|
832
|
+
kind: "concat",
|
|
833
|
+
parts: m.parts.map(
|
|
834
|
+
(p) => typeof p === "string" ? p : rewriteMarker(p, providerAppId, serviceKey)
|
|
835
|
+
)
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
return marker;
|
|
839
|
+
}
|
|
840
|
+
var routedApps = peerable(
|
|
841
|
+
"peers.routedApps",
|
|
842
|
+
z4.literal(true).default(true),
|
|
843
|
+
{
|
|
844
|
+
extract(appId, state) {
|
|
845
|
+
const services = [];
|
|
846
|
+
for (const [serviceId, svc] of Object.entries(state.services)) {
|
|
847
|
+
if (svc.router == null) continue;
|
|
848
|
+
const { publicDomain: publicDomain2, ...rest } = svc.router;
|
|
849
|
+
const hosts = publicDomain2?.hosts ?? [];
|
|
850
|
+
services.push({
|
|
851
|
+
serviceId,
|
|
852
|
+
router: {
|
|
853
|
+
...rest,
|
|
854
|
+
domainMode: publicDomain2?.domainMode ?? "redirect",
|
|
855
|
+
// Key omitted entirely when the operator granted no hosts, so a
|
|
856
|
+
// downstream truthiness check can't see an empty array as "has a
|
|
857
|
+
// public domain".
|
|
858
|
+
...hosts.length > 0 ? { publicDomain: [...hosts] } : {}
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
if (services.length === 0) return [];
|
|
863
|
+
const trustedHeaderKeys = state.trustedHeaders?.keys ?? [];
|
|
864
|
+
return [{ appId, network: appNetwork(appId), services, trustedHeaderKeys }];
|
|
865
|
+
},
|
|
866
|
+
resolve(entries) {
|
|
867
|
+
return [...entries].sort((a, b) => a.appId < b.appId ? -1 : a.appId > b.appId ? 1 : 0);
|
|
868
|
+
},
|
|
869
|
+
extractNetworks(entry) {
|
|
870
|
+
return entry.network;
|
|
871
|
+
},
|
|
872
|
+
producerPredicate(svc) {
|
|
873
|
+
return svc.router != null;
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
name: "Routed apps",
|
|
878
|
+
phrase: "join routed app networks",
|
|
879
|
+
helpText: "This app reads the list of all routed apps on this Worker and joins their networks to route traffic.",
|
|
880
|
+
icon: "ShareNetworkIcon",
|
|
881
|
+
danger: true,
|
|
882
|
+
// System-only: an aggregated view over every app on the worker.
|
|
883
|
+
// Widening later is additive where narrowing is not.
|
|
884
|
+
appKinds: "system"
|
|
885
|
+
}
|
|
886
|
+
);
|
|
887
|
+
var hostPorts = peerable(
|
|
888
|
+
"peers.hostPorts",
|
|
889
|
+
z4.literal(true).default(true),
|
|
890
|
+
{
|
|
891
|
+
extract(_appId, state) {
|
|
892
|
+
const out = [];
|
|
893
|
+
for (const svc of Object.values(state.services)) {
|
|
894
|
+
for (const hp of svc.hostPorts ?? []) out.push({ port: hp.hostPort });
|
|
895
|
+
}
|
|
896
|
+
return out;
|
|
897
|
+
},
|
|
898
|
+
resolve(entries) {
|
|
899
|
+
return entries;
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
name: "Host ports",
|
|
904
|
+
phrase: "read host port allocations",
|
|
905
|
+
helpText: "This app reads the list of all approved host ports across apps on this Worker.",
|
|
906
|
+
icon: "ShareNetworkIcon",
|
|
907
|
+
// System-only: same aggregated shape as routedApps.
|
|
908
|
+
appKinds: "system"
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
var backupVolumes = peerable(
|
|
912
|
+
"peers.backupVolumes",
|
|
913
|
+
z4.literal(true).default(true),
|
|
914
|
+
{
|
|
915
|
+
extract(appId, state) {
|
|
916
|
+
const out = [];
|
|
917
|
+
for (const [serviceKey, svc] of Object.entries(state.services)) {
|
|
918
|
+
for (const vol of svc.volumes ?? []) {
|
|
919
|
+
if (!vol.backup) continue;
|
|
920
|
+
const scopedPath = vol.hostPath.startsWith("./") ? `./apps/${appId}/${vol.hostPath.slice(2)}` : vol.hostPath;
|
|
921
|
+
const isNetwork = "url" in vol.backup;
|
|
922
|
+
const rewritten = rewriteSelfDerivedToShared(vol.backup, appId, serviceKey);
|
|
923
|
+
const strategy = isNetwork ? { ...rewritten, network: appNetwork(appId) } : rewritten;
|
|
924
|
+
out.push({
|
|
925
|
+
appId,
|
|
926
|
+
serviceKey,
|
|
927
|
+
hostPath: scopedPath,
|
|
928
|
+
strategy
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return out;
|
|
933
|
+
},
|
|
934
|
+
resolve(entries) {
|
|
935
|
+
return entries;
|
|
936
|
+
},
|
|
937
|
+
extractNetworks(entry) {
|
|
938
|
+
return isNetworkStrategy(entry.strategy) ? entry.strategy.network : void 0;
|
|
939
|
+
},
|
|
940
|
+
extractPeerVolumes(entry) {
|
|
941
|
+
return isNetworkStrategy(entry.strategy) ? void 0 : entry.hostPath;
|
|
942
|
+
},
|
|
943
|
+
extractAllowedProvider(entry) {
|
|
944
|
+
return { appId: entry.appId, serviceKey: entry.serviceKey };
|
|
945
|
+
},
|
|
946
|
+
producerPredicate(svc) {
|
|
947
|
+
return svc.volumes?.some((v) => !!v.backup) ?? false;
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
name: "Backup volumes",
|
|
952
|
+
phrase: "mount peer volumes for backup",
|
|
953
|
+
helpText: "This app reads the list of all volumes with backup strategies and mounts them for snapshotting.",
|
|
954
|
+
icon: "ShareNetworkIcon",
|
|
955
|
+
danger: true,
|
|
956
|
+
// System-only: same aggregated shape as routedApps.
|
|
957
|
+
appKinds: "system"
|
|
958
|
+
}
|
|
959
|
+
);
|
|
960
|
+
var buildApps = peerable(
|
|
961
|
+
"peers.buildApps",
|
|
962
|
+
z4.literal(true).default(true),
|
|
963
|
+
// `extract` + `resolve` and NOTHING else, deliberately. Home unions this
|
|
964
|
+
// kind's entries across workers (`isBuildAppEntry` in peer-world.ts is the
|
|
965
|
+
// filter), which is only sound while an entry is inert data anywhere in the
|
|
966
|
+
// realm. Each of the four hooks omitted here would turn one into something
|
|
967
|
+
// that exists on exactly one box, leaving a consumer on another worker with a
|
|
968
|
+
// dangling reference:
|
|
969
|
+
// - extractNetworks: a docker network on that box to join
|
|
970
|
+
// - extractPeerVolumes: a host path on that box to mount
|
|
971
|
+
// - extractAllowedProvider: a `shared` ref you may dereference
|
|
972
|
+
// - producerPredicate: a depends_on edge, which also fails the worker's
|
|
973
|
+
// dependsOn validation because a pinless build target is off the wire
|
|
974
|
+
// Adding any of the four gives up realm scope — peer-world.test.ts fails.
|
|
975
|
+
{
|
|
976
|
+
extract(appId, state) {
|
|
977
|
+
const out = [];
|
|
978
|
+
for (const [service, svc] of Object.entries(state.services)) {
|
|
979
|
+
if (svc.build != null && svc.image == null) {
|
|
980
|
+
out.push({ appId, service, recipe: svc.build });
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return out;
|
|
984
|
+
},
|
|
985
|
+
resolve(entries) {
|
|
986
|
+
return entries;
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: "Build apps",
|
|
991
|
+
phrase: "read the recipes of build apps",
|
|
992
|
+
helpText: "This app reads the build recipe of every build-from-source service across every app in your Realm, on any Worker.",
|
|
993
|
+
icon: "ShareNetworkIcon",
|
|
994
|
+
danger: true,
|
|
995
|
+
// Single-only: the one consumer is forge, and the builder is a
|
|
996
|
+
// singleton — enforced on `home.apiAccess` by builderScopeIssue,
|
|
997
|
+
// not here. Aggregated like the three above; the restriction is
|
|
998
|
+
// about who reads it, not what it holds.
|
|
999
|
+
appKinds: "single"
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
// src/schema/remote.ts
|
|
1004
|
+
var remote_exports = {};
|
|
1005
|
+
__export(remote_exports, {
|
|
1006
|
+
artifact: () => artifact,
|
|
1007
|
+
fetchUrl: () => fetchUrl
|
|
1008
|
+
});
|
|
1009
|
+
import { z as z5 } from "zod";
|
|
1010
|
+
|
|
1011
|
+
// src/remote-registry.ts
|
|
1012
|
+
function makeRemotable(inner) {
|
|
1013
|
+
return (kind, schema, meta) => {
|
|
1014
|
+
const factory = inner(kind, schema);
|
|
1015
|
+
return tagPermissionMeta(factory, kind, meta);
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/schema/remote.ts
|
|
1020
|
+
var remotable = makeRemotable(basePermission);
|
|
1021
|
+
var remotableSchemable = makeRemotable(schemablePermission);
|
|
1022
|
+
var fetchUrl = remotableSchemable(
|
|
1023
|
+
"remote.fetchUrl",
|
|
1024
|
+
z5.object({
|
|
1025
|
+
url: z5.string(),
|
|
1026
|
+
maxSizeMb: z5.number().positive().optional()
|
|
1027
|
+
}),
|
|
1028
|
+
{
|
|
1029
|
+
name: "Fetch URL",
|
|
1030
|
+
phrase: "fetch a remote URL",
|
|
1031
|
+
helpText: "Home fetches this URL on behalf of the app and validates the response. The app itself never makes network requests.",
|
|
1032
|
+
icon: "CloudArrowDownIcon",
|
|
1033
|
+
appKinds: "both"
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
var artifact = remotable(
|
|
1037
|
+
"remote.artifact",
|
|
1038
|
+
z5.object({ url: z5.string(), digest: z5.string() }),
|
|
1039
|
+
{
|
|
1040
|
+
name: "Download artifact",
|
|
1041
|
+
phrase: "download a remote artifact",
|
|
1042
|
+
helpText: "Home downloads this file, verifies its SHA-256 hash, and caches it. The app gets the verified file at deploy time.",
|
|
1043
|
+
icon: "CloudArrowDownIcon",
|
|
1044
|
+
appKinds: "both"
|
|
1045
|
+
}
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
// src/deep-merge.ts
|
|
1049
|
+
function deepMergeConfig(base, over) {
|
|
1050
|
+
const out = { ...base };
|
|
1051
|
+
for (const [k, v] of Object.entries(over)) {
|
|
1052
|
+
const current = out[k];
|
|
1053
|
+
if (isPlainMergeableObject(v) && isPlainMergeableObject(current)) {
|
|
1054
|
+
out[k] = deepMergeConfig(current, v);
|
|
1055
|
+
} else {
|
|
1056
|
+
out[k] = v;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return out;
|
|
1060
|
+
}
|
|
1061
|
+
function isPlainMergeableObject(v) {
|
|
1062
|
+
return v != null && typeof v === "object" && !Array.isArray(v) && !("__realmRef" in v);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/security.ts
|
|
1066
|
+
import { z as z6 } from "zod";
|
|
1067
|
+
|
|
1068
|
+
// src/grants.ts
|
|
1069
|
+
var HOST_REGISTRY = deriveHostRegistry(host_exports);
|
|
1070
|
+
function hostHooks(kind) {
|
|
1071
|
+
return HOST_REGISTRY.get(kind);
|
|
1072
|
+
}
|
|
1073
|
+
var HOME_REGISTRY = deriveHomeRegistry(home_exports);
|
|
1074
|
+
function homeHooks(kind) {
|
|
1075
|
+
return HOME_REGISTRY.get(kind);
|
|
1076
|
+
}
|
|
1077
|
+
function dataMatches(grantData, useData) {
|
|
1078
|
+
return JSON.stringify(grantData) === JSON.stringify(useData);
|
|
1079
|
+
}
|
|
1080
|
+
function isHostGrant(g) {
|
|
1081
|
+
return g.kind.startsWith("host.");
|
|
1082
|
+
}
|
|
1083
|
+
function isHomeGrant(g) {
|
|
1084
|
+
return g.kind.startsWith("home.");
|
|
1085
|
+
}
|
|
1086
|
+
function toValue(grant) {
|
|
1087
|
+
if (isHomeGrant(grant)) {
|
|
1088
|
+
const hooks2 = HOME_REGISTRY.get(grant.kind);
|
|
1089
|
+
if (!hooks2) return void 0;
|
|
1090
|
+
return hooks2.toValue(grant.data);
|
|
1091
|
+
}
|
|
1092
|
+
if (!isHostGrant(grant)) return void 0;
|
|
1093
|
+
const hooks = HOST_REGISTRY.get(grant.kind);
|
|
1094
|
+
if (!hooks) return void 0;
|
|
1095
|
+
return hooks.toValue(grant.data);
|
|
1096
|
+
}
|
|
1097
|
+
function resolveValue(grant, args) {
|
|
1098
|
+
if (isHostGrant(grant)) return viaHooks(HOST_REGISTRY.get(grant.kind), grant.data, args);
|
|
1099
|
+
if (isHomeGrant(grant)) return viaHooks(HOME_REGISTRY.get(grant.kind), grant.data, args);
|
|
1100
|
+
return toValue(grant);
|
|
1101
|
+
}
|
|
1102
|
+
function viaHooks(hooks, data, args) {
|
|
1103
|
+
if (!hooks) return void 0;
|
|
1104
|
+
return hooks.resolve ? hooks.resolve({ grant: data, ...args }) : hooks.toValue(data);
|
|
1105
|
+
}
|
|
1106
|
+
function collectUses(svc) {
|
|
1107
|
+
const uses = [];
|
|
1108
|
+
for (const [kind, hooks] of HOST_REGISTRY) {
|
|
1109
|
+
for (const data of hooks.iterateUses(svc)) {
|
|
1110
|
+
uses.push({ kind, data });
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
for (const [kind, hooks] of HOME_REGISTRY) {
|
|
1114
|
+
for (const data of hooks.iterateUses(svc)) {
|
|
1115
|
+
uses.push({ kind, data });
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return uses;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/permissions.ts
|
|
1122
|
+
var PERMISSION_META = derivePermissionMeta(
|
|
1123
|
+
host_exports,
|
|
1124
|
+
peer_exports,
|
|
1125
|
+
remote_exports,
|
|
1126
|
+
home_exports
|
|
1127
|
+
);
|
|
1128
|
+
var PERMISSION_KINDS = Object.keys(
|
|
1129
|
+
PERMISSION_META
|
|
1130
|
+
).sort();
|
|
1131
|
+
var HOST_PERMISSION_KINDS = Object.keys(
|
|
1132
|
+
derivePermissionMeta(host_exports)
|
|
1133
|
+
).sort();
|
|
1134
|
+
var WORKER_CAPABILITIES = [
|
|
1135
|
+
...HOST_PERMISSION_KINDS,
|
|
1136
|
+
"host.volumes"
|
|
1137
|
+
].sort();
|
|
1138
|
+
function permissionAllowsAppKind(kind, appKind) {
|
|
1139
|
+
const meta = PERMISSION_META[kind];
|
|
1140
|
+
return meta ? scopeAllows(meta.appKinds, appKind) : true;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/security.ts
|
|
1144
|
+
var rawGrantSchema = z6.object({
|
|
1145
|
+
kind: z6.string(),
|
|
1146
|
+
data: z6.unknown().optional()
|
|
1147
|
+
});
|
|
1148
|
+
var grantsSchema = z6.record(z6.string(), rawGrantSchema);
|
|
1149
|
+
function validateGrantData(grant) {
|
|
1150
|
+
if (isHostGrant(grant)) {
|
|
1151
|
+
const hooks = hostHooks(grant.kind);
|
|
1152
|
+
if (!hooks?.grantSchema) {
|
|
1153
|
+
return { ok: false, reason: `unknown host permission kind: ${grant.kind}` };
|
|
1154
|
+
}
|
|
1155
|
+
const r = hooks.grantSchema.safeParse(grant.data);
|
|
1156
|
+
if (!r.success) {
|
|
1157
|
+
const reason = r.error.issues.map((i) => i.message).join("; ");
|
|
1158
|
+
return { ok: false, reason };
|
|
1159
|
+
}
|
|
1160
|
+
return { ok: true, grant: { kind: grant.kind, data: r.data } };
|
|
1161
|
+
}
|
|
1162
|
+
if (isHomeGrant(grant)) {
|
|
1163
|
+
const hooks = homeHooks(grant.kind);
|
|
1164
|
+
if (!hooks?.grantSchema) {
|
|
1165
|
+
return { ok: false, reason: `unknown home permission kind: ${grant.kind}` };
|
|
1166
|
+
}
|
|
1167
|
+
const r = hooks.grantSchema.safeParse(grant.data);
|
|
1168
|
+
if (!r.success) {
|
|
1169
|
+
const reason = r.error.issues.map((i) => i.message).join("; ");
|
|
1170
|
+
return { ok: false, reason };
|
|
1171
|
+
}
|
|
1172
|
+
return { ok: true, grant: { kind: grant.kind, data: r.data } };
|
|
1173
|
+
}
|
|
1174
|
+
if (grant.kind.startsWith("peers.") || grant.kind.startsWith("remote.")) {
|
|
1175
|
+
return { ok: true, grant: { kind: grant.kind } };
|
|
1176
|
+
}
|
|
1177
|
+
return { ok: false, reason: `unknown permission kind: ${grant.kind}` };
|
|
1178
|
+
}
|
|
1179
|
+
function sanitizeGrants(permsFlat, grants) {
|
|
1180
|
+
if (!grants) return { sanitized: {}, rejected: [], pruned: [] };
|
|
1181
|
+
const sanitized = {};
|
|
1182
|
+
const rejected = [];
|
|
1183
|
+
const pruned = [];
|
|
1184
|
+
for (const [path, grant] of Object.entries(grants)) {
|
|
1185
|
+
const perm = permsFlat[path];
|
|
1186
|
+
if (!perm) {
|
|
1187
|
+
pruned.push({ path, kind: grant.kind, reason: "no declared permission at this path" });
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (grant.kind !== perm.kind) {
|
|
1191
|
+
pruned.push({
|
|
1192
|
+
path,
|
|
1193
|
+
kind: grant.kind,
|
|
1194
|
+
reason: `kind ${grant.kind} does not match declared ${perm.kind}`
|
|
1195
|
+
});
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
const result = validateGrantData(grant);
|
|
1199
|
+
if (!result.ok) {
|
|
1200
|
+
rejected.push({ path, kind: grant.kind, reason: result.reason });
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
sanitized[path] = result.grant;
|
|
1204
|
+
}
|
|
1205
|
+
return { sanitized, rejected, pruned };
|
|
1206
|
+
}
|
|
1207
|
+
function siloMountKey(hostPath, gid) {
|
|
1208
|
+
return `${hostPath}\0${gid}`;
|
|
1209
|
+
}
|
|
1210
|
+
function checkAppGrants(state, grants, permsFlat, options) {
|
|
1211
|
+
const violations = [];
|
|
1212
|
+
const declarationsByKind = /* @__PURE__ */ new Map();
|
|
1213
|
+
for (const [path, perm] of Object.entries(permsFlat)) {
|
|
1214
|
+
const list = declarationsByKind.get(perm.kind) ?? [];
|
|
1215
|
+
list.push([path, perm]);
|
|
1216
|
+
declarationsByKind.set(perm.kind, list);
|
|
1217
|
+
}
|
|
1218
|
+
for (const [serviceName, svc] of Object.entries(state.services)) {
|
|
1219
|
+
for (const v of svc.volumes ?? []) {
|
|
1220
|
+
const grantedBy = options?.readOnlyHostPaths?.get(v.hostPath);
|
|
1221
|
+
if (grantedBy === void 0) continue;
|
|
1222
|
+
if (v.readOnly === true) continue;
|
|
1223
|
+
violations.push({
|
|
1224
|
+
kind: "grant",
|
|
1225
|
+
permissionKind: grantedBy,
|
|
1226
|
+
serviceName,
|
|
1227
|
+
use: v.hostPath,
|
|
1228
|
+
description: "write to a directory the grant only covers read-only"
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
for (const v of svc.volumes ?? []) {
|
|
1232
|
+
const hostPath = v.hostPath;
|
|
1233
|
+
if (hostPath.startsWith("./")) continue;
|
|
1234
|
+
if (v.gid !== void 0) continue;
|
|
1235
|
+
if (options?.allowedAbsoluteHostPaths?.has(hostPath)) continue;
|
|
1236
|
+
violations.push({
|
|
1237
|
+
kind: "grant",
|
|
1238
|
+
permissionKind: "host.mount",
|
|
1239
|
+
serviceName,
|
|
1240
|
+
use: hostPath,
|
|
1241
|
+
description: "mount an absolute host path"
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
for (const use of collectUses(svc)) {
|
|
1245
|
+
if (use.kind === "host.silo") {
|
|
1246
|
+
const { hostPath, gid } = use.data;
|
|
1247
|
+
if (options?.allowedSiloMounts?.has(siloMountKey(hostPath, gid))) continue;
|
|
1248
|
+
violations.push({
|
|
1249
|
+
kind: "grant",
|
|
1250
|
+
permissionKind: use.kind,
|
|
1251
|
+
serviceName,
|
|
1252
|
+
use: use.data,
|
|
1253
|
+
description: PERMISSION_META["host.silo"].phrase
|
|
1254
|
+
});
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (use.kind === "home.publicDomain") {
|
|
1258
|
+
const { hosts, domainMode } = use.data;
|
|
1259
|
+
const declarations2 = declarationsByKind.get(use.kind) ?? [];
|
|
1260
|
+
const matched2 = declarations2.some(([path]) => {
|
|
1261
|
+
const grant = grants[path];
|
|
1262
|
+
if (grant == null || !isHomeGrant(grant) || grant.kind !== use.kind) return false;
|
|
1263
|
+
const data = grant.data;
|
|
1264
|
+
if (data.domainMode !== domainMode) return false;
|
|
1265
|
+
const granted = new Set(data.hosts ?? []);
|
|
1266
|
+
return hosts.every((h) => granted.has(h));
|
|
1267
|
+
});
|
|
1268
|
+
if (!matched2) {
|
|
1269
|
+
violations.push({
|
|
1270
|
+
kind: "grant",
|
|
1271
|
+
permissionKind: use.kind,
|
|
1272
|
+
serviceName,
|
|
1273
|
+
use: use.data,
|
|
1274
|
+
description: PERMISSION_META["home.publicDomain"].phrase
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const declarations = declarationsByKind.get(use.kind) ?? [];
|
|
1280
|
+
const matched = declarations.some(([path]) => {
|
|
1281
|
+
const grant = grants[path];
|
|
1282
|
+
return grant != null && isHostGrant(grant) && grant.kind === use.kind && dataMatches(grant.data, use.data);
|
|
1283
|
+
});
|
|
1284
|
+
if (!matched) {
|
|
1285
|
+
violations.push({
|
|
1286
|
+
kind: "grant",
|
|
1287
|
+
permissionKind: use.kind,
|
|
1288
|
+
serviceName,
|
|
1289
|
+
use: use.data,
|
|
1290
|
+
description: PERMISSION_META[use.kind]?.phrase ?? use.kind
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
for (const net of svc.joinNetworks ?? []) {
|
|
1295
|
+
if (!options?.allowedNetworks?.has(net)) {
|
|
1296
|
+
violations.push({
|
|
1297
|
+
kind: "grant",
|
|
1298
|
+
permissionKind: "network.join",
|
|
1299
|
+
serviceName,
|
|
1300
|
+
use: [net],
|
|
1301
|
+
description: "join network"
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (state.egress === true) {
|
|
1307
|
+
const declarations = declarationsByKind.get("host.egress") ?? [];
|
|
1308
|
+
const matched = declarations.some(([path]) => {
|
|
1309
|
+
const grant = grants[path];
|
|
1310
|
+
return grant != null && isHostGrant(grant) && grant.kind === "host.egress";
|
|
1311
|
+
});
|
|
1312
|
+
if (!matched) {
|
|
1313
|
+
violations.push({
|
|
1314
|
+
kind: "grant",
|
|
1315
|
+
permissionKind: "host.egress",
|
|
1316
|
+
use: true,
|
|
1317
|
+
description: PERMISSION_META["host.egress"]?.phrase ?? "host.egress"
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return violations;
|
|
1322
|
+
}
|
|
1323
|
+
function checkSystemAppRoutes(state) {
|
|
1324
|
+
const violations = [];
|
|
1325
|
+
for (const [serviceName, svc] of Object.entries(state.services)) {
|
|
1326
|
+
if (svc.router != null) {
|
|
1327
|
+
violations.push({
|
|
1328
|
+
kind: "app-kind",
|
|
1329
|
+
appKind: "system",
|
|
1330
|
+
description: `service "${serviceName}" declares a router \u2014 routes are not allowed on a system app`
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (state.trustedHeaders != null) {
|
|
1335
|
+
violations.push({
|
|
1336
|
+
kind: "app-kind",
|
|
1337
|
+
appKind: "system",
|
|
1338
|
+
// Not a routing argument: brokered identity also reaches a container
|
|
1339
|
+
// through a page or widget loader, and a system app has those. It is a
|
|
1340
|
+
// consent argument - a system app is seeded by the platform, with no
|
|
1341
|
+
// operator install click to grant the disclosure.
|
|
1342
|
+
description: "getTrustedHeaders is not supported on a system app \u2014 a system app is seeded by the platform, so no operator ever consented to it learning who is signed in"
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
if (state.hasAnonymousGate === true) {
|
|
1346
|
+
violations.push({
|
|
1347
|
+
kind: "app-kind",
|
|
1348
|
+
appKind: "system",
|
|
1349
|
+
description: "verifyAnonymousRequest is not supported on a system app \u2014 anonymous gates admit callers to an app's routes, and a system app has none"
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
return violations;
|
|
1353
|
+
}
|
|
1354
|
+
function checkPermissionAppKinds(appKind, permissionsFlat) {
|
|
1355
|
+
const violations = [];
|
|
1356
|
+
for (const [path, perm] of Object.entries(permissionsFlat)) {
|
|
1357
|
+
if (permissionAllowsAppKind(perm.kind, appKind)) continue;
|
|
1358
|
+
const phrase = PERMISSION_META[perm.kind]?.phrase;
|
|
1359
|
+
const allowed = appKind === "single" ? "system" : "single";
|
|
1360
|
+
const factory = allowed === "system" ? "createSystemApp" : "createApp";
|
|
1361
|
+
violations.push({
|
|
1362
|
+
kind: "app-kind",
|
|
1363
|
+
appKind,
|
|
1364
|
+
description: `"${path}" declares ${perm.kind}${phrase ? ` (${phrase})` : ""}, which only a ${allowed} app may declare \u2014 move the app to ${factory}, or drop the permission`
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
return violations;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// src/app-icon.ts
|
|
1371
|
+
import { z as z7 } from "zod";
|
|
1372
|
+
var PACK_REPOS = {
|
|
1373
|
+
"dashboard-icons": "homarr-labs/dashboard-icons",
|
|
1374
|
+
selfhst: "selfhst/icons"
|
|
1375
|
+
};
|
|
1376
|
+
var PACK_KEY_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
1377
|
+
var URL_UNSAFE_RE = /[\s"'<>\\`]/;
|
|
1378
|
+
var FORM_MESSAGE = 'icon must be "dashboard-icons:<key>", "selfhst:<key>" or an https:// URL';
|
|
1379
|
+
function parseIcon(spec) {
|
|
1380
|
+
for (const [prefix, repo] of Object.entries(PACK_REPOS)) {
|
|
1381
|
+
if (!spec.startsWith(`${prefix}:`)) continue;
|
|
1382
|
+
const key = spec.slice(prefix.length + 1);
|
|
1383
|
+
if (!PACK_KEY_RE.test(key)) {
|
|
1384
|
+
return {
|
|
1385
|
+
error: `${prefix} key must be lowercase and match ${PACK_KEY_RE.source}`
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
return { url: `https://cdn.jsdelivr.net/gh/${repo}/svg/${key}.svg` };
|
|
1389
|
+
}
|
|
1390
|
+
if (spec.startsWith("https://")) {
|
|
1391
|
+
const rest = spec.slice("https://".length);
|
|
1392
|
+
if (rest === "") return { error: "https icon URL must have a host" };
|
|
1393
|
+
if (URL_UNSAFE_RE.test(rest))
|
|
1394
|
+
return { error: "https icon URL must not contain whitespace or quotes" };
|
|
1395
|
+
return { url: spec };
|
|
1396
|
+
}
|
|
1397
|
+
return { error: FORM_MESSAGE };
|
|
1398
|
+
}
|
|
1399
|
+
var appIconSchema = z7.string().superRefine((spec, ctx) => {
|
|
1400
|
+
const parsed = parseIcon(spec);
|
|
1401
|
+
if ("error" in parsed) ctx.addIssue({ code: "custom", message: parsed.error });
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
export {
|
|
1405
|
+
appIconSchema,
|
|
1406
|
+
publicDomainValueSchema,
|
|
1407
|
+
deepMergeConfig,
|
|
1408
|
+
defineMethod,
|
|
1409
|
+
cloneAnd,
|
|
1410
|
+
baseInput,
|
|
1411
|
+
numberInput,
|
|
1412
|
+
RealmCompileError,
|
|
1413
|
+
formatIssue,
|
|
1414
|
+
host_exports,
|
|
1415
|
+
home_exports,
|
|
1416
|
+
isHostGrant,
|
|
1417
|
+
isHomeGrant,
|
|
1418
|
+
resolveValue,
|
|
1419
|
+
derivePeerRegistry,
|
|
1420
|
+
isNetworkStrategy,
|
|
1421
|
+
isVolumeStrategy,
|
|
1422
|
+
peer_exports,
|
|
1423
|
+
remote_exports,
|
|
1424
|
+
grantsSchema,
|
|
1425
|
+
sanitizeGrants,
|
|
1426
|
+
siloMountKey,
|
|
1427
|
+
checkAppGrants,
|
|
1428
|
+
checkSystemAppRoutes,
|
|
1429
|
+
checkPermissionAppKinds
|
|
1430
|
+
};
|