@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,557 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConfigContainerNode,
|
|
3
|
+
ConfigFieldLeaf,
|
|
4
|
+
ConfigFieldTree,
|
|
5
|
+
ConfigPermissionRef,
|
|
6
|
+
SerializablePermission,
|
|
7
|
+
} from "../../sandbox.ts";
|
|
8
|
+
import { SectionLabel } from "../components/permission-section.tsx";
|
|
9
|
+
import { GearIcon } from "../components/icons.tsx";
|
|
10
|
+
import { ConfigField, type ConfigFieldKind } from "../components/config-field.tsx";
|
|
11
|
+
import { PermissionRow } from "../components/permission-row.tsx";
|
|
12
|
+
import { SiloPicker } from "../components/silo-picker.tsx";
|
|
13
|
+
import { DomainEditor } from "../components/domain-editor.tsx";
|
|
14
|
+
import { PERMISSION_META, type PermissionKind, type SiloGrants } from "../../home.ts";
|
|
15
|
+
import type { PublicDomainGrantData } from "../../schema/public-domain.ts";
|
|
16
|
+
import type { SiloOption } from "./adapter.ts";
|
|
17
|
+
import { SectionBlock } from "../components/schema-block.tsx";
|
|
18
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../components/tabs.tsx";
|
|
19
|
+
import { Badge } from "../components/badge.tsx";
|
|
20
|
+
|
|
21
|
+
// The config-field descriptor types are produced once by the sandbox walker
|
|
22
|
+
// (see sandbox.ts); import them rather than mirror them here. A type-only
|
|
23
|
+
// import erases at build, so no QuickJS reaches the client island bundle.
|
|
24
|
+
// Re-exported for the consumers that pull them from this module.
|
|
25
|
+
export type { ConfigContainerNode, ConfigFieldLeaf, ConfigFieldTree };
|
|
26
|
+
export type ConfigField = ConfigFieldLeaf | ConfigContainerNode | ConfigPermissionRef;
|
|
27
|
+
|
|
28
|
+
interface TabsFormProps {
|
|
29
|
+
configFields: ConfigFieldTree;
|
|
30
|
+
permissionsFlat: Record<string, SerializablePermission>;
|
|
31
|
+
configValues: Record<string, unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Dotted paths of permissions the admin has enabled. Anything not in
|
|
34
|
+
* this set is treated as denied on install.
|
|
35
|
+
*/
|
|
36
|
+
enabledPermissions: Set<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Per-path admin-override values. Sparse: only entries the admin
|
|
39
|
+
* actively edited. Renderers fall back to the descriptor's `perm.data`
|
|
40
|
+
* for the displayed default; `descriptorsToGrants` does the same on
|
|
41
|
+
* submit. Presence-only kinds (host.gpu, host.egress, host.management)
|
|
42
|
+
* and peers.* / remote.* never appear here.
|
|
43
|
+
*/
|
|
44
|
+
grantValues: Record<string, unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* Dotted paths whose stored value is redacted on the server. The
|
|
47
|
+
* corresponding input renders a "leave blank to keep current" hint; the
|
|
48
|
+
* caller filters those out of the PATCH body when the input is empty.
|
|
49
|
+
*/
|
|
50
|
+
redactedPaths?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Per-redacted-path display mask (e.g. `pk_live_••••`), shown as the secret
|
|
53
|
+
* field's placeholder so the operator can tell which key is set. Optional —
|
|
54
|
+
* the new-app install path has no stored secrets, so it never passes this.
|
|
55
|
+
*/
|
|
56
|
+
secretHints?: Record<string, string>;
|
|
57
|
+
onConfigChange: (path: string, value: unknown) => void;
|
|
58
|
+
onPermissionToggle: (path: string, enabled: boolean) => void;
|
|
59
|
+
onGrantValueChange?: (path: string, value: unknown) => void;
|
|
60
|
+
/**
|
|
61
|
+
* A `home.publicDomain` leaf has a hostname typed into its add field that
|
|
62
|
+
* hasn't been turned into a row. Reported per path (an app may declare two
|
|
63
|
+
* leaves) so the panel can gate Install/Save on it.
|
|
64
|
+
*/
|
|
65
|
+
onDomainPendingChange?: (path: string, pending: boolean) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Every Silo the host knows. Only fetched when the app declares a
|
|
68
|
+
* `host.silo` permission; the picker narrows to `siloWorkerId`.
|
|
69
|
+
*/
|
|
70
|
+
silos?: SiloOption[];
|
|
71
|
+
/** The worker this app deploys to. Null until the operator picks one. */
|
|
72
|
+
siloWorkerId?: string | null;
|
|
73
|
+
siloKind?: "single" | "system";
|
|
74
|
+
/** Create a Silo on `siloWorkerId` and resolve with the stored row. */
|
|
75
|
+
onCreateSilo?: (input: {
|
|
76
|
+
name: string;
|
|
77
|
+
containerPath?: string;
|
|
78
|
+
hostPath?: string;
|
|
79
|
+
}) => Promise<SiloOption>;
|
|
80
|
+
/** Confirm an adopted Silo's setup command from the picker. */
|
|
81
|
+
onAcknowledgeSilo?: (id: string) => Promise<void>;
|
|
82
|
+
/** Disable every input (e.g. while saving). */
|
|
83
|
+
disabled?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Per-tab count of still-unmet required items (config + permissions),
|
|
86
|
+
* keyed by the tab's object key. Drives the count badge on each tab so
|
|
87
|
+
* the operator can see at a glance which tabs still need input. Absent /
|
|
88
|
+
* zero = no badge. Only meaningful when the top level is tabs.
|
|
89
|
+
*/
|
|
90
|
+
needsByTab?: Record<string, number>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isKnownPermissionKind(key: string): key is PermissionKind {
|
|
94
|
+
return key in PERMISSION_META;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The Silo props, bundled once so the tree drills one prop, not five.
|
|
99
|
+
* Null when the host has no Silos to offer at all - `realm-sdk dev` has no
|
|
100
|
+
* Home, so it has no Silo records and no worker model to scope them by. The
|
|
101
|
+
* picker is left out entirely there rather than shown in a state it can never
|
|
102
|
+
* leave; the dev compiler already fails the install with an actionable
|
|
103
|
+
* "no Silo in the fixture" message.
|
|
104
|
+
*/
|
|
105
|
+
interface SiloContext {
|
|
106
|
+
silos: SiloOption[];
|
|
107
|
+
workerId: string | null;
|
|
108
|
+
kind: "single" | "system";
|
|
109
|
+
onCreate?: (input: {
|
|
110
|
+
name: string;
|
|
111
|
+
containerPath?: string;
|
|
112
|
+
hostPath?: string;
|
|
113
|
+
}) => Promise<SiloOption>;
|
|
114
|
+
onAcknowledge?: (id: string) => Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Unified renderer for the config + permissions tree. Walks `configFields`
|
|
119
|
+
* and inlines permission rows at their dotted paths. Optional sections
|
|
120
|
+
* render as a fieldset with an on/off toggle; children render disabled
|
|
121
|
+
* when the section is off.
|
|
122
|
+
*/
|
|
123
|
+
export function TabsForm({
|
|
124
|
+
configFields,
|
|
125
|
+
permissionsFlat,
|
|
126
|
+
configValues,
|
|
127
|
+
enabledPermissions,
|
|
128
|
+
grantValues,
|
|
129
|
+
redactedPaths,
|
|
130
|
+
secretHints,
|
|
131
|
+
onConfigChange,
|
|
132
|
+
onPermissionToggle,
|
|
133
|
+
onGrantValueChange,
|
|
134
|
+
onDomainPendingChange,
|
|
135
|
+
silos,
|
|
136
|
+
siloWorkerId,
|
|
137
|
+
siloKind,
|
|
138
|
+
onCreateSilo,
|
|
139
|
+
onAcknowledgeSilo,
|
|
140
|
+
disabled,
|
|
141
|
+
needsByTab,
|
|
142
|
+
}: TabsFormProps) {
|
|
143
|
+
const redactedSet = new Set(redactedPaths ?? []);
|
|
144
|
+
// `silos` absent (not empty) is the host saying it has no Silo model.
|
|
145
|
+
const silo: SiloContext | null = silos
|
|
146
|
+
? {
|
|
147
|
+
silos,
|
|
148
|
+
workerId: siloWorkerId ?? null,
|
|
149
|
+
kind: siloKind ?? "single",
|
|
150
|
+
onCreate: onCreateSilo,
|
|
151
|
+
onAcknowledge: onAcknowledgeSilo,
|
|
152
|
+
}
|
|
153
|
+
: null;
|
|
154
|
+
const tabEntries = Object.entries(configFields).filter(
|
|
155
|
+
(e): e is [string, ConfigContainerNode] => e[1].type === "tab",
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (tabEntries.length === 0) {
|
|
159
|
+
return (
|
|
160
|
+
<div class="flex flex-col gap-4">
|
|
161
|
+
<FlatBlock
|
|
162
|
+
configFields={configFields}
|
|
163
|
+
permissionsFlat={permissionsFlat}
|
|
164
|
+
configValues={configValues}
|
|
165
|
+
enabledPermissions={enabledPermissions}
|
|
166
|
+
grantValues={grantValues}
|
|
167
|
+
redactedSet={redactedSet}
|
|
168
|
+
secretHints={secretHints}
|
|
169
|
+
onConfigChange={onConfigChange}
|
|
170
|
+
onPermissionToggle={onPermissionToggle}
|
|
171
|
+
onGrantValueChange={onGrantValueChange}
|
|
172
|
+
onDomainPendingChange={onDomainPendingChange}
|
|
173
|
+
silo={silo}
|
|
174
|
+
disabled={disabled ?? false}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Tabs defaultValue={tabEntries[0]![0]} data-testid="install-tabs">
|
|
182
|
+
<TabsList>
|
|
183
|
+
{tabEntries.map(([name, field]) => {
|
|
184
|
+
const needs = needsByTab?.[name] ?? 0;
|
|
185
|
+
return (
|
|
186
|
+
<TabsTrigger key={name} value={name} data-testid={`tab-${name}`}>
|
|
187
|
+
<span>{field.title ?? name}</span>
|
|
188
|
+
{needs > 0 && (
|
|
189
|
+
// Calm "needs attention" count (amber, not error-red) so the
|
|
190
|
+
// operator sees which tabs still want input at a glance.
|
|
191
|
+
<Badge
|
|
192
|
+
variant="outline"
|
|
193
|
+
className="min-w-5 border-amber-300/60 bg-amber-50 px-1.5 text-amber-700 tabular-nums"
|
|
194
|
+
data-testid={`tab-needs-${name}`}
|
|
195
|
+
>
|
|
196
|
+
{needs}
|
|
197
|
+
</Badge>
|
|
198
|
+
)}
|
|
199
|
+
</TabsTrigger>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
</TabsList>
|
|
203
|
+
{tabEntries.map(([name, field]) => {
|
|
204
|
+
const nested = (configValues[name] as Record<string, unknown> | undefined) ?? {};
|
|
205
|
+
return (
|
|
206
|
+
<TabsContent key={name} value={name}>
|
|
207
|
+
{field.description && (
|
|
208
|
+
<p class="text-xs text-muted-foreground leading-snug">{field.description}</p>
|
|
209
|
+
)}
|
|
210
|
+
{Object.entries(field.fields).map(([childName, childField]) => (
|
|
211
|
+
<FieldNode
|
|
212
|
+
key={childName}
|
|
213
|
+
path={`${name}.${childName}`}
|
|
214
|
+
name={childName}
|
|
215
|
+
field={childField}
|
|
216
|
+
values={nested}
|
|
217
|
+
permissionsFlat={permissionsFlat}
|
|
218
|
+
enabledPermissions={enabledPermissions}
|
|
219
|
+
grantValues={grantValues}
|
|
220
|
+
redactedSet={redactedSet}
|
|
221
|
+
secretHints={secretHints}
|
|
222
|
+
onConfigChange={onConfigChange}
|
|
223
|
+
onPermissionToggle={onPermissionToggle}
|
|
224
|
+
onGrantValueChange={onGrantValueChange}
|
|
225
|
+
onDomainPendingChange={onDomainPendingChange}
|
|
226
|
+
silo={silo}
|
|
227
|
+
disabled={disabled ?? false}
|
|
228
|
+
sectionOptionalOff={false}
|
|
229
|
+
/>
|
|
230
|
+
))}
|
|
231
|
+
</TabsContent>
|
|
232
|
+
);
|
|
233
|
+
})}
|
|
234
|
+
</Tabs>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface FlatBlockProps {
|
|
239
|
+
configFields: ConfigFieldTree;
|
|
240
|
+
permissionsFlat: Record<string, SerializablePermission>;
|
|
241
|
+
configValues: Record<string, unknown>;
|
|
242
|
+
enabledPermissions: Set<string>;
|
|
243
|
+
grantValues: Record<string, unknown>;
|
|
244
|
+
redactedSet: Set<string>;
|
|
245
|
+
secretHints?: Record<string, string>;
|
|
246
|
+
onConfigChange: (path: string, value: unknown) => void;
|
|
247
|
+
onPermissionToggle: (path: string, enabled: boolean) => void;
|
|
248
|
+
onGrantValueChange?: (path: string, value: unknown) => void;
|
|
249
|
+
onDomainPendingChange?: (path: string, pending: boolean) => void;
|
|
250
|
+
silo: SiloContext | null;
|
|
251
|
+
disabled: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Flat-leaves rendering: one card with every leaf (config + permission)
|
|
256
|
+
* in declared order. The walker stamps permission markers into
|
|
257
|
+
* `configFields` so iteration order matches the source.
|
|
258
|
+
*/
|
|
259
|
+
function FlatBlock(props: FlatBlockProps) {
|
|
260
|
+
const {
|
|
261
|
+
configFields,
|
|
262
|
+
permissionsFlat,
|
|
263
|
+
configValues,
|
|
264
|
+
enabledPermissions,
|
|
265
|
+
grantValues,
|
|
266
|
+
redactedSet,
|
|
267
|
+
secretHints,
|
|
268
|
+
onConfigChange,
|
|
269
|
+
onPermissionToggle,
|
|
270
|
+
onGrantValueChange,
|
|
271
|
+
onDomainPendingChange,
|
|
272
|
+
silo,
|
|
273
|
+
disabled,
|
|
274
|
+
} = props;
|
|
275
|
+
const entries = Object.entries(configFields);
|
|
276
|
+
if (entries.length === 0) return null;
|
|
277
|
+
return (
|
|
278
|
+
<div class="bg-trust-bg rounded-3xl shadow-clay-blue p-4 flex flex-col gap-3">
|
|
279
|
+
<SectionLabel
|
|
280
|
+
icon={<GearIcon className="w-4 h-4 text-trust" />}
|
|
281
|
+
className="text-trust pl-0.5"
|
|
282
|
+
>
|
|
283
|
+
Configuration
|
|
284
|
+
</SectionLabel>
|
|
285
|
+
{entries.map(([name, field]) => (
|
|
286
|
+
<FieldNode
|
|
287
|
+
key={name}
|
|
288
|
+
path={name}
|
|
289
|
+
name={name}
|
|
290
|
+
field={field}
|
|
291
|
+
values={configValues}
|
|
292
|
+
permissionsFlat={permissionsFlat}
|
|
293
|
+
enabledPermissions={enabledPermissions}
|
|
294
|
+
grantValues={grantValues}
|
|
295
|
+
redactedSet={redactedSet}
|
|
296
|
+
secretHints={secretHints}
|
|
297
|
+
onConfigChange={onConfigChange}
|
|
298
|
+
onPermissionToggle={onPermissionToggle}
|
|
299
|
+
onGrantValueChange={onGrantValueChange}
|
|
300
|
+
onDomainPendingChange={onDomainPendingChange}
|
|
301
|
+
silo={silo}
|
|
302
|
+
disabled={disabled}
|
|
303
|
+
sectionOptionalOff={false}
|
|
304
|
+
/>
|
|
305
|
+
))}
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
interface FieldNodeProps {
|
|
311
|
+
path: string;
|
|
312
|
+
name: string;
|
|
313
|
+
field: ConfigField;
|
|
314
|
+
values: Record<string, unknown>;
|
|
315
|
+
permissionsFlat: Record<string, SerializablePermission>;
|
|
316
|
+
enabledPermissions: Set<string>;
|
|
317
|
+
grantValues: Record<string, unknown>;
|
|
318
|
+
redactedSet: Set<string>;
|
|
319
|
+
secretHints?: Record<string, string>;
|
|
320
|
+
onConfigChange: (path: string, value: unknown) => void;
|
|
321
|
+
onPermissionToggle: (path: string, enabled: boolean) => void;
|
|
322
|
+
onGrantValueChange?: (path: string, value: unknown) => void;
|
|
323
|
+
onDomainPendingChange?: (path: string, pending: boolean) => void;
|
|
324
|
+
silo: SiloContext | null;
|
|
325
|
+
disabled: boolean;
|
|
326
|
+
/** Inherited from an ancestor optional section whose feature flag is OFF. */
|
|
327
|
+
sectionOptionalOff: boolean;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function toFieldKind(type: ConfigFieldLeaf["type"]): ConfigFieldKind {
|
|
331
|
+
return type;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function FieldNode(props: FieldNodeProps) {
|
|
335
|
+
const {
|
|
336
|
+
path,
|
|
337
|
+
name,
|
|
338
|
+
field,
|
|
339
|
+
values,
|
|
340
|
+
permissionsFlat,
|
|
341
|
+
enabledPermissions,
|
|
342
|
+
grantValues,
|
|
343
|
+
redactedSet,
|
|
344
|
+
secretHints,
|
|
345
|
+
onConfigChange,
|
|
346
|
+
onPermissionToggle,
|
|
347
|
+
onGrantValueChange,
|
|
348
|
+
onDomainPendingChange,
|
|
349
|
+
silo,
|
|
350
|
+
disabled,
|
|
351
|
+
sectionOptionalOff,
|
|
352
|
+
} = props;
|
|
353
|
+
|
|
354
|
+
if (field.type === "tab") {
|
|
355
|
+
// Nested tab inside a section would be a design violation; ignore.
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (field.type === "permission") {
|
|
360
|
+
const meta = permissionsFlat[field.path];
|
|
361
|
+
if (!meta) return null;
|
|
362
|
+
return (
|
|
363
|
+
<PermissionLeaf
|
|
364
|
+
path={field.path}
|
|
365
|
+
meta={meta}
|
|
366
|
+
enabled={enabledPermissions.has(field.path)}
|
|
367
|
+
grantValues={grantValues}
|
|
368
|
+
onPermissionToggle={onPermissionToggle}
|
|
369
|
+
onGrantValueChange={onGrantValueChange}
|
|
370
|
+
onDomainPendingChange={onDomainPendingChange}
|
|
371
|
+
silo={silo}
|
|
372
|
+
disabled={disabled}
|
|
373
|
+
sectionOptionalOff={sectionOptionalOff}
|
|
374
|
+
/>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (field.type === "section") {
|
|
379
|
+
const current = values[name];
|
|
380
|
+
// Toggle reflects whether the section's value is defined.
|
|
381
|
+
const on = field.optional ? current !== undefined : true;
|
|
382
|
+
const nested = on ? ((current as Record<string, unknown> | undefined) ?? {}) : {};
|
|
383
|
+
return (
|
|
384
|
+
<SectionBlock
|
|
385
|
+
name={field.title ?? name}
|
|
386
|
+
optional={field.optional}
|
|
387
|
+
on={on}
|
|
388
|
+
// Flipping on initializes an empty object so children can render.
|
|
389
|
+
// Flipping off resets to undefined so the sandbox sees the section
|
|
390
|
+
// as absent.
|
|
391
|
+
onChange={(next) => onConfigChange(path, next ? {} : undefined)}
|
|
392
|
+
disabled={disabled}
|
|
393
|
+
>
|
|
394
|
+
{Object.entries(field.fields).map(([childName, childField]) => (
|
|
395
|
+
<FieldNode
|
|
396
|
+
key={childName}
|
|
397
|
+
path={`${path}.${childName}`}
|
|
398
|
+
name={childName}
|
|
399
|
+
field={childField}
|
|
400
|
+
values={nested}
|
|
401
|
+
permissionsFlat={permissionsFlat}
|
|
402
|
+
enabledPermissions={enabledPermissions}
|
|
403
|
+
grantValues={grantValues}
|
|
404
|
+
redactedSet={redactedSet}
|
|
405
|
+
secretHints={secretHints}
|
|
406
|
+
onConfigChange={onConfigChange}
|
|
407
|
+
onPermissionToggle={onPermissionToggle}
|
|
408
|
+
onGrantValueChange={onGrantValueChange}
|
|
409
|
+
onDomainPendingChange={onDomainPendingChange}
|
|
410
|
+
silo={silo}
|
|
411
|
+
disabled={disabled || !on}
|
|
412
|
+
sectionOptionalOff={sectionOptionalOff || (field.optional === true && !on)}
|
|
413
|
+
/>
|
|
414
|
+
))}
|
|
415
|
+
</SectionBlock>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const leaf = field as ConfigFieldLeaf;
|
|
420
|
+
const id = `cfg-${path.replace(/\./g, "-")}`;
|
|
421
|
+
const label = leaf.label ?? name;
|
|
422
|
+
const current = values[name];
|
|
423
|
+
const isRedacted = redactedSet.has(path);
|
|
424
|
+
return (
|
|
425
|
+
<ConfigField
|
|
426
|
+
id={id}
|
|
427
|
+
kind={toFieldKind(leaf.type)}
|
|
428
|
+
label={label}
|
|
429
|
+
description={leaf.description}
|
|
430
|
+
placeholder={leaf.placeholder == null ? undefined : String(leaf.placeholder)}
|
|
431
|
+
values={leaf.values}
|
|
432
|
+
min={leaf.min}
|
|
433
|
+
max={leaf.max}
|
|
434
|
+
step={leaf.step}
|
|
435
|
+
required={sectionOptionalOff ? undefined : !leaf.optional}
|
|
436
|
+
value={current as string | number | boolean | undefined}
|
|
437
|
+
onChange={(v) => onConfigChange(path, v)}
|
|
438
|
+
disabled={disabled}
|
|
439
|
+
redacted={isRedacted}
|
|
440
|
+
secretMask={isRedacted ? secretHints?.[path] : undefined}
|
|
441
|
+
/>
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
interface PermissionLeafProps {
|
|
446
|
+
path: string;
|
|
447
|
+
meta: SerializablePermission;
|
|
448
|
+
enabled: boolean;
|
|
449
|
+
grantValues: Record<string, unknown>;
|
|
450
|
+
onPermissionToggle: (path: string, enabled: boolean) => void;
|
|
451
|
+
onGrantValueChange?: (path: string, value: unknown) => void;
|
|
452
|
+
onDomainPendingChange?: (path: string, pending: boolean) => void;
|
|
453
|
+
silo: SiloContext | null;
|
|
454
|
+
disabled: boolean;
|
|
455
|
+
/** Inherited from an ancestor optional section whose feature flag is OFF. */
|
|
456
|
+
sectionOptionalOff: boolean;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function PermissionLeaf(props: PermissionLeafProps) {
|
|
460
|
+
const {
|
|
461
|
+
path,
|
|
462
|
+
meta,
|
|
463
|
+
enabled,
|
|
464
|
+
grantValues,
|
|
465
|
+
onPermissionToggle,
|
|
466
|
+
onGrantValueChange,
|
|
467
|
+
onDomainPendingChange,
|
|
468
|
+
silo,
|
|
469
|
+
disabled,
|
|
470
|
+
sectionOptionalOff,
|
|
471
|
+
} = props;
|
|
472
|
+
const kindKey = meta.kind;
|
|
473
|
+
if (!isKnownPermissionKind(kindKey)) return null;
|
|
474
|
+
// grantValues is sparse — only entries the admin actively edited. Fall
|
|
475
|
+
// back to the descriptor's perm.data so untouched fields show the
|
|
476
|
+
// proposed value. `??` on null/undefined only, so an admin-cleared
|
|
477
|
+
// empty string passes through verbatim; the server's grantSchema
|
|
478
|
+
// (z.coerce.number()...) does the parse and rejects it as invalid.
|
|
479
|
+
const portArg =
|
|
480
|
+
kindKey === "host.port"
|
|
481
|
+
? ((grantValues[path] ?? meta.data) as string | number | undefined)
|
|
482
|
+
: undefined;
|
|
483
|
+
const urlArg =
|
|
484
|
+
kindKey === "remote.fetchUrl" || kindKey === "remote.artifact"
|
|
485
|
+
? (meta.data as { url: string }).url
|
|
486
|
+
: undefined;
|
|
487
|
+
// The descriptor carries the author's affordances; the grant carries the
|
|
488
|
+
// operator's chosen Silo id. Different shapes, so no descriptor fallback —
|
|
489
|
+
// an unpicked Silo is genuinely nothing, and the Install gate blocks it.
|
|
490
|
+
const siloControl =
|
|
491
|
+
kindKey === "host.silo" && silo ? (
|
|
492
|
+
<SiloPicker
|
|
493
|
+
ask={meta.data as SiloGrants}
|
|
494
|
+
silos={silo.silos}
|
|
495
|
+
workerId={silo.workerId}
|
|
496
|
+
kind={silo.kind}
|
|
497
|
+
value={(grantValues[path] as string | undefined) ?? null}
|
|
498
|
+
onChange={(id) => onGrantValueChange?.(path, id)}
|
|
499
|
+
onCreate={async (input) => {
|
|
500
|
+
if (!silo.onCreate) throw new Error("This host can't create Silos.");
|
|
501
|
+
const created = await silo.onCreate(input);
|
|
502
|
+
onGrantValueChange?.(path, created.id);
|
|
503
|
+
}}
|
|
504
|
+
onAcknowledge={silo.onAcknowledge}
|
|
505
|
+
disabled={disabled}
|
|
506
|
+
/>
|
|
507
|
+
) : undefined;
|
|
508
|
+
// Same shape as the Silo picker: the operator supplies the elevated value,
|
|
509
|
+
// and the descriptor (`true`) has nothing to fall back to. Editing lives
|
|
510
|
+
// here rather than on the Domains page, which is a read surface in v1 (see
|
|
511
|
+
// `todo/ideas/naming-layer.md`, slice 3).
|
|
512
|
+
const domainControl =
|
|
513
|
+
kindKey === "home.publicDomain" ? (
|
|
514
|
+
<DomainEditor
|
|
515
|
+
value={grantValues[path] as PublicDomainGrantData | undefined}
|
|
516
|
+
onChange={(next) => onGrantValueChange?.(path, next)}
|
|
517
|
+
// Not locked behind the switch, unlike every other control here:
|
|
518
|
+
// supplying a hostname IS the operator granting the permission, so
|
|
519
|
+
// adding one flips the switch. A domain typed under an off switch is
|
|
520
|
+
// dropped at submit by `descriptorsToGrants`, and blank is a valid
|
|
521
|
+
// grant, so no save-gate would catch that.
|
|
522
|
+
onEnable={() => onPermissionToggle(path, true)}
|
|
523
|
+
onPendingChange={
|
|
524
|
+
onDomainPendingChange ? (pending) => onDomainPendingChange(path, pending) : undefined
|
|
525
|
+
}
|
|
526
|
+
// Greys the rows and the mode checkbox rather than locking the whole
|
|
527
|
+
// control: adding stays live because adding is what turns the switch
|
|
528
|
+
// on, but a parked list must not read as a published one.
|
|
529
|
+
enabled={enabled}
|
|
530
|
+
disabled={disabled}
|
|
531
|
+
/>
|
|
532
|
+
) : undefined;
|
|
533
|
+
const titleLabel = portArg != null ? String(portArg) : undefined;
|
|
534
|
+
const reason = (meta.meta as { reason?: string }).reason ?? "";
|
|
535
|
+
const isOptional = (meta.meta as { isOptional?: boolean }).isOptional;
|
|
536
|
+
return (
|
|
537
|
+
<div data-perm-path={path}>
|
|
538
|
+
<PermissionRow
|
|
539
|
+
kind={kindKey}
|
|
540
|
+
reason={reason}
|
|
541
|
+
required={sectionOptionalOff ? undefined : !isOptional}
|
|
542
|
+
checked={enabled}
|
|
543
|
+
onCheckedChange={(v) => onPermissionToggle(path, v)}
|
|
544
|
+
label={titleLabel}
|
|
545
|
+
port={portArg}
|
|
546
|
+
onPortChange={onGrantValueChange ? (v) => onGrantValueChange(path, v) : undefined}
|
|
547
|
+
control={siloControl ?? domainControl}
|
|
548
|
+
url={urlArg}
|
|
549
|
+
/>
|
|
550
|
+
{sectionOptionalOff && enabled && (
|
|
551
|
+
<p class="text-xs text-muted-foreground leading-snug italic mt-1.5">
|
|
552
|
+
Part of an inactive feature.
|
|
553
|
+
</p>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|