@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,407 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The edge harbor: the core over one Durable Object. The object is the
3
+ // harbor, and the platform is its lease: one instance of it runs at a time,
4
+ // which is the single writer every ward needs. The store is the object's
5
+ // storage with the seed sealed under a platform secret; the code is the
6
+ // deployed worker, the built-in beings plus whatever the worker hands in.
7
+ // It is a listener and never a dialer: reached by request at its route,
8
+ // holding the sockets dialers open to it, the rendezvous for them. It wakes
9
+ // per request, and a held socket keeps it awake.
10
+ //
11
+ // Its owner door is a route, not a socket: the platform has no local
12
+ // process, so the root is whoever holds `QUO_ROOT`, a platform secret, and
13
+ // the root's asks arrive as one POST under it. That is the device's own
14
+ // rule on this terrain, as file permissions are on a disk. The first thing
15
+ // a root does is invite an owner on the ward's pk, and from then on the
16
+ // edge is piloted from elsewhere over a standing, like any ward.
17
+ //
18
+ // POST <route>/quo/<pk> one sealed ask to a pk held here, or on a socket here
19
+ // GET <route>/quo upgrade a dialer's held socket, its pks bound at announce
20
+ // GET <route>/health the wards, the directory, the sockets
21
+ // GET <route>/web/... the worlds' pages, the same route the daemon serves
22
+ // <route>/api/... the program side, the same route the daemon serves
23
+ // <route>/mcp/... the model side, the dance and the endpoint
24
+ // GET <route>/.well-known/ the dance's discovery, for a caller at this prefix
25
+ // POST <route>/root { init: { ward, user } } | { ward?, method?, args? }, under the root secret
26
+ // POST <route>/root { settings } merged and answered, { wear } a design file put or taken
27
+ //
28
+ // What the sides here are given is `given.ts`: the bundles and classes the
29
+ // deploy carries, the settings the root's door writes, and the design rows
30
+ // those pages are painted with. It also holds what this terrain lends, since
31
+ // a worker stands a box ward like every other terrain: the box's state has
32
+ // one home everywhere, and what differs is which faculties that ward lends.
33
+ import type { BeingClass } from '@nervur-org/nervur';
34
+ import { isSilence } from '@nervur-org/nervur';
35
+ import { User, Desk, Avatar, Clock } from '../../beings/index.ts';
36
+ import { setup } from '../../beings/setup.ts';
37
+ import { Harbor, Socket as Held } from '@nervur-org/nervur/harbor';
38
+ import { quoRoute } from '../quo.ts';
39
+ import { webRoute, type Folder } from '../../human/web.ts';
40
+ import { DOCK, TIMER, Console, facultyOf, settled, stand } from '../../dock/index.ts';
41
+ import { Alarm, alarmsIn } from './alarm.ts';
42
+ import { Grants, GRANTS } from '../../dock/grants.ts';
43
+ import { find, letIn } from '../../beings/join.ts';
44
+ import { apiRoute } from '../../api/route.ts';
45
+ import { mcpRoute, type Routes } from '../../mcp/route.ts';
46
+ import { McpHttp } from '../../mcp/http.ts';
47
+ import { Exchange } from '../../mcp/allow.ts';
48
+ import type { Invitation } from '@nervur-org/nervur';
49
+ import { DurableStorage, danceInStorage, folderInStorage, settingsInStorage, wearInStorage } from './storage.ts';
50
+ import { LENT, type Deployed, type Given, type Settings, type SettingsStore } from './given.ts';
51
+ import { sealKey } from '../seal.ts';
52
+ import type { State, Env, Storage } from './platform.d.ts';
53
+
54
+ // The beings a deployed worker carries. The Clock is among them because this
55
+ // terrain lends a timer: a box that lends one and holds no class that can ask
56
+ // for a wake lends a name nobody there could use.
57
+ export const BUILT_IN: Record<string, BeingClass> = { User, Desk, Avatar, Clock };
58
+ const json = (status: number, v: unknown, headers: Record<string, string> = {}) => new Response(JSON.stringify(v), { status, headers: { 'content-type': 'application/json', ...headers } });
59
+
60
+ export class EdgeHarbor extends Harbor {
61
+ readonly sockets = new Set<Held>();
62
+ readonly root: string | undefined;
63
+ // What the deploy carried, held from the worker's first line.
64
+ readonly deployed: Deployed;
65
+ // The rows: the settings a root writes, and the design a world wears.
66
+ readonly settings: SettingsStore;
67
+ readonly folder: Folder;
68
+ readonly wear: ReturnType<typeof wearInStorage>;
69
+ // The model side's dance, one key in the same object: clients, pending
70
+ // requests and codes, all of them living minutes.
71
+ readonly dance: ReturnType<typeof danceInStorage>;
72
+ // The object's storage, unprefixed and unsealed: the alarm is the object's
73
+ // own and is no row.
74
+ readonly #storage: Storage;
75
+ #booted: Promise<void> | undefined;
76
+ // The MCP endpoint, one per instance: a session is a client's connection
77
+ // and belongs to the harbor, not to the request that opened it.
78
+ #mcp: McpHttp | undefined;
79
+
80
+ // A deployed worker: the box ward stands and lends what this terrain can
81
+ // answer. The exercise builds harbors with the constructor instead and
82
+ // says nothing, so it stands no ward of its own, as a command on a disk
83
+ // folder stands none.
84
+ static async open(state: State, env: Env, deployed: Deployed = {}): Promise<EdgeHarbor> {
85
+ if (!env.QUO_SEED_KEY) throw new Error('no QUO_SEED_KEY: a seed is never kept plain on the edge');
86
+ const harbor = new EdgeHarbor(new DurableStorage(state.storage, await sealKey(env.QUO_SEED_KEY)), env.QUO_ROOT, deployed);
87
+ harbor.lent = LENT;
88
+ return harbor;
89
+ }
90
+
91
+ constructor(store: DurableStorage, root: string | undefined, deployed: Deployed = {}) {
92
+ // The faculty classes this terrain can stand, beside the beings: the box
93
+ // ward's own front desk and the grants its routes honour. They are named
94
+ // here and not left to the deploy, because `lent` names classes and a
95
+ // name the loader cannot find is a ward that would not boot.
96
+ super(store, async () => ({ ...BUILT_IN, Console, Grants, Alarm, ...deployed.classes }));
97
+ this.root = root;
98
+ this.deployed = deployed;
99
+ this.#storage = store.storage;
100
+ // The rows live beside the wards in the same object, under the same
101
+ // prefix, so an exercise standing two harbors in one object gives each
102
+ // its own settings and its own design.
103
+ this.settings = settingsInStorage(store.storage, store.prefix);
104
+ this.folder = folderInStorage(store.storage, store.prefix);
105
+ this.wear = wearInStorage(store.storage, store.prefix);
106
+ this.dance = danceInStorage(store.storage, store.prefix);
107
+ }
108
+
109
+ // The whole of what a side is handed, read at every request: the deploy's
110
+ // half never changes and the rows are worn while the worker runs.
111
+ async given(): Promise<Given> {
112
+ return { ...this.deployed, settings: await this.settings.load(), folder: this.folder };
113
+ }
114
+
115
+ // What this box lends, by name. Set before the first request and read at
116
+ // the boot; a harbor that says nothing stands no ward of its own and lends
117
+ // nothing. `given.ts` holds the list a deployed worker uses and why it is
118
+ // that list.
119
+ lent: Record<string, string> | undefined;
120
+
121
+ // Once per instance, on the first request: the platform may have woken a
122
+ // fresh instance, and every ward boots from storage before anything is
123
+ // answered.
124
+ ready(): Promise<void> {
125
+ return (this.#booted ??= this.boot());
126
+ }
127
+
128
+ // The box's own ward first, because a being born in any other ward lends
129
+ // from her first line and a ground that could not reach it yet would leave
130
+ // her holding nothing until her next birth. Then every ward the object
131
+ // keeps, then the beings who took what the box lends: a box is up when
132
+ // they have landed.
133
+ override async boot(): Promise<void> {
134
+ if (this.lent) {
135
+ this.lend = await stand(this, this.lent);
136
+ // A token issued is a cell written, and the cells are the dock ward's
137
+ // partition like every faculty's: there is no second place on this
138
+ // terrain for the box to keep its state.
139
+ const grants = facultyOf<Grants>(this, GRANTS);
140
+ if (grants) grants.persist = () => this.wards.get(DOCK)?.save() ?? Promise.resolve();
141
+ // The clock here is the object's alarm, and only the object may set its
142
+ // own, so the timer is handed the two calls the same way.
143
+ const timer = facultyOf<Alarm>(this, TIMER);
144
+ if (timer) timer.alarms = alarmsIn(this.#storage);
145
+ }
146
+ await super.boot();
147
+ await settled();
148
+ }
149
+
150
+ // The alarm went off: no request, no route and nobody asking, which is the
151
+ // whole point of it. The box ward is up by now, so the round runs and the
152
+ // timer arms for whatever the Clocks say they are next due at.
153
+ //
154
+ // The wards are written here because no ask returned to write them: a wake
155
+ // went through a Clock's own door and wrote her fires, and the harbor never
156
+ // saw it. A box whose alarm fired and whose partition was not written would
157
+ // fire the same minute again on its next wake.
158
+ async rang(): Promise<void> {
159
+ const timer = facultyOf<Alarm>(this, TIMER);
160
+ if (!timer) return;
161
+ try {
162
+ await timer.rang();
163
+ } finally {
164
+ await timer.setting;
165
+ for (const w of this.wards.values()) await w.save();
166
+ }
167
+ }
168
+
169
+ // The box's grants, for the routes that honour a bearer. Undefined is a
170
+ // harbor that stands no box ward, which is the exercise and nothing a
171
+ // deploy does.
172
+ get grants(): Grants | undefined {
173
+ return facultyOf<Grants>(this, GRANTS);
174
+ }
175
+
176
+ // The web side, built per request because its whole configuration is rows
177
+ // a root writes while the worker runs. It is the same `webRoute` the daemon
178
+ // mounts, handed the same things by a terrain that has no disk: the bundles
179
+ // from the deploy, the design out of storage, the origins out of the
180
+ // settings, and the worker's own address when no root has named one.
181
+ //
182
+ // A deployment carrying no bundles serves no page at all, and says so
183
+ // rather than serving one that loads nothing.
184
+ //
185
+ // The credential exchange's allow page shares this prefix, as it does on
186
+ // the daemon: the worlds' pages first, then the page where a human hands
187
+ // an invitation and the model side's dance finishes.
188
+ async web(request: Request, path: string, base: string): Promise<Response> {
189
+ const given = await this.given();
190
+ if (!given.code) return json(404, { error: 'this deployment carries no page code' });
191
+ const at = await this.routes(base);
192
+ const route = webRoute(this, { at, code: given.code, folder: given.folder, apps: given.settings.apps ?? null, bell: given.settings.bell ?? null });
193
+ const rest = path.slice('/web'.length);
194
+ const page = await route(request, rest);
195
+ if (page) return page;
196
+ const exchange = await this.exchange(base);
197
+ return (await exchange?.handle(request, rest)) ?? new Response('no such page', { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8' } });
198
+ }
199
+
200
+ // Where the four routes answer, as the world outside sees them: what a root
201
+ // named, and the worker's own address under this harbor's prefix for every
202
+ // one it did not.
203
+ async routes(base: string): Promise<Routes & { quo: string; api: string }> {
204
+ const r = (await this.settings.load()).routes;
205
+ return { quo: r?.quo ?? `${base}/quo`, web: r?.web ?? `${base}/web`, mcp: r?.mcp ?? `${base}/mcp`, api: r?.api ?? `${base}/api` };
206
+ }
207
+
208
+ // Which ward a caller who names none is at. On a disk it is the mark an
209
+ // operator wrote in the folder; here it is the one world this object holds,
210
+ // and an object holding several says nothing rather than guessing, exactly
211
+ // as a folder with several wards and no mark does. The box's own ward is
212
+ // furniture and never a candidate.
213
+ here(): string {
214
+ const worlds = [...this.wards.keys()].filter((n) => n !== DOCK);
215
+ return worlds.length === 1 ? worlds[0] : '';
216
+ }
217
+
218
+ // What a route asks of a ward here: her avatar as she stands, and her way
219
+ // in with an invitation. Both are `beings/join.ts`, the same two the daemon
220
+ // hands its own routes.
221
+ #resolve = async (identity: string, ward: string) => {
222
+ const hosted = this.wards.get(ward);
223
+ return hosted ? find(hosted, identity) : { error: 'no such world' };
224
+ };
225
+ #join = async (identity: string, invitation: Invitation, ward: string) => {
226
+ const hosted = this.wards.get(ward);
227
+ if (!hosted) return { error: 'no such world' };
228
+ const r = await letIn(hosted, identity, invitation);
229
+ return r.error ? { error: r.error } : {};
230
+ };
231
+ // The worlds a client may be let into: every ward here but the box's own.
232
+ #worlds = () =>
233
+ [...this.wards]
234
+ .filter(([ward]) => ward !== DOCK)
235
+ .map(([ward, h]) => ({ ward, pk: h.pk, user: h.record.user }));
236
+
237
+ // The program side. The same `apiRoute` the daemon mounts, on the grants of
238
+ // this box's own ward: a request with no bearer is the stranger every route
239
+ // knows, and one with a bearer this box granted is an occupant.
240
+ async api(request: Request, path: string, base: string): Promise<Response> {
241
+ const grants = this.grants;
242
+ if (!grants) return json(503, { error: 'this box stands no ward of its own, so it grants nothing' });
243
+ const at = await this.routes(base);
244
+ const route = apiRoute({ wards: this.wards, here: () => this.here() }, { grants, resolve: this.#resolve, join: this.#join, web: at.web });
245
+ return (await route(request, path.slice('/api'.length))) ?? json(404, { error: 'no such route' });
246
+ }
247
+
248
+ // The model side: the OAuth dance, kept in this object's own storage, and
249
+ // the MCP endpoint beside it. The endpoint's sessions live on this harbor
250
+ // and not on the request, because a session is a client's connection and
251
+ // outlives the fetch that opened it; the platform keeps the object awake
252
+ // while one is held and a client that comes back to a woken instance
253
+ // reconnects, which is what a reconnect is for.
254
+ async mcp(request: Request, path: string, base: string): Promise<Response> {
255
+ const grants = this.grants;
256
+ if (!grants) return json(503, { error: 'this box stands no ward of its own, so it grants nothing' });
257
+ const endpoint = (this.#mcp ??= new McpHttp(this.#resolve, async () => {
258
+ for (const h of this.wards.values()) await h.save();
259
+ }));
260
+ endpoint.gone = (identity, ward) => grants.revoke(identity, ward); // removal at the ward ends the grant at the box
261
+ const route = await mcpRoute(this.dance, await this.routes(base), endpoint, grants);
262
+ const rest = path.slice('/mcp'.length);
263
+ return (await route.handler(request, rest)) ?? json(404, { error: 'no such route' });
264
+ }
265
+
266
+ // The dance's discovery under this prefix, for a caller that reaches the
267
+ // harbor at a path rather than at an origin of its own.
268
+ async wellKnown(request: Request, path: string, base: string): Promise<Response> {
269
+ const grants = this.grants;
270
+ if (!grants) return json(404, { error: 'no such route' });
271
+ const route = await mcpRoute(this.dance, await this.routes(base), null, grants);
272
+ return (await route.wellKnown(request, path.slice('/.well-known'.length))) ?? json(404, { error: 'no such route' });
273
+ }
274
+
275
+ // The exchange, for the allow page on the web side: the dance it finishes
276
+ // is the same one the model side keeps.
277
+ async exchange(base: string): Promise<Exchange | undefined> {
278
+ const grants = this.grants;
279
+ if (!grants) return undefined;
280
+ const route = await mcpRoute(this.dance, await this.routes(base), null, grants);
281
+ return new Exchange({ oauth: route.oauth, worlds: this.#worlds, join: this.#join });
282
+ }
283
+
284
+ // The worker hands the request here with the path after the harbor's
285
+ // prefix, and with that prefix as an address, since a page tells a tab
286
+ // where to dial: `/quo/<pk>`, `/quo`, `/health`, `/root`, `/web`.
287
+ // Every door of this object, and one thing after all of them: the arming.
288
+ // A schedule kept at any of them moves the moment the box is armed for, an
289
+ // arming here is a write to the object's storage, and the ask that caused
290
+ // it cannot answer for it. So the response waits for the write, because a
291
+ // box that answered "kept" and was put to sleep before its alarm landed is
292
+ // a schedule nothing would fire.
293
+ async fetch(request: Request, path: string, base: string): Promise<Response> {
294
+ await this.ready();
295
+ const out = await this.#route(request, path, base);
296
+ await facultyOf<Alarm>(this, TIMER)?.setting;
297
+ return out;
298
+ }
299
+
300
+ async #route(request: Request, path: string, base: string): Promise<Response> {
301
+ if (path === '/health') return json(200, this.health(), { 'access-control-allow-origin': '*' });
302
+ if (path === '/root') return this.rootAsk(request);
303
+ if (path === '/web' || path.startsWith('/web/')) return this.web(request, path, base);
304
+ if (path === '/api' || path.startsWith('/api/')) return this.api(request, path, base);
305
+ if (path === '/mcp' || path.startsWith('/mcp/')) return this.mcp(request, path, base);
306
+ if (path.startsWith('/.well-known/')) return this.wellKnown(request, path, base);
307
+ // The sealed bytes are the same route the daemon mounts; the upgrade is
308
+ // this terrain's own listener half, a socket pair held by the object.
309
+ if (path === '/quo' || path.startsWith('/quo/')) {
310
+ if ((path === '/quo' || path === '/quo/') && request.headers.get('upgrade')?.toLowerCase() === 'websocket') return this.upgrade();
311
+ return (await quoRoute(this)(request, path.slice('/quo'.length))) ?? json(404, { error: 'no such route' });
312
+ }
313
+ return json(404, { error: 'no such route' });
314
+ }
315
+
316
+ health() {
317
+ return {
318
+ ok: true,
319
+ wards: Object.fromEntries([...this.wards].map(([n, h]) => [n, h.pk])),
320
+ reaches: Object.fromEntries([...this.reaches].map(([pk, b]) => [pk, b.held ? 'held' : 'reach'])),
321
+ sockets: this.sockets.size,
322
+ // How many saves this object's storage refused, per ward. A refusal is
323
+ // already the answer of the ask that caused it, so nobody is waiting to
324
+ // be told; this is the other half, which is an operator asking the box
325
+ // how it is and being told about the one fault a healthy-looking box
326
+ // can carry for hours. Zero wards refused is an empty object, and the
327
+ // shape does not move.
328
+ faults: Object.fromEntries([...this.faults]),
329
+ };
330
+ }
331
+
332
+ // A dialer's socket: the server end of a pair, held here, its pks bound
333
+ // at announce and unbound at close. The platform keeps the object awake
334
+ // while the socket is open.
335
+ upgrade(): Response {
336
+ const pair = new WebSocketPair();
337
+ const server = pair[1];
338
+ server.accept();
339
+ const s: Held = new Held(
340
+ server,
341
+ (pk, bytes) => this.deliver(pk, bytes),
342
+ (far) => void this.bind(far, s, true),
343
+ () => {
344
+ this.unbind(s);
345
+ this.sockets.delete(s);
346
+ },
347
+ );
348
+ this.sockets.add(s);
349
+ s.announce([...this.doors.keys()]);
350
+ return new Response(null, { status: 101, webSocket: pair[0] });
351
+ }
352
+
353
+ // The root's door. Under the secret only; without one configured the
354
+ // edge has no root at all, and every owner it will ever have was invited
355
+ // by a root that existed when it did.
356
+ async rootAsk(request: Request): Promise<Response> {
357
+ const bearer = request.headers.get('authorization')?.replace(/^Bearer\s+/i, '') ?? '';
358
+ if (request.method !== 'POST' || !this.root || bearer.length !== this.root.length || !same(bearer, this.root)) return json(401, { error: 'the root holds the secret' });
359
+ let body: { init?: { ward?: unknown; user?: unknown }; settings?: Partial<Settings>; wear?: Record<string, unknown>; ward?: unknown; method?: unknown; args?: unknown } = {};
360
+ try {
361
+ body = (await request.json()) as typeof body;
362
+ } catch {
363
+ return json(400, { error: 'a root ask is JSON' });
364
+ }
365
+ try {
366
+ if (body.init) {
367
+ const name = typeof body.init.ward === 'string' ? body.init.ward : 'main';
368
+ const user = typeof body.init.user === 'string' ? body.init.user : 'me';
369
+ if (this.wards.has(name)) return json(409, { error: `ward ${name} already exists here` });
370
+ const hosted = await this.create(name, user);
371
+ await setup(hosted, user);
372
+ return json(200, { name, pk: hosted.pk, user });
373
+ }
374
+ // The settings, merged over what is kept and answered whole, so a root
375
+ // that writes one field sees the rest and a root that writes nothing
376
+ // reads the row. This is how a box is configured after it is deployed:
377
+ // the origins the proxy serves, the apps table and the bell key.
378
+ if (body.settings) {
379
+ const settings = { ...(await this.settings.load()), ...body.settings };
380
+ await this.settings.save(settings);
381
+ return json(200, { settings });
382
+ }
383
+ // One design file put there, or taken away with null, and the names now
384
+ // worn. A world wears a design only when it holds `tokens.css`.
385
+ if (body.wear) {
386
+ for (const [file, text] of Object.entries(body.wear)) {
387
+ if (!/^[\w.-]+$/.test(file) && !/^fonts\/[\w.-]+$/.test(file)) return json(400, { error: `not a design file: ${file}` });
388
+ await this.wear.put(file, typeof text === 'string' ? text : null);
389
+ }
390
+ return json(200, { worn: await this.wear.worn() });
391
+ }
392
+ const hosted = this.wards.get(typeof body.ward === 'string' ? body.ward : 'main');
393
+ if (!hosted) return json(404, { error: 'no such ward' });
394
+ const out = await hosted.ask(typeof body.method === 'string' ? body.method : undefined, (body.args ?? {}) as Record<string, unknown>);
395
+ return json(200, { result: isSilence(out) ? { silence: true } : out });
396
+ } catch (e) {
397
+ return json(500, { error: e instanceof Error ? e.message : String(e) });
398
+ }
399
+ }
400
+ }
401
+
402
+ // Constant time, as far as a comparison in a worker can be.
403
+ function same(a: string, b: string): boolean {
404
+ let out = 0;
405
+ for (let i = 0; i < a.length; i++) out |= a.charCodeAt(i) ^ b.charCodeAt(i);
406
+ return out === 0;
407
+ }
@@ -0,0 +1,241 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // What the edge terrain runs, inside the platform: the library's
3
+ // conformance suite against two edge harbors in one object, each over its
4
+ // own prefixed view of the object's storage, reaching each other by request
5
+ // through the worker's own origin under `/x/<sub>/quo`, so every relation
6
+ // crosses the route. Down is the reach refused on both; migrate lifts seed,
7
+ // partition and record out of one view into the other and boots there. It
8
+ // reports rather than throws, so the worker hands the list back and Node
9
+ // asserts on it. Only with `QUO_EXERCISE` set, which a deployment never sets.
10
+ import { conform, conformStore, conformReach } from '@nervur-org/nervur/conformance';
11
+ import { request as byRequest } from '@nervur-org/nervur/harbor';
12
+ import type { World, Handle, Census } from '@nervur-org/nervur/conformance';
13
+ import { Printer, Shop, Customer, Echo, Member } from '@nervur-org/nervur/conformance';
14
+ import type { BeingClass, Cells, Invitation, JsonObject } from '@nervur-org/nervur';
15
+ import { EdgeHarbor } from './edge.ts';
16
+ import { DurableStorage } from './storage.ts';
17
+ import { ROW_CEILING } from './given.ts';
18
+ import { told } from '@nervur-org/nervur';
19
+ import { sealKey } from '../seal.ts';
20
+ import type { Hosted } from '@nervur-org/nervur/harbor';
21
+ import type { State, Env } from './platform.d.ts';
22
+
23
+ export type Result = { name: string; skip?: string; error?: string };
24
+ const assert = {
25
+ equal(a: unknown, b: unknown, m: string) {
26
+ if (a !== b) throw new Error(`${m}: ${JSON.stringify(a)} is not ${JSON.stringify(b)}`);
27
+ },
28
+ deepEqual(a: unknown, b: unknown, m = 'not equal') {
29
+ if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${m}: ${JSON.stringify(a)} is not ${JSON.stringify(b)}`);
30
+ },
31
+ };
32
+ type Partition = { beings: Record<string, Cells>; bind: Record<string, { standings: Record<string, { ward: string; heir: string | null; seq: number; current: string | null; next: string | null }>; occupants: Record<string, string>; knocks: Record<string, { current: string | null; next: string | null }>; answered: Record<string, unknown>; minted: string[] }>; heirs: Record<string, { being: string; id: string; current: string; announced: string | null; fresh: boolean; mark: number; spent: number[] }>; public: string | null };
33
+ // The suite's own classes, and no bundles: the exercise mounts no side and
34
+ // serves no page, so this deployment names no code.
35
+ const DEPLOYED = { classes: { Printer, Shop, Customer, Echo, Member } satisfies Record<string, BeingClass> };
36
+
37
+ // The harbors the exercise made, by sub name, for the router below.
38
+ const subs = new Map<string, EdgeHarbor>();
39
+
40
+ async function pair(state: State, env: Env, run: number): Promise<{ harbors: EdgeHarbor[]; names: string[] }> {
41
+ const key = await sealKey(env.QUO_SEED_KEY ?? '');
42
+ const names = [`a-${run}-${Date.now()}`, `b-${run}-${Date.now()}`];
43
+ const harbors = names.map((n) => new EdgeHarbor(new DurableStorage(state.storage, key, `x:${n}:`), undefined, DEPLOYED));
44
+ for (const [i, h] of harbors.entries()) {
45
+ await h.ready();
46
+ subs.set(names[i], h);
47
+ }
48
+ return { harbors, names };
49
+ }
50
+
51
+ function probe(base: string, state: State, env: Env): () => Promise<World> {
52
+ let n = 0;
53
+ return async () => {
54
+ const p = await pair(state, env, n++);
55
+ const wardOf = new Map<string, string>();
56
+ const where = new Map<string, number>();
57
+ let k = 0;
58
+ const at = (name: string) => {
59
+ const h = p.harbors[where.get(name)!];
60
+ const w = h.wards.get(name)!;
61
+ return { h, w, p: h.partitionOf(name) as unknown as Partition };
62
+ };
63
+ // the far harbor learns the pk by a hint: the link's reach hint, this world's route
64
+ const learn = (i: number, w: Hosted) => p.harbors[1 - i].hint(w.pk, `${base}/x/${p.names[i]}/quo`);
65
+ return {
66
+ oneWard: false,
67
+ async boot<B>(key: string, Class: BeingClass, cells: JsonObject = {}, { isPublic = false } = {}): Promise<Handle<B>> {
68
+ for (const h of p.harbors) h.classes[Class.name] = Class;
69
+ const name = `W-${key}`;
70
+ const i = where.get(name) ?? k++ % 2;
71
+ const h = p.harbors[i];
72
+ const w = h.wards.get(name) ?? (await h.create(name));
73
+ if (!where.has(name)) {
74
+ where.set(name, i);
75
+ learn(i, w);
76
+ }
77
+ wardOf.set(key, name);
78
+ assert.deepEqual(await w.ask('boot', { key, class: Class.name }), { booted: key });
79
+ if (isPublic) assert.deepEqual(await w.ask('public', { key }), { public: key });
80
+ if (Object.keys(cells).length) Object.assign(at(name).p.beings[key], cells);
81
+ return {
82
+ pk: w.pk,
83
+ get cells() {
84
+ return at(name).p.beings[key];
85
+ },
86
+ get being() {
87
+ return at(name).w.being(key) as B;
88
+ },
89
+ minted: { has: (pk: string) => at(name).p.bind[key].minted.includes(pk) },
90
+ forgeKnock: (inv: Invitation) => void ((at(name).p.bind[key].knocks as Record<string, unknown>)[`${inv.ward}:${inv.heir}`] = { current: 'ab'.repeat(32), next: null, spoke: true, sent: true, seq: 0 }),
91
+ set down(v: boolean) {
92
+ for (const each of p.harbors) {
93
+ if (v) each.down.add(at(name).w.pk);
94
+ else each.down.delete(at(name).w.pk);
95
+ }
96
+ },
97
+ get down() {
98
+ return p.harbors[0].down.has(at(name).w.pk);
99
+ },
100
+ };
101
+ },
102
+ heir(inv: Invitation) {
103
+ for (const h of p.harbors)
104
+ for (const w of h.wards.values())
105
+ if (w.pk === inv.ward) {
106
+ const r = (h.partitionOf(w.name) as unknown as Partition).heirs[inv.heir!];
107
+ return r && { current: r.current, announced: r.announced, fresh: r.fresh };
108
+ }
109
+ return undefined;
110
+ },
111
+ census(): Census {
112
+ const c: Census = { wards: {}, binds: {}, cells: {} };
113
+ for (const h of p.harbors)
114
+ for (const w of h.wards.values()) {
115
+ const pt = h.partitionOf(w.name) as unknown as Partition;
116
+ c.wards[w.pk] = { beings: Object.keys(pt.beings), public: pt.public, heirs: Object.fromEntries(Object.entries(pt.heirs).map(([pk, r]) => [pk, { being: r.being, id: r.id, current: r.current, announced: r.announced, fresh: r.fresh, mark: r.mark, spent: [...r.spent] }])) };
117
+ for (const [key, b] of Object.entries(pt.bind)) {
118
+ c.binds[key] = {
119
+ ward: w.pk,
120
+ standings: Object.fromEntries(Object.entries(b.standings).map(([id, s]) => [id, { ward: s.ward, heir: s.heir, seq: s.seq }])),
121
+ occupants: { ...b.occupants },
122
+ knocks: Object.keys(b.knocks),
123
+ answered: Object.keys(b.answered),
124
+ minted: [...b.minted],
125
+ secrets: [...Object.values(b.standings).flatMap((s) => [s.current, s.next]), ...Object.values(b.knocks).flatMap((s) => [s.current, s.next])].filter((x): x is string => x !== null),
126
+ };
127
+ c.cells[key] = pt.beings[key]!;
128
+ }
129
+ }
130
+ return c;
131
+ },
132
+ // seed, partition and record lifted out of one object's storage into
133
+ // the other's, same seed, same pk; the old copy gone before the new boots
134
+ owner(key: string) {
135
+ return (method?: string, args?: JsonObject) => at(wardOf.get(key)!).w.ask(method, args);
136
+ },
137
+ async migrate(key: string): Promise<void> {
138
+ const name = wardOf.get(key)!;
139
+ const from = where.get(name)!,
140
+ to = 1 - from;
141
+ const pk = p.harbors[from].wards.get(name)!.pk;
142
+ const kept = (await p.harbors[from].drop(name))!;
143
+ const w = await p.harbors[to].adopt(name, kept);
144
+ assert.equal(w.pk, pk, 'a ward that moved harbor changed its pk');
145
+ where.set(name, to);
146
+ p.harbors[from].reaches.delete(pk);
147
+ learn(to, w);
148
+ },
149
+ };
150
+ };
151
+ }
152
+
153
+ // `/exercise` runs the suite; `/x/<sub>/quo/<pk>` is a sub-harbor's route,
154
+ // which is how the two reach each other through the worker.
155
+ export async function exercise(request: Request, base: string, path: string, state: State, env: Env): Promise<Response> {
156
+ const sub = /^\/x\/([\w.-]+)(\/.*)$/.exec(path);
157
+ if (sub) {
158
+ const h = subs.get(sub[1]);
159
+ if (!h) return new Response(JSON.stringify({ error: 'no such harbor' }), { status: 404, headers: { 'content-type': 'application/json' } });
160
+ return h.fetch(request, sub[2], `${base}/x/${sub[1]}`);
161
+ }
162
+ const queued: { name: string; skip: string | false; fn: () => Promise<void> | void }[] = [];
163
+ const test = (name: string, opts: { skip?: string | false }, fn: () => Promise<void> | void) => {
164
+ queued.push({ name, skip: opts.skip ?? false, fn });
165
+ };
166
+ const key = await sealKey(env.QUO_SEED_KEY ?? '');
167
+ let views = 0;
168
+ conformStore('durable storage', async () => new DurableStorage(state.storage, key, `s:${views++}:${Date.now()}:`), { test });
169
+ let lines = 0;
170
+ conformReach(
171
+ 'request, through the worker',
172
+ async () => {
173
+ const name = `r-${lines++}-${Date.now()}`;
174
+ const h = new EdgeHarbor(new DurableStorage(state.storage, key, `x:${name}:`), undefined, DEPLOYED);
175
+ await h.ready();
176
+ subs.set(name, h);
177
+ return { reach: byRequest(`${base}/x/${name}/quo`), hold: async (pk, door) => void h.doors.set(pk, async (b) => ({ bytes: await door(b), heard: true })), drop: async () => void subs.delete(name) };
178
+ },
179
+ { test },
180
+ );
181
+ conform('edge, two harbors in one object by request', probe(base, state, env), { canDown: true, canLend: false, canRestart: false, test });
182
+
183
+ // The ceiling, on the terrain that has one. A being whose row is past it is
184
+ // refused by the store before the platform is asked, the arrival that wrote
185
+ // the row is a failed ask at the door, and she is left standing at what the
186
+ // object keeps rather than at what she was answered.
187
+ // The ceiling here is small, so what crosses the door is a few kilobytes:
188
+ // a row of the platform's real size costs a great deal to carry and proves
189
+ // the same refusal. That the number a deployment runs under is the
190
+ // platform's own is the line below it, which asks the store directly.
191
+ test('[edge] a row past the object s ceiling is refused, the arrival that wrote it fails at the door, and she stands at what is kept', {}, async () => {
192
+ const key = await sealKey(env.QUO_SEED_KEY ?? '');
193
+ const small = 8_000;
194
+ const h = new EdgeHarbor(new DurableStorage(state.storage, key, `c:${Date.now()}:`, small), undefined, DEPLOYED);
195
+ await h.ready();
196
+ const w = await h.create(`ceiling-${Date.now()}`);
197
+ for (const [k, c] of [
198
+ ['P', 'Printer'],
199
+ ['A', 'Customer'],
200
+ ])
201
+ assert.deepEqual(await w.ask('boot', { key: k, class: c }), { booted: k });
202
+ const printer = w.being('P') as unknown as Printer;
203
+ const alice = w.being('A') as unknown as Customer;
204
+ const inv = (await printer.invite('a'))!;
205
+ assert.deepEqual(await alice.knock(inv, 'print', { doc: 'one' }), { printed: 'one' });
206
+
207
+ const out = await alice.knock(inv, 'print', { doc: 'x'.repeat(small * 2) });
208
+ assert.equal(told(out), 'threw', 'a row the object will not keep is a failed ask at the door');
209
+ const jobs = (printer.cells.jobs as { doc: unknown }[]).map((j) => j.doc);
210
+ assert.deepEqual(jobs, ['one'], 'she stands at what is kept, and not at what she was answered');
211
+ assert.deepEqual(await alice.knock(inv, 'print', { doc: 'two' }), { printed: 'two' }, 'and a small write after it is kept');
212
+
213
+ // The number a deployment runs under, asked of the store itself: this is
214
+ // the platform's published ceiling and nothing this suite chose.
215
+ const store = new DurableStorage(state.storage, key, `c2:${Date.now()}:`);
216
+ const kept = { seed: new Uint8Array(32).fill(3), partition: {}, record: { pk: '', code: 'x', user: '' } };
217
+ await store.put('big', kept);
218
+ const over = { beings: { B: { fat: 'x'.repeat(ROW_CEILING) } }, bind: { B: {} } };
219
+ let refused = false;
220
+ try {
221
+ await store.save('big', over, ['B']);
222
+ } catch {
223
+ refused = true;
224
+ }
225
+ assert.equal(refused, true, 'a row past the platform s own ceiling is refused by the store');
226
+ });
227
+ const results: Result[] = [];
228
+ for (const { name, skip, fn } of queued) {
229
+ if (skip) {
230
+ results.push({ name, skip });
231
+ continue;
232
+ }
233
+ try {
234
+ await fn();
235
+ results.push({ name });
236
+ } catch (e) {
237
+ results.push({ name, error: String((e as Error)?.stack ?? e) });
238
+ }
239
+ }
240
+ return new Response(JSON.stringify({ results }), { headers: { 'content-type': 'application/json' } });
241
+ }