@nervur-org/dock 0.3.0

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 (285) hide show
  1. package/GETTING_STARTED.md +128 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +37 -0
  5. package/api/quo-api.md +196 -0
  6. package/api/route.ts +227 -0
  7. package/beings/GLOSSARY.md +118 -0
  8. package/beings/WORLDS.md +567 -0
  9. package/beings/avatar.ts +101 -0
  10. package/beings/carry.ts +159 -0
  11. package/beings/clock.ts +552 -0
  12. package/beings/courier.ts +107 -0
  13. package/beings/desk.ts +128 -0
  14. package/beings/doorbell.ts +129 -0
  15. package/beings/envoy.ts +218 -0
  16. package/beings/index.ts +16 -0
  17. package/beings/join.ts +51 -0
  18. package/beings/link.ts +104 -0
  19. package/beings/look.ts +139 -0
  20. package/beings/porter.ts +43 -0
  21. package/beings/presence.ts +39 -0
  22. package/beings/quo-app-bells.md +280 -0
  23. package/beings/quo-dock.md +1942 -0
  24. package/beings/setup.ts +45 -0
  25. package/beings/side.ts +60 -0
  26. package/beings/user.ts +459 -0
  27. package/beings/way.ts +65 -0
  28. package/cli/bundles.ts +20 -0
  29. package/cli/client.ts +73 -0
  30. package/cli/daemon.ts +581 -0
  31. package/cli/estate/Caddyfile +33 -0
  32. package/cli/estate/quo.service +35 -0
  33. package/cli/estate/systems.quo.plist +44 -0
  34. package/cli/estate.ts +48 -0
  35. package/cli/folder.ts +27 -0
  36. package/cli/oauth.ts +32 -0
  37. package/cli/pilot.ts +38 -0
  38. package/cli/quo-dock.md +1942 -0
  39. package/cli/quo.ts +203 -0
  40. package/cli/stand.ts +160 -0
  41. package/dist/api/route.d.ts +21 -0
  42. package/dist/api/route.js +193 -0
  43. package/dist/beings/avatar.d.ts +32 -0
  44. package/dist/beings/avatar.js +100 -0
  45. package/dist/beings/carry.d.ts +12 -0
  46. package/dist/beings/carry.js +174 -0
  47. package/dist/beings/clock.d.ts +161 -0
  48. package/dist/beings/clock.js +528 -0
  49. package/dist/beings/courier.d.ts +50 -0
  50. package/dist/beings/courier.js +96 -0
  51. package/dist/beings/desk.d.ts +53 -0
  52. package/dist/beings/desk.js +135 -0
  53. package/dist/beings/doorbell.d.ts +78 -0
  54. package/dist/beings/doorbell.js +133 -0
  55. package/dist/beings/envoy.d.ts +54 -0
  56. package/dist/beings/envoy.js +211 -0
  57. package/dist/beings/index.d.ts +14 -0
  58. package/dist/beings/index.js +16 -0
  59. package/dist/beings/join.d.ts +15 -0
  60. package/dist/beings/join.js +42 -0
  61. package/dist/beings/link.d.ts +14 -0
  62. package/dist/beings/link.js +91 -0
  63. package/dist/beings/look.d.ts +30 -0
  64. package/dist/beings/look.js +104 -0
  65. package/dist/beings/porter.d.ts +14 -0
  66. package/dist/beings/porter.js +41 -0
  67. package/dist/beings/presence.d.ts +8 -0
  68. package/dist/beings/presence.js +21 -0
  69. package/dist/beings/setup.d.ts +3 -0
  70. package/dist/beings/setup.js +39 -0
  71. package/dist/beings/side.d.ts +32 -0
  72. package/dist/beings/side.js +37 -0
  73. package/dist/beings/user.d.ts +315 -0
  74. package/dist/beings/user.js +462 -0
  75. package/dist/beings/way.d.ts +19 -0
  76. package/dist/beings/way.js +28 -0
  77. package/dist/cli/bundles.d.ts +5 -0
  78. package/dist/cli/bundles.js +17 -0
  79. package/dist/cli/client.d.ts +15 -0
  80. package/dist/cli/client.js +58 -0
  81. package/dist/cli/daemon.d.ts +50 -0
  82. package/dist/cli/daemon.js +584 -0
  83. package/dist/cli/estate/Caddyfile +33 -0
  84. package/dist/cli/estate/quo.service +35 -0
  85. package/dist/cli/estate/systems.quo.plist +44 -0
  86. package/dist/cli/estate.d.ts +5 -0
  87. package/dist/cli/estate.js +50 -0
  88. package/dist/cli/folder.d.ts +2 -0
  89. package/dist/cli/folder.js +25 -0
  90. package/dist/cli/oauth.d.ts +2 -0
  91. package/dist/cli/oauth.js +32 -0
  92. package/dist/cli/pilot.d.ts +5 -0
  93. package/dist/cli/pilot.js +22 -0
  94. package/dist/cli/quo.d.ts +2 -0
  95. package/dist/cli/quo.js +201 -0
  96. package/dist/cli/stand.d.ts +37 -0
  97. package/dist/cli/stand.js +110 -0
  98. package/dist/dock/agents.d.ts +102 -0
  99. package/dist/dock/agents.js +105 -0
  100. package/dist/dock/apns.d.ts +27 -0
  101. package/dist/dock/apns.js +98 -0
  102. package/dist/dock/console.d.ts +6 -0
  103. package/dist/dock/console.js +20 -0
  104. package/dist/dock/dialers.d.ts +56 -0
  105. package/dist/dock/dialers.js +75 -0
  106. package/dist/dock/faculty.d.ts +24 -0
  107. package/dist/dock/faculty.js +142 -0
  108. package/dist/dock/fcm.d.ts +13 -0
  109. package/dist/dock/fcm.js +77 -0
  110. package/dist/dock/grants.d.ts +41 -0
  111. package/dist/dock/grants.js +99 -0
  112. package/dist/dock/http2.d.ts +4 -0
  113. package/dist/dock/http2.js +68 -0
  114. package/dist/dock/index.d.ts +41 -0
  115. package/dist/dock/index.js +113 -0
  116. package/dist/dock/local.d.ts +13 -0
  117. package/dist/dock/local.js +44 -0
  118. package/dist/dock/location.d.ts +19 -0
  119. package/dist/dock/location.js +11 -0
  120. package/dist/dock/notifications.d.ts +38 -0
  121. package/dist/dock/notifications.js +60 -0
  122. package/dist/dock/push.d.ts +79 -0
  123. package/dist/dock/push.js +132 -0
  124. package/dist/dock/service.d.ts +4 -0
  125. package/dist/dock/service.js +107 -0
  126. package/dist/dock/timer.d.ts +52 -0
  127. package/dist/dock/timer.js +149 -0
  128. package/dist/dock/webpush.d.ts +12 -0
  129. package/dist/dock/webpush.js +144 -0
  130. package/dist/harbor/browser.d.ts +15 -0
  131. package/dist/harbor/browser.js +82 -0
  132. package/dist/harbor/capacitor.d.ts +28 -0
  133. package/dist/harbor/capacitor.js +218 -0
  134. package/dist/harbor/disk.d.ts +27 -0
  135. package/dist/harbor/disk.js +249 -0
  136. package/dist/harbor/edge/alarm.d.ts +10 -0
  137. package/dist/harbor/edge/alarm.js +38 -0
  138. package/dist/harbor/edge/edge.d.ts +54 -0
  139. package/dist/harbor/edge/edge.js +370 -0
  140. package/dist/harbor/edge/exercise.d.ts +7 -0
  141. package/dist/harbor/edge/exercise.js +232 -0
  142. package/dist/harbor/edge/given.d.ts +26 -0
  143. package/dist/harbor/edge/given.js +42 -0
  144. package/dist/harbor/edge/platform.d.ts +39 -0
  145. package/dist/harbor/edge/storage.d.ts +30 -0
  146. package/dist/harbor/edge/storage.js +180 -0
  147. package/dist/harbor/edge/worker.d.ts +17 -0
  148. package/dist/harbor/edge/worker.js +67 -0
  149. package/dist/harbor/files.d.ts +15 -0
  150. package/dist/harbor/files.js +220 -0
  151. package/dist/harbor/http.d.ts +19 -0
  152. package/dist/harbor/http.js +177 -0
  153. package/dist/harbor/idb.d.ts +15 -0
  154. package/dist/harbor/idb.js +0 -0
  155. package/dist/harbor/quo.d.ts +10 -0
  156. package/dist/harbor/quo.js +42 -0
  157. package/dist/harbor/resolve.d.ts +16 -0
  158. package/dist/harbor/resolve.js +17 -0
  159. package/dist/harbor/seal.d.ts +3 -0
  160. package/dist/harbor/seal.js +25 -0
  161. package/dist/harbor/sealed.d.ts +35 -0
  162. package/dist/harbor/sealed.js +162 -0
  163. package/dist/harbor/tauri.d.ts +16 -0
  164. package/dist/harbor/tauri.js +59 -0
  165. package/dist/harbor/ward.d.ts +9 -0
  166. package/dist/harbor/ward.js +15 -0
  167. package/dist/human/app.d.ts +32 -0
  168. package/dist/human/app.js +527 -0
  169. package/dist/human/apps.d.ts +10 -0
  170. package/dist/human/apps.js +29 -0
  171. package/dist/human/dom.d.ts +2 -0
  172. package/dist/human/dom.js +15 -0
  173. package/dist/human/door.d.ts +19 -0
  174. package/dist/human/door.js +92 -0
  175. package/dist/human/guest.d.ts +3 -0
  176. package/dist/human/guest.js +33 -0
  177. package/dist/human/html.d.ts +55 -0
  178. package/dist/human/html.js +451 -0
  179. package/dist/human/local.d.ts +7 -0
  180. package/dist/human/local.js +10 -0
  181. package/dist/human/mark.d.ts +8 -0
  182. package/dist/human/mark.js +47 -0
  183. package/dist/human/move.d.ts +17 -0
  184. package/dist/human/move.js +90 -0
  185. package/dist/human/screen.d.ts +19 -0
  186. package/dist/human/screen.js +119 -0
  187. package/dist/human/style.d.ts +2 -0
  188. package/dist/human/style.js +60 -0
  189. package/dist/human/tab.bundle.txt +4775 -0
  190. package/dist/human/tab.d.ts +15 -0
  191. package/dist/human/tab.js +409 -0
  192. package/dist/human/tree.d.ts +50 -0
  193. package/dist/human/tree.js +157 -0
  194. package/dist/human/wake.d.ts +34 -0
  195. package/dist/human/wake.js +179 -0
  196. package/dist/human/web.d.ts +33 -0
  197. package/dist/human/web.js +184 -0
  198. package/dist/human/worker.bundle.txt +3868 -0
  199. package/dist/human/worker.d.ts +1 -0
  200. package/dist/human/worker.js +81 -0
  201. package/dist/human/worlds.d.ts +24 -0
  202. package/dist/human/worlds.js +99 -0
  203. package/dist/mcp/agent.d.ts +10 -0
  204. package/dist/mcp/agent.js +78 -0
  205. package/dist/mcp/allow.d.ts +20 -0
  206. package/dist/mcp/allow.js +88 -0
  207. package/dist/mcp/http.d.ts +27 -0
  208. package/dist/mcp/http.js +92 -0
  209. package/dist/mcp/oauth.d.ts +91 -0
  210. package/dist/mcp/oauth.js +241 -0
  211. package/dist/mcp/route.d.ts +15 -0
  212. package/dist/mcp/route.js +42 -0
  213. package/dist/mcp/runner.d.ts +45 -0
  214. package/dist/mcp/runner.js +99 -0
  215. package/dist/mcp/server.d.ts +11 -0
  216. package/dist/mcp/server.js +102 -0
  217. package/dock/agents.ts +121 -0
  218. package/dock/apns.ts +138 -0
  219. package/dock/console.ts +23 -0
  220. package/dock/dialers.ts +85 -0
  221. package/dock/faculty.ts +148 -0
  222. package/dock/fcm.ts +103 -0
  223. package/dock/grants.ts +111 -0
  224. package/dock/http2.ts +70 -0
  225. package/dock/index.ts +133 -0
  226. package/dock/local.ts +48 -0
  227. package/dock/location.ts +22 -0
  228. package/dock/notifications.ts +67 -0
  229. package/dock/push.ts +161 -0
  230. package/dock/quo-app-bells.md +280 -0
  231. package/dock/quo-dock.md +1942 -0
  232. package/dock/service.ts +100 -0
  233. package/dock/timer.ts +174 -0
  234. package/dock/webpush.ts +182 -0
  235. package/harbor/browser.ts +97 -0
  236. package/harbor/capacitor.ts +223 -0
  237. package/harbor/disk.ts +237 -0
  238. package/harbor/edge/alarm.ts +48 -0
  239. package/harbor/edge/edge.ts +407 -0
  240. package/harbor/edge/exercise.ts +241 -0
  241. package/harbor/edge/given.ts +106 -0
  242. package/harbor/edge/platform.d.ts +39 -0
  243. package/harbor/edge/storage.ts +179 -0
  244. package/harbor/edge/text.d.ts +15 -0
  245. package/harbor/edge/worker.ts +70 -0
  246. package/harbor/edge/wrangler.toml +30 -0
  247. package/harbor/files.ts +221 -0
  248. package/harbor/http.ts +188 -0
  249. package/harbor/idb.ts +0 -0
  250. package/harbor/quo-harbor.md +850 -0
  251. package/harbor/quo.ts +44 -0
  252. package/harbor/resolve.ts +31 -0
  253. package/harbor/seal.ts +26 -0
  254. package/harbor/sealed.ts +183 -0
  255. package/harbor/tauri.ts +79 -0
  256. package/harbor/ward.ts +28 -0
  257. package/human/app.ts +580 -0
  258. package/human/apps.ts +47 -0
  259. package/human/dom.ts +21 -0
  260. package/human/door.ts +113 -0
  261. package/human/guest.ts +34 -0
  262. package/human/html.ts +490 -0
  263. package/human/local.ts +30 -0
  264. package/human/mark.ts +53 -0
  265. package/human/move.ts +122 -0
  266. package/human/quo-human.md +1042 -0
  267. package/human/screen.ts +136 -0
  268. package/human/style.ts +63 -0
  269. package/human/tab.bundle.txt +4775 -0
  270. package/human/tab.ts +446 -0
  271. package/human/tree.ts +217 -0
  272. package/human/wake.ts +191 -0
  273. package/human/web.ts +259 -0
  274. package/human/worker.bundle.txt +3868 -0
  275. package/human/worker.ts +124 -0
  276. package/human/worlds.ts +127 -0
  277. package/mcp/agent.ts +94 -0
  278. package/mcp/allow.ts +131 -0
  279. package/mcp/http.ts +92 -0
  280. package/mcp/oauth.ts +270 -0
  281. package/mcp/quo-mcp.md +343 -0
  282. package/mcp/route.ts +45 -0
  283. package/mcp/runner.ts +136 -0
  284. package/mcp/server.ts +101 -0
  285. package/package.json +121 -0
@@ -0,0 +1,42 @@
1
+ import { GRANTS } from '../../dock/grants.js';
2
+ import { TIMER } from '../../dock/index.js';
3
+ // What a worker lends the beings of the wards it holds, by name to the class
4
+ // that stands for it. The box's state has one home on every terrain, the box
5
+ // ward's partition, so the edge stands that ward like every other terrain;
6
+ // what differs is this list, and a faculty the platform has no answer for is
7
+ // not lent at all rather than lent and hollow.
8
+ //
9
+ // `grants` and `timer` are lent, and they are the whole of it. A bearer to an
10
+ // identity in a world is a record in her cells and nothing else, so the
11
+ // grants are the same code here as on a disk and the routes that honour a
12
+ // bearer, `api.` and `mcp.`, work the moment she stands. The timer is the
13
+ // same class too, with the one method that knows a terrain replaced: she arms
14
+ // the object's own storage alarm, which is the clock here, so a home on this
15
+ // terrain wakes for its schedules with nobody asking. `Alarm` in
16
+ // `edge/alarm.ts` is that subclass.
17
+ //
18
+ // The three others every box ships are not lent here, each for its own
19
+ // reason. `location` and `notifications` are a place and a screen, and a
20
+ // worker is at neither: it runs wherever the platform put it and no human is
21
+ // looking at it. `push` rings under this world's own web origin, and a worker
22
+ // learns its origin from the request it is answering and not at boot, so a
23
+ // ring signed at the moment the faculty stood would be signed for nowhere.
24
+ //
25
+ // There is no process on the other side of a worker either, so `local`,
26
+ // `dialers` and `agents`, which the daemon adds, name nothing here: no
27
+ // socket a process was seen on, no line this box holds open, and no command
28
+ // to run an agent in.
29
+ export const LENT = { [GRANTS]: 'Grants', [TIMER]: 'Alarm' };
30
+ // The ceiling on one row in this object's storage, key and value together,
31
+ // in bytes. The platform publishes it for a SQLite-backed Durable Object,
32
+ // which is what `wrangler.toml` declares this class to be, and past it the
33
+ // storage refuses the write. It is written here once, beside the rest of
34
+ // what this terrain is, because a number a store carries in its own body is
35
+ // a number nobody finds when the platform moves it.
36
+ //
37
+ // A being is one row, so this is the ceiling on one being's cells and her
38
+ // bind table. The store measures the JSON it is about to write and refuses
39
+ // over it rather than handing the platform a value it will refuse anyway, so
40
+ // the refusal is the same on the day the platform's own message changes and
41
+ // the same in a suite that never reaches a real object.
42
+ export const ROW_CEILING = 2_000_000;
@@ -0,0 +1,39 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The edge platform, as much of it as the harbor touches and no more. The
3
+ // vendor's own type package declares every global a worker has, and it
4
+ // collides with the DOM library this tree compiles against; these few
5
+ // lines are what the edge harbor and worker actually call.
6
+ // The rows: everything the object keeps under a key. Whatever answers these
7
+ // five is a place to keep rows, which is why what is kept in storage is
8
+ // written against this and not against the object.
9
+ export type Rows = {
10
+ get<T = unknown>(key: string): Promise<T | undefined>;
11
+ get<T = unknown>(keys: string[]): Promise<Map<string, T>>;
12
+ put(key: string, value: unknown): Promise<void>;
13
+ delete(key: string): Promise<boolean>;
14
+ list<T = unknown>(options?: { prefix?: string }): Promise<Map<string, T>>;
15
+ };
16
+ // The object's storage: its rows and its clock. One alarm per object,
17
+ // replaced by the next set and gone when it is deleted, and the object's
18
+ // `alarm` handler runs when it goes off, with no request behind it.
19
+ export type Storage = Rows & {
20
+ setAlarm(at: number | Date): Promise<void>;
21
+ getAlarm(): Promise<number | null>;
22
+ deleteAlarm(): Promise<void>;
23
+ };
24
+ export type State = { id: { toString(): string; name?: string }; storage: Storage };
25
+ export type Stub = { fetch(input: string | Request, init?: RequestInit): Promise<Response> };
26
+ export type Namespace = { idFromName(name: string): unknown; get(id: unknown): Stub };
27
+ export type Env = { HARBOR: Namespace; QUO_SEED_KEY?: string; QUO_ROOT?: string; QUO_EXERCISE?: string };
28
+ // The server end of a WebSocketPair: the standard surface plus accept.
29
+ export type ServerSocket = { accept(): void; readyState: number; binaryType: string; send(data: string | Uint8Array): void; close(): void; addEventListener(type: string, fn: (e: never) => void): void };
30
+ declare global {
31
+ // eslint-disable-next-line no-var
32
+ var WebSocketPair: new () => { 0: ServerSocket; 1: ServerSocket };
33
+ interface ResponseInit {
34
+ webSocket?: unknown;
35
+ }
36
+ interface Response {
37
+ readonly webSocket?: unknown;
38
+ }
39
+ }
@@ -0,0 +1,30 @@
1
+ import { type Kept, type Store, type WardRecord } from '@nervur-org/nervur/harbor';
2
+ import { type Store as DanceStore } from '../../mcp/oauth.ts';
3
+ import type { Folder } from '../../human/web.ts';
4
+ import { type SettingsStore } from './given.ts';
5
+ import type { Rows, Storage } from './platform.d.ts';
6
+ export declare function settingsInStorage(storage: Rows, prefix?: string): SettingsStore;
7
+ export declare const DESIGN_ROW = "design:";
8
+ export declare const BEINGS_ROW = "beings";
9
+ export declare function folderInStorage(storage: Rows, prefix?: string): Folder;
10
+ export declare function wearInStorage(storage: Rows, prefix?: string): {
11
+ put: (file: string, text: string | null) => Promise<void>;
12
+ worn: () => Promise<string[]>;
13
+ };
14
+ export declare function danceInStorage(storage: Rows, prefix?: string): DanceStore;
15
+ export declare class DurableStorage implements Store {
16
+ #private;
17
+ readonly storage: Storage;
18
+ readonly key: CryptoKey;
19
+ readonly prefix: string;
20
+ readonly ceiling: number;
21
+ constructor(storage: Storage, key: CryptoKey, prefix?: string, ceiling?: number);
22
+ list(): Promise<string[]>;
23
+ load(name: string): Promise<Kept | undefined>;
24
+ put(name: string, kept: Kept): Promise<void>;
25
+ save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
26
+ record(name: string, record: WardRecord): Promise<void>;
27
+ take(name: string): Promise<Kept | undefined>;
28
+ hints(): Promise<Record<string, string>>;
29
+ hint(pk: string, url: string): Promise<void>;
30
+ }
@@ -0,0 +1,180 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The store as Durable Object storage: one object per harbor, its storage
3
+ // holding one row per ward under `ward:<name>`, seed and record, one row per
4
+ // row of its partition under `row:<name> <row>`, and one row per hint under
5
+ // `hint:<pk>`. A being is a row of her own because a being's cells are the
6
+ // one part of a partition that grows without limit, and a ward kept as one
7
+ // value would write every being in it down whenever any one was asked.
8
+ //
9
+ // The seed is kept sealed under a
10
+ // key from the platform's secrets, `QUO_SEED_KEY`, with the seal in
11
+ // `../seal.ts`, so the storage holds ciphertext and the secret store holds
12
+ // the one key, which is what the harbor document's table says the edge
13
+ // does. Values cross as JSON, as they do into a file, because the ward
14
+ // hands the partition out through a guard that structured clone refuses.
15
+ // The same storage answers the mcp route's dance, under `oauth`, the box's
16
+ // own settings, under `settings`, and the design a world wears, one row per
17
+ // file under `design:<file>`, because the object is the whole of what this
18
+ // terrain has to keep anything in.
19
+ import { fromRows, rowsIn, rowsOf } from '@nervur-org/nervur/harbor';
20
+ import { seal, open } from '../seal.js';
21
+ import { emptyDance } from '../../mcp/oauth.js';
22
+ import { ROW_CEILING } from './given.js';
23
+ // The box's settings: the origins, the apps table and the bell key, one row
24
+ // read whole and written whole. Nothing of Quo's is in it, so nothing here
25
+ // is sealed, and a box a root has never configured reads an empty one.
26
+ export function settingsInStorage(storage, prefix = '') {
27
+ const key = `${prefix}settings`;
28
+ return {
29
+ load: async () => (await storage.get(key)) ?? {},
30
+ save: (settings) => storage.put(key, settings),
31
+ };
32
+ }
33
+ // The design a world wears and the world's own code for the tab, as rows:
34
+ // what a disk answers out of `design/` and `tab/index.js`, answered out of
35
+ // storage instead. A row is text, because a stylesheet is text and so is a
36
+ // bundle; a file the root has not put there answers nothing and the route
37
+ // declines the path.
38
+ export const DESIGN_ROW = 'design:';
39
+ export const BEINGS_ROW = 'beings';
40
+ export function folderInStorage(storage, prefix = '') {
41
+ const design = (file) => `${prefix}${DESIGN_ROW}${file}`;
42
+ const held = (file) => storage.get(design(file)).then((v) => v ?? null);
43
+ return {
44
+ wearing: async () => ((await held('tokens.css')) === null ? { tokens: false, fonts: false } : { tokens: true, fonts: (await held('fonts.css')) !== null }),
45
+ design: (file) => held(file),
46
+ serving: async () => (await storage.get(`${prefix}${BEINGS_ROW}`)) !== undefined,
47
+ beings: async () => (await storage.get(`${prefix}${BEINGS_ROW}`)) ?? null,
48
+ };
49
+ }
50
+ // The writing half of that folder, which is the root's door and nothing
51
+ // else: a file put there, a file taken away with null, and the names now
52
+ // held so that a root sees what it wrote.
53
+ export function wearInStorage(storage, prefix = '') {
54
+ const head = `${prefix}${DESIGN_ROW}`;
55
+ return {
56
+ put: async (file, text) => {
57
+ if (text === null)
58
+ await storage.delete(head + file);
59
+ else
60
+ await storage.put(head + file, text);
61
+ },
62
+ worn: async () => [...(await storage.list({ prefix: head })).keys()].map((k) => k.slice(head.length)),
63
+ };
64
+ }
65
+ // The mcp route's dance, kept in the same object's storage under one key.
66
+ // It is one value and not a row apiece: clients, pending requests and codes
67
+ // all die in minutes, and the whole of it is smaller than one ward's seed.
68
+ // Nothing of Quo's is in it, so nothing here is sealed.
69
+ export function danceInStorage(storage, prefix = '') {
70
+ const key = `${prefix}oauth`;
71
+ return {
72
+ load: async () => ({ ...emptyDance(), ...((await storage.get(key)) ?? {}) }),
73
+ save: (dance) => storage.put(key, dance),
74
+ };
75
+ }
76
+ // `prefix` keeps more than one harbor apart in one object's storage: the
77
+ // exercise does that, a deployment never does.
78
+ // `ceiling` is the platform's, and is a parameter for the one suite that
79
+ // has to see a row refused: a row of the real size crossing a real door
80
+ // costs more than the fault it proves, and the ceiling a deployment runs
81
+ // under is the platform's number and never an argument.
82
+ export class DurableStorage {
83
+ storage;
84
+ key;
85
+ prefix;
86
+ ceiling;
87
+ constructor(storage, key, prefix = '', ceiling = ROW_CEILING) {
88
+ this.storage = storage;
89
+ this.key = key;
90
+ this.prefix = prefix;
91
+ this.ceiling = ceiling;
92
+ }
93
+ #k(kind, name) {
94
+ return `${this.prefix}${kind}:${name}`;
95
+ }
96
+ // A ward's rows sort together under one prefix, and nothing else falls
97
+ // between them: no name carries the separator.
98
+ #rows(name) {
99
+ return this.#k('row', `${name} `);
100
+ }
101
+ async list() {
102
+ const head = this.#k('ward', '');
103
+ return [...(await this.storage.list({ prefix: head })).keys()].map((k) => k.slice(head.length));
104
+ }
105
+ async load(name) {
106
+ const row = await this.storage.get(this.#k('ward', name));
107
+ if (!row)
108
+ return undefined;
109
+ const head = this.#rows(name);
110
+ const kept = [...(await this.storage.list({ prefix: head }))].map(([k, v]) => [k.slice(head.length), v]);
111
+ return { seed: await open(this.key, row.seed), partition: fromRows(Object.fromEntries(kept)), record: row.record };
112
+ }
113
+ async put(name, kept) {
114
+ if ((await this.storage.get(this.#k('ward', name))) !== undefined)
115
+ throw new Error(`ward ${name} already exists here`);
116
+ await this.storage.put(this.#k('ward', name), { seed: await seal(this.key, kept.seed), record: kept.record });
117
+ await this.#keep(name, kept.partition, rowsIn(kept.partition));
118
+ }
119
+ async save(name, partition, rows) {
120
+ if ((await this.storage.get(this.#k('ward', name))) === undefined)
121
+ return; // a name not kept is nothing
122
+ await this.#keep(name, partition, rows);
123
+ }
124
+ // The rows named, written or deleted. A row whose being has left comes back
125
+ // undefined and her value goes with her.
126
+ //
127
+ // A row past the object's ceiling is refused here, before the platform is
128
+ // asked. The platform refuses it too, and the two refusals must be the
129
+ // same one: a store that let the platform decide would keep a row on the
130
+ // day the limit moved and refuse it the day after, with nothing in this
131
+ // tree saying so. The harbor turns the throw into the answer of the ask
132
+ // that wrote the row, and the being is stood back at what is kept.
133
+ //
134
+ // Every row is measured before any is written, so a save that is refused
135
+ // writes nothing at all. A store that wrote half a save and threw would
136
+ // leave one being moved and her counterpart not, and the harbor standing
137
+ // the ward back at what is kept would find a torn state to stand at: a
138
+ // caller's numbers rolled back and the door's mark not, which is a
139
+ // relation that can never speak again.
140
+ async #keep(name, partition, rows) {
141
+ const head = this.#rows(name);
142
+ const write = rowsOf(partition, rows).map(([row, value]) => {
143
+ const key = head + row;
144
+ if (value === undefined)
145
+ return { key, value };
146
+ const size = new TextEncoder().encode(JSON.stringify(value)).length + new TextEncoder().encode(key).length;
147
+ if (size > this.ceiling)
148
+ throw new Error(`the row ${key} is ${size} bytes, past this object's ceiling of ${this.ceiling}`);
149
+ return { key, value };
150
+ });
151
+ for (const { key, value } of write) {
152
+ if (value === undefined)
153
+ await this.storage.delete(key);
154
+ else
155
+ await this.storage.put(key, value);
156
+ }
157
+ }
158
+ async record(name, record) {
159
+ const row = await this.storage.get(this.#k('ward', name));
160
+ if (row)
161
+ await this.storage.put(this.#k('ward', name), { ...row, record });
162
+ }
163
+ async take(name) {
164
+ const kept = await this.load(name);
165
+ if (!kept)
166
+ return undefined;
167
+ const head = this.#rows(name);
168
+ for (const key of (await this.storage.list({ prefix: head })).keys())
169
+ await this.storage.delete(key); // her rows go with her, or the next put reads them
170
+ await this.storage.delete(this.#k('ward', name));
171
+ return kept;
172
+ }
173
+ async hints() {
174
+ const head = this.#k('hint', '');
175
+ return Object.fromEntries([...(await this.storage.list({ prefix: head }))].map(([k, v]) => [k.slice(head.length), v]));
176
+ }
177
+ async hint(pk, url) {
178
+ await this.storage.put(this.#k('hint', pk), url);
179
+ }
180
+ }
@@ -0,0 +1,17 @@
1
+ import { EdgeHarbor } from './edge.ts';
2
+ import type { Deployed } from './given.ts';
3
+ import type { State, Env } from './platform.d.ts';
4
+ export declare const DEPLOYED: Deployed;
5
+ export declare class Quo {
6
+ #private;
7
+ readonly state: State;
8
+ readonly env: Env;
9
+ constructor(state: State, env: Env);
10
+ harbor(): Promise<EdgeHarbor>;
11
+ alarm(): Promise<void>;
12
+ fetch(request: Request): Promise<Response>;
13
+ }
14
+ declare const _default: {
15
+ fetch(request: Request, env: Env): Promise<Response>;
16
+ };
17
+ export default _default;
@@ -0,0 +1,67 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The worker: the edge harbor deployed. One Durable Object class, `Quo`,
3
+ // one instance per harbor name, and the worker routes by the first path
4
+ // segment: `/h/<name>/...` goes to that harbor. A world's quo. route on the
5
+ // edge is `<origin>/h/<name>/quo`, and its health `<origin>/h/<name>/health`.
6
+ // `wrangler.toml` beside this file names the class and the secrets.
7
+ //
8
+ // One thing lives here for the terrain test alone: with `QUO_EXERCISE` set,
9
+ // which no deployment sets, `/exercise` runs the library's conformance
10
+ // suite inside the platform against two harbors in this one object, over
11
+ // two prefixed views of its storage, reaching each other by request
12
+ // through the worker under `/x/<sub>/quo`. One object cannot touch
13
+ // another's storage, the platform says so, and so the two harbors share an
14
+ // object the way two tabs share a page.
15
+ import { EdgeHarbor } from './edge.js';
16
+ import { exercise } from './exercise.js';
17
+ import tab from '../../human/tab.bundle.txt';
18
+ import serviceWorker from '../../human/worker.bundle.txt';
19
+ // What the deploy carries, from this module's first line: the tab's bundle
20
+ // and the service worker's, as text modules the `wrangler.toml` names. They
21
+ // are built artefacts and never rows, so they arrive with the code and not
22
+ // out of the object's storage. Everything a root configures while the worker
23
+ // runs is a row, and `given.ts` holds the whole shape.
24
+ export const DEPLOYED = { code: { tab, worker: serviceWorker } };
25
+ export class Quo {
26
+ state;
27
+ env;
28
+ #harbor;
29
+ constructor(state, env) {
30
+ this.state = state;
31
+ this.env = env;
32
+ }
33
+ harbor() {
34
+ return (this.#harbor ??= EdgeHarbor.open(this.state, this.env, DEPLOYED));
35
+ }
36
+ // The platform's one way of waking an object with nobody asking. The
37
+ // harbor opens as it does for a request, every ward boots from storage,
38
+ // and the box's timer runs her round: a home here with a sweep at three in
39
+ // the morning wakes once a night, and no request is made to cause it.
40
+ async alarm() {
41
+ const harbor = await this.harbor();
42
+ await harbor.ready();
43
+ await harbor.rang();
44
+ }
45
+ async fetch(request) {
46
+ const url = new URL(request.url);
47
+ const m = /^\/h\/([\w.-]+)(\/.*)?$/.exec(url.pathname);
48
+ if (!m)
49
+ return new Response(JSON.stringify({ error: 'no such route' }), { status: 404, headers: { 'content-type': 'application/json' } });
50
+ const path = m[2] ?? '/';
51
+ // Where this harbor answers, as the world outside sees it: the routes a
52
+ // page hands its tab are these unless a root has named public ones.
53
+ const base = `${url.origin}/h/${m[1]}`;
54
+ if (this.env.QUO_EXERCISE && (path === '/exercise' || path.startsWith('/x/')))
55
+ return exercise(request, base, path, this.state, this.env);
56
+ return (await this.harbor()).fetch(request, path, base);
57
+ }
58
+ }
59
+ export default {
60
+ async fetch(request, env) {
61
+ const url = new URL(request.url);
62
+ const m = /^\/h\/([\w.-]+)(\/.*)?$/.exec(url.pathname);
63
+ if (!m)
64
+ return new Response(JSON.stringify({ error: 'a harbor is /h/<name>/...' }), { status: 404, headers: { 'content-type': 'application/json' } });
65
+ return env.HARBOR.get(env.HARBOR.idFromName(m[1])).fetch(request);
66
+ },
67
+ };
@@ -0,0 +1,15 @@
1
+ import { type Kept, type Store, type WardRecord } from '@nervur-org/nervur/harbor';
2
+ export declare class Files implements Store {
3
+ #private;
4
+ readonly dir: string;
5
+ constructor(dir: string, key?: string);
6
+ get sealed(): boolean;
7
+ list(): Promise<string[]>;
8
+ load(name: string): Promise<Kept | undefined>;
9
+ put(name: string, kept: Kept): Promise<void>;
10
+ save(name: string, partition: Record<string, unknown>, rows: readonly string[]): Promise<void>;
11
+ record(name: string, record: WardRecord): Promise<void>;
12
+ take(name: string): Promise<Kept | undefined>;
13
+ hints(): Promise<Record<string, string>>;
14
+ hint(pk: string, url: string): Promise<void>;
15
+ }
@@ -0,0 +1,220 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The store as a folder on disk, one folder per ward, in one of two forms.
3
+ //
4
+ // Plain, when the store has no key, what the droplets run:
5
+ //
6
+ // <dir>/wards/<name>/seed 32 bytes, hex, mode 0600
7
+ // <dir>/wards/<name>/rows/head.json the partition's head
8
+ // <dir>/wards/<name>/rows/b-<hex>.json one being, her key as hex utf8
9
+ // <dir>/wards/<name>/ward.json the ward record
10
+ //
11
+ // A being is a file of her own because a being's cells are the one part of a
12
+ // partition that grows without limit, and a ward that wrote one file would
13
+ // write every being in it down whenever any one of them was asked. Her key is
14
+ // a being's own word and can be anything a string can be, so the file is
15
+ // named for its bytes and never for the key itself.
16
+ //
17
+ // Sealed, when the store holds a key, what a device's daemon runs with the
18
+ // key from its Keychain:
19
+ //
20
+ // <dir>/wards/<name>/ward.sealed seed, partition and record as one
21
+ // JSON, sealed under the key, hex
22
+ //
23
+ // and in both:
24
+ //
25
+ // <dir>/reach.json the directory's hints
26
+ //
27
+ // A ward is whole in either form, and a store that meets the other form
28
+ // refuses it by name: a daemon started without its key, or with one against
29
+ // a plain folder, fails loudly instead of booting on what it cannot read.
30
+ // Sealing a plain folder is a deliberate command, never a boot's doing.
31
+ // Every write of a ward goes through a temp file and a rename, and writes
32
+ // are queued per ward so two calls never race on one file.
33
+ import { mkdir, readFile, writeFile, rename, readdir, rm, chmod } from 'node:fs/promises';
34
+ import { existsSync } from 'node:fs';
35
+ import { join } from 'node:path';
36
+ import { arithmetic } from '@nervur-org/nervur/ward';
37
+ import { HEAD, fromRows, rowsIn, rowsOf } from '@nervur-org/nervur/harbor';
38
+ import { sealKey, seal, open } from './seal.js';
39
+ const { hex, unhex } = arithmetic;
40
+ // A row's file. The head has a name of its own so that the empty key it goes
41
+ // by never becomes an empty file name; a being's file is named for the digest
42
+ // of her key, and her key is written inside it.
43
+ //
44
+ // The digest, and not the key itself in some spelling: a being's key is her
45
+ // owner's own word and can be as long as a string can be, and a ward's own
46
+ // key is a hundred and twenty-eight characters before anything is added to
47
+ // it. Every encoding of a key grows it, and a file system stops at a length.
48
+ // A digest is the same size whatever it is given.
49
+ const utf8 = new TextEncoder();
50
+ const fileOf = async (row) => (row === HEAD ? 'head.json' : `b-${hex(await arithmetic.sha256(utf8.encode(row)))}.json`);
51
+ export class Files {
52
+ dir;
53
+ #key;
54
+ #queues = new Map();
55
+ constructor(dir, key) {
56
+ this.dir = dir;
57
+ if (key)
58
+ this.#key = sealKey(key);
59
+ }
60
+ #ward(name) {
61
+ return join(this.dir, 'wards', name);
62
+ }
63
+ get sealed() {
64
+ return this.#key !== undefined;
65
+ }
66
+ // The form a folder holds, checked against the form this store speaks.
67
+ #form(name) {
68
+ const wd = this.#ward(name);
69
+ const form = existsSync(join(wd, 'ward.sealed')) ? 'sealed' : existsSync(join(wd, 'seed')) ? 'plain' : undefined;
70
+ if (form === 'sealed' && !this.sealed)
71
+ throw new Error(`ward ${name} in ${this.dir} is sealed and this harbor has no key`);
72
+ if (form === 'plain' && this.sealed)
73
+ throw new Error(`ward ${name} in ${this.dir} is plain and this harbor holds a key`);
74
+ return form;
75
+ }
76
+ // One write at a time per ward, through a temp file and a rename.
77
+ #write(name, file, body) {
78
+ const next = (this.#queues.get(name) ?? Promise.resolve()).then(async () => {
79
+ const tmp = join(this.#ward(name), `${file}.tmp`);
80
+ await writeFile(tmp, body, { mode: 0o600 });
81
+ await rename(tmp, join(this.#ward(name), file));
82
+ });
83
+ this.#queues.set(name, next.catch(() => { }));
84
+ return next;
85
+ }
86
+ async #read(name) {
87
+ const blob = (await readFile(join(this.#ward(name), 'ward.sealed'), 'utf8')).trim();
88
+ return JSON.parse(new TextDecoder().decode(await open(await this.#key, blob)));
89
+ }
90
+ // A sealed ward is rewritten whole, so the read sits inside the queue
91
+ // with the write: two changes to one ward never lose each other's part.
92
+ #keep(name, change) {
93
+ const next = (this.#queues.get(name) ?? Promise.resolve()).then(async () => {
94
+ const wd = this.#ward(name);
95
+ const blob = change(existsSync(join(wd, 'ward.sealed')) ? await this.#read(name) : undefined);
96
+ const sealed = await seal(await this.#key, new TextEncoder().encode(JSON.stringify(blob)));
97
+ await writeFile(join(wd, 'ward.sealed.tmp'), sealed + '\n', { mode: 0o600 });
98
+ await rename(join(wd, 'ward.sealed.tmp'), join(wd, 'ward.sealed'));
99
+ });
100
+ this.#queues.set(name, next.catch(() => { }));
101
+ return next;
102
+ }
103
+ async list() {
104
+ const wards = join(this.dir, 'wards');
105
+ return existsSync(wards) ? (await readdir(wards)).filter((n) => existsSync(join(wards, n, 'seed')) || existsSync(join(wards, n, 'ward.sealed'))) : [];
106
+ }
107
+ async load(name) {
108
+ const form = this.#form(name);
109
+ if (!form)
110
+ return undefined;
111
+ if (form === 'sealed') {
112
+ const b = await this.#read(name);
113
+ return { seed: unhex(b.seed), partition: b.partition, record: b.record };
114
+ }
115
+ const wd = this.#ward(name);
116
+ return {
117
+ seed: unhex((await readFile(join(wd, 'seed'), 'utf8')).trim()),
118
+ partition: fromRows(await this.#rows(name)),
119
+ record: JSON.parse(await readFile(join(wd, 'ward.json'), 'utf8')),
120
+ };
121
+ }
122
+ // Every row a ward's folder holds. A file the naming does not recognise is
123
+ // not a row and is left where it is: a folder is a place a person looks.
124
+ async #rows(name) {
125
+ const dir = join(this.#ward(name), 'rows');
126
+ if (!existsSync(dir))
127
+ return {};
128
+ const out = {};
129
+ for (const file of await readdir(dir)) {
130
+ if (file === 'head.json')
131
+ out[HEAD] = JSON.parse(await readFile(join(dir, file), 'utf8'));
132
+ else if (/^b-[0-9a-f]{64}\.json$/.test(file)) {
133
+ const kept = JSON.parse(await readFile(join(dir, file), 'utf8'));
134
+ out[kept.row] = kept.value;
135
+ }
136
+ // anything else is not a row and is left where it is: a folder is a
137
+ // place a person looks
138
+ }
139
+ return out;
140
+ }
141
+ // The rows named, written or removed. A row whose being has left comes back
142
+ // undefined and her file goes with her.
143
+ //
144
+ // The whole of it is one job in the ward's line, the folder included, so a
145
+ // save cannot lay a file down inside a ward that a take is removing: `take`
146
+ // waits for this line before it removes anything, and a mkdir outside it
147
+ // would build the folder again underneath.
148
+ #keepRows(name, partition, rows) {
149
+ const written = rowsOf(partition, rows);
150
+ const next = (this.#queues.get(name) ?? Promise.resolve()).then(async () => {
151
+ // Read here and not before: a ward taken while this waited its turn is
152
+ // a ward this must not write, and a mkdir would build its folder again
153
+ // under whoever removed it. A name not kept is nothing, and that is as
154
+ // true at the head of the line as it was at the back of it.
155
+ if (!existsSync(this.#ward(name)))
156
+ return;
157
+ const dir = join(this.#ward(name), 'rows');
158
+ await mkdir(dir, { recursive: true, mode: 0o700 });
159
+ for (const [row, value] of written) {
160
+ const file = join(dir, await fileOf(row));
161
+ if (value === undefined) {
162
+ await rm(file, { force: true });
163
+ continue;
164
+ }
165
+ const tmp = `${file}.tmp`;
166
+ await writeFile(tmp, JSON.stringify(row === HEAD ? value : { row, value }) + '\n', { mode: 0o600 });
167
+ await rename(tmp, file);
168
+ }
169
+ });
170
+ this.#queues.set(name, next.catch(() => { }));
171
+ return next;
172
+ }
173
+ async put(name, kept) {
174
+ if (this.#form(name))
175
+ throw new Error(`ward ${name} already exists in ${this.dir}`);
176
+ const wd = this.#ward(name);
177
+ await mkdir(wd, { recursive: true, mode: 0o700 });
178
+ if (this.sealed)
179
+ return this.#keep(name, () => ({ seed: hex(kept.seed), partition: kept.partition, record: kept.record }));
180
+ await writeFile(join(wd, 'seed'), hex(kept.seed), { mode: 0o600 });
181
+ await chmod(join(wd, 'seed'), 0o600);
182
+ await writeFile(join(wd, 'ward.json'), JSON.stringify(kept.record, null, 2) + '\n', { mode: 0o600 });
183
+ await this.#keepRows(name, kept.partition, rowsIn(kept.partition));
184
+ }
185
+ // A sealed ward is one sealed value and is written whole; a plain one is
186
+ // rows, and only the rows the ward named are touched.
187
+ async save(name, partition, rows) {
188
+ const form = this.#form(name);
189
+ if (!form)
190
+ return; // a name not kept is nothing
191
+ if (form === 'sealed')
192
+ return this.#keep(name, (b) => ({ ...b, partition }));
193
+ return this.#keepRows(name, partition, rows);
194
+ }
195
+ async record(name, record) {
196
+ const form = this.#form(name);
197
+ if (!form)
198
+ return;
199
+ if (form === 'sealed')
200
+ return this.#keep(name, (b) => ({ ...b, record }));
201
+ return this.#write(name, 'ward.json', JSON.stringify(record, null, 2) + '\n');
202
+ }
203
+ async take(name) {
204
+ const kept = await this.load(name);
205
+ if (!kept)
206
+ return undefined;
207
+ await this.#queues.get(name);
208
+ await rm(this.#ward(name), { recursive: true, force: true });
209
+ return kept;
210
+ }
211
+ async hints() {
212
+ const p = join(this.dir, 'reach.json');
213
+ return existsSync(p) ? JSON.parse(await readFile(p, 'utf8')) : {};
214
+ }
215
+ async hint(pk, url) {
216
+ const all = await this.hints();
217
+ all[pk] = url;
218
+ await writeFile(join(this.dir, 'reach.json'), JSON.stringify(all, null, 2) + '\n', { mode: 0o600 });
219
+ }
220
+ }
@@ -0,0 +1,19 @@
1
+ import { type Server as HttpServer } from 'node:http';
2
+ import { Socket as Held } from '@nervur-org/nervur/harbor';
3
+ import type { Dialer } from '@nervur-org/nervur/harbor';
4
+ import type { DiskHarbor } from './disk.ts';
5
+ export type Handler = (req: Request, rest: string) => Response | undefined | Promise<Response | undefined>;
6
+ export type Http = {
7
+ port: number;
8
+ host: string;
9
+ mount(prefix: string, handler: Handler): void;
10
+ };
11
+ export type Quo = {
12
+ sockets: Set<Held>;
13
+ dialers: Dialer[];
14
+ };
15
+ export declare function readAll(req: Request): Promise<Uint8Array | undefined>;
16
+ export declare function readText(req: Request): Promise<string | undefined>;
17
+ export declare function listenHttp(harbor: DiskHarbor, host: string, port: number, quo: Quo): Promise<Http & {
18
+ server: HttpServer;
19
+ }>;