@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,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
+ }