@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,165 @@
1
+ // Wire constants for `host.spawn()` — the worker's disposable-container
2
+ // broker, keyed per granted app. Mirrors `home-proxy-paths.ts`: one socket
3
+ // per app, so the worker attaches the caller's identity from *which* socket a
4
+ // request arrived on, never from the wire. The resolved
5
+ // `{socket, scratch, artifacts}` is built per-app by `host.spawn`'s resolver at
6
+ // eval, not a fixed constant — so the SDK only ships the placeholder + the path
7
+ // builders.
8
+ //
9
+ // Pure constants + string-join builders, no runtime imports (the
10
+ // `WorkerHostPath` import is type-only and erases), so schema/host.ts can pull
11
+ // `SPAWN_PLACEHOLDER` under QuickJS and any realm.ts consumer can read the
12
+ // resolved shape without dragging Deno deps.
13
+
14
+ import type { WorkerHostPath } from "./logs-socket-path.ts";
15
+ import type { WorkerMount } from "./worker-mount.ts";
16
+
17
+ /**
18
+ * Host directory the worker keeps every spawn app's socket + per-app tree
19
+ * under. Lives beneath `/opt/realm/worker/` — the one directory the worker
20
+ * container already shares with the host — so the inode the worker binds is
21
+ * the same one a granted app bind-mounts through the host docker daemon
22
+ * (mirrors `REALM_HOME_PROXY_DIR`).
23
+ *
24
+ * Two fixed sub-roots hang off this: `sock/` holds one per-app socket dir each
25
+ * (`sock/<appId>/spawn.sock`), and `data/` holds the *heavy* per-app build
26
+ * storage (scratch / artifacts / the broker-private back room). Keeping the
27
+ * sockets under their own `sock/` segment — a sibling of `data/` — means a
28
+ * per-app socket dir can never collide with the `data/<appId>` tree, even for
29
+ * an app literally named `data`. The `data/` segment is also the relocation
30
+ * seam: an operator who wants build storage on another drive mounts it at
31
+ * `${REALM_SPAWN_DIR}/data` (a fixed path, not a config knob), which keeps
32
+ * scratch / artifacts / private on the *same* filesystem — load-bearing,
33
+ * because the back-room seal renames between them and a rename must not cross a
34
+ * device boundary.
35
+ */
36
+ export const REALM_SPAWN_DIR = "/opt/realm/worker/spawn";
37
+
38
+ /**
39
+ * Host directory holding one app's spawn broker socket
40
+ * (`/opt/realm/worker/spawn/sock/<appId>`). The socket sits alone inside it, so
41
+ * this is the path the app bind-mounts (a file bind-mount of a unix socket
42
+ * doesn't wire across containers on macOS Docker; the app mounts the dir and
43
+ * connects to the socket inside it). `host.spawn`'s resolver builds its
44
+ * `{ socket }` from this per-app dir at eval, and `collectAllowed` exempts it
45
+ * from the absolute-mount gate.
46
+ */
47
+ export function spawnSocketDir(appId: string): WorkerHostPath {
48
+ return `${REALM_SPAWN_DIR}/sock/${appId}` as WorkerHostPath;
49
+ }
50
+
51
+ /**
52
+ * Host path of one app's spawn broker socket, inside {@link spawnSocketDir}
53
+ * (`/opt/realm/worker/spawn/sock/<appId>/spawn.sock`). The worker binds this in
54
+ * `reconcileSpawnSockets` and carries it on the wire; the app connects to it
55
+ * inside the mounted per-app socket dir to start one disposable box per job.
56
+ */
57
+ export function spawnSocketPath(appId: string): string {
58
+ return `${spawnSocketDir(appId)}/spawn.sock`;
59
+ }
60
+
61
+ /**
62
+ * Host path of one app's scratch dir
63
+ * (`/opt/realm/worker/spawn/data/<appId>/scratch`). The app stages a build
64
+ * context under a relative key here; the broker renames that one subdir into
65
+ * its per-box back room (see {@link spawnPrivatePath}) and mounts it into the
66
+ * box read-only. Per-app, so one spawn app can't read another's in-flight
67
+ * context. Under `data/` so it shares a filesystem with artifacts +
68
+ * private (the seal's rename must not cross a device boundary).
69
+ */
70
+ export function spawnScratchPath(appId: string): WorkerHostPath {
71
+ return `${REALM_SPAWN_DIR}/data/${appId}/scratch` as WorkerHostPath;
72
+ }
73
+
74
+ /**
75
+ * Host path of one app's artifacts dir
76
+ * (`/opt/realm/worker/spawn/data/<appId>/artifacts`). On a successful build
77
+ * the broker renames the box's output here under a per-box subdir; the app
78
+ * reads the result back out of it (e.g. `output-<boxId>/image.tar`). A sibling
79
+ * of the scratch dir so input (the cloned source the app stages) and output
80
+ * (the built image) live in separate per-app mounts. Under `data/` so it
81
+ * shares a filesystem with scratch + private (the success rename must not
82
+ * cross a device boundary).
83
+ */
84
+ export function spawnArtifactsPath(appId: string): WorkerHostPath {
85
+ return `${REALM_SPAWN_DIR}/data/${appId}/artifacts` as WorkerHostPath;
86
+ }
87
+
88
+ /**
89
+ * Host path of one app's broker-private "back room"
90
+ * (`/opt/realm/worker/spawn/data/<appId>/private`). The broker moves both bind
91
+ * sources (the validated context, and the box's output dir) into a per-box
92
+ * subdir here for the duration of a build — a dir the calling app has NO mount
93
+ * into. That's the seal: the broker re-validates the *moved* inode and
94
+ * runs the box against a path the caller can't reach, so the caller can't swap
95
+ * the validated dir for a symlink between validate and Docker mount.
96
+ *
97
+ * Broker-only: unlike socket / scratch / artifacts this is NOT part of
98
+ * {@link SpawnValue}, never travels the wire, and is never exempted from the
99
+ * absolute-mount gate — no app ever mounts it. It's a sibling of scratch +
100
+ * artifacts under `data/<appId>/` so the seal's renames stay same-filesystem,
101
+ * and so the worker's reconcile reclaims it with the rest of the app's `data/`
102
+ * subtree once the app is off the wire.
103
+ */
104
+ export function spawnPrivatePath(appId: string): WorkerHostPath {
105
+ return `${REALM_SPAWN_DIR}/data/${appId}/private` as WorkerHostPath;
106
+ }
107
+
108
+ /**
109
+ * Resolved shape of a `host.spawn()` grant. All three fields are per-app paths
110
+ * built at eval (no fixed constant); `SPAWN_PLACEHOLDER` is type-level only,
111
+ * there so `ValueOf` can read this shape off `toValue` (mirrors
112
+ * `HomeApiAccessValue` / `HOME_API_PLACEHOLDER`). `socket` is the per-app
113
+ * DIRECTORY holding the broker socket — the app mounts it and connects to
114
+ * `<mount>/spawn.sock` inside (a file bind-mount of a unix socket doesn't wire
115
+ * across containers on macOS Docker). `scratch` is where the app stages a build
116
+ * context (input); `artifacts` is where it reads a build's output back. There
117
+ * is no network field: the box attaches to the untrusted-zone DMZ broker-side
118
+ * (see spawn-server/spec.ts), and that DMZ has no internal members — an app
119
+ * never puts a service on it, so it has nothing to wire.
120
+ */
121
+ export interface SpawnValue {
122
+ readonly socket: WorkerMount;
123
+ readonly scratch: WorkerMount;
124
+ readonly artifacts: WorkerMount;
125
+ }
126
+
127
+ /**
128
+ * Build the resolved `host.spawn` value for one app. `socket` is read-only -
129
+ * the app connects to the broker, it never writes to the socket's directory.
130
+ * `scratch` and `artifacts` are the app's own working storage on both sides of
131
+ * a build (stage a context in, read an image back out), so they stay writable.
132
+ */
133
+ export function spawnMounts(appId: string): SpawnValue {
134
+ return {
135
+ socket: {
136
+ hostPath: spawnSocketDir(appId),
137
+ containerPath: "/run/realm/spawn-sock",
138
+ readOnly: true,
139
+ },
140
+ scratch: {
141
+ hostPath: spawnScratchPath(appId),
142
+ containerPath: "/run/realm/spawn",
143
+ readOnly: false,
144
+ },
145
+ artifacts: {
146
+ hostPath: spawnArtifactsPath(appId),
147
+ containerPath: "/run/realm/artifacts",
148
+ readOnly: false,
149
+ },
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Type-level stand-in for `host.spawn()`'s resolved value, so `ValueOf` can
155
+ * read the shape. Empty paths stand in for "never resolved"; the container paths and the
156
+ * writability are already final, since those don't depend on the app.
157
+ */
158
+ const UNFILLED = spawnMounts("");
159
+ const blank = (m: WorkerMount): WorkerMount => ({ ...m, hostPath: "" as WorkerHostPath });
160
+
161
+ export const SPAWN_PLACEHOLDER: SpawnValue = {
162
+ socket: blank(UNFILLED.socket),
163
+ scratch: blank(UNFILLED.scratch),
164
+ artifacts: blank(UNFILLED.artifacts),
165
+ };
@@ -0,0 +1,40 @@
1
+ import type { WorkerHostPath } from "./logs-socket-path.ts";
2
+ import type { WorkerMount } from "./worker-mount.ts";
3
+
4
+ /**
5
+ * Host directory the worker keeps the shared stats socket inside. This is the
6
+ * path an app mounts (the DIRECTORY, not the socket file): a file bind-mount
7
+ * of a unix socket does NOT wire to the live listener across containers on
8
+ * macOS Docker (connect succeeds, no bytes flow), so every broker socket lives
9
+ * alone in its own directory and the app mounts that directory. The value
10
+ * `r.host.stats()` resolves to (and the path the resolver exempts from the
11
+ * absolute-mount gate).
12
+ *
13
+ * Lives under `/opt/realm/worker/` for the same reason as
14
+ * {@link REALM_LOGS_DIR}: it's the one directory the worker container already
15
+ * shares with the host, so the socket the worker binds is the same inode an
16
+ * app container can bind-mount through the host docker daemon. It also puts
17
+ * the path under the `/opt/realm` entry of `HOST_DIRECTORY_BLOCKLIST`, so an
18
+ * operator can't route a Silo around the `host.stats`
19
+ * gate.
20
+ */
21
+ export const REALM_STATS_DIR = "/opt/realm/worker/stats" as WorkerHostPath;
22
+
23
+ /**
24
+ * Host path of the stats socket, inside {@link REALM_STATS_DIR}. The worker's
25
+ * stats-server binds this; a consumer mounts {@link REALM_STATS_DIR} and
26
+ * connects to the socket at the corresponding path inside the mounted
27
+ * directory.
28
+ */
29
+ export const REALM_STATS_SOCKET = `${REALM_STATS_DIR}/stats.sock`;
30
+
31
+ /**
32
+ * What `r.host.stats()` resolves to: the mount, not the bare path. Read-only
33
+ * for the same reason as {@link REALM_LOGS_MOUNT} - {@link REALM_STATS_DIR} is
34
+ * shared by every app holding the grant.
35
+ */
36
+ export const REALM_STATS_MOUNT: WorkerMount = {
37
+ hostPath: REALM_STATS_DIR,
38
+ containerPath: "/run/realm/stats",
39
+ readOnly: true,
40
+ };
@@ -0,0 +1,77 @@
1
+ // The two files an updater-driven compose project carries beside its `.env`:
2
+ // `.update`, the pending request and the updater's retry token, and
3
+ // `.update.error`, what the last failed attempt said. The worker reads them
4
+ // for its own box's `worker` and `ingress` projects and Home reads its own
5
+ // `etc/home`, so the one reader lives here rather than twice.
6
+ //
7
+ // A read, never a write: the worker writes `.update` (self-update.ts) and the
8
+ // updater writes `.update.error` (apply.rs). This module only says what is on
9
+ // disk right now.
10
+
11
+ /**
12
+ * Longest `error` this reader hands back, and so the longest an
13
+ * `update.status` carries: the tail of `.update.error`. It lives beside the
14
+ * reader rather than in the uplink catalogue so this module, on the
15
+ * author-safe barrel, imports nothing from `uplink/`.
16
+ */
17
+ export const UPDATE_ERROR_MAX_CHARS = 2048;
18
+
19
+ /** The two files an updater-driven project carries beside its `.env`. */
20
+ export interface UpdateFiles {
21
+ /** `.update` exists (the pending request / retry token). */
22
+ pending: boolean;
23
+ /**
24
+ * Trimmed `.update.error` content, or null when absent or empty. Capped at
25
+ * `UPDATE_ERROR_MAX_CHARS` from the end, because the last lines of a compose
26
+ * failure are the ones that say why.
27
+ */
28
+ error: string | null;
29
+ }
30
+
31
+ /**
32
+ * Anything bigger is junk, not a message: the updater bounds what it writes
33
+ * well under this, so a file over it was put there by something else.
34
+ */
35
+ const MAX_ERROR_FILE_BYTES = 16 * 1024;
36
+
37
+ /**
38
+ * Read `<dir>/.update` and `<dir>/.update.error`.
39
+ *
40
+ * `Deno.*` is touched inside the body only: this module is reachable from the
41
+ * author-safe `worker-protocol` barrel, which an author's realm.ts may import
42
+ * into QuickJS, where `Deno` does not exist (`quickjs-safe.test.ts`).
43
+ *
44
+ * Never throws for a missing file or dir - that is `{ pending: false, error:
45
+ * null }`, the shape of a project with nothing going on. The worker's
46
+ * `.tmp-*` siblings from a tmp-then-rename in flight are not these files and
47
+ * are never read.
48
+ */
49
+ export async function readUpdateFiles(dir: string): Promise<UpdateFiles> {
50
+ const [pending, error] = await Promise.all([
51
+ isFile(`${dir}/.update`),
52
+ readError(`${dir}/.update.error`),
53
+ ]);
54
+ return { pending, error };
55
+ }
56
+
57
+ async function isFile(path: string): Promise<boolean> {
58
+ try {
59
+ return (await Deno.stat(path)).isFile;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ async function readError(path: string): Promise<string | null> {
66
+ let text: string;
67
+ try {
68
+ const stat = await Deno.stat(path);
69
+ if (!stat.isFile || stat.size > MAX_ERROR_FILE_BYTES) return null;
70
+ text = await Deno.readTextFile(path);
71
+ } catch {
72
+ return null;
73
+ }
74
+ const trimmed = text.trim();
75
+ if (trimmed === "") return null;
76
+ return trimmed.length > UPDATE_ERROR_MAX_CHARS ? trimmed.slice(-UPDATE_ERROR_MAX_CHARS) : trimmed;
77
+ }
@@ -0,0 +1,86 @@
1
+ // The numbers this layer owns, from `todo/ideas/uplink.md` § Constants.
2
+ // That table is the authority; change it there, not here.
3
+
4
+ /** `[len u16][streamId u32][type u8]`. */
5
+ export const FRAME_HEADER_BYTES = 7;
6
+
7
+ /** What `len` can express. `encodeFrame` refuses more; the wire rule is below. */
8
+ export const MAX_FRAME_LEN = 65535;
9
+
10
+ /** OPEN headers and DATA fragments are both capped here, in both directions. */
11
+ export const MAX_PAYLOAD_BYTES = 16 * 1024;
12
+
13
+ export const DEFAULT_LIMITS = {
14
+ fragmentSize: 16 * 1024,
15
+ initialWindow: 256 * 1024,
16
+ maxStreams: 64,
17
+ maxInFlight: 8 * 1024 * 1024,
18
+ } as const;
19
+
20
+ /** PING/PONG ride stream 0 and carry 8 opaque bytes. */
21
+ export const CONTROL_STREAM_ID = 0;
22
+ export const PING_PAYLOAD_BYTES = 8;
23
+
24
+ /** WINDOW carries one u32 credit increment. */
25
+ export const WINDOW_PAYLOAD_BYTES = 4;
26
+
27
+ /** The RPC response head is `[u16 headLen][head JSON]`, so the head fits a u16. */
28
+ export const MAX_RPC_HEAD_BYTES = 65535;
29
+
30
+ /** RESET reasons are diagnostics, not payloads; long ones are cut here. */
31
+ export const MAX_RESET_REASON_CHARS = 512;
32
+
33
+ // --- the session below the mux ---------------------------------------
34
+
35
+ /**
36
+ * The Noise prologue, `realm-uplink/1`. It versions the bytes below the RPC -
37
+ * the handshake, the frames, the mux - and a peer on another number fails the
38
+ * handshake. The catalogue above those bytes is versioned separately, by
39
+ * `PROTOCOL_VERSION` in `protocol.ts`, which two peers compare in `hello` and
40
+ * can act on.
41
+ *
42
+ * Those are different failures on purpose. A `PROTOCOL_VERSION` mismatch still
43
+ * gives both sides a session, so Home can tell the box to update over it. A
44
+ * prologue mismatch gives them nothing: there is no session to carry the
45
+ * instruction, and moving that box takes the updater's `.update` file by hand.
46
+ * So this number changes only when the transport itself does.
47
+ *
48
+ * Spelled byte by byte rather than through `encodeUtf8` so it can be a
49
+ * module-load constant: this file is reachable from an author bundle, where
50
+ * `TextEncoder` does not exist.
51
+ */
52
+ export const UPLINK_PROLOGUE: Uint8Array = Uint8Array.from("realm-uplink/1", (c) =>
53
+ c.charCodeAt(0),
54
+ );
55
+
56
+ /** A Noise message is at most 65535 bytes including its 16-byte tag. */
57
+ export const NOISE_MAX_PAYLOAD = 65519;
58
+
59
+ /** Socket open and all three handshake messages, together. */
60
+ export const HANDSHAKE_TIMEOUT_MS = 5000;
61
+
62
+ /** `WebSocket.send` buffers unboundedly; the session stops filling past this. */
63
+ export const SOCKET_HIGH_WATER = 256 * 1024;
64
+
65
+ /**
66
+ * Undecrypted messages queued ahead of the mux. A WHATWG socket cannot pause
67
+ * its reads, so a peer that outruns decryption is closed rather than buffered.
68
+ */
69
+ export const SOCKET_INBOUND_CAP = 16 * 1024 * 1024;
70
+
71
+ // --- liveness ---------------------------------------------------------
72
+ // The session runs none of these. The worker's client loop owns the ping
73
+ // cadence, the pong deadline and the reconnect; Home's session registry owns
74
+ // the idle deadline. They are here so both sides read one number.
75
+
76
+ export const PING_INTERVAL_MS = 25_000;
77
+ export const PONG_TIMEOUT_MS = 10_000;
78
+ export const IDLE_TIMEOUT_MS = 35_000;
79
+
80
+ /** Rekey by reconnecting, so no session holds one key forever. */
81
+ export const REKEY_INTERVAL_MS = 24 * 60 * 60 * 1000;
82
+ export const REKEY_JITTER_MS = 2 * 60 * 60 * 1000;
83
+
84
+ /** Reconnect backoff: doubling from the base to the cap, full jitter. */
85
+ export const BACKOFF_BASE_MS = 1000;
86
+ export const BACKOFF_CAP_MS = 30_000;
@@ -0,0 +1,61 @@
1
+ // Everything the mux and the RPC layer reject with. One base so a caller
2
+ // can tell an uplink failure from an application one in a single check.
3
+
4
+ export class UplinkError extends Error {}
5
+
6
+ /** The peer violated the framing. The mux closes the session on one of these. */
7
+ export class UplinkProtocolError extends UplinkError {
8
+ constructor(message: string) {
9
+ super(message);
10
+ this.name = "UplinkProtocolError";
11
+ }
12
+ }
13
+
14
+ /** The session is gone; every open and pending stream fails with this. */
15
+ export class SessionClosed extends UplinkError {
16
+ readonly reason: string;
17
+
18
+ constructor(reason: string) {
19
+ super(`uplink session closed: ${reason}`);
20
+ this.name = "SessionClosed";
21
+ this.reason = reason;
22
+ }
23
+ }
24
+
25
+ /** One stream was aborted, in either direction. Carries the peer's reason. */
26
+ export class StreamReset extends UplinkError {
27
+ readonly reason: string;
28
+
29
+ constructor(reason: string) {
30
+ super(reason === "" ? "stream reset" : `stream reset: ${reason}`);
31
+ this.name = "StreamReset";
32
+ this.reason = reason;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * The responder's static key is not the one the worker pinned. Raised after
38
+ * handshake message 2 and before message 3, so the worker's own static never
39
+ * goes out. Not retryable, and it is the worker's loop — not this layer —
40
+ * that must refuse to retry (`todo/ideas/uplink.md` § What it guarantees).
41
+ */
42
+ export class HomeKeyMismatch extends UplinkError {
43
+ /** Hex, for a log line the operator can compare against the paste. */
44
+ readonly expected: string;
45
+ readonly actual: string;
46
+
47
+ constructor(expected: Uint8Array, actual: Uint8Array) {
48
+ const want = hex(expected);
49
+ const got = hex(actual);
50
+ super(`Home answered with static key ${got}, not the pinned ${want}`);
51
+ this.name = "HomeKeyMismatch";
52
+ this.expected = want;
53
+ this.actual = got;
54
+ }
55
+ }
56
+
57
+ // Not `Uint8Array.prototype.toHex`: this module is reachable from an author
58
+ // bundle, and QuickJS has no such method.
59
+ function hex(bytes: Uint8Array): string {
60
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
61
+ }
@@ -0,0 +1,114 @@
1
+ // The uplink frame codec: `[len u16][streamId u32][type u8][payload]`,
2
+ // big-endian, `len` counting payload only. The byte stream underneath may
3
+ // split or coalesce arbitrarily, so `FrameParser` is fed raw chunks and
4
+ // emits whole frames, keeping any partial tail for the next chunk.
5
+
6
+ import { FRAME_HEADER_BYTES, MAX_FRAME_LEN, MAX_PAYLOAD_BYTES } from "./constants.ts";
7
+ import { UplinkProtocolError } from "./errors.ts";
8
+
9
+ export const FrameType = {
10
+ OPEN: 1,
11
+ DATA: 2,
12
+ END: 3,
13
+ RESET: 4,
14
+ WINDOW: 5,
15
+ PING: 6,
16
+ PONG: 7,
17
+ } as const;
18
+
19
+ export type FrameType = (typeof FrameType)[keyof typeof FrameType];
20
+
21
+ const FRAME_TYPE_NAMES: Record<number, string> = Object.fromEntries(
22
+ Object.entries(FrameType).map(([name, id]) => [id, name]),
23
+ );
24
+
25
+ export function frameTypeName(type: number): string {
26
+ return FRAME_TYPE_NAMES[type] ?? `type ${type}`;
27
+ }
28
+
29
+ export interface Frame {
30
+ type: FrameType;
31
+ streamId: number;
32
+ payload: Uint8Array;
33
+ }
34
+
35
+ export const EMPTY_PAYLOAD = new Uint8Array(0);
36
+
37
+ export function encodeFrame(type: FrameType, streamId: number, payload: Uint8Array): Uint8Array {
38
+ if (payload.length > MAX_FRAME_LEN) {
39
+ throw new RangeError(`frame payload ${payload.length} exceeds ${MAX_FRAME_LEN} bytes`);
40
+ }
41
+ const out = new Uint8Array(FRAME_HEADER_BYTES + payload.length);
42
+ const view = new DataView(out.buffer);
43
+ view.setUint16(0, payload.length);
44
+ view.setUint32(2, streamId);
45
+ view.setUint8(6, type);
46
+ out.set(payload, FRAME_HEADER_BYTES);
47
+ return out;
48
+ }
49
+
50
+ export class FrameParser {
51
+ #pending = EMPTY_PAYLOAD;
52
+
53
+ /** Throws `UplinkProtocolError` on anything the framing forbids. */
54
+ push(chunk: Uint8Array): Frame[] {
55
+ const buf = this.#pending.length === 0 ? chunk : concatBytes(this.#pending, chunk);
56
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
57
+ const frames: Frame[] = [];
58
+ let offset = 0;
59
+ while (buf.length - offset >= FRAME_HEADER_BYTES) {
60
+ const len = view.getUint16(offset);
61
+ const total = FRAME_HEADER_BYTES + len;
62
+ if (buf.length - offset < total) break;
63
+ const type = view.getUint8(offset + 6);
64
+ if (FRAME_TYPE_NAMES[type] === undefined) {
65
+ throw new UplinkProtocolError(`unknown frame type ${type}`);
66
+ }
67
+ // The `len` field can express 64 KiB, the protocol allows 16.
68
+ if (len > MAX_PAYLOAD_BYTES) {
69
+ throw new UplinkProtocolError(`frame payload ${len} exceeds ${MAX_PAYLOAD_BYTES} bytes`);
70
+ }
71
+ frames.push({
72
+ type: type as FrameType,
73
+ streamId: view.getUint32(offset + 2),
74
+ // Copied: the payload outlives `chunk` once it lands in a stream queue.
75
+ payload: buf.slice(offset + FRAME_HEADER_BYTES, offset + total),
76
+ });
77
+ offset += total;
78
+ }
79
+ this.#pending = offset === buf.length ? EMPTY_PAYLOAD : buf.slice(offset);
80
+ return frames;
81
+ }
82
+ }
83
+
84
+ export function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
85
+ const out = new Uint8Array(a.length + b.length);
86
+ out.set(a);
87
+ out.set(b, a.length);
88
+ return out;
89
+ }
90
+
91
+ export function encodeU32(value: number): Uint8Array {
92
+ const out = new Uint8Array(4);
93
+ new DataView(out.buffer).setUint32(0, value);
94
+ return out;
95
+ }
96
+
97
+ export function decodeU32(bytes: Uint8Array): number {
98
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
99
+ }
100
+
101
+ // Lazy so no Web global is touched at module load: this file is reachable
102
+ // from the SDK graph that gets inlined into QuickJS author bundles.
103
+ let encoder: TextEncoder | undefined;
104
+ let decoder: TextDecoder | undefined;
105
+
106
+ export function encodeUtf8(text: string): Uint8Array {
107
+ encoder ??= new TextEncoder();
108
+ return encoder.encode(text);
109
+ }
110
+
111
+ export function decodeUtf8(bytes: Uint8Array): string {
112
+ decoder ??= new TextDecoder();
113
+ return decoder.decode(bytes);
114
+ }
@@ -0,0 +1,40 @@
1
+ // The one text encoding of an uplink static public key, shared by everything
2
+ // that writes or reads one: the `worker.public_key` column, `uplink.pub` on
3
+ // Home's box, `home.pub` in the worker dir, and the enrollment token
4
+ // `{homePublicKey}.{otp}`. Unpadded base64url, 43 chars for 32 bytes.
5
+
6
+ import { KEY_LENGTH } from "@yourrealm/noise";
7
+
8
+ const ENCODED_LENGTH = 43;
9
+
10
+ export function encodePublicKey(key: Uint8Array): string {
11
+ if (key.length !== KEY_LENGTH) {
12
+ throw new RangeError(`public key must be ${KEY_LENGTH} bytes, got ${key.length}`);
13
+ }
14
+ return key.toBase64({ alphabet: "base64url", omitPadding: true });
15
+ }
16
+
17
+ /** Returns null for anything that is not exactly one encoded key. */
18
+ export function decodePublicKey(text: string): Uint8Array | null {
19
+ if (text.length !== ENCODED_LENGTH || !/^[A-Za-z0-9_-]+$/.test(text)) return null;
20
+ try {
21
+ const key = Uint8Array.fromBase64(text, { alphabet: "base64url" });
22
+ return key.length === KEY_LENGTH ? key : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /** Splits `{homePublicKey}.{otp}`; a bare OTP has no key half. */
29
+ export function parseEnrollmentToken(
30
+ token: string,
31
+ ): { homePublicKey: Uint8Array | null; otp: string } | null {
32
+ const trimmed = token.trim();
33
+ if (trimmed === "") return null;
34
+ const dot = trimmed.indexOf(".");
35
+ if (dot === -1) return { homePublicKey: null, otp: trimmed };
36
+ const homePublicKey = decodePublicKey(trimmed.slice(0, dot));
37
+ const otp = trimmed.slice(dot + 1);
38
+ if (homePublicKey === null || otp === "") return null;
39
+ return { homePublicKey, otp };
40
+ }
@@ -0,0 +1,63 @@
1
+ // @yourrealm/sdk/worker-protocol/uplink — one uplink session and everything
2
+ // that rides it: the Noise handshake over a WebSocket, the mux, the RPC
3
+ // framing and the method catalogue.
4
+ //
5
+ // The layers below the session know nothing about Noise, WebSockets, Home or
6
+ // workers: `Mux` and `Rpc` run over any `ByteDuplex` and own no timers.
7
+ // `connectUplink` / `acceptUplink` are what join them to a socket.
8
+
9
+ export { Mux } from "./mux.ts";
10
+ export type { ByteDuplex, Limits, MuxSide, Stream } from "./mux.ts";
11
+ export { jsonResponse, readJson, Rpc } from "./rpc.ts";
12
+ export type { RpcHandler, RpcRequest, RpcResponse } from "./rpc.ts";
13
+ export { acceptUplink, connectUplink } from "./session.ts";
14
+ export type { SessionOptions, UplinkSession } from "./session.ts";
15
+ export { decodePublicKey, encodePublicKey, parseEnrollmentToken } from "./identity.ts";
16
+ export { PROTOCOL_VERSION } from "./protocol.ts";
17
+ export {
18
+ APPLY_MESSAGE_MAX_CHARS,
19
+ appApplyResultSchema,
20
+ callJson,
21
+ callUiProxy,
22
+ ETAG_HEADER,
23
+ jsonHandler,
24
+ MethodError,
25
+ methods,
26
+ pullState,
27
+ UPDATE_ERROR_MAX_CHARS,
28
+ UPDATE_PROJECTS,
29
+ updateStatusSchema,
30
+ } from "./methods.ts";
31
+ export type {
32
+ AppApplyResult,
33
+ JsonMethod,
34
+ MethodName,
35
+ Params,
36
+ Result,
37
+ StatePull,
38
+ UiProxyResult,
39
+ UpdateProject,
40
+ UpdateStatus,
41
+ } from "./methods.ts";
42
+ export {
43
+ HomeKeyMismatch,
44
+ SessionClosed,
45
+ StreamReset,
46
+ UplinkError,
47
+ UplinkProtocolError,
48
+ } from "./errors.ts";
49
+ // The framing itself stays inside: callers get streams, not frames.
50
+ export {
51
+ BACKOFF_BASE_MS,
52
+ BACKOFF_CAP_MS,
53
+ DEFAULT_LIMITS,
54
+ HANDSHAKE_TIMEOUT_MS,
55
+ IDLE_TIMEOUT_MS,
56
+ MAX_PAYLOAD_BYTES,
57
+ PING_INTERVAL_MS,
58
+ PONG_TIMEOUT_MS,
59
+ REKEY_INTERVAL_MS,
60
+ REKEY_JITTER_MS,
61
+ SOCKET_HIGH_WATER,
62
+ UPLINK_PROLOGUE,
63
+ } from "./constants.ts";