@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
package/harbor/quo.ts ADDED
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The quo. route: the door sealed bytes arrive at, the same route on every
3
+ // terrain. POST `/quo/<pk>` carries one ask to a pk this harbor holds or
4
+ // holds a socket for; bytes from here go to an own door or a held socket,
5
+ // never onward. A URL of this route is the base of every reach.
6
+ //
7
+ // It is `Request` in and `Response` out and names no runtime, so the daemon
8
+ // mounts it under `/quo` behind the proxy that maps the quo. hostname and the
9
+ // worker at the edge mounts it under its own prefix.
10
+ //
11
+ // The listener half of the socket reach is not here, because holding a socket
12
+ // is the one thing no two runtimes do alike: `ws` on Node, at the daemon's
13
+ // own door in `http.ts`, and a `WebSocketPair` at the edge, in the harbor
14
+ // there. What a dialer's socket becomes once it is held is the library's
15
+ // `Socket` on both.
16
+ import { SUITE } from '@nervur-org/nervur/harbor';
17
+ import { readAll, type Handler } from './http.ts';
18
+
19
+ // What this route asks of the harbor it answers for: one ask delivered to a
20
+ // pk, which is a door here or a socket held here. Nothing else.
21
+ export type Deliver = { deliver(pk: string, bytes: Uint8Array): Promise<Uint8Array | undefined> };
22
+
23
+ // The route is public and carries sealed bytes, so any origin may POST
24
+ // to it: a tab on a world's web. reaching another world's quo. is the
25
+ // ordinary case, and a binary POST needs the preflight answered.
26
+ export const OPEN = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST, OPTIONS', 'access-control-allow-headers': 'content-type, quo-suite' };
27
+
28
+ export function quoRoute(harbor: Deliver): Handler {
29
+ return async (req, rest) => {
30
+ if (req.method === 'OPTIONS') return new Response(null, { status: 204, headers: OPEN });
31
+ // The wire suite the caller speaks. Absent is this one, because a caller
32
+ // older than the header is this one. Anything else this door cannot open
33
+ // and says so as nothing delivered, rather than taking bytes that will
34
+ // never open and answering a silence that names no reason.
35
+ const suite = req.headers.get('quo-suite');
36
+ if (suite !== null && suite !== String(SUITE)) return Response.json({ error: `this door speaks wire suite ${SUITE}` }, { status: 404, headers: OPEN });
37
+ const pk = rest.replace(/^\//, '');
38
+ if (req.method !== 'POST' || !/^[0-9a-f]{128}$/.test(pk)) return Response.json({ error: 'POST /quo/<pk>' }, { status: 404, headers: OPEN });
39
+ const raw = await readAll(req);
40
+ const back = raw === undefined ? undefined : await harbor.deliver(pk, raw);
41
+ if (back === undefined) return Response.json({ error: 'no reach for that pk' }, { status: 404, headers: OPEN });
42
+ return new Response(new Uint8Array(back), { headers: { 'content-type': 'application/octet-stream', ...OPEN } });
43
+ };
44
+ }
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // How a class file the harbor folder holds finds its packages. The folder
3
+ // is the device's, not a package: it has no node_modules, and a class in it
4
+ // that imports `@nervur-org/nervur` would otherwise fail to resolve from where
5
+ // it sits. This is a module resolution hook, registered once by the disk
6
+ // harbor: a bare specifier that does not resolve from a file outside any
7
+ // node_modules is resolved again from where the dock itself is installed,
8
+ // so a class file sees exactly the packages the daemon sees, the library
9
+ // and the dock's own, and one copy of each.
10
+ type Context = { parentURL?: string; conditions: string[]; importAttributes: Record<string, string> };
11
+ type Resolved = { url: string; format?: string | null; shortCircuit?: boolean };
12
+ type Next = (specifier: string, context: Context) => Promise<Resolved>;
13
+
14
+ let parent = '';
15
+
16
+ export function initialize(data: { parent: string }): void {
17
+ parent = data.parent;
18
+ }
19
+
20
+ const bare = (s: string) => !s.startsWith('.') && !s.startsWith('/') && !/^[a-z]+:/i.test(s);
21
+
22
+ export async function resolve(specifier: string, context: Context, next: Next): Promise<Resolved> {
23
+ try {
24
+ return await next(specifier, context);
25
+ } catch (e) {
26
+ const missing = (e as { code?: string }).code === 'ERR_MODULE_NOT_FOUND';
27
+ const from = context.parentURL ?? '';
28
+ if (missing && parent && bare(specifier) && from.startsWith('file:') && !from.includes('/node_modules/')) return next(specifier, { ...context, parentURL: parent });
29
+ throw e;
30
+ }
31
+ }
package/harbor/seal.ts ADDED
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The seal: what a store does to a ward's bytes before they rest on a data
3
+ // store, under one key the secret store holds. One piece with three users,
4
+ // the edge under the platform's secret, the daemon under a key from its
5
+ // environment, the phone under a key from the Keychain. AES-GCM under a
6
+ // 32-byte key, a fresh nonce each time, nonce and ciphertext together as
7
+ // hex. WebCrypto only, so it runs wherever a store does.
8
+ import { arithmetic } from '@nervur-org/nervur/ward';
9
+
10
+ const { hex, unhex } = arithmetic;
11
+ // Bytes as the platform's crypto wants them: over a plain ArrayBuffer.
12
+ const plain = (b: Uint8Array): Uint8Array<ArrayBuffer> => new Uint8Array(b);
13
+
14
+ export async function sealKey(secret: string): Promise<CryptoKey> {
15
+ if (!/^[0-9a-f]{64}$/.test(secret)) throw new Error('a seal key is 32 bytes as hex');
16
+ return crypto.subtle.importKey('raw', plain(unhex(secret)), 'AES-GCM', false, ['encrypt', 'decrypt']);
17
+ }
18
+ export async function seal(key: CryptoKey, bytes: Uint8Array): Promise<string> {
19
+ const iv = crypto.getRandomValues(new Uint8Array(12));
20
+ const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plain(bytes)));
21
+ return hex(iv) + hex(ct);
22
+ }
23
+ export async function open(key: CryptoKey, sealed: string): Promise<Uint8Array> {
24
+ const b = plain(unhex(sealed));
25
+ return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: b.subarray(0, 12) }, key, b.subarray(12)));
26
+ }
@@ -0,0 +1,183 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The sealed store: the store an app holds on a device in a hand, the phone
3
+ // and the desk alike. One key per harbor in the platform's secret store,
4
+ // this device only, so it never travels in a backup; one sealed file per
5
+ // ward, seed, partition and record as one blob, in a folder no backup
6
+ // copies. The seal is `seal.ts`, the same piece the daemon and the edge use.
7
+ //
8
+ // <files>/<harbor>/wards/<name>.sealed
9
+ // <files>/<harbor>/reach.json
10
+ // <secrets>: quo-<harbor> 32 bytes as hex
11
+ //
12
+ // Custody is the backup question, and opening the store answers it: a key
13
+ // with no folder is a reinstall, and the stale key is deleted; a folder
14
+ // with no key is a restore to another device, and the unreadable files are
15
+ // deleted. Either way the harbor starts fresh and there is never a twin.
16
+ //
17
+ // What a platform gives is two small things, files and secrets, and the
18
+ // rest is here once: the phone hands its plugins, the desk hands its core,
19
+ // and the memory harbor hands a map, which is where the rule is proven.
20
+ import { arithmetic } from '@nervur-org/nervur/ward';
21
+ import type { Kept, Store, WardRecord } from '@nervur-org/nervur/harbor';
22
+ import { sealKey, seal, open } from './seal.ts';
23
+
24
+ // A folder the platform keeps for this app and no backup copies. Paths are
25
+ // relative to it and forward-slashed; a platform maps them to its own.
26
+ export type Files = {
27
+ exists(path: string): Promise<boolean>;
28
+ read(path: string): Promise<string>;
29
+ write(path: string, text: string): Promise<void>;
30
+ remove(path: string): Promise<void>;
31
+ mkdir(path: string): Promise<void>;
32
+ rmdir(path: string): Promise<void>;
33
+ list(path: string): Promise<string[]>;
34
+ };
35
+
36
+ // The platform's secret store, one string per item, this device only.
37
+ export type Secrets = {
38
+ get(item: string): Promise<string | undefined>;
39
+ set(item: string, value: string): Promise<void>;
40
+ remove(item: string): Promise<void>;
41
+ };
42
+
43
+ const { hex, unhex } = arithmetic;
44
+ type Blob = { seed: string; partition: Record<string, unknown>; record: WardRecord };
45
+
46
+ export class Sealed implements Store {
47
+ readonly harbor: string;
48
+ readonly #files: Files;
49
+ readonly #key: CryptoKey;
50
+ readonly #queues = new Map<string, Promise<void>>();
51
+ private constructor(harbor: string, files: Files, key: CryptoKey) {
52
+ this.harbor = harbor;
53
+ this.#files = files;
54
+ this.#key = key;
55
+ }
56
+
57
+ // Open one harbor's store, applying the custody rule, minting the key and
58
+ // the folder when this is a fresh install.
59
+ static async open(harbor: string, files: Files, secrets: Secrets): Promise<Sealed> {
60
+ const item = `quo-${harbor}`;
61
+ const root = `quo/${harbor}`;
62
+ let secret = await secrets.get(item);
63
+ const folder = await files.exists(root);
64
+ if (secret && !folder) {
65
+ await secrets.remove(item);
66
+ secret = undefined;
67
+ }
68
+ if (!secret && folder) await files.rmdir(root);
69
+ if (!secret) {
70
+ secret = hex(crypto.getRandomValues(new Uint8Array(32)));
71
+ await secrets.set(item, secret);
72
+ }
73
+ if (!(await files.exists(`${root}/wards`))) await files.mkdir(`${root}/wards`);
74
+ return new Sealed(harbor, files, await sealKey(secret));
75
+ }
76
+
77
+ // Everything this harbor has, key and files: what a person does by hand
78
+ // to leave a device, and what a test does between two openings.
79
+ static async wipe(harbor: string, files: Files, secrets: Secrets): Promise<void> {
80
+ await secrets.remove(`quo-${harbor}`);
81
+ if (await files.exists(`quo/${harbor}`)) await files.rmdir(`quo/${harbor}`);
82
+ }
83
+
84
+ #path(name: string) {
85
+ return `quo/${this.harbor}/wards/${name}.sealed`;
86
+ }
87
+ async #read(name: string): Promise<Blob> {
88
+ const data = await this.#files.read(this.#path(name));
89
+ return JSON.parse(new TextDecoder().decode(await open(this.#key, data.trim()))) as Blob;
90
+ }
91
+ // A sealed ward is rewritten whole, so the read sits inside the queue
92
+ // with the write: two changes to one ward never lose each other's part.
93
+ #keep(name: string, change: (b: Blob | undefined) => Blob): Promise<void> {
94
+ const next = (this.#queues.get(name) ?? Promise.resolve()).then(async () => {
95
+ const blob = change((await this.#files.exists(this.#path(name))) ? await this.#read(name) : undefined);
96
+ const sealed = await seal(this.#key, new TextEncoder().encode(JSON.stringify(blob)));
97
+ await this.#files.write(this.#path(name), sealed + '\n');
98
+ });
99
+ this.#queues.set(name, next.catch(() => {}));
100
+ return next;
101
+ }
102
+
103
+ async list(): Promise<string[]> {
104
+ const files = await this.#files.list(`quo/${this.harbor}/wards`);
105
+ return files.filter((f) => f.endsWith('.sealed')).map((f) => f.slice(0, -'.sealed'.length));
106
+ }
107
+
108
+ async load(name: string): Promise<Kept | undefined> {
109
+ if (!(await this.#files.exists(this.#path(name)))) return undefined;
110
+ const b = await this.#read(name);
111
+ return { seed: unhex(b.seed), partition: b.partition, record: b.record };
112
+ }
113
+
114
+ async put(name: string, kept: Kept): Promise<void> {
115
+ if (await this.#files.exists(this.#path(name))) throw new Error(`ward ${name} already exists on this device`);
116
+ return this.#keep(name, () => ({ seed: hex(kept.seed), partition: kept.partition, record: kept.record }));
117
+ }
118
+
119
+ async save(name: string, partition: Record<string, unknown>): Promise<void> {
120
+ if (!(await this.#files.exists(this.#path(name)))) return; // a name not kept is nothing
121
+ return this.#keep(name, (b) => ({ ...b!, partition }));
122
+ }
123
+
124
+ async record(name: string, record: WardRecord): Promise<void> {
125
+ if (!(await this.#files.exists(this.#path(name)))) return;
126
+ return this.#keep(name, (b) => ({ ...b!, record }));
127
+ }
128
+
129
+ async take(name: string): Promise<Kept | undefined> {
130
+ const kept = await this.load(name);
131
+ if (!kept) return undefined;
132
+ await this.#queues.get(name);
133
+ await this.#files.remove(this.#path(name));
134
+ return kept;
135
+ }
136
+
137
+ async hints(): Promise<Record<string, string>> {
138
+ const p = `quo/${this.harbor}/reach.json`;
139
+ if (!(await this.#files.exists(p))) return {};
140
+ return JSON.parse(await this.#files.read(p)) as Record<string, string>;
141
+ }
142
+
143
+ async hint(pk: string, url: string): Promise<void> {
144
+ const all = await this.hints();
145
+ all[pk] = url;
146
+ await this.#files.write(`quo/${this.harbor}/reach.json`, JSON.stringify(all) + '\n');
147
+ }
148
+ }
149
+
150
+ // Files and secrets in memory, for a suite and for a box that keeps
151
+ // nothing: the custody rule is proven here once and holds on every
152
+ // platform that hands the two.
153
+ export function inMemory(): { files: Files; secrets: Secrets; disk: Map<string, string> } {
154
+ const disk = new Map<string, string>();
155
+ const dirs = new Set<string>();
156
+ const vault = new Map<string, string>();
157
+ const under = (dir: string) => [...disk.keys()].filter((k) => k.startsWith(`${dir}/`));
158
+ const files: Files = {
159
+ exists: async (p) => disk.has(p) || dirs.has(p),
160
+ read: async (p) => {
161
+ const v = disk.get(p);
162
+ if (v === undefined) throw new Error(`no such file: ${p}`);
163
+ return v;
164
+ },
165
+ write: async (p, text) => void disk.set(p, text),
166
+ remove: async (p) => void disk.delete(p),
167
+ mkdir: async (p) => {
168
+ const parts = p.split('/');
169
+ for (let i = 1; i <= parts.length; i += 1) dirs.add(parts.slice(0, i).join('/'));
170
+ },
171
+ rmdir: async (p) => {
172
+ for (const k of under(p)) disk.delete(k);
173
+ for (const d of [...dirs]) if (d === p || d.startsWith(`${p}/`)) dirs.delete(d);
174
+ },
175
+ list: async (p) => under(p).map((k) => k.slice(p.length + 1)).filter((k) => !k.includes('/')),
176
+ };
177
+ const secrets: Secrets = {
178
+ get: async (i) => vault.get(i),
179
+ set: async (i, v) => void vault.set(i, v),
180
+ remove: async (i) => void vault.delete(i),
181
+ };
182
+ return { files, secrets, disk };
183
+ }
@@ -0,0 +1,79 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The desk: the sealed store over the desk app's core, the browser harbor
3
+ // over that store, and the desk's faculties as the platform gives them.
4
+ // The core is Tauri's Rust side, reached by its commands, and it holds
5
+ // three small things: the files under the app's own data folder, the key
6
+ // in the OS keychain, and the notification center. The custody rule is the
7
+ // sealed store's, `sealed.ts`, and this file hands it the two seams.
8
+ //
9
+ // The desk is a presence like the phone: a laptop sleeps and stands behind
10
+ // a network nobody can address, so its harbor dials, holds no listener and
11
+ // ships no daemon. It has no ear, since no store rings a desk; a desk is
12
+ // reached while the app is open and drains what it missed when it opens.
13
+ import type { BeingClass, JsonObject } from '@nervur-org/nervur';
14
+ import { NOTIFICATIONS, Notifications } from '../dock/index.ts';
15
+ import { BrowserHarbor } from './browser.ts';
16
+ import { Sealed, type Files, type Secrets } from './sealed.ts';
17
+
18
+ // The core, as the window sees it: one call in, one answer back.
19
+ export type Core = { invoke(cmd: string, args?: Record<string, unknown>): Promise<unknown> };
20
+
21
+ export class DeskNotifications extends Notifications {
22
+ readonly #core: Core;
23
+ constructor(stance: ConstructorParameters<typeof Notifications>[0], core: Core) {
24
+ super(stance);
25
+ this.#core = core;
26
+ }
27
+ override async notify(args: JsonObject): Promise<JsonObject> {
28
+ const title = typeof args.title === 'string' ? args.title : '';
29
+ const body = typeof args.body === 'string' ? args.body : '';
30
+ await this.#core.invoke('notify', { title, body });
31
+ return { shown: true };
32
+ }
33
+ }
34
+
35
+ // What a desk lends: its notification center, under the name every box
36
+ // lends it under. Location is what the platform gives, which on a desk is
37
+ // nothing, so it is not lent rather than lent and dumb. No timer either: a
38
+ // desk holds presences and no home, so no Clock lives on it and a Timer
39
+ // there would arm for nobody. A Timer stands only on a box that hosts a
40
+ // home.
41
+ export const deskLent: Record<string, string> = { [NOTIFICATIONS]: 'Notifications' };
42
+
43
+ // The two seams over the core's commands. Paths are relative to the app's
44
+ // data folder, and the core refuses one that climbs out of it.
45
+ export function deskFiles(core: Core): Files {
46
+ return {
47
+ exists: (path) => core.invoke('file_exists', { path }) as Promise<boolean>,
48
+ read: (path) => core.invoke('file_read', { path }) as Promise<string>,
49
+ write: async (path, text) => void (await core.invoke('file_write', { path, text })),
50
+ remove: async (path) => void (await core.invoke('file_remove', { path })),
51
+ mkdir: async (path) => void (await core.invoke('dir_make', { path })),
52
+ rmdir: async (path) => void (await core.invoke('dir_remove', { path })),
53
+ list: (path) => core.invoke('dir_list', { path }) as Promise<string[]>,
54
+ };
55
+ }
56
+ export function deskSecrets(core: Core): Secrets {
57
+ return {
58
+ get: async (item) => (await core.invoke('secret_get', { item })) as string | undefined ?? undefined,
59
+ set: async (item, value) => void (await core.invoke('secret_set', { item, value })),
60
+ remove: async (item) => void (await core.invoke('secret_remove', { item })),
61
+ };
62
+ }
63
+
64
+ // The desk app's one harbor, booted, lending what the desk has to its
65
+ // wards. The core hands the faculty its notification center by being in
66
+ // the class body the harbor holds, which is the one place a terrain's
67
+ // body may hold a live object.
68
+ export async function deskHarbor(core: Core, name = 'quo', classes: Record<string, BeingClass> = {}): Promise<BrowserHarbor> {
69
+ class Center extends DeskNotifications {
70
+ constructor(stance: ConstructorParameters<typeof DeskNotifications>[0]) {
71
+ super(stance, core);
72
+ }
73
+ }
74
+ const store = await Sealed.open(name, deskFiles(core), deskSecrets(core));
75
+ const h = new BrowserHarbor(store, { Notifications: Center, ...classes });
76
+ await h.stand(deskLent);
77
+ await h.boot();
78
+ return h;
79
+ }
package/harbor/ward.ts ADDED
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // What a ward says about itself, read the one way anything is read from a
3
+ // ward: by asking it. A harbor hands its device the beings it made and the
4
+ // keys it has, and nothing else about a world; the partition is not on offer
5
+ // and should not be, since it holds every seed the ward has. So a side that
6
+ // wants to know which being is public, or what a key is a class of, asks
7
+ // through the ask pointer and reads the describe, which is the same answer
8
+ // the owner gets on every other surface. Terrain-free: a daemon and a tab
9
+ // both hold a Hosted and both read it here.
10
+ import type { Hosted } from '@nervur-org/nervur/harbor';
11
+
12
+ // One being of a ward, as her own ward describes her.
13
+ export type Row = { class: string | null; public: boolean; digest: string | null; absent?: boolean };
14
+
15
+ // Every being of the ward, by key: the ones a door holds this run and the
16
+ // ones whose class did not construct, which are hers all the same.
17
+ export async function beingsOf(h: Hosted): Promise<Record<string, Row>> {
18
+ const bp = (await h.ask()) as { notes?: { beings?: Record<string, Row> } };
19
+ return bp.notes?.beings ?? {};
20
+ }
21
+
22
+ // The key of the one public being, or null. The ward marks her and the ward
23
+ // is asked, because no harbor reads that mark and none should: which being
24
+ // answers strangers is a decision, and a harbor decides nothing.
25
+ export async function publicKey(h: Hosted): Promise<string | null> {
26
+ for (const [key, row] of Object.entries(await beingsOf(h))) if (row.public) return key;
27
+ return null;
28
+ }