@nervur-org/dock 0.5.0 → 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 (216) hide show
  1. package/README.md +7 -4
  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 +9 -8
  192. package/beings/envoy.ts +0 -218
  193. package/dist/beings/envoy.d.ts +0 -54
  194. package/dist/beings/envoy.js +0 -211
  195. package/dist/dock/console.d.ts +0 -6
  196. package/dist/dock/console.js +0 -20
  197. package/dist/dock/dialers.js +0 -75
  198. package/dist/dock/faculty.d.ts +0 -24
  199. package/dist/dock/faculty.js +0 -142
  200. package/dist/dock/grants.js +0 -99
  201. package/dist/dock/index.d.ts +0 -41
  202. package/dist/dock/index.js +0 -113
  203. package/dist/dock/local.js +0 -44
  204. package/dist/dock/location.js +0 -11
  205. package/dist/dock/push.js +0 -132
  206. package/dist/dock/timer.js +0 -149
  207. package/dock/console.ts +0 -23
  208. package/dock/dialers.ts +0 -85
  209. package/dock/faculty.ts +0 -148
  210. package/dock/grants.ts +0 -111
  211. package/dock/index.ts +0 -133
  212. package/dock/local.ts +0 -48
  213. package/dock/push.ts +0 -161
  214. package/dock/timer.ts +0 -174
  215. /package/dist/{dock → box}/fcm.js +0 -0
  216. /package/dist/{dock → box}/webpush.js +0 -0
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
- }
package/dock/local.ts DELETED
@@ -1,48 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The box's way in: `local`, one faculty of the dock ward. The daemon saw a
3
- // process reach a socket only the device's user can open, and that is the
4
- // whole of the vouch; file permissions are the device's rules, which is what
5
- // `quo/SPEC.md` allows a harbor to hand the root by.
6
- //
7
- // She holds the nonces this box minted, in this process and never in cells:
8
- // a vouch that outlived a restart would be a vouch for a process that is
9
- // gone. Each is honoured once, and a `local` proof arriving any other way
10
- // names no nonce and is refused.
11
- //
12
- // The device's own code mints on her directly, as a device fires its own
13
- // timer: `mint` is not an ask, it crosses no door, and nothing at a door
14
- // can reach it. The desk asks `verify` at her standing, as she asks any way
15
- // in.
16
- import { Faculty } from './faculty.ts';
17
- import type { JsonObject } from 'nervur';
18
- import { VERIFY, type Verified } from '../beings/way.ts';
19
-
20
- // The name the dock ward lends her under. A box with no local side lends no
21
- // such name, and the desk there holds no such standing.
22
- export const LOCAL = 'local';
23
-
24
- export class Local extends Faculty {
25
- static override asks = { verify: VERIFY };
26
-
27
- readonly #nonces = new Map<string, Verified>();
28
-
29
- // A nonce for one client this box saw itself, spent by the first proof
30
- // that names it. The caller drops it when the admit is over: a nonce that
31
- // was never spent is not a way in that stays open.
32
- mint(nonce: string, who: Verified): void {
33
- this.#nonces.set(nonce, who);
34
- }
35
- drop(nonce: string): void {
36
- this.#nonces.delete(nonce);
37
- }
38
-
39
- verify(args: JsonObject): Verified | null {
40
- const proof = args.proof;
41
- if (!proof || typeof proof !== 'object' || Array.isArray(proof)) return null;
42
- const nonce = (proof as JsonObject).nonce;
43
- const who = typeof nonce === 'string' ? this.#nonces.get(nonce) : undefined;
44
- if (!who) return null;
45
- this.#nonces.delete(nonce as string);
46
- return who;
47
- }
48
- }
package/dock/push.ts DELETED
@@ -1,161 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // Waking a device that is not reachable, the one way its platform allows: a
3
- // push that carries the device's token and the banner sealed to that
4
- // device's own key, which nothing between the world and the phone can
5
- // read. A token names its
6
- // kind, and a kind is one ring: `web` is Web Push under a keypair a world
7
- // mints itself, `apns` and `fcm` are the two stores' own, handed on to the
8
- // publisher's bell service by a world that holds no platform key. A box that
9
- // was given no ring for a kind says so, which is what a world that rings no
10
- // iPhones says to one.
11
- //
12
- // The rings are live objects and not values, so they are not cells: a
13
- // terrain subclasses and builds its own in `rings`, out of what its
14
- // environment and this being's cells give it.
15
- import { OWNER } from 'nervur';
16
- import { arithmetic } from 'nervur/ward';
17
- import type { Asker, JsonObject, OccupantRecord } from 'nervur';
18
- import { Faculty } from './faculty.ts';
19
- import { mintVapid } from './webpush.ts';
20
-
21
- // What a device hands a world so it can be woken, kept in that device's
22
- // occupant notes and re-sent whenever the device comes to the foreground,
23
- // since a push service may hand a device a new one at any time. A token of
24
- // a store's kind names its bell, the address of the service holding the
25
- // keys of the app that minted it, and that app, so a world rings a phone
26
- // running an app it never shipped and holds no key; Web Push is the
27
- // world's own key and names no service.
28
- export type Token = { kind: string; bell?: string; app?: string } & JsonObject;
29
- export const isToken = (v: unknown): v is Token => v !== null && typeof v === 'object' && !Array.isArray(v) && typeof (v as { kind?: unknown }).kind === 'string';
30
-
31
- // The bell a token names, when it names one a ring can reach.
32
- export function bellOf(token: Token): string | undefined {
33
- if (typeof token.bell !== 'string') return undefined;
34
- try {
35
- const u = new URL(token.bell);
36
- return u.protocol === 'https:' || u.protocol === 'http:' ? token.bell : undefined;
37
- } catch {
38
- return undefined;
39
- }
40
- }
41
-
42
- // One platform's way of ringing. It is handed the token whole, because only
43
- // the platform knows what is in one, and it says nothing back: a ring either
44
- // left or threw.
45
- //
46
- // `sealed` is the banner, sealed to the device's banner key by the world
47
- // that rang. Nothing between the world and the device can read it: not this
48
- // box, not the bell service, not the platform that carries it. A ring with
49
- // neither a seal nor a line wakes the device and says nothing, which is
50
- // what a world with nothing to show sends.
51
- //
52
- // `line` is the same banner, unsealed, and only a ground whose own
53
- // encryption the world performs may take one: Web Push, where RFC 8291
54
- // encrypts the body to keys only that browser holds and the world holds the
55
- // keypair the subscription was made under. A store's ground is handed the
56
- // seal and never this, because what carries it is a service the world does
57
- // not own.
58
- export type Line = { title: string; body: string };
59
- export type Ring = { ring(token: Token, sealed?: string, line?: Line): Promise<void> };
60
-
61
- // What a platform will carry, so a seal too big is refused where a human
62
- // can still read why rather than two hops later by a service nobody reads.
63
- export const SEAL = 4096;
64
-
65
- // The line a device shows when it was woken while nobody was looking,
66
- // composed from one object. A pushed object is whatever the far being sent,
67
- // so the two names a notification has are taken when they are there and the
68
- // object is shown as it is when they are not.
69
- export function banner(o: JsonObject): Line {
70
- const text = (v: unknown) => (typeof v === 'string' && v.trim() ? v.trim() : '');
71
- const title = text(o.title) || text(o.from) || 'Nervur';
72
- const body =
73
- text(o.body) ||
74
- text(o.said) ||
75
- Object.entries(o)
76
- .filter(([k]) => k !== 'title' && k !== 'from')
77
- .map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
78
- .join(', ');
79
- return { title, body };
80
- }
81
-
82
- // A banner sealed to a device's banner key, as hex, or null when it will
83
- // not seal or will not fit. The world seals and the process that draws the
84
- // banner opens it; no doorbell, no bell service and no platform between
85
- // them holds either half, which is why a ring may carry a line at all.
86
- export async function sealBanner(line: JsonObject, key: string): Promise<string | null> {
87
- if (!/^[0-9a-f]{64}$/.test(key)) return null;
88
- try {
89
- const boxed = await arithmetic.box(new TextEncoder().encode(JSON.stringify(line)), arithmetic.unhex(key), globalThis.crypto.getRandomValues(new Uint8Array(32)));
90
- const wire = arithmetic.hex(boxed.bytes);
91
- return wire.length > SEAL ? null : wire;
92
- } catch {
93
- return null;
94
- }
95
- }
96
-
97
- // The root and nobody else: where a box hands a phone's ring is the box's own.
98
- const root = (_occ: OccupantRecord | undefined, asker: Asker) => asker.id === OWNER;
99
-
100
- export class Push extends Faculty {
101
- // What she keeps, beside every faculty's: the world's doorbell key, a P-256
102
- // JWK she mints herself the first time a terrain asks for one, so a Web
103
- // Push is signed under a key that lives with the wards and in no file; and
104
- // the bell service a phone's ring is handed to when its token names none,
105
- // a row the root keeps, since whose service it is belongs to an estate.
106
- static override cells: JsonObject = { watched: {}, minted: 0, key: null, service: null };
107
- static override asks = {
108
- 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'] } },
109
- kinds: { description: 'which kinds this box can ring', input: { type: 'object' } },
110
- 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 },
111
- };
112
-
113
- // The doorbell key, minted once and kept. A terrain asks for it when it
114
- // serves a web route; a box that never does holds none and rings no tabs.
115
- async mint(): Promise<JsonWebKey> {
116
- if (this.cells.key === null) this.cells.key = (await mintVapid()).key as unknown as JsonObject;
117
- return this.cells.key as unknown as JsonWebKey;
118
- }
119
- get key(): JsonWebKey | null {
120
- return this.cells.key as unknown as JsonWebKey | null;
121
- }
122
- get service(): string | undefined {
123
- return typeof this.cells.service === 'string' ? this.cells.service : undefined;
124
- }
125
-
126
- keep(args: JsonObject): JsonObject {
127
- const service = args.service;
128
- if (service !== null && (typeof service !== 'string' || !/^https?:\/\/\S+$/.test(service))) return { error: 'a bell service is a URL over http or https, or null' };
129
- this.cells.service = service === null ? null : service.replace(/\/$/, '');
130
- return { service: this.cells.service };
131
- }
132
-
133
- // What this box can ring. A terrain overrides it and builds its rings from
134
- // its environment and this being's cells; the base rings nothing.
135
- protected get rings(): Readonly<Record<string, Ring>> {
136
- return {};
137
- }
138
-
139
- kinds(): JsonObject {
140
- return { kinds: Object.keys(this.rings) };
141
- }
142
-
143
- async ring(args: JsonObject): Promise<JsonObject> {
144
- const token = args.token;
145
- if (!isToken(token)) return { error: 'a token names its kind' };
146
- const sealed = args.sealed;
147
- if (sealed !== undefined && typeof sealed !== 'string') return { error: 'a seal is text' };
148
- if (typeof sealed === 'string' && sealed.length > SEAL) return { error: `a seal is at most ${SEAL} bytes` };
149
- const said = args.line;
150
- if (said !== undefined && (said === null || typeof said !== 'object' || Array.isArray(said) || typeof said.title !== 'string' || typeof said.body !== 'string')) return { error: 'a line is a title and a body' };
151
- const line = said as Line | undefined;
152
- const r = Object.hasOwn(this.rings, token.kind) ? this.rings[token.kind] : undefined;
153
- if (!r) return { error: `this box cannot ring a ${token.kind}` };
154
- try {
155
- await r.ring(token, sealed, line);
156
- return { rang: token.kind };
157
- } catch (e) {
158
- return { error: e instanceof Error ? e.message : 'the ring threw' };
159
- }
160
- }
161
- }