@nervur-org/dock 0.4.1 → 0.6.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 (225) hide show
  1. package/README.md +17 -15
  2. package/api/route.ts +4 -72
  3. package/beings/avatar.ts +11 -7
  4. package/beings/carry.ts +129 -116
  5. package/beings/clock.ts +11 -7
  6. package/beings/desk.ts +7 -6
  7. package/beings/doorbell.ts +36 -7
  8. package/beings/index.ts +17 -4
  9. package/beings/join.ts +7 -4
  10. package/beings/link.ts +1 -1
  11. package/beings/porter.ts +5 -1
  12. package/beings/presence.ts +15 -5
  13. package/beings/setup.ts +27 -21
  14. package/beings/user.ts +26 -17
  15. package/beings/way.ts +1 -1
  16. package/{dock → box}/agents.ts +37 -44
  17. package/{dock → box}/apns.ts +4 -4
  18. package/box/box.ts +31 -0
  19. package/box/contract.ts +41 -0
  20. package/box/dialers.ts +88 -0
  21. package/box/dock.ts +148 -0
  22. package/box/envoy.ts +96 -0
  23. package/box/faculty.ts +111 -0
  24. package/{dock → box}/fcm.ts +1 -1
  25. package/box/grants.ts +90 -0
  26. package/box/http.ts +93 -0
  27. package/{dock → box}/http2.ts +7 -7
  28. package/box/index.ts +201 -0
  29. package/box/join.ts +78 -0
  30. package/box/local.ts +37 -0
  31. package/{dock → box}/location.ts +15 -3
  32. package/{dock → box}/notifications.ts +17 -5
  33. package/box/push.ts +85 -0
  34. package/box/ring.ts +85 -0
  35. package/{dock → box}/service.ts +2 -2
  36. package/box/timer.ts +150 -0
  37. package/box/tree.ts +22 -0
  38. package/{dock → box}/webpush.ts +2 -2
  39. package/cli/client.ts +1 -1
  40. package/cli/daemon.ts +101 -103
  41. package/cli/estate.ts +2 -3
  42. package/cli/nervur.ts +44 -25
  43. package/cli/stand.ts +1 -1
  44. package/dist/api/route.d.ts +1 -2
  45. package/dist/api/route.js +4 -76
  46. package/dist/beings/avatar.d.ts +4 -3
  47. package/dist/beings/avatar.js +9 -6
  48. package/dist/beings/carry.d.ts +10 -12
  49. package/dist/beings/carry.js +119 -127
  50. package/dist/beings/clock.js +10 -7
  51. package/dist/beings/desk.js +8 -7
  52. package/dist/beings/doorbell.d.ts +3 -1
  53. package/dist/beings/doorbell.js +37 -4
  54. package/dist/beings/index.d.ts +24 -4
  55. package/dist/beings/index.js +15 -4
  56. package/dist/beings/join.d.ts +1 -0
  57. package/dist/beings/join.js +6 -4
  58. package/dist/beings/link.d.ts +0 -1
  59. package/dist/beings/link.js +1 -1
  60. package/dist/beings/porter.js +5 -1
  61. package/dist/beings/presence.d.ts +1 -0
  62. package/dist/beings/presence.js +12 -3
  63. package/dist/beings/setup.d.ts +5 -2
  64. package/dist/beings/setup.js +16 -15
  65. package/dist/beings/user.d.ts +4 -3
  66. package/dist/beings/user.js +18 -11
  67. package/dist/{dock → box}/agents.d.ts +7 -4
  68. package/dist/{dock → box}/agents.js +30 -37
  69. package/dist/{dock → box}/apns.d.ts +1 -1
  70. package/dist/{dock → box}/apns.js +3 -3
  71. package/dist/box/box.d.ts +9 -0
  72. package/dist/box/box.js +18 -0
  73. package/dist/box/contract.d.ts +6 -0
  74. package/dist/box/contract.js +35 -0
  75. package/dist/{dock → box}/dialers.d.ts +7 -4
  76. package/dist/box/dialers.js +76 -0
  77. package/dist/box/dock.d.ts +182 -0
  78. package/dist/box/dock.js +128 -0
  79. package/dist/box/envoy.d.ts +44 -0
  80. package/dist/box/envoy.js +75 -0
  81. package/dist/box/faculty.d.ts +68 -0
  82. package/dist/box/faculty.js +97 -0
  83. package/dist/{dock → box}/fcm.d.ts +1 -1
  84. package/dist/{dock → box}/grants.d.ts +4 -3
  85. package/dist/box/grants.js +78 -0
  86. package/dist/box/http.d.ts +26 -0
  87. package/dist/box/http.js +89 -0
  88. package/dist/{dock → box}/http2.d.ts +1 -1
  89. package/dist/{dock → box}/http2.js +6 -6
  90. package/dist/box/index.d.ts +46 -0
  91. package/dist/box/index.js +179 -0
  92. package/dist/box/join.d.ts +19 -0
  93. package/dist/box/join.js +67 -0
  94. package/dist/{dock → box}/local.d.ts +3 -2
  95. package/dist/box/local.js +27 -0
  96. package/dist/{dock → box}/location.d.ts +3 -0
  97. package/dist/box/location.js +21 -0
  98. package/dist/{dock → box}/notifications.d.ts +3 -0
  99. package/dist/{dock → box}/notifications.js +14 -4
  100. package/dist/{dock → box}/push.d.ts +12 -18
  101. package/dist/box/push.js +83 -0
  102. package/dist/box/ring.d.ts +18 -0
  103. package/dist/box/ring.js +55 -0
  104. package/dist/{dock → box}/service.d.ts +1 -1
  105. package/dist/{dock → box}/service.js +1 -1
  106. package/dist/{dock → box}/timer.d.ts +2 -0
  107. package/dist/box/timer.js +127 -0
  108. package/dist/box/tree.d.ts +5 -0
  109. package/dist/box/tree.js +14 -0
  110. package/dist/{dock → box}/webpush.d.ts +1 -1
  111. package/dist/cli/client.d.ts +1 -0
  112. package/dist/cli/daemon.d.ts +2 -8
  113. package/dist/cli/daemon.js +102 -107
  114. package/dist/cli/estate.js +2 -3
  115. package/dist/cli/nervur.js +44 -24
  116. package/dist/cli/stand.d.ts +0 -14
  117. package/dist/cli/stand.js +1 -1
  118. package/dist/harbor/browser.d.ts +8 -5
  119. package/dist/harbor/browser.js +20 -21
  120. package/dist/harbor/capacitor.d.ts +3 -3
  121. package/dist/harbor/capacitor.js +9 -8
  122. package/dist/harbor/disk.d.ts +9 -9
  123. package/dist/harbor/disk.js +56 -51
  124. package/dist/harbor/edge/alarm.d.ts +4 -2
  125. package/dist/harbor/edge/alarm.js +9 -19
  126. package/dist/harbor/edge/edge.d.ts +7 -6
  127. package/dist/harbor/edge/edge.js +58 -53
  128. package/dist/harbor/edge/exercise.js +4 -4
  129. package/dist/harbor/edge/given.d.ts +0 -1
  130. package/dist/harbor/edge/given.js +0 -29
  131. package/dist/harbor/edge/storage.d.ts +0 -2
  132. package/dist/harbor/edge/storage.js +2 -2
  133. package/dist/harbor/http.js +4 -2
  134. package/dist/harbor/sealed.d.ts +0 -5
  135. package/dist/harbor/sealed.js +0 -39
  136. package/dist/harbor/tauri.d.ts +2 -2
  137. package/dist/harbor/tauri.js +13 -10
  138. package/dist/human/app.d.ts +1 -1
  139. package/dist/human/app.js +10 -12
  140. package/dist/human/door.d.ts +0 -1
  141. package/dist/human/door.js +1 -1
  142. package/dist/human/html.js +1 -1
  143. package/dist/human/move.js +1 -1
  144. package/dist/human/tab.bundle.txt +3629 -2824
  145. package/dist/human/wake.d.ts +5 -2
  146. package/dist/human/wake.js +23 -14
  147. package/dist/human/web.d.ts +0 -1
  148. package/dist/human/web.js +3 -3
  149. package/dist/human/worker.bundle.txt +2945 -2118
  150. package/dist/human/worlds.js +3 -3
  151. package/dist/mcp/agent.d.ts +0 -1
  152. package/dist/mcp/agent.js +1 -1
  153. package/dist/mcp/allow.js +3 -2
  154. package/dist/mcp/http.d.ts +0 -1
  155. package/dist/mcp/http.js +1 -1
  156. package/dist/mcp/oauth.d.ts +3 -4
  157. package/dist/mcp/oauth.js +4 -4
  158. package/dist/mcp/route.d.ts +1 -1
  159. package/dist/mcp/runner.d.ts +0 -1
  160. package/dist/mcp/runner.js +1 -1
  161. package/dist/mcp/server.d.ts +0 -1
  162. package/dist/mcp/server.js +1 -1
  163. package/harbor/browser.ts +22 -24
  164. package/harbor/capacitor.ts +9 -8
  165. package/harbor/disk.ts +53 -48
  166. package/harbor/edge/alarm.ts +15 -12
  167. package/harbor/edge/edge.ts +61 -55
  168. package/harbor/edge/exercise.ts +4 -4
  169. package/harbor/edge/given.ts +3 -33
  170. package/harbor/edge/storage.ts +2 -2
  171. package/harbor/http.ts +5 -3
  172. package/harbor/sealed.ts +0 -35
  173. package/harbor/tauri.ts +13 -10
  174. package/human/app.ts +12 -13
  175. package/human/door.ts +1 -1
  176. package/human/html.ts +1 -1
  177. package/human/local.ts +1 -1
  178. package/human/move.ts +1 -1
  179. package/human/tab.bundle.txt +3629 -2824
  180. package/human/wake.ts +36 -16
  181. package/human/web.ts +4 -4
  182. package/human/worker.bundle.txt +2945 -2118
  183. package/human/worlds.ts +3 -3
  184. package/mcp/agent.ts +1 -1
  185. package/mcp/allow.ts +2 -1
  186. package/mcp/http.ts +1 -1
  187. package/mcp/oauth.ts +4 -4
  188. package/mcp/route.ts +1 -1
  189. package/mcp/runner.ts +1 -1
  190. package/mcp/server.ts +1 -1
  191. package/package.json +10 -18
  192. package/GETTING_STARTED.md +0 -138
  193. package/GLOSSARY.md +0 -118
  194. package/WORLDS.md +0 -569
  195. package/beings/envoy.ts +0 -218
  196. package/dist/beings/envoy.d.ts +0 -54
  197. package/dist/beings/envoy.js +0 -211
  198. package/dist/dock/console.d.ts +0 -6
  199. package/dist/dock/console.js +0 -20
  200. package/dist/dock/dialers.js +0 -75
  201. package/dist/dock/faculty.d.ts +0 -24
  202. package/dist/dock/faculty.js +0 -142
  203. package/dist/dock/grants.js +0 -99
  204. package/dist/dock/index.d.ts +0 -41
  205. package/dist/dock/index.js +0 -113
  206. package/dist/dock/local.js +0 -44
  207. package/dist/dock/location.js +0 -11
  208. package/dist/dock/push.js +0 -132
  209. package/dist/dock/timer.js +0 -149
  210. package/dock/console.ts +0 -23
  211. package/dock/dialers.ts +0 -85
  212. package/dock/faculty.ts +0 -148
  213. package/dock/grants.ts +0 -111
  214. package/dock/index.ts +0 -133
  215. package/dock/local.ts +0 -48
  216. package/dock/push.ts +0 -161
  217. package/dock/timer.ts +0 -174
  218. package/quo-api.md +0 -196
  219. package/quo-app-bells.md +0 -280
  220. package/quo-dock.md +0 -1945
  221. package/quo-harbor.md +0 -874
  222. package/quo-human.md +0 -1042
  223. package/quo-mcp.md +0 -343
  224. /package/dist/{dock → box}/fcm.js +0 -0
  225. /package/dist/{dock → box}/webpush.js +0 -0
@@ -30,16 +30,15 @@
30
30
  // those pages are painted with. It also holds what this terrain lends, since
31
31
  // a worker stands a box ward like every other terrain: the box's state has
32
32
  // one home everywhere, and what differs is which faculties that ward lends.
33
- import type { BeingClass } from 'nervur';
33
+ import type { Stance } from 'nervur';
34
34
  import { isSilence } from 'nervur';
35
- import { User, Desk, Avatar, Clock } from '../../beings/index.ts';
35
+ import { BEINGS } from '../../beings/index.ts';
36
36
  import { setup } from '../../beings/setup.ts';
37
37
  import { Harbor, Socket as Held } from 'nervur/harbor';
38
38
  import { quoRoute } from '../quo.ts';
39
39
  import { webRoute, type Folder } from '../../human/web.ts';
40
- import { DOCK, TIMER, Console, facultyOf, settled, stand } from '../../dock/index.ts';
41
40
  import { Alarm, alarmsIn } from './alarm.ts';
42
- import { Grants, GRANTS } from '../../dock/grants.ts';
41
+ import { BOX_SEED, GRANTS, Grants, Http, TIMER, WIRE, harborageOver, keyed, lent, settled, standHarbor, type Harborage, type Tree } from '../../box/index.ts';
43
42
  import { find, letIn } from '../../beings/join.ts';
44
43
  import { apiRoute } from '../../api/route.ts';
45
44
  import { mcpRoute, type Routes } from '../../mcp/route.ts';
@@ -47,14 +46,10 @@ import { McpHttp } from '../../mcp/http.ts';
47
46
  import { Exchange } from '../../mcp/allow.ts';
48
47
  import type { Invitation } from 'nervur';
49
48
  import { DurableStorage, danceInStorage, folderInStorage, settingsInStorage, wearInStorage } from './storage.ts';
50
- import { LENT, type Deployed, type Given, type Settings, type SettingsStore } from './given.ts';
49
+ import { type Deployed, type Given, type Settings, type SettingsStore } from './given.ts';
51
50
  import { sealKey } from '../seal.ts';
52
- import type { State, Env, Storage } from './platform.d.ts';
51
+ import type { State, Env } from './platform.d.ts';
53
52
 
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
53
  const json = (status: number, v: unknown, headers: Record<string, string> = {}) => new Response(JSON.stringify(v), { status, headers: { 'content-type': 'application/json', ...headers } });
59
54
 
60
55
  export class EdgeHarbor extends Harbor {
@@ -69,9 +64,6 @@ export class EdgeHarbor extends Harbor {
69
64
  // The model side's dance, one key in the same object: clients, pending
70
65
  // requests and codes, all of them living minutes.
71
66
  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
67
  #booted: Promise<void> | undefined;
76
68
  // The MCP endpoint, one per instance: a session is a client's connection
77
69
  // and belongs to the harbor, not to the request that opened it.
@@ -84,19 +76,33 @@ export class EdgeHarbor extends Harbor {
84
76
  static async open(state: State, env: Env, deployed: Deployed = {}): Promise<EdgeHarbor> {
85
77
  if (!env.NERVUR_SEED_KEY) throw new Error('no NERVUR_SEED_KEY: a seed is never kept plain on the edge');
86
78
  const harbor = new EdgeHarbor(new DurableStorage(state.storage, await sealKey(env.NERVUR_SEED_KEY)), env.NERVUR_ROOT, deployed);
87
- harbor.lent = LENT;
79
+ // What a worker brings to its own ward, and it is the whole of it. A
80
+ // bearer to an identity in a world is a record in the grants' cells and
81
+ // nothing else, so the grants are the same code here as on a disk. The
82
+ // timer is the same class too, with the one method that knows a terrain
83
+ // replaced: `Alarm` arms the object's own storage alarm.
84
+ //
85
+ // Nothing else is brought, each for its own reason. A worker is at no
86
+ // place and before no screen, so no location and no notifications. Push
87
+ // rings under this world's own web origin, which a worker learns from
88
+ // the request and not at boot. And no process stands behind a worker, so
89
+ // no way in over a socket, no dialers and no agents.
90
+ // Only the object may set its own alarm, so the two calls are handed to
91
+ // the timer in the class body this object brings.
92
+ const alarms = alarmsIn(state.storage);
93
+ class ObjectAlarm extends Alarm {
94
+ constructor(stance: Stance, box?: Harborage) {
95
+ super(stance, box, alarms);
96
+ }
97
+ }
98
+ harbor.faculties = { box: { Grants, Http }, harbor: { edge: { Alarm: ObjectAlarm } } };
88
99
  return harbor;
89
100
  }
90
101
 
91
102
  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 }));
103
+ super(store, async () => keyed({ ...BEINGS, classes: deployed.classes ?? {} }));
97
104
  this.root = root;
98
105
  this.deployed = deployed;
99
- this.#storage = store.storage;
100
106
  // The rows live beside the wards in the same object, under the same
101
107
  // prefix, so an exercise standing two harbors in one object gives each
102
108
  // its own settings and its own design.
@@ -112,11 +118,14 @@ export class EdgeHarbor extends Harbor {
112
118
  return { ...this.deployed, settings: await this.settings.load(), folder: this.folder };
113
119
  }
114
120
 
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;
121
+ // The classes this terrain brings to its own ward, as a tree. Set before
122
+ // the first request and read at the boot; a harbor that brings none stands
123
+ // no ward of its own and lends nothing. The deploy's own classes join them,
124
+ // so an estate's faculty stands beside the terrain's.
125
+ faculties: Tree | undefined;
126
+ // What this harbor is to its faculties: the box ward's save, which on this
127
+ // terrain is the one place the box keeps its state, and up once booted.
128
+ readonly box = harborageOver(() => this.wards.get(BOX_SEED)?.save() ?? Promise.resolve());
120
129
 
121
130
  // Once per instance, on the first request: the platform may have woken a
122
131
  // fresh instance, and every ward boots from storage before anything is
@@ -131,20 +140,13 @@ export class EdgeHarbor extends Harbor {
131
140
  // keeps, then the beings who took what the box lends: a box is up when
132
141
  // they have landed.
133
142
  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);
143
+ if (this.faculties) {
144
+ const tree = { ...this.faculties, classes: this.deployed.classes ?? {} };
145
+ await standHarbor(this, tree, this.box);
145
146
  }
146
147
  await super.boot();
147
- await settled();
148
+ await settled(this);
149
+ await this.box.raise();
148
150
  }
149
151
 
150
152
  // The alarm went off: no request, no route and nobody asking, which is the
@@ -156,7 +158,7 @@ export class EdgeHarbor extends Harbor {
156
158
  // saw it. A box whose alarm fired and whose partition was not written would
157
159
  // fire the same minute again on its next wake.
158
160
  async rang(): Promise<void> {
159
- const timer = facultyOf<Alarm>(this, TIMER);
161
+ const timer = await lent<Alarm>(this, TIMER);
160
162
  if (!timer) return;
161
163
  try {
162
164
  await timer.rang();
@@ -169,8 +171,8 @@ export class EdgeHarbor extends Harbor {
169
171
  // The box's grants, for the routes that honour a bearer. Undefined is a
170
172
  // harbor that stands no box ward, which is the exercise and nothing a
171
173
  // deploy does.
172
- get grants(): Grants | undefined {
173
- return facultyOf<Grants>(this, GRANTS);
174
+ grants(): Promise<Grants | undefined> {
175
+ return lent<Grants>(this, GRANTS);
174
176
  }
175
177
 
176
178
  // The web side, built per request because its whole configuration is rows
@@ -211,7 +213,7 @@ export class EdgeHarbor extends Harbor {
211
213
  // as a folder with several wards and no mark does. The box's own ward is
212
214
  // furniture and never a candidate.
213
215
  here(): string {
214
- const worlds = [...this.wards.keys()].filter((n) => n !== DOCK);
216
+ const worlds = [...this.wards.keys()].filter((n) => n !== BOX_SEED);
215
217
  return worlds.length === 1 ? worlds[0] : '';
216
218
  }
217
219
 
@@ -231,17 +233,16 @@ export class EdgeHarbor extends Harbor {
231
233
  // The worlds a client may be let into: every ward here but the box's own.
232
234
  #worlds = () =>
233
235
  [...this.wards]
234
- .filter(([ward]) => ward !== DOCK)
236
+ .filter(([ward]) => ward !== BOX_SEED)
235
237
  .map(([ward, h]) => ({ ward, pk: h.pk, user: h.record.user }));
236
238
 
237
239
  // The program side. The same `apiRoute` the daemon mounts, on the grants of
238
240
  // this box's own ward: a request with no bearer is the stranger every route
239
241
  // 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
+ async api(request: Request, path: string): Promise<Response> {
243
+ const grants = await this.grants();
242
244
  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
+ const route = apiRoute({ wards: this.wards, here: () => this.here() }, { grants, resolve: this.#resolve, join: this.#join });
245
246
  return (await route(request, path.slice('/api'.length))) ?? json(404, { error: 'no such route' });
246
247
  }
247
248
 
@@ -252,9 +253,9 @@ export class EdgeHarbor extends Harbor {
252
253
  // while one is held and a client that comes back to a woken instance
253
254
  // reconnects, which is what a reconnect is for.
254
255
  async mcp(request: Request, path: string, base: string): Promise<Response> {
255
- const grants = this.grants;
256
+ const grants = await this.grants();
256
257
  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
+ const endpoint =(this.#mcp ??= new McpHttp(this.#resolve, async () => {
258
259
  for (const h of this.wards.values()) await h.save();
259
260
  }));
260
261
  endpoint.gone = (identity, ward) => grants.revoke(identity, ward); // removal at the ward ends the grant at the box
@@ -266,7 +267,7 @@ export class EdgeHarbor extends Harbor {
266
267
  // The dance's discovery under this prefix, for a caller that reaches the
267
268
  // harbor at a path rather than at an origin of its own.
268
269
  async wellKnown(request: Request, path: string, base: string): Promise<Response> {
269
- const grants = this.grants;
270
+ const grants = await this.grants();
270
271
  if (!grants) return json(404, { error: 'no such route' });
271
272
  const route = await mcpRoute(this.dance, await this.routes(base), null, grants);
272
273
  return (await route.wellKnown(request, path.slice('/.well-known'.length))) ?? json(404, { error: 'no such route' });
@@ -275,7 +276,7 @@ export class EdgeHarbor extends Harbor {
275
276
  // The exchange, for the allow page on the web side: the dance it finishes
276
277
  // is the same one the model side keeps.
277
278
  async exchange(base: string): Promise<Exchange | undefined> {
278
- const grants = this.grants;
279
+ const grants = await this.grants();
279
280
  if (!grants) return undefined;
280
281
  const route = await mcpRoute(this.dance, await this.routes(base), null, grants);
281
282
  return new Exchange({ oauth: route.oauth, worlds: this.#worlds, join: this.#join });
@@ -293,7 +294,7 @@ export class EdgeHarbor extends Harbor {
293
294
  async fetch(request: Request, path: string, base: string): Promise<Response> {
294
295
  await this.ready();
295
296
  const out = await this.#route(request, path, base);
296
- await facultyOf<Alarm>(this, TIMER)?.setting;
297
+ await (await lent<Alarm>(this, TIMER))?.setting;
297
298
  return out;
298
299
  }
299
300
 
@@ -301,7 +302,7 @@ export class EdgeHarbor extends Harbor {
301
302
  if (path === '/health') return json(200, this.health(), { 'access-control-allow-origin': '*' });
302
303
  if (path === '/root') return this.rootAsk(request);
303
304
  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 === '/api' || path.startsWith('/api/')) return this.api(request, path);
305
306
  if (path === '/mcp' || path.startsWith('/mcp/')) return this.mcp(request, path, base);
306
307
  if (path.startsWith('/.well-known/')) return this.wellKnown(request, path, base);
307
308
  // The sealed bytes are the same route the daemon mounts; the upgrade is
@@ -310,7 +311,12 @@ export class EdgeHarbor extends Harbor {
310
311
  if ((path === '/quo' || path === '/quo/') && request.headers.get('upgrade')?.toLowerCase() === 'websocket') return this.upgrade();
311
312
  return (await quoRoute(this)(request, path.slice('/quo'.length))) ?? json(404, { error: 'no such route' });
312
313
  }
313
- return json(404, { error: 'no such route' });
314
+ // Every other path is the box's wire: a faculty that claims it answers,
315
+ // as the object's path and never the platform's prefix.
316
+ const wire = await lent<Http>(this, WIRE);
317
+ if (!wire) return json(404, { error: 'no such route' });
318
+ const url = new URL(request.url);
319
+ return wire.handle(new Request(new URL(`${path}${url.search}`, url.origin), request));
314
320
  }
315
321
 
316
322
  health() {
@@ -356,7 +362,7 @@ export class EdgeHarbor extends Harbor {
356
362
  async rootAsk(request: Request): Promise<Response> {
357
363
  const bearer = request.headers.get('authorization')?.replace(/^Bearer\s+/i, '') ?? '';
358
364
  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 } = {};
365
+ let body: { init?: { ward?: unknown; user?: unknown; class?: unknown }; settings?: Partial<Settings>; wear?: Record<string, unknown>; ward?: unknown; method?: unknown; args?: unknown } = {};
360
366
  try {
361
367
  body = (await request.json()) as typeof body;
362
368
  } catch {
@@ -368,7 +374,7 @@ export class EdgeHarbor extends Harbor {
368
374
  const user = typeof body.init.user === 'string' ? body.init.user : 'me';
369
375
  if (this.wards.has(name)) return json(409, { error: `ward ${name} already exists here` });
370
376
  const hosted = await this.create(name, user);
371
- await setup(hosted, user);
377
+ await setup(hosted, user, typeof body.init.class === 'string' ? { home: body.init.class } : {});
372
378
  return json(200, { name, pk: hosted.pk, user });
373
379
  }
374
380
  // The settings, merged over what is kept and answered whole, so a root
@@ -189,14 +189,14 @@ export async function exercise(request: Request, base: string, path: string, sta
189
189
  // the same refusal. That the number a deployment runs under is the
190
190
  // platform's own is the line below it, which asks the store directly.
191
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.NERVUR_SEED_KEY ?? '');
192
+ const sealed = await sealKey(env.NERVUR_SEED_KEY ?? '');
193
193
  const small = 8_000;
194
- const h = new EdgeHarbor(new DurableStorage(state.storage, key, `c:${Date.now()}:`, small), undefined, DEPLOYED);
194
+ const h = new EdgeHarbor(new DurableStorage(state.storage, sealed, `c:${Date.now()}:`, small), undefined, DEPLOYED);
195
195
  await h.ready();
196
196
  const w = await h.create(`ceiling-${Date.now()}`);
197
197
  for (const [k, c] of [
198
- ['P', 'Printer'],
199
- ['A', 'Customer'],
198
+ ['P', 'classes/Printer'],
199
+ ['A', 'classes/Customer'],
200
200
  ])
201
201
  assert.deepEqual(await w.ask('boot', { key: k, class: c }), { booted: k });
202
202
  const printer = w.being('P') as unknown as Printer;
@@ -2,7 +2,7 @@
2
2
  // What a side mounted at the edge is given. `Given` is the one shape: the
3
3
  // code a page runs, the design a world wears, the public origins, the apps
4
4
  // table and the bell key. On the daemon the same list is what `cli/daemon.ts`
5
- // reads off a disk and out of `routes.json`; here it has two provenances,
5
+ // reads off a disk and out of the Dock's cells; here it has two provenances,
6
6
  // and the split is what the value is, not an accident of this file.
7
7
  //
8
8
  // `Deployed` is the half the deploy carries: the tab's bundle, the service
@@ -18,39 +18,9 @@
18
18
  // rows beside it, one per file, reached through `Folder` like every other
19
19
  // terrain's.
20
20
  import type { BeingClass } from 'nervur';
21
- import { GRANTS } from '../../dock/grants.ts';
22
- import { TIMER } from '../../dock/index.ts';
23
21
  import type { Apps, Folder } from '../../human/web.ts';
24
22
  import type { Routes } from '../../mcp/route.ts';
25
23
 
26
- // What a worker lends the beings of the wards it holds, by name to the class
27
- // that stands for it. The box's state has one home on every terrain, the box
28
- // ward's partition, so the edge stands that ward like every other terrain;
29
- // what differs is this list, and a faculty the platform has no answer for is
30
- // not lent at all rather than lent and hollow.
31
- //
32
- // `grants` and `timer` are lent, and they are the whole of it. A bearer to an
33
- // identity in a world is a record in her cells and nothing else, so the
34
- // grants are the same code here as on a disk and the routes that honour a
35
- // bearer, `api.` and `mcp.`, work the moment she stands. The timer is the
36
- // same class too, with the one method that knows a terrain replaced: she arms
37
- // the object's own storage alarm, which is the clock here, so a home on this
38
- // terrain wakes for its schedules with nobody asking. `Alarm` in
39
- // `edge/alarm.ts` is that subclass.
40
- //
41
- // The three others every box ships are not lent here, each for its own
42
- // reason. `location` and `notifications` are a place and a screen, and a
43
- // worker is at neither: it runs wherever the platform put it and no human is
44
- // looking at it. `push` rings under this world's own web origin, and a worker
45
- // learns its origin from the request it is answering and not at boot, so a
46
- // ring signed at the moment the faculty stood would be signed for nowhere.
47
- //
48
- // There is no process on the other side of a worker either, so `local`,
49
- // `dialers` and `agents`, which the daemon adds, name nothing here: no
50
- // socket a process was seen on, no line this box holds open, and no command
51
- // to run an agent in.
52
- export const LENT: Record<string, string> = { [GRANTS]: 'Grants', [TIMER]: 'Alarm' };
53
-
54
24
  // The ceiling on one row in this object's storage, key and value together,
55
25
  // in bytes. The platform publishes it for a SQLite-backed Durable Object,
56
26
  // which is what `wrangler.toml` declares this class to be, and past it the
@@ -74,7 +44,7 @@ export type Code = { tab: string; worker: string };
74
44
  // the worker's own origin, no apps, no doorbell.
75
45
  export type Settings = {
76
46
  // the public faces of the four routes, as the proxy serves them, the same
77
- // shape `routes.json` holds on a disk
47
+ // shape the Dock's routes cell holds on a disk
78
48
  routes?: Routes;
79
49
  // the apps this world's links open, or none
80
50
  apps?: Apps | null;
@@ -97,7 +67,7 @@ export type SettingsStore = {
97
67
  // platform is, and never a deploy.
98
68
  export type Deployed = {
99
69
  code?: Code;
100
- // being classes beyond the built-ins, this deployment's own
70
+ // this deployment's own classes, held under `classes/` beside the dock's `beings/`
101
71
  classes?: Record<string, BeingClass>;
102
72
  };
103
73
 
@@ -39,8 +39,8 @@ export function settingsInStorage(storage: Rows, prefix = ''): SettingsStore {
39
39
  // storage instead. A row is text, because a stylesheet is text and so is a
40
40
  // bundle; a file the root has not put there answers nothing and the route
41
41
  // declines the path.
42
- export const DESIGN_ROW = 'design:';
43
- export const BEINGS_ROW = 'beings';
42
+ const DESIGN_ROW = 'design:';
43
+ const BEINGS_ROW = 'beings';
44
44
 
45
45
  export function folderInStorage(storage: Rows, prefix = ''): Folder {
46
46
  const design = (file: string) => `${prefix}${DESIGN_ROW}${file}`;
package/harbor/http.ts CHANGED
@@ -100,7 +100,9 @@ export async function listenHttp(harbor: DiskHarbor, host: string, port: number,
100
100
  { headers: { 'access-control-allow-origin': '*' } },
101
101
  );
102
102
  }
103
- for (const [prefix, handler] of mounts) {
103
+ // the longest prefix first, so a route mounted later under a longer
104
+ // path is still tried before the wire mounted at the root
105
+ for (const [prefix, handler] of [...mounts].sort(([a], [b]) => b.length - a.length)) {
104
106
  if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) {
105
107
  const out = await handler(asRequest(req, url), url.pathname.slice(prefix.length));
106
108
  if (out) return out;
@@ -167,14 +169,14 @@ function asRequest(req: import('node:http').IncomingMessage, url: URL): Request
167
169
  method,
168
170
  headers,
169
171
  ...(bodied ? { body: Readable.toWeb(req) as ReadableStream<Uint8Array>, duplex: 'half' } : {}),
170
- } as RequestInit);
172
+ });
171
173
  }
172
174
 
173
175
  // And the one translation out.
174
176
  async function writeResponse(out: Response, res: import('node:http').ServerResponse): Promise<void> {
175
177
  const headers: Record<string, string | string[]> = {};
176
178
  out.headers.forEach((v, k) => {
177
- headers[k] = k === 'set-cookie' ? [...(Array.isArray(headers[k]) ? (headers[k] as string[]) : []), v] : v;
179
+ headers[k] = k === 'set-cookie' ? [...(Array.isArray(headers[k]) ? (headers[k]) : []), v] : v;
178
180
  });
179
181
  res.writeHead(out.status, headers);
180
182
  if (!out.body) return void res.end();
package/harbor/sealed.ts CHANGED
@@ -146,38 +146,3 @@ export class Sealed implements Store {
146
146
  await this.#files.write(`quo/${this.harbor}/reach.json`, JSON.stringify(all) + '\n');
147
147
  }
148
148
  }
149
-
150
- // Files and secrets in memory, for a suite and for a box that keeps
151
- // nothing: the custody rule is proven here once and holds on every
152
- // platform that hands the two.
153
- export function inMemory(): { files: Files; secrets: Secrets; disk: Map<string, string> } {
154
- const disk = new Map<string, string>();
155
- const dirs = new Set<string>();
156
- const vault = new Map<string, string>();
157
- const under = (dir: string) => [...disk.keys()].filter((k) => k.startsWith(`${dir}/`));
158
- const files: Files = {
159
- exists: async (p) => disk.has(p) || dirs.has(p),
160
- read: async (p) => {
161
- const v = disk.get(p);
162
- if (v === undefined) throw new Error(`no such file: ${p}`);
163
- return v;
164
- },
165
- write: async (p, text) => void disk.set(p, text),
166
- remove: async (p) => void disk.delete(p),
167
- mkdir: async (p) => {
168
- const parts = p.split('/');
169
- for (let i = 1; i <= parts.length; i += 1) dirs.add(parts.slice(0, i).join('/'));
170
- },
171
- rmdir: async (p) => {
172
- for (const k of under(p)) disk.delete(k);
173
- for (const d of [...dirs]) if (d === p || d.startsWith(`${p}/`)) dirs.delete(d);
174
- },
175
- list: async (p) => under(p).map((k) => k.slice(p.length + 1)).filter((k) => !k.includes('/')),
176
- };
177
- const secrets: Secrets = {
178
- get: async (i) => vault.get(i),
179
- set: async (i, v) => void vault.set(i, v),
180
- remove: async (i) => void vault.delete(i),
181
- };
182
- return { files, secrets, disk };
183
- }
package/harbor/tauri.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  // ships no daemon. It has no ear, since no store rings a desk; a desk is
12
12
  // reached while the app is open and drains what it missed when it opens.
13
13
  import type { BeingClass, JsonObject } from 'nervur';
14
- import { NOTIFICATIONS, Notifications } from '../dock/index.ts';
14
+ import { Notifications } from '../box/index.ts';
15
15
  import { BrowserHarbor } from './browser.ts';
16
16
  import { Sealed, type Files, type Secrets } from './sealed.ts';
17
17
 
@@ -24,6 +24,11 @@ export class DeskNotifications extends Notifications {
24
24
  super(stance);
25
25
  this.#core = core;
26
26
  }
27
+ // A desk's notification center is its core's, and a class handed a core
28
+ // stands wherever it was handed one.
29
+ static override stands(): boolean {
30
+ return true;
31
+ }
27
32
  override async notify(args: JsonObject): Promise<JsonObject> {
28
33
  const title = typeof args.title === 'string' ? args.title : '';
29
34
  const body = typeof args.body === 'string' ? args.body : '';
@@ -32,13 +37,6 @@ export class DeskNotifications extends Notifications {
32
37
  }
33
38
  }
34
39
 
35
- // What a desk lends: its notification center, under the name every box
36
- // lends it under. Location is what the platform gives, which on a desk is
37
- // nothing, so it is not lent rather than lent and dumb. No timer either: a
38
- // desk holds presences and no home, so no Clock lives on it and a Timer
39
- // there would arm for nobody. A Timer stands only on a box that hosts a
40
- // home.
41
- export const deskLent: Record<string, string> = { [NOTIFICATIONS]: 'Notifications' };
42
40
 
43
41
  // The two seams over the core's commands. Paths are relative to the app's
44
42
  // data folder, and the core refuses one that climbs out of it.
@@ -72,8 +70,13 @@ export async function deskHarbor(core: Core, name = 'nervur', classes: Record<st
72
70
  }
73
71
  }
74
72
  const store = await Sealed.open(name, deskFiles(core), deskSecrets(core));
75
- const h = new BrowserHarbor(store, { Notifications: Center, ...classes });
76
- await h.stand(deskLent);
73
+ // A desk brings its notification center and nothing else. Location is what
74
+ // the platform gives, which on a desk is nothing, so no class for it is
75
+ // brought rather than one brought and dumb. No timer either: a desk holds
76
+ // presences and no home, so no Clock lives on it and a Timer there would
77
+ // arm for nobody.
78
+ const h = new BrowserHarbor(store, classes);
79
+ await h.stand({ harbor: { DeskNotifications: Center } });
77
80
  await h.boot();
78
81
  return h;
79
82
  }
package/human/app.ts CHANGED
@@ -19,12 +19,12 @@ import { domSurface } from './dom.ts';
19
19
  import { guest } from './guest.ts';
20
20
  import { door, held as heldPage } from './door.ts';
21
21
  import { USER } from '../beings/avatar.ts';
22
- import { DOCK, CONSOLE, NOTIFICATIONS, settled } from '../dock/index.ts';
22
+ import { BOX, BOX_SEED, NOTIFICATIONS, Notifications, lent, settled } from '../box/index.ts';
23
23
  import { fresh, joined, localName, ordered, relations, world, type Joined, type Relation, type World } from './worlds.ts';
24
24
  import { giveWorld, takeWorld, type Giving } from './move.ts';
25
25
  import { invitationOf, reachOf } from '../beings/link.ts';
26
26
  import { doorbell, drained, told, notice as noticeOf, type Bell, type Held } from './wake.ts';
27
- import type { Token } from '../dock/index.ts';
27
+ import type { Token } from '../box/index.ts';
28
28
  import type { Config } from './tab.ts';
29
29
 
30
30
  // What one world's address answers when an app asks it for JSON, and the
@@ -58,7 +58,7 @@ export type Dialing = Harbor & { dial(url: string): unknown };
58
58
  //
59
59
  // What the human reads when the device was woken while nobody was looking
60
60
  // is composed here from objects that crossed sealed, never from the ring,
61
- // and shown by asking the dock ward's notifications being through the
61
+ // and shown by asking the box ward's notifications being through the
62
62
  // harbor's own pointer: the same act on every terrain, and no second body.
63
63
  export type Ear = () => Promise<Token | null>;
64
64
 
@@ -161,7 +161,7 @@ export async function app(o: AppOptions): Promise<Running> {
161
161
  // by class and a class beats the hidden attribute.
162
162
  //
163
163
  // Its last entry is never a world: it is this device, the box's own ward
164
- // shown through the Console, where a human sees what the box lends and
164
+ // shown through its own being, where a human sees what the box lends and
165
165
  // leaves a world. A box that lends nothing has no such page.
166
166
  const switcher = () => {
167
167
  nav.replaceChildren();
@@ -289,15 +289,15 @@ export async function app(o: AppOptions): Promise<Running> {
289
289
  side = s;
290
290
  };
291
291
 
292
- // The box's own ward, as its harbor stood it: the Console carrying every
292
+ // The box's own ward, as its harbor unpacked it: its being carrying every
293
293
  // faculty this box lends. A box that lends nothing stands none, and there
294
294
  // is no page of its own to show.
295
295
  const deviceWard = async (): Promise<Hosted | null> => {
296
- const ward = o.harbor.wards.get(DOCK);
297
- if (!ward || !ward.being(CONSOLE)) return null;
296
+ const ward = o.harbor.wards.get(BOX_SEED);
297
+ if (!ward || !ward.being(BOX)) return null;
298
298
  return ward;
299
299
  };
300
- const hasDevice = (): boolean => o.harbor.wards.get(DOCK)?.being(CONSOLE) !== undefined;
300
+ const hasDevice = (): boolean => o.harbor.wards.get(BOX_SEED)?.being(BOX) !== undefined;
301
301
 
302
302
  // The device shown by itself: the last entry of the switcher, and the
303
303
  // whole screen of a device that has joined no world. A fresh install has
@@ -316,7 +316,7 @@ export async function app(o: AppOptions): Promise<Running> {
316
316
  const had = relations(ward)[0];
317
317
  if (had) return { ward, rel: had };
318
318
  const rel = await fresh(ward);
319
- const inv = await ward.ask('invite', { being: CONSOLE, id: rel.key });
319
+ const inv = await ward.ask('invite', { being: BOX, id: rel.key });
320
320
  if (!inv || typeof inv !== 'object') throw new Error('this device is already held under that name');
321
321
  const took = (await ward.ask('knock', { being: rel.key, id: USER, invitation: inv as unknown as JsonObject })) as { taken?: string; error?: string };
322
322
  if (took.taken !== USER) throw new Error(took.error ?? 'the shell could not take this device');
@@ -424,7 +424,7 @@ export async function app(o: AppOptions): Promise<Running> {
424
424
  // was there a moment ago. A page paints once, so asking once more here
425
425
  // is the difference between a device with its faculties on screen and a
426
426
  // device that looks empty until the person navigates away and back.
427
- await settled();
427
+ await settled(o.harbor);
428
428
  const now = await rel.avatar.tools();
429
429
  if ('asks' in now) got = now;
430
430
  await inside(ward, rel, notice, got);
@@ -518,9 +518,8 @@ export async function app(o: AppOptions): Promise<Running> {
518
518
  // the shell asks and does not read the answer, since the news is already
519
519
  // on the page where the push landed.
520
520
  const shown = async (line: { title: string; body: string }) => {
521
- const dock = o.harbor.wards.get(DOCK);
522
- if (!dock) return;
523
- await dock.ask('ask', { being: NOTIFICATIONS, method: 'notify', args: line }).catch(() => {});
521
+ await (await lent<Notifications>(o.harbor, NOTIFICATIONS))?.notify(line).catch(() => {});
522
+ await o.harbor.wards.get(BOX_SEED)?.save();
524
523
  };
525
524
 
526
525
  // The device ward before anything is dialed: a harbor announces the wards
package/human/door.ts CHANGED
@@ -90,7 +90,7 @@ export const ours: Painter = {
90
90
  // an error object to a machine, and neither is a sentence.
91
91
  export type Landed = { world?: string; host: string; at?: string; ok: boolean };
92
92
 
93
- export function landedTree(l: Landed): Node {
93
+ function landedTree(l: Landed): Node {
94
94
  const of: Node[] = [
95
95
  { kind: 'image', src: MARK, alt: '' },
96
96
  { kind: 'text', text: l.ok ? 'That is done' : 'That did not finish', role: 'title' },
package/human/html.ts CHANGED
@@ -436,7 +436,7 @@ export function page(m: Model): string {
436
436
  const a = inGroup.find((x) => x.name === prefix + name);
437
437
  return a ? one(kept, prefix)(a) : '';
438
438
  },
439
- answer: (name, as) => (inGroup.some((x) => x.name === prefix + name) ? answer(prefix + name, as) : ''),
439
+ answer: (name, shown) => (inGroup.some((x) => x.name === prefix + name) ? answer(prefix + name, shown) : ''),
440
440
  link: linkedBy(kept), // a carried standing vouches by her own look and never her carrier's
441
441
  standing: () => '',
442
442
  standings: () => '',
package/human/local.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  // the subject is the owner's `ask` and nothing else: no standing and no
12
12
  // relation, since the local ward is the human's and she is on it. The ward
13
13
  // names the asker, bounds the wait, judges her answer and saves before the
14
- // pointer returns, so this file holds an id nobody minted no longer.
14
+ // pointer returns, so this file holds no id of its own.
15
15
  import type { Answer, Blueprint, JsonObject } from 'nervur';
16
16
  import type { Subject } from '../beings/side.ts';
17
17
 
package/human/move.ts CHANGED
@@ -35,7 +35,7 @@ const KEY = 'courier';
35
35
  async function courier(harbor: Harbor): Promise<Courier> {
36
36
  const ward = harbor.wards.get(COURIER) ?? (await harbor.create(COURIER, 'me'));
37
37
  if (!ward.being(KEY)) {
38
- const out = (await ward.ask('boot', { key: KEY, class: 'Courier' })) as { error?: string };
38
+ const out = (await ward.ask('boot', { key: KEY, class: 'beings/Courier' })) as { error?: string };
39
39
  if (out.error) throw new Error(`no courier on this device: ${out.error}`);
40
40
  }
41
41
  return ward.being(KEY) as Courier;