@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
@@ -23,6 +23,13 @@
23
23
  // it would cost her a line of text rather than her world.
24
24
  import { arithmetic } from 'nervur/ward';
25
25
  import { Faculty } from './faculty.js';
26
+ import { contract } from './contract.js';
27
+ const ASKS = {
28
+ notify: { description: 'show the human a line', input: { type: 'object', properties: { title: { type: 'string' }, body: { type: 'string' } }, required: ['title'] } },
29
+ banner: { description: "this device's banner key, the public half, which a world seals a notification to", input: { type: 'object' } },
30
+ };
31
+ // What a being needs from a box that can show the human a line.
32
+ export const NOTIFICATIONS = contract('notifications', ASKS);
26
33
  // A key is thirty two bytes of the platform's own randomness, as every seed
27
34
  // in this kit is.
28
35
  const seed = () => globalThis.crypto.getRandomValues(new Uint8Array(32));
@@ -30,10 +37,13 @@ export class Notifications extends Faculty {
30
37
  // The banner key, as hex: the seed it was made from, kept so the pair can
31
38
  // be made again, and the public half a device hands out.
32
39
  static cells = { watched: {}, minted: 0, banner: null };
33
- static asks = {
34
- notify: { description: 'show the human a line', input: { type: 'object', properties: { title: { type: 'string' }, body: { type: 'string' } }, required: ['title'] } },
35
- banner: { description: "this device's banner key, the public half, which a world seals a notification to", input: { type: 'object' } },
36
- };
40
+ static asks = ASKS;
41
+ static fulfils = [NOTIFICATIONS];
42
+ // This body shows nothing, so it stands nowhere: a terrain with a
43
+ // notification center subclasses her and says so.
44
+ static stands() {
45
+ return false;
46
+ }
37
47
  notify(_args) {
38
48
  return Promise.resolve({ error: 'this box shows no notifications' });
39
49
  }
@@ -1,23 +1,8 @@
1
1
  import type { Asker, JsonObject, OccupantRecord } from 'nervur';
2
+ import { type Ring } from './ring.ts';
2
3
  import { Faculty } from './faculty.ts';
3
- export type Token = {
4
- kind: string;
5
- bell?: string;
6
- app?: string;
7
- } & JsonObject;
8
- export declare const isToken: (v: unknown) => v is Token;
9
- export declare function bellOf(token: Token): string | undefined;
10
- export type Line = {
11
- title: string;
12
- body: string;
13
- };
14
- export type Ring = {
15
- ring(token: Token, sealed?: string, line?: Line): Promise<void>;
16
- };
17
- export declare const SEAL = 4096;
18
- export declare function banner(o: JsonObject): Line;
19
- export declare function sealBanner(line: JsonObject, key: string): Promise<string | null>;
20
4
  declare const root: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
5
+ export declare const PUSH: import("nervur").Blueprint;
21
6
  export declare class Push extends Faculty {
22
7
  static cells: JsonObject;
23
8
  static asks: {
@@ -54,6 +39,13 @@ export declare class Push extends Faculty {
54
39
  type: string;
55
40
  };
56
41
  };
42
+ bell: {
43
+ description: string;
44
+ input: {
45
+ type: string;
46
+ };
47
+ for: typeof root;
48
+ };
57
49
  keep: {
58
50
  description: string;
59
51
  input: {
@@ -68,7 +60,9 @@ export declare class Push extends Faculty {
68
60
  for: typeof root;
69
61
  };
70
62
  };
71
- mint(): Promise<JsonWebKey>;
63
+ static fulfils: import("nervur").Blueprint[];
64
+ static stands(): boolean;
65
+ bell(): Promise<JsonObject>;
72
66
  get key(): JsonWebKey | null;
73
67
  get service(): string | undefined;
74
68
  keep(args: JsonObject): JsonObject;
@@ -0,0 +1,83 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Waking a device that is not reachable, the one way its platform allows: a
3
+ // ring carrying the device's token and a banner sealed to its key. A token
4
+ // names its kind and a kind is one ring. The rings are live objects and not
5
+ // values, so a terrain that can ring brings a class that builds its own; this
6
+ // body rings nothing and stands nowhere.
7
+ //
8
+ // What she keeps: the world's doorbell key, a P-256 JWK she mints herself the
9
+ // first time a terrain asks, and the bell service a phone's ring is handed
10
+ // to when its token names none, a row the root keeps.
11
+ import { OWNER } from 'nervur';
12
+ import { SEAL, isToken } from './ring.js';
13
+ import { mintVapid, publicOf } from './webpush.js';
14
+ import { contract } from './contract.js';
15
+ import { Faculty } from './faculty.js';
16
+ const root = (_occ, asker) => asker.id === OWNER;
17
+ // What a holder is owed: a ring, and which kinds this box can ring.
18
+ const OWED = {
19
+ ring: { description: 'wake a device: its token, the banner sealed to it, or the line itself where the ground encrypts it', input: { type: 'object', properties: { token: { type: 'object' }, sealed: { type: 'string' }, line: { type: 'object', properties: { title: { type: 'string' }, body: { type: 'string' } }, required: ['title', 'body'] } }, required: ['token'] } },
20
+ kinds: { description: 'which kinds this box can ring', input: { type: 'object' } },
21
+ };
22
+ // What a being needs from a box that can wake a device.
23
+ export const PUSH = contract('push', OWED);
24
+ export class Push extends Faculty {
25
+ static cells = { watched: {}, minted: 0, key: null, service: null };
26
+ static asks = { ...OWED, bell: { description: 'the public half of this box’s doorbell key, minted at the first ask and kept in her cells', input: { type: 'object' }, for: root }, keep: { description: 'the bell service a phone is rung through when its token names none; null forgets it', input: { type: 'object', properties: { service: { type: ['string', 'null'] } }, required: ['service'] }, for: root } };
27
+ static fulfils = [PUSH];
28
+ static stands() {
29
+ return false;
30
+ }
31
+ // The doorbell key, minted at the root's first ask and kept; nobody mints
32
+ // it from outside. What crosses is the public half, which every page's
33
+ // config carries.
34
+ async bell() {
35
+ if (this.cells.key === null)
36
+ this.cells.key = (await mintVapid()).key;
37
+ return { bell: publicOf(this.cells.key) };
38
+ }
39
+ get key() {
40
+ return this.cells.key;
41
+ }
42
+ get service() {
43
+ return typeof this.cells.service === 'string' ? this.cells.service : undefined;
44
+ }
45
+ keep(args) {
46
+ const service = args.service;
47
+ if (service !== null && (typeof service !== 'string' || !/^https?:\/\/\S+$/.test(service)))
48
+ return { error: 'a bell service is a URL over http or https, or null' };
49
+ this.cells.service = service === null ? null : service.replace(/\/$/, '');
50
+ return { service: this.cells.service };
51
+ }
52
+ // What this box can ring. The base rings nothing.
53
+ get rings() {
54
+ return {};
55
+ }
56
+ kinds() {
57
+ return { kinds: Object.keys(this.rings) };
58
+ }
59
+ async ring(args) {
60
+ const token = args.token;
61
+ if (!isToken(token))
62
+ return { error: 'a token names its kind' };
63
+ const sealed = args.sealed;
64
+ if (sealed !== undefined && typeof sealed !== 'string')
65
+ return { error: 'a seal is text' };
66
+ if (typeof sealed === 'string' && sealed.length > SEAL)
67
+ return { error: `a seal is at most ${SEAL} bytes` };
68
+ const said = args.line;
69
+ if (said !== undefined && (said === null || typeof said !== 'object' || Array.isArray(said) || typeof said.title !== 'string' || typeof said.body !== 'string'))
70
+ return { error: 'a line is a title and a body' };
71
+ const line = said;
72
+ const r = Object.hasOwn(this.rings, token.kind) ? this.rings[token.kind] : undefined;
73
+ if (!r)
74
+ return { error: `this box cannot ring a ${token.kind}` };
75
+ try {
76
+ await r.ring(token, sealed, line);
77
+ return { rang: token.kind };
78
+ }
79
+ catch (e) {
80
+ return { error: e instanceof Error ? e.message : 'the ring threw' };
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,18 @@
1
+ import type { JsonObject } from 'nervur';
2
+ export type Token = {
3
+ kind: string;
4
+ bell?: string;
5
+ app?: string;
6
+ } & JsonObject;
7
+ export declare const isToken: (v: unknown) => v is Token;
8
+ export declare function bellOf(token: Token): string | undefined;
9
+ export type Line = {
10
+ title: string;
11
+ body: string;
12
+ };
13
+ export type Ring = {
14
+ ring(token: Token, sealed?: string, line?: Line): Promise<void>;
15
+ };
16
+ export declare const SEAL = 4096;
17
+ export declare function banner(o: JsonObject): Line;
18
+ export declare function sealBanner(line: JsonObject, key: string): Promise<string | null>;
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // What waking a device is made of, beside the push faculty: the token a
3
+ // device hands a world, the ring a platform rings through, the banner a
4
+ // device shows, and the seal only the device can open. A token names its
5
+ // kind, and a kind is one ring: `web` is Web Push under a keypair a world
6
+ // mints itself, `apns` and `fcm` are the two stores' own, handed on to the
7
+ // publisher's bell service by a world that holds no platform key.
8
+ import { arithmetic } from 'nervur/ward';
9
+ export const isToken = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && typeof v.kind === 'string';
10
+ // The bell a token names, when it names one a ring can reach.
11
+ export function bellOf(token) {
12
+ if (typeof token.bell !== 'string')
13
+ return undefined;
14
+ try {
15
+ const u = new URL(token.bell);
16
+ return u.protocol === 'https:' || u.protocol === 'http:' ? token.bell : undefined;
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ // What a platform will carry, so a seal too big is refused where a human
23
+ // can still read why rather than two hops later by a service nobody reads.
24
+ export const SEAL = 4096;
25
+ // The line a device shows when it was woken while nobody was looking,
26
+ // composed from one object. A pushed object is whatever the far being sent,
27
+ // so the two names a notification has are taken when they are there and the
28
+ // object is shown as it is when they are not.
29
+ export function banner(o) {
30
+ const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : '');
31
+ const title = text(o.title) || text(o.from) || 'Nervur';
32
+ const body = text(o.body) ||
33
+ text(o.said) ||
34
+ Object.entries(o)
35
+ .filter(([k]) => k !== 'title' && k !== 'from')
36
+ .map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
37
+ .join(', ');
38
+ return { title, body };
39
+ }
40
+ // A banner sealed to a device's banner key, as hex, or null when it will
41
+ // not seal or will not fit. The world seals and the process that draws the
42
+ // banner opens it; no doorbell, no bell service and no platform between
43
+ // them holds either half, which is why a ring may carry a line at all.
44
+ export async function sealBanner(line, key) {
45
+ if (!/^[0-9a-f]{64}$/.test(key))
46
+ return null;
47
+ try {
48
+ const boxed = await arithmetic.box(new TextEncoder().encode(JSON.stringify(line)), arithmetic.unhex(key), globalThis.crypto.getRandomValues(new Uint8Array(32)));
49
+ const wire = arithmetic.hex(boxed.bytes);
50
+ return wire.length > SEAL ? null : wire;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
@@ -1,4 +1,4 @@
1
- import { type Ring } from './push.ts';
1
+ import { type Ring } from './ring.ts';
2
2
  export declare function routed(fallback?: string): Ring;
3
3
  export declare function handedTo(at: string): Ring;
4
4
  export declare function ringing(rings: Record<string, Ring>, apps?: readonly string[]): (request: Request) => Promise<Response>;
@@ -26,7 +26,7 @@
26
26
  // own. An account here would be a registry of worlds, which is the one
27
27
  // thing the app is built not to have. The day this is abused it wants a
28
28
  // rate limit at the edge, which is the terrain's and not a word of Quo.
29
- import { bellOf } from './push.js';
29
+ import { bellOf } from './ring.js';
30
30
  // The ring a world holds for a store's kind: the token names its bell and
31
31
  // the ring goes there, so a phone running an app this world never shipped
32
32
  // is rung through that app's own service; a token naming none goes to the
@@ -1,5 +1,6 @@
1
1
  import type { JsonObject } from 'nervur';
2
2
  import { Faculty, type Woke } from './faculty.ts';
3
+ export declare const TIMER: import("nervur").Blueprint;
3
4
  export declare class Timer extends Faculty {
4
5
  #private;
5
6
  static asks: {
@@ -40,6 +41,7 @@ export declare class Timer extends Faculty {
40
41
  };
41
42
  };
42
43
  };
44
+ static fulfils: import("nervur").Blueprint[];
43
45
  get armed(): number | null;
44
46
  due(args: JsonObject): JsonObject;
45
47
  protected welcomed(id: string): Promise<void>;
@@ -0,0 +1,127 @@
1
+ import { contract } from './contract.js';
2
+ import { Faculty, WATCH } from './faculty.js';
3
+ const ASKS = {
4
+ watch: WATCH,
5
+ due: { description: 'bring my next round forward to this moment', input: { type: 'object', properties: { at: { type: 'number' } }, required: ['at'] } },
6
+ now: { description: 'this box’s moment', input: { type: 'object' } },
7
+ };
8
+ // What a being needs from a box that can wake her at a moment.
9
+ export const TIMER = contract('timer', ASKS);
10
+ const MINUTE = 60_000;
11
+ // A timeout further out than this is not trusted, so the arming is capped and
12
+ // made again when it goes off.
13
+ const DAY = 86_400_000;
14
+ export class Timer extends Faculty {
15
+ static asks = ASKS;
16
+ static fulfils = [TIMER];
17
+ #timeout;
18
+ #stopped = false;
19
+ #armed = null;
20
+ #round = null;
21
+ // The moment she is armed for, or null while she is asleep.
22
+ get armed() {
23
+ return this.#armed;
24
+ }
25
+ // A watcher says she is due sooner than the box knew. It only ever brings
26
+ // the round forward.
27
+ due(args) {
28
+ const at = args.at;
29
+ if (typeof at !== 'number' || !Number.isFinite(at))
30
+ return { error: 'a due carries the moment: { at }' };
31
+ const when = moment(at);
32
+ if (this.#armed === null || when < this.#armed)
33
+ this.#arm(when);
34
+ return { armed: this.#armed };
35
+ }
36
+ // Whoever was just filed is ticked at once, and what she answers is what
37
+ // the box arms for, so she is never waiting for a round nobody scheduled.
38
+ async welcomed(id) {
39
+ await this.round(this.box.now(), id);
40
+ }
41
+ // The arming went off. A moment still well ahead is the day's cap, so it is
42
+ // armed again rather than fired.
43
+ async rang() {
44
+ if (this.#stopped)
45
+ return [];
46
+ if (this.#armed !== null && this.#armed - this.box.now() > 1_000) {
47
+ this.arm(this.#armed);
48
+ return [];
49
+ }
50
+ return this.fire(this.box.now());
51
+ }
52
+ // The device's own call at a moment it names: how a suite proves a wake
53
+ // without waiting for one and how an operator makes a round happen now.
54
+ fire(now) {
55
+ return this.round(now);
56
+ }
57
+ // One round, and never two at once.
58
+ round(now, only) {
59
+ if (this.#round)
60
+ return this.#round;
61
+ const mine = this.#run(now, only);
62
+ this.#round = mine;
63
+ return mine.finally(() => {
64
+ if (this.#round === mine)
65
+ this.#round = null;
66
+ });
67
+ }
68
+ async #run(now, only) {
69
+ if (only === undefined)
70
+ this.#arm(null);
71
+ const woke = await this.woke((w) => ({ ...w.args, now }), only);
72
+ if (woke === null) {
73
+ this.stop();
74
+ return [];
75
+ }
76
+ // A moment already gone is no arming.
77
+ const named = soonest(woke);
78
+ const said = named !== null && named > this.box.now() ? named : null;
79
+ const held = this.#armed;
80
+ const at = said === null ? held : held === null ? moment(said) : Math.min(held, moment(said));
81
+ if (at !== null && at !== this.#armed)
82
+ this.#arm(at);
83
+ else if (at === null && this.#armed !== null)
84
+ this.#arm(null);
85
+ return woke;
86
+ }
87
+ #arm(when) {
88
+ if (this.#stopped)
89
+ return;
90
+ this.#armed = when;
91
+ this.arm(when);
92
+ }
93
+ // The one thing a terrain says about itself: be woken at this moment, or at
94
+ // no moment at all. Here a timeout, capped at a day, unreferenced so the box
95
+ // stays up for its work and never for this.
96
+ arm(when) {
97
+ clearTimeout(this.#timeout);
98
+ this.#timeout = undefined;
99
+ if (when === null)
100
+ return;
101
+ this.#timeout = setTimeout(() => void this.rang(), Math.min(Math.max(when - this.box.now(), 0), DAY));
102
+ this.#timeout.unref?.();
103
+ }
104
+ // The device puts the box down.
105
+ stop() {
106
+ this.#stopped = true;
107
+ this.#armed = null;
108
+ this.arm(null);
109
+ }
110
+ now() {
111
+ return { now: this.box.now() };
112
+ }
113
+ }
114
+ const moment = (at) => Math.max(at, Math.floor(at / MINUTE) * MINUTE + 500);
115
+ function soonest(woke) {
116
+ let at = null;
117
+ for (const w of woke) {
118
+ if (w.out === null || typeof w.out !== 'object')
119
+ continue;
120
+ const next = w.out.next;
121
+ if (typeof next !== 'number' || !Number.isFinite(next))
122
+ continue;
123
+ if (at === null || next < at)
124
+ at = next;
125
+ }
126
+ return at;
127
+ }
@@ -0,0 +1,5 @@
1
+ import type { BeingClass } from 'nervur';
2
+ export type Tree = {
3
+ readonly [name: string]: unknown;
4
+ };
5
+ export declare function keyed(tree: Tree, at?: string): Record<string, BeingClass>;
@@ -0,0 +1,14 @@
1
+ // Every class in a tree, keyed by where it lives: the folders down to it and
2
+ // its export name, `box/Dock`. The key is the class's identity in a ward,
3
+ // as a being's key is hers, and it moves only when the class moves.
4
+ export function keyed(tree, at = '') {
5
+ const out = {};
6
+ for (const [name, v] of Object.entries(tree)) {
7
+ const key = at ? `${at}/${name}` : name;
8
+ if (typeof v === 'function' && 'prototype' in v)
9
+ out[key] = v;
10
+ else if (v !== null && typeof v === 'object' && !Array.isArray(v))
11
+ Object.assign(out, keyed(v, key));
12
+ }
13
+ return out;
14
+ }
@@ -1,4 +1,4 @@
1
- import type { Line, Ring } from './push.ts';
1
+ import type { Line, Ring } from './ring.ts';
2
2
  export type Vapid = {
3
3
  key: JsonWebKey;
4
4
  subject: string;
@@ -1,5 +1,6 @@
1
1
  export type Answer = {
2
2
  result: unknown;
3
+ ward?: string;
3
4
  } | {
4
5
  error: string;
5
6
  };
@@ -2,9 +2,8 @@ import { DiskHarbor, type Hosted } from '../harbor/disk.ts';
2
2
  import type { Harbor } from 'nervur/harbor';
3
3
  import type { Avatar } from '../beings/index.ts';
4
4
  export { avatarOf, letIn, find } from '../beings/join.ts';
5
- import { type Ring } from '../dock/index.ts';
6
- import { Grants } from '../dock/grants.ts';
7
- import { Agents } from '../dock/agents.ts';
5
+ import { type Ring } from '../box/index.ts';
6
+ import { Agents, Grants } from '../box/index.ts';
8
7
  import type { OAuth } from '../mcp/oauth.ts';
9
8
  import { Exchange } from '../mcp/allow.ts';
10
9
  import { McpHttp } from '../mcp/http.ts';
@@ -43,8 +42,3 @@ export declare function admit(harbor: Harbor, hosted: Hosted, identity: string,
43
42
  error?: string;
44
43
  }>;
45
44
  export declare function serve(dir: string, options?: Options): Promise<Serving>;
46
- export declare const DOMAIN: RegExp;
47
- export declare function routesFor(domain: string): Required<Routes>;
48
- export declare function writeRoutes(dir: string, domain: string): Promise<Required<Routes>>;
49
- export declare function readRoutes(dir: string): Promise<Routes | null>;
50
- export declare function readApps(dir: string): Promise<Apps | null>;