@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
@@ -1,149 +0,0 @@
1
- import { Faculty } from './faculty.js';
2
- const MINUTE = 60_000;
3
- // A timeout further out than this is not trusted, so the arming is capped and
4
- // made again when it goes off. It is the base class's own bound and no rule
5
- // of the faculty: a terrain whose alarm holds a month arms for the month.
6
- const DAY = 86_400_000;
7
- export class Timer extends Faculty {
8
- static asks = {
9
- watch: { description: 'wake me when I say I am next due', input: { type: 'object', properties: { invitation: { type: 'object' }, method: { type: 'string' }, args: { type: 'object' } }, required: ['invitation', 'method'] } },
10
- due: { description: 'bring my next round forward to this moment', input: { type: 'object', properties: { at: { type: 'number' } }, required: ['at'] } },
11
- now: { description: 'this box’s moment', input: { type: 'object' } },
12
- };
13
- #timeout;
14
- #stopped = false;
15
- #armed = null;
16
- #round = null;
17
- // The moment she is armed for, or null while she is asleep. For the
18
- // device's own code and for a suite; it crosses no door.
19
- get armed() {
20
- return this.#armed;
21
- }
22
- // A watcher says she is due sooner than the box knew. It only ever brings
23
- // the round forward: what fires is decided at the tick, in the watcher's
24
- // own cells, so an arming that moved later on somebody's word would be a
25
- // schedule somebody else held silenced. The door is the whole gate here:
26
- // a standing at a faculty comes from `lend` alone, and this adds no second
27
- // one.
28
- due(args) {
29
- const at = args.at;
30
- if (typeof at !== 'number' || !Number.isFinite(at))
31
- return { error: 'a due carries the moment: { at }' };
32
- const when = moment(at);
33
- if (this.#armed === null || when < this.#armed)
34
- this.#arm(when);
35
- return { armed: this.#armed };
36
- }
37
- // Whoever was just filed is ticked at once, and what she answers is what
38
- // the box arms for. One move covers a box booting, a home moving in and a
39
- // watcher born long after the box was up: she is never waiting for a round
40
- // nobody scheduled.
41
- async welcomed(id) {
42
- await this.round(Date.now(), id);
43
- }
44
- // The arming went off. A moment still well ahead is the day's cap this
45
- // class put on its own timeout, so it is armed again rather than fired: a
46
- // terrain calls this and never has to know how far out the arming was.
47
- async rang() {
48
- if (this.#stopped)
49
- return [];
50
- if (this.#armed !== null && this.#armed - Date.now() > 1_000) {
51
- this.arm(this.#armed);
52
- return [];
53
- }
54
- return this.fire(Date.now());
55
- }
56
- // The moment, read here and handed on. A device's own code may call it at a
57
- // moment it names, which is how a suite proves a wake without waiting for
58
- // one and how an operator makes a round happen now.
59
- fire(now) {
60
- return this.round(now);
61
- }
62
- // One round, and never two: a second while one is in flight is handed the
63
- // one already running. What every watcher answered decides the next
64
- // arming, and a `due` that landed while the round ran is not lost.
65
- round(now, only) {
66
- if (this.#round)
67
- return this.#round;
68
- const mine = this.#run(now, only);
69
- this.#round = mine;
70
- return mine.finally(() => {
71
- if (this.#round === mine)
72
- this.#round = null;
73
- });
74
- }
75
- async #run(now, only) {
76
- if (only === undefined)
77
- this.#arm(null); // the arming is spent, whether it went off or the device called this by hand
78
- const woke = await this.woke((w) => ({ ...w.args, now }), only);
79
- if (woke === null) {
80
- this.stop(); // her ward took her out; there is nobody left to wake
81
- return [];
82
- }
83
- // A moment already gone is no arming. Every watcher works out when she is
84
- // next due from the moment she was handed, so a round fired by hand at a
85
- // moment that is not this box's answers one that is not this box's
86
- // either, and a box that armed for it would run a round for a minute long
87
- // past. A round at the box's own moment always names one still ahead.
88
- const named = soonest(woke);
89
- const said = named !== null && named > Date.now() ? named : null;
90
- const held = this.#armed;
91
- const at = said === null ? held : held === null ? moment(said) : Math.min(held, moment(said));
92
- if (at !== null && at !== this.#armed)
93
- this.#arm(at);
94
- else if (at === null && this.#armed !== null)
95
- this.#arm(null);
96
- return woke;
97
- }
98
- #arm(when) {
99
- if (this.#stopped)
100
- return;
101
- this.#armed = when;
102
- this.arm(when);
103
- }
104
- // The one thing a terrain says about itself: be woken at this moment, or at
105
- // no moment at all. A subclass replaces this and nothing else of her.
106
- //
107
- // Here it is a timeout, capped at a day and armed again when the cap goes
108
- // off. The box stays up for its work and never for this, so the handle is
109
- // unreferenced.
110
- arm(when) {
111
- clearTimeout(this.#timeout);
112
- this.#timeout = undefined;
113
- if (when === null)
114
- return;
115
- this.#timeout = setTimeout(() => void this.rang(), Math.min(Math.max(when - Date.now(), 0), DAY));
116
- this.#timeout.unref?.();
117
- }
118
- // A being her ward took out reaches nobody, so nothing is left to wake. The
119
- // device calls this when it puts the box down.
120
- stop() {
121
- this.#stopped = true;
122
- this.#armed = null;
123
- this.arm(null);
124
- }
125
- now() {
126
- return { now: Date.now() };
127
- }
128
- }
129
- // Half a second past the minute a moment falls in, and never before the
130
- // moment itself: a watcher floors what she is handed, so a round that fired a
131
- // hair early would floor to the minute before and fire nothing.
132
- const moment = (at) => Math.max(at, Math.floor(at / MINUTE) * MINUTE + 500);
133
- // What one round learned. Every watcher says when she is next due, and the
134
- // box arms for the earliest of them. A watcher who named no moment has
135
- // nothing to be woken for, and so has one who answered something that is no
136
- // tally at all.
137
- function soonest(woke) {
138
- let at = null;
139
- for (const w of woke) {
140
- if (w.out === null || typeof w.out !== 'object')
141
- continue;
142
- const next = w.out.next;
143
- if (typeof next !== 'number' || !Number.isFinite(next))
144
- continue;
145
- if (at === null || next < at)
146
- at = next;
147
- }
148
- return at;
149
- }
package/dock/console.ts DELETED
@@ -1,23 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The box, as one being. A device has no surface of its own, so a box that
3
- // has joined no world has nothing to draw: the Console is that page and only
4
- // that. She carries the faculties her harbor stood, showing each one's asks
5
- // as her own, and the ordinary screen draws her exactly as it draws a
6
- // world's user being, through one avatar holding one standing.
7
- //
8
- // She exists because a fresh install otherwise asks for location and
9
- // notifications in its manifest and shows nothing that ever asks for them,
10
- // which is a store reviewer's first question and a human's second.
11
- //
12
- // She carries for whoever the harbor's root let in, and for nobody else: the
13
- // root is the box, and it invites the one avatar its own screen runs.
14
- import { Carrier } from '../beings/carry.ts';
15
- import type { Asker, OccupantRecord } from 'nervur';
16
-
17
- export const CONSOLE = 'console';
18
-
19
- export class Console extends Carrier {
20
- static override carries(occupant: OccupantRecord | undefined, _asker: Asker): boolean {
21
- return occupant !== undefined;
22
- }
23
- }
package/dock/dialers.ts DELETED
@@ -1,85 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The box's dialers: `dialers`, one faculty of the dock ward, standing for
3
- // the sockets this box holds open to other harbors' `quo.` routes. A Mac
4
- // behind NAT, a phone on the mobile network, a droplet dialing home: each
5
- // is a dialer, and what it dials is a row in her cells, placed once by the
6
- // root with `keep` the way a schedule is placed on the clock and an agent
7
- // on the agents faculty, in no file.
8
- //
9
- // A row is one URL, the `quo.` route of the harbor dialed. She holds the
10
- // socket from her birth on, as every faculty holds what the box holds: one
11
- // line per row, announcing this harbor's wards on open, binding the far
12
- // side's pks, reconnecting with backoff when the line drops, and closed
13
- // when the row is dropped or the box goes down. The dialer itself is the
14
- // library's, written on the standard socket surface, so this faculty holds
15
- // on every terrain that has a socket; the daemon hands her the harbor to
16
- // dial for, as it hands the agents faculty how to run a row.
17
- import { OWNER } from 'nervur';
18
- import type { Asker, JsonObject, OccupantRecord } from 'nervur';
19
- import { dial, type Dialer, type Harbor } from 'nervur/harbor';
20
- import { Faculty } from './faculty.ts';
21
-
22
- // The name the dock ward holds her under.
23
- export const DIALERS = 'dialers';
24
-
25
- export type Row = { url: string };
26
-
27
- // The root and nobody else: a line held open is the device's own to place.
28
- const root = (_occ: OccupantRecord | undefined, asker: Asker) => asker.id === OWNER;
29
-
30
- const URL_ = { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] };
31
-
32
- export class Dialers extends Faculty {
33
- static override cells: JsonObject = { watched: {}, minted: 0, dialers: {} };
34
- static override asks = {
35
- keep: { description: 'hold a socket to that quo. route, from now and from every boot, until it is dropped', input: URL_, for: root },
36
- drop: { description: 'close the socket to that route and forget it', input: URL_, for: root },
37
- list: { description: 'every route she dials, and whether the line is open now', input: { type: 'object' }, for: root },
38
- };
39
-
40
- harbor: Harbor | undefined;
41
- readonly open = new Map<string, Dialer>();
42
-
43
- private get rows(): Record<string, Row> {
44
- return this.cells.dialers as unknown as Record<string, Row>;
45
- }
46
-
47
- // Every row dialed, once the daemon has handed the harbor to dial for.
48
- start(harbor: Harbor): void {
49
- this.harbor = harbor;
50
- for (const row of Object.values(this.rows)) this.#dial(row.url);
51
- }
52
-
53
- #dial(url: string): void {
54
- if (!this.harbor || this.open.has(url)) return;
55
- this.open.set(url, dial(this.harbor, url));
56
- }
57
-
58
- keep(args: JsonObject): JsonObject {
59
- const url = args.url;
60
- if (typeof url !== 'string' || !/^https?:\/\/\S+$/.test(url)) return { error: 'url is the quo. route of the harbor dialed, over http or https' };
61
- const clean = url.replace(/\/$/, '');
62
- this.rows[clean] = { url: clean };
63
- this.#dial(clean);
64
- return { kept: clean };
65
- }
66
-
67
- drop(args: JsonObject): JsonObject {
68
- const url = typeof args.url === 'string' ? args.url.replace(/\/$/, '') : '';
69
- if (!this.rows[url]) return { error: 'no such route' };
70
- this.open.get(url)?.close();
71
- this.open.delete(url);
72
- delete this.rows[url];
73
- return { dropped: url };
74
- }
75
-
76
- list(): JsonObject {
77
- return { dialers: Object.keys(this.rows).map((url) => ({ url, open: this.open.get(url)?.socket !== null && this.open.has(url) })) };
78
- }
79
-
80
- // The device puts the box down: every line closes, the rows stay.
81
- close(): void {
82
- for (const d of this.open.values()) d.close();
83
- this.open.clear();
84
- }
85
- }
package/dock/faculty.ts DELETED
@@ -1,148 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // A faculty: one being of the dock ward, standing for one thing the box can
3
- // do. She is a being and nothing else, so what is here is not a second kind
4
- // of object but the two moves every faculty shares.
5
- //
6
- // Out: whoever holds a standing at her asks her methods, and the occupant's
7
- // notes say what that caller is, a prefix on a store, an account at a
8
- // vendor. Her own cells say what the thing is.
9
- //
10
- // Back: `watch` is the one subscribe verb every faculty has. A being who
11
- // wants waking mints an invitation and hands it over, this takes it as a
12
- // standing, and from then on she is asked at her own door as the occupant
13
- // she named. `occupants.remove` on her side is the whole of unsubscribe: the
14
- // next wake hears `removed` and the row goes.
15
- //
16
- // She holds what the box holds and begins holding it at birth: the
17
- // connection, the interval, the worker, the route are opened in her
18
- // constructor, and she is born whether or not the thing is reachable,
19
- // answering an error object while it is not.
20
- import { Being, isInvitation, isSilence, isWord, wordOf } from 'nervur';
21
- import type { Asker, JsonObject } from 'nervur';
22
-
23
- // One subscription, as she files it: her own id for the relation, who asked
24
- // for it, and what to ask them.
25
- export type Watched = { by: string; method: string; args: JsonObject };
26
-
27
- // What one round of waking did, per subscription.
28
- export type Woke = { id: string; by: string; out: unknown };
29
-
30
- export class Faculty extends Being {
31
- static override cells: JsonObject = { watched: {}, minted: 0 };
32
-
33
- // Someone wants waking. She hands the invitation she minted and says what
34
- // to ask her; this knocks it, takes it under an id of this being's own,
35
- // and files it. The id never crosses: it is this side's name for the
36
- // relation, and the caller's name for the same relation is her own.
37
- //
38
- // The empty ask is the knock, because every being answers one and asking
39
- // for a named ask would make every subscriber owe a method she does not
40
- // otherwise need.
41
- async watch(args: JsonObject, asker: Asker): Promise<JsonObject> {
42
- const inv = args.invitation;
43
- if (!isInvitation(inv) || typeof args.method !== 'string') return { error: 'an invitation and a method' };
44
- const extra = args.args;
45
- if (extra !== undefined && (extra === null || typeof extra !== 'object' || Array.isArray(extra))) return { error: 'args is one object' };
46
- const out = await this.knock(inv);
47
- if (isSilence(out) || isWord(out)) return { error: 'no way back' };
48
- const cells = this.cells as unknown as { watched: Record<string, Watched>; minted: number };
49
- const id = await this.take(`wake:${(cells.minted += 1)}`, inv);
50
- if (id === null) return { error: 'not taken' };
51
- cells.watched[id] = { by: asker.id ?? '', method: args.method, args: extra ?? {} };
52
- await this.welcomed(id);
53
- return { watching: id };
54
- }
55
-
56
- // Somebody new is filed, and the answer to her `watch` waits for this. A
57
- // faculty that owes a new subscriber something at once says it here: the
58
- // timer wakes her, learns when she is next due and arms the box for it, so
59
- // a being born long after the box was up is not waiting for a round nobody
60
- // scheduled. Most faculties owe nothing, and this is why they say nothing.
61
- protected async welcomed(_id: string): Promise<void> {}
62
-
63
- // One round of waking. Each subscriber is asked what she asked to be
64
- // asked, with whatever this round adds. A subscriber who removed the
65
- // occupant says `removed` once, and her row goes with the standing; there
66
- // is nothing to retry and nothing to remember.
67
- //
68
- // `only` is one subscriber of the round's own choosing, named by this
69
- // being's own id for the relation, and it is the same round down to one
70
- // row: a faculty that wakes somebody just filed learns exactly what a full
71
- // round would have learned about her.
72
- //
73
- // `dropped` is this being herself: her ward took her out, so the standings
74
- // she holds reach nobody. Null says so, and whoever holds the interval puts
75
- // it down rather than waking an empty room for as long as the box is up.
76
- protected async woke(round: (w: Watched) => JsonObject = (w) => w.args, only?: string): Promise<Woke[] | null> {
77
- const cells = this.cells as unknown as { watched: Record<string, Watched> };
78
- const out: Woke[] = [];
79
- for (const [id, w] of Object.entries(cells.watched)) {
80
- if (only !== undefined && id !== only) continue;
81
- const said = await this.standings[id]?.ask(w.method, round(w));
82
- if (said === undefined || (isWord(said) && wordOf(said) === 'dropped')) return null;
83
- if (isWord(said) && wordOf(said) === 'removed') {
84
- this.standings.remove(id);
85
- delete cells.watched[id];
86
- continue;
87
- }
88
- out.push({ id, by: w.by, out: said });
89
- }
90
- return out;
91
- }
92
-
93
- // Who is waiting, by this being's own id for the relation. For a device's
94
- // own code and for a suite; it crosses no door.
95
- get watching(): Record<string, Watched> {
96
- return this.cells.watched as unknown as Record<string, Watched>;
97
- }
98
- }
99
-
100
- // The three moves a being makes at every birth to hold what her box lends.
101
- // A standing points at a being and not at a box, so one she wakes up holding
102
- // names a being on the box she was on when it was made; a restart and a
103
- // migration are the same silent event to her, so she does these again every
104
- // time she is constructed and is always pointed at the box she is on.
105
- //
106
- // `watch` says she wants waking too: her ward mints the occupant, the
107
- // faculty takes the standing it makes, and from then on the wake arrives at
108
- // her door under the id she chose.
109
- // Every join in flight on this box. A being takes what her box lends at her
110
- // birth, and birth is synchronous while taking a standing is not, so a
111
- // harbor that answered "up" before they landed would be a box whose first
112
- // tick woke nobody and whose first ring reached no platform. The terrain
113
- // waits on this once, after its wards are hosted; nothing else reads it, and
114
- // a join that fails leaves nothing behind but a being who asks again.
115
- const flight = new Set<Promise<unknown>>();
116
- export const settled = async (): Promise<void> => {
117
- while (flight.size) {
118
- const now = [...flight];
119
- await Promise.allSettled(now);
120
- for (const p of now) flight.delete(p);
121
- }
122
- };
123
-
124
- export function join(b: Being, name: string, watch?: { method: string; args?: JsonObject }, id = name): Promise<string | null> {
125
- const p = joining(b, name, watch, id);
126
- flight.add(p);
127
- void p.catch(() => {});
128
- return p;
129
- }
130
-
131
- async function joining(b: Being, name: string, watch: { method: string; args?: JsonObject } | undefined, id: string): Promise<string | null> {
132
- // One turn, before anything is asked. A being is called at her door and
133
- // holds her stance from her first line, but her ward sets that door after
134
- // her constructor returns, so a stance call made inside it reaches a being
135
- // her ward does not hold yet and is answered as one taken out. Every
136
- // faculty relation is made at birth, so the wait belongs here rather than
137
- // in each of them.
138
- await Promise.resolve();
139
- b.standings.remove(id);
140
- b.occupants.remove(`${id}:wake`);
141
- const got = await b.lend(name, id);
142
- if (got === null) return null;
143
- if (!watch) return got;
144
- const back = await b.invite(`${id}:wake`);
145
- if (!back) return got;
146
- await b.standings[id]?.ask('watch', { invitation: back, method: watch.method, ...(watch.args ? { args: watch.args } : {}) });
147
- return got;
148
- }
package/dock/grants.ts DELETED
@@ -1,111 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The box's grants: `grants`, one faculty of the dock ward. A bearer to an
3
- // identity in a world, honoured on every route alike. A route says the
4
- // grammar a caller speaks, forms, tools or JSON, and a grant says who the
5
- // caller is; no route holds a table of its own. She spans every world the
6
- // box hosts, and a grant maps a token to an identity and nothing else: the
7
- // credential is the invitation the avatar knocked with, spent once, and a
8
- // grant only remembers which avatar a client speaks through. No secret of
9
- // Quo's is ever here.
10
- //
11
- // Her tokens are her cells, kept by the dock ward's partition like every
12
- // faculty's and by no file of her own, so a box has one place its state is
13
- // and the harbor's one secret wraps them. Issuing, refreshing and revoking
14
- // are the routes' to call on her in this process, as the daemon mints a
15
- // nonce on `local`: none is an ask, none crosses a door, and she answers
16
- // nothing at hers.
17
- //
18
- // A grant is issued by whoever finished a way in: the OAuth exchange on the
19
- // model side, or the api route trading an invitation straight for tokens.
20
- // An access token lives an hour and a refresh token thirty days, and
21
- // refresh rotates. Revocation is the user being removing the occupant,
22
- // and the route that hears `removed` asks for the grants to go with it.
23
- import { Faculty } from './faculty.ts';
24
-
25
- // The name the dock ward holds her under.
26
- export const GRANTS = 'grants';
27
-
28
- export const ACCESS_TTL = 60 * 60 * 1000; // an access token, one hour
29
- export const REFRESH_TTL = 30 * 24 * 60 * 60 * 1000; // a refresh token, thirty days
30
-
31
- // A token is a string the far side offers, and a bare lookup would find
32
- // `__proto__` and hand back an object with no identity. Own keys only.
33
- const own = <T>(rec: Record<string, T>, key: string): T | undefined => (Object.hasOwn(rec, key) ? rec[key] : undefined);
34
- // Thirty-two random bytes as base64url, from the web crypto and the base64
35
- // every runtime has, so nothing of node stands in a file a tab bundles or a
36
- // worker imports. `Buffer` is not among those: a worker has one only when a
37
- // deploy asks for node's shims, and the box's grants are the same code on
38
- // every terrain.
39
- const token = () => {
40
- const bytes = globalThis.crypto.getRandomValues(new Uint8Array(32));
41
- let binary = '';
42
- for (const b of bytes) binary += String.fromCharCode(b);
43
- return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
44
- };
45
-
46
- // A grant names the world the identity was let into, since one harbor holds
47
- // many, and the client it was issued to: an OAuth client id, or the route
48
- // that traded for it.
49
- export type Grant = { identity: string; ward: string; client: string; exp: number };
50
- type Kept = { access: Record<string, Grant>; refresh: Record<string, Grant> };
51
- export type Tokens = { access_token: string; token_type: 'Bearer'; expires_in: number; refresh_token: string; scope: 'nervur' };
52
-
53
- export class Grants extends Faculty {
54
- static override cells = { ...Faculty.cells, access: {}, refresh: {} };
55
- static override asks = {};
56
-
57
- // The ward's save, set by whoever holds her ward: a token issued is a
58
- // cell written, and the routes answer with a token the partition keeps.
59
- persist: () => Promise<void> = async () => {};
60
- // The clock, a suite's to move.
61
- now: () => number = () => Date.now();
62
-
63
- get kept(): Kept {
64
- return this.cells as unknown as Kept;
65
- }
66
-
67
- async issue(identity: string, ward: string, client: string): Promise<Tokens> {
68
- const access_token = token(),
69
- refresh_token = token();
70
- this.kept.access[access_token] = { identity, ward, client, exp: this.now() + ACCESS_TTL };
71
- this.kept.refresh[refresh_token] = { identity, ward, client, exp: this.now() + REFRESH_TTL };
72
- this.sweep();
73
- await this.persist();
74
- return { access_token, token_type: 'Bearer', expires_in: ACCESS_TTL / 1000, refresh_token, scope: 'nervur' };
75
- }
76
-
77
- // Rotated: the old one is gone with the new one's birth. Null is a token
78
- // nobody issued, or one that died.
79
- async refresh(refresh_token: string): Promise<Tokens | null> {
80
- const r = own(this.kept.refresh, refresh_token);
81
- if (!r || r.exp < this.now()) return null;
82
- delete this.kept.refresh[refresh_token];
83
- return this.issue(r.identity, r.ward, r.client);
84
- }
85
-
86
- // The bearer on a request, to an identity in a world. Null is a stranger,
87
- // or a token that died; a route tells the two apart by whether the
88
- // request offered one at all.
89
- bearer(req: Request): { identity: string; ward: string } | null {
90
- const g = own(this.kept.access, offered(req));
91
- return g && g.exp >= this.now() && typeof g.identity === 'string' ? { identity: g.identity, ward: g.ward } : null;
92
- }
93
-
94
- // Every grant an identity holds in a world, gone: the box's half of
95
- // revocation. The other half is the user being removing the occupant.
96
- async revoke(identity: string, ward: string): Promise<void> {
97
- for (const k of ['access', 'refresh'] as const) for (const [t, g] of Object.entries(this.kept[k])) if (g.identity === identity && g.ward === ward) delete this.kept[k][t];
98
- await this.persist();
99
- }
100
-
101
- sweep(): void {
102
- const now = this.now();
103
- for (const k of ['access', 'refresh'] as const) for (const [t, g] of Object.entries(this.kept[k])) if (!(g.exp >= now)) delete this.kept[k][t];
104
- }
105
- }
106
-
107
- // What a request offered as its bearer, or nothing.
108
- export const offered = (req: Request): string => {
109
- const h = req.headers.get('authorization') ?? '';
110
- return h.startsWith('Bearer ') ? h.slice(7) : '';
111
- };
package/dock/index.ts DELETED
@@ -1,133 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The dock ward: the ward a harbor boots and roots for itself, holding what
3
- // the box can do. `stand` puts it up and answers the `lend` a harbor hands
4
- // its other wards, which is the whole of what a terrain says about itself.
5
- //
6
- // Which names a box lends is the terrain's word; which ward may ask for one
7
- // is the harbor's, decided in `lendFor`, and a ward whose code came from a
8
- // stranger is lent nothing.
9
- import type { Harbor, Lend } from 'nervur/harbor';
10
- import type { JsonObject } from 'nervur';
11
- import { isInvitation } from 'nervur';
12
- import { Faculty } from './faculty.ts';
13
- import { Console, CONSOLE } from './console.ts';
14
- import { Local } from './local.ts';
15
- import { Location } from './location.ts';
16
- import { Notifications } from './notifications.ts';
17
- import { Push } from './push.ts';
18
- import { Timer } from './timer.ts';
19
-
20
- export { Console, CONSOLE } from './console.ts';
21
- export { Faculty, join, settled, type Watched, type Woke } from './faculty.ts';
22
- export { Local, LOCAL } from './local.ts';
23
- export { Location, type Where } from './location.ts';
24
- export { Notifications } from './notifications.ts';
25
- export { Push, isToken, bellOf, banner, sealBanner, SEAL, type Token, type Ring, type Line } from './push.ts';
26
- export { Timer } from './timer.ts';
27
- // The three grounds the push faculty rings through, and the bell service
28
- // that holds a publisher's keys: what the box can do, beside the faculty.
29
- export { webPush, mintVapid, publicOf, type Vapid } from './webpush.ts';
30
- export { apnsPush, pkcs8, HOSTS, type Apns } from './apns.ts';
31
- export { fcmPush, isAccount, DOORS, type Account, type Doors } from './fcm.ts';
32
- export { handedTo, ringing, routed } from './service.ts';
33
-
34
- // The names this package lends under. They are conventions of the dock and
35
- // nothing of Quo's: a name is the harbor's namespace, and a being holds each
36
- // one under an id of her own choosing.
37
- export const LOCATION = 'location';
38
- export const NOTIFICATIONS = 'notifications';
39
- export const PUSH = 'push';
40
- export const TIMER = 'timer';
41
-
42
- // The classes every dock ward may hold, by class name. A terrain hands its
43
- // own subclasses under the same names and the harbor holds those instead.
44
- // `local` and `grants` are not among them: the box's way in and the box's
45
- // grants stand only where a terrain serves routes, and that terrain adds
46
- // them to what it lends. `Grants` is not in this file at all, so that a tab
47
- // bundling this index carries no route's notion: the terrain that serves
48
- // routes hands her class to its harbor, as the daemon hands its own push.
49
- export const FACULTIES: Record<string, string> = { [LOCATION]: 'Location', [NOTIFICATIONS]: 'Notifications', [PUSH]: 'Push', [TIMER]: 'Timer' };
50
- export const FACULTY_CLASSES = { Console, Local, Location, Notifications, Push, Timer };
51
-
52
- // The name a harbor keeps its own ward under. It is a ward in every other
53
- // respect, and this one is never adopted or dropped as a migration: its
54
- // beings are the box's and die with it.
55
- export const DOCK = 'dock';
56
-
57
- // Put the dock ward up and answer the `lend` this harbor hands its wards.
58
- // `lent` says which names this box offers and which class stands for each;
59
- // a name it does not hold is null, and so is every other kind of no.
60
- //
61
- // The harbor keeps this ward's ask pointer rather than handing it out, so it
62
- // is the root here, and the root may invite on any being of its ward. That
63
- // is the whole of what `lend` does: it never mints, it asks its own root to.
64
- export async function stand(harbor: Harbor, lent: Record<string, string> = FACULTIES): Promise<Lend> {
65
- const kept = await harbor.store.load(DOCK);
66
- const ward = harbor.wards.get(DOCK) ?? (kept ? await harbor.host(DOCK, kept) : await harbor.create(DOCK, CONSOLE));
67
- return standOn(ward, lent);
68
- }
69
-
70
- // A ward as its root holds it: the ask pointer, and a save where the terrain
71
- // keeps one. Every harbor here has both; a memory harbor has the first.
72
- export type Rooted = { ask(method?: string, args?: Record<string, unknown>): Promise<unknown>; save?(): Promise<void> };
73
-
74
- // The faculties up in a ward already made, and the `lend` that reaches them.
75
- // Apart from `stand` so a suite can put its own stubs in its own ward and
76
- // get the same thing a box gets. It is run again at every boot and says the
77
- // same thing twice without harm: a key already booted is refused, and an
78
- // invitation on an id already held is null.
79
- export async function standOn(ward: Rooted, lent: Record<string, string> = FACULTIES): Promise<Lend> {
80
- for (const [name, className] of Object.entries(lent)) await ward.ask('boot', { key: name, class: className });
81
- // The box as one being, carrying what it stood. She is the page a device
82
- // that has joined no world shows, and she is placed by the root because
83
- // the root is the box: a standing at every faculty, minted and knocked in
84
- // one move, as an owner places any relation into a being of its ward.
85
- await ward.ask('boot', { key: CONSOLE, class: 'Console' });
86
- for (const name of Object.keys(lent)) {
87
- const inv = await ward.ask('invite', { being: name, id: `${CONSOLE}:${name}` });
88
- if (!isInvitation(inv)) continue;
89
- await ward.ask('knock', { being: CONSOLE, id: name, invitation: inv as unknown as JsonObject });
90
- }
91
- await ward.save?.();
92
- return lending(ward);
93
- }
94
-
95
- // The root's half alone: a name and a taker in, whether the ward took it out.
96
- // It never mints; it asks the root of the ward it holds to, which is what a
97
- // root may do on any being of its ward. And it unmints: a taker that would
98
- // not take leaves an occupant on that faculty which nobody holds, so the root
99
- // removes it before answering, and a refused lend costs the box nothing.
100
- //
101
- // Any being of the dock ward is lent by her key, not only the ones a terrain
102
- // stood: the faculties the dock ships are examples, and an estate boots its
103
- // own into the dock ward, of a class in its own `classes/`, and lends it the
104
- // same way. A name nobody there answers to is refused at the invite.
105
- //
106
- // The id is drawn and never counted. A faculty's occupants are in her cells
107
- // and outlive the process, so a count that started again at every boot would
108
- // name, on the first lend after a restart, an id she already holds, and the
109
- // invite would be refused for the rest of the box's life.
110
- export function lending(ward: Rooted): Lend {
111
- return async (name, take) => {
112
- const id = `lent:${fresh()}`;
113
- const out = await ward.ask('invite', { being: name, id });
114
- if (!isInvitation(out)) return false;
115
- const took = await take(out);
116
- if (!took) await ward.ask('remove', { being: name, id });
117
- await ward.save?.();
118
- return took;
119
- };
120
- }
121
-
122
- const fresh = (): string =>
123
- Array.from(globalThis.crypto.getRandomValues(new Uint8Array(8)))
124
- .map((b) => b.toString(16).padStart(2, '0'))
125
- .join('');
126
-
127
- // One faculty of this box, as the device's own code holds it: the object the
128
- // harbor made, in this process. Not a path around a door, and nothing of it
129
- // crosses an edge; it is how a device fires its own timer at a moment it
130
- // names and how a suite reads what a stub recorded.
131
- export function facultyOf<F extends Faculty>(harbor: Harbor, name: string): F | undefined {
132
- return harbor.wards.get(DOCK)?.being(name) as F | undefined;
133
- }