@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
package/README.md CHANGED
@@ -14,21 +14,23 @@ npx nervur serve --dir my-estate/droplet
14
14
  ```
15
15
 
16
16
  `nervur estate` writes an estate folder: one folder per harbor, the first a
17
- droplet's harbor directory with its routes under your domain, its unit and
18
- its Caddyfile, and a package file that depends on the dock and nothing
19
- else. Add your beings to `droplet/classes/`, and your placement to the
20
- routes and agents files.
21
-
22
- [`quo-dock.md`](quo-dock.md) is the truth of this package and the trunk of
23
- its documents; read it first. It knows nothing the spec of Quo does not say,
24
- and adds no word to harbor, ward or being. Beside it are the sides,
25
- [`quo-human.md`](quo-human.md), [`quo-mcp.md`](quo-mcp.md),
26
- [`quo-api.md`](quo-api.md) and [`quo-harbor.md`](quo-harbor.md), and the
27
- bells, [`quo-app-bells.md`](quo-app-bells.md);
28
- [`WORLDS.md`](WORLDS.md) is what home, presence and membership mean, and
29
- [`GLOSSARY.md`](GLOSSARY.md) is every word above the spec. Each stands once,
30
- at the root of the package, the way `nervur` and `@nervur-org/ui` carry
31
- theirs.
17
+ droplet's, with its stand script, its classes folder, its systemd unit and
18
+ a Caddyfile mapping the four routes under your domain, beside a Mac's
19
+ launchd agent, a package file that depends on the dock and nothing else,
20
+ and one document to fill in. Add your beings to `droplet/classes/` and your
21
+ placement to `droplet/stand.ts`. What the box does on its own, the routes
22
+ it answers under, the agents it runs and the lines it holds, is placed with
23
+ `nervur` and kept in the box's own cells, never in a file.
24
+
25
+ The documentation is at <https://nervur.org/docs>: the trunk of this
26
+ package, the human, model, api and harbor sides, the bells, what home,
27
+ presence and membership mean, and every word above the spec. This package
28
+ carries its code and this file, and nothing written for a reader lives
29
+ inside it, because a document and a release move at different speeds.
30
+
31
+ Quo itself is at <https://quo.systems>: the spec, the vectors a kit is
32
+ checked against, and the verifier that replays them. This package
33
+ implements it and never restates it.
32
34
 
33
35
  ## Versions
34
36
 
package/api/route.ts CHANGED
@@ -37,9 +37,8 @@ import { readText } from '../harbor/http.ts';
37
37
  import type { Hosted } from 'nervur/harbor';
38
38
  import type { Avatar } from '../beings/avatar.ts';
39
39
  import { invitationOf } from '../beings/link.ts';
40
- import { landed, POLICY } from '../human/door.ts';
41
- import { CSS } from '../human/style.ts';
42
- import type { Grants } from '../dock/grants.ts';
40
+ import { isIdentity } from '../beings/join.ts';
41
+ import type { Bearers as Grants } from '../box/grants.ts';
43
42
  import { isInvitation, isSilence, isWord, wordOf, type Invitation, type JsonObject } from 'nervur';
44
43
 
45
44
  const open = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET, POST, OPTIONS', 'access-control-allow-headers': 'content-type, authorization' };
@@ -58,10 +57,6 @@ export type Options = {
58
57
  resolve: (identity: string, ward: string) => Promise<{ avatar?: Avatar; error?: string }>;
59
58
  // Boot or find the avatar for this identity in that world, and knock.
60
59
  join: (identity: string, invitation: Invitation, ward: string) => Promise<{ error?: string }>;
61
- // Where the worlds' pages are, for the one arrival here that is a human:
62
- // a redirect from somewhere a world sent her. Without it she still gets a
63
- // page, with no way back offered on it.
64
- web?: string;
65
60
  };
66
61
 
67
62
  const word = (s: unknown) => (typeof s === 'string' && /^[\w.-]{1,40}$/.test(s) ? s : null);
@@ -69,28 +64,6 @@ const word = (s: unknown) => (typeof s === 'string' && /^[\w.-]{1,40}$/.test(s)
69
64
  export function apiRoute(harbor: Held, o: Options): Handler {
70
65
  return async (req, rest) => {
71
66
  const json = (status: number, body: unknown) => Response.json(body, { status, headers: open });
72
- // The one page this road serves, and it wears the one stylesheet every
73
- // screen wears. No cross-origin header on it: a page is for the browser
74
- // that was sent here and never for a script somewhere else. Its policy
75
- // is the allow page's, and for the same reason: a page that runs no
76
- // script of its own should be allowed none.
77
- const page = (status: number, body: string) =>
78
- new Response(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>nervur</title>\n<style>${CSS}</style>\n</head><body>${body}</body></html>`, {
79
- status,
80
- headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': POLICY, 'referrer-policy': 'no-referrer' },
81
- });
82
- // Where this world lives, as the daemon was told and not as the request
83
- // said: a Host header is the caller's to write, and a page that printed
84
- // one would print whatever a caller wanted a human to read. Falling back
85
- // to it is for a box configured with no web address at all, where the
86
- // request is the only thing that knows.
87
- const hostOf = (r: Request) => {
88
- try {
89
- return o.web === undefined ? (r.headers.get('host') ?? '') : new URL(o.web).host;
90
- } catch {
91
- return r.headers.get('host') ?? '';
92
- }
93
- };
94
67
  if (req.method === 'OPTIONS') return new Response(null, { status: 204, headers: open });
95
68
  if (req.method !== 'GET' && req.method !== 'POST') return json(405, { error: 'GET or POST' });
96
69
  const body = async (): Promise<JsonObject | null> => {
@@ -119,7 +92,7 @@ export function apiRoute(harbor: Held, o: Options): Handler {
119
92
  if (!world) return json(400, { error: 'that invitation is for a world that does not live here' });
120
93
  const [ward, hosted] = world;
121
94
  const identity = word(b.identity);
122
- if (identity === null || identity === hosted.record.user || identity === 'desk') return json(400, { error: 'an identity is one word, and not the user or the desk' });
95
+ if (!isIdentity(hosted, identity)) return json(400, { error: 'an identity is one word, and not a being of the ward' });
123
96
  // Nobody at a page vouches for a program, so a trade is a knock that
124
97
  // spends: an identity already in refreshes, or is removed at the user
125
98
  // being and invited again, and never re-enters on a spent invitation.
@@ -135,48 +108,7 @@ export function apiRoute(harbor: Held, o: Options): Handler {
135
108
  const [, named = '', ask = '', ...more] = rest.split('/');
136
109
  const ward = named === '' ? harbor.here() : named;
137
110
 
138
- // The delivery: a vendor's webhook to an envoy, `/api/<ward>/<being>/deliver`.
139
- // Not a stranger's ask and not an occupant's: the owner asking her
140
- // `deliver` with the raw bytes and the headers, and nothing else. The
141
- // body is read as a string and never parsed here, since the vendor
142
- // signed the bytes and a reparsed body fails her check; no bearer is
143
- // read, since the rung is not the caller's to prove. She answers `{ ok }`
144
- // or her error object, and the route carries it.
145
- // A GET on the same path is the same ask with an empty body: where an
146
- // OAuth redirect lands, the code in the query. The query crosses inside
147
- // the headers under `:query`, a name no header on the wire can have, so
148
- // her one ask in stays one.
149
- if (more.length === 1 && more[0] === 'deliver') {
150
- if (req.method !== 'POST' && req.method !== 'GET') return json(405, { error: 'POST, or GET for a redirect' });
151
- const hosted = harbor.wards.get(ward);
152
- if (!hosted || !/^[\w-]+$/.test(ask)) return json(404, { error: 'no such world' });
153
- const raw = req.method === 'GET' ? '' : await readText(req);
154
- if (raw === undefined) return json(413, { error: 'too big' });
155
- const headers: Record<string, string> = {};
156
- req.headers.forEach((v, k) => (headers[k] = v));
157
- const query = new URL(req.url).search.slice(1);
158
- if (query) headers[':query'] = query;
159
- const out = await hosted.ask('ask', { being: ask, method: 'deliver', args: { body: raw, headers } });
160
- await hosted.save();
161
- // A GET here is a browser and a browser is a human: a vendor a world
162
- // sent her to has opened this URL in her tab, so what she is looking
163
- // at is whatever the envoy answered a machine. She gets a page
164
- // instead, and only she does; a vendor asking for JSON is answered in
165
- // JSON as it always was, and so is a GET that says nothing about what
166
- // it wants.
167
- const wants = req.method === 'GET' && (req.headers.get('accept') ?? '').includes('text/html');
168
- if (wants) {
169
- const ok = out !== null && typeof out === 'object' && !Array.isArray(out) && typeof (out as { error?: unknown }).error !== 'string';
170
- return page(ok ? 200 : 400, landed({ world: ward, host: hostOf(req), ...(o.web ? { at: `${o.web.replace(/\/$/, '')}/${encodeURIComponent(ward)}` } : {}), ok }));
171
- }
172
- if (isSilence(out) || out === null || typeof out !== 'object' || Array.isArray(out)) return json(503, { error: 'silence' });
173
- const said = (out as { error?: unknown }).error;
174
- if (said === 'no such being' || said === 'unknown ask') return json(404, { error: 'no such envoy' });
175
- if (said === 'silence' || said === 'threw' || said === 'late') return json(503, { error: 'silence' });
176
- if (typeof said === 'string') return json(400, out);
177
- return json(200, out);
178
- }
179
- if (!/^[\w.-]+$/.test(ward) || more.length > 0 || (ask !== '' && !/^[\w-]+$/.test(ask))) return json(404, { error: 'GET /api for the device’s ward, GET /api/<ward> for the describe, GET or POST /api/<ward>/<ask> for one ask, POST /api/<ward>/<being>/deliver for a delivery' });
111
+ if (!/^[\w.-]+$/.test(ward) || more.length > 0 || (ask !== '' && !/^[\w-]+$/.test(ask))) return json(404, { error: 'GET /api for the device’s ward, GET /api/<ward> for the describe, GET or POST /api/<ward>/<ask> for one ask' });
180
112
  const hosted = harbor.wards.get(ward);
181
113
  if (!hosted) return json(404, { error: 'no such world' });
182
114
  let args: JsonObject;
package/beings/avatar.ts CHANGED
@@ -7,9 +7,9 @@
7
7
  // calls the four methods below: one of the two ways in, `enter` behind a
8
8
  // route's proof or `join` with an invitation, then `tools` and `call`.
9
9
  // Nothing of a side is in her cells.
10
- import { isSilence, isWord, wordOf } from 'nervur';
11
- import type { Answer, Blueprint, Invitation, JsonObject, OccupantRecord, Wanted } from 'nervur';
12
- import { Carrier } from './carry.ts';
10
+ import { Being, isSilence, isWord, wordOf } from 'nervur';
11
+ import type { Answer, Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Wanted } from 'nervur';
12
+ import { carry } from './carry.ts';
13
13
 
14
14
  // Her one standing, and the id under which the user being pushes to her.
15
15
  // Two ids for one far being, because standings and occupants share one
@@ -17,15 +17,19 @@ import { Carrier } from './carry.ts';
17
17
  export const USER = 'user';
18
18
  export const PUSHER = 'user:push';
19
19
 
20
- // A carrier who carries for nobody, until a subclass says who: the presence
21
- // being carries the box's faculties to her user being, and a bare avatar
22
- // holds one standing and shows nothing of it.
23
- export class Avatar extends Carrier {
20
+ // She carries for nobody, until a subclass says who: the presence being
21
+ // carries the box's faculties to her user being, and a bare avatar holds
22
+ // one standing and shows nothing of it.
23
+ export class Avatar extends Being {
24
24
  static override cells = { inbox: [] as JsonObject[] };
25
25
  static override asks = {
26
26
  notify: { description: 'a push from the user being', input: { type: 'object' }, for: (occ: OccupantRecord | undefined) => occ?.id === PUSHER },
27
27
  };
28
28
 
29
+ override answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
30
+ return carry(this, asker, method, args, (a, m, g) => super.answer(a, m, g));
31
+ }
32
+
29
33
  // Her sides' ears. One identity is one avatar for life, and two sessions
30
34
  // of that identity are two sides on her at once, so a push goes to every
31
35
  // ear present; each side adds its own on open and removes it on close.
package/beings/carry.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // A carrier: a being who shows the asks of the standings she holds as asks
3
- // of her own, and forwards. Whoever she carries for sees, in one describe,
2
+ // Carrying: a being shows the asks of the standings she holds as asks of
3
+ // her own, and forwards. Whoever she carries for sees, in one describe,
4
4
  // what she can be asked and what every being she holds can be asked, each
5
5
  // under that standing's name; asking one is an ask on that standing, in her
6
6
  // name, so the far being sees her and never who is behind her. A user being
7
- // carries acme for the human's devices; a house being could carry its
8
- // rooms; nothing here knows what she is.
7
+ // carries her org for the human's devices; a presence being carries her
8
+ // device's faculties to her world; the Dock carries the box's faculties to
9
+ // a screen. Nothing here knows what she is.
10
+ //
11
+ // It is composed and never extended: carrying is something a being does,
12
+ // not what she is, so a being on any rung of the kit's ladder carries by
13
+ // answering through `carry` and saying her rules as statics of her class.
14
+ // The four rules have a default each, and a class says only the ones she
15
+ // means to change.
9
16
  //
10
17
  // A carried ask is `<id>-<name>`, and the mapping back is kept in her cells
11
18
  // and never parsed from the name, so an id with a dash in it is no trouble.
@@ -18,13 +25,49 @@
18
25
  // `look` once per digest, and keeps both in her cells. So a describe of
19
26
  // hers may cost one ask per stale standing, which is the price of a page
20
27
  // that is never more than one ask behind.
21
- import { Being, isSilence, isWord, wordOf } from 'nervur';
22
- import type { Asker, Blueprint, JsonObject, OccupantRecord, Reply } from 'nervur';
28
+ import { isSilence, isWord, wordOf } from 'nervur';
29
+ import type { Asker, Being, Blueprint, JsonObject, OccupantRecord, Reply } from 'nervur';
23
30
  import { sanitise, type Look } from './look.ts';
24
31
 
25
32
  type Carried = Record<string, { id: string; ask: string }>;
26
33
  type Looks = Record<string, { digest: string | null; look: Look }>;
27
34
 
35
+ // The rules a carrying class declares as statics.
36
+ export type Carrying = {
37
+ // Who sees what she carries. Nobody, until the class says who.
38
+ carries(occupant: OccupantRecord | undefined, asker: Asker): boolean;
39
+ // Standings that are plumbing: their own asks are never carried, and
40
+ // what they carry is. A way back to a device is one, by the dock's own
41
+ // convention, so a bare avatar's inbox never shows on her and a presence
42
+ // being's faculties do, under `to:<id>-<name>`, which is how home carries
43
+ // her phone. A class may name more.
44
+ hidden(id: string): boolean;
45
+ // Standings she never reads for carrying at all: a presence being's
46
+ // standing at her own user being is one, since carrying it would carry
47
+ // the user being's carrying of her, and round.
48
+ skips(id: string): boolean;
49
+ // Which of the standings she carries this asker may see. Every one,
50
+ // until the class keeps some for fewer: a user being shows her own door
51
+ // to a device that may mint and to nobody else.
52
+ shows(id: string, occupant: OccupantRecord | undefined, asker: Asker): boolean;
53
+ };
54
+
55
+ export const CARRIES: Carrying = {
56
+ carries: () => false,
57
+ hidden: (id) => id.startsWith('to:'),
58
+ skips: () => false,
59
+ shows: () => true,
60
+ };
61
+
62
+ // Her rules, read off her class; one she left unsaid is the default.
63
+ function rulesOf(self: Being): Carrying {
64
+ const C = self.constructor as Partial<Carrying>;
65
+ return { carries: C.carries ?? CARRIES.carries, hidden: C.hidden ?? CARRIES.hidden, skips: C.skips ?? CARRIES.skips, shows: C.shows ?? CARRIES.shows };
66
+ }
67
+
68
+ const carriedOf = (self: Being): Carried => ((self.cells.carried as Carried | undefined) ??= {});
69
+ const looksOf = (self: Being): Looks => ((self.cells.looks as Looks | undefined) ??= {});
70
+
28
71
  // The names a far being carries, read off her notes: every ask her
29
72
  // `standings` groups name, and each group's page. Empty when she carries
30
73
  // nothing, which a bare avatar does not.
@@ -40,120 +83,90 @@ function carriedBy(bp: Blueprint): Set<string> {
40
83
  return out;
41
84
  }
42
85
 
43
- export class Carrier extends Being {
44
- // Who sees what she carries. Nobody, until a subclass says who.
45
- static carries(_occupant: OccupantRecord | undefined, _asker: Asker): boolean {
46
- return false;
47
- }
48
- // Standings that are plumbing: their own asks are never carried, and
49
- // what they carry is. A way back to a device is one, by the dock's own
50
- // convention, so a bare avatar's inbox never shows on her and a presence
51
- // being's faculties do, under `to:<id>-<name>`, which is how home carries
52
- // her phone. A subclass may name more.
53
- static hidden(id: string): boolean {
54
- return id.startsWith('to:');
55
- }
56
- // Standings she never reads for carrying at all: a presence being's
57
- // standing at her own user being is one, since carrying it would carry
58
- // the user being's carrying of her, and round. None, until a subclass
59
- // says which.
60
- static skips(_id: string): boolean {
61
- return false;
62
- }
63
- // Which of the standings she carries this asker may see. Every one,
64
- // until a subclass keeps some for fewer: a user being shows her own door
65
- // to a device that may mint and to nobody else.
66
- static shows(_id: string, _occupant: OccupantRecord | undefined, _asker: Asker): boolean {
67
- return true;
68
- }
69
-
70
- private get carried(): Carried {
71
- return ((this.cells.carried as Carried | undefined) ??= {});
72
- }
73
- private get looks(): Looks {
74
- return ((this.cells.looks as Looks | undefined) ??= {});
75
- }
76
-
77
- // Her describe with her standings carried into it, for an asker who may
78
- // see them. Every other asker gets her describe alone.
79
- async blueprint(asker: Asker): Promise<Blueprint> {
80
- const C = this.constructor as typeof Carrier;
81
- const bp = this.describe(asker);
82
- if (!C.carries(this.occupant(asker), asker)) return bp;
83
- const own = new Set(bp.asks.map((a) => a.name));
84
- const carried: Carried = {};
85
- const groups: Record<string, { asks: string[]; look?: Look }> = {};
86
- const occ = this.occupant(asker);
87
- // By id, and never in the order she happened to take them. Her cells keep
88
- // standings in the order they were taken, which is the order a lend
89
- // answered, a device came back or a relation was dropped and taken again:
90
- // history, and no fact about her. Carried into her blueprint in that
91
- // order, her shape would come out differently on two runs that made her
92
- // the same way, and her digest with it. The digest is how everyone
93
- // holding a standing on her learns she changed, so a shape that follows
94
- // history says she changed when nothing about her did, and a watcher
95
- // refreshes or walks away over the order two lends came back in.
96
- for (const [id, rec] of [...Object.entries(this.cells.standings)].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
97
- if (C.skips(id)) continue;
98
- const st = this.standings[id];
99
- if (!st) continue;
100
- if (!rec.blueprint || rec.seen !== rec.digest) await st.ask(); // her ward writes the record
101
- if (!rec.blueprint) continue;
102
- // plumbing carries what it carries and nothing of its own: the names
103
- // the far being's notes group under her standings, or none
104
- const through = C.hidden(id) ? carriedBy(rec.blueprint) : null;
105
- if (through && through.size === 0) continue;
106
- // The mapping is written for every standing she carries, whoever is
107
- // asking: it is one map in her cells, and the last describe must not
108
- // decide what the next asker may call. Who sees a standing is decided
109
- // here and again at answer, by `shows`.
110
- const shown = C.shows(id, occ, asker);
111
- // her look is kept here once per digest and never carried as an ask;
112
- // her page follows every call, so it is carried as one and named in
113
- // the notes, where every side finds it and none takes it for a form
114
- const names: string[] = [];
115
- const group: { asks: string[]; look?: Look; page?: string } = { asks: names };
116
- for (const a of rec.blueprint.asks) {
117
- if (a.name === 'look') continue;
118
- if (through && !through.has(a.name)) continue;
119
- const name = `${id}-${a.name}`;
120
- if (own.has(name)) continue;
121
- carried[name] = { id, ask: a.name };
122
- if (!shown) continue;
123
- bp.asks.push({ ...a, name });
124
- if (a.name === 'page') group.page = name;
125
- else names.push(name);
126
- }
86
+ // Her describe with her standings carried into it, for an asker who may
87
+ // see them. Every other asker gets her describe alone.
88
+ export async function blueprint(self: Being, asker: Asker): Promise<Blueprint> {
89
+ const rules = rulesOf(self);
90
+ const bp = self.describe(asker);
91
+ const occ = self.occupant(asker);
92
+ if (!rules.carries(occ, asker)) return bp;
93
+ const own = new Set(bp.asks.map((a) => a.name));
94
+ const carried: Carried = {};
95
+ const groups: Record<string, { asks: string[]; look?: Look }> = {};
96
+ const looks = looksOf(self);
97
+ // By id, and never in the order she happened to take them. Her cells keep
98
+ // standings in the order they were taken, which is the order a lend
99
+ // answered, a device came back or a relation was dropped and taken again:
100
+ // history, and no fact about her. Carried into her blueprint in that
101
+ // order, her shape would come out differently on two runs that made her
102
+ // the same way, and her digest with it. The digest is how everyone
103
+ // holding a standing on her learns she changed, so a shape that follows
104
+ // history says she changed when nothing about her did, and a watcher
105
+ // refreshes or walks away over the order two lends came back in.
106
+ for (const [id, rec] of [...Object.entries(self.cells.standings)].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
107
+ if (rules.skips(id)) continue;
108
+ const st = self.standings[id];
109
+ if (!st) continue;
110
+ if (!rec.blueprint || rec.seen !== rec.digest) await st.ask(); // her ward writes the record
111
+ if (!rec.blueprint) continue;
112
+ // plumbing carries what it carries and nothing of its own: the names
113
+ // the far being's notes group under her standings, or none
114
+ const through = rules.hidden(id) ? carriedBy(rec.blueprint) : null;
115
+ if (through && through.size === 0) continue;
116
+ // The mapping is written for every standing she carries, whoever is
117
+ // asking: it is one map in her cells, and the last describe must not
118
+ // decide what the next asker may call. Who sees a standing is decided
119
+ // here and again at answer, by `shows`.
120
+ const shown = rules.shows(id, occ, asker);
121
+ // her look is kept here once per digest and never carried as an ask;
122
+ // her page follows every call, so it is carried as one and named in
123
+ // the notes, where every side finds it and none takes it for a form
124
+ const names: string[] = [];
125
+ const group: { asks: string[]; look?: Look; page?: string } = { asks: names };
126
+ for (const a of rec.blueprint.asks) {
127
+ if (a.name === 'look') continue;
128
+ if (through && !through.has(a.name)) continue;
129
+ const name = `${id}-${a.name}`;
130
+ if (own.has(name)) continue;
131
+ carried[name] = { id, ask: a.name };
127
132
  if (!shown) continue;
128
- if (rec.blueprint.asks.some((a) => a.name === 'look')) {
129
- const kept = this.looks[id];
130
- if (!kept || kept.digest !== rec.digest) {
131
- const l = await st.ask('look');
132
- this.looks[id] = { digest: rec.digest, look: isSilence(l) || isWord(l) ? {} : sanitise(l) };
133
- }
134
- group.look = this.looks[id].look;
133
+ bp.asks.push({ ...a, name });
134
+ if (a.name === 'page') group.page = name;
135
+ else names.push(name);
136
+ }
137
+ if (!shown) continue;
138
+ if (rec.blueprint.asks.some((a) => a.name === 'look')) {
139
+ const kept = looks[id];
140
+ if (!kept || kept.digest !== rec.digest) {
141
+ const l = await st.ask('look');
142
+ looks[id] = { digest: rec.digest, look: isSilence(l) || isWord(l) ? {} : sanitise(l) };
135
143
  }
136
- groups[id] = group;
144
+ group.look = looks[id].look;
137
145
  }
138
- for (const id of Object.keys(this.looks)) if (!(id in groups)) delete this.looks[id];
139
- this.cells.carried = carried;
140
- const notes = bp.notes !== null && typeof bp.notes === 'object' && !Array.isArray(bp.notes) ? bp.notes : {};
141
- return { asks: bp.asks, notes: { ...notes, standings: groups as unknown as JsonObject } };
146
+ groups[id] = group;
142
147
  }
148
+ for (const id of Object.keys(looks)) if (!(id in groups)) delete looks[id];
149
+ self.cells.carried = carried;
150
+ const notes = bp.notes !== null && typeof bp.notes === 'object' && !Array.isArray(bp.notes) ? bp.notes : {};
151
+ return { asks: bp.asks, notes: { ...notes, standings: groups as unknown as JsonObject } };
152
+ }
143
153
 
144
- override async answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
145
- if (method === undefined) return this.blueprint(asker);
146
- const C = this.constructor as typeof Carrier;
147
- const to = Object.hasOwn(this.carried, method) ? this.carried[method] : undefined;
148
- const occ = this.occupant(asker);
149
- if (to && C.carries(occ, asker) && C.shows(to.id, occ, asker)) {
150
- const st = this.standings[to.id];
151
- if (!st) return { error: 'unknown ask' };
152
- const out = await st.ask(to.ask, args);
153
- if (isSilence(out)) return out;
154
- if (isWord(out)) return { error: wordOf(out) };
155
- return out;
156
- }
157
- return super.answer(asker, method, args);
154
+ // Her answer, carrying: the empty ask is her carried blueprint, a carried
155
+ // name is an ask on that standing in her name, and everything else is her
156
+ // own, which the class hands in as `own`.
157
+ export async function carry(self: Being, asker: Asker, method: string | undefined, args: JsonObject, own: (asker: Asker, method: string, args: JsonObject) => Promise<Reply>): Promise<Reply> {
158
+ if (method === undefined) return blueprint(self, asker);
159
+ const rules = rulesOf(self);
160
+ const carried = carriedOf(self);
161
+ const to = Object.hasOwn(carried, method) ? carried[method] : undefined;
162
+ const occ = self.occupant(asker);
163
+ if (to && rules.carries(occ, asker) && rules.shows(to.id, occ, asker)) {
164
+ const st = self.standings[to.id];
165
+ if (!st) return { error: 'unknown ask' };
166
+ const out = await st.ask(to.ask, args);
167
+ if (isSilence(out)) return out;
168
+ if (isWord(out)) return { error: wordOf(out) };
169
+ return out;
158
170
  }
171
+ return own(asker, method, args);
159
172
  }
package/beings/clock.ts CHANGED
@@ -31,7 +31,11 @@
31
31
  // process the way every cell does, and is gone when it is dropped.
32
32
  import { Being, isSilence, isWord, wordOf, OWNER } from 'nervur';
33
33
  import type { Asker, Invitation, Json, JsonObject, OccupantRecord, Stance } from 'nervur';
34
- import { TIMER, join } from '../dock/index.ts';
34
+ import { TIMER as TIMING } from '../box/timer.ts';
35
+ import { join } from '../box/join.ts';
36
+
37
+ // Her standing at the box's timer, under an id of her own.
38
+ const TIMER = 'timer';
35
39
 
36
40
  // The root and nobody else. `OWNER` is one of the ward's own words, refused at
37
41
  // every mint, so no occupant can ever wear it: an ask gated this way is the
@@ -311,7 +315,7 @@ export class Clock extends Being {
311
315
  readonly joined: Promise<string | null>;
312
316
  constructor(stance: Stance) {
313
317
  super(stance);
314
- this.joined = join(this, TIMER, { method: 'tick' });
318
+ this.joined = join(this, TIMING, TIMER, { method: 'tick' });
315
319
  }
316
320
 
317
321
  static override asks = {
@@ -377,15 +381,15 @@ export class Clock extends Being {
377
381
  // it keeps them, for the same reason the base does.
378
382
  protected async told(): Promise<void> {
379
383
  if (!Object.hasOwn(this.cells.standings, TIMER)) return;
380
- const timer = this.standings[TIMER];
381
- const said = await timer?.ask('now', {});
382
- const now = said !== null && typeof said === 'object' ? (said as { now?: unknown }).now : undefined;
384
+ const hers = this.standings[TIMER];
385
+ const heard = await hers?.ask('now', {});
386
+ const now = heard !== null && typeof heard === 'object' ? (heard as { now?: unknown }).now : undefined;
383
387
  if (typeof now !== 'number' || !Number.isFinite(now)) return;
384
388
  // A schedule due at this very moment is a round now, not a round at the
385
389
  // next moment after it: what she was handed is a minute already begun,
386
390
  // and the schedule kept inside it names it.
387
391
  const at = this.schedules().some((s) => this.#ready(s, now)) ? now : this.#soonest(floor(now));
388
- if (at !== null) await timer?.ask('due', { at });
392
+ if (at !== null) await hers?.ask('due', { at });
389
393
  }
390
394
 
391
395
  // One list, her own cell and nothing else.
@@ -536,7 +540,7 @@ export const CLOCK = 'clock';
536
540
  // because it is the same act: the owner creates and places, and the work is
537
541
  // hers from then on.
538
542
  export async function setupClock(hosted: Owner, wakes: string[] = []): Promise<void> {
539
- const out = (await hosted.ask('boot', { key: CLOCK, class: 'Clock' })) as { error?: string };
543
+ const out = (await hosted.ask('boot', { key: CLOCK, class: 'beings/Clock' })) as { error?: string };
540
544
  if (out.error) throw new Error(`clock: ${out.error}`);
541
545
  for (const being of wakes) await placeClock(hosted, being);
542
546
  }
package/beings/desk.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // way in turns a proof of one kind into who it is for: which user, which
9
9
  // client identity. The ways in are beings, and the desk reaches each of
10
10
  // them as she reaches anything, at a standing under `way:<kind>`: the
11
- // world's own, placed by the root, and the box's, lent by the dock ward and
11
+ // world's own, placed by the root, and the box's, lent by the box ward and
12
12
  // taken at her birth, so a box with no local side offers no such kind.
13
13
  //
14
14
  // A kind whose proof the world has to hand out first answers `send` beside
@@ -18,8 +18,8 @@
18
18
  // place, so a side that renders her renders every way into this world.
19
19
  import { Being, isSilence, isWord, wordOf } from 'nervur';
20
20
  import type { Asker, Blueprint, JsonObject, Reply, Stance } from 'nervur';
21
- import { join } from '../dock/faculty.ts';
22
- import { LOCAL } from '../dock/local.ts';
21
+ import { join } from '../box/join.ts';
22
+ import { LOCAL } from '../box/local.ts';
23
23
  import { WAY, kindOf, verified } from './way.ts';
24
24
 
25
25
  export class Desk extends Being {
@@ -34,7 +34,7 @@ export class Desk extends Being {
34
34
  // nothing under that id and the kind is not offered.
35
35
  constructor(stance: Stance) {
36
36
  super(stance);
37
- void join(this, LOCAL, undefined, WAY(LOCAL));
37
+ void join(this, LOCAL, WAY('local'));
38
38
  }
39
39
 
40
40
  // The ids of the ways in she holds, in the order they were placed.
@@ -114,8 +114,9 @@ export class Desk extends Being {
114
114
  // key. A way in that does not say `make` reaches an existing household
115
115
  // and nothing else, which is what the local kind on a device does.
116
116
  if (!this.standings[id] && (who.make === true || typeof who.make === 'string')) {
117
- if (!/^[\w.-]{1,80}$/.test(who.user) || who.user === 'desk') return { error: 'a user is a word, and not desk' };
118
- if ((await this.boot(typeof who.make === 'string' ? who.make : 'User', who.user, id)) === null) return { error: 'no such user' };
117
+ // a key the ward already holds, her own included, is refused by the boot
118
+ if (!/^[\w.-]{1,80}$/.test(who.user)) return { error: 'a user is a word' };
119
+ if ((await this.boot(typeof who.make === 'string' ? who.make : 'beings/User', who.user, id)) === null) return { error: 'no such user' };
119
120
  await this.standings[id]!.ask('hello', { name: who.user });
120
121
  }
121
122
  const user = this.standings[id];