@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,527 @@
1
+ import { screenSide } from './screen.js';
2
+ import { domSurface } from './dom.js';
3
+ import { guest } from './guest.js';
4
+ import { door, held as heldPage } from './door.js';
5
+ import { USER } from '../beings/avatar.js';
6
+ import { DOCK, CONSOLE, NOTIFICATIONS, settled } from '../dock/index.js';
7
+ import { fresh, joined, localName, ordered, relations, world } from './worlds.js';
8
+ import { giveWorld, takeWorld } from './move.js';
9
+ import { invitationOf, reachOf } from '../beings/link.js';
10
+ import { doorbell, drained, told, notice as noticeOf } from './wake.js';
11
+ // The fetch, as every terrain but a test does it: the world's address, its
12
+ // own origin asking, and JSON. A world answers this to an app and to
13
+ // nobody else, `web.ts`.
14
+ export const learn = async (at) => {
15
+ const r = await fetch(at, { headers: { accept: 'application/json' } });
16
+ if (!r.ok)
17
+ throw new Error(`${at} answered ${r.status}`);
18
+ return (await r.json());
19
+ };
20
+ const el = (tag, text = '', attrs = {}) => {
21
+ const e = document.createElement(tag);
22
+ if (text)
23
+ e.textContent = text;
24
+ for (const [k, v] of Object.entries(attrs))
25
+ e.setAttribute(k, v);
26
+ return e;
27
+ };
28
+ export async function app(o) {
29
+ const look = o.learn ?? learn;
30
+ const nav = el('nav', '', { class: 'worlds nv-strip' });
31
+ const status = el('p', 'booting', { class: 'state nv-t-quiet' });
32
+ const screen = el('div');
33
+ o.root.append(nav, status, screen);
34
+ const say = (s) => (status.textContent = s);
35
+ // A device is one harbor, and this browser may already hold it in
36
+ // another tab. That tab is the device; this one is the held page, as a
37
+ // world's tab is, and it offers nothing that takes the lock.
38
+ try {
39
+ await o.harbor.boot();
40
+ }
41
+ catch {
42
+ status.remove();
43
+ screen.innerHTML = heldPage({ host: location.host });
44
+ const none = async () => { };
45
+ return { show: none, open: none, woken: async () => [], worlds: () => [], close: none };
46
+ }
47
+ // Every world this device holds is dialed, not only the one on screen: a
48
+ // world reaches a device by the socket the device opened, and a device
49
+ // that dialed only what it is looking at could be woken by nothing else.
50
+ const dialed = new Set();
51
+ const dial = (quo) => {
52
+ if (dialed.has(quo))
53
+ return;
54
+ dialed.add(quo);
55
+ o.harbor.dial(quo);
56
+ };
57
+ let list = o.kept.read();
58
+ let showing = null;
59
+ let side = null;
60
+ // How this device is woken, asked of the terrain and never decided here.
61
+ let heard = null;
62
+ const listen = async () => {
63
+ if (!o.ear)
64
+ return null;
65
+ try {
66
+ heard = await o.ear();
67
+ }
68
+ catch {
69
+ heard = null; // a device whose ear will not answer is a device with no doorbell, and everything else stands
70
+ }
71
+ return heard;
72
+ };
73
+ // This device's doorbell, at the bell the build named. It is taken once
74
+ // and kept, and the platform's token goes to it and to nowhere else.
75
+ let bellAt = null;
76
+ // Telling one world how to wake this device, which is `wake.ts`'s and not
77
+ // the shell's: the shell holds the doorbell and the ear, and says where
78
+ // the ward is saved.
79
+ const tell = async (ward, rel) => {
80
+ await told(rel.avatar, bellAt, heard);
81
+ await ward.save(); // her keys rotated on those asks
82
+ };
83
+ // What the switcher switches between is the human's versions of
84
+ // themselves, one world each and a ward each: an employee at one world, a
85
+ // consultant at a second, family at a third, and never one of them
86
+ // knowing about another. So a device with one version has no switcher at
87
+ // all, since a control over one thing can only do nothing. It is taken
88
+ // out of the page rather than hidden, because the stylesheet lays it out
89
+ // by class and a class beats the hidden attribute.
90
+ //
91
+ // Its last entry is never a world: it is this device, the box's own ward
92
+ // shown through the Console, where a human sees what the box lends and
93
+ // leaves a world. A box that lends nothing has no such page.
94
+ const switcher = () => {
95
+ nav.replaceChildren();
96
+ const device = hasDevice();
97
+ if (list.length + (device ? 1 : 0) < 2) {
98
+ nav.remove();
99
+ return;
100
+ }
101
+ if (!nav.isConnected)
102
+ o.root.insertBefore(nav, o.root.firstChild);
103
+ for (const w of ordered(list)) {
104
+ const b = el('button', w.name || w.pk.slice(0, 8), { type: 'button', class: w.home ? 'home nv-chip' : 'nv-chip' });
105
+ if (w.pk === showing)
106
+ b.setAttribute('aria-current', 'true');
107
+ b.onclick = () => void show(w.pk).catch((e) => say(String(e)));
108
+ nav.append(b);
109
+ }
110
+ if (device) {
111
+ const b = el('button', 'this device', { type: 'button', class: 'device nv-chip' });
112
+ if (showing === DEVICE)
113
+ b.setAttribute('aria-current', 'true');
114
+ b.onclick = () => void showDevice().catch((e) => say(String(e)));
115
+ nav.append(b);
116
+ }
117
+ };
118
+ const remember = (w) => {
119
+ list = joined(list, w);
120
+ o.kept.write(list);
121
+ switcher();
122
+ };
123
+ // One world on screen: its ward, whoever was in it last, and her page.
124
+ // A world whose address will not answer is still a world: the ward and
125
+ // the standings are here, and the switcher keeps it.
126
+ const show = async (pk) => {
127
+ const w = list.find((x) => x.pk === pk);
128
+ if (!w)
129
+ return void say('no such world');
130
+ showing = pk;
131
+ await side?.close();
132
+ side = null;
133
+ screen.replaceChildren();
134
+ held.remove();
135
+ if (!status.isConnected)
136
+ o.root.insertBefore(status, screen);
137
+ say(`reaching ${w.name || w.at}`);
138
+ switcher();
139
+ let cfg = null;
140
+ try {
141
+ cfg = await look(w.at);
142
+ dial(cfg.quo);
143
+ }
144
+ catch (e) {
145
+ say(`${w.name || w.at} did not answer: ${e instanceof Error ? e.message : String(e)}`);
146
+ }
147
+ const ward = await world(o.harbor, pk);
148
+ const named = cfg ? Object.entries(cfg.wards).find(([, v]) => v.pk === pk) : undefined;
149
+ if (cfg && named)
150
+ remember({ pk, at: w.at, name: named[0] });
151
+ const rels = relations(ward);
152
+ const back = rels[rels.length - 1];
153
+ if (back) {
154
+ const had = await back.avatar.tools();
155
+ await ward.save(); // her keys rotated on that ask, whatever it answered
156
+ if ('asks' in had)
157
+ return inside(ward, back, `in, at ${w.name || named?.[0] || w.at}`, had);
158
+ say(`not in (${had.error})`);
159
+ }
160
+ await atDoor(ward, pk, w, named?.[1]?.public === true);
161
+ };
162
+ const inside = async (ward, rel, notice, bp) => {
163
+ // Whether this world is a home is the user being's word, in her notes,
164
+ // and the switcher puts home first
165
+ const w = list.find((x) => x.pk === showing);
166
+ if (w)
167
+ remember({ ...w, home: isHome(bp) });
168
+ await side?.close();
169
+ // A device has no surface of its own, and nothing of it is handed to a
170
+ // world: what this box can do is beings of its own ward, and the being
171
+ // of this world who lives on this box holds standings at them. What a
172
+ // world reaches is her, on the standing it already holds.
173
+ const note = notice;
174
+ status.remove();
175
+ screen.replaceChildren();
176
+ // The world's page address travels with the side, since that is what an
177
+ // invitation she answers becomes a link on: a being knows no routes and
178
+ // the side is where one is composed, in an app exactly as in a tab.
179
+ side = await screenSide(rel.avatar, domSurface(screen), {
180
+ after: () => ward.save(),
181
+ notice: note,
182
+ // A world that ended the relation is over, so the list of worlds is
183
+ // put up with its way out in reach. Nothing is dropped: one answer at
184
+ // one moment cannot delete a ward, and the far world is never asked.
185
+ ended: async () => {
186
+ leaving();
187
+ screen.after(held);
188
+ },
189
+ ...(w ? { at: w.at } : {}),
190
+ });
191
+ // This world learns how to wake this device, and hands over whatever it
192
+ // pushed while nobody could be reached. Both are ordinary asks on this
193
+ // relation, sealed like every other, and each object lands on the avatar
194
+ // exactly where a push would have landed.
195
+ await tell(ward, rel);
196
+ if (showing && showing !== DEVICE)
197
+ await drained(o.harbor, [showing]);
198
+ };
199
+ // At the door of a world this device has no standing in: the public
200
+ // being's page when there is one, and a form whose answer is an
201
+ // invitation is the way in; the door page when there is nobody, since
202
+ // then a link is the only way and there is nothing to type.
203
+ const atDoor = async (ward, pk, w, isPublic) => {
204
+ if (!isPublic) {
205
+ status.remove();
206
+ screen.innerHTML = door({ world: w.name || undefined, host: hostOf(w.at) });
207
+ return;
208
+ }
209
+ say(`at the door of ${w.name || hostOf(w.at)}`);
210
+ const rel = await fresh(ward);
211
+ const gate = guest(rel.avatar, pk);
212
+ const s = await screenSide(gate, domSurface(screen), {
213
+ after: () => ward.save(),
214
+ notice: `a guest at ${w.name || hostOf(w.at)}: what she shows strangers`,
215
+ at: w.at,
216
+ admit: async (inv) => {
217
+ await s.close();
218
+ await admit(ward, inv, `in, as a guest of ${w.name || hostOf(w.at)}`);
219
+ },
220
+ });
221
+ side = s;
222
+ };
223
+ // The box's own ward, as its harbor stood it: the Console carrying every
224
+ // faculty this box lends. A box that lends nothing stands none, and there
225
+ // is no page of its own to show.
226
+ const deviceWard = async () => {
227
+ const ward = o.harbor.wards.get(DOCK);
228
+ if (!ward || !ward.being(CONSOLE))
229
+ return null;
230
+ return ward;
231
+ };
232
+ const hasDevice = () => o.harbor.wards.get(DOCK)?.being(CONSOLE) !== undefined;
233
+ // The device shown by itself: the last entry of the switcher, and the
234
+ // whole screen of a device that has joined no world. A fresh install has
235
+ // no world's page to carry her forms, and a first screen that says only
236
+ // "open a link someone sent you" is an app with permissions in its
237
+ // manifest and nothing that ever asks for them, which is a store
238
+ // reviewer's first question and a human's second.
239
+ //
240
+ // She is shown by the ordinary screen and by no other way: one avatar in
241
+ // the box's own ward holding her standing under the name a world's user
242
+ // being would stand under, so the side sees what it always sees. The root
243
+ // places it, because the root is the box.
244
+ const deviceScreen = async () => {
245
+ const ward = await deviceWard();
246
+ if (!ward)
247
+ return null;
248
+ const had = relations(ward)[0];
249
+ if (had)
250
+ return { ward, rel: had };
251
+ const rel = await fresh(ward);
252
+ const inv = await ward.ask('invite', { being: CONSOLE, id: rel.key });
253
+ if (!inv || typeof inv !== 'object')
254
+ throw new Error('this device is already held under that name');
255
+ const took = (await ward.ask('knock', { being: rel.key, id: USER, invitation: inv }));
256
+ if (took.taken !== USER)
257
+ throw new Error(took.error ?? 'the shell could not take this device');
258
+ await ward.save();
259
+ return { ward, rel };
260
+ };
261
+ // The device's page: what the box lends, as forms that ask the faculty
262
+ // and show its answer, a refusal included; the worlds this device is in,
263
+ // each with a way out; and the one sentence about custody a human needs.
264
+ const showDevice = async () => {
265
+ let found = null;
266
+ try {
267
+ found = await deviceScreen();
268
+ }
269
+ catch (e) {
270
+ return void say(`this device would not answer: ${e instanceof Error ? e.message : String(e)}`);
271
+ }
272
+ if (!found)
273
+ return void say('this device lends nothing');
274
+ const { ward, rel } = found;
275
+ showing = DEVICE;
276
+ await side?.close();
277
+ side = null;
278
+ screen.replaceChildren();
279
+ if (!status.isConnected)
280
+ o.root.insertBefore(status, screen);
281
+ say('');
282
+ switcher();
283
+ side = await screenSide(rel.avatar, domSurface(screen), { after: () => ward.save(), notice: CUSTODY });
284
+ leaving();
285
+ screen.after(held);
286
+ };
287
+ // The worlds this device is in, each with a way out and a way to another
288
+ // device. Leaving is the local ward dropped whole, seed and every
289
+ // standing in it, and the world forgotten from the list: the far world
290
+ // is never asked and never told, as WORLDS says of a lost phone, and it
291
+ // sees one device gone. Moving is the Courier: this device offers, the
292
+ // human carries the offer to the other device, and this copy goes the
293
+ // moment the other one says it landed.
294
+ const held = el('section', '', { class: 'held' });
295
+ const offer = el('p', '', { class: 'invitation' });
296
+ let giving = null;
297
+ const stopGiving = async () => {
298
+ const g = giving;
299
+ giving = null;
300
+ offer.textContent = '';
301
+ await g?.close();
302
+ leaving();
303
+ };
304
+ const give = async (w) => {
305
+ if (giving)
306
+ return stopGiving();
307
+ const cfg = await look(w.at);
308
+ dial(cfg.quo);
309
+ giving = await giveWorld(o.harbor, w, cfg.quo);
310
+ offer.textContent = giving.offer;
311
+ leaving();
312
+ void giving.moved.then(async () => {
313
+ // it is on the other device now, and this one is out of the world
314
+ giving = null;
315
+ offer.textContent = '';
316
+ list = list.filter((x) => x.pk !== w.pk);
317
+ o.kept.write(list);
318
+ leaving();
319
+ switcher();
320
+ });
321
+ };
322
+ const leaving = () => {
323
+ held.replaceChildren();
324
+ if (list.length === 0)
325
+ return;
326
+ held.append(el('p', 'the worlds this device is in', { class: 'quiet' }));
327
+ for (const w of list) {
328
+ const name = w.name || w.pk.slice(0, 8);
329
+ const b = el('button', `leave ${name}`, { type: 'button', 'data-leave': w.pk, class: 'nv-button', 'data-variant': 'danger' });
330
+ b.onclick = () => void leave(w.pk).catch((e) => say(String(e)));
331
+ const m = el('button', giving ? 'stop offering it' : `move ${name} to another device`, { type: 'button', 'data-give': w.pk, class: 'nv-button', 'data-variant': 'quiet' });
332
+ m.onclick = () => void give(w).catch((e) => say(String(e)));
333
+ held.append(b, m);
334
+ }
335
+ held.append(offer);
336
+ };
337
+ const leave = async (pk) => {
338
+ const name = localName(pk);
339
+ if (o.harbor.wards.has(name))
340
+ await o.harbor.drop(name);
341
+ list = list.filter((w) => w.pk !== pk);
342
+ o.kept.write(list);
343
+ leaving();
344
+ switcher();
345
+ };
346
+ const cold = async () => {
347
+ await showDevice();
348
+ say('no worlds yet: open a link someone sent you');
349
+ };
350
+ const admit = async (ward, inv, notice) => {
351
+ say('knocking');
352
+ const rel = await fresh(ward);
353
+ let got = await rel.avatar.join(inv);
354
+ await ward.save(); // the knock went through her own door, which the harbor never sees
355
+ if (!('asks' in got))
356
+ return void say(`not in: ${got.error}`);
357
+ // What came back is the far being as she was during the knock. If
358
+ // anything on this device was still landing then, she read a device that
359
+ // can do nothing and carries that, and the empty ask is the only way
360
+ // anyone learns a shape moved: nothing pushes. So the page asks once
361
+ // more, with this box quiet, and paints what is there rather than what
362
+ // was there a moment ago. A page paints once, so asking once more here
363
+ // is the difference between a device with its faculties on screen and a
364
+ // device that looks empty until the person navigates away and back.
365
+ await settled();
366
+ const now = await rel.avatar.tools();
367
+ if ('asks' in now)
368
+ got = now;
369
+ await inside(ward, rel, notice, got);
370
+ };
371
+ // A link, whether the app was opened by it or handed it while running.
372
+ // The invitation names its ward by pk, so the address in the link is
373
+ // where to reach that world and never which world it is.
374
+ const open = async (at, invitation) => {
375
+ let cfg = null;
376
+ try {
377
+ cfg = await look(at);
378
+ }
379
+ catch (e) {
380
+ return void say(`${at} did not answer: ${e instanceof Error ? e.message : String(e)}`);
381
+ }
382
+ dial(cfg.quo);
383
+ const pk = invitation?.ward ?? (cfg.ward ? cfg.wards[cfg.ward]?.pk : undefined);
384
+ if (!pk)
385
+ return void say('that link names no world');
386
+ const name = Object.entries(cfg.wards).find(([, v]) => v.pk === pk)?.[0] ?? '';
387
+ remember({ pk, at, name });
388
+ if (!invitation)
389
+ return show(pk);
390
+ showing = pk;
391
+ switcher();
392
+ if (!status.isConnected)
393
+ o.root.insertBefore(status, screen);
394
+ screen.replaceChildren();
395
+ await admit(await world(o.harbor, pk), invitation, 'in, by the link');
396
+ };
397
+ // Taking a world from another device. The device that holds the world
398
+ // shows an offer, an invitation next to the rendezvous to reach it
399
+ // through; the human carries it here, by eye or by camera, and the ward
400
+ // arrives whole: the avatars, the standings, everything won there, with
401
+ // the far world never asked and never told. This device dials the note
402
+ // first, because a device reaches a pk only through a listener it holds
403
+ // a socket to, and one that has joined no world dials nothing at all.
404
+ const taking = el('form', '', { class: 'move' });
405
+ const field = el('input', '', { name: 'offer', placeholder: 'the offer the other device shows', autocomplete: 'off', spellcheck: 'false' });
406
+ const said = el('p', '', { class: 'quiet' });
407
+ taking.append(el('label', 'take a world from another device'), field, el('button', 'take it', { type: 'submit', class: 'nv-button', 'data-variant': 'quiet' }), said);
408
+ o.root.append(taking);
409
+ taking.onsubmit = (e) => {
410
+ e.preventDefault();
411
+ const inv = invitationOf(field.value);
412
+ if (!inv)
413
+ return void (said.textContent = 'that is not an offer');
414
+ const note = reachOf(field.value);
415
+ if (note)
416
+ dial(note);
417
+ said.textContent = 'taking it…';
418
+ void (async () => {
419
+ const out = await takeWorld(o.harbor, inv);
420
+ if ('error' in out)
421
+ return void (said.textContent = out.error);
422
+ said.textContent = '';
423
+ field.value = '';
424
+ remember(out.world);
425
+ await show(out.world.pk);
426
+ })().catch((why) => (said.textContent = String(why)));
427
+ };
428
+ // The device is back. A ring carries nothing, so waking is three moves and
429
+ // the harbor already made the first: it told its dialers, since a socket
430
+ // that died while the device slept died silently. What is left is to say
431
+ // the token again, because a platform may have handed out a new one, and
432
+ // to ask every world what it missed.
433
+ //
434
+ // Every world and not only the one on screen: a device is woken for
435
+ // whichever world had something to say, and one that drained only what it
436
+ // was looking at would leave the rest waiting for a second wake that may
437
+ // never come.
438
+ const woken = async () => {
439
+ await listen();
440
+ // The token goes to this device's doorbell and to nobody else, which is
441
+ // the whole of a rotation: one write, and no world is told. A world
442
+ // that has no doorbell yet, because it was down when this device joined
443
+ // it, is given one here.
444
+ if (heard)
445
+ await bellAt?.keep(heard);
446
+ for (const w of list) {
447
+ const ward = o.harbor.wards.get(localName(w.pk));
448
+ if (!ward)
449
+ continue;
450
+ for (const rel of relations(ward))
451
+ await tell(ward, rel);
452
+ }
453
+ const got = await drained(o.harbor, list.map((w) => w.pk));
454
+ // What the human reads, composed here from what crossed sealed. Only
455
+ // when nobody was looking: a page that is open already shows every one
456
+ // of these where the push would have landed, and a banner over it would
457
+ // be the same news twice.
458
+ if (got.length && globalThis.document?.visibilityState !== 'visible')
459
+ await shown(noticeOf(got));
460
+ return got;
461
+ };
462
+ // The line the human reads, shown by the box's own notifications being.
463
+ // A box that lends none shows nothing, and a refusal is hers to answer;
464
+ // the shell asks and does not read the answer, since the news is already
465
+ // on the page where the push landed.
466
+ const shown = async (line) => {
467
+ const dock = o.harbor.wards.get(DOCK);
468
+ if (!dock)
469
+ return;
470
+ await dock.ask('ask', { being: NOTIFICATIONS, method: 'notify', args: line }).catch(() => { });
471
+ };
472
+ // The device ward before anything is dialed: a harbor announces the wards
473
+ // it hosts when it opens a socket, and a ward made after that is a pk the
474
+ // far world cannot knock. A device whose ward will not stand is a device
475
+ // with no grounds and a shell that works in every other way, so nothing
476
+ // here stops the app; the cold screen is where that is said out loud.
477
+ try {
478
+ await deviceWard();
479
+ }
480
+ catch {
481
+ /* said by the cold screen, which is the one place a device speaks for itself */
482
+ }
483
+ await listen();
484
+ // The bell is dialed like a world, because a device reaches a pk only
485
+ // through a listener it holds a socket to; then the doorbell, taken the
486
+ // first time this device ever ran and kept from then on, and the
487
+ // platform's token said to it.
488
+ if (o.bell) {
489
+ dial(o.bell.at);
490
+ bellAt = await doorbell(o.harbor, o.bell);
491
+ if (heard)
492
+ await bellAt?.keep(heard);
493
+ }
494
+ switcher();
495
+ if (o.opened)
496
+ await open(o.opened.at, o.opened.invitation);
497
+ else if (list.length)
498
+ await show(list[list.length - 1].pk);
499
+ else
500
+ await cold();
501
+ return {
502
+ show,
503
+ open,
504
+ woken,
505
+ worlds: () => list,
506
+ close: async () => {
507
+ await side?.close();
508
+ side = null;
509
+ },
510
+ };
511
+ }
512
+ // What the switcher shows when the device's own page is up: not a pk, and
513
+ // never a world.
514
+ const DEVICE = 'device';
515
+ // A world is a home when the being reached there says so: the user being
516
+ // writes `home` in her notes, and no other being does.
517
+ const isHome = (bp) => bp.notes !== null && typeof bp.notes === 'object' && !Array.isArray(bp.notes) && bp.notes.home === true;
518
+ // The one sentence about custody a human needs, on the device's page.
519
+ const CUSTODY = 'this device: what it lends, and the worlds it is in. A reinstall starts fresh, so a world that holds nothing of you elsewhere must invite you again.';
520
+ const hostOf = (at) => {
521
+ try {
522
+ return new URL(at).host;
523
+ }
524
+ catch {
525
+ return at;
526
+ }
527
+ };
@@ -0,0 +1,10 @@
1
+ export type Apps = {
2
+ apple: string[];
3
+ android: {
4
+ package: string;
5
+ fingerprints: string[];
6
+ }[];
7
+ };
8
+ export declare const APPLE = "/.well-known/apple-app-site-association";
9
+ export declare const ANDROID = "/.well-known/assetlinks.json";
10
+ export declare function association(apps: Apps | null | undefined, path: string, notApp?: readonly string[]): string | null;
@@ -0,0 +1,29 @@
1
+ // The two paths, which are the platforms' and not ours.
2
+ export const APPLE = '/.well-known/apple-app-site-association';
3
+ export const ANDROID = '/.well-known/assetlinks.json';
4
+ // The claim `path` should answer with, or `null` when this hostname claims
5
+ // no app there: no apps at all, none of that platform, or a path that is
6
+ // neither file. `null` is a 404 and not an empty file, because an empty
7
+ // claim and no claim mean the same thing to a phone and only one of them
8
+ // is honest about a hostname that ships no app.
9
+ //
10
+ // `notApp` is the paths on this hostname that are the browser's even when
11
+ // the app is installed. A world's route has one, the credential exchange,
12
+ // which belongs in the browser that started it; the app's own domain has
13
+ // none, since it has no pages at all.
14
+ export function association(apps, path, notApp = []) {
15
+ if (!apps)
16
+ return null;
17
+ if (path === ANDROID) {
18
+ if (!apps.android.length)
19
+ return null;
20
+ return JSON.stringify(apps.android.map((a) => ({ relation: ['delegate_permission/common.handle_all_urls'], target: { namespace: 'android_app', package_name: a.package, sha256_cert_fingerprints: a.fingerprints } })));
21
+ }
22
+ if (path === APPLE) {
23
+ if (!apps.apple.length)
24
+ return null;
25
+ const components = [...notApp.map((p) => ({ '/': p, exclude: true })), { '/': '/*' }];
26
+ return JSON.stringify({ applinks: { details: [{ appIDs: apps.apple, components }] } });
27
+ }
28
+ return null;
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { Surface } from './screen.ts';
2
+ export declare function domSurface(root: HTMLElement): Surface;
@@ -0,0 +1,15 @@
1
+ export function domSurface(root) {
2
+ const s = { show: (html) => (root.innerHTML = html), onSubmit: undefined };
3
+ root.addEventListener('submit', (ev) => {
4
+ const f = ev.target;
5
+ const ask = f?.dataset.ask;
6
+ if (!f || ask === undefined)
7
+ return;
8
+ ev.preventDefault();
9
+ const raw = {};
10
+ for (const [k, v] of new FormData(f))
11
+ raw[k] = typeof v === 'string' ? v : '';
12
+ s.onSubmit?.(ask, raw);
13
+ });
14
+ return s;
15
+ }
@@ -0,0 +1,19 @@
1
+ import { type Node, type Painter } from './tree.ts';
2
+ export type Door = {
3
+ world?: string;
4
+ host: string;
5
+ };
6
+ export declare function doorTree(at: Door): Node;
7
+ export declare function door(at: Door): string;
8
+ export declare function heldTree(at: Door): Node;
9
+ export declare function held(at: Door): string;
10
+ export declare const POLICY = "default-src 'none'; style-src 'unsafe-inline'; img-src data:; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
11
+ export declare const ours: Painter;
12
+ export type Landed = {
13
+ world?: string;
14
+ host: string;
15
+ at?: string;
16
+ ok: boolean;
17
+ };
18
+ export declare function landedTree(l: Landed): Node;
19
+ export declare function landed(l: Landed): string;