@nervur-org/dock 0.5.0 → 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 (216) hide show
  1. package/README.md +7 -4
  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 +9 -8
  192. package/beings/envoy.ts +0 -218
  193. package/dist/beings/envoy.d.ts +0 -54
  194. package/dist/beings/envoy.js +0 -211
  195. package/dist/dock/console.d.ts +0 -6
  196. package/dist/dock/console.js +0 -20
  197. package/dist/dock/dialers.js +0 -75
  198. package/dist/dock/faculty.d.ts +0 -24
  199. package/dist/dock/faculty.js +0 -142
  200. package/dist/dock/grants.js +0 -99
  201. package/dist/dock/index.d.ts +0 -41
  202. package/dist/dock/index.js +0 -113
  203. package/dist/dock/local.js +0 -44
  204. package/dist/dock/location.js +0 -11
  205. package/dist/dock/push.js +0 -132
  206. package/dist/dock/timer.js +0 -149
  207. package/dock/console.ts +0 -23
  208. package/dock/dialers.ts +0 -85
  209. package/dock/faculty.ts +0 -148
  210. package/dock/grants.ts +0 -111
  211. package/dock/index.ts +0 -133
  212. package/dock/local.ts +0 -48
  213. package/dock/push.ts +0 -161
  214. package/dock/timer.ts +0 -174
  215. /package/dist/{dock → box}/fcm.js +0 -0
  216. /package/dist/{dock → box}/webpush.js +0 -0
@@ -1,24 +1,19 @@
1
1
  import { isSilence } from 'nervur';
2
- import { User, Desk, Avatar, Clock } from '../../beings/index.js';
2
+ import { BEINGS } from '../../beings/index.js';
3
3
  import { setup } from '../../beings/setup.js';
4
4
  import { Harbor, Socket as Held } from 'nervur/harbor';
5
5
  import { quoRoute } from '../quo.js';
6
6
  import { webRoute } from '../../human/web.js';
7
- import { DOCK, TIMER, Console, facultyOf, settled, stand } from '../../dock/index.js';
8
7
  import { Alarm, alarmsIn } from './alarm.js';
9
- import { Grants, GRANTS } from '../../dock/grants.js';
8
+ import { BOX_SEED, GRANTS, Grants, Http, TIMER, WIRE, harborageOver, keyed, lent, settled, standHarbor } from '../../box/index.js';
10
9
  import { find, letIn } from '../../beings/join.js';
11
10
  import { apiRoute } from '../../api/route.js';
12
11
  import { mcpRoute } from '../../mcp/route.js';
13
12
  import { McpHttp } from '../../mcp/http.js';
14
13
  import { Exchange } from '../../mcp/allow.js';
15
14
  import { DurableStorage, danceInStorage, folderInStorage, settingsInStorage, wearInStorage } from './storage.js';
16
- import { LENT } from './given.js';
15
+ import {} from './given.js';
17
16
  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
17
  const json = (status, v, headers = {}) => new Response(JSON.stringify(v), { status, headers: { 'content-type': 'application/json', ...headers } });
23
18
  export class EdgeHarbor extends Harbor {
24
19
  sockets = new Set();
@@ -32,9 +27,6 @@ export class EdgeHarbor extends Harbor {
32
27
  // The model side's dance, one key in the same object: clients, pending
33
28
  // requests and codes, all of them living minutes.
34
29
  dance;
35
- // The object's storage, unprefixed and unsealed: the alarm is the object's
36
- // own and is no row.
37
- #storage;
38
30
  #booted;
39
31
  // The MCP endpoint, one per instance: a session is a client's connection
40
32
  // and belongs to the harbor, not to the request that opened it.
@@ -47,18 +39,32 @@ export class EdgeHarbor extends Harbor {
47
39
  if (!env.NERVUR_SEED_KEY)
48
40
  throw new Error('no NERVUR_SEED_KEY: a seed is never kept plain on the edge');
49
41
  const harbor = new EdgeHarbor(new DurableStorage(state.storage, await sealKey(env.NERVUR_SEED_KEY)), env.NERVUR_ROOT, deployed);
50
- harbor.lent = LENT;
42
+ // What a worker brings to its own ward, and it is the whole of it. A
43
+ // bearer to an identity in a world is a record in the grants' cells and
44
+ // nothing else, so the grants are the same code here as on a disk. The
45
+ // timer is the same class too, with the one method that knows a terrain
46
+ // replaced: `Alarm` arms the object's own storage alarm.
47
+ //
48
+ // Nothing else is brought, each for its own reason. A worker is at no
49
+ // place and before no screen, so no location and no notifications. Push
50
+ // rings under this world's own web origin, which a worker learns from
51
+ // the request and not at boot. And no process stands behind a worker, so
52
+ // no way in over a socket, no dialers and no agents.
53
+ // Only the object may set its own alarm, so the two calls are handed to
54
+ // the timer in the class body this object brings.
55
+ const alarms = alarmsIn(state.storage);
56
+ class ObjectAlarm extends Alarm {
57
+ constructor(stance, box) {
58
+ super(stance, box, alarms);
59
+ }
60
+ }
61
+ harbor.faculties = { box: { Grants, Http }, harbor: { edge: { Alarm: ObjectAlarm } } };
51
62
  return harbor;
52
63
  }
53
64
  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 }));
65
+ super(store, async () => keyed({ ...BEINGS, classes: deployed.classes ?? {} }));
59
66
  this.root = root;
60
67
  this.deployed = deployed;
61
- this.#storage = store.storage;
62
68
  // The rows live beside the wards in the same object, under the same
63
69
  // prefix, so an exercise standing two harbors in one object gives each
64
70
  // its own settings and its own design.
@@ -72,11 +78,14 @@ export class EdgeHarbor extends Harbor {
72
78
  async given() {
73
79
  return { ...this.deployed, settings: await this.settings.load(), folder: this.folder };
74
80
  }
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;
81
+ // The classes this terrain brings to its own ward, as a tree. Set before
82
+ // the first request and read at the boot; a harbor that brings none stands
83
+ // no ward of its own and lends nothing. The deploy's own classes join them,
84
+ // so an estate's faculty stands beside the terrain's.
85
+ faculties;
86
+ // What this harbor is to its faculties: the box ward's save, which on this
87
+ // terrain is the one place the box keeps its state, and up once booted.
88
+ box = harborageOver(() => this.wards.get(BOX_SEED)?.save() ?? Promise.resolve());
80
89
  // Once per instance, on the first request: the platform may have woken a
81
90
  // fresh instance, and every ward boots from storage before anything is
82
91
  // answered.
@@ -89,22 +98,13 @@ export class EdgeHarbor extends Harbor {
89
98
  // keeps, then the beings who took what the box lends: a box is up when
90
99
  // they have landed.
91
100
  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);
101
+ if (this.faculties) {
102
+ const tree = { ...this.faculties, classes: this.deployed.classes ?? {} };
103
+ await standHarbor(this, tree, this.box);
105
104
  }
106
105
  await super.boot();
107
- await settled();
106
+ await settled(this);
107
+ await this.box.raise();
108
108
  }
109
109
  // The alarm went off: no request, no route and nobody asking, which is the
110
110
  // whole point of it. The box ward is up by now, so the round runs and the
@@ -115,7 +115,7 @@ export class EdgeHarbor extends Harbor {
115
115
  // saw it. A box whose alarm fired and whose partition was not written would
116
116
  // fire the same minute again on its next wake.
117
117
  async rang() {
118
- const timer = facultyOf(this, TIMER);
118
+ const timer = await lent(this, TIMER);
119
119
  if (!timer)
120
120
  return;
121
121
  try {
@@ -130,8 +130,8 @@ export class EdgeHarbor extends Harbor {
130
130
  // The box's grants, for the routes that honour a bearer. Undefined is a
131
131
  // harbor that stands no box ward, which is the exercise and nothing a
132
132
  // deploy does.
133
- get grants() {
134
- return facultyOf(this, GRANTS);
133
+ grants() {
134
+ return lent(this, GRANTS);
135
135
  }
136
136
  // The web side, built per request because its whole configuration is rows
137
137
  // a root writes while the worker runs. It is the same `webRoute` the daemon
@@ -171,7 +171,7 @@ export class EdgeHarbor extends Harbor {
171
171
  // as a folder with several wards and no mark does. The box's own ward is
172
172
  // furniture and never a candidate.
173
173
  here() {
174
- const worlds = [...this.wards.keys()].filter((n) => n !== DOCK);
174
+ const worlds = [...this.wards.keys()].filter((n) => n !== BOX_SEED);
175
175
  return worlds.length === 1 ? worlds[0] : '';
176
176
  }
177
177
  // What a route asks of a ward here: her avatar as she stands, and her way
@@ -190,17 +190,16 @@ export class EdgeHarbor extends Harbor {
190
190
  };
191
191
  // The worlds a client may be let into: every ward here but the box's own.
192
192
  #worlds = () => [...this.wards]
193
- .filter(([ward]) => ward !== DOCK)
193
+ .filter(([ward]) => ward !== BOX_SEED)
194
194
  .map(([ward, h]) => ({ ward, pk: h.pk, user: h.record.user }));
195
195
  // The program side. The same `apiRoute` the daemon mounts, on the grants of
196
196
  // this box's own ward: a request with no bearer is the stranger every route
197
197
  // knows, and one with a bearer this box granted is an occupant.
198
- async api(request, path, base) {
199
- const grants = this.grants;
198
+ async api(request, path) {
199
+ const grants = await this.grants();
200
200
  if (!grants)
201
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 });
202
+ const route = apiRoute({ wards: this.wards, here: () => this.here() }, { grants, resolve: this.#resolve, join: this.#join });
204
203
  return (await route(request, path.slice('/api'.length))) ?? json(404, { error: 'no such route' });
205
204
  }
206
205
  // The model side: the OAuth dance, kept in this object's own storage, and
@@ -210,7 +209,7 @@ export class EdgeHarbor extends Harbor {
210
209
  // while one is held and a client that comes back to a woken instance
211
210
  // reconnects, which is what a reconnect is for.
212
211
  async mcp(request, path, base) {
213
- const grants = this.grants;
212
+ const grants = await this.grants();
214
213
  if (!grants)
215
214
  return json(503, { error: 'this box stands no ward of its own, so it grants nothing' });
216
215
  const endpoint = (this.#mcp ??= new McpHttp(this.#resolve, async () => {
@@ -225,7 +224,7 @@ export class EdgeHarbor extends Harbor {
225
224
  // The dance's discovery under this prefix, for a caller that reaches the
226
225
  // harbor at a path rather than at an origin of its own.
227
226
  async wellKnown(request, path, base) {
228
- const grants = this.grants;
227
+ const grants = await this.grants();
229
228
  if (!grants)
230
229
  return json(404, { error: 'no such route' });
231
230
  const route = await mcpRoute(this.dance, await this.routes(base), null, grants);
@@ -234,7 +233,7 @@ export class EdgeHarbor extends Harbor {
234
233
  // The exchange, for the allow page on the web side: the dance it finishes
235
234
  // is the same one the model side keeps.
236
235
  async exchange(base) {
237
- const grants = this.grants;
236
+ const grants = await this.grants();
238
237
  if (!grants)
239
238
  return undefined;
240
239
  const route = await mcpRoute(this.dance, await this.routes(base), null, grants);
@@ -252,7 +251,7 @@ export class EdgeHarbor extends Harbor {
252
251
  async fetch(request, path, base) {
253
252
  await this.ready();
254
253
  const out = await this.#route(request, path, base);
255
- await facultyOf(this, TIMER)?.setting;
254
+ await (await lent(this, TIMER))?.setting;
256
255
  return out;
257
256
  }
258
257
  async #route(request, path, base) {
@@ -263,7 +262,7 @@ export class EdgeHarbor extends Harbor {
263
262
  if (path === '/web' || path.startsWith('/web/'))
264
263
  return this.web(request, path, base);
265
264
  if (path === '/api' || path.startsWith('/api/'))
266
- return this.api(request, path, base);
265
+ return this.api(request, path);
267
266
  if (path === '/mcp' || path.startsWith('/mcp/'))
268
267
  return this.mcp(request, path, base);
269
268
  if (path.startsWith('/.well-known/'))
@@ -275,7 +274,13 @@ export class EdgeHarbor extends Harbor {
275
274
  return this.upgrade();
276
275
  return (await quoRoute(this)(request, path.slice('/quo'.length))) ?? json(404, { error: 'no such route' });
277
276
  }
278
- return json(404, { error: 'no such route' });
277
+ // Every other path is the box's wire: a faculty that claims it answers,
278
+ // as the object's path and never the platform's prefix.
279
+ const wire = await lent(this, WIRE);
280
+ if (!wire)
281
+ return json(404, { error: 'no such route' });
282
+ const url = new URL(request.url);
283
+ return wire.handle(new Request(new URL(`${path}${url.search}`, url.origin), request));
279
284
  }
280
285
  health() {
281
286
  return {
@@ -328,7 +333,7 @@ export class EdgeHarbor extends Harbor {
328
333
  if (this.wards.has(name))
329
334
  return json(409, { error: `ward ${name} already exists here` });
330
335
  const hosted = await this.create(name, user);
331
- await setup(hosted, user);
336
+ await setup(hosted, user, typeof body.init.class === 'string' ? { home: body.init.class } : {});
332
337
  return json(200, { name, pk: hosted.pk, user });
333
338
  }
334
339
  // The settings, merged over what is kept and answered whole, so a root
@@ -180,14 +180,14 @@ export async function exercise(request, base, path, state, env) {
180
180
  // the same refusal. That the number a deployment runs under is the
181
181
  // platform's own is the line below it, which asks the store directly.
182
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.NERVUR_SEED_KEY ?? '');
183
+ const sealed = await sealKey(env.NERVUR_SEED_KEY ?? '');
184
184
  const small = 8_000;
185
- const h = new EdgeHarbor(new DurableStorage(state.storage, key, `c:${Date.now()}:`, small), undefined, DEPLOYED);
185
+ const h = new EdgeHarbor(new DurableStorage(state.storage, sealed, `c:${Date.now()}:`, small), undefined, DEPLOYED);
186
186
  await h.ready();
187
187
  const w = await h.create(`ceiling-${Date.now()}`);
188
188
  for (const [k, c] of [
189
- ['P', 'Printer'],
190
- ['A', 'Customer'],
189
+ ['P', 'classes/Printer'],
190
+ ['A', 'classes/Customer'],
191
191
  ])
192
192
  assert.deepEqual(await w.ask('boot', { key: k, class: c }), { booted: k });
193
193
  const printer = w.being('P');
@@ -1,7 +1,6 @@
1
1
  import type { BeingClass } from 'nervur';
2
2
  import type { Apps, Folder } from '../../human/web.ts';
3
3
  import type { Routes } from '../../mcp/route.ts';
4
- export declare const LENT: Record<string, string>;
5
4
  export declare const ROW_CEILING = 2000000;
6
5
  export type Code = {
7
6
  tab: string;
@@ -1,32 +1,3 @@
1
- import { GRANTS } from '../../dock/grants.js';
2
- import { TIMER } from '../../dock/index.js';
3
- // What a worker lends the beings of the wards it holds, by name to the class
4
- // that stands for it. The box's state has one home on every terrain, the box
5
- // ward's partition, so the edge stands that ward like every other terrain;
6
- // what differs is this list, and a faculty the platform has no answer for is
7
- // not lent at all rather than lent and hollow.
8
- //
9
- // `grants` and `timer` are lent, and they are the whole of it. A bearer to an
10
- // identity in a world is a record in her cells and nothing else, so the
11
- // grants are the same code here as on a disk and the routes that honour a
12
- // bearer, `api.` and `mcp.`, work the moment she stands. The timer is the
13
- // same class too, with the one method that knows a terrain replaced: she arms
14
- // the object's own storage alarm, which is the clock here, so a home on this
15
- // terrain wakes for its schedules with nobody asking. `Alarm` in
16
- // `edge/alarm.ts` is that subclass.
17
- //
18
- // The three others every box ships are not lent here, each for its own
19
- // reason. `location` and `notifications` are a place and a screen, and a
20
- // worker is at neither: it runs wherever the platform put it and no human is
21
- // looking at it. `push` rings under this world's own web origin, and a worker
22
- // learns its origin from the request it is answering and not at boot, so a
23
- // ring signed at the moment the faculty stood would be signed for nowhere.
24
- //
25
- // There is no process on the other side of a worker either, so `local`,
26
- // `dialers` and `agents`, which the daemon adds, name nothing here: no
27
- // socket a process was seen on, no line this box holds open, and no command
28
- // to run an agent in.
29
- export const LENT = { [GRANTS]: 'Grants', [TIMER]: 'Alarm' };
30
1
  // The ceiling on one row in this object's storage, key and value together,
31
2
  // in bytes. The platform publishes it for a SQLite-backed Durable Object,
32
3
  // which is what `wrangler.toml` declares this class to be, and past it the
@@ -4,8 +4,6 @@ import type { Folder } from '../../human/web.ts';
4
4
  import { type SettingsStore } from './given.ts';
5
5
  import type { Rows, Storage } from './platform.d.ts';
6
6
  export declare function settingsInStorage(storage: Rows, prefix?: string): SettingsStore;
7
- export declare const DESIGN_ROW = "design:";
8
- export declare const BEINGS_ROW = "beings";
9
7
  export declare function folderInStorage(storage: Rows, prefix?: string): Folder;
10
8
  export declare function wearInStorage(storage: Rows, prefix?: string): {
11
9
  put: (file: string, text: string | null) => Promise<void>;
@@ -35,8 +35,8 @@ export function settingsInStorage(storage, prefix = '') {
35
35
  // storage instead. A row is text, because a stylesheet is text and so is a
36
36
  // bundle; a file the root has not put there answers nothing and the route
37
37
  // declines the path.
38
- export const DESIGN_ROW = 'design:';
39
- export const BEINGS_ROW = 'beings';
38
+ const DESIGN_ROW = 'design:';
39
+ const BEINGS_ROW = 'beings';
40
40
  export function folderInStorage(storage, prefix = '') {
41
41
  const design = (file) => `${prefix}${DESIGN_ROW}${file}`;
42
42
  const held = (file) => storage.get(design(file)).then((v) => v ?? null);
@@ -87,7 +87,9 @@ export async function listenHttp(harbor, host, port, quo) {
87
87
  faults: Object.fromEntries([...harbor.faults]),
88
88
  }, { headers: { 'access-control-allow-origin': '*' } });
89
89
  }
90
- for (const [prefix, handler] of mounts) {
90
+ // the longest prefix first, so a route mounted later under a longer
91
+ // path is still tried before the wire mounted at the root
92
+ for (const [prefix, handler] of [...mounts].sort(([a], [b]) => b.length - a.length)) {
91
93
  if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) {
92
94
  const out = await handler(asRequest(req, url), url.pathname.slice(prefix.length));
93
95
  if (out)
@@ -161,7 +163,7 @@ function asRequest(req, url) {
161
163
  async function writeResponse(out, res) {
162
164
  const headers = {};
163
165
  out.headers.forEach((v, k) => {
164
- headers[k] = k === 'set-cookie' ? [...(Array.isArray(headers[k]) ? headers[k] : []), v] : v;
166
+ headers[k] = k === 'set-cookie' ? [...(Array.isArray(headers[k]) ? (headers[k]) : []), v] : v;
165
167
  });
166
168
  res.writeHead(out.status, headers);
167
169
  if (!out.body)
@@ -28,8 +28,3 @@ export declare class Sealed implements Store {
28
28
  hints(): Promise<Record<string, string>>;
29
29
  hint(pk: string, url: string): Promise<void>;
30
30
  }
31
- export declare function inMemory(): {
32
- files: Files;
33
- secrets: Secrets;
34
- disk: Map<string, string>;
35
- };
@@ -121,42 +121,3 @@ export class Sealed {
121
121
  await this.#files.write(`quo/${this.harbor}/reach.json`, JSON.stringify(all) + '\n');
122
122
  }
123
123
  }
124
- // Files and secrets in memory, for a suite and for a box that keeps
125
- // nothing: the custody rule is proven here once and holds on every
126
- // platform that hands the two.
127
- export function inMemory() {
128
- const disk = new Map();
129
- const dirs = new Set();
130
- const vault = new Map();
131
- const under = (dir) => [...disk.keys()].filter((k) => k.startsWith(`${dir}/`));
132
- const files = {
133
- exists: async (p) => disk.has(p) || dirs.has(p),
134
- read: async (p) => {
135
- const v = disk.get(p);
136
- if (v === undefined)
137
- throw new Error(`no such file: ${p}`);
138
- return v;
139
- },
140
- write: async (p, text) => void disk.set(p, text),
141
- remove: async (p) => void disk.delete(p),
142
- mkdir: async (p) => {
143
- const parts = p.split('/');
144
- for (let i = 1; i <= parts.length; i += 1)
145
- dirs.add(parts.slice(0, i).join('/'));
146
- },
147
- rmdir: async (p) => {
148
- for (const k of under(p))
149
- disk.delete(k);
150
- for (const d of [...dirs])
151
- if (d === p || d.startsWith(`${p}/`))
152
- dirs.delete(d);
153
- },
154
- list: async (p) => under(p).map((k) => k.slice(p.length + 1)).filter((k) => !k.includes('/')),
155
- };
156
- const secrets = {
157
- get: async (i) => vault.get(i),
158
- set: async (i, v) => void vault.set(i, v),
159
- remove: async (i) => void vault.delete(i),
160
- };
161
- return { files, secrets, disk };
162
- }
@@ -1,5 +1,5 @@
1
1
  import type { BeingClass, JsonObject } from 'nervur';
2
- import { Notifications } from '../dock/index.ts';
2
+ import { Notifications } from '../box/index.ts';
3
3
  import { BrowserHarbor } from './browser.ts';
4
4
  import { type Files, type Secrets } from './sealed.ts';
5
5
  export type Core = {
@@ -8,9 +8,9 @@ export type Core = {
8
8
  export declare class DeskNotifications extends Notifications {
9
9
  #private;
10
10
  constructor(stance: ConstructorParameters<typeof Notifications>[0], core: Core);
11
+ static stands(): boolean;
11
12
  notify(args: JsonObject): Promise<JsonObject>;
12
13
  }
13
- export declare const deskLent: Record<string, string>;
14
14
  export declare function deskFiles(core: Core): Files;
15
15
  export declare function deskSecrets(core: Core): Secrets;
16
16
  export declare function deskHarbor(core: Core, name?: string, classes?: Record<string, BeingClass>): Promise<BrowserHarbor>;
@@ -1,4 +1,4 @@
1
- import { NOTIFICATIONS, Notifications } from '../dock/index.js';
1
+ import { Notifications } from '../box/index.js';
2
2
  import { BrowserHarbor } from './browser.js';
3
3
  import { Sealed } from './sealed.js';
4
4
  export class DeskNotifications extends Notifications {
@@ -7,6 +7,11 @@ export class DeskNotifications extends Notifications {
7
7
  super(stance);
8
8
  this.#core = core;
9
9
  }
10
+ // A desk's notification center is its core's, and a class handed a core
11
+ // stands wherever it was handed one.
12
+ static stands() {
13
+ return true;
14
+ }
10
15
  async notify(args) {
11
16
  const title = typeof args.title === 'string' ? args.title : '';
12
17
  const body = typeof args.body === 'string' ? args.body : '';
@@ -14,13 +19,6 @@ export class DeskNotifications extends Notifications {
14
19
  return { shown: true };
15
20
  }
16
21
  }
17
- // What a desk lends: its notification center, under the name every box
18
- // lends it under. Location is what the platform gives, which on a desk is
19
- // nothing, so it is not lent rather than lent and dumb. No timer either: a
20
- // desk holds presences and no home, so no Clock lives on it and a Timer
21
- // there would arm for nobody. A Timer stands only on a box that hosts a
22
- // home.
23
- export const deskLent = { [NOTIFICATIONS]: 'Notifications' };
24
22
  // The two seams over the core's commands. Paths are relative to the app's
25
23
  // data folder, and the core refuses one that climbs out of it.
26
24
  export function deskFiles(core) {
@@ -52,8 +50,13 @@ export async function deskHarbor(core, name = 'nervur', classes = {}) {
52
50
  }
53
51
  }
54
52
  const store = await Sealed.open(name, deskFiles(core), deskSecrets(core));
55
- const h = new BrowserHarbor(store, { Notifications: Center, ...classes });
56
- await h.stand(deskLent);
53
+ // A desk brings its notification center and nothing else. Location is what
54
+ // the platform gives, which on a desk is nothing, so no class for it is
55
+ // brought rather than one brought and dumb. No timer either: a desk holds
56
+ // presences and no home, so no Clock lives on it and a Timer there would
57
+ // arm for nobody.
58
+ const h = new BrowserHarbor(store, classes);
59
+ await h.stand({ harbor: { DeskNotifications: Center } });
57
60
  await h.boot();
58
61
  return h;
59
62
  }
@@ -2,7 +2,7 @@ import type { Harbor } from 'nervur/harbor';
2
2
  import type { Invitation, JsonObject } from 'nervur';
3
3
  import { type Joined, type World } from './worlds.ts';
4
4
  import { type Bell } from './wake.ts';
5
- import type { Token } from '../dock/index.ts';
5
+ import type { Token } from '../box/index.ts';
6
6
  import type { Config } from './tab.ts';
7
7
  export type Learn = (at: string) => Promise<Config>;
8
8
  export declare const learn: Learn;
package/dist/human/app.js CHANGED
@@ -3,7 +3,7 @@ import { domSurface } from './dom.js';
3
3
  import { guest } from './guest.js';
4
4
  import { door, held as heldPage } from './door.js';
5
5
  import { USER } from '../beings/avatar.js';
6
- import { DOCK, CONSOLE, NOTIFICATIONS, settled } from '../dock/index.js';
6
+ import { BOX, BOX_SEED, NOTIFICATIONS, Notifications, lent, settled } from '../box/index.js';
7
7
  import { fresh, joined, localName, ordered, relations, world } from './worlds.js';
8
8
  import { giveWorld, takeWorld } from './move.js';
9
9
  import { invitationOf, reachOf } from '../beings/link.js';
@@ -89,7 +89,7 @@ export async function app(o) {
89
89
  // by class and a class beats the hidden attribute.
90
90
  //
91
91
  // Its last entry is never a world: it is this device, the box's own ward
92
- // shown through the Console, where a human sees what the box lends and
92
+ // shown through its own being, where a human sees what the box lends and
93
93
  // leaves a world. A box that lends nothing has no such page.
94
94
  const switcher = () => {
95
95
  nav.replaceChildren();
@@ -220,16 +220,16 @@ export async function app(o) {
220
220
  });
221
221
  side = s;
222
222
  };
223
- // The box's own ward, as its harbor stood it: the Console carrying every
223
+ // The box's own ward, as its harbor unpacked it: its being carrying every
224
224
  // faculty this box lends. A box that lends nothing stands none, and there
225
225
  // is no page of its own to show.
226
226
  const deviceWard = async () => {
227
- const ward = o.harbor.wards.get(DOCK);
228
- if (!ward || !ward.being(CONSOLE))
227
+ const ward = o.harbor.wards.get(BOX_SEED);
228
+ if (!ward || !ward.being(BOX))
229
229
  return null;
230
230
  return ward;
231
231
  };
232
- const hasDevice = () => o.harbor.wards.get(DOCK)?.being(CONSOLE) !== undefined;
232
+ const hasDevice = () => o.harbor.wards.get(BOX_SEED)?.being(BOX) !== undefined;
233
233
  // The device shown by itself: the last entry of the switcher, and the
234
234
  // whole screen of a device that has joined no world. A fresh install has
235
235
  // no world's page to carry her forms, and a first screen that says only
@@ -249,7 +249,7 @@ export async function app(o) {
249
249
  if (had)
250
250
  return { ward, rel: had };
251
251
  const rel = await fresh(ward);
252
- const inv = await ward.ask('invite', { being: CONSOLE, id: rel.key });
252
+ const inv = await ward.ask('invite', { being: BOX, id: rel.key });
253
253
  if (!inv || typeof inv !== 'object')
254
254
  throw new Error('this device is already held under that name');
255
255
  const took = (await ward.ask('knock', { being: rel.key, id: USER, invitation: inv }));
@@ -362,7 +362,7 @@ export async function app(o) {
362
362
  // was there a moment ago. A page paints once, so asking once more here
363
363
  // is the difference between a device with its faculties on screen and a
364
364
  // device that looks empty until the person navigates away and back.
365
- await settled();
365
+ await settled(o.harbor);
366
366
  const now = await rel.avatar.tools();
367
367
  if ('asks' in now)
368
368
  got = now;
@@ -464,10 +464,8 @@ export async function app(o) {
464
464
  // the shell asks and does not read the answer, since the news is already
465
465
  // on the page where the push landed.
466
466
  const shown = async (line) => {
467
- const dock = o.harbor.wards.get(DOCK);
468
- if (!dock)
469
- return;
470
- await dock.ask('ask', { being: NOTIFICATIONS, method: 'notify', args: line }).catch(() => { });
467
+ await (await lent(o.harbor, NOTIFICATIONS))?.notify(line).catch(() => { });
468
+ await o.harbor.wards.get(BOX_SEED)?.save();
471
469
  };
472
470
  // The device ward before anything is dialed: a harbor announces the wards
473
471
  // it hosts when it opens a socket, and a ward made after that is a pk the
@@ -15,5 +15,4 @@ export type Landed = {
15
15
  at?: string;
16
16
  ok: boolean;
17
17
  };
18
- export declare function landedTree(l: Landed): Node;
19
18
  export declare function landed(l: Landed): string;
@@ -68,7 +68,7 @@ export const ours = {
68
68
  ...nobody,
69
69
  link: (href, text) => `<a class="link" href="${escape(href)}">${escape(text)}</a>`,
70
70
  };
71
- export function landedTree(l) {
71
+ function landedTree(l) {
72
72
  const of = [
73
73
  { kind: 'image', src: MARK, alt: '' },
74
74
  { kind: 'text', text: l.ok ? 'That is done' : 'That did not finish', role: 'title' },
@@ -397,7 +397,7 @@ export function page(m) {
397
397
  const a = inGroup.find((x) => x.name === prefix + name);
398
398
  return a ? one(kept, prefix)(a) : '';
399
399
  },
400
- answer: (name, as) => (inGroup.some((x) => x.name === prefix + name) ? answer(prefix + name, as) : ''),
400
+ answer: (name, shown) => (inGroup.some((x) => x.name === prefix + name) ? answer(prefix + name, shown) : ''),
401
401
  link: linkedBy(kept), // a carried standing vouches by her own look and never her carrier's
402
402
  standing: () => '',
403
403
  standings: () => '',
@@ -10,7 +10,7 @@ const KEY = 'courier';
10
10
  async function courier(harbor) {
11
11
  const ward = harbor.wards.get(COURIER) ?? (await harbor.create(COURIER, 'me'));
12
12
  if (!ward.being(KEY)) {
13
- const out = (await ward.ask('boot', { key: KEY, class: 'Courier' }));
13
+ const out = (await ward.ask('boot', { key: KEY, class: 'beings/Courier' }));
14
14
  if (out.error)
15
15
  throw new Error(`no courier on this device: ${out.error}`);
16
16
  }