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