@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.
Files changed (230) hide show
  1. package/LICENSE-EXCEPTION +26 -0
  2. package/README.md +34 -0
  3. package/dist/chunk-3R6KEFOI.js +22 -0
  4. package/dist/chunk-67BDAMYX.js +376 -0
  5. package/dist/chunk-AITGS27R.js +139 -0
  6. package/dist/chunk-CMAEHQ4F.js +6 -0
  7. package/dist/chunk-MLKGABMK.js +9 -0
  8. package/dist/chunk-O3UIK34B.js +1430 -0
  9. package/dist/chunk-VHTUSZUX.js +176 -0
  10. package/dist/cli.js +9006 -0
  11. package/dist/docker.js +54 -0
  12. package/dist/index.js +1053 -0
  13. package/dist/ingress.js +436 -0
  14. package/dist/jsx-runtime.js +2 -0
  15. package/dist/preact.js +22 -0
  16. package/dist/service-url.js +11 -0
  17. package/dist/services.js +213 -0
  18. package/dist/test.js +5069 -0
  19. package/dist/ui.js +254 -0
  20. package/dist/worker-protocol.js +89 -0
  21. package/package.json +112 -0
  22. package/src/app-icon.ts +74 -0
  23. package/src/apply.ts +52 -0
  24. package/src/bundle-ui.ts +211 -0
  25. package/src/bundle.ts +546 -0
  26. package/src/cli/build-dev-landing.ts +84 -0
  27. package/src/cli/build-install-panel.ts +90 -0
  28. package/src/cli/build-runtime.ts +133 -0
  29. package/src/cli/build.ts +63 -0
  30. package/src/cli/dev.ts +107 -0
  31. package/src/cli/down.ts +60 -0
  32. package/src/cli/main.ts +56 -0
  33. package/src/cli/test.ts +61 -0
  34. package/src/cli/validate.ts +37 -0
  35. package/src/compile.ts +100 -0
  36. package/src/create-app.ts +982 -0
  37. package/src/deep-merge.ts +33 -0
  38. package/src/dev/fixture.ts +48 -0
  39. package/src/dev/install-handler.ts +383 -0
  40. package/src/dev/server.ts +968 -0
  41. package/src/dev/stack.ts +108 -0
  42. package/src/dev/state.ts +159 -0
  43. package/src/docker.ts +96 -0
  44. package/src/errors.ts +61 -0
  45. package/src/factory.ts +481 -0
  46. package/src/garage.ts +56 -0
  47. package/src/generator/hash.ts +6 -0
  48. package/src/generator/resolve-refs.ts +207 -0
  49. package/src/grants-builder.ts +100 -0
  50. package/src/grants.ts +260 -0
  51. package/src/home-api.ts +172 -0
  52. package/src/home-cookie.ts +10 -0
  53. package/src/home-registry.ts +120 -0
  54. package/src/home.ts +187 -0
  55. package/src/host-registry.ts +143 -0
  56. package/src/http-header.ts +20 -0
  57. package/src/identifiers.ts +49 -0
  58. package/src/index.ts +66 -0
  59. package/src/info.ts +64 -0
  60. package/src/ini.ts +50 -0
  61. package/src/json.ts +14 -0
  62. package/src/jsx-runtime/index.ts +5 -0
  63. package/src/keys.ts +29 -0
  64. package/src/mysql.ts +56 -0
  65. package/src/pack-build.ts +108 -0
  66. package/src/pack.ts +115 -0
  67. package/src/peer-registry.ts +121 -0
  68. package/src/peer-world.ts +117 -0
  69. package/src/permission-meta.ts +116 -0
  70. package/src/permissions.ts +108 -0
  71. package/src/pkg-root.ts +34 -0
  72. package/src/postgres.ts +79 -0
  73. package/src/preact/index.ts +27 -0
  74. package/src/redis.ts +43 -0
  75. package/src/ref.ts +271 -0
  76. package/src/remote-registry.ts +33 -0
  77. package/src/required-host-kinds.ts +110 -0
  78. package/src/resolve-context.ts +111 -0
  79. package/src/resolve.ts +471 -0
  80. package/src/runtime/docker/compose/compose-schema.ts +118 -0
  81. package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
  82. package/src/runtime/docker/compose/default-hardening.ts +21 -0
  83. package/src/runtime/docker/compose/effects.ts +107 -0
  84. package/src/runtime/docker/compose/index.ts +17 -0
  85. package/src/runtime/docker/compose/labels.ts +39 -0
  86. package/src/runtime/docker/compose/types.ts +78 -0
  87. package/src/runtime/docker/home-proxy-server/index.ts +177 -0
  88. package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
  89. package/src/runtime/docker/ingress/build-config.ts +819 -0
  90. package/src/runtime/docker/ingress/index.ts +16 -0
  91. package/src/runtime/docker/ingress/types.ts +182 -0
  92. package/src/runtime/docker/log-frames.ts +121 -0
  93. package/src/runtime/docker/logs-server/index.ts +366 -0
  94. package/src/runtime/docker/spawn-server/context-key.ts +82 -0
  95. package/src/runtime/docker/spawn-server/index.ts +492 -0
  96. package/src/runtime/docker/spawn-server/protocol.ts +70 -0
  97. package/src/runtime/docker/spawn-server/spec.ts +122 -0
  98. package/src/runtime/docker/stats-server/disks.ts +161 -0
  99. package/src/runtime/docker/stats-server/index.ts +322 -0
  100. package/src/runtime/home-client/index.ts +71 -0
  101. package/src/runtime/log-line.ts +24 -0
  102. package/src/runtime/logs-client/index.ts +175 -0
  103. package/src/runtime/spawn-client/index.ts +93 -0
  104. package/src/runtime/spawn-client/kaniko.ts +79 -0
  105. package/src/runtime/stats-client/index.ts +65 -0
  106. package/src/runtime/stats-sample.ts +95 -0
  107. package/src/safe-json.ts +37 -0
  108. package/src/sandbox.ts +1209 -0
  109. package/src/schema/build-apps.ts +38 -0
  110. package/src/schema/home.ts +217 -0
  111. package/src/schema/host.ts +792 -0
  112. package/src/schema/inputs.ts +26 -0
  113. package/src/schema/nodes.ts +75 -0
  114. package/src/schema/peer.ts +400 -0
  115. package/src/schema/public-domain.ts +116 -0
  116. package/src/schema/remote.ts +74 -0
  117. package/src/security.ts +594 -0
  118. package/src/serialize.ts +21 -0
  119. package/src/service-definition.ts +284 -0
  120. package/src/service-url.ts +64 -0
  121. package/src/service.ts +39 -0
  122. package/src/services/index.ts +5 -0
  123. package/src/shared.ts +65 -0
  124. package/src/speculative-silo.ts +96 -0
  125. package/src/stable-secret.ts +14 -0
  126. package/src/surfaces.ts +264 -0
  127. package/src/tailwind-bin.ts +56 -0
  128. package/src/test/boot.ts +192 -0
  129. package/src/test/compile.ts +501 -0
  130. package/src/test/describe.ts +117 -0
  131. package/src/test/index.ts +11 -0
  132. package/src/test/ingress-fixture.ts +37 -0
  133. package/src/test/types.ts +318 -0
  134. package/src/trusted-headers.ts +79 -0
  135. package/src/types.ts +126 -0
  136. package/src/ui/components/aside.tsx +96 -0
  137. package/src/ui/components/badge.tsx +42 -0
  138. package/src/ui/components/button.tsx +59 -0
  139. package/src/ui/components/card.tsx +106 -0
  140. package/src/ui/components/checkbox.tsx +37 -0
  141. package/src/ui/components/code-block.tsx +29 -0
  142. package/src/ui/components/combobox.tsx +119 -0
  143. package/src/ui/components/config-field.tsx +284 -0
  144. package/src/ui/components/dialog.tsx +137 -0
  145. package/src/ui/components/domain-editor.tsx +317 -0
  146. package/src/ui/components/domain-list.ts +205 -0
  147. package/src/ui/components/dropdown-menu.tsx +73 -0
  148. package/src/ui/components/empty-state.tsx +51 -0
  149. package/src/ui/components/highlight.ts +59 -0
  150. package/src/ui/components/icons.tsx +101 -0
  151. package/src/ui/components/image-posture.tsx +85 -0
  152. package/src/ui/components/input.tsx +28 -0
  153. package/src/ui/components/label.tsx +16 -0
  154. package/src/ui/components/line-chart.tsx +113 -0
  155. package/src/ui/components/page.tsx +39 -0
  156. package/src/ui/components/permission-row.tsx +262 -0
  157. package/src/ui/components/permission-section.tsx +51 -0
  158. package/src/ui/components/pie-chart.tsx +245 -0
  159. package/src/ui/components/schema-block.tsx +60 -0
  160. package/src/ui/components/separator.tsx +42 -0
  161. package/src/ui/components/sheet.tsx +130 -0
  162. package/src/ui/components/sidebar.tsx +650 -0
  163. package/src/ui/components/silo-picker.tsx +510 -0
  164. package/src/ui/components/skeleton.tsx +13 -0
  165. package/src/ui/components/sonner.tsx +33 -0
  166. package/src/ui/components/switch.tsx +58 -0
  167. package/src/ui/components/table-view.ts +219 -0
  168. package/src/ui/components/table.tsx +506 -0
  169. package/src/ui/components/tabs.tsx +59 -0
  170. package/src/ui/components/timeago.tsx +84 -0
  171. package/src/ui/components/tooltip.tsx +56 -0
  172. package/src/ui/components.json +21 -0
  173. package/src/ui/dev/dev-shell.tsx +145 -0
  174. package/src/ui/home/home-grid.tsx +177 -0
  175. package/src/ui/hooks/use-mobile.ts +19 -0
  176. package/src/ui/index.ts +189 -0
  177. package/src/ui/install/adapter.ts +193 -0
  178. package/src/ui/install/deploy-targets-picker.tsx +185 -0
  179. package/src/ui/install/install-app-panel.tsx +912 -0
  180. package/src/ui/install/install-error.tsx +62 -0
  181. package/src/ui/install/perms-inactive.ts +50 -0
  182. package/src/ui/install/required-fields.ts +37 -0
  183. package/src/ui/install/tabs-form.tsx +557 -0
  184. package/src/ui/tokens.css +279 -0
  185. package/src/ui/utils.ts +78 -0
  186. package/src/ui-error.ts +139 -0
  187. package/src/ui-runtime/dev-landing-entry.tsx +258 -0
  188. package/src/ui-runtime/index.ts +83 -0
  189. package/src/ui-runtime/install-entry.tsx +119 -0
  190. package/src/ui-runtime/mount.ts +367 -0
  191. package/src/ui-runtime/runtime.css +56 -0
  192. package/src/ui-surface.ts +451 -0
  193. package/src/valkey.ts +43 -0
  194. package/src/worker/extract-tar.ts +125 -0
  195. package/src/worker/identity.ts +183 -0
  196. package/src/worker/index.ts +16 -0
  197. package/src/worker/public-ip.ts +92 -0
  198. package/src/worker/queue.ts +54 -0
  199. package/src/worker/reconcile.ts +127 -0
  200. package/src/worker/start-worker.ts +227 -0
  201. package/src/worker/ui-proxy.ts +335 -0
  202. package/src/worker/uplink.ts +523 -0
  203. package/src/worker/validate.ts +34 -0
  204. package/src/worker/verify-relay.ts +102 -0
  205. package/src/worker-protocol/app-dirs.ts +94 -0
  206. package/src/worker-protocol/home-proxy-paths.ts +76 -0
  207. package/src/worker-protocol/index.ts +55 -0
  208. package/src/worker-protocol/ingress.ts +166 -0
  209. package/src/worker-protocol/logs-socket-path.ts +50 -0
  210. package/src/worker-protocol/networks.ts +39 -0
  211. package/src/worker-protocol/node-version.ts +70 -0
  212. package/src/worker-protocol/silo-paths.ts +114 -0
  213. package/src/worker-protocol/spawn-paths.ts +165 -0
  214. package/src/worker-protocol/stats-socket-path.ts +40 -0
  215. package/src/worker-protocol/update-files.ts +77 -0
  216. package/src/worker-protocol/uplink/constants.ts +86 -0
  217. package/src/worker-protocol/uplink/errors.ts +61 -0
  218. package/src/worker-protocol/uplink/frame.ts +114 -0
  219. package/src/worker-protocol/uplink/identity.ts +40 -0
  220. package/src/worker-protocol/uplink/index.ts +63 -0
  221. package/src/worker-protocol/uplink/methods.ts +462 -0
  222. package/src/worker-protocol/uplink/mux.ts +646 -0
  223. package/src/worker-protocol/uplink/protocol.ts +11 -0
  224. package/src/worker-protocol/uplink/rpc.ts +309 -0
  225. package/src/worker-protocol/uplink/session.ts +540 -0
  226. package/src/worker-protocol/uplink/test-support.ts +188 -0
  227. package/src/worker-protocol/worker-mount.ts +27 -0
  228. package/src/worker-protocol/worker-state.ts +190 -0
  229. package/src/xml.ts +49 -0
  230. package/src/yaml.ts +15 -0
@@ -0,0 +1,26 @@
1
+ // input.* form-field factories.
2
+ //
3
+ // Inputs declare a schema for the user's runtime value (the Home UI uses
4
+ // the schema to validate what the user types). The factory takes no
5
+ // config-time value — chain methods configure title, prefill, etc.
6
+
7
+ import { z } from "zod";
8
+ import { baseInput, cloneAnd, numberInput } from "../factory.ts";
9
+
10
+ export const text = baseInput("input.text", z.string());
11
+
12
+ export const secret = baseInput("input.secret", z.string());
13
+
14
+ // numberInput carries the `.int()/.min()/.max()` chain (see factory.ts).
15
+ export const number = numberInput("input.number", z.number());
16
+
17
+ export const boolean = baseInput("input.boolean", z.boolean());
18
+
19
+ // Enum is parameterized — `r.input.enum(["a", "b"])` returns a fully-bound
20
+ // record, not a 0-arg factory like text/secret, because the values are part
21
+ // of the kind binding. `values` is also attached to the record so the sandbox
22
+ // walker can surface them to the Home UI without poking at Zod internals.
23
+ export function enum_<const T extends readonly [string, ...string[]]>(values: T) {
24
+ const record = baseInput("input.enum", z.enum(values))();
25
+ return cloneAnd(record, { values }) as typeof record & { readonly values: T };
26
+ }
@@ -0,0 +1,75 @@
1
+ // Sections and tabs — UI structural nodes that wrap inputs (and nested
2
+ // sections) into groupings. They are generic functions rather than Builders
3
+ // because their "value" is a children record whose types must narrow per
4
+ // call site (`section({port: number()})` preserves `port: NumberInput`),
5
+ // and the Builder binds I at schema-bind time. Chain-method helpers come
6
+ // from factory.ts.
7
+
8
+ import { cloneAnd, defineMethod, type CompleteInput, type CompletePermission } from "../factory.ts";
9
+
10
+ // ----- Section -------------------------------------------------------------
11
+
12
+ export type SectionChild = CompletePermission | CompleteInput | CompleteSection;
13
+
14
+ export interface SectionRecord<C extends Record<string, SectionChild>> {
15
+ readonly kind: "node.section";
16
+ readonly children: C;
17
+ readonly meta: {
18
+ readonly isOptional: boolean;
19
+ readonly title?: string;
20
+ readonly description?: string;
21
+ };
22
+ title(this: this, text: string): this;
23
+ describe(this: this, text: string): this;
24
+ optional(this: this): this & { readonly meta: { readonly isOptional: true } };
25
+ }
26
+
27
+ export type CompleteSection = SectionRecord<Record<string, SectionChild>>;
28
+
29
+ export function section<C extends Record<string, SectionChild>>(children: C): SectionRecord<C> {
30
+ const record = {
31
+ kind: "node.section" as const,
32
+ children,
33
+ meta: { isOptional: false },
34
+ } as unknown as SectionRecord<C>;
35
+ defineMethod(record, "title", function (this: SectionRecord<C>, text: string) {
36
+ return cloneAnd(this, { meta: { ...this.meta, title: text } });
37
+ });
38
+ defineMethod(record, "describe", function (this: SectionRecord<C>, text: string) {
39
+ return cloneAnd(this, { meta: { ...this.meta, description: text } });
40
+ });
41
+ defineMethod(record, "optional", function (this: SectionRecord<C>) {
42
+ return cloneAnd(this, { meta: { ...this.meta, isOptional: true } });
43
+ });
44
+ return record;
45
+ }
46
+
47
+ // ----- Tab -----------------------------------------------------------------
48
+
49
+ export type TabChild = CompletePermission | CompleteInput | CompleteSection;
50
+
51
+ export interface TabRecord<C extends Record<string, TabChild>> {
52
+ readonly kind: "node.tab";
53
+ readonly children: C;
54
+ readonly meta: {
55
+ readonly title?: string;
56
+ readonly description?: string;
57
+ };
58
+ title(this: this, text: string): this;
59
+ describe(this: this, text: string): this;
60
+ }
61
+
62
+ export function tab<C extends Record<string, TabChild>>(children: C): TabRecord<C> {
63
+ const record = {
64
+ kind: "node.tab" as const,
65
+ children,
66
+ meta: {},
67
+ } as unknown as TabRecord<C>;
68
+ defineMethod(record, "title", function (this: TabRecord<C>, text: string) {
69
+ return cloneAnd(this, { meta: { ...this.meta, title: text } });
70
+ });
71
+ defineMethod(record, "describe", function (this: TabRecord<C>, text: string) {
72
+ return cloneAnd(this, { meta: { ...this.meta, description: text } });
73
+ });
74
+ return record;
75
+ }
@@ -0,0 +1,400 @@
1
+ // peers.* permission factories + per-kind Home-side mechanics.
2
+ //
3
+ // Each factory is built via `makePeerable`, which calls `basePermission`
4
+ // AND tags the factory with the kind's hooks (extract / resolve /
5
+ // extractNetworks / etc.) as non-enumerable symbol properties. The peer
6
+ // registry is derived later via `derivePeerRegistry(peers)` — there is
7
+ // no mutable module-scoped table. Adding a peer kind is one declaration.
8
+
9
+ import { z } from "zod";
10
+ import type { Network } from "../identifiers.ts";
11
+ import type { HomeService } from "../resolve.ts";
12
+ import { basePermission } from "../factory.ts";
13
+ import { makePeerable, type AllowedProvider } from "../peer-registry.ts";
14
+ import type { BuildAppEntry } from "./build-apps.ts";
15
+ import type { DomainMode } from "./public-domain.ts";
16
+
17
+ export type { AllowedProvider } from "../peer-registry.ts";
18
+
19
+ const peerable = makePeerable(basePermission);
20
+
21
+ // ----- Domain types -------------------------------------------------------
22
+
23
+ /**
24
+ * The router shape a proxy app consumes. Deliberately NOT a structural alias
25
+ * of `HomeService["router"]`: the resolved side carries
26
+ * `r.home.publicDomain()`'s whole record (hosts + the operator's mode), and a
27
+ * proxy wants the two flattened - one list of hostnames to route, one mode to
28
+ * apply. `extract` below does that flattening, so narrowing the resolved shape
29
+ * can't silently change what Caddy is handed.
30
+ *
31
+ * `publicDomain` keeps the `string | string[]` union rather than narrowing to
32
+ * an array: a single host is the overwhelmingly common case, and the union is
33
+ * what a proxy's own tests can write by hand. The key is **absent** (not `[]`)
34
+ * when the operator granted no hosts, so "no public route" is structural
35
+ * rather than a truthiness check on an empty array.
36
+ */
37
+ export type RoutedRouter = Omit<NonNullable<HomeService["router"]>, "publicDomain"> & {
38
+ publicDomain?: string | string[];
39
+ domainMode: DomainMode;
40
+ };
41
+
42
+ export interface RoutedService {
43
+ serviceId: string;
44
+ router: RoutedRouter;
45
+ }
46
+
47
+ export interface RoutedAppEntry {
48
+ appId: string;
49
+ network: Network;
50
+ services: RoutedService[];
51
+ /**
52
+ * Header-brokering names the app declared via `getTrustedHeaders` (absent
53
+ * when it didn't). The proxy injects these on the app's authenticated
54
+ * route (lifted from the ForwardAuth response) and strips them from
55
+ * every other path so identity can't be spoofed.
56
+ */
57
+ trustedHeaderKeys?: string[];
58
+ }
59
+
60
+ export interface HostPortEntry {
61
+ port: number;
62
+ }
63
+
64
+ export type BackupStrategy =
65
+ | { kind: "file-copy" }
66
+ | { kind: "sqlite"; file: string }
67
+ | { kind: "pg-dump"; url: string; version: number; network: Network }
68
+ | { kind: "valkey-save"; url: string; network: Network }
69
+ | { kind: "mysql-dump"; url: string; version: number; network: Network };
70
+
71
+ export type NetworkStrategy = Extract<BackupStrategy, { network: Network }>;
72
+ export type VolumeStrategy = Exclude<BackupStrategy, { network: Network }>;
73
+
74
+ /**
75
+ * Branded host-absolute path sourced from a peer (e.g. resolved
76
+ * operator-placed Silo of a producer). Carries through `BackupTarget` so it
77
+ * flows into `volumes.hostPath` alongside `` `./${string}` `` without
78
+ * widening the literal-path type.
79
+ */
80
+ export type PeerHostPath = string & { readonly __peerHostPath: unique symbol };
81
+
82
+ export interface BackupTarget {
83
+ appId: string;
84
+ serviceKey: string;
85
+ hostPath: PeerHostPath;
86
+ strategy: BackupStrategy;
87
+ }
88
+
89
+ export function isNetworkStrategy(s: BackupStrategy): s is NetworkStrategy {
90
+ return "network" in s;
91
+ }
92
+
93
+ export function isVolumeStrategy(s: BackupStrategy): s is VolumeStrategy {
94
+ return !("network" in s);
95
+ }
96
+
97
+ // ----- Helpers ------------------------------------------------------------
98
+
99
+ function appNetwork(appId: string): Network {
100
+ return `realm_${appId}` as Network;
101
+ }
102
+
103
+ /**
104
+ * Walk a JSON structure and rewrite every `{kind:"derived",
105
+ * appId:"__self__"}` ref marker to `{kind:"shared", providerAppId,
106
+ * serviceKey}`. Backup volumes' extract uses this: the provider's
107
+ * `getServiceDefinition` emits `derived(__self__)` for its own URL, and
108
+ * `__self__` resolves against whichever app the walker is running for.
109
+ * The backup app is a different app, so without the rewrite it would
110
+ * hash with its own appId instead of the provider's and derive the wrong
111
+ * password. The rewrite also puts the marker under the grant check, so
112
+ * only an app holding the matching peer grant can resolve it.
113
+ */
114
+ function rewriteSelfDerivedToShared(
115
+ value: unknown,
116
+ providerAppId: string,
117
+ serviceKey: string,
118
+ ): unknown {
119
+ if (value == null || typeof value !== "object") return value;
120
+ if (Array.isArray(value)) {
121
+ return value.map((v) => rewriteSelfDerivedToShared(v, providerAppId, serviceKey));
122
+ }
123
+ if ("__realmRef" in value) {
124
+ const inner = (value as { __realmRef: unknown }).__realmRef;
125
+ return { __realmRef: rewriteMarker(inner, providerAppId, serviceKey) };
126
+ }
127
+ const out: Record<string, unknown> = {};
128
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
129
+ out[k] = rewriteSelfDerivedToShared(v, providerAppId, serviceKey);
130
+ }
131
+ return out;
132
+ }
133
+
134
+ function rewriteMarker(marker: unknown, providerAppId: string, serviceKey: string): unknown {
135
+ if (marker == null || typeof marker !== "object") return marker;
136
+ const m = marker as { kind?: string; appId?: string; name?: string; parts?: unknown[] };
137
+ if (m.kind === "derived" && m.appId === "__self__" && typeof m.name === "string") {
138
+ return { kind: "shared", providerAppId, serviceKey, name: m.name };
139
+ }
140
+ if (m.kind === "concat" && Array.isArray(m.parts)) {
141
+ return {
142
+ kind: "concat",
143
+ parts: m.parts.map((p) =>
144
+ typeof p === "string" ? p : rewriteMarker(p, providerAppId, serviceKey),
145
+ ),
146
+ };
147
+ }
148
+ return marker;
149
+ }
150
+
151
+ // ----- Factories ----------------------------------------------------------
152
+
153
+ /**
154
+ * Read every app on this worker that exposes a routed service.
155
+ *
156
+ * Home aggregates routing info from every peer that declares a
157
+ * `router` and passes it in. The grant also lets the app join each
158
+ * routed app's network so traffic can actually reach them.
159
+ *
160
+ * Only single apps can declare a `router` (system apps are routeless by
161
+ * construction — `checkSystemAppRoutes`), so a system app never appears
162
+ * in this aggregation as a producer.
163
+ *
164
+ * Only a system app (`createSystemApp`) may declare this. It aggregates
165
+ * across every app on the worker, which is fleet-infrastructure work.
166
+ *
167
+ * @see https://yourrealm.eu/docs/realm-ts/peers/routed-apps/
168
+ */
169
+ export const routedApps = peerable(
170
+ "peers.routedApps",
171
+ z.literal(true).default(true),
172
+ {
173
+ extract(appId, state): RoutedAppEntry[] {
174
+ const services: RoutedService[] = [];
175
+ for (const [serviceId, svc] of Object.entries(state.services)) {
176
+ if (svc.router == null) continue;
177
+ // Destructured rather than spread-then-overwrite so the flattened
178
+ // shape falls out of the code: `rest` cannot carry the record
179
+ // through, and an added router field arrives in `rest` for free.
180
+ const { publicDomain, ...rest } = svc.router;
181
+ const hosts = publicDomain?.hosts ?? [];
182
+ services.push({
183
+ serviceId,
184
+ router: {
185
+ ...rest,
186
+ domainMode: publicDomain?.domainMode ?? "redirect",
187
+ // Key omitted entirely when the operator granted no hosts, so a
188
+ // downstream truthiness check can't see an empty array as "has a
189
+ // public domain".
190
+ ...(hosts.length > 0 ? { publicDomain: [...hosts] } : {}),
191
+ },
192
+ });
193
+ }
194
+ if (services.length === 0) return [];
195
+ const trustedHeaderKeys = state.trustedHeaders?.keys ?? [];
196
+ return [{ appId, network: appNetwork(appId), services, trustedHeaderKeys }];
197
+ },
198
+ resolve(entries) {
199
+ // Sorted, because the world arrives in `contributions` Map insertion
200
+ // order and the fixpoint loop mutates that: a re-evaluated app is
201
+ // deleted and re-set, moving its entries to the end, so two renders
202
+ // of the same realm would emit different route order. `appId` is
203
+ // unique per entry, so this is a total order needing no tiebreak.
204
+ return [...entries].sort((a, b) => (a.appId < b.appId ? -1 : a.appId > b.appId ? 1 : 0));
205
+ },
206
+ extractNetworks(entry) {
207
+ return entry.network;
208
+ },
209
+ producerPredicate(svc) {
210
+ return svc.router != null;
211
+ },
212
+ },
213
+ {
214
+ name: "Routed apps",
215
+ phrase: "join routed app networks",
216
+ helpText:
217
+ "This app reads the list of all routed apps on this Worker and joins their networks to route traffic.",
218
+ icon: "ShareNetworkIcon",
219
+ danger: true,
220
+ // System-only: an aggregated view over every app on the worker.
221
+ // Widening later is additive where narrowing is not.
222
+ appKinds: "system",
223
+ },
224
+ );
225
+
226
+ /**
227
+ * Read every approved host port across apps on this worker.
228
+ *
229
+ * Home aggregates every approved `host.port()` and passes the port
230
+ * numbers in. Reactive: when port approvals change anywhere on the
231
+ * worker, this app re-evaluates and redeploys if the list shifts.
232
+ *
233
+ * Only a system app (`createSystemApp`) may declare this. It aggregates
234
+ * across every app on the worker, which is fleet-infrastructure work.
235
+ *
236
+ * @see https://yourrealm.eu/docs/realm-ts/peers/host-ports/
237
+ */
238
+ export const hostPorts = peerable(
239
+ "peers.hostPorts",
240
+ z.literal(true).default(true),
241
+ {
242
+ extract(_appId, state): HostPortEntry[] {
243
+ const out: HostPortEntry[] = [];
244
+ for (const svc of Object.values(state.services)) {
245
+ for (const hp of svc.hostPorts ?? []) out.push({ port: hp.hostPort });
246
+ }
247
+ return out;
248
+ },
249
+ resolve(entries) {
250
+ return entries;
251
+ },
252
+ },
253
+ {
254
+ name: "Host ports",
255
+ phrase: "read host port allocations",
256
+ helpText: "This app reads the list of all approved host ports across apps on this Worker.",
257
+ icon: "ShareNetworkIcon",
258
+ // System-only: same aggregated shape as routedApps.
259
+ appKinds: "system",
260
+ },
261
+ );
262
+
263
+ /**
264
+ * Read every backup target across apps on this worker.
265
+ *
266
+ * Home aggregates volume metadata from every app with a backup
267
+ * strategy and passes in a list of targets - app ID, service key, host
268
+ * path, and the strategy itself. Network-based strategies (pg-dump,
269
+ * valkey-save, mysql-dump) carry a connection URL and network handle.
270
+ * Reactive: re-evaluates when apps are installed or removed.
271
+ *
272
+ * Only a system app (`createSystemApp`) may declare this. It aggregates
273
+ * across every app on the worker, which is fleet-infrastructure work.
274
+ *
275
+ * @see https://yourrealm.eu/docs/realm-ts/peers/backup-volumes/
276
+ */
277
+ export const backupVolumes = peerable(
278
+ "peers.backupVolumes",
279
+ z.literal(true).default(true),
280
+ {
281
+ extract(appId, state): BackupTarget[] {
282
+ const out: BackupTarget[] = [];
283
+ for (const [serviceKey, svc] of Object.entries(state.services)) {
284
+ for (const vol of svc.volumes ?? []) {
285
+ if (!vol.backup) continue;
286
+ const scopedPath = vol.hostPath.startsWith("./")
287
+ ? `./apps/${appId}/${vol.hostPath.slice(2)}`
288
+ : vol.hostPath;
289
+ const isNetwork = "url" in vol.backup;
290
+ const rewritten = rewriteSelfDerivedToShared(vol.backup, appId, serviceKey) as Record<
291
+ string,
292
+ unknown
293
+ >;
294
+ const strategy = isNetwork
295
+ ? ({ ...rewritten, network: appNetwork(appId) } as BackupStrategy)
296
+ : (rewritten as BackupStrategy);
297
+ out.push({
298
+ appId,
299
+ serviceKey,
300
+ hostPath: scopedPath as PeerHostPath,
301
+ strategy,
302
+ });
303
+ }
304
+ }
305
+ return out;
306
+ },
307
+ resolve(entries) {
308
+ return entries;
309
+ },
310
+ extractNetworks(entry) {
311
+ return isNetworkStrategy(entry.strategy) ? entry.strategy.network : undefined;
312
+ },
313
+ extractPeerVolumes(entry) {
314
+ return isNetworkStrategy(entry.strategy) ? undefined : entry.hostPath;
315
+ },
316
+ extractAllowedProvider(entry): AllowedProvider {
317
+ return { appId: entry.appId, serviceKey: entry.serviceKey };
318
+ },
319
+ producerPredicate(svc) {
320
+ return svc.volumes?.some((v) => !!v.backup) ?? false;
321
+ },
322
+ },
323
+ {
324
+ name: "Backup volumes",
325
+ phrase: "mount peer volumes for backup",
326
+ helpText:
327
+ "This app reads the list of all volumes with backup strategies and mounts them for snapshotting.",
328
+ icon: "ShareNetworkIcon",
329
+ danger: true,
330
+ // System-only: same aggregated shape as routedApps.
331
+ appKinds: "system",
332
+ },
333
+ );
334
+
335
+ /**
336
+ * Read the build recipe of every build-from-source service in the realm.
337
+ *
338
+ * Home resolves each `build:` service's recipe (dockerfile / context /
339
+ * buildArgs, with `build: true` normalized to `{}`) and passes the list in,
340
+ * keyed by `(appId, service)`. Forge materializes it as a watched file and
341
+ * looks the recipe up when a push triggers a build, so the image is built with
342
+ * the author's recipe instead of the root-Dockerfile default. Reactive:
343
+ * re-evaluates when a build app is installed or removed. No networks — Forge
344
+ * clones source and builds in a disposable box; it never joins the targets.
345
+ *
346
+ * Realm-wide, unlike every other peer permission: the list covers every Worker
347
+ * in the Realm, not just the one the builder runs on. A Realm has a single
348
+ * builder, so a per-Worker list would mean only the box it sits on could host
349
+ * build-from-source apps at all. Safe because an entry is only ever read — it
350
+ * carries no network to join, no host path to mount and no startup ordering,
351
+ * so one from another Worker is the same inert data as one from your own.
352
+ *
353
+ * Only a single app (`createApp`) may declare this. Its consumer is the
354
+ * builder, and a realm has one builder.
355
+ *
356
+ * @see https://yourrealm.eu/docs/realm-ts/peers/build-apps/
357
+ */
358
+ export const buildApps = peerable(
359
+ "peers.buildApps",
360
+ z.literal(true).default(true),
361
+ // `extract` + `resolve` and NOTHING else, deliberately. Home unions this
362
+ // kind's entries across workers (`isBuildAppEntry` in peer-world.ts is the
363
+ // filter), which is only sound while an entry is inert data anywhere in the
364
+ // realm. Each of the four hooks omitted here would turn one into something
365
+ // that exists on exactly one box, leaving a consumer on another worker with a
366
+ // dangling reference:
367
+ // - extractNetworks: a docker network on that box to join
368
+ // - extractPeerVolumes: a host path on that box to mount
369
+ // - extractAllowedProvider: a `shared` ref you may dereference
370
+ // - producerPredicate: a depends_on edge, which also fails the worker's
371
+ // dependsOn validation because a pinless build target is off the wire
372
+ // Adding any of the four gives up realm scope — peer-world.test.ts fails.
373
+ {
374
+ extract(appId, state): BuildAppEntry[] {
375
+ const out: BuildAppEntry[] = [];
376
+ for (const [service, svc] of Object.entries(state.services)) {
377
+ if (svc.build != null && svc.image == null) {
378
+ out.push({ appId, service, recipe: svc.build });
379
+ }
380
+ }
381
+ return out;
382
+ },
383
+ resolve(entries) {
384
+ return entries;
385
+ },
386
+ },
387
+ {
388
+ name: "Build apps",
389
+ phrase: "read the recipes of build apps",
390
+ helpText:
391
+ "This app reads the build recipe of every build-from-source service across every app in your Realm, on any Worker.",
392
+ icon: "ShareNetworkIcon",
393
+ danger: true,
394
+ // Single-only: the one consumer is forge, and the builder is a
395
+ // singleton — enforced on `home.apiAccess` by builderScopeIssue,
396
+ // not here. Aggregated like the three above; the restriction is
397
+ // about who reads it, not what it holds.
398
+ appKinds: "single",
399
+ },
400
+ );
@@ -0,0 +1,116 @@
1
+ // The public-domain value type, its brand, and the hostname schema both
2
+ // sides of the boundary parse against.
3
+ //
4
+ // A leaf module holding only `zod` on purpose: `service-definition.ts`,
5
+ // `resolve.ts`, `schema/peer.ts` and `schema/home.ts` all need these, and
6
+ // putting them in `schema/home.ts` would form an import cycle with
7
+ // `resolve.ts`. `normalizeHost()` stays private: everything that needs it gets
8
+ // it by parsing through a schema that normalizes on the way in - the
9
+ // `domain_claim` key through `publicDomainValueSchema`, the install form's
10
+ // domain editor through `publicDomainHostSchema`.
11
+ //
12
+ // QuickJS-safe: only `z` + pure constants at module load.
13
+
14
+ import { z } from "zod";
15
+
16
+ /**
17
+ * A hostname the operator brought and granted to one app.
18
+ *
19
+ * Branded so `router.publicDomain` can only be fed the resolved value of a
20
+ * `r.home.publicDomain()` grant - a bare `"example.com"` doesn't typecheck.
21
+ * That is what makes the permission the sole door onto a public route.
22
+ * Erased at runtime, so the real gate is `iterateUses` + `checkAppGrants`
23
+ * (see `schema/home.ts`); the brand is DX.
24
+ */
25
+ export type PublicDomain = string & { readonly __publicDomain: unique symbol };
26
+
27
+ /**
28
+ * What to do when the operator granted more than one host. `"redirect"`
29
+ * (the default) serves the first and 301s the rest to it; `"all"` serves
30
+ * the app at every granted host. Operator state on the grant, not an
31
+ * author decision - which is why it travels inside the value rather than
32
+ * sitting beside it on the router.
33
+ */
34
+ export type DomainMode = "redirect" | "all";
35
+
36
+ /**
37
+ * What `r.home.publicDomain()` resolves to. Ordered, with `hosts[0]`
38
+ * canonical - there is no separate `primary` field, because storing the
39
+ * canonical host twice is how the two drift. `hosts: []` when the operator
40
+ * enabled the permission but supplied no domain, which must emit no route.
41
+ */
42
+ export interface PublicDomainValue {
43
+ readonly hosts: readonly PublicDomain[];
44
+ readonly domainMode: DomainMode;
45
+ }
46
+
47
+ // One DNS label: 1-63 characters, alphanumeric, interior hyphens only.
48
+ // Assembled from a string so the two-label minimum below stays readable.
49
+ const LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
50
+ const HOSTNAME = new RegExp(`^${LABEL}(?:\\.${LABEL})+$`);
51
+
52
+ // Lowercase + trailing dot stripped. Both matter beyond tidiness: the
53
+ // baseDomain reservation in `checkPublicDomains` is a suffix comparison, so
54
+ // `Photos.EXAMPLE.com` or `photos.example.com.` would slip past it otherwise,
55
+ // and running every claim through this is what makes `Photos.example.com` and
56
+ // `photos.example.com` one `domain_claim` row.
57
+ function normalizeHost(host: string): string {
58
+ return host.trim().replace(/\.$/, "").toLowerCase();
59
+ }
60
+
61
+ /**
62
+ * One granted hostname. Normalized first, then constrained to the DNS
63
+ * charset - this is the gate the Caddy config builder inherits, and the
64
+ * reason a host can carry no quote, backtick, brace or whitespace into a
65
+ * route matcher.
66
+ *
67
+ * Exported for the install form's domain editor, which parses each typed host
68
+ * against this exact schema rather than a second hostname regex: the operator
69
+ * reads the server's own message, and the row that lands is already in the
70
+ * normalized form `domain_claim`'s primary key compares on.
71
+ */
72
+ export const publicDomainHostSchema = z
73
+ .string()
74
+ .transform(normalizeHost)
75
+ .refine((h) => h.length <= 253 && HOSTNAME.test(h), {
76
+ message:
77
+ "must be a hostname of at least two labels using letters, digits and hyphens (e.g. photos.example.com)",
78
+ });
79
+
80
+ /**
81
+ * First occurrence wins, order preserved - so `hosts[0]` stays canonical.
82
+ *
83
+ * Run after normalization, which is what makes `Photos.example.com` and
84
+ * `photos.example.com` one entry rather than two.
85
+ */
86
+ export function dedupeHosts(hosts: string[]): string[] {
87
+ const seen = new Set<string>();
88
+ const out: string[] = [];
89
+ for (const host of hosts) {
90
+ if (seen.has(host)) continue;
91
+ seen.add(host);
92
+ out.push(host);
93
+ }
94
+ return out;
95
+ }
96
+
97
+ /**
98
+ * The stored/wire shape of the value: plain strings, before branding.
99
+ * Parsed in two places - out of the sandbox (`resolve.ts`'s router schema)
100
+ * and at grant write (`sanitizeGrants`, via the kind's `grantSchema`) - so
101
+ * neither an author's packed bundle nor an operator's form post can put an
102
+ * unconstrained string on a route.
103
+ *
104
+ * The dedupe is here, not only in the form, because `syncDomainClaims`
105
+ * deliberately tolerates the same host twice under one leaf (the table is a
106
+ * set) - so a raw PATCH could otherwise store `["a.com", "a.com"]`, and
107
+ * `pushRoutes` would emit a 301 from the primary to itself. Dead config rather
108
+ * than a loop, since the proxy route is pushed first and is terminal, but it
109
+ * has no reason to exist.
110
+ */
111
+ export const publicDomainValueSchema = z.object({
112
+ hosts: z.array(publicDomainHostSchema).transform(dedupeHosts),
113
+ domainMode: z.enum(["redirect", "all"]),
114
+ });
115
+
116
+ export type PublicDomainGrantData = z.infer<typeof publicDomainValueSchema>;
@@ -0,0 +1,74 @@
1
+ // remote.* permission factories.
2
+ //
3
+ // `fetchUrl` carries a live Zod schema for response validation, attached
4
+ // non-enumerably so JSON.stringify drops it on the QuickJS boundary while
5
+ // it stays reachable inside the sandbox. The type-level gate
6
+ // (CompleteSchemablePermission) makes `.schema()` mandatory — without it a
7
+ // fetchUrl won't satisfy `Leaf` and fails to typecheck in `configSchema`.
8
+
9
+ import { z } from "zod";
10
+ import { basePermission, schemablePermission } from "../factory.ts";
11
+ import { makeRemotable } from "../remote-registry.ts";
12
+
13
+ const remotable = makeRemotable(basePermission);
14
+ const remotableSchemable = makeRemotable(schemablePermission);
15
+
16
+ /**
17
+ * Branded string returned by `remote.artifact` — authors land it in
18
+ * `ServiceDefinition.files` to fill a file with the downloaded artifact's
19
+ * contents. Branded to keep artifact values from leaking into unrelated
20
+ * string slots without an explicit cast.
21
+ */
22
+ export type Artifact = string & { readonly __brand: "Artifact" };
23
+
24
+ /**
25
+ * Fetch and validate external data at eval time.
26
+ *
27
+ * Home makes the HTTP request, validates the response against the
28
+ * Zod schema you provide via `.schema(z)`, and passes the parsed data
29
+ * into `getDesiredState`. The app never opens a socket itself.
30
+ *
31
+ * @see https://yourrealm.eu/docs/realm-ts/remote/fetch-url/
32
+ */
33
+ // `url` is plain z.string() rather than z.url(): the schema runs inside
34
+ // QuickJS at module-load time, where Zod's URL regex doesn't behave the same
35
+ // as on the host. Home validates at fetch time via resolveAndValidateUrl
36
+ // (SSRF + scheme + DNS pinning).
37
+ export const fetchUrl = remotableSchemable(
38
+ "remote.fetchUrl",
39
+ z.object({
40
+ url: z.string(),
41
+ maxSizeMb: z.number().positive().optional(),
42
+ }),
43
+ {
44
+ name: "Fetch URL",
45
+ phrase: "fetch a remote URL",
46
+ helpText:
47
+ "Home fetches this URL on behalf of the app and validates the response. The app itself never makes network requests.",
48
+ icon: "CloudArrowDownIcon",
49
+ appKinds: "both",
50
+ },
51
+ );
52
+
53
+ /**
54
+ * Download a content-addressed file at eval time.
55
+ *
56
+ * Home fetches the file, caches it by SHA-256 digest, and exposes it
57
+ * as a path the app can mount via `files`. The digest is required for
58
+ * integrity - Home re-hashes after download and refuses to deploy on
59
+ * a mismatch.
60
+ *
61
+ * @see https://yourrealm.eu/docs/realm-ts/remote/artifact/
62
+ */
63
+ export const artifact = remotable(
64
+ "remote.artifact",
65
+ z.object({ url: z.string(), digest: z.string() }),
66
+ {
67
+ name: "Download artifact",
68
+ phrase: "download a remote artifact",
69
+ helpText:
70
+ "Home downloads this file, verifies its SHA-256 hash, and caches it. The app gets the verified file at deploy time.",
71
+ icon: "CloudArrowDownIcon",
72
+ appKinds: "both",
73
+ },
74
+ );