@nervur-org/dock 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +17 -15
  2. package/api/route.ts +4 -72
  3. package/beings/avatar.ts +11 -7
  4. package/beings/carry.ts +129 -116
  5. package/beings/clock.ts +11 -7
  6. package/beings/desk.ts +7 -6
  7. package/beings/doorbell.ts +36 -7
  8. package/beings/index.ts +17 -4
  9. package/beings/join.ts +7 -4
  10. package/beings/link.ts +1 -1
  11. package/beings/porter.ts +5 -1
  12. package/beings/presence.ts +15 -5
  13. package/beings/setup.ts +27 -21
  14. package/beings/user.ts +26 -17
  15. package/beings/way.ts +1 -1
  16. package/{dock → box}/agents.ts +37 -44
  17. package/{dock → box}/apns.ts +4 -4
  18. package/box/box.ts +31 -0
  19. package/box/contract.ts +41 -0
  20. package/box/dialers.ts +88 -0
  21. package/box/dock.ts +148 -0
  22. package/box/envoy.ts +96 -0
  23. package/box/faculty.ts +111 -0
  24. package/{dock → box}/fcm.ts +1 -1
  25. package/box/grants.ts +90 -0
  26. package/box/http.ts +93 -0
  27. package/{dock → box}/http2.ts +7 -7
  28. package/box/index.ts +201 -0
  29. package/box/join.ts +78 -0
  30. package/box/local.ts +37 -0
  31. package/{dock → box}/location.ts +15 -3
  32. package/{dock → box}/notifications.ts +17 -5
  33. package/box/push.ts +85 -0
  34. package/box/ring.ts +85 -0
  35. package/{dock → box}/service.ts +2 -2
  36. package/box/timer.ts +150 -0
  37. package/box/tree.ts +22 -0
  38. package/{dock → box}/webpush.ts +2 -2
  39. package/cli/client.ts +1 -1
  40. package/cli/daemon.ts +101 -103
  41. package/cli/estate.ts +2 -3
  42. package/cli/nervur.ts +44 -25
  43. package/cli/stand.ts +1 -1
  44. package/dist/api/route.d.ts +1 -2
  45. package/dist/api/route.js +4 -76
  46. package/dist/beings/avatar.d.ts +4 -3
  47. package/dist/beings/avatar.js +9 -6
  48. package/dist/beings/carry.d.ts +10 -12
  49. package/dist/beings/carry.js +119 -127
  50. package/dist/beings/clock.js +10 -7
  51. package/dist/beings/desk.js +8 -7
  52. package/dist/beings/doorbell.d.ts +3 -1
  53. package/dist/beings/doorbell.js +37 -4
  54. package/dist/beings/index.d.ts +24 -4
  55. package/dist/beings/index.js +15 -4
  56. package/dist/beings/join.d.ts +1 -0
  57. package/dist/beings/join.js +6 -4
  58. package/dist/beings/link.d.ts +0 -1
  59. package/dist/beings/link.js +1 -1
  60. package/dist/beings/porter.js +5 -1
  61. package/dist/beings/presence.d.ts +1 -0
  62. package/dist/beings/presence.js +12 -3
  63. package/dist/beings/setup.d.ts +5 -2
  64. package/dist/beings/setup.js +16 -15
  65. package/dist/beings/user.d.ts +4 -3
  66. package/dist/beings/user.js +18 -11
  67. package/dist/{dock → box}/agents.d.ts +7 -4
  68. package/dist/{dock → box}/agents.js +30 -37
  69. package/dist/{dock → box}/apns.d.ts +1 -1
  70. package/dist/{dock → box}/apns.js +3 -3
  71. package/dist/box/box.d.ts +9 -0
  72. package/dist/box/box.js +18 -0
  73. package/dist/box/contract.d.ts +6 -0
  74. package/dist/box/contract.js +35 -0
  75. package/dist/{dock → box}/dialers.d.ts +7 -4
  76. package/dist/box/dialers.js +76 -0
  77. package/dist/box/dock.d.ts +182 -0
  78. package/dist/box/dock.js +128 -0
  79. package/dist/box/envoy.d.ts +44 -0
  80. package/dist/box/envoy.js +75 -0
  81. package/dist/box/faculty.d.ts +68 -0
  82. package/dist/box/faculty.js +97 -0
  83. package/dist/{dock → box}/fcm.d.ts +1 -1
  84. package/dist/{dock → box}/grants.d.ts +4 -3
  85. package/dist/box/grants.js +78 -0
  86. package/dist/box/http.d.ts +26 -0
  87. package/dist/box/http.js +89 -0
  88. package/dist/{dock → box}/http2.d.ts +1 -1
  89. package/dist/{dock → box}/http2.js +6 -6
  90. package/dist/box/index.d.ts +46 -0
  91. package/dist/box/index.js +179 -0
  92. package/dist/box/join.d.ts +19 -0
  93. package/dist/box/join.js +67 -0
  94. package/dist/{dock → box}/local.d.ts +3 -2
  95. package/dist/box/local.js +27 -0
  96. package/dist/{dock → box}/location.d.ts +3 -0
  97. package/dist/box/location.js +21 -0
  98. package/dist/{dock → box}/notifications.d.ts +3 -0
  99. package/dist/{dock → box}/notifications.js +14 -4
  100. package/dist/{dock → box}/push.d.ts +12 -18
  101. package/dist/box/push.js +83 -0
  102. package/dist/box/ring.d.ts +18 -0
  103. package/dist/box/ring.js +55 -0
  104. package/dist/{dock → box}/service.d.ts +1 -1
  105. package/dist/{dock → box}/service.js +1 -1
  106. package/dist/{dock → box}/timer.d.ts +2 -0
  107. package/dist/box/timer.js +127 -0
  108. package/dist/box/tree.d.ts +5 -0
  109. package/dist/box/tree.js +14 -0
  110. package/dist/{dock → box}/webpush.d.ts +1 -1
  111. package/dist/cli/client.d.ts +1 -0
  112. package/dist/cli/daemon.d.ts +2 -8
  113. package/dist/cli/daemon.js +102 -107
  114. package/dist/cli/estate.js +2 -3
  115. package/dist/cli/nervur.js +44 -24
  116. package/dist/cli/stand.d.ts +0 -14
  117. package/dist/cli/stand.js +1 -1
  118. package/dist/harbor/browser.d.ts +8 -5
  119. package/dist/harbor/browser.js +20 -21
  120. package/dist/harbor/capacitor.d.ts +3 -3
  121. package/dist/harbor/capacitor.js +9 -8
  122. package/dist/harbor/disk.d.ts +9 -9
  123. package/dist/harbor/disk.js +56 -51
  124. package/dist/harbor/edge/alarm.d.ts +4 -2
  125. package/dist/harbor/edge/alarm.js +9 -19
  126. package/dist/harbor/edge/edge.d.ts +7 -6
  127. package/dist/harbor/edge/edge.js +58 -53
  128. package/dist/harbor/edge/exercise.js +4 -4
  129. package/dist/harbor/edge/given.d.ts +0 -1
  130. package/dist/harbor/edge/given.js +0 -29
  131. package/dist/harbor/edge/storage.d.ts +0 -2
  132. package/dist/harbor/edge/storage.js +2 -2
  133. package/dist/harbor/http.js +4 -2
  134. package/dist/harbor/sealed.d.ts +0 -5
  135. package/dist/harbor/sealed.js +0 -39
  136. package/dist/harbor/tauri.d.ts +2 -2
  137. package/dist/harbor/tauri.js +13 -10
  138. package/dist/human/app.d.ts +1 -1
  139. package/dist/human/app.js +10 -12
  140. package/dist/human/door.d.ts +0 -1
  141. package/dist/human/door.js +1 -1
  142. package/dist/human/html.js +1 -1
  143. package/dist/human/move.js +1 -1
  144. package/dist/human/tab.bundle.txt +3629 -2824
  145. package/dist/human/wake.d.ts +5 -2
  146. package/dist/human/wake.js +23 -14
  147. package/dist/human/web.d.ts +0 -1
  148. package/dist/human/web.js +3 -3
  149. package/dist/human/worker.bundle.txt +2945 -2118
  150. package/dist/human/worlds.js +3 -3
  151. package/dist/mcp/agent.d.ts +0 -1
  152. package/dist/mcp/agent.js +1 -1
  153. package/dist/mcp/allow.js +3 -2
  154. package/dist/mcp/http.d.ts +0 -1
  155. package/dist/mcp/http.js +1 -1
  156. package/dist/mcp/oauth.d.ts +3 -4
  157. package/dist/mcp/oauth.js +4 -4
  158. package/dist/mcp/route.d.ts +1 -1
  159. package/dist/mcp/runner.d.ts +0 -1
  160. package/dist/mcp/runner.js +1 -1
  161. package/dist/mcp/server.d.ts +0 -1
  162. package/dist/mcp/server.js +1 -1
  163. package/harbor/browser.ts +22 -24
  164. package/harbor/capacitor.ts +9 -8
  165. package/harbor/disk.ts +53 -48
  166. package/harbor/edge/alarm.ts +15 -12
  167. package/harbor/edge/edge.ts +61 -55
  168. package/harbor/edge/exercise.ts +4 -4
  169. package/harbor/edge/given.ts +3 -33
  170. package/harbor/edge/storage.ts +2 -2
  171. package/harbor/http.ts +5 -3
  172. package/harbor/sealed.ts +0 -35
  173. package/harbor/tauri.ts +13 -10
  174. package/human/app.ts +12 -13
  175. package/human/door.ts +1 -1
  176. package/human/html.ts +1 -1
  177. package/human/local.ts +1 -1
  178. package/human/move.ts +1 -1
  179. package/human/tab.bundle.txt +3629 -2824
  180. package/human/wake.ts +36 -16
  181. package/human/web.ts +4 -4
  182. package/human/worker.bundle.txt +2945 -2118
  183. package/human/worlds.ts +3 -3
  184. package/mcp/agent.ts +1 -1
  185. package/mcp/allow.ts +2 -1
  186. package/mcp/http.ts +1 -1
  187. package/mcp/oauth.ts +4 -4
  188. package/mcp/route.ts +1 -1
  189. package/mcp/runner.ts +1 -1
  190. package/mcp/server.ts +1 -1
  191. package/package.json +10 -18
  192. package/GETTING_STARTED.md +0 -138
  193. package/GLOSSARY.md +0 -118
  194. package/WORLDS.md +0 -569
  195. package/beings/envoy.ts +0 -218
  196. package/dist/beings/envoy.d.ts +0 -54
  197. package/dist/beings/envoy.js +0 -211
  198. package/dist/dock/console.d.ts +0 -6
  199. package/dist/dock/console.js +0 -20
  200. package/dist/dock/dialers.js +0 -75
  201. package/dist/dock/faculty.d.ts +0 -24
  202. package/dist/dock/faculty.js +0 -142
  203. package/dist/dock/grants.js +0 -99
  204. package/dist/dock/index.d.ts +0 -41
  205. package/dist/dock/index.js +0 -113
  206. package/dist/dock/local.js +0 -44
  207. package/dist/dock/location.js +0 -11
  208. package/dist/dock/push.js +0 -132
  209. package/dist/dock/timer.js +0 -149
  210. package/dock/console.ts +0 -23
  211. package/dock/dialers.ts +0 -85
  212. package/dock/faculty.ts +0 -148
  213. package/dock/grants.ts +0 -111
  214. package/dock/index.ts +0 -133
  215. package/dock/local.ts +0 -48
  216. package/dock/push.ts +0 -161
  217. package/dock/timer.ts +0 -174
  218. package/quo-api.md +0 -196
  219. package/quo-app-bells.md +0 -280
  220. package/quo-dock.md +0 -1945
  221. package/quo-harbor.md +0 -874
  222. package/quo-human.md +0 -1042
  223. package/quo-mcp.md +0 -343
  224. /package/dist/{dock → box}/fcm.js +0 -0
  225. /package/dist/{dock → box}/webpush.js +0 -0
@@ -0,0 +1,78 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The box's grants: a bearer to an identity in a world, honoured on every
3
+ // route alike. A route says the grammar a caller speaks and a grant says who
4
+ // the caller is. A grant maps a token to an identity and nothing else; no
5
+ // secret of Quo's is ever here.
6
+ //
7
+ // Her tokens are her cells. Issuing, refreshing and revoking are the routes'
8
+ // to call on her in this process, and a token issued is a cell written
9
+ // outside any ask, so she keeps her ward through her box. Her clock is the
10
+ // box's. An access token lives an hour and a refresh token thirty days, and
11
+ // refresh rotates.
12
+ import { contract } from './contract.js';
13
+ import { Faculty } from './faculty.js';
14
+ // What a route needs from the box's grants. She lends nothing to beings, so
15
+ // the contract has no ask; its title is what the box's own code finds her by.
16
+ export const GRANTS = contract('grants', {});
17
+ export const ACCESS_TTL = 60 * 60 * 1000;
18
+ export const REFRESH_TTL = 30 * 24 * 60 * 60 * 1000;
19
+ // Own keys only: a token is a string the far side offers.
20
+ const own = (rec, key) => (Object.hasOwn(rec, key) ? rec[key] : undefined);
21
+ // Thirty-two random bytes as base64url, from what every runtime has.
22
+ const token = () => {
23
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(32));
24
+ let binary = '';
25
+ for (const b of bytes)
26
+ binary += String.fromCharCode(b);
27
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
28
+ };
29
+ export class Grants extends Faculty {
30
+ static cells = { ...Faculty.cells, access: {}, refresh: {} };
31
+ static asks = {};
32
+ static fulfils = [GRANTS];
33
+ get kept() {
34
+ return this.cells;
35
+ }
36
+ // The box's moment, which is hers.
37
+ now() {
38
+ return this.box.now();
39
+ }
40
+ async issue(identity, ward, client) {
41
+ const access_token = token(), refresh_token = token();
42
+ this.kept.access[access_token] = { identity, ward, client, exp: this.now() + ACCESS_TTL };
43
+ this.kept.refresh[refresh_token] = { identity, ward, client, exp: this.now() + REFRESH_TTL };
44
+ this.sweep();
45
+ await this.box.save();
46
+ return { access_token, token_type: 'Bearer', expires_in: ACCESS_TTL / 1000, refresh_token, scope: 'nervur' };
47
+ }
48
+ async refresh(refresh_token) {
49
+ const r = own(this.kept.refresh, refresh_token);
50
+ if (!r || r.exp < this.now())
51
+ return null;
52
+ delete this.kept.refresh[refresh_token];
53
+ return this.issue(r.identity, r.ward, r.client);
54
+ }
55
+ bearer(req) {
56
+ const g = own(this.kept.access, offered(req));
57
+ return g && g.exp >= this.now() && typeof g.identity === 'string' ? { identity: g.identity, ward: g.ward } : null;
58
+ }
59
+ async revoke(identity, ward) {
60
+ for (const k of ['access', 'refresh'])
61
+ for (const [t, g] of Object.entries(this.kept[k]))
62
+ if (g.identity === identity && g.ward === ward)
63
+ delete this.kept[k][t];
64
+ await this.box.save();
65
+ }
66
+ sweep() {
67
+ const now = this.now();
68
+ for (const k of ['access', 'refresh'])
69
+ for (const [t, g] of Object.entries(this.kept[k]))
70
+ if (!(g.exp >= now))
71
+ delete this.kept[k][t];
72
+ }
73
+ }
74
+ // What a request offered as its bearer, or nothing.
75
+ export const offered = (req) => {
76
+ const h = req.headers.get('authorization') ?? '';
77
+ return h.startsWith('Bearer ') ? h.slice(7) : '';
78
+ };
@@ -0,0 +1,26 @@
1
+ import type { Asker, JsonObject, OccupantRecord } from 'nervur';
2
+ import { Faculty } from './faculty.ts';
3
+ export declare const WIRE: import("nervur").Blueprint;
4
+ declare const root: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
5
+ export declare class Http extends Faculty {
6
+ static fulfils: import("nervur").Blueprint[];
7
+ static cells: JsonObject;
8
+ static asks: {
9
+ wired: {
10
+ description: string;
11
+ input: {
12
+ type: string;
13
+ properties: {
14
+ claimed: {
15
+ type: string;
16
+ };
17
+ };
18
+ required: string[];
19
+ };
20
+ for: typeof root;
21
+ };
22
+ };
23
+ wired(args: JsonObject): JsonObject;
24
+ handle(req: Request): Promise<Response>;
25
+ }
26
+ export {};
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The http faculty: the box's wire. A terrain that owns a wire hands her every
3
+ // request no route of its own took, and she hands the bytes on a claimed
4
+ // path to the faculty that claims it, through a standing the root placed at
5
+ // unpacking, as the occupant `http:<path>`. Nobody asks with owner rights:
6
+ // the faculty called in is asked by her, and answers in her own words.
7
+ //
8
+ // What crosses back is what every route here speaks: an object is 200, an
9
+ // error object 400, and no object at all 503. A path nobody claims is 404, a
10
+ // method but GET and POST is 405, and a body past a megabyte is 413. A GET
11
+ // is where a redirect lands, the code in the query, and a GET from a browser
12
+ // is a human sent back from somewhere: she is shown a page saying whether it
13
+ // finished, and never what the faculty answered a machine.
14
+ import { isSilence, isWord, OWNER } from 'nervur';
15
+ import { landed, POLICY } from '../human/door.js';
16
+ import { CSS } from '../human/style.js';
17
+ import { contract } from './contract.js';
18
+ import { Faculty } from './faculty.js';
19
+ // The box's wire. She lends nothing to beings, so her contract has no ask;
20
+ // its title is what unpacking finds her by.
21
+ export const WIRE = contract('http', {});
22
+ const LIMIT = 1 << 20;
23
+ const json = (status, body) => Response.json(body, { status });
24
+ // The one page this wire serves, wearing the one stylesheet every screen
25
+ // wears, and allowed no script, since it runs none.
26
+ const page = (status, body) => new Response(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>nervur</title>\n<style>${CSS}</style>\n</head><body>${body}</body></html>`, {
27
+ status,
28
+ headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': POLICY, 'referrer-policy': 'no-referrer' },
29
+ });
30
+ const root = (_occ, asker) => asker.id === OWNER;
31
+ export class Http extends Faculty {
32
+ static fulfils = [WIRE];
33
+ static cells = { watched: {}, minted: 0, claimed: {} };
34
+ static asks = {
35
+ wired: {
36
+ description: 'what this boot claims: each path to the faculty that claims it; a path held that this does not say goes',
37
+ input: { type: 'object', properties: { claimed: { type: 'object' } }, required: ['claimed'] },
38
+ for: root,
39
+ },
40
+ };
41
+ // The paths this run placed, held to: a standing under a path nobody
42
+ // claims now, or that another faculty claims now, goes here, and the
43
+ // root removes the far end. She answers what she holds and what went.
44
+ wired(args) {
45
+ const claimed = args.claimed;
46
+ if (claimed === null || typeof claimed !== 'object' || Array.isArray(claimed) || !Object.values(claimed).every((k) => typeof k === 'string'))
47
+ return { error: 'claimed is a path to a key' };
48
+ const was = (this.cells.claimed ?? {});
49
+ const pruned = [];
50
+ for (const p of Object.keys(this.cells.standings)) {
51
+ if (Object.hasOwn(claimed, p) && was[p] === claimed[p])
52
+ continue;
53
+ this.standings.remove(p);
54
+ pruned.push(p);
55
+ }
56
+ this.cells.claimed = claimed;
57
+ return { held: Object.keys(this.cells.standings), pruned };
58
+ }
59
+ // The terrain's own call, crossing no door: one request, one response.
60
+ // The longest claimed path the request's path begins with, at a segment
61
+ // boundary, is the faculty it goes to.
62
+ async handle(req) {
63
+ const url = new URL(req.url);
64
+ const claimed = Object.keys(this.cells.standings)
65
+ .filter((p) => url.pathname === p || url.pathname.startsWith(`${p}/`))
66
+ .sort((a, b) => b.length - a.length)[0];
67
+ const at = claimed === undefined ? undefined : this.standings[claimed];
68
+ if (!at)
69
+ return json(404, { error: 'nobody claims this path' });
70
+ if (req.method !== 'GET' && req.method !== 'POST')
71
+ return json(405, { error: 'POST, or GET for a redirect' });
72
+ const bytes = req.method === 'GET' ? new ArrayBuffer(0) : await req.arrayBuffer();
73
+ if (bytes.byteLength > LIMIT)
74
+ return json(413, { error: 'too big' });
75
+ const headers = {};
76
+ req.headers.forEach((v, k) => (headers[k] = v));
77
+ const out = await at.ask('inbound', { body: new TextDecoder().decode(bytes), headers, query: url.search.slice(1), method: req.method });
78
+ const object = out !== undefined && !isSilence(out) && !isWord(out) && out !== null && typeof out === 'object' && !Array.isArray(out) ? out : null;
79
+ if (req.method === 'GET' && (req.headers.get('accept') ?? '').includes('text/html')) {
80
+ const ok = object !== null && typeof object.error !== 'string';
81
+ // No host is printed: a Host header is the caller's to write, and a
82
+ // page that printed one would print whatever a caller wanted read.
83
+ return page(ok ? 200 : 400, landed({ host: '', ok }));
84
+ }
85
+ if (object === null)
86
+ return json(503, { error: 'silence' });
87
+ return json(typeof object.error === 'string' ? 400 : 200, object);
88
+ }
89
+ }
@@ -1,4 +1,4 @@
1
- import type { Ring } from './push.ts';
1
+ import type { Ring } from './ring.ts';
2
2
  import { HOSTS, type Apns } from './apns.ts';
3
3
  export declare function apnsOf(env: Record<string, string | undefined>): Apns | null;
4
4
  export declare function apnsHttp2(a: Apns, at?: typeof HOSTS, within?: number): Ring;
@@ -9,7 +9,7 @@
9
9
  // It is its own file for one reason, and the reason is the import. A worker
10
10
  // bundle that pulled `node:http2` in would not build, so nothing terrain
11
11
  // neutral may name this file: it is imported by the Node terrain and by
12
- // nothing else, and it is not exported from `dock/index.ts`.
12
+ // nothing else, and it is not exported from `box/index.ts`.
13
13
  //
14
14
  // A session is one TCP connection carrying every ring to one of Apple's two
15
15
  // hosts, which is what HTTP/2 is for and what Apple asks of a provider that
@@ -54,12 +54,12 @@ export function apnsHttp2(a, at = HOSTS, within = WITHIN) {
54
54
  stream.setEncoding('utf8');
55
55
  stream.setTimeout(within, () => stream.destroy(new Error('apns did not answer')));
56
56
  const { status, text } = await new Promise((ok, no) => {
57
- let status = 0;
58
- let text = '';
59
- stream.on('response', (h) => (status = Number(h[':status'] ?? 0)));
60
- stream.on('data', (chunk) => (text += chunk));
57
+ let code = 0;
58
+ let body = '';
59
+ stream.on('response', (h) => (code = h[':status'] ?? 0));
60
+ stream.on('data', (chunk) => (body += chunk));
61
61
  stream.on('error', no);
62
- stream.on('end', () => ok({ status, text }));
62
+ stream.on('end', () => ok({ status: code, text: body }));
63
63
  stream.end(req.body);
64
64
  });
65
65
  answered(status, text);
@@ -0,0 +1,46 @@
1
+ import type { Harbor, WardPointers } from 'nervur/harbor';
2
+ import type { BeingClass } from 'nervur';
3
+ import { type BoxCells } from './dock.ts';
4
+ import { type Contract } from './contract.ts';
5
+ import { type Faculty } from './faculty.ts';
6
+ import type { Harborage } from './box.ts';
7
+ import { type Tree } from './tree.ts';
8
+ export { ALONE, harborageOver, type Harborage } from './box.ts';
9
+ export { Dock, BOX, DOMAIN, routesFor, type Routes, type Apps, type BoxCells } from './dock.ts';
10
+ export { BOX_SEED } from 'nervur/harbor';
11
+ export { Faculty, WATCH, INBOUND, HTTP, type Watched } from './faculty.ts';
12
+ export { Http, WIRE } from './http.ts';
13
+ export { Timer, TIMER } from './timer.ts';
14
+ export { Local, LOCAL } from './local.ts';
15
+ export { Grants, GRANTS, offered, ACCESS_TTL, REFRESH_TTL, type Grant, type Tokens, type Bearers } from './grants.ts';
16
+ export { Push, PUSH } from './push.ts';
17
+ export { Agents, AGENTS, isProcess, type Row as AgentRow, type Runner, type Side } from './agents.ts';
18
+ export { Dialers, DIALERS } from './dialers.ts';
19
+ export { EnvoyFaculty, WATCH_ACCOUNT, type Delivery } from './envoy.ts';
20
+ export { contract, covers, nameOf, titleOf, type Contract } from './contract.ts';
21
+ export { join, settled } from './join.ts';
22
+ export { Location, LOCATION, type Where } from './location.ts';
23
+ export { Notifications, NOTIFICATIONS } from './notifications.ts';
24
+ export { keyed, type Tree } from './tree.ts';
25
+ export { isToken, bellOf, banner, sealBanner, SEAL, type Token, type Ring, type Line } from './ring.ts';
26
+ export { webPush, mintVapid, publicOf, type Vapid } from './webpush.ts';
27
+ export { apnsPush, pkcs8, HOSTS, type Apns } from './apns.ts';
28
+ export { fcmPush, isAccount, DOORS, type Account, type Doors } from './fcm.ts';
29
+ export { handedTo, ringing, routed } from './service.ts';
30
+ export declare const OWN: Tree;
31
+ export declare function classes(tree: Tree, box: Harborage): Record<string, BeingClass>;
32
+ type Boxed = {
33
+ wards: ReadonlyMap<string, {
34
+ ask: WardPointers['ask'];
35
+ being(key: string): unknown;
36
+ }>;
37
+ };
38
+ export declare function keyLent(harbor: Boxed, needs: Contract): Promise<string | undefined>;
39
+ export declare function lent<F extends Faculty>(harbor: Boxed, needs: Contract): Promise<F | undefined>;
40
+ export declare function boxOf(harbor: Boxed): Promise<BoxCells>;
41
+ export declare function standHarbor(harbor: Harbor, tree: Tree, box: Harborage): Promise<void>;
42
+ export type Rooted = {
43
+ ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
44
+ save?(): Promise<void>;
45
+ };
46
+ export declare function stand(ward: Rooted, tree?: Tree): Promise<void>;
@@ -0,0 +1,179 @@
1
+ import { BOX, hold, offerTo, BOX_SEED } from 'nervur/harbor';
2
+ import { isInvitation } from 'nervur';
3
+ import { Dock } from './dock.js';
4
+ import { nameOf, titleOf } from './contract.js';
5
+ import { HTTP } from './faculty.js';
6
+ import { WIRE } from './http.js';
7
+ import { keyed } from './tree.js';
8
+ export { ALONE, harborageOver } from './box.js';
9
+ export { Dock, BOX, DOMAIN, routesFor } from './dock.js';
10
+ export { BOX_SEED } from 'nervur/harbor';
11
+ export { Faculty, WATCH, INBOUND, HTTP } from './faculty.js';
12
+ export { Http, WIRE } from './http.js';
13
+ export { Timer, TIMER } from './timer.js';
14
+ export { Local, LOCAL } from './local.js';
15
+ export { Grants, GRANTS, offered, ACCESS_TTL, REFRESH_TTL } from './grants.js';
16
+ export { Push, PUSH } from './push.js';
17
+ export { Agents, AGENTS, isProcess } from './agents.js';
18
+ export { Dialers, DIALERS } from './dialers.js';
19
+ export { EnvoyFaculty, WATCH_ACCOUNT } from './envoy.js';
20
+ export { contract, covers, nameOf, titleOf } from './contract.js';
21
+ export { join, settled } from './join.js';
22
+ export { Location, LOCATION } from './location.js';
23
+ export { Notifications, NOTIFICATIONS } from './notifications.js';
24
+ export { keyed } from './tree.js';
25
+ // What waking a device rings through: the push parts every terrain shares,
26
+ // the three grounds, and the bell service that holds a publisher's keys.
27
+ export { isToken, bellOf, banner, sealBanner, SEAL } from './ring.js';
28
+ export { webPush, mintVapid, publicOf } from './webpush.js';
29
+ export { apnsPush, pkcs8, HOSTS } from './apns.js';
30
+ export { fcmPush, isAccount, DOORS } from './fcm.js';
31
+ export { handedTo, ringing, routed } from './service.js';
32
+ // The classes this folder brings to every box ward, as a tree.
33
+ export const OWN = { box: { Dock } };
34
+ const isFaculty = (v) => typeof v === 'function' && Array.isArray(v.fulfils) && typeof v.stands === 'function';
35
+ // The classes a box ward is handed, keyed where they live, every faculty
36
+ // among them born holding this box. The kit makes a being with her stance
37
+ // alone, so the class found under a faculty's key is her own class with the
38
+ // harborage handed on.
39
+ export function classes(tree, box) {
40
+ const out = keyed(OWN);
41
+ for (const [key, C] of Object.entries(keyed(tree))) {
42
+ if (!isFaculty(C))
43
+ continue;
44
+ const F = C;
45
+ out[key] = class extends F {
46
+ constructor(stance) {
47
+ super(stance, box);
48
+ }
49
+ };
50
+ }
51
+ return out;
52
+ }
53
+ export async function keyLent(harbor, needs) {
54
+ const ward = harbor.wards.get(BOX_SEED);
55
+ if (!ward)
56
+ return undefined;
57
+ const out = (await ward.ask('ask', { being: BOX, method: 'lends' }));
58
+ return out.lends?.[await nameOf(needs)];
59
+ }
60
+ export async function lent(harbor, needs) {
61
+ const key = await keyLent(harbor, needs);
62
+ return key === undefined ? undefined : harbor.wards.get(BOX_SEED)?.being(key);
63
+ }
64
+ // What the box is, asked of the Dock: its routes, its apps and its default
65
+ // ward, each null until the root said it. A harbor with no box ward is a
66
+ // box that is nothing yet.
67
+ export async function boxOf(harbor) {
68
+ const ward = harbor.wards.get(BOX_SEED);
69
+ const out = ward ? (await ward.ask('ask', { being: BOX, method: 'box' })) : {};
70
+ return { routes: out.routes ?? null, apps: out.apps ?? null, default: out.default ?? null };
71
+ }
72
+ // Act one on a harbor: its classes handed to it, each faculty born holding
73
+ // this box, the box's ward woken from what the store keeps or made, and
74
+ // stood. From here the harbor hands every other ward one invitation on the
75
+ // Dock, which is act two, the kit's.
76
+ export async function standHarbor(harbor, tree, box) {
77
+ Object.assign(harbor.classes, classes(tree, box));
78
+ const kept = await harbor.store.load(BOX_SEED);
79
+ const ward = harbor.wards.get(BOX_SEED) ?? (kept ? await harbor.host(BOX_SEED, kept) : await harbor.create(BOX_SEED, BOX));
80
+ await stand(ward, tree);
81
+ harbor.offering = (name) => offerTo(ward, name);
82
+ }
83
+ // Stand a box ward already made, in the order a harbor stands: her own
84
+ // being first, then every faculty of the tree whose class stands, each held
85
+ // by her under its key, then the index of which faculty fulfils which
86
+ // contract written into her cells, then every claimed path placed on the
87
+ // wire. A key already booted is refused and costs nothing, so standing a
88
+ // ward that woke from its partition finds every being as it was; and a run
89
+ // says only itself, so a title or a path an earlier run placed that this
90
+ // one does not is removed at both ends before anything is placed.
91
+ export async function stand(ward, tree = {}) {
92
+ const own = keyed(OWN);
93
+ await ward.ask('boot', { key: BOX, class: Object.keys(own).find((k) => own[k] === Dock) });
94
+ const standing = Object.entries(keyed(tree)).filter((e) => isFaculty(e[1]) && e[1].stands());
95
+ // The index. Under each contract, the classes that fulfil it, and of
96
+ // those the ones no other extends, so a subclass wins over its base when
97
+ // both stand. One left is lent under it. Two left, with neither extending
98
+ // the other, are a clash: the contract is lent to neither, the Dock keeps
99
+ // the clash by name in her cells for the root to read and a terrain to
100
+ // print, and the box stays up, so one bad contract costs that contract
101
+ // and nothing else.
102
+ const under = new Map();
103
+ const names = new Map();
104
+ for (const [key, C] of standing) {
105
+ for (const c of C.fulfils) {
106
+ const name = await nameOf(c);
107
+ names.set(key, [...(names.get(key) ?? []), name]);
108
+ const u = under.get(name) ?? { title: titleOf(c), keys: [] };
109
+ u.keys.push([key, C]);
110
+ under.set(name, u);
111
+ }
112
+ }
113
+ const lends = {};
114
+ const clashes = {};
115
+ for (const [name, u] of under) {
116
+ const leaves = u.keys.filter(([, C]) => !u.keys.some(([, D]) => D !== C && D.prototype instanceof C));
117
+ if (leaves.length === 1)
118
+ lends[name] = leaves[0][0];
119
+ else
120
+ clashes[u.title || name] = leaves.map(([k]) => k);
121
+ }
122
+ // The word a screen shows a faculty under: what the first contract she is
123
+ // lent under calls itself. A key carries a slash and a title never does.
124
+ const titles = {};
125
+ for (const [key] of standing) {
126
+ const first = (names.get(key) ?? []).find((n) => lends[n] === key);
127
+ const title = first === undefined ? '' : (under.get(first)?.title ?? '');
128
+ if (title && title !== key)
129
+ titles[title] = key;
130
+ }
131
+ const claims = new Map();
132
+ for (const [key, C] of standing) {
133
+ const made = (await ward.ask('boot', { key, class: key }));
134
+ if (made.booted === key)
135
+ await hold(ward, key);
136
+ for (const p of C.claims ?? [])
137
+ claims.set(p, [...(claims.get(p) ?? []), key]);
138
+ }
139
+ // The index written whole, and the Dock's title standings held to this
140
+ // run: she removes her end of one this run does not place, and the root
141
+ // removes the far end from every faculty she holds, before placing what
142
+ // she does not hold yet.
143
+ const stood = (await ward.ask('ask', { being: BOX, method: 'stood', args: { lends, titles, clashes } }));
144
+ const keys = (stood.held ?? []).filter((id) => id.includes('/'));
145
+ for (const t of stood.pruned ?? [])
146
+ for (const key of keys)
147
+ await ward.ask('remove', { being: key, id: `${BOX}:${t}` });
148
+ for (const [title, key] of Object.entries(titles)) {
149
+ if (stood.held?.includes(title))
150
+ continue;
151
+ const inv = await ward.ask('invite', { being: key, id: `${BOX}:${title}` });
152
+ if (isInvitation(inv))
153
+ await ward.ask('knock', { being: BOX, id: title, invitation: inv });
154
+ }
155
+ // Every claimed path, placed on the box's wire: the root invites on the
156
+ // faculty that claims it for the http faculty, who holds it under the
157
+ // path. A path two faculties claim is neither's, and the box stays up. A
158
+ // path she holds that this run does not claim, or that another faculty
159
+ // claims now, goes at both ends the same way a title does.
160
+ const wire = lends[await nameOf(WIRE)];
161
+ if (wire) {
162
+ const claimed = {};
163
+ for (const [p, ks] of claims)
164
+ if (ks.length === 1)
165
+ claimed[p] = ks[0];
166
+ const wired = (await ward.ask('ask', { being: wire, method: 'wired', args: { claimed } }));
167
+ for (const p of wired.pruned ?? [])
168
+ for (const key of keys)
169
+ await ward.ask('remove', { being: key, id: `${HTTP}${p}` });
170
+ for (const [p, key] of Object.entries(claimed)) {
171
+ if (wired.held?.includes(p))
172
+ continue;
173
+ const inv = await ward.ask('invite', { being: key, id: `${HTTP}${p}` });
174
+ if (isInvitation(inv))
175
+ await ward.ask('knock', { being: wire, id: p, invitation: inv });
176
+ }
177
+ }
178
+ await ward.save?.();
179
+ }
@@ -0,0 +1,19 @@
1
+ import type { Being, JsonObject } from 'nervur';
2
+ import { type Contract } from './contract.ts';
3
+ export type Hosting = {
4
+ keys(): string[];
5
+ being(key: string): unknown;
6
+ };
7
+ type Memory = {
8
+ partitions: ReadonlyMap<string, Record<string, unknown>>;
9
+ objects: WeakMap<object, unknown>;
10
+ };
11
+ export declare function settled(of: {
12
+ wards: ReadonlyMap<string, Hosting>;
13
+ } | Hosting | Memory): Promise<void>;
14
+ export declare function join(b: Being, needs: Contract, id: string, watch?: {
15
+ method: string;
16
+ args?: JsonObject;
17
+ with?: JsonObject;
18
+ }): Promise<string | null>;
19
+ export {};
@@ -0,0 +1,67 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // How a being takes what her box lends. She names the contract she needs and
3
+ // the id she holds it under; the box lends under the contract's digest; she
4
+ // reads the describe she was handed and keeps it only if it covers what she
5
+ // asked for. With `watch` she also asks to be woken, as the occupant she
6
+ // names `<id>:wake`, handing whatever the faculty needs to know her by; a
7
+ // watch refused puts the whole relation down.
8
+ //
9
+ // A being takes what her box lends at her birth, again at every birth, since
10
+ // a restart and a move are one event to her. Birth is synchronous and taking
11
+ // a standing is not, so every join in flight is kept on the being who made
12
+ // it, and a terrain waits on `settled` over its own wards once they are
13
+ // hosted: a box is up when its beings have landed, and another box's beings
14
+ // are no business of this one.
15
+ import { isBlueprint } from 'nervur';
16
+ import { covers, nameOf } from './contract.js';
17
+ const flight = new WeakMap();
18
+ const beingsOf = (of) => {
19
+ if ('partitions' in of)
20
+ return [...of.partitions.values()].flatMap((rows) => Object.values((rows.beings ?? {})).map((cells) => of.objects.get(cells)));
21
+ const wards = 'wards' in of ? [...of.wards.values()] : [of];
22
+ return wards.flatMap((w) => w.keys().map((k) => w.being(k)));
23
+ };
24
+ // Every join in flight on one box's wards, or on one ward, landed. Walked
25
+ // again until nothing is in flight, since a being landing may boot another.
26
+ export async function settled(of) {
27
+ for (;;) {
28
+ const now = beingsOf(of).flatMap((b) => [...(flight.get(b) ?? [])]);
29
+ if (now.length === 0)
30
+ return;
31
+ await Promise.allSettled(now);
32
+ }
33
+ }
34
+ export function join(b, needs, id, watch) {
35
+ const p = joining(b, needs, id, watch);
36
+ const mine = flight.get(b) ?? new Set();
37
+ mine.add(p);
38
+ flight.set(b, mine);
39
+ const done = () => void mine.delete(p);
40
+ void p.then(done, done);
41
+ return p;
42
+ }
43
+ async function joining(b, needs, id, watch) {
44
+ // One turn first: her ward sets her door after her constructor returns, so
45
+ // a stance call made inside it would reach a being her ward does not hold
46
+ // yet.
47
+ await Promise.resolve();
48
+ b.standings.remove(id);
49
+ b.occupants.remove(`${id}:wake`);
50
+ if ((await b.lend(await nameOf(needs), id)) === null)
51
+ return null;
52
+ const held = await b.standings[id]?.ask();
53
+ if (!isBlueprint(held) || !covers(held, needs)) {
54
+ b.standings.remove(id);
55
+ return null;
56
+ }
57
+ if (watch) {
58
+ const back = await b.invite(`${id}:wake`);
59
+ const said = back ? await b.standings[id]?.ask('watch', { ...watch.with, invitation: back, method: watch.method, ...(watch.args ? { args: watch.args } : {}) }) : undefined;
60
+ if (!said || typeof said !== 'object' || 'error' in said) {
61
+ b.standings.remove(id);
62
+ b.occupants.remove(`${id}:wake`);
63
+ return null;
64
+ }
65
+ }
66
+ return id;
67
+ }
@@ -1,12 +1,13 @@
1
- import { Faculty } from './faculty.ts';
2
1
  import type { JsonObject } from 'nervur';
3
2
  import { type Verified } from '../beings/way.ts';
4
- export declare const LOCAL = "local";
3
+ import { Faculty } from './faculty.ts';
4
+ export declare const LOCAL: import("nervur").Blueprint;
5
5
  export declare class Local extends Faculty {
6
6
  #private;
7
7
  static asks: {
8
8
  verify: import("nervur").AskSpec;
9
9
  };
10
+ static fulfils: import("nervur").Blueprint[];
10
11
  mint(nonce: string, who: Verified): void;
11
12
  drop(nonce: string): void;
12
13
  verify(args: JsonObject): Verified | null;
@@ -0,0 +1,27 @@
1
+ import { VERIFY } from '../beings/way.js';
2
+ import { contract } from './contract.js';
3
+ import { Faculty } from './faculty.js';
4
+ // What a desk needs from a box that saw a process on its own socket.
5
+ export const LOCAL = contract('local', { verify: VERIFY });
6
+ export class Local extends Faculty {
7
+ static asks = { verify: VERIFY };
8
+ static fulfils = [LOCAL];
9
+ #nonces = new Map();
10
+ mint(nonce, who) {
11
+ this.#nonces.set(nonce, who);
12
+ }
13
+ drop(nonce) {
14
+ this.#nonces.delete(nonce);
15
+ }
16
+ verify(args) {
17
+ const proof = args.proof;
18
+ if (!proof || typeof proof !== 'object' || Array.isArray(proof))
19
+ return null;
20
+ const nonce = proof.nonce;
21
+ const who = typeof nonce === 'string' ? this.#nonces.get(nonce) : undefined;
22
+ if (!who)
23
+ return null;
24
+ this.#nonces.delete(nonce);
25
+ return who;
26
+ }
27
+ }
@@ -6,6 +6,7 @@ export type Where = {
6
6
  accuracy?: number;
7
7
  at: number;
8
8
  };
9
+ export declare const LOCATION: import("nervur").Blueprint;
9
10
  export declare class Location extends Faculty {
10
11
  static asks: {
11
12
  where: {
@@ -15,5 +16,7 @@ export declare class Location extends Faculty {
15
16
  };
16
17
  };
17
18
  };
19
+ static fulfils: import("nervur").Blueprint[];
20
+ static stands(): boolean;
18
21
  where(): Promise<JsonObject>;
19
22
  }
@@ -0,0 +1,21 @@
1
+ import { Faculty } from './faculty.js';
2
+ import { contract } from './contract.js';
3
+ const ASKS = {
4
+ where: { description: 'where this box is', input: { type: 'object' } },
5
+ };
6
+ // What a being needs from a box that can say where it is.
7
+ export const LOCATION = contract('location', ASKS);
8
+ export class Location extends Faculty {
9
+ static asks = ASKS;
10
+ static fulfils = [LOCATION];
11
+ // This body says where nothing is, so it stands nowhere: a terrain whose
12
+ // platform can answer subclasses her and says so.
13
+ static stands() {
14
+ return false;
15
+ }
16
+ // The platform's own permission prompt is the first ask's, and a refusal is
17
+ // the error object every ask after it answers.
18
+ where() {
19
+ return Promise.resolve({ error: 'this box cannot say where it is' });
20
+ }
21
+ }
@@ -1,5 +1,6 @@
1
1
  import type { JsonObject } from 'nervur';
2
2
  import { Faculty } from './faculty.ts';
3
+ export declare const NOTIFICATIONS: import("nervur").Blueprint;
3
4
  export declare class Notifications extends Faculty {
4
5
  static cells: JsonObject;
5
6
  static asks: {
@@ -25,6 +26,8 @@ export declare class Notifications extends Faculty {
25
26
  };
26
27
  };
27
28
  };
29
+ static fulfils: import("nervur").Blueprint[];
30
+ static stands(): boolean;
28
31
  notify(_args: JsonObject): Promise<JsonObject>;
29
32
  key(): Promise<{
30
33
  secret: Uint8Array;