@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,26 @@
|
|
|
1
|
+
Realm SDK Licensing Exception
|
|
2
|
+
|
|
3
|
+
SPDX-License-Identifier: AGPL-3.0-only WITH Classpath-exception-2.0
|
|
4
|
+
|
|
5
|
+
The Realm SDK (@yourrealm/sdk) is licensed under the GNU Affero General
|
|
6
|
+
Public License, version 3 (see the LICENSE file at the repository root),
|
|
7
|
+
with the following exception. The exception text is the GNU Classpath
|
|
8
|
+
Exception 2.0, with its reference to the covering license retargeted to
|
|
9
|
+
the GNU Affero General Public License version 3:
|
|
10
|
+
|
|
11
|
+
Linking this library statically or dynamically with other modules is
|
|
12
|
+
making a combined work based on this library. Thus, the terms and
|
|
13
|
+
conditions of the GNU Affero General Public License version 3 cover the
|
|
14
|
+
whole combination.
|
|
15
|
+
|
|
16
|
+
As a special exception, the copyright holders of this library give you
|
|
17
|
+
permission to link this library with independent modules to produce an
|
|
18
|
+
executable, regardless of the license terms of these independent
|
|
19
|
+
modules, and to copy and distribute the resulting executable under
|
|
20
|
+
terms of your choice, provided that you also meet, for each linked
|
|
21
|
+
independent module, the terms and conditions of the license of that
|
|
22
|
+
module. An independent module is a module which is not derived from or
|
|
23
|
+
based on this library. If you modify this library, you may extend this
|
|
24
|
+
exception to your version of the library, but you are not obligated to
|
|
25
|
+
do so. If you do not wish to do so, delete this exception statement
|
|
26
|
+
from your version.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @yourrealm/sdk
|
|
2
|
+
|
|
3
|
+
The Realm SDK: the authoring surface for Realm apps (`createApp`, `r`, the
|
|
4
|
+
service classes), the `ui` surface, the `realm-sdk` CLI and the test harness.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
Every push to Realm's `main` publishes the SDK under the `alpha` dist-tag,
|
|
9
|
+
versioned by commit. Pin the tag and move it when you want to:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @yourrealm/sdk@alpha
|
|
13
|
+
pnpm update @yourrealm/sdk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The runtime is Deno: `realm-sdk test` runs `Deno.test` underneath, and the
|
|
17
|
+
package ships its TypeScript source as the `types` condition, so `deno check`
|
|
18
|
+
sees the real signatures. Set `nodeModulesDir` to `"manual"` in `deno.json`
|
|
19
|
+
and let pnpm own `node_modules`.
|
|
20
|
+
|
|
21
|
+
## License
|
|
22
|
+
|
|
23
|
+
`AGPL-3.0-only WITH Classpath-exception-2.0`.
|
|
24
|
+
|
|
25
|
+
Realm's platform (Home, Worker) is licensed under the GNU Affero General
|
|
26
|
+
Public License v3. This SDK adds a linking exception, based on the GNU
|
|
27
|
+
Classpath Exception 2.0, so that authoring a Realm app (`realm.ts`) that
|
|
28
|
+
imports this SDK does not, by itself, place your app under the AGPL. The
|
|
29
|
+
exception frees users of the SDK; it does not extend to modified versions
|
|
30
|
+
of the SDK itself, which remain AGPL.
|
|
31
|
+
|
|
32
|
+
See [LICENSE-EXCEPTION](./LICENSE-EXCEPTION) for the full text and the
|
|
33
|
+
repository's [LICENSE](https://github.com/benjick/holden/blob/main/LICENSE) for
|
|
34
|
+
the AGPL.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/service-url.ts
|
|
2
|
+
function structuredHost(appId, serviceId, slug, baseDomain) {
|
|
3
|
+
return `${appId}-${serviceId}.${slug}.${baseDomain}`;
|
|
4
|
+
}
|
|
5
|
+
function serviceUrl(args) {
|
|
6
|
+
if (!args.hasRouter) return null;
|
|
7
|
+
if (args.publicDomain) return `https://${args.publicDomain}`;
|
|
8
|
+
if (!args.slug || !args.baseDomain) return null;
|
|
9
|
+
return `https://${structuredHost(args.appId, args.serviceId, args.slug, args.baseDomain)}`;
|
|
10
|
+
}
|
|
11
|
+
function firstPublicDomain(publicDomain) {
|
|
12
|
+
if (publicDomain == null || typeof publicDomain !== "object") return void 0;
|
|
13
|
+
const hosts = publicDomain.hosts;
|
|
14
|
+
if (Array.isArray(hosts) && typeof hosts[0] === "string") return hosts[0];
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
structuredHost,
|
|
20
|
+
serviceUrl,
|
|
21
|
+
firstPublicDomain
|
|
22
|
+
};
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SPECULATIVE_SILO,
|
|
3
|
+
hostKey,
|
|
4
|
+
nameSchema
|
|
5
|
+
} from "./chunk-VHTUSZUX.js";
|
|
6
|
+
|
|
7
|
+
// src/worker-protocol/worker-state.ts
|
|
8
|
+
import { z as z2 } from "zod";
|
|
9
|
+
|
|
10
|
+
// src/worker-protocol/ingress.ts
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
var acmeIssuerSchema = z.object({
|
|
13
|
+
serverUrl: z.string(),
|
|
14
|
+
username: z.string(),
|
|
15
|
+
apiKey: z.string(),
|
|
16
|
+
ca: z.string(),
|
|
17
|
+
caRootPem: z.string()
|
|
18
|
+
});
|
|
19
|
+
var ingressRouterSchema = z.object({
|
|
20
|
+
containerPort: z.number().int().min(1).max(65535),
|
|
21
|
+
/** Operator-granted hostnames for the additional ungated public route. */
|
|
22
|
+
publicDomain: z.union([z.string(), z.array(z.string())]).optional(),
|
|
23
|
+
/** With several granted hosts: serve them all, or serve one and 301 the rest. */
|
|
24
|
+
domainMode: z.enum(["redirect", "all"]),
|
|
25
|
+
pathPrefix: z.string().regex(/^\/[a-zA-Z0-9/_-]*$/, "pathPrefix must start with / and contain only [a-zA-Z0-9/_-]").optional(),
|
|
26
|
+
compress: z.boolean(),
|
|
27
|
+
maxRequestSizeMb: z.number().nullable(),
|
|
28
|
+
rateLimit: z.union([
|
|
29
|
+
z.number(),
|
|
30
|
+
z.object({
|
|
31
|
+
requests: z.number(),
|
|
32
|
+
window: z.string().regex(/^\d+(ms|s|m|h)$/, "window must be a duration like 10s, 1m, 1h")
|
|
33
|
+
})
|
|
34
|
+
]).nullable(),
|
|
35
|
+
/** Response-header overrides; `null` deletes the renderer's default. */
|
|
36
|
+
headers: z.record(z.string(), z.string().nullable()).optional()
|
|
37
|
+
});
|
|
38
|
+
var ingressRoutedAppSchema = z.object({
|
|
39
|
+
appId: z.string(),
|
|
40
|
+
services: z.array(z.object({ serviceId: z.string(), router: ingressRouterSchema })),
|
|
41
|
+
/**
|
|
42
|
+
* Header-brokering names the app declared via `getTrustedHeaders`. The
|
|
43
|
+
* renderer injects these on the authenticated route (lifted off the
|
|
44
|
+
* forward-auth response) and strips them everywhere else, so identity can't
|
|
45
|
+
* be spoofed.
|
|
46
|
+
*/
|
|
47
|
+
trustedHeaderKeys: z.array(z.string()).optional()
|
|
48
|
+
});
|
|
49
|
+
var ingressInputSchema = z.object({
|
|
50
|
+
/** Every routed service on this worker, ordered canonically by `appId`. */
|
|
51
|
+
routedApps: z.array(ingressRoutedAppSchema),
|
|
52
|
+
/**
|
|
53
|
+
* The Home dashboard's own host, `{homeSlug}.{workerId}.{baseDomain}` - one
|
|
54
|
+
* label under the worker wildcard, which a renderer has to match ahead of
|
|
55
|
+
* that wildcard so the not-routed holding page never shadows it.
|
|
56
|
+
*
|
|
57
|
+
* Null on a box that does not run Home. The name resolves realm-wide (it
|
|
58
|
+
* sits under the realm's wildcard), so emitting the route on a worker-only
|
|
59
|
+
* box would answer a hit there with a page claiming the control plane is
|
|
60
|
+
* down - and, under BYO public providers, cost a real per-host certificate
|
|
61
|
+
* on every worker. Home knows which box is which from `home_state
|
|
62
|
+
* .home_worker_id`.
|
|
63
|
+
*/
|
|
64
|
+
dashboardHost: z.string().nullable(),
|
|
65
|
+
/**
|
|
66
|
+
* This worker's short slug: the `{workerId}` level of every app subdomain,
|
|
67
|
+
* and the label it owns under acme-dns.
|
|
68
|
+
*/
|
|
69
|
+
workerId: z.string(),
|
|
70
|
+
/**
|
|
71
|
+
* The realm's domain base (`{slug}.yourrealm.me`, or the operator's own).
|
|
72
|
+
* With `workerId` it forms every structured authenticated subdomain. Empty
|
|
73
|
+
* on the placeholder identity, before an install is configured.
|
|
74
|
+
*/
|
|
75
|
+
baseDomain: z.string(),
|
|
76
|
+
/** DNS-01 inputs for the worker wildcard, or null (dev / e2e / first boot). */
|
|
77
|
+
acmeDns: acmeIssuerSchema.extend({ baseDomain: z.string() }).nullable(),
|
|
78
|
+
/**
|
|
79
|
+
* BYO public-DNS providers, in issue order. `apiToken` is a plain string:
|
|
80
|
+
* a `Ref` cannot ride the wire, and by this point Home has resolved it.
|
|
81
|
+
*/
|
|
82
|
+
publicProviders: z.array(
|
|
83
|
+
z.object({ name: z.enum(["cloudflare", "hetzner"]), apiToken: z.string() })
|
|
84
|
+
),
|
|
85
|
+
/**
|
|
86
|
+
* CDNs in front of this realm, whose edge addresses ingress trusts when
|
|
87
|
+
* resolving the real client IP. The addresses themselves are not here - the
|
|
88
|
+
* renderer's runtime fetches and refreshes them.
|
|
89
|
+
*/
|
|
90
|
+
cdnProviders: z.array(z.string()),
|
|
91
|
+
/**
|
|
92
|
+
* The signing-certificate fingerprints in the Digital Asset Links statement
|
|
93
|
+
* the ingress serves on every host - the statement's
|
|
94
|
+
* `sha256_cert_fingerprints`, which Chrome matches the companion APK's own
|
|
95
|
+
* signature against before it drops the Trusted Web Activity's toolbar.
|
|
96
|
+
*
|
|
97
|
+
* Operator-set rather than a constant, because which certificate signs the
|
|
98
|
+
* installed APK is a property of how that operator got it: Play App Signing
|
|
99
|
+
* re-signs with Google's key, direct distribution keeps the release
|
|
100
|
+
* keystore's, and a locally built debug APK carries a third.
|
|
101
|
+
*
|
|
102
|
+
* The shape is pinned here because the renderer inlines these strings into a
|
|
103
|
+
* JSON body Caddy's `ReplaceKnown` walks: uppercase hex bytes and colons can
|
|
104
|
+
* never form a `{placeholder}`, so a value that parses cannot reach into the
|
|
105
|
+
* replacer.
|
|
106
|
+
*/
|
|
107
|
+
companionFingerprints: z.array(
|
|
108
|
+
z.string().regex(/^[0-9A-F]{2}(:[0-9A-F]{2}){31}$/, "expected 32 colon-separated uppercase hex bytes")
|
|
109
|
+
)
|
|
110
|
+
});
|
|
111
|
+
var PLATFORM_INGRESS_PORTS = [80, 443];
|
|
112
|
+
|
|
113
|
+
// src/worker-protocol/worker-state.ts
|
|
114
|
+
var dependsOnEdgeSchema = z2.object({
|
|
115
|
+
providerAppId: z2.string(),
|
|
116
|
+
providerServiceName: z2.string(),
|
|
117
|
+
condition: z2.enum(["started", "healthy"])
|
|
118
|
+
});
|
|
119
|
+
var duration = z2.number().int().nonnegative();
|
|
120
|
+
var containerPort = z2.number().int().min(1).max(65535);
|
|
121
|
+
var healthcheckSchema = z2.object({
|
|
122
|
+
test: z2.array(z2.string()),
|
|
123
|
+
interval: duration,
|
|
124
|
+
timeout: duration,
|
|
125
|
+
retries: z2.number().int().nonnegative(),
|
|
126
|
+
startPeriod: duration
|
|
127
|
+
});
|
|
128
|
+
var hostPortSchema = z2.object({
|
|
129
|
+
containerPort,
|
|
130
|
+
hostPort: z2.number().int().min(1).max(65535),
|
|
131
|
+
protocol: z2.enum(["tcp", "udp"]).optional()
|
|
132
|
+
});
|
|
133
|
+
var backupStrategySchema = z2.discriminatedUnion("kind", [
|
|
134
|
+
z2.object({ kind: z2.literal("file-copy") }),
|
|
135
|
+
z2.object({ kind: z2.literal("sqlite"), file: z2.string() }),
|
|
136
|
+
z2.object({ kind: z2.literal("pg-dump"), url: z2.string(), version: z2.number().int().positive() }),
|
|
137
|
+
z2.object({ kind: z2.literal("valkey-save"), url: z2.string() }),
|
|
138
|
+
z2.object({
|
|
139
|
+
kind: z2.literal("mysql-dump"),
|
|
140
|
+
url: z2.string(),
|
|
141
|
+
version: z2.number().int().positive()
|
|
142
|
+
})
|
|
143
|
+
]);
|
|
144
|
+
var volumeSchema = z2.object({
|
|
145
|
+
// Refused by name: the wire is where "this was fully resolved" stops being a
|
|
146
|
+
// promise and becomes a check. A speculative eval fabricates a Silo so an app
|
|
147
|
+
// can be previewed before the operator picks one (`speculative-silo.ts`), and
|
|
148
|
+
// that placeholder is otherwise an ordinary absolute path - shipping it binds
|
|
149
|
+
// an empty directory in place of the operator's library, silently. `evalApps`
|
|
150
|
+
// refuses that combination up front; this is the half that cannot be bypassed.
|
|
151
|
+
hostPath: z2.string().refine((p) => p !== SPECULATIVE_SILO.hostPath, {
|
|
152
|
+
message: "the speculative Silo placeholder must never reach the wire"
|
|
153
|
+
}),
|
|
154
|
+
containerPath: z2.string(),
|
|
155
|
+
// The Silo group that owns this directory. The renderer turns it into
|
|
156
|
+
// `group_add` on every service that mounts the volume, so directory
|
|
157
|
+
// preparation and group membership are driven by the same field and can
|
|
158
|
+
// never disagree.
|
|
159
|
+
gid: z2.number().int().positive().optional(),
|
|
160
|
+
readOnly: z2.boolean().optional(),
|
|
161
|
+
backup: z2.union([backupStrategySchema, z2.literal(false)]).optional()
|
|
162
|
+
});
|
|
163
|
+
var wireServiceSchema = z2.object({
|
|
164
|
+
image: z2.string().min(1),
|
|
165
|
+
hostPorts: z2.array(hostPortSchema).optional(),
|
|
166
|
+
env: z2.record(z2.string(), z2.union([z2.string(), z2.number()])).optional(),
|
|
167
|
+
// containerPath -> 64-char hex digest the renderer resolves to bytes. The two
|
|
168
|
+
// sources hash different things: artifact refs collapse to their bare content
|
|
169
|
+
// sha, while inline content is keyed by *identity* — sha256 of
|
|
170
|
+
// `appId:serviceKey:fileKey` (plaintext rides in `WorkerApp.inlineFiles`). An
|
|
171
|
+
// inline file therefore keeps the same digest and worker path across content
|
|
172
|
+
// changes, so `--watch` consumers reload without recreating the container.
|
|
173
|
+
files: z2.record(z2.string().startsWith("/"), z2.string().regex(/^[a-f0-9]{64}$/)).optional(),
|
|
174
|
+
command: z2.array(z2.string()).optional(),
|
|
175
|
+
volumes: z2.array(volumeSchema).optional(),
|
|
176
|
+
gpu: z2.boolean().optional(),
|
|
177
|
+
iptables: z2.boolean().optional(),
|
|
178
|
+
joinNetworks: z2.array(z2.string()).optional(),
|
|
179
|
+
healthcheck: healthcheckSchema.optional(),
|
|
180
|
+
memory: z2.string().optional(),
|
|
181
|
+
cpu: z2.number().positive().max(128).optional(),
|
|
182
|
+
// Defaulted to 1 upstream, so always present on the wire.
|
|
183
|
+
replicas: z2.number().int().min(1).max(100),
|
|
184
|
+
labels: z2.array(z2.string()).optional(),
|
|
185
|
+
shmSizeMb: z2.number().int().positive().optional()
|
|
186
|
+
});
|
|
187
|
+
var workerAppSchema = z2.object({
|
|
188
|
+
appId: z2.string(),
|
|
189
|
+
// Flattened: in the resolve model these live under `.state` (matching
|
|
190
|
+
// `getDesiredState`'s shape), but on the wire only these two of that
|
|
191
|
+
// object's fields survive, so they're hoisted onto the app directly.
|
|
192
|
+
services: z2.record(nameSchema, wireServiceSchema),
|
|
193
|
+
/** `true` makes the per-app network `internal: true` (no egress). */
|
|
194
|
+
egress: z2.literal(true).optional(),
|
|
195
|
+
/** Worker-owned external nets this app's services join (e.g. `management-net`). */
|
|
196
|
+
externalNetworks: z2.array(z2.string()),
|
|
197
|
+
/**
|
|
198
|
+
* Service-name (unprefixed) -> producer edges. The renderer namespaces
|
|
199
|
+
* `providerServiceName` with `providerAppId` to form the final compose key.
|
|
200
|
+
*/
|
|
201
|
+
dependsOn: z2.record(z2.string(), z2.array(dependsOnEdgeSchema)),
|
|
202
|
+
/** Plaintext for inline `files` entries, keyed by their identity digest. */
|
|
203
|
+
inlineFiles: z2.record(z2.string(), z2.string()),
|
|
204
|
+
/**
|
|
205
|
+
* Absolute host paths exempted from the absolute-mount gate and
|
|
206
|
+
* bound verbatim by the renderer (peer backup volumes, the logs socket).
|
|
207
|
+
*/
|
|
208
|
+
allowedAbsoluteHostPaths: z2.array(z2.string()),
|
|
209
|
+
/**
|
|
210
|
+
* Host path the worker binds this app's `home-proxy.sock` at. Present only
|
|
211
|
+
* when the app holds a `home.apiAccess` grant.
|
|
212
|
+
*/
|
|
213
|
+
homeProxySocketPath: z2.string().optional(),
|
|
214
|
+
/**
|
|
215
|
+
* Host path the worker binds this app's spawn broker socket at. Present
|
|
216
|
+
* only when the app holds a `host.spawn` grant.
|
|
217
|
+
*/
|
|
218
|
+
spawnSocketPath: z2.string().optional(),
|
|
219
|
+
/**
|
|
220
|
+
* This app's registry credentials, keyed by registry host. The worker
|
|
221
|
+
* renders a per-app docker `config.json` from these so private images pull.
|
|
222
|
+
*/
|
|
223
|
+
registryAuth: z2.record(hostKey, z2.object({ username: z2.string(), password: z2.string() })).optional()
|
|
224
|
+
});
|
|
225
|
+
var workerStateSchema = z2.object({
|
|
226
|
+
apps: z2.array(workerAppSchema),
|
|
227
|
+
/** SHAs of every artifact the worker keeps in `files/` after a reconcile. */
|
|
228
|
+
artifactHashes: z2.array(z2.string()),
|
|
229
|
+
/** Docker networks the worker container itself joins this reconcile. */
|
|
230
|
+
workerNetworks: z2.array(z2.string()),
|
|
231
|
+
/**
|
|
232
|
+
* Where traffic goes on this box, as `apps` says what runs on it. Runtime-
|
|
233
|
+
* neutral: the Docker worker renders it with `buildCaddyConfig`
|
|
234
|
+
* (`runtime/docker/ingress`) exactly as it renders `apps` with
|
|
235
|
+
* `convertToDockerCompose`.
|
|
236
|
+
*
|
|
237
|
+
* Required, not optional. Every producer can state an honest value - an
|
|
238
|
+
* unconfigured box gets the empty-identity placeholder, which is a real
|
|
239
|
+
* answer ("no domain yet") rather than a missing one - and a box with no
|
|
240
|
+
* schedulable app still needs a front door, so absence must never be a legal
|
|
241
|
+
* way to say "no ingress here".
|
|
242
|
+
*/
|
|
243
|
+
ingress: ingressInputSchema
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// src/worker-protocol/node-version.ts
|
|
247
|
+
import { z as z3 } from "zod";
|
|
248
|
+
var nodeVersionSchema = z3.object({
|
|
249
|
+
// The pinned ref this container runs: `benjick/realm-worker@sha256:…` for a
|
|
250
|
+
// pulled image, a bare image ID for a locally-loaded one (see the installer's
|
|
251
|
+
// pin_image). This is the string an `applied` audit entry matches against.
|
|
252
|
+
imageRef: z3.string().nullable(),
|
|
253
|
+
// The commit the image was built from - a full SHA from the publish workflow,
|
|
254
|
+
// null for anything built outside it.
|
|
255
|
+
gitHash: z3.string().nullable(),
|
|
256
|
+
// ISO-8601 build timestamp, read back off the image's OCI `created` label by
|
|
257
|
+
// the publish workflow so the reported date and the image's own can't drift.
|
|
258
|
+
buildDate: z3.string().nullable()
|
|
259
|
+
});
|
|
260
|
+
function readEnv(name) {
|
|
261
|
+
const value = Deno.env.get(name);
|
|
262
|
+
return value === void 0 || value === "" ? null : value;
|
|
263
|
+
}
|
|
264
|
+
function readNodeVersion() {
|
|
265
|
+
return {
|
|
266
|
+
imageRef: readEnv("REALM_IMAGE_REF"),
|
|
267
|
+
gitHash: readEnv("REALM_GIT_HASH"),
|
|
268
|
+
buildDate: readEnv("REALM_BUILD_DATE")
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function shortNodeVersion(version) {
|
|
272
|
+
if (version.gitHash) return version.gitHash.slice(0, 7);
|
|
273
|
+
const digest = version.imageRef?.split("sha256:")[1];
|
|
274
|
+
return digest ? digest.slice(0, 12) : null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/worker-protocol/update-files.ts
|
|
278
|
+
var UPDATE_ERROR_MAX_CHARS = 2048;
|
|
279
|
+
var MAX_ERROR_FILE_BYTES = 16 * 1024;
|
|
280
|
+
async function readUpdateFiles(dir) {
|
|
281
|
+
const [pending, error] = await Promise.all([
|
|
282
|
+
isFile(`${dir}/.update`),
|
|
283
|
+
readError(`${dir}/.update.error`)
|
|
284
|
+
]);
|
|
285
|
+
return { pending, error };
|
|
286
|
+
}
|
|
287
|
+
async function isFile(path) {
|
|
288
|
+
try {
|
|
289
|
+
return (await Deno.stat(path)).isFile;
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function readError(path) {
|
|
295
|
+
let text;
|
|
296
|
+
try {
|
|
297
|
+
const stat = await Deno.stat(path);
|
|
298
|
+
if (!stat.isFile || stat.size > MAX_ERROR_FILE_BYTES) return null;
|
|
299
|
+
text = await Deno.readTextFile(path);
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const trimmed = text.trim();
|
|
304
|
+
if (trimmed === "") return null;
|
|
305
|
+
return trimmed.length > UPDATE_ERROR_MAX_CHARS ? trimmed.slice(-UPDATE_ERROR_MAX_CHARS) : trimmed;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/worker-protocol/app-dirs.ts
|
|
309
|
+
var REALM_APP_DIR_GID = 999999;
|
|
310
|
+
function isPreparedAppDir(appId, volume, exemptHostPaths) {
|
|
311
|
+
if (volume.gid !== void 0) return false;
|
|
312
|
+
if (exemptHostPaths.has(volume.hostPath)) return false;
|
|
313
|
+
if (!volume.hostPath.startsWith("./")) return false;
|
|
314
|
+
return scopeBindSource(volume.hostPath, appId) !== `./apps/${appId}`;
|
|
315
|
+
}
|
|
316
|
+
function scopeBindSource(source, appId) {
|
|
317
|
+
if (!source.startsWith("./")) return source;
|
|
318
|
+
const scoped = `./apps/${appId}/${source.slice(2)}`;
|
|
319
|
+
const segments = scoped.split("/");
|
|
320
|
+
const resolved = [];
|
|
321
|
+
for (const seg of segments) {
|
|
322
|
+
if (seg === "..") resolved.pop();
|
|
323
|
+
else if (seg !== "." && seg !== "") resolved.push(seg);
|
|
324
|
+
}
|
|
325
|
+
const normalized = "./" + resolved.join("/");
|
|
326
|
+
if (!normalized.startsWith(`./apps/${appId}/`) && normalized !== `./apps/${appId}`) {
|
|
327
|
+
throw new Error(`bind source escapes app directory: ${source}`);
|
|
328
|
+
}
|
|
329
|
+
return normalized;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/worker-protocol/silo-paths.ts
|
|
333
|
+
var REALM_SILO_DIR = "/opt/realm/worker/silo";
|
|
334
|
+
function managedSiloPath(id) {
|
|
335
|
+
return `${REALM_SILO_DIR}/${id}`;
|
|
336
|
+
}
|
|
337
|
+
function isManagedSiloPath(hostPath) {
|
|
338
|
+
return hostPath.startsWith(`${REALM_SILO_DIR}/`);
|
|
339
|
+
}
|
|
340
|
+
function shellQuote(path) {
|
|
341
|
+
return `'${path.replaceAll("'", "'\\''")}'`;
|
|
342
|
+
}
|
|
343
|
+
function siloSetupCommand(silo) {
|
|
344
|
+
const path = shellQuote(silo.hostPath);
|
|
345
|
+
return `chgrp -R ${silo.gid} ${path}
|
|
346
|
+
chmod g+rwxs ${path}`;
|
|
347
|
+
}
|
|
348
|
+
function siloWidenCommand(silo) {
|
|
349
|
+
return `find ${shellQuote(silo.hostPath)} -type d -exec chmod g+rwxs {} +`;
|
|
350
|
+
}
|
|
351
|
+
function siloReclaimCommand(orphan) {
|
|
352
|
+
return `ssh root@${orphan.workerIp}
|
|
353
|
+
rm -rf ${shellQuote(orphan.hostPath)}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export {
|
|
357
|
+
ingressInputSchema,
|
|
358
|
+
PLATFORM_INGRESS_PORTS,
|
|
359
|
+
dependsOnEdgeSchema,
|
|
360
|
+
wireServiceSchema,
|
|
361
|
+
workerAppSchema,
|
|
362
|
+
workerStateSchema,
|
|
363
|
+
nodeVersionSchema,
|
|
364
|
+
readNodeVersion,
|
|
365
|
+
shortNodeVersion,
|
|
366
|
+
readUpdateFiles,
|
|
367
|
+
REALM_APP_DIR_GID,
|
|
368
|
+
isPreparedAppDir,
|
|
369
|
+
scopeBindSource,
|
|
370
|
+
REALM_SILO_DIR,
|
|
371
|
+
managedSiloPath,
|
|
372
|
+
isManagedSiloPath,
|
|
373
|
+
siloSetupCommand,
|
|
374
|
+
siloWidenCommand,
|
|
375
|
+
siloReclaimCommand
|
|
376
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// src/ref.ts
|
|
2
|
+
var REF_SYMBOL = /* @__PURE__ */ Symbol.for("realm.ref");
|
|
3
|
+
var DEFERRED_SYMBOL = /* @__PURE__ */ Symbol.for("realm.deferred");
|
|
4
|
+
function isRef(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && REF_SYMBOL in value;
|
|
6
|
+
}
|
|
7
|
+
function containsRef(value) {
|
|
8
|
+
if (isRef(value) || isRefMarker(value)) return true;
|
|
9
|
+
if (Array.isArray(value)) return value.some(containsRef);
|
|
10
|
+
if (typeof value === "object" && value !== null) {
|
|
11
|
+
for (const v of Object.values(value)) {
|
|
12
|
+
if (containsRef(v)) return true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
function isRefMarker(value) {
|
|
18
|
+
return typeof value === "object" && value !== null && "__realmRef" in value && typeof value.__realmRef === "object";
|
|
19
|
+
}
|
|
20
|
+
function throwOnToString() {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"Refs cannot be stringified directly. Use interpolate`...` for templates or pass the ref through as an env/command/volume value."
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
function identityRef(field) {
|
|
26
|
+
return {
|
|
27
|
+
[REF_SYMBOL]: true,
|
|
28
|
+
toJSON: () => ({ __realmRef: { kind: "identity", field } }),
|
|
29
|
+
toString: throwOnToString
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function ref(marker) {
|
|
33
|
+
return {
|
|
34
|
+
[REF_SYMBOL]: true,
|
|
35
|
+
toJSON: () => ({ __realmRef: marker }),
|
|
36
|
+
toString: throwOnToString
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function stringRef(resolve) {
|
|
40
|
+
return {
|
|
41
|
+
[REF_SYMBOL]: true,
|
|
42
|
+
toJSON: resolve,
|
|
43
|
+
toString: throwOnToString
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function deferredRef(marker) {
|
|
47
|
+
return {
|
|
48
|
+
[DEFERRED_SYMBOL]: true,
|
|
49
|
+
toJSON: () => ({ __realmRef: marker }),
|
|
50
|
+
toString: throwOnToString
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function interpolate(strings, ...values) {
|
|
54
|
+
return {
|
|
55
|
+
[REF_SYMBOL]: true,
|
|
56
|
+
toJSON() {
|
|
57
|
+
const parts = [];
|
|
58
|
+
let allStrings = true;
|
|
59
|
+
for (let i = 0; i < strings.length; i++) {
|
|
60
|
+
const literal = strings[i];
|
|
61
|
+
if (literal.length > 0) parts.push(literal);
|
|
62
|
+
if (i < values.length) {
|
|
63
|
+
const val = values[i];
|
|
64
|
+
let marker = null;
|
|
65
|
+
if (isRef(val)) {
|
|
66
|
+
const resolved = val.toJSON();
|
|
67
|
+
if (typeof resolved === "object" && resolved !== null && "__realmRef" in resolved) {
|
|
68
|
+
marker = resolved.__realmRef;
|
|
69
|
+
} else {
|
|
70
|
+
parts.push(String(resolved));
|
|
71
|
+
}
|
|
72
|
+
} else if (isRefMarker(val)) {
|
|
73
|
+
const m = val.__realmRef;
|
|
74
|
+
if (m.kind === "deferred") {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"interpolate cannot accept a deferred ref; move affix logic into apply()"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
marker = m;
|
|
80
|
+
} else {
|
|
81
|
+
parts.push(String(val));
|
|
82
|
+
}
|
|
83
|
+
if (marker) {
|
|
84
|
+
if (marker.kind === "concat") {
|
|
85
|
+
for (const p of marker.parts) {
|
|
86
|
+
parts.push(p);
|
|
87
|
+
if (typeof p !== "string") allStrings = false;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
parts.push(marker);
|
|
91
|
+
allStrings = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (allStrings) return parts.join("");
|
|
97
|
+
return { __realmRef: { kind: "concat", parts } };
|
|
98
|
+
},
|
|
99
|
+
toString: throwOnToString
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/service.ts
|
|
104
|
+
var SELF_APP_ID = "__self__";
|
|
105
|
+
var Service = class {
|
|
106
|
+
_serviceKey;
|
|
107
|
+
/** Called by the framework after scanning the services map */
|
|
108
|
+
_setServiceKey(key) {
|
|
109
|
+
this._serviceKey = key;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A per-service derived secret. Within the same app, same-named secrets
|
|
113
|
+
* collide intentionally (Postgres.password and Valkey.password in the
|
|
114
|
+
* same app share a hash) - that's the ergonomic contract.
|
|
115
|
+
*/
|
|
116
|
+
derived(name) {
|
|
117
|
+
return ref({ kind: "derived", appId: SELF_APP_ID, name });
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* DNS name of this service within the per-app compose project. Resolves
|
|
121
|
+
* to a plain string at serialization time - `_setServiceKey` is
|
|
122
|
+
* guaranteed to have run before `getServiceDefinition`, which is the
|
|
123
|
+
* only time a `host` ref is serialized.
|
|
124
|
+
*/
|
|
125
|
+
get host() {
|
|
126
|
+
return stringRef(() => this._serviceKey ?? "__unresolved__");
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
REF_SYMBOL,
|
|
132
|
+
containsRef,
|
|
133
|
+
identityRef,
|
|
134
|
+
ref,
|
|
135
|
+
deferredRef,
|
|
136
|
+
interpolate,
|
|
137
|
+
SELF_APP_ID,
|
|
138
|
+
Service
|
|
139
|
+
};
|