@nervur-org/dock 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/GETTING_STARTED.md +128 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +37 -0
  5. package/api/quo-api.md +196 -0
  6. package/api/route.ts +227 -0
  7. package/beings/GLOSSARY.md +118 -0
  8. package/beings/WORLDS.md +567 -0
  9. package/beings/avatar.ts +101 -0
  10. package/beings/carry.ts +159 -0
  11. package/beings/clock.ts +552 -0
  12. package/beings/courier.ts +107 -0
  13. package/beings/desk.ts +128 -0
  14. package/beings/doorbell.ts +129 -0
  15. package/beings/envoy.ts +218 -0
  16. package/beings/index.ts +16 -0
  17. package/beings/join.ts +51 -0
  18. package/beings/link.ts +104 -0
  19. package/beings/look.ts +139 -0
  20. package/beings/porter.ts +43 -0
  21. package/beings/presence.ts +39 -0
  22. package/beings/quo-app-bells.md +280 -0
  23. package/beings/quo-dock.md +1942 -0
  24. package/beings/setup.ts +45 -0
  25. package/beings/side.ts +60 -0
  26. package/beings/user.ts +459 -0
  27. package/beings/way.ts +65 -0
  28. package/cli/bundles.ts +20 -0
  29. package/cli/client.ts +73 -0
  30. package/cli/daemon.ts +581 -0
  31. package/cli/estate/Caddyfile +33 -0
  32. package/cli/estate/quo.service +35 -0
  33. package/cli/estate/systems.quo.plist +44 -0
  34. package/cli/estate.ts +48 -0
  35. package/cli/folder.ts +27 -0
  36. package/cli/oauth.ts +32 -0
  37. package/cli/pilot.ts +38 -0
  38. package/cli/quo-dock.md +1942 -0
  39. package/cli/quo.ts +203 -0
  40. package/cli/stand.ts +160 -0
  41. package/dist/api/route.d.ts +21 -0
  42. package/dist/api/route.js +193 -0
  43. package/dist/beings/avatar.d.ts +32 -0
  44. package/dist/beings/avatar.js +100 -0
  45. package/dist/beings/carry.d.ts +12 -0
  46. package/dist/beings/carry.js +174 -0
  47. package/dist/beings/clock.d.ts +161 -0
  48. package/dist/beings/clock.js +528 -0
  49. package/dist/beings/courier.d.ts +50 -0
  50. package/dist/beings/courier.js +96 -0
  51. package/dist/beings/desk.d.ts +53 -0
  52. package/dist/beings/desk.js +135 -0
  53. package/dist/beings/doorbell.d.ts +78 -0
  54. package/dist/beings/doorbell.js +133 -0
  55. package/dist/beings/envoy.d.ts +54 -0
  56. package/dist/beings/envoy.js +211 -0
  57. package/dist/beings/index.d.ts +14 -0
  58. package/dist/beings/index.js +16 -0
  59. package/dist/beings/join.d.ts +15 -0
  60. package/dist/beings/join.js +42 -0
  61. package/dist/beings/link.d.ts +14 -0
  62. package/dist/beings/link.js +91 -0
  63. package/dist/beings/look.d.ts +30 -0
  64. package/dist/beings/look.js +104 -0
  65. package/dist/beings/porter.d.ts +14 -0
  66. package/dist/beings/porter.js +41 -0
  67. package/dist/beings/presence.d.ts +8 -0
  68. package/dist/beings/presence.js +21 -0
  69. package/dist/beings/setup.d.ts +3 -0
  70. package/dist/beings/setup.js +39 -0
  71. package/dist/beings/side.d.ts +32 -0
  72. package/dist/beings/side.js +37 -0
  73. package/dist/beings/user.d.ts +315 -0
  74. package/dist/beings/user.js +462 -0
  75. package/dist/beings/way.d.ts +19 -0
  76. package/dist/beings/way.js +28 -0
  77. package/dist/cli/bundles.d.ts +5 -0
  78. package/dist/cli/bundles.js +17 -0
  79. package/dist/cli/client.d.ts +15 -0
  80. package/dist/cli/client.js +58 -0
  81. package/dist/cli/daemon.d.ts +50 -0
  82. package/dist/cli/daemon.js +584 -0
  83. package/dist/cli/estate/Caddyfile +33 -0
  84. package/dist/cli/estate/quo.service +35 -0
  85. package/dist/cli/estate/systems.quo.plist +44 -0
  86. package/dist/cli/estate.d.ts +5 -0
  87. package/dist/cli/estate.js +50 -0
  88. package/dist/cli/folder.d.ts +2 -0
  89. package/dist/cli/folder.js +25 -0
  90. package/dist/cli/oauth.d.ts +2 -0
  91. package/dist/cli/oauth.js +32 -0
  92. package/dist/cli/pilot.d.ts +5 -0
  93. package/dist/cli/pilot.js +22 -0
  94. package/dist/cli/quo.d.ts +2 -0
  95. package/dist/cli/quo.js +201 -0
  96. package/dist/cli/stand.d.ts +37 -0
  97. package/dist/cli/stand.js +110 -0
  98. package/dist/dock/agents.d.ts +102 -0
  99. package/dist/dock/agents.js +105 -0
  100. package/dist/dock/apns.d.ts +27 -0
  101. package/dist/dock/apns.js +98 -0
  102. package/dist/dock/console.d.ts +6 -0
  103. package/dist/dock/console.js +20 -0
  104. package/dist/dock/dialers.d.ts +56 -0
  105. package/dist/dock/dialers.js +75 -0
  106. package/dist/dock/faculty.d.ts +24 -0
  107. package/dist/dock/faculty.js +142 -0
  108. package/dist/dock/fcm.d.ts +13 -0
  109. package/dist/dock/fcm.js +77 -0
  110. package/dist/dock/grants.d.ts +41 -0
  111. package/dist/dock/grants.js +99 -0
  112. package/dist/dock/http2.d.ts +4 -0
  113. package/dist/dock/http2.js +68 -0
  114. package/dist/dock/index.d.ts +41 -0
  115. package/dist/dock/index.js +113 -0
  116. package/dist/dock/local.d.ts +13 -0
  117. package/dist/dock/local.js +44 -0
  118. package/dist/dock/location.d.ts +19 -0
  119. package/dist/dock/location.js +11 -0
  120. package/dist/dock/notifications.d.ts +38 -0
  121. package/dist/dock/notifications.js +60 -0
  122. package/dist/dock/push.d.ts +79 -0
  123. package/dist/dock/push.js +132 -0
  124. package/dist/dock/service.d.ts +4 -0
  125. package/dist/dock/service.js +107 -0
  126. package/dist/dock/timer.d.ts +52 -0
  127. package/dist/dock/timer.js +149 -0
  128. package/dist/dock/webpush.d.ts +12 -0
  129. package/dist/dock/webpush.js +144 -0
  130. package/dist/harbor/browser.d.ts +15 -0
  131. package/dist/harbor/browser.js +82 -0
  132. package/dist/harbor/capacitor.d.ts +28 -0
  133. package/dist/harbor/capacitor.js +218 -0
  134. package/dist/harbor/disk.d.ts +27 -0
  135. package/dist/harbor/disk.js +249 -0
  136. package/dist/harbor/edge/alarm.d.ts +10 -0
  137. package/dist/harbor/edge/alarm.js +38 -0
  138. package/dist/harbor/edge/edge.d.ts +54 -0
  139. package/dist/harbor/edge/edge.js +370 -0
  140. package/dist/harbor/edge/exercise.d.ts +7 -0
  141. package/dist/harbor/edge/exercise.js +232 -0
  142. package/dist/harbor/edge/given.d.ts +26 -0
  143. package/dist/harbor/edge/given.js +42 -0
  144. package/dist/harbor/edge/platform.d.ts +39 -0
  145. package/dist/harbor/edge/storage.d.ts +30 -0
  146. package/dist/harbor/edge/storage.js +180 -0
  147. package/dist/harbor/edge/worker.d.ts +17 -0
  148. package/dist/harbor/edge/worker.js +67 -0
  149. package/dist/harbor/files.d.ts +15 -0
  150. package/dist/harbor/files.js +220 -0
  151. package/dist/harbor/http.d.ts +19 -0
  152. package/dist/harbor/http.js +177 -0
  153. package/dist/harbor/idb.d.ts +15 -0
  154. package/dist/harbor/idb.js +0 -0
  155. package/dist/harbor/quo.d.ts +10 -0
  156. package/dist/harbor/quo.js +42 -0
  157. package/dist/harbor/resolve.d.ts +16 -0
  158. package/dist/harbor/resolve.js +17 -0
  159. package/dist/harbor/seal.d.ts +3 -0
  160. package/dist/harbor/seal.js +25 -0
  161. package/dist/harbor/sealed.d.ts +35 -0
  162. package/dist/harbor/sealed.js +162 -0
  163. package/dist/harbor/tauri.d.ts +16 -0
  164. package/dist/harbor/tauri.js +59 -0
  165. package/dist/harbor/ward.d.ts +9 -0
  166. package/dist/harbor/ward.js +15 -0
  167. package/dist/human/app.d.ts +32 -0
  168. package/dist/human/app.js +527 -0
  169. package/dist/human/apps.d.ts +10 -0
  170. package/dist/human/apps.js +29 -0
  171. package/dist/human/dom.d.ts +2 -0
  172. package/dist/human/dom.js +15 -0
  173. package/dist/human/door.d.ts +19 -0
  174. package/dist/human/door.js +92 -0
  175. package/dist/human/guest.d.ts +3 -0
  176. package/dist/human/guest.js +33 -0
  177. package/dist/human/html.d.ts +55 -0
  178. package/dist/human/html.js +451 -0
  179. package/dist/human/local.d.ts +7 -0
  180. package/dist/human/local.js +10 -0
  181. package/dist/human/mark.d.ts +8 -0
  182. package/dist/human/mark.js +47 -0
  183. package/dist/human/move.d.ts +17 -0
  184. package/dist/human/move.js +90 -0
  185. package/dist/human/screen.d.ts +19 -0
  186. package/dist/human/screen.js +119 -0
  187. package/dist/human/style.d.ts +2 -0
  188. package/dist/human/style.js +60 -0
  189. package/dist/human/tab.bundle.txt +4775 -0
  190. package/dist/human/tab.d.ts +15 -0
  191. package/dist/human/tab.js +409 -0
  192. package/dist/human/tree.d.ts +50 -0
  193. package/dist/human/tree.js +157 -0
  194. package/dist/human/wake.d.ts +34 -0
  195. package/dist/human/wake.js +179 -0
  196. package/dist/human/web.d.ts +33 -0
  197. package/dist/human/web.js +184 -0
  198. package/dist/human/worker.bundle.txt +3868 -0
  199. package/dist/human/worker.d.ts +1 -0
  200. package/dist/human/worker.js +81 -0
  201. package/dist/human/worlds.d.ts +24 -0
  202. package/dist/human/worlds.js +99 -0
  203. package/dist/mcp/agent.d.ts +10 -0
  204. package/dist/mcp/agent.js +78 -0
  205. package/dist/mcp/allow.d.ts +20 -0
  206. package/dist/mcp/allow.js +88 -0
  207. package/dist/mcp/http.d.ts +27 -0
  208. package/dist/mcp/http.js +92 -0
  209. package/dist/mcp/oauth.d.ts +91 -0
  210. package/dist/mcp/oauth.js +241 -0
  211. package/dist/mcp/route.d.ts +15 -0
  212. package/dist/mcp/route.js +42 -0
  213. package/dist/mcp/runner.d.ts +45 -0
  214. package/dist/mcp/runner.js +99 -0
  215. package/dist/mcp/server.d.ts +11 -0
  216. package/dist/mcp/server.js +102 -0
  217. package/dock/agents.ts +121 -0
  218. package/dock/apns.ts +138 -0
  219. package/dock/console.ts +23 -0
  220. package/dock/dialers.ts +85 -0
  221. package/dock/faculty.ts +148 -0
  222. package/dock/fcm.ts +103 -0
  223. package/dock/grants.ts +111 -0
  224. package/dock/http2.ts +70 -0
  225. package/dock/index.ts +133 -0
  226. package/dock/local.ts +48 -0
  227. package/dock/location.ts +22 -0
  228. package/dock/notifications.ts +67 -0
  229. package/dock/push.ts +161 -0
  230. package/dock/quo-app-bells.md +280 -0
  231. package/dock/quo-dock.md +1942 -0
  232. package/dock/service.ts +100 -0
  233. package/dock/timer.ts +174 -0
  234. package/dock/webpush.ts +182 -0
  235. package/harbor/browser.ts +97 -0
  236. package/harbor/capacitor.ts +223 -0
  237. package/harbor/disk.ts +237 -0
  238. package/harbor/edge/alarm.ts +48 -0
  239. package/harbor/edge/edge.ts +407 -0
  240. package/harbor/edge/exercise.ts +241 -0
  241. package/harbor/edge/given.ts +106 -0
  242. package/harbor/edge/platform.d.ts +39 -0
  243. package/harbor/edge/storage.ts +179 -0
  244. package/harbor/edge/text.d.ts +15 -0
  245. package/harbor/edge/worker.ts +70 -0
  246. package/harbor/edge/wrangler.toml +30 -0
  247. package/harbor/files.ts +221 -0
  248. package/harbor/http.ts +188 -0
  249. package/harbor/idb.ts +0 -0
  250. package/harbor/quo-harbor.md +850 -0
  251. package/harbor/quo.ts +44 -0
  252. package/harbor/resolve.ts +31 -0
  253. package/harbor/seal.ts +26 -0
  254. package/harbor/sealed.ts +183 -0
  255. package/harbor/tauri.ts +79 -0
  256. package/harbor/ward.ts +28 -0
  257. package/human/app.ts +580 -0
  258. package/human/apps.ts +47 -0
  259. package/human/dom.ts +21 -0
  260. package/human/door.ts +113 -0
  261. package/human/guest.ts +34 -0
  262. package/human/html.ts +490 -0
  263. package/human/local.ts +30 -0
  264. package/human/mark.ts +53 -0
  265. package/human/move.ts +122 -0
  266. package/human/quo-human.md +1042 -0
  267. package/human/screen.ts +136 -0
  268. package/human/style.ts +63 -0
  269. package/human/tab.bundle.txt +4775 -0
  270. package/human/tab.ts +446 -0
  271. package/human/tree.ts +217 -0
  272. package/human/wake.ts +191 -0
  273. package/human/web.ts +259 -0
  274. package/human/worker.bundle.txt +3868 -0
  275. package/human/worker.ts +124 -0
  276. package/human/worlds.ts +127 -0
  277. package/mcp/agent.ts +94 -0
  278. package/mcp/allow.ts +131 -0
  279. package/mcp/http.ts +92 -0
  280. package/mcp/oauth.ts +270 -0
  281. package/mcp/quo-mcp.md +343 -0
  282. package/mcp/route.ts +45 -0
  283. package/mcp/runner.ts +136 -0
  284. package/mcp/server.ts +101 -0
  285. package/package.json +121 -0
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The root's setup of a fresh ward, as every terrain does it: the front desk
3
+ // booted public, one user being, the desk holding a standing to her under
4
+ // `user:<name>`, placed by the root's knock, and she holding her own ward's
5
+ // door as an owner under `home`. She names the desk `desk`,
6
+ // and that id is the only thing that makes it the desk. A disk harbor does
7
+ // this at `quo init`, the edge at its root route's init; the memory
8
+ // harbor's test world does the same by hand.
9
+ import type { Invitation } from '@nervur-org/nervur';
10
+ import type { Hosted } from '@nervur-org/nervur/harbor';
11
+ import { DESK, HOME } from './user.ts';
12
+
13
+ // The root boots and steps back: it invites an owner on the ward's own pk
14
+ // and the user being knocks for it, so from then on placing a being,
15
+ // minting a way in or removing one is an ask on her, shown by her gate to a
16
+ // device that may mint, and the box is not logged into for it. The same
17
+ // standing a user being of another ward takes to own this one; here it is
18
+ // her own.
19
+ export async function setupHome(hosted: Hosted, user: string): Promise<void> {
20
+ const inv = (await hosted.ask('invite', { being: hosted.pk, id: HOME })) as Invitation | { error?: string };
21
+ if ('error' in inv) throw new Error(`home: ${inv.error}`);
22
+ const placed = (await hosted.ask('knock', { being: user, id: HOME, invitation: inv })) as { taken?: string; error?: string };
23
+ if (placed.taken !== HOME) throw new Error(`home: ${placed.error ?? 'she could not take her own door'}`);
24
+ }
25
+
26
+ // The ward's home being is a `User` unless the device says which class: an
27
+ // organisation's ward has an org's home being, of the estate's own class,
28
+ // and everything else of setup, the desk and the door, is placed on her the
29
+ // same way.
30
+ export async function setup(hosted: Hosted, user: string, cls = 'User'): Promise<void> {
31
+ if (!/^[\w.-]+$/.test(user) || user === 'desk') throw new Error(`a user is a word, and not desk: ${user}`);
32
+ const boot = async (args: Record<string, unknown>) => {
33
+ const out = (await hosted.ask('boot', args)) as { booted?: string; error?: string };
34
+ if (out.error) throw new Error(`init: ${out.error}`);
35
+ };
36
+ await boot({ key: 'desk', class: 'Desk' });
37
+ const pub = (await hosted.ask('public', { key: 'desk' })) as { error?: string };
38
+ if (pub.error) throw new Error(`init: ${pub.error}`);
39
+ await boot({ key: user, class: cls });
40
+ const inv = (await hosted.ask('invite', { being: user, id: DESK })) as Invitation;
41
+ // the desk's first hello names her, since a being does not know her own key
42
+ const placed = (await hosted.ask('knock', { being: 'desk', id: `user:${user}`, invitation: inv, method: 'hello', args: { name: user } })) as { taken?: string };
43
+ if (placed.taken !== `user:${user}`) throw new Error('init: the desk could not reach the user being');
44
+ await setupHome(hosted, user);
45
+ }
package/beings/side.ts ADDED
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The side: the one interface an avatar speaks through to someone outside
3
+ // Quo. A side takes the avatar object, in-process, and renders three things
4
+ // its own way: her describe as what that someone reads, a call as an ask on
5
+ // her one standing, and a push as a notification. Sides in this repository:
6
+ // a screen, an MCP server, a runner, an event-driven agent. None knows
7
+ // another exists, and none of this file names a platform.
8
+ import { isSilence, isUnreached, isWord, wordOf } from '@nervur-org/nervur';
9
+ import type { Answer, Blueprint, Json, JsonObject, Wanted, WordName } from '@nervur-org/nervur';
10
+ import type { Avatar } from './avatar.ts';
11
+
12
+ // A side, running: it holds the avatar until closed.
13
+ export type Serving = { close(): Promise<void> };
14
+ export type Side = (avatar: Avatar) => Promise<Serving>;
15
+
16
+ // What a side speaks for: the empty ask and a named one, and the ears a
17
+ // push reaches. An avatar on her one standing is one; the owner's asks on a
18
+ // ward are another; a guest at a world's public being is a third.
19
+ export type Subject = {
20
+ tools(): Promise<Blueprint | { error: string }>;
21
+ call(name: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
22
+ ears: Set<(object: JsonObject) => void>;
23
+ };
24
+
25
+ // The three words for "no object", and the object itself, as every side must
26
+ // tell them apart before rendering. An error object is an ordinary answer
27
+ // that the side may mark; silence and unreached are not objects at all.
28
+ export type Word =
29
+ | { word: 'object'; value: Json }
30
+ | { word: 'error'; value: JsonObject }
31
+ | { word: 'silence' }
32
+ | { word: 'unreached' }
33
+ | { word: 'quo'; name: WordName }; // the ward's word, named: why no object came
34
+
35
+ export function word(answer: Answer): Word {
36
+ if (isSilence(answer)) return { word: 'silence' };
37
+ if (isUnreached(answer)) return { word: 'unreached' };
38
+ if (isWord(answer)) return { word: 'quo', name: wordOf(answer) };
39
+ if (answer !== null && typeof answer === 'object' && !Array.isArray(answer) && 'error' in answer) return { word: 'error', value: answer };
40
+ return { word: 'object', value: answer };
41
+ }
42
+
43
+ // What a side says about the two words that carry nothing, so that whoever
44
+ // reads them can decide: silence promises nothing, unreached promises that
45
+ // nothing was delivered.
46
+ export const SILENCE_TEXT = 'silence: the work may have happened, and no answer came back';
47
+ export const UNREACHED_TEXT = 'unreached: nothing was delivered; asking again is safe';
48
+ // And the ward's other words, each with what a side says of it.
49
+ export const WORD_TEXT: Record<WordName, string> = {
50
+ unreached: UNREACHED_TEXT,
51
+ late: 'late: the wait ran out; the work may have happened',
52
+ invitation: 'invitation: that is not an invitation',
53
+ dropped: 'dropped: the standing is gone',
54
+ removed: 'removed: the far being removed you',
55
+ absent: 'absent: the far being did not come back this run',
56
+ unannounced: 'unannounced: the knock announced no key of its own',
57
+ repeated: 'repeated: that number was already honoured',
58
+ threw: 'threw: the far being threw',
59
+ };
60
+ export const wordText = (name: WordName): string => WORD_TEXT[name];
package/beings/user.ts ADDED
@@ -0,0 +1,459 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The user being. One per human, running wherever the human's world runs.
3
+ // She holds the human's standings, and everyone who acts for the human is
4
+ // her occupant, seeing what her gate shows. Two of her occupants are
5
+ // special only by the id the root chose for them: `desk`, the front desk,
6
+ // who may ask her to invite a device; and each device, whose id is the
7
+ // client identity the desk established. A device she let mint brings the
8
+ // next one in, so a human adds a device from a device already in and only
9
+ // the first one has to come from the root, on the box.
10
+ import { isInvitation, isSilence, isWord, wordOf } from '@nervur-org/nervur';
11
+ import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Stance } from '@nervur-org/nervur';
12
+ import { Carrier } from './carry.ts';
13
+ import { PUSH, banner, isToken, join, sealBanner } from '../dock/index.ts';
14
+
15
+ export const DESK = 'desk';
16
+ // The prefix her doorbell standings are filed under, one per device: the
17
+ // way she rings that device, and never a way anything else may ask on.
18
+ export const BELL = 'bell';
19
+ // Her standing at her own ward's door, as an owner: the root placed it at
20
+ // setup, and through it she boots, invites and removes in her own world.
21
+ export const HOME = 'home';
22
+ const isDesk = (occ: OccupantRecord | undefined) => occ?.id === DESK;
23
+ const isDevice = (occ: OccupantRecord | undefined) => occ !== undefined && occ.id !== DESK;
24
+ // A device is known by its client identity. The desk writes it as a note
25
+ // when it asks her to mint; the root, minting on her directly, writes
26
+ // nothing, and then the id the root chose is the identity.
27
+ const rootMinted = (occ: OccupantRecord | undefined) => isDevice(occ) && occ?.notes.client === undefined;
28
+ const client = (occ: OccupantRecord | undefined) => (typeof occ?.notes.client === 'string' ? occ.notes.client : rootMinted(occ) ? occ!.id : null);
29
+
30
+ // The three words that travel with a minting, each a note on the occupant
31
+ // minted: `wake` may ring her other devices, `reach` may see and ask what
32
+ // she holds, `mint` may bring the next device in. What a device holds is
33
+ // also the most it may write on a device of its own, so a chain of devices
34
+ // never gains a power along the way.
35
+ //
36
+ // The desk speaks the human's word at the exchange and holds all three. A
37
+ // device the root minted reaches and may wake without a note, since the
38
+ // root owns everything she holds and chose to mint it. `mint` is never
39
+ // implied, not even there: it admits somebody else, which is not the root's
40
+ // own reaching, and it is said out loud or not at all. The root says it by
41
+ // minting with `{ notes: { mint: true } }`, which is the ward's owner
42
+ // invite, so a harbor with no desk can still let a device beget devices.
43
+ const MINTINGS = ['wake', 'reach', 'mint', 'revoke'] as const;
44
+ // How many unreached pushes she keeps for one device. A device gone for a
45
+ // month is a device that will be told what is recent, not everything.
46
+ const OUTBOX = 32;
47
+ type Minting = (typeof MINTINGS)[number];
48
+ // Whether a ring left. A doorbell and the bell both say `rang`, and every
49
+ // other answer, a word, silence or an error object, is a device unreached.
50
+ const rang = (out: unknown): boolean => !isSilence(out) && !isWord(out) && out !== null && typeof out === 'object' && !Array.isArray(out) && (out as JsonObject).rang !== undefined && (out as JsonObject).rang !== false;
51
+ const held = (occ: OccupantRecord | undefined): Record<Minting, boolean> =>
52
+ isDesk(occ)
53
+ ? { wake: true, reach: true, mint: true, revoke: true }
54
+ : { wake: occ?.notes.wake === true || rootMinted(occ), reach: occ?.notes.reach === true || rootMinted(occ), mint: occ?.notes.mint === true, revoke: occ?.notes.revoke === true };
55
+ const mayWake = (occ: OccupantRecord | undefined) => held(occ).wake;
56
+ // The desk mints for a client it vouched for; a device of hers mints the
57
+ // next one. Nobody else, and no ask on her lets an occupant grant itself
58
+ // a note it does not hold.
59
+ const mayMint = (occ: OccupantRecord | undefined) => isDesk(occ) || (isDevice(occ) && held(occ).mint);
60
+
61
+ // Revoking is its own word and never `mint`'s. A device that mints adds
62
+ // devices, which is noisy and stays visible; one that revokes can take the
63
+ // human's own devices out of her world, so a stolen phone that could do
64
+ // both would lock her out before she reached a laptop. It follows `mint`
65
+ // otherwise: never a default, held only where it was granted, and handed on
66
+ // by a device that holds it and no further. A world where nobody was given
67
+ // it has the act out of reach and the root's own removals instead, and that
68
+ // is a choice an org makes rather than one the dock makes for it.
69
+ const mayRevoke = (occ: OccupantRecord | undefined) => isDesk(occ) || (isDevice(occ) && held(occ).revoke);
70
+
71
+ // She carries her standings, acme, the calendar, the house, for a device the
72
+ // human allowed to reach them at the exchange: the note says so. A device
73
+ // without the note sees her own asks alone, and a model sees acme only
74
+ // because the human said it may. A device the root minted reaches: the root
75
+ // is the owner of everything she holds and chose to mint it.
76
+ export class User extends Carrier {
77
+ static override carries(occ: OccupantRecord | undefined): boolean {
78
+ return isDevice(occ) && held(occ).reach;
79
+ }
80
+ // `ways` holds a device's way back that could not be taken when it was
81
+ // handed over: the knock was unreached, a tab whose pk the far harbor had
82
+ // not bound yet, a phone in a tunnel. Nothing delivered spends nothing,
83
+ // so the invitation is tried again at the next push.
84
+ // The bell standing is plumbing and carries nothing: a device that may
85
+ // reach what she holds sees acme and the house, and never the envoy that
86
+ // wakes it. Her ways back are plumbing too, and a way back to a presence
87
+ // being carries that device's faculties, `to:phone-location-where`, which
88
+ // is how a model at home learns where the human is.
89
+ static override hidden(id: string): boolean {
90
+ return super.hidden(id) || id === PUSH || id.startsWith(`${BELL}:`);
91
+ }
92
+ // Her own door is carried to a device that may mint, and to nobody else:
93
+ // `mint` is the word for a device trusted to change who is in her world,
94
+ // and owning the world from that device is that trust said in full. A
95
+ // device that reaches sees acme and the house, and no `home-boot`.
96
+ static override shows(id: string, occ: OccupantRecord | undefined): boolean {
97
+ return id !== HOME || held(occ).mint;
98
+ }
99
+ static override cells = { name: '', reports: [] as JsonObject[], ways: {} as Record<string, Invitation>, outbox: {} as Record<string, JsonObject[]> };
100
+
101
+ // Her standing at the box's push faculty, taken again at every birth: a
102
+ // standing points at a being and not at a box, and a ward that moved is
103
+ // handed the new harbor's ground. It is how she rings a browser endpoint,
104
+ // which is the one kind of doorbell a world holds itself, since an
105
+ // endpoint answers only to the VAPID secret this world minted. Birth is
106
+ // synchronous and taking a standing is not, so the promise is kept and
107
+ // the ring waits on it.
108
+ //
109
+ // A box that lends no push rings no tabs, which is what a world with no
110
+ // web route behind it is.
111
+ readonly #push: Promise<string | null>;
112
+ constructor(stance: Stance) {
113
+ super(stance);
114
+ this.#push = join(this, PUSH);
115
+ }
116
+
117
+ // A world whose own being she is, is a home: a device reaching her holds
118
+ // a standing at home and not a membership, and she says so in her notes,
119
+ // since nothing else in a describe tells a shell which of its worlds is
120
+ // the human's own. She knows it by the door she holds: the being setup
121
+ // knocked on the ward's own door as its owner is home's, and a member of
122
+ // the same class the desk made in an org's ward holds no door and says
123
+ // nothing.
124
+ override describe(asker: Asker): Blueprint {
125
+ const bp = super.describe(asker);
126
+ if (!this.standings[HOME]) return bp;
127
+ const notes = bp.notes !== null && typeof bp.notes === 'object' && !Array.isArray(bp.notes) ? bp.notes : {};
128
+ return { ...bp, notes: { ...notes, home: true } };
129
+ }
130
+ static override asks = {
131
+ hello: { description: 'say hello, and hand back an invitation so she can reach you', input: { type: 'object', properties: { invitation: { type: 'object' } } } },
132
+ whoami: { description: 'who she thinks you are', input: { type: 'object' }, for: isDevice },
133
+ device: { description: 'mint an invitation for another device', input: { type: 'object', properties: { client: { type: 'string' }, wake: { type: 'boolean' }, reach: { type: 'boolean' }, mint: { type: 'boolean' }, revoke: { type: 'boolean' } }, required: ['client'] }, for: mayMint },
134
+ push: { description: 'push an object to a device: wake it with an event', input: { type: 'object', properties: { client: { type: 'string' }, object: { type: 'object' } }, required: ['client', 'object'] }, for: mayWake },
135
+ chores: { description: 'what the agent may run', input: { type: 'object' }, for: (occ: OccupantRecord | undefined) => client(occ) === 'agent' },
136
+ look: { description: 'how she is shown', input: { type: 'object' }, for: isDevice },
137
+ page: { description: 'her page, as a tree of values', input: { type: 'object' }, for: isDevice },
138
+ name: { description: 'what she is called', input: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, for: rootMinted },
139
+ devices: { description: 'the devices in this world, and which one you are', input: { type: 'object' }, for: isDevice },
140
+ join: { description: 'join a world with an invitation: she knocks, hands back a way to reach her, and keeps the standing', input: { type: 'object', properties: { invitation: { type: 'object', description: 'the invitation that world minted' }, name: { type: 'string', description: 'what to call it here' } }, required: ['invitation', 'name'] }, for: mayMint },
141
+ forget: { description: 'revoke a device: drop its way in and her way back to it, in one act', input: { type: 'object', properties: { client: { type: 'string' } }, required: ['client'] }, for: mayRevoke },
142
+ report: { description: 'what a run of yours found', input: { type: 'object', properties: { event: { type: 'object' }, result: {} }, required: ['event', 'result'] }, for: isDevice },
143
+ bell: { description: 'how to wake this device: a standing at its own doorbell and the banner key it seals to, or a browser push endpoint', input: { type: 'object', properties: { invitation: { type: 'object' }, banner: { type: 'string' }, token: { type: 'object' } } }, for: isDevice },
144
+ missed: { description: 'what this device missed while it was unreachable', input: { type: 'object' }, for: isDevice },
145
+ };
146
+
147
+ // Anyone may say hello. A device that hands her an invitation in the args
148
+ // is taken as a standing under its own id, so she can push to it later;
149
+ // if it cannot be taken now, it is kept and tried at the next push. The
150
+ // desk's first hello names her: a being does not know her own key, and the
151
+ // root's setup knocks with the name it booted her under. Once named, she
152
+ // keeps it.
153
+ async hello(args: JsonObject, asker: Asker) {
154
+ if (asker.id !== undefined && args.invitation && typeof args.invitation === 'object' && !Array.isArray(args.invitation)) {
155
+ await this.wayBack(asker.id, args.invitation as Invitation);
156
+ }
157
+ if (asker.id === DESK && !this.cells.name && typeof args.name === 'string' && /^[\w.-]{1,80}$/.test(args.name)) this.cells.name = args.name;
158
+ return { welcome: asker.id ?? null, name: this.cells.name };
159
+ }
160
+
161
+ // Take a device's way back, or keep it for later when the knock did not
162
+ // reach. A refusal spends the heir and is final; the way is dropped.
163
+ async wayBack(id: string, inv: Invitation): Promise<boolean> {
164
+ const ways = this.cells.ways as Record<string, Invitation>;
165
+ const back = await this.knock(inv);
166
+ if (!isSilence(back) && !isWord(back)) {
167
+ await this.take(`to:${id}`, inv);
168
+ delete ways[id];
169
+ return true;
170
+ }
171
+ if (isWord(back) && wordOf(back) !== 'unreached') delete ways[id];
172
+ else ways[id] = inv;
173
+ return false;
174
+ }
175
+
176
+ whoami(_args: JsonObject, asker: Asker) {
177
+ return { id: asker.id ?? null, client: client(this.occupant(asker)) };
178
+ }
179
+
180
+ // The front desk asks, or a device of hers that may mint; she mints. The
181
+ // client identity goes into the occupant's notes, and that is what every
182
+ // gate reads. The three words travel with it, each asked for and each
183
+ // written only if the asker holds it: devices beget devices, and a device
184
+ // hands on no more than it was given.
185
+ async device(args: JsonObject, asker: Asker) {
186
+ const c = typeof args.client === 'string' ? args.client : null;
187
+ if (c === null) return { error: 'client is a string' };
188
+ const mine = held(this.occupant(asker));
189
+ const inv = await this.invite(c);
190
+ if (inv === null) return { error: 'id taken' };
191
+ const rec = this.cells.occupants[c];
192
+ if (rec) {
193
+ rec.notes.client = c;
194
+ for (const w of MINTINGS) if (args[w] === true && mine[w]) rec.notes[w] = true;
195
+ }
196
+ return inv as unknown as JsonObject;
197
+ }
198
+
199
+ // Her way out and the device's way in are two relations, and Quo keeps
200
+ // them apart. She joins them herself, here, as her own rule: she does
201
+ // not reach a device she no longer admits. Without it a revoked device
202
+ // stops being able to ask her and keeps receiving everything she pushes.
203
+ async push(args: JsonObject) {
204
+ const c = typeof args.client === 'string' ? args.client : null;
205
+ if (c === null || !this.cells.occupants[c]) return { error: 'no such device, or it gave no way back' };
206
+ const ways = this.cells.ways as Record<string, Invitation>;
207
+ const object = (args.object as JsonObject) ?? {};
208
+ if (!this.standings[`to:${c}`] && ways[c]) await this.wayBack(c, ways[c]);
209
+ const st = this.standings[`to:${c}`];
210
+ // A way back she still could not take is a device that is not there:
211
+ // the knock itself was unreached, which is the same nothing delivered
212
+ // as an unreached ask, and it is rung like one.
213
+ if (!st) return ways[c] ? this.unreached(c, object) : { error: 'no such device, or it gave no way back' };
214
+ const out = await st.ask('notify', object);
215
+ if (isWord(out) && wordOf(out) === 'unreached') return this.unreached(c, object);
216
+ return isSilence(out) ? { error: 'silence' } : isWord(out) ? { error: wordOf(out) } : { pushed: out };
217
+ }
218
+
219
+ // Nothing was delivered, so the device is asleep, closed or in a tunnel.
220
+ // The object waits in her outbox under that device, bounded and oldest
221
+ // out, since a device gone for a month must not grow without end; then
222
+ // she rings, and the pusher hears both facts. What the ring carries is
223
+ // the device's token and nothing of the object: the device wakes, dials,
224
+ // and drains it over its own standing, sealed.
225
+ private async unreached(id: string, object: JsonObject) {
226
+ const box = ((this.cells.outbox as Record<string, JsonObject[]>)[id] ??= []);
227
+ box.push(object);
228
+ while (box.length > OUTBOX) box.shift();
229
+ return { unreached: true, rang: await this.ring(id, object) };
230
+ }
231
+
232
+ // A phone is rung on the standing it handed her, with the line sealed to
233
+ // the banner key it handed her beside it. Who is ringing is the standing
234
+ // the ring arrives on, so the ask carries no world, no pk and no token,
235
+ // and there is nothing in it to forge. A browser is rung on its endpoint
236
+ // through this box's own push faculty, because an endpoint answers only
237
+ // to the key this world minted, and the line goes with it unsealed:
238
+ // Web Push encrypts a body to the subscription's own keys, so a second
239
+ // seal would hide it from nobody it is not already hidden from.
240
+ private async ring(id: string, object: JsonObject): Promise<boolean> {
241
+ const rec = this.cells.occupants[id];
242
+ const at = this.standings[`${BELL}:${id}`];
243
+ const key = rec?.notes.banner;
244
+ if (at && typeof key === 'string') {
245
+ const sealed = await sealBanner(banner(object), key);
246
+ return sealed === null ? false : rang(await at.ask('ring', { sealed }));
247
+ }
248
+ const token = rec?.notes.bell;
249
+ if (!isToken(token)) return false;
250
+ await this.#push;
251
+ const st = this.standings[PUSH];
252
+ if (!st) return false;
253
+ // A browser is handed the line itself. The ground encrypts it under the
254
+ // subscription's own keys, RFC 8291, and the box that does it is this
255
+ // world's own, so the line is never seen by anything between them.
256
+ return rang(await st.ask('ring', { token: token as unknown as JsonObject, line: banner(object) }));
257
+ }
258
+
259
+ // A device says how it is woken, and what it says is kept on its own
260
+ // occupant, so forgetting the device forgets the way to wake it in the
261
+ // same act.
262
+ //
263
+ // A phone or a desk hands a standing at a doorbell of its own, and the
264
+ // banner key that doorbell never learns. This world holds a way to wake
265
+ // that one device and nothing it could hand on: no other world rings on
266
+ // this standing, the device cuts it by itself at the doorbell, and the
267
+ // platform's token rotating is one write there that no world is told of.
268
+ //
269
+ // A browser hands its endpoint instead and needs no doorbell: an endpoint
270
+ // is rung only by whoever holds the VAPID secret the subscription was
271
+ // made under, and that secret is this world's own.
272
+ //
273
+ // A store's token never arrives here. The same token at every world is
274
+ // the bearer credential the doorbell exists to take away, so one is
275
+ // refused where a human can read why.
276
+ async bell(args: JsonObject, asker: Asker): Promise<JsonObject> {
277
+ const c = client(this.occupant(asker));
278
+ const rec = c === null ? undefined : this.cells.occupants[c];
279
+ if (c === null || !rec) return { error: 'no client' };
280
+ // Asked with nothing, she says what she already holds. That is what
281
+ // makes handing a doorbell over idempotent: a standing does not rotate
282
+ // the way a token did, so a device hands one over when this world has
283
+ // none and never again, and the doorbell's list of the worlds it allows
284
+ // stays a list a human can read and cut.
285
+ if (args.invitation === undefined && args.token === undefined) {
286
+ const at = this.standings[`${BELL}:${c}`];
287
+ return { kept: at && typeof rec.notes.banner === 'string' ? 'doorbell' : isToken(rec.notes.bell) ? 'web' : null };
288
+ }
289
+ if (isInvitation(args.invitation)) {
290
+ const key = args.banner;
291
+ if (typeof key !== 'string' || !/^[0-9a-f]{64}$/.test(key)) return { error: 'a banner key is thirty two bytes as hex' };
292
+ const id = `${BELL}:${c}`;
293
+ this.standings.remove(id);
294
+ // She knocks and then takes, as every side does: an invitation is
295
+ // taken only where it was answered. What she asks for is the
296
+ // blueprint, which at a doorbell is the one ask `ring`.
297
+ const out = await this.knock(args.invitation);
298
+ if (isSilence(out) || isWord(out)) return { error: isSilence(out) ? 'silence' : wordOf(out) };
299
+ if ((await this.take(id, args.invitation)) === null) return { error: 'that invitation was refused' };
300
+ rec.notes.banner = key;
301
+ delete rec.notes.bell;
302
+ return { kept: 'doorbell' };
303
+ }
304
+ if (!isToken(args.token)) return { error: 'a token names its kind' };
305
+ if (args.token.kind !== 'web') return { error: 'a device hands a standing at its own doorbell, never a store token' };
306
+ rec.notes.bell = args.token;
307
+ return { kept: 'web' };
308
+ }
309
+
310
+ // The device is back. It drains what it missed and composes what the
311
+ // human sees from these objects, which crossed sealed and were never on
312
+ // a push service. Emptied as it is handed over: an ask that answered is
313
+ // an ask that arrived, and a device that lost the answer has lost one
314
+ // notification, not its relation.
315
+ missed(_args: JsonObject, asker: Asker) {
316
+ const c = client(this.occupant(asker));
317
+ if (c === null) return { error: 'no client' };
318
+ const box = this.cells.outbox as Record<string, JsonObject[]>;
319
+ const out = box[c] ?? [];
320
+ delete box[c];
321
+ return { missed: out };
322
+ }
323
+
324
+ // She joins a world. This is the day a homeless org gains a home: an org
325
+ // whose whole existence was standings in other orgs' worlds boots a home
326
+ // on a box it trusts, and that home joins each of those from there.
327
+ // Nothing is converted and no id changes, because from the far world's
328
+ // side this home is one more thing of hers holding a standing, exactly as
329
+ // her phone is: her device there minted the invitation with `device`, and
330
+ // this is the other half, the knock.
331
+ //
332
+ // It is a device that may mint who asks it, and that needs no word of its
333
+ // own: `mint` is what shows her own door to a device, so one that holds
334
+ // it can boot and invite in her world already, and adding one standing to
335
+ // what she carries is less than that. In practice such a device holds
336
+ // `reach` as well, since a device that joined a world it could not then
337
+ // see would be a device doing something for somebody else. She hands back a way for the far being
338
+ // to reach her, as a device does at hello, and a far being who refuses it
339
+ // is joined all the same.
340
+ async join(args: JsonObject): Promise<JsonObject> {
341
+ const inv = args.invitation;
342
+ if (!isInvitation(inv)) return { error: 'that is not an invitation' };
343
+ const name = typeof args.name === 'string' ? args.name.trim() : '';
344
+ if (!/^[\w.-]{1,80}$/.test(name) || name === HOME || name === PUSH) return { error: 'a name is a word, and not home or the push' };
345
+ if (this.standings[name]) return { error: 'name taken' };
346
+ const back = `from:${name}`;
347
+ const mine = await this.invite(back);
348
+ if (mine === null) return { error: 'name taken' };
349
+ const out = await this.knock(inv, 'hello', { invitation: mine as unknown as JsonObject });
350
+ if (isSilence(out) || isWord(out)) {
351
+ this.occupants.remove(back);
352
+ return { error: isSilence(out) ? 'silence' : wordOf(out) };
353
+ }
354
+ if (out !== null && typeof out === 'object' && !Array.isArray(out) && 'error' in out) this.occupants.remove(back);
355
+ const took = await this.take(name, inv);
356
+ if (took === null) {
357
+ this.occupants.remove(back);
358
+ return { error: 'that invitation was refused' };
359
+ }
360
+ return { joined: name };
361
+ }
362
+
363
+ // Her devices, by the names they were given, with what each was granted
364
+ // and which of them is asking. Without this a human cannot use `forget`
365
+ // or `push` at all: both name a device, and nobody who has just lost a
366
+ // phone remembers what it was called. It is read-only and says nothing
367
+ // secret; a device is a name and three words, and the keys are in the
368
+ // relations rather than here.
369
+ // People-shaped rows, since a page says devices are people: a name, and
370
+ // a line that says what each may do and which one is asking.
371
+ devices(_args: JsonObject, asker: Asker) {
372
+ const me = client(this.occupant(asker));
373
+ return Object.entries(this.cells.occupants)
374
+ .filter(([id]) => id !== DESK)
375
+ .map(([id, rec]) => {
376
+ const notes = (rec as OccupantRecord).notes;
377
+ const name = typeof notes.client === 'string' ? notes.client : id;
378
+ const has = MINTINGS.filter((w) => held(rec as OccupantRecord)[w]);
379
+ const line = [...(name === me ? ['this one'] : []), ...(has.length ? [`may ${has.join(', ')}`] : [])].join(' · ');
380
+ return { name, line, client: name, ...(name === me ? { you: true } : {}), ...(has.length ? { may: has } : {}) };
381
+ });
382
+ }
383
+
384
+ // Revocation is one act at her, because only she knows both ids. A side
385
+ // that had to remove two relations by hand could leave half of one
386
+ // standing, and every side would have to remember which half.
387
+ forget(args: JsonObject) {
388
+ const c = typeof args.client === 'string' ? args.client : null;
389
+ if (c === null) return { error: 'client is a string' };
390
+ this.occupants.remove(c);
391
+ this.standings.remove(`to:${c}`);
392
+ this.standings.remove(`${BELL}:${c}`);
393
+ delete (this.cells.ways as Record<string, Invitation>)[c];
394
+ delete (this.cells.outbox as Record<string, JsonObject[]>)[c];
395
+ return { forgot: c };
396
+ }
397
+
398
+ chores() {
399
+ return { chores: ['census', 'report'] };
400
+ }
401
+
402
+ // How a device shows her: her name as the title, and what each ask is
403
+ // called. Values only; the screen and the model side each read their part.
404
+ look() {
405
+ return {
406
+ name: (this.cells.name as string) || 'you',
407
+ order: ['whoami', 'devices', 'device', 'join', 'push', 'forget', 'name'],
408
+ asks: { whoami: { title: 'who am I', readOnly: true }, devices: { title: 'your devices', readOnly: true }, device: { title: 'invite a device' }, join: { title: 'join a world' }, push: { title: 'wake a device' }, forget: { title: 'take a device out of your world', destructive: true }, name: { title: 'call her' } },
409
+ };
410
+ }
411
+
412
+ // Her page for a device: her name, who this device is, the worlds she
413
+ // holds as sections, and the three things a human does here by hand,
414
+ // bringing her next device in, waking one and naming her. The invitation
415
+ // `device` answers is a way in, and the side paints it as the link it is,
416
+ // since a being does not know where her page lives. `hello` and `report` are wiring, a
417
+ // device's first word and an agent's callback; they stay in her describe
418
+ // under the gate and off her page, since a page is presentation and the
419
+ // gate is permission. A device that may not do a thing sees no form for
420
+ // it, however the page names it.
421
+ page(_args: JsonObject, asker: Asker) {
422
+ return {
423
+ kind: 'stack',
424
+ of: [
425
+ { kind: 'text', text: (this.cells.name as string) || 'you', role: 'title' },
426
+ { kind: 'text', text: `on ${client(this.occupant(asker)) ?? 'a device'}`, role: 'quiet' },
427
+ { kind: 'text', text: 'your devices', role: 'label' },
428
+ { kind: 'answer', ask: 'devices', as: 'people' },
429
+ // every standing she carries as its section, and the door of her
430
+ // own world as a door: seven owner asks are a page of their own
431
+ ...Object.keys(this.cells.standings)
432
+ .sort((a, b) => (a === HOME ? 1 : b === HOME ? -1 : a.localeCompare(b)))
433
+ .map((id) => (id === HOME ? { kind: 'standing', id, as: 'door' } : { kind: 'standing', id })),
434
+ { kind: 'form', ask: 'device' },
435
+ { kind: 'form', ask: 'join' },
436
+ { kind: 'form', ask: 'push' },
437
+ { kind: 'form', ask: 'forget' },
438
+ { kind: 'form', ask: 'name' },
439
+ ],
440
+ };
441
+ }
442
+
443
+ // A device the root minted may say what she is called: the root trusts
444
+ // it with its id and its reach already, and a being does not know her own
445
+ // key. A name is a word; she keeps the last one given.
446
+ name(args: JsonObject) {
447
+ const name = typeof args.name === 'string' ? args.name.trim() : '';
448
+ if (!/^[\w.-]{1,80}$/.test(name)) return { error: 'a name is a word' };
449
+ this.cells.name = name;
450
+ return { named: name };
451
+ }
452
+
453
+ // A device that ran something for her says what it found. Kept, so that
454
+ // whoever renders her can show it; the ask itself is the callback.
455
+ report(args: JsonObject, asker: Asker) {
456
+ (this.cells.reports as JsonObject[]).push({ from: asker.id ?? null, event: args.event ?? null, result: args.result ?? null });
457
+ return { reported: true };
458
+ }
459
+ }
package/beings/way.ts ADDED
@@ -0,0 +1,65 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A way in: one being that turns a proof of one kind into who it is for.
3
+ // The desk holds a standing at each of them, under `way:<kind>`, and that
4
+ // id is the whole of what makes a being a way in: there is no table, no
5
+ // registry and nothing set on a class, so a way in is added and removed the
6
+ // way every other relation is, by the root's knock and by remove.
7
+ //
8
+ // Two kinds of holder, and both are ordinary beings:
9
+ //
10
+ // - the box's, lent by the dock ward, as `local` is: the daemon vouches for
11
+ // a process it saw on its own socket, and what it knows lives in that
12
+ // being, on the box, dying with it.
13
+ // - the world's own, in the world's ward, as an estate's post is: she holds
14
+ // the addresses, mints what she handed out and spends it once.
15
+ //
16
+ // A kind is a word the org chose and the dock knows none of them. There is
17
+ // no list here, no registry and no flag on a class, so mail, a message, a
18
+ // code on a screen, a letter, a voice on a call or a sign-in a person holds
19
+ // at some other company are each one kind, named nowhere in this package.
20
+ // Building one nobody here imagined is booting a being and knocking her
21
+ // onto the desk, and that is the whole of it.
22
+ //
23
+ // A way in answers `verify`. One that hands its proof out itself answers
24
+ // `send` beside it, and the desk shows the form for it because her standing
25
+ // says the being can be asked it. She judges neither: she does not know what
26
+ // an address is, whether one was sent to before, or what a link says.
27
+ import type { AskSpec, Json } from '@nervur-org/nervur';
28
+
29
+ export type Proof = { kind: string; [more: string]: Json };
30
+ // Who the proof is for, and how far the device it admits may go. `make` is
31
+ // the world saying this is a way in that brings its own people: a being
32
+ // under that key is made when there is none, which is what a world anyone
33
+ // may join needs and a world with a fixed household must never say. What
34
+ // is made is the org's being about that person, a membership and never a
35
+ // home: `true` makes a `User`, a class name makes one of the estate's own.
36
+ export type Verified = { user: string; client: string; wake?: boolean; reach?: boolean; mint?: boolean; revoke?: boolean; make?: boolean | string };
37
+
38
+ // The desk's id for the way in of one kind, and the kind read back off it.
39
+ // The kinds a world offers are the ids the desk holds, and nothing else.
40
+ export const WAY = (kind: string): string => `way:${kind}`;
41
+ export const kindOf = (id: string): string | null => (id.startsWith('way:') ? id.slice(4) : null);
42
+
43
+ // The two asks a way in may be asked. Every implementation declares these,
44
+ // so one suite reaches all of them and the desk asks one thing of each.
45
+ export const VERIFY: AskSpec = { description: 'who this proof is for', input: { type: 'object', properties: { proof: { type: 'object' } }, required: ['proof'] } };
46
+ export const SEND: AskSpec = { description: 'send a way in to an address of that kind', input: { type: 'object', properties: { to: { type: 'string' } }, required: ['to'] } };
47
+
48
+ // What came back from `verify`, read before it is trusted. A way in is
49
+ // somebody else's being, so an answer that is not this shape is a refusal
50
+ // and never a caller the desk makes up.
51
+ export function verified(out: unknown): Verified | null {
52
+ if (out === null || typeof out !== 'object' || Array.isArray(out)) return null;
53
+ const v = out as Record<string, unknown>;
54
+ if (typeof v.user !== 'string' || typeof v.client !== 'string') return null;
55
+ const make = v.make;
56
+ return {
57
+ user: v.user,
58
+ client: v.client,
59
+ ...(v.wake === true ? { wake: true } : {}),
60
+ ...(v.reach === true ? { reach: true } : {}),
61
+ ...(v.mint === true ? { mint: true } : {}),
62
+ ...(v.revoke === true ? { revoke: true } : {}),
63
+ ...(make === true || typeof make === 'string' ? { make } : {}),
64
+ };
65
+ }
package/cli/bundles.ts ADDED
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Where the daemon finds the browser bundles the dock's build emitted. The
3
+ // route is handed two strings and reads nothing; this file is the terrain's
4
+ // half, and it is Node's because the daemon is. One path answers in both
5
+ // trees, because `dist` mirrors the source layout: `human/<name>.bundle.txt`
6
+ // is beside this file's own folder, whether this file is the source the
7
+ // daemon runs from or the JavaScript in an installed package.
8
+ import { readFile } from 'node:fs/promises';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ export type Bundles = { tab: string; worker: string };
12
+
13
+ const read = async (name: string) => {
14
+ const at = fileURLToPath(new URL(`../human/${name}.bundle.txt`, import.meta.url));
15
+ const text = await readFile(at, 'utf8').catch(() => null);
16
+ if (text === null) throw new Error(`no ${name} bundle at ${at}: run the dock's build, which emits human/${name}.bundle.txt`);
17
+ return text;
18
+ };
19
+
20
+ export const bundles = async (): Promise<Bundles> => ({ tab: await read('tab'), worker: await read('worker') });