@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,49 @@
|
|
|
1
|
+
// Identifier validation schemas + the Network brand.
|
|
2
|
+
//
|
|
3
|
+
// nameSchema / domainSchema gate strings that land in container names, network
|
|
4
|
+
// names, file paths, and host-match rules in the proxy config — places where a
|
|
5
|
+
// hostile string could close a quote/backtick or escape into a sibling path.
|
|
6
|
+
// The brand on Network keeps Docker-network handles from being confused with
|
|
7
|
+
// arbitrary strings as they flow between the SDK and Home.
|
|
8
|
+
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Valid service name, app ID, and realm.ts-authored identifier: lowercase
|
|
13
|
+
* alphanumeric + hyphens, must start with a letter. Stays tight because
|
|
14
|
+
* realm.ts authors choose these — we don't want a service named `123`
|
|
15
|
+
* or a realm.ts whose name is purely numeric, both of which read as
|
|
16
|
+
* suspicious / confusing in container names and config. App IDs follow
|
|
17
|
+
* the same shape: system apps use bare slugs (`logger`, `stats`, …) and
|
|
18
|
+
* user-installed apps get `${slug}-${nanoid}` minted from
|
|
19
|
+
* `createApp({ name })`.
|
|
20
|
+
*/
|
|
21
|
+
export const nameSchema = z
|
|
22
|
+
.string()
|
|
23
|
+
.regex(
|
|
24
|
+
/^[a-z][a-z0-9-]*$/,
|
|
25
|
+
"Must be lowercase alphanumeric with hyphens, starting with a letter",
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Valid domain name for routing rules. DNS charset only; rejects anything
|
|
30
|
+
* that could close a backtick-delimited host matcher and append a second
|
|
31
|
+
* rule.
|
|
32
|
+
*/
|
|
33
|
+
export const domainSchema = z.string().regex(/^[a-zA-Z0-9.*-]+$/, "Invalid domain name");
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Registry host key for the wire `registryAuth` map: hostname + optional
|
|
37
|
+
* port (`ghcr.io`, `registry-1.docker.io`, `registry.local:5000`). Mirrors
|
|
38
|
+
* the registry client's host pattern so a credential key can't smuggle a
|
|
39
|
+
* path, userinfo, scheme, or query into the place a host is expected.
|
|
40
|
+
*/
|
|
41
|
+
export const hostKey = z.string().regex(/^[a-zA-Z0-9.-]+(:[0-9]+)?$/, "Invalid registry host");
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Branded Docker network handle. Distinct from arbitrary strings so a
|
|
45
|
+
* raw network name can't be passed where a `joinNetworks` entry is
|
|
46
|
+
* expected (and vice versa).
|
|
47
|
+
*/
|
|
48
|
+
export const networkSchema = z.string().brand<"Network">();
|
|
49
|
+
export type Network = z.infer<typeof networkSchema>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Author-facing SDK barrel. Realm.ts apps import from here:
|
|
2
|
+
//
|
|
3
|
+
// import { createApp, r, z } from "@yourrealm/sdk";
|
|
4
|
+
// import { Postgres, Valkey } from "@yourrealm/sdk/services";
|
|
5
|
+
//
|
|
6
|
+
// Home-side primitives (sanitizeGrants, checkAppGrants, peer-world helpers,
|
|
7
|
+
// the SerializablePermission shape, etc.) live on `@yourrealm/sdk/home`.
|
|
8
|
+
// The bundler/sandbox/generator pipeline lives on `@yourrealm/sdk/compile`.
|
|
9
|
+
// Both Home paths are gated off this entry so author bundles stay lean and
|
|
10
|
+
// don't transitively pull esbuild or quickjs.
|
|
11
|
+
|
|
12
|
+
export { createApp, createSystemApp } from "./create-app.ts";
|
|
13
|
+
export type { User } from "./create-app.ts";
|
|
14
|
+
export { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// `r` namespace — author-facing factories.
|
|
17
|
+
import * as host from "./schema/host.ts";
|
|
18
|
+
import * as peers from "./schema/peer.ts";
|
|
19
|
+
import * as remote from "./schema/remote.ts";
|
|
20
|
+
import * as home from "./schema/home.ts";
|
|
21
|
+
import * as input from "./schema/inputs.ts";
|
|
22
|
+
import { section, tab } from "./schema/nodes.ts";
|
|
23
|
+
import { stableSecret } from "./stable-secret.ts";
|
|
24
|
+
|
|
25
|
+
export const r = {
|
|
26
|
+
host,
|
|
27
|
+
peers,
|
|
28
|
+
remote,
|
|
29
|
+
home,
|
|
30
|
+
input: {
|
|
31
|
+
text: input.text,
|
|
32
|
+
secret: input.secret,
|
|
33
|
+
number: input.number,
|
|
34
|
+
boolean: input.boolean,
|
|
35
|
+
enum: input.enum_,
|
|
36
|
+
},
|
|
37
|
+
section,
|
|
38
|
+
tab,
|
|
39
|
+
stableSecret,
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
// Control-plane client: a granted app calls Home through the worker's
|
|
43
|
+
// home-proxy.sock. `Deno` is touched only inside the function body, so this
|
|
44
|
+
// re-export stays QuickJS-safe.
|
|
45
|
+
export { callHome } from "./runtime/home-client/index.ts";
|
|
46
|
+
|
|
47
|
+
export { HOME_COOKIE_PREFIX } from "./home-cookie.ts";
|
|
48
|
+
export type { CallHomeOpts } from "./runtime/home-client/index.ts";
|
|
49
|
+
|
|
50
|
+
// realm.ts ergonomics actually used by author code.
|
|
51
|
+
export { apply } from "./apply.ts";
|
|
52
|
+
export { toYaml } from "./yaml.ts";
|
|
53
|
+
export { toIni } from "./ini.ts";
|
|
54
|
+
export { toJson } from "./json.ts";
|
|
55
|
+
export { toXml } from "./xml.ts";
|
|
56
|
+
export { interpolate, ref } from "./ref.ts";
|
|
57
|
+
export type { Ref } from "./ref.ts";
|
|
58
|
+
export { isNetworkStrategy, isVolumeStrategy } from "./schema/peer.ts";
|
|
59
|
+
|
|
60
|
+
// Author types. Anything Home-only lives on `/home`.
|
|
61
|
+
export type { AppIcon } from "./app-icon.ts";
|
|
62
|
+
export type { PeerHostPath, RoutedAppEntry, RoutedRouter } from "./schema/peer.ts";
|
|
63
|
+
|
|
64
|
+
// Identifier schemas + the Network brand.
|
|
65
|
+
export { nameSchema } from "./identifiers.ts";
|
|
66
|
+
export type { Network } from "./identifiers.ts";
|
package/src/info.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Normalization for `info` (createApp's operator-facing setup values). The
|
|
2
|
+
// caller runs the author's `info` fn in the sandbox and resolves its
|
|
3
|
+
// `{__realmRef}` markers; this pure step flattens the resolved dict to
|
|
4
|
+
// `ResolvedInfoEntry`, in declaration order. Shared by Home's summary page
|
|
5
|
+
// and the `t.compile` test harness so both produce identical panels.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One resolved `info` entry, the shape the dashboard's Setup-details panel
|
|
9
|
+
* renders. Structurally identical to Home's `InfoPanel` island `InfoEntry`.
|
|
10
|
+
*/
|
|
11
|
+
export interface ResolvedInfoEntry {
|
|
12
|
+
key: string;
|
|
13
|
+
label: string;
|
|
14
|
+
value: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
sensitive?: boolean;
|
|
17
|
+
copyable?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Flatten a ref-resolved `info` dict to ordered `ResolvedInfoEntry[]`.
|
|
22
|
+
*
|
|
23
|
+
* - A bare value (string / null) becomes `{key, label: key, value, copyable: true}`.
|
|
24
|
+
* - A rich object carries `label ?? key`, `description`, `sensitive`, and
|
|
25
|
+
* `copyable ?? true`.
|
|
26
|
+
* - Any entry whose resolved value isn't a string is dropped (a `null`
|
|
27
|
+
* service URL, or anything that didn't resolve to text).
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeInfo(raw: Record<string, unknown> | null): ResolvedInfoEntry[] {
|
|
30
|
+
if (!raw) return [];
|
|
31
|
+
const out: ResolvedInfoEntry[] = [];
|
|
32
|
+
for (const [key, entry] of Object.entries(raw)) {
|
|
33
|
+
let value: unknown;
|
|
34
|
+
let label = key;
|
|
35
|
+
let description: string | undefined;
|
|
36
|
+
let sensitive: boolean | undefined;
|
|
37
|
+
let copyable = true;
|
|
38
|
+
|
|
39
|
+
if (entry !== null && typeof entry === "object" && !Array.isArray(entry)) {
|
|
40
|
+
const obj = entry as Record<string, unknown>;
|
|
41
|
+
value = obj.value;
|
|
42
|
+
if (typeof obj.label === "string") label = obj.label;
|
|
43
|
+
if (typeof obj.description === "string") description = obj.description;
|
|
44
|
+
if (typeof obj.sensitive === "boolean") sensitive = obj.sensitive;
|
|
45
|
+
if (typeof obj.copyable === "boolean") copyable = obj.copyable;
|
|
46
|
+
} else {
|
|
47
|
+
value = entry;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Drop values that aren't strings: this is how `services.<name>` entries
|
|
51
|
+
// vanish until the install is placed and reachable.
|
|
52
|
+
if (typeof value !== "string") continue;
|
|
53
|
+
|
|
54
|
+
out.push({
|
|
55
|
+
key,
|
|
56
|
+
label,
|
|
57
|
+
value,
|
|
58
|
+
...(description !== undefined ? { description } : {}),
|
|
59
|
+
...(sensitive ? { sensitive } : {}),
|
|
60
|
+
copyable,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
package/src/ini.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Deferred, Ref } from "./ref.ts";
|
|
2
|
+
import { serializeRefs } from "./serialize.ts";
|
|
3
|
+
|
|
4
|
+
type IniScalar = string | number | boolean | Ref<string>;
|
|
5
|
+
type IniSection = Record<string, IniScalar>;
|
|
6
|
+
type IniInput = Record<string, IniScalar | IniSection>;
|
|
7
|
+
|
|
8
|
+
function scalar(value: string | number | boolean): string {
|
|
9
|
+
return typeof value === "boolean" ? (value ? "true" : "false") : String(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Emit a resolved (ref-free) tree: orphan scalar lines first, then one
|
|
13
|
+
// `[section]` block per nested object. A blank line separates the orphan
|
|
14
|
+
// run from the first section.
|
|
15
|
+
function emitIni(value: Record<string, unknown>): string {
|
|
16
|
+
const orphans: [string, unknown][] = [];
|
|
17
|
+
const sections: [string, Record<string, unknown>][] = [];
|
|
18
|
+
for (const [k, v] of Object.entries(value)) {
|
|
19
|
+
if (typeof v === "object" && v !== null) {
|
|
20
|
+
sections.push([k, v as Record<string, unknown>]);
|
|
21
|
+
} else {
|
|
22
|
+
orphans.push([k, v]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let out = "";
|
|
27
|
+
for (const [k, v] of orphans) out += `${k} = ${scalar(v as string | number | boolean)}\n`;
|
|
28
|
+
for (const [section, body] of sections) {
|
|
29
|
+
if (out.length > 0) out += "\n";
|
|
30
|
+
out += `[${section}]\n`;
|
|
31
|
+
for (const [k, v] of Object.entries(body)) {
|
|
32
|
+
out += `${k} = ${scalar(v as string | number | boolean)}\n`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Serialize a flat object (orphan keys) or sectioned object (one level
|
|
40
|
+
* of nesting → `[section]` headers) to INI. Refs in values are resolved
|
|
41
|
+
* at deploy time (via `apply()`); a ref-free tree collapses to a plain
|
|
42
|
+
* string at build time.
|
|
43
|
+
*
|
|
44
|
+
* Top-level keys are either scalars (orphan lines emitted before any
|
|
45
|
+
* section) or objects (section bodies). Mixing both is legal — orphans
|
|
46
|
+
* come first.
|
|
47
|
+
*/
|
|
48
|
+
export function toIni(value: IniInput): string | Deferred<string> {
|
|
49
|
+
return serializeRefs(value, emitIni);
|
|
50
|
+
}
|
package/src/json.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Deferred } from "./ref.ts";
|
|
2
|
+
import { serializeRefs } from "./serialize.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Serialize a value to a JSON string. Refs anywhere in the tree are
|
|
6
|
+
* resolved at deploy time (via `apply()`) before `JSON.stringify` runs, so
|
|
7
|
+
* a resolved secret is escaped correctly rather than spliced raw. A
|
|
8
|
+
* ref-free value collapses to a plain string at build time.
|
|
9
|
+
*
|
|
10
|
+
* files: { "/etc/app/config.json": toJson({ token: secretRef, port: 8080 }) }
|
|
11
|
+
*/
|
|
12
|
+
export function toJson<T>(value: T): string | Deferred<string> {
|
|
13
|
+
return serializeRefs(value, (resolved) => JSON.stringify(resolved));
|
|
14
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// `@yourrealm/sdk/jsx-runtime` — re-exports preact's automatic JSX
|
|
2
|
+
// runtime (functions and the `JSX` namespace) so realm.ts authors can
|
|
3
|
+
// set `jsxImportSource: "@yourrealm/sdk"` instead of `"preact"`, which
|
|
4
|
+
// keeps preact out of their declared deps.
|
|
5
|
+
export * from "preact/jsx-runtime";
|
package/src/keys.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hex-encode a byte array. Lowercase, two chars per byte, no separator.
|
|
3
|
+
*/
|
|
4
|
+
export function toHex(bytes: Uint8Array): string {
|
|
5
|
+
return Array.from(bytes)
|
|
6
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
7
|
+
.join("");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Derive a per-app secret from the master derivation seed.
|
|
12
|
+
*
|
|
13
|
+
* HMAC with a JSON-array message structurally prevents canonicalization
|
|
14
|
+
* collisions - distinct (appId, name) inputs can't map to the same
|
|
15
|
+
* serialized form, which is a property string-concatenation with a colon
|
|
16
|
+
* separator never had (`appId="a", name="b:c"` collided with
|
|
17
|
+
* `appId="a:b", name="c"`).
|
|
18
|
+
*/
|
|
19
|
+
export async function deriveAppPassword(
|
|
20
|
+
seedKey: CryptoKey,
|
|
21
|
+
appId: string,
|
|
22
|
+
name: string,
|
|
23
|
+
): Promise<string> {
|
|
24
|
+
const message = new TextEncoder().encode(
|
|
25
|
+
JSON.stringify(["derived-password", appId, name]),
|
|
26
|
+
) as Uint8Array<ArrayBuffer>;
|
|
27
|
+
const sig = await crypto.subtle.sign("HMAC", seedKey, message);
|
|
28
|
+
return toHex(new Uint8Array(sig));
|
|
29
|
+
}
|
package/src/mysql.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Ref } from "./ref.ts";
|
|
2
|
+
import { interpolate as i } from "./ref.ts";
|
|
3
|
+
import { Service } from "./service.ts";
|
|
4
|
+
|
|
5
|
+
interface MySQLOptions {
|
|
6
|
+
majorVersion?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class MySQL extends Service {
|
|
10
|
+
readonly image: string;
|
|
11
|
+
readonly port = 3306;
|
|
12
|
+
readonly user = "realm";
|
|
13
|
+
readonly database = "realm";
|
|
14
|
+
private readonly _majorVersion: number;
|
|
15
|
+
|
|
16
|
+
constructor(options?: MySQLOptions) {
|
|
17
|
+
super();
|
|
18
|
+
const majorVersion = options?.majorVersion ?? 9;
|
|
19
|
+
this.image = `mysql:${majorVersion}`;
|
|
20
|
+
this._majorVersion = majorVersion;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get password(): Ref<string> {
|
|
24
|
+
return this.derived("mysql-password");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get rootPassword(): Ref<string> {
|
|
28
|
+
return this.derived("root-password");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get url(): Ref<string> {
|
|
32
|
+
return i`mysql://${this.user}:${this.password}@${this.host}:${this.port}/${this.database}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getServiceDefinition() {
|
|
36
|
+
return {
|
|
37
|
+
image: this.image,
|
|
38
|
+
volumes: [
|
|
39
|
+
{
|
|
40
|
+
hostPath: i`./data/${this.host}`,
|
|
41
|
+
containerPath: "/var/lib/mysql",
|
|
42
|
+
backup: { kind: "mysql-dump" as const, url: this.url, version: this._majorVersion },
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
env: {
|
|
46
|
+
MYSQL_USER: this.user,
|
|
47
|
+
MYSQL_PASSWORD: this.password,
|
|
48
|
+
MYSQL_DATABASE: this.database,
|
|
49
|
+
MYSQL_ROOT_PASSWORD: this.rootPassword,
|
|
50
|
+
},
|
|
51
|
+
healthcheck: {
|
|
52
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Build-time app packer: collect an app's source files and stitch them into the
|
|
2
|
+
// shipped `// realm-file:` artifact. The relative-import closure comes off the
|
|
3
|
+
// TypeScript AST, so every import form (static of any line layout, dynamic
|
|
4
|
+
// `import()`, `require()`) is caught where a regex would miss some.
|
|
5
|
+
//
|
|
6
|
+
// It must NEVER be imported by Home: `typescript` trips Home's least-privilege
|
|
7
|
+
// sandbox (ts.sys' env probe → NotCapable). Only `realm-sdk build`, `realm-sdk
|
|
8
|
+
// dev`'s install panel, and tests reach for it; Home merges the raw artifact
|
|
9
|
+
// with esbuild alone (see surfaces.ts).
|
|
10
|
+
|
|
11
|
+
import { join } from "@std/path";
|
|
12
|
+
import ts from "typescript";
|
|
13
|
+
import { packFiles } from "./pack.ts";
|
|
14
|
+
import { findRealmEntry, globSurfaces } from "./surfaces.ts";
|
|
15
|
+
|
|
16
|
+
// Every module specifier a file imports/exports/requires, read off the real AST.
|
|
17
|
+
function moduleSpecifiers(source: string, fileName: string): string[] {
|
|
18
|
+
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
19
|
+
const specs: string[] = [];
|
|
20
|
+
const visit = (node: ts.Node): void => {
|
|
21
|
+
if (
|
|
22
|
+
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
23
|
+
node.moduleSpecifier &&
|
|
24
|
+
ts.isStringLiteralLike(node.moduleSpecifier)
|
|
25
|
+
) {
|
|
26
|
+
specs.push(node.moduleSpecifier.text); // static import/export ... from "x"
|
|
27
|
+
} else if (ts.isCallExpression(node)) {
|
|
28
|
+
const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
29
|
+
const isRequire = ts.isIdentifier(node.expression) && node.expression.text === "require";
|
|
30
|
+
const arg = node.arguments[0];
|
|
31
|
+
if ((isDynamicImport || isRequire) && arg && ts.isStringLiteralLike(arg)) {
|
|
32
|
+
specs.push(arg.text); // import("x") / require("x")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
ts.forEachChild(node, visit);
|
|
36
|
+
};
|
|
37
|
+
visit(sf);
|
|
38
|
+
return specs;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Resolve a relative specifier against the importing file's app-relative path,
|
|
42
|
+
// returning the app-relative posix target — or null if it climbs out of the app
|
|
43
|
+
// root. Deno requires explicit extensions, so the literal path is the file: no
|
|
44
|
+
// extension guessing, no index resolution.
|
|
45
|
+
function resolveRelative(fromPath: string, spec: string): string | null {
|
|
46
|
+
const dir = fromPath.includes("/") ? fromPath.slice(0, fromPath.lastIndexOf("/")) : "";
|
|
47
|
+
const stack: string[] = [];
|
|
48
|
+
for (const seg of (dir ? dir.split("/") : []).concat(spec.split("/"))) {
|
|
49
|
+
if (seg === "" || seg === ".") continue;
|
|
50
|
+
if (seg === "..") {
|
|
51
|
+
if (stack.length === 0) return null;
|
|
52
|
+
stack.pop();
|
|
53
|
+
} else {
|
|
54
|
+
stack.push(seg);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return stack.join("/");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Discover every local source file an app is built from — the realm entry, the
|
|
61
|
+
// file-based surfaces (pages/*, widgets/*), and their relative-import closure —
|
|
62
|
+
// keyed by app-root-relative posix path with the entry first. Pure reads: no
|
|
63
|
+
// esbuild, no temp files.
|
|
64
|
+
async function collectAppFiles(appDir: string): Promise<Map<string, string>> {
|
|
65
|
+
const entry = findRealmEntry(appDir);
|
|
66
|
+
const seeds = [
|
|
67
|
+
entry,
|
|
68
|
+
...[...(await globSurfaces(appDir, "page")), ...(await globSurfaces(appDir, "widget"))].map(
|
|
69
|
+
(f) => `${f.dir}/${f.file}`,
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const contents = new Map<string, string>();
|
|
74
|
+
const queue = [...seeds];
|
|
75
|
+
while (queue.length > 0) {
|
|
76
|
+
const rel = queue.shift()!;
|
|
77
|
+
if (contents.has(rel)) continue;
|
|
78
|
+
let text: string;
|
|
79
|
+
try {
|
|
80
|
+
text = await Deno.readTextFile(join(appDir, rel));
|
|
81
|
+
} catch {
|
|
82
|
+
continue; // unresolvable import — bundle() surfaces it later with context
|
|
83
|
+
}
|
|
84
|
+
contents.set(rel, text);
|
|
85
|
+
for (const spec of moduleSpecifiers(text, rel)) {
|
|
86
|
+
if (!spec.startsWith("./") && !spec.startsWith("../")) continue;
|
|
87
|
+
const target = resolveRelative(rel, spec);
|
|
88
|
+
if (target && !contents.has(target)) queue.push(target);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Entry first (so a preview leads with realm.tsx), then the rest sorted.
|
|
93
|
+
const ordered = [entry, ...[...contents.keys()].filter((p) => p !== entry).sort()];
|
|
94
|
+
return new Map(ordered.map((p) => [p, contents.get(p)!]));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the shipped artifact for an app: its raw source files stitched into the
|
|
99
|
+
* marker-delimited pack. No merge, no derived content — Home merges at install.
|
|
100
|
+
* A single-file app (just the entry, no surfaces or helpers) ships raw with no
|
|
101
|
+
* markers, so `isPacked` stays false and every consumer treats it as the entry.
|
|
102
|
+
* Pure reads, so this runs anywhere (incl. a sandboxed test process).
|
|
103
|
+
*/
|
|
104
|
+
export async function buildPackedApp(appDir: string): Promise<string> {
|
|
105
|
+
const files = await collectAppFiles(appDir);
|
|
106
|
+
if (files.size === 1) return [...files.values()][0]!;
|
|
107
|
+
return packFiles(files);
|
|
108
|
+
}
|
package/src/pack.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Marker-delimited single-file archive for multi-file realm apps.
|
|
2
|
+
//
|
|
3
|
+
// A multi-file app (realm.tsx + pages/*.tsx + widgets/*.tsx + relative
|
|
4
|
+
// helpers) is shipped as ONE text artifact whose chunks are delimited by
|
|
5
|
+
// `// realm-file: <path>` lines:
|
|
6
|
+
//
|
|
7
|
+
// // realm-file: realm.tsx
|
|
8
|
+
// <contents of realm.tsx>
|
|
9
|
+
// // realm-file: widgets/status.tsx
|
|
10
|
+
// <contents of widgets/status.tsx>
|
|
11
|
+
//
|
|
12
|
+
// Home stores this verbatim in one `source` text column and unpacks it into a
|
|
13
|
+
// confined tmp tree at install, then runs the same merge + bundle the dev/build
|
|
14
|
+
// path uses. The artifact IS the human-readable source, so the store can
|
|
15
|
+
// preview it file by file and any integrity digest is over source the operator
|
|
16
|
+
// can read rather than an esbuild blob.
|
|
17
|
+
//
|
|
18
|
+
// A single-file app is an artifact with no markers; `isPacked` is false and
|
|
19
|
+
// every consumer treats the whole string as the realm entry, unchanged.
|
|
20
|
+
|
|
21
|
+
const MARKER_PREFIX = "// realm-file: ";
|
|
22
|
+
|
|
23
|
+
// The realm entry, in preference order. A packed app puts one of these first.
|
|
24
|
+
export const REALM_ENTRIES = ["realm.tsx", "realm.ts"] as const;
|
|
25
|
+
|
|
26
|
+
// A marker line at the start of a content line would be misread as a chunk
|
|
27
|
+
// boundary, so on pack we slip one `\` in after `// ` (escape ladder: an
|
|
28
|
+
// already-escaped line gets another `\`); unpack peels exactly one back. A
|
|
29
|
+
// real boundary never carries a `\`, so the two are unambiguous.
|
|
30
|
+
const ESCAPE_RE = /^(\/\/ )(\\*realm-file:)/gm;
|
|
31
|
+
const UNESCAPE_RE = /^(\/\/ )(\\+realm-file:)/gm;
|
|
32
|
+
|
|
33
|
+
// Boundary lines, matched at line start. `.+` is the path; greedy is fine
|
|
34
|
+
// since a path never contains a newline.
|
|
35
|
+
const BOUNDARY_RE = /^\/\/ realm-file: (.+)$/gm;
|
|
36
|
+
|
|
37
|
+
/** A packed artifact always begins with the entry's marker at byte 0. */
|
|
38
|
+
export function isPacked(source: string): boolean {
|
|
39
|
+
return source.startsWith(MARKER_PREFIX);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function escapeContent(content: string): string {
|
|
43
|
+
return content.replace(ESCAPE_RE, "$1\\$2");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function unescapeContent(content: string): string {
|
|
47
|
+
return content.replace(
|
|
48
|
+
UNESCAPE_RE,
|
|
49
|
+
(_full, slashes: string, rest: string) => slashes + rest.slice(1),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A path is confined when it's a plain forward-slash relative path that can't
|
|
54
|
+
// climb out of the app root: no absolute, no `..`/`.` segments, no backslash
|
|
55
|
+
// (Windows-y paths + an escape vector), no empty segments.
|
|
56
|
+
function assertConfinedPath(path: string): void {
|
|
57
|
+
if (path === "" || path.startsWith("/") || path.includes("\\") || path.includes("\0")) {
|
|
58
|
+
throw new Error(`realm-file: unsafe path "${path}"`);
|
|
59
|
+
}
|
|
60
|
+
for (const seg of path.split("/")) {
|
|
61
|
+
if (seg === "" || seg === "." || seg === "..") {
|
|
62
|
+
throw new Error(`realm-file: unsafe path "${path}"`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Pack a set of files into the marker-delimited artifact. `files` is keyed by
|
|
69
|
+
* app-root-relative posix path; iteration order is preserved (callers put the
|
|
70
|
+
* realm entry first). Each chunk's content is normalized to end in a single
|
|
71
|
+
* newline so the next marker always lands at a line start.
|
|
72
|
+
*/
|
|
73
|
+
export function packFiles(files: Map<string, string>): string {
|
|
74
|
+
const parts: string[] = [];
|
|
75
|
+
for (const [path, content] of files) {
|
|
76
|
+
assertConfinedPath(path);
|
|
77
|
+
const normalized = content.endsWith("\n") ? content : content + "\n";
|
|
78
|
+
parts.push(MARKER_PREFIX + path + "\n" + escapeContent(normalized));
|
|
79
|
+
}
|
|
80
|
+
return parts.join("");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Unpack a marker-delimited artifact into a path → content map. Paths are
|
|
85
|
+
* confined to the app root (throws on any `..`/absolute/backslash escape).
|
|
86
|
+
* A string with no markers is returned as a single-entry map under the given
|
|
87
|
+
* `singleFileName` (default `realm.tsx`).
|
|
88
|
+
*/
|
|
89
|
+
export function unpackFiles(packed: string, singleFileName = "realm.tsx"): Map<string, string> {
|
|
90
|
+
if (!isPacked(packed)) {
|
|
91
|
+
return new Map([[singleFileName, packed]]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const markers: { path: string; contentStart: number; lineStart: number }[] = [];
|
|
95
|
+
BOUNDARY_RE.lastIndex = 0;
|
|
96
|
+
let m: RegExpExecArray | null;
|
|
97
|
+
while ((m = BOUNDARY_RE.exec(packed)) !== null) {
|
|
98
|
+
const nl = packed.indexOf("\n", m.index + m[0]!.length);
|
|
99
|
+
markers.push({
|
|
100
|
+
path: m[1]!.trim(),
|
|
101
|
+
lineStart: m.index,
|
|
102
|
+
contentStart: nl === -1 ? packed.length : nl + 1,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const files = new Map<string, string>();
|
|
107
|
+
for (let i = 0; i < markers.length; i++) {
|
|
108
|
+
const { path, contentStart } = markers[i]!;
|
|
109
|
+
assertConfinedPath(path);
|
|
110
|
+
const end = i + 1 < markers.length ? markers[i + 1]!.lineStart : packed.length;
|
|
111
|
+
if (files.has(path)) throw new Error(`realm-file: duplicate path "${path}"`);
|
|
112
|
+
files.set(path, unescapeContent(packed.slice(contentStart, end)));
|
|
113
|
+
}
|
|
114
|
+
return files;
|
|
115
|
+
}
|