@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,14 @@
1
+ export { User, DESK, HOME, BELL } from './user.ts';
2
+ export { Desk } from './desk.ts';
3
+ export { WAY, kindOf, verified, VERIFY, SEND, type Proof, type Verified } from './way.ts';
4
+ export { Avatar, USER, PUSHER } from './avatar.ts';
5
+ export { Carrier } from './carry.ts';
6
+ export { Presence } from './presence.ts';
7
+ export { Courier, MOVER, isMoving, type Moving } from './courier.ts';
8
+ export { Porter } from './porter.ts';
9
+ export { Doorbell } from './doorbell.ts';
10
+ export { Envoy, owner, occupant, type Delivery, type Wake, type Heard } from './envoy.ts';
11
+ export { DOCK, CONSOLE, LOCAL, LOCATION, NOTIFICATIONS, PUSH, TIMER, FACULTIES, FACULTY_CLASSES, Console, Faculty, Local, Location, Notifications, Push, Timer, isToken, join, settled, stand, standOn, lending, facultyOf, type Ring, type Token, type Where, type Watched, type Woke, type Rooted } from '../dock/index.ts';
12
+ export { webPush, mintVapid, publicOf, apnsPush, pkcs8, HOSTS, fcmPush, isAccount, DOORS, handedTo, ringing, routed, type Vapid, type Apns, type Account, type Doors } from '../dock/index.ts';
13
+ export { Clock, CLOCK, setupClock, placeClock, readSchedule, parseCron, nextFire, civil, fromCivil, type Schedule, type Fire, type Cron, type Civil } from './clock.ts';
14
+ export { sanitise, hint, hintFor, groups, type Look, type Hint } from './look.ts';
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The beings every side shares. See quo-dock.md, the trunk.
3
+ export { User, DESK, HOME, BELL } from './user.js';
4
+ export { Desk } from './desk.js';
5
+ export { WAY, kindOf, verified, VERIFY, SEND } from './way.js';
6
+ export { Avatar, USER, PUSHER } from './avatar.js';
7
+ export { Carrier } from './carry.js';
8
+ export { Presence } from './presence.js';
9
+ export { Courier, MOVER, isMoving } from './courier.js';
10
+ export { Porter } from './porter.js';
11
+ export { Doorbell } from './doorbell.js';
12
+ export { Envoy, owner, occupant } from './envoy.js';
13
+ export { DOCK, CONSOLE, LOCAL, LOCATION, NOTIFICATIONS, PUSH, TIMER, FACULTIES, FACULTY_CLASSES, Console, Faculty, Local, Location, Notifications, Push, Timer, isToken, join, settled, stand, standOn, lending, facultyOf } from '../dock/index.js';
14
+ export { webPush, mintVapid, publicOf, apnsPush, pkcs8, HOSTS, fcmPush, isAccount, DOORS, handedTo, ringing, routed } from '../dock/index.js';
15
+ export { Clock, CLOCK, setupClock, placeClock, readSchedule, parseCron, nextFire, civil, fromCivil } from './clock.js';
16
+ export { sanitise, hint, hintFor, groups } from './look.js';
@@ -0,0 +1,15 @@
1
+ import type { Hosted } from '@nervur-org/nervur/harbor';
2
+ import type { Invitation } from '@nervur-org/nervur';
3
+ import type { Avatar } from './avatar.ts';
4
+ export declare function avatarOf(hosted: Hosted, identity: string): Promise<{
5
+ avatar?: Avatar;
6
+ error?: string;
7
+ }>;
8
+ export declare function letIn(hosted: Hosted, identity: string, invitation: Invitation): Promise<{
9
+ avatar?: Avatar;
10
+ error?: string;
11
+ }>;
12
+ export declare function find(hosted: Hosted, identity: string): Promise<{
13
+ avatar?: Avatar;
14
+ error?: string;
15
+ }>;
@@ -0,0 +1,42 @@
1
+ // Her avatar in a ward, found or booted under the identity's key. An
2
+ // identity is a word, and never a being of the ward.
3
+ export async function avatarOf(hosted, identity) {
4
+ if (!/^[\w.-]+$/.test(identity) || identity === hosted.record.user || identity === 'desk')
5
+ return { error: 'an identity is a word, and not a being of the ward' };
6
+ const key = `avatar:${identity}`;
7
+ let avatar = hosted.being(key);
8
+ if (!avatar) {
9
+ const out = (await hosted.ask('boot', { key, class: 'Avatar' }));
10
+ if (out.error)
11
+ return { error: out.error };
12
+ avatar = hosted.being(key);
13
+ }
14
+ return { avatar };
15
+ }
16
+ // Join a client identity to a ward with an invitation the root minted for
17
+ // it: find or boot her avatar, and she knocks. The one path for an identity
18
+ // from elsewhere, the exchange page. Nothing is typed to get in but the
19
+ // invitation, and the knock spends it.
20
+ export async function letIn(hosted, identity, invitation) {
21
+ const found = await avatarOf(hosted, identity);
22
+ if (!found.avatar)
23
+ return found;
24
+ const joined = await found.avatar.join(invitation);
25
+ await hosted.save();
26
+ if ('error' in joined && !('asks' in joined))
27
+ return { error: joined.error };
28
+ return { avatar: found.avatar };
29
+ }
30
+ // Her avatar as she stands, for a session opening under a grant: she is in
31
+ // or she is not, and nothing is minted here. An identity the user being
32
+ // removed hears `removed` on her describe, which the endpoint speaks as 401.
33
+ export async function find(hosted, identity) {
34
+ const avatar = hosted.being(`avatar:${identity}`);
35
+ if (!avatar)
36
+ return { error: 'not joined' };
37
+ const bp = await avatar.tools();
38
+ await hosted.save();
39
+ if ('error' in bp && !('asks' in bp))
40
+ return { error: bp.error };
41
+ return { avatar };
42
+ }
@@ -0,0 +1,14 @@
1
+ import type { Invitation } from '@nervur-org/nervur';
2
+ export declare const KEY = "quo";
3
+ export declare const PROOF = "proof";
4
+ export declare function format(inv: Invitation): string;
5
+ export declare function parse(hash: string): Invitation | null;
6
+ export declare function parseProof(hash: string): {
7
+ kind: string;
8
+ token: string;
9
+ } | null;
10
+ export declare function strip(hash: string): string;
11
+ export declare function link(page: string, inv: Invitation): string;
12
+ export declare function proofLink(page: string, kind: string, token: string): string;
13
+ export declare function invitationOf(text: string): Invitation | null;
14
+ export declare function reachOf(text: string): string | null;
@@ -0,0 +1,91 @@
1
+ export const KEY = 'quo';
2
+ // The other reserved key: a proof, for a way in the world sent out itself.
3
+ // A proof is not a capability into a ward, it is a word one door will trade
4
+ // once for an invitation, and it rides in the fragment for the same reason:
5
+ // a query string is in every log and every referer, and one read of it is
6
+ // one way in taken. `<kind>.<token>`, the kind the desk knows it by and the
7
+ // token that door minted.
8
+ export const PROOF = 'proof';
9
+ const HEX = (n) => `[0-9a-f]{${n}}`;
10
+ const FULL = new RegExp(`^(${HEX(128)})\\.(${HEX(64)})\\.(${HEX(64)})$`);
11
+ const PUBLIC = new RegExp(`^${HEX(128)}$`);
12
+ export function format(inv) {
13
+ return inv.heir && inv.secret ? `${inv.ward}.${inv.heir}.${inv.secret}` : inv.ward;
14
+ }
15
+ // The invitation in a fragment, or null. Anything else in the fragment is
16
+ // the page's own and is left alone.
17
+ export function parse(hash) {
18
+ const m = /(?:^#|&)quo=([^&]+)/.exec(hash.startsWith('#') ? hash : `#${hash}`);
19
+ if (!m)
20
+ return null;
21
+ const v = decodeURIComponent(m[1]);
22
+ const full = FULL.exec(v);
23
+ if (full)
24
+ return { ward: full[1], heir: full[2], secret: full[3] };
25
+ return PUBLIC.test(v) ? { ward: v } : null;
26
+ }
27
+ // The proof in a fragment, or null. The kind is a word and the token is
28
+ // whatever the door that minted it wrote, opaque here and never read.
29
+ export function parseProof(hash) {
30
+ const m = new RegExp(`(?:^#|&)${PROOF}=([^&]+)`).exec(hash.startsWith('#') ? hash : `#${hash}`);
31
+ if (!m)
32
+ return null;
33
+ const v = decodeURIComponent(m[1]);
34
+ const at = v.indexOf('.');
35
+ if (at < 1 || at === v.length - 1)
36
+ return null;
37
+ const kind = v.slice(0, at);
38
+ return /^[\w-]{1,40}$/.test(kind) ? { kind, token: v.slice(at + 1) } : null;
39
+ }
40
+ // The fragment without what the dock reserves, so the page keeps whatever
41
+ // else it put there and neither capability is left in the address bar.
42
+ export function strip(hash) {
43
+ const rest = (hash.startsWith('#') ? hash.slice(1) : hash)
44
+ .split('&')
45
+ .filter((p) => !p.startsWith(`${KEY}=`) && !p.startsWith(`${PROOF}=`))
46
+ .join('&');
47
+ return rest ? `#${rest}` : '';
48
+ }
49
+ export function link(page, inv) {
50
+ return `${page}#${KEY}=${format(inv)}`;
51
+ }
52
+ // A world's own way in, as it goes out in a mail or a message: the page of
53
+ // the world it is for, with the proof in the fragment.
54
+ export function proofLink(page, kind, token) {
55
+ return `${page}#${PROOF}=${encodeURIComponent(`${kind}.${token}`)}`;
56
+ }
57
+ // The invitation as a human hands it over: the compact form, the whole link
58
+ // it came in, or the JSON value `quo invite` printed. Wherever a person
59
+ // types or pastes an invitation, this is what reads it, so the three forms
60
+ // are one form everywhere.
61
+ export function invitationOf(text) {
62
+ const t = text.trim();
63
+ const hash = t.indexOf('#');
64
+ const linked = parse(hash === -1 ? `#${KEY}=${t}` : t.slice(hash));
65
+ if (linked?.heir)
66
+ return linked;
67
+ try {
68
+ const v = JSON.parse(t);
69
+ if (v && typeof v === 'object' && typeof v.ward === 'string' && typeof v.heir === 'string' && typeof v.secret === 'string')
70
+ return { ward: v.ward, heir: v.heir, secret: v.secret };
71
+ }
72
+ catch {
73
+ /* not JSON either */
74
+ }
75
+ return null;
76
+ }
77
+ // The reach note in front of an invitation: the URL a link was written on,
78
+ // which for a link between two devices is the rendezvous to dial to reach
79
+ // the one that wrote it. A hint is not a capability, so it travels beside
80
+ // the invitation in the open. Nothing that is not a URL is one.
81
+ export function reachOf(text) {
82
+ const t = text.trim();
83
+ const hash = t.indexOf('#');
84
+ const before = hash === -1 ? '' : t.slice(0, hash);
85
+ try {
86
+ return /^https?:$/.test(new URL(before).protocol) ? before : null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
@@ -0,0 +1,30 @@
1
+ import type { Blueprint, Json } from '@nervur-org/nervur';
2
+ export type Hint = {
3
+ title?: string;
4
+ readOnly?: boolean;
5
+ destructive?: boolean;
6
+ idempotent?: boolean;
7
+ icon?: string;
8
+ };
9
+ export type Look = {
10
+ name?: string;
11
+ logo?: string;
12
+ accent?: string;
13
+ background?: string;
14
+ foreground?: string;
15
+ font?: string;
16
+ radius?: number;
17
+ order?: string[];
18
+ asks?: Record<string, Hint>;
19
+ stands?: string[];
20
+ };
21
+ export declare function sanitise(v: Json | undefined): Look;
22
+ export declare const hint: (l: Look | undefined, name: string) => Hint;
23
+ export type Group = {
24
+ asks?: string[];
25
+ look?: Json;
26
+ page?: string;
27
+ };
28
+ export declare function groups(bp: Blueprint | null): Record<string, Group>;
29
+ export declare function presentation(bp: Blueprint | null): Set<string>;
30
+ export declare function hintFor(bp: Blueprint | null, mine: Look, name: string): Hint;
@@ -0,0 +1,104 @@
1
+ const COLOUR = /^#[0-9a-fA-F]{3,8}$/;
2
+ const FONT = /^[\w\s,'"-]{1,80}$/;
3
+ const LOGO = /^data:image\/(svg\+xml|png|jpeg|webp);base64,[A-Za-z0-9+/=]{1,40000}$/;
4
+ const NAME = /^[^<>&]{1,60}$/;
5
+ const ICON = /^[^<>&"']{1,8}$/;
6
+ const STANDS = 8; // origins a being may stand in for
7
+ const ORIGIN = 200; // characters one of them may be
8
+ const str = (v, re) => (typeof v === 'string' && re.test(v) ? v : undefined);
9
+ // The look as a side may use it: every token held to its shape, everything
10
+ // else gone. A value that is not an object is no look at all.
11
+ export function sanitise(v) {
12
+ if (v === null || v === undefined || typeof v !== 'object' || Array.isArray(v))
13
+ return {};
14
+ const l = {};
15
+ const name = str(v.name, NAME);
16
+ if (name !== undefined)
17
+ l.name = name;
18
+ const logo = str(v.logo, LOGO);
19
+ if (logo !== undefined)
20
+ l.logo = logo;
21
+ for (const k of ['accent', 'background', 'foreground']) {
22
+ const c = str(v[k], COLOUR);
23
+ if (c !== undefined)
24
+ l[k] = c;
25
+ }
26
+ const font = str(v.font, FONT);
27
+ if (font !== undefined)
28
+ l.font = font;
29
+ if (typeof v.radius === 'number' && Number.isFinite(v.radius) && v.radius >= 0 && v.radius <= 40)
30
+ l.radius = Math.round(v.radius);
31
+ if (Array.isArray(v.order))
32
+ l.order = v.order.filter((x) => typeof x === 'string');
33
+ if (Array.isArray(v.stands)) {
34
+ // An origin and never a page: whatever she writes is parsed and reduced
35
+ // to its scheme, host and port, so a path, a query or a fragment she
36
+ // put there cannot narrow what a side vouches for into something a
37
+ // reader would misjudge. Bounded, because a list nobody could read is a
38
+ // list nobody can check.
39
+ const stands = [];
40
+ for (const x of v.stands.slice(0, STANDS)) {
41
+ if (typeof x !== 'string' || x.length > ORIGIN)
42
+ continue;
43
+ try {
44
+ const u = new URL(x);
45
+ if ((u.protocol === 'https:' || u.protocol === 'http:') && !stands.includes(u.origin))
46
+ stands.push(u.origin);
47
+ }
48
+ catch {
49
+ /* not an origin */
50
+ }
51
+ }
52
+ if (stands.length)
53
+ l.stands = stands;
54
+ }
55
+ if (v.asks !== null && typeof v.asks === 'object' && !Array.isArray(v.asks)) {
56
+ const asks = {};
57
+ for (const [ask, h] of Object.entries(v.asks)) {
58
+ if (h === null || typeof h !== 'object' || Array.isArray(h))
59
+ continue;
60
+ const hint = {};
61
+ const title = str(h.title, NAME);
62
+ if (title !== undefined)
63
+ hint.title = title;
64
+ const icon = str(h.icon, ICON);
65
+ if (icon !== undefined)
66
+ hint.icon = icon;
67
+ for (const k of ['readOnly', 'destructive', 'idempotent'])
68
+ if (typeof h[k] === 'boolean')
69
+ hint[k] = h[k];
70
+ if (Object.keys(hint).length)
71
+ asks[ask] = hint;
72
+ }
73
+ if (Object.keys(asks).length)
74
+ l.asks = asks;
75
+ }
76
+ return l;
77
+ }
78
+ // The one hint a side reads first: whether this ask is in the look at all.
79
+ export const hint = (l, name) => l?.asks?.[name] ?? {};
80
+ export function groups(bp) {
81
+ const n = bp?.notes;
82
+ const g = n !== null && n !== undefined && typeof n === 'object' && !Array.isArray(n) ? n.standings : undefined;
83
+ return g !== null && g !== undefined && typeof g === 'object' && !Array.isArray(g) ? g : {};
84
+ }
85
+ // The asks that are presentation and never a form nor a tool: her look, her
86
+ // page, and the page of every standing she carries, named in her notes.
87
+ export function presentation(bp) {
88
+ const p = new Set(['look', 'page']);
89
+ for (const g of Object.values(groups(bp)))
90
+ if (typeof g.page === 'string')
91
+ p.add(g.page);
92
+ return p;
93
+ }
94
+ // The hint for one ask, hers or a standing's: a carried ask is looked up
95
+ // under its bare name in that standing's look.
96
+ export function hintFor(bp, mine, name) {
97
+ for (const [id, g] of Object.entries(groups(bp))) {
98
+ if (!g.asks?.includes(name))
99
+ continue;
100
+ const prefix = `${id}-`;
101
+ return hint(sanitise(g.look), name.startsWith(prefix) ? name.slice(prefix.length) : name);
102
+ }
103
+ return hint(mine, name);
104
+ }
@@ -0,0 +1,14 @@
1
+ import { Being } from '@nervur-org/nervur';
2
+ import type { JsonObject } from '@nervur-org/nervur';
3
+ export declare class Porter extends Being {
4
+ static cells: JsonObject;
5
+ static asks: {
6
+ hello: {
7
+ description: string;
8
+ input: {
9
+ type: string;
10
+ };
11
+ };
12
+ };
13
+ hello(_args: JsonObject): Promise<JsonObject>;
14
+ }
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The bell's public being. She is the one door a phone nobody knows can
3
+ // reach: her asker is `{}` and she can tell, which is what the spec puts a
4
+ // public being there for. A stranger says hello, she boots that device a
5
+ // doorbell of its own and hands back its one way in, and from then on the
6
+ // phone is a named occupant with a heir and a count, which is where
7
+ // anything that must happen once belongs.
8
+ //
9
+ // She needs no owner standing and no root. A being boots a being of her
10
+ // ward and holds a standing to it, which is the whole of what she does.
11
+ //
12
+ // Her answer is safe to repeat, as a public being's must be: saying hello
13
+ // twice is two doorbells and no harm. A stranger who keeps knocking is the
14
+ // harbor's, never hers and never the door's, and the count below is only so
15
+ // that a loop in this session stops somewhere a human can see.
16
+ import { Being, isSilence, isWord, wordOf } from '@nervur-org/nervur';
17
+ const ROOM = 64;
18
+ export class Porter extends Being {
19
+ static cells = { minted: 0 };
20
+ static asks = {
21
+ hello: { description: 'a device asks for a doorbell of its own', input: { type: 'object' } },
22
+ };
23
+ async hello(_args) {
24
+ const minted = typeof this.cells.minted === 'number' ? this.cells.minted : 0;
25
+ if (minted >= ROOM)
26
+ return { error: 'this bell holds no more doorbells' };
27
+ const key = `doorbell:${minted + 1}`;
28
+ const held = await this.boot('Doorbell', key, key);
29
+ if (held === null)
30
+ return { error: 'this bell could not make a doorbell' };
31
+ this.cells.minted = minted + 1;
32
+ // The doorbell hands out its one way in, and only while it has no
33
+ // device. So the porter cannot keep a key to a phone she introduced.
34
+ const out = await this.standings[held]?.ask('claim', {});
35
+ if (out === undefined || isSilence(out))
36
+ return { error: 'the doorbell said nothing' };
37
+ if (isWord(out))
38
+ return { error: wordOf(out) };
39
+ return { doorbell: key, ...out };
40
+ }
41
+ }
@@ -0,0 +1,8 @@
1
+ import type { Asker, OccupantRecord, Stance } from '@nervur-org/nervur';
2
+ import { Avatar } from './avatar.ts';
3
+ export declare class Presence extends Avatar {
4
+ static carries(occupant: OccupantRecord | undefined, _asker: Asker): boolean;
5
+ static skips(id: string): boolean;
6
+ readonly lent: Promise<Record<string, string | null>>;
7
+ constructor(stance: Stance);
8
+ }
@@ -0,0 +1,21 @@
1
+ import { Avatar, PUSHER, USER } from './avatar.js';
2
+ import { FACULTIES, join } from '../dock/index.js';
3
+ export class Presence extends Avatar {
4
+ // Her world's user being sees the box, and nobody else: the way back she
5
+ // handed over at the join is the one occupant who may ask through her.
6
+ static carries(occupant, _asker) {
7
+ return occupant?.id === PUSHER;
8
+ }
9
+ // Her standing at the user being is hers to speak through and never to
10
+ // carry: the user being carries her, so carrying it back would be a
11
+ // loop, and it is the one standing that is not a faculty.
12
+ static skips(id) {
13
+ return id === USER;
14
+ }
15
+ // Which names the box lent her, by name, settled after birth.
16
+ lent;
17
+ constructor(stance) {
18
+ super(stance);
19
+ this.lent = Promise.all(Object.keys(FACULTIES).map(async (name) => [name, await join(this, name)])).then((all) => Object.fromEntries(all));
20
+ }
21
+ }
@@ -0,0 +1,3 @@
1
+ import type { Hosted } from '@nervur-org/nervur/harbor';
2
+ export declare function setupHome(hosted: Hosted, user: string): Promise<void>;
3
+ export declare function setup(hosted: Hosted, user: string, cls?: string): Promise<void>;
@@ -0,0 +1,39 @@
1
+ import { DESK, HOME } from './user.js';
2
+ // The root boots and steps back: it invites an owner on the ward's own pk
3
+ // and the user being knocks for it, so from then on placing a being,
4
+ // minting a way in or removing one is an ask on her, shown by her gate to a
5
+ // device that may mint, and the box is not logged into for it. The same
6
+ // standing a user being of another ward takes to own this one; here it is
7
+ // her own.
8
+ export async function setupHome(hosted, user) {
9
+ const inv = (await hosted.ask('invite', { being: hosted.pk, id: HOME }));
10
+ if ('error' in inv)
11
+ throw new Error(`home: ${inv.error}`);
12
+ const placed = (await hosted.ask('knock', { being: user, id: HOME, invitation: inv }));
13
+ if (placed.taken !== HOME)
14
+ throw new Error(`home: ${placed.error ?? 'she could not take her own door'}`);
15
+ }
16
+ // The ward's home being is a `User` unless the device says which class: an
17
+ // organisation's ward has an org's home being, of the estate's own class,
18
+ // and everything else of setup, the desk and the door, is placed on her the
19
+ // same way.
20
+ export async function setup(hosted, user, cls = 'User') {
21
+ if (!/^[\w.-]+$/.test(user) || user === 'desk')
22
+ throw new Error(`a user is a word, and not desk: ${user}`);
23
+ const boot = async (args) => {
24
+ const out = (await hosted.ask('boot', args));
25
+ if (out.error)
26
+ throw new Error(`init: ${out.error}`);
27
+ };
28
+ await boot({ key: 'desk', class: 'Desk' });
29
+ const pub = (await hosted.ask('public', { key: 'desk' }));
30
+ if (pub.error)
31
+ throw new Error(`init: ${pub.error}`);
32
+ await boot({ key: user, class: cls });
33
+ const inv = (await hosted.ask('invite', { being: user, id: DESK }));
34
+ // the desk's first hello names her, since a being does not know her own key
35
+ const placed = (await hosted.ask('knock', { being: 'desk', id: `user:${user}`, invitation: inv, method: 'hello', args: { name: user } }));
36
+ if (placed.taken !== `user:${user}`)
37
+ throw new Error('init: the desk could not reach the user being');
38
+ await setupHome(hosted, user);
39
+ }
@@ -0,0 +1,32 @@
1
+ import type { Answer, Blueprint, Json, JsonObject, Wanted, WordName } from '@nervur-org/nervur';
2
+ import type { Avatar } from './avatar.ts';
3
+ export type Serving = {
4
+ close(): Promise<void>;
5
+ };
6
+ export type Side = (avatar: Avatar) => Promise<Serving>;
7
+ export type Subject = {
8
+ tools(): Promise<Blueprint | {
9
+ error: string;
10
+ }>;
11
+ call(name: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
12
+ ears: Set<(object: JsonObject) => void>;
13
+ };
14
+ export type Word = {
15
+ word: 'object';
16
+ value: Json;
17
+ } | {
18
+ word: 'error';
19
+ value: JsonObject;
20
+ } | {
21
+ word: 'silence';
22
+ } | {
23
+ word: 'unreached';
24
+ } | {
25
+ word: 'quo';
26
+ name: WordName;
27
+ };
28
+ export declare function word(answer: Answer): Word;
29
+ export declare const SILENCE_TEXT = "silence: the work may have happened, and no answer came back";
30
+ export declare const UNREACHED_TEXT = "unreached: nothing was delivered; asking again is safe";
31
+ export declare const WORD_TEXT: Record<WordName, string>;
32
+ export declare const wordText: (name: WordName) => string;
@@ -0,0 +1,37 @@
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
+ export function word(answer) {
10
+ if (isSilence(answer))
11
+ return { word: 'silence' };
12
+ if (isUnreached(answer))
13
+ return { word: 'unreached' };
14
+ if (isWord(answer))
15
+ return { word: 'quo', name: wordOf(answer) };
16
+ if (answer !== null && typeof answer === 'object' && !Array.isArray(answer) && 'error' in answer)
17
+ return { word: 'error', value: answer };
18
+ return { word: 'object', value: answer };
19
+ }
20
+ // What a side says about the two words that carry nothing, so that whoever
21
+ // reads them can decide: silence promises nothing, unreached promises that
22
+ // nothing was delivered.
23
+ export const SILENCE_TEXT = 'silence: the work may have happened, and no answer came back';
24
+ export const UNREACHED_TEXT = 'unreached: nothing was delivered; asking again is safe';
25
+ // And the ward's other words, each with what a side says of it.
26
+ export const WORD_TEXT = {
27
+ unreached: UNREACHED_TEXT,
28
+ late: 'late: the wait ran out; the work may have happened',
29
+ invitation: 'invitation: that is not an invitation',
30
+ dropped: 'dropped: the standing is gone',
31
+ removed: 'removed: the far being removed you',
32
+ absent: 'absent: the far being did not come back this run',
33
+ unannounced: 'unannounced: the knock announced no key of its own',
34
+ repeated: 'repeated: that number was already honoured',
35
+ threw: 'threw: the far being threw',
36
+ };
37
+ export const wordText = (name) => WORD_TEXT[name];