@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,142 +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
- export class Faculty extends Being {
22
- static cells = { watched: {}, minted: 0 };
23
- // Someone wants waking. She hands the invitation she minted and says what
24
- // to ask her; this knocks it, takes it under an id of this being's own,
25
- // and files it. The id never crosses: it is this side's name for the
26
- // relation, and the caller's name for the same relation is her own.
27
- //
28
- // The empty ask is the knock, because every being answers one and asking
29
- // for a named ask would make every subscriber owe a method she does not
30
- // otherwise need.
31
- async watch(args, asker) {
32
- const inv = args.invitation;
33
- if (!isInvitation(inv) || typeof args.method !== 'string')
34
- return { error: 'an invitation and a method' };
35
- const extra = args.args;
36
- if (extra !== undefined && (extra === null || typeof extra !== 'object' || Array.isArray(extra)))
37
- return { error: 'args is one object' };
38
- const out = await this.knock(inv);
39
- if (isSilence(out) || isWord(out))
40
- return { error: 'no way back' };
41
- const cells = this.cells;
42
- const id = await this.take(`wake:${(cells.minted += 1)}`, inv);
43
- if (id === null)
44
- return { error: 'not taken' };
45
- cells.watched[id] = { by: asker.id ?? '', method: args.method, args: extra ?? {} };
46
- await this.welcomed(id);
47
- return { watching: id };
48
- }
49
- // Somebody new is filed, and the answer to her `watch` waits for this. A
50
- // faculty that owes a new subscriber something at once says it here: the
51
- // timer wakes her, learns when she is next due and arms the box for it, so
52
- // a being born long after the box was up is not waiting for a round nobody
53
- // scheduled. Most faculties owe nothing, and this is why they say nothing.
54
- async welcomed(_id) { }
55
- // One round of waking. Each subscriber is asked what she asked to be
56
- // asked, with whatever this round adds. A subscriber who removed the
57
- // occupant says `removed` once, and her row goes with the standing; there
58
- // is nothing to retry and nothing to remember.
59
- //
60
- // `only` is one subscriber of the round's own choosing, named by this
61
- // being's own id for the relation, and it is the same round down to one
62
- // row: a faculty that wakes somebody just filed learns exactly what a full
63
- // round would have learned about her.
64
- //
65
- // `dropped` is this being herself: her ward took her out, so the standings
66
- // she holds reach nobody. Null says so, and whoever holds the interval puts
67
- // it down rather than waking an empty room for as long as the box is up.
68
- async woke(round = (w) => w.args, only) {
69
- const cells = this.cells;
70
- const out = [];
71
- for (const [id, w] of Object.entries(cells.watched)) {
72
- if (only !== undefined && id !== only)
73
- continue;
74
- const said = await this.standings[id]?.ask(w.method, round(w));
75
- if (said === undefined || (isWord(said) && wordOf(said) === 'dropped'))
76
- return null;
77
- if (isWord(said) && wordOf(said) === 'removed') {
78
- this.standings.remove(id);
79
- delete cells.watched[id];
80
- continue;
81
- }
82
- out.push({ id, by: w.by, out: said });
83
- }
84
- return out;
85
- }
86
- // Who is waiting, by this being's own id for the relation. For a device's
87
- // own code and for a suite; it crosses no door.
88
- get watching() {
89
- return this.cells.watched;
90
- }
91
- }
92
- // The three moves a being makes at every birth to hold what her box lends.
93
- // A standing points at a being and not at a box, so one she wakes up holding
94
- // names a being on the box she was on when it was made; a restart and a
95
- // migration are the same silent event to her, so she does these again every
96
- // time she is constructed and is always pointed at the box she is on.
97
- //
98
- // `watch` says she wants waking too: her ward mints the occupant, the
99
- // faculty takes the standing it makes, and from then on the wake arrives at
100
- // her door under the id she chose.
101
- // Every join in flight on this box. A being takes what her box lends at her
102
- // birth, and birth is synchronous while taking a standing is not, so a
103
- // harbor that answered "up" before they landed would be a box whose first
104
- // tick woke nobody and whose first ring reached no platform. The terrain
105
- // waits on this once, after its wards are hosted; nothing else reads it, and
106
- // a join that fails leaves nothing behind but a being who asks again.
107
- const flight = new Set();
108
- export const settled = async () => {
109
- while (flight.size) {
110
- const now = [...flight];
111
- await Promise.allSettled(now);
112
- for (const p of now)
113
- flight.delete(p);
114
- }
115
- };
116
- export function join(b, name, watch, id = name) {
117
- const p = joining(b, name, watch, id);
118
- flight.add(p);
119
- void p.catch(() => { });
120
- return p;
121
- }
122
- async function joining(b, name, watch, id) {
123
- // One turn, before anything is asked. A being is called at her door and
124
- // holds her stance from her first line, but her ward sets that door after
125
- // her constructor returns, so a stance call made inside it reaches a being
126
- // her ward does not hold yet and is answered as one taken out. Every
127
- // faculty relation is made at birth, so the wait belongs here rather than
128
- // in each of them.
129
- await Promise.resolve();
130
- b.standings.remove(id);
131
- b.occupants.remove(`${id}:wake`);
132
- const got = await b.lend(name, id);
133
- if (got === null)
134
- return null;
135
- if (!watch)
136
- return got;
137
- const back = await b.invite(`${id}:wake`);
138
- if (!back)
139
- return got;
140
- await b.standings[id]?.ask('watch', { invitation: back, method: watch.method, ...(watch.args ? { args: watch.args } : {}) });
141
- return got;
142
- }
@@ -1,99 +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.js';
24
- // The name the dock ward holds her under.
25
- export const GRANTS = 'grants';
26
- export const ACCESS_TTL = 60 * 60 * 1000; // an access token, one hour
27
- export const REFRESH_TTL = 30 * 24 * 60 * 60 * 1000; // a refresh token, thirty days
28
- // A token is a string the far side offers, and a bare lookup would find
29
- // `__proto__` and hand back an object with no identity. Own keys only.
30
- const own = (rec, key) => (Object.hasOwn(rec, key) ? rec[key] : undefined);
31
- // Thirty-two random bytes as base64url, from the web crypto and the base64
32
- // every runtime has, so nothing of node stands in a file a tab bundles or a
33
- // worker imports. `Buffer` is not among those: a worker has one only when a
34
- // deploy asks for node's shims, and the box's grants are the same code on
35
- // every terrain.
36
- const token = () => {
37
- const bytes = globalThis.crypto.getRandomValues(new Uint8Array(32));
38
- let binary = '';
39
- for (const b of bytes)
40
- binary += String.fromCharCode(b);
41
- return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
42
- };
43
- export class Grants extends Faculty {
44
- static cells = { ...Faculty.cells, access: {}, refresh: {} };
45
- static asks = {};
46
- // The ward's save, set by whoever holds her ward: a token issued is a
47
- // cell written, and the routes answer with a token the partition keeps.
48
- persist = async () => { };
49
- // The clock, a suite's to move.
50
- now = () => Date.now();
51
- get kept() {
52
- return this.cells;
53
- }
54
- async issue(identity, ward, client) {
55
- const access_token = token(), refresh_token = token();
56
- this.kept.access[access_token] = { identity, ward, client, exp: this.now() + ACCESS_TTL };
57
- this.kept.refresh[refresh_token] = { identity, ward, client, exp: this.now() + REFRESH_TTL };
58
- this.sweep();
59
- await this.persist();
60
- return { access_token, token_type: 'Bearer', expires_in: ACCESS_TTL / 1000, refresh_token, scope: 'nervur' };
61
- }
62
- // Rotated: the old one is gone with the new one's birth. Null is a token
63
- // nobody issued, or one that died.
64
- async refresh(refresh_token) {
65
- const r = own(this.kept.refresh, refresh_token);
66
- if (!r || r.exp < this.now())
67
- return null;
68
- delete this.kept.refresh[refresh_token];
69
- return this.issue(r.identity, r.ward, r.client);
70
- }
71
- // The bearer on a request, to an identity in a world. Null is a stranger,
72
- // or a token that died; a route tells the two apart by whether the
73
- // request offered one at all.
74
- bearer(req) {
75
- const g = own(this.kept.access, offered(req));
76
- return g && g.exp >= this.now() && typeof g.identity === 'string' ? { identity: g.identity, ward: g.ward } : null;
77
- }
78
- // Every grant an identity holds in a world, gone: the box's half of
79
- // revocation. The other half is the user being removing the occupant.
80
- async revoke(identity, ward) {
81
- for (const k of ['access', 'refresh'])
82
- for (const [t, g] of Object.entries(this.kept[k]))
83
- if (g.identity === identity && g.ward === ward)
84
- delete this.kept[k][t];
85
- await this.persist();
86
- }
87
- sweep() {
88
- const now = this.now();
89
- for (const k of ['access', 'refresh'])
90
- for (const [t, g] of Object.entries(this.kept[k]))
91
- if (!(g.exp >= now))
92
- delete this.kept[k][t];
93
- }
94
- }
95
- // What a request offered as its bearer, or nothing.
96
- export const offered = (req) => {
97
- const h = req.headers.get('authorization') ?? '';
98
- return h.startsWith('Bearer ') ? h.slice(7) : '';
99
- };
@@ -1,41 +0,0 @@
1
- import type { Harbor, Lend } from 'nervur/harbor';
2
- import { Faculty } from './faculty.ts';
3
- import { Console } from './console.ts';
4
- import { Local } from './local.ts';
5
- import { Location } from './location.ts';
6
- import { Notifications } from './notifications.ts';
7
- import { Push } from './push.ts';
8
- import { Timer } from './timer.ts';
9
- export { Console, CONSOLE } from './console.ts';
10
- export { Faculty, join, settled, type Watched, type Woke } from './faculty.ts';
11
- export { Local, LOCAL } from './local.ts';
12
- export { Location, type Where } from './location.ts';
13
- export { Notifications } from './notifications.ts';
14
- export { Push, isToken, bellOf, banner, sealBanner, SEAL, type Token, type Ring, type Line } from './push.ts';
15
- export { Timer } from './timer.ts';
16
- export { webPush, mintVapid, publicOf, type Vapid } from './webpush.ts';
17
- export { apnsPush, pkcs8, HOSTS, type Apns } from './apns.ts';
18
- export { fcmPush, isAccount, DOORS, type Account, type Doors } from './fcm.ts';
19
- export { handedTo, ringing, routed } from './service.ts';
20
- export declare const LOCATION = "location";
21
- export declare const NOTIFICATIONS = "notifications";
22
- export declare const PUSH = "push";
23
- export declare const TIMER = "timer";
24
- export declare const FACULTIES: Record<string, string>;
25
- export declare const FACULTY_CLASSES: {
26
- Console: typeof Console;
27
- Local: typeof Local;
28
- Location: typeof Location;
29
- Notifications: typeof Notifications;
30
- Push: typeof Push;
31
- Timer: typeof Timer;
32
- };
33
- export declare const DOCK = "dock";
34
- export declare function stand(harbor: Harbor, lent?: Record<string, string>): Promise<Lend>;
35
- export type Rooted = {
36
- ask(method?: string, args?: Record<string, unknown>): Promise<unknown>;
37
- save?(): Promise<void>;
38
- };
39
- export declare function standOn(ward: Rooted, lent?: Record<string, string>): Promise<Lend>;
40
- export declare function lending(ward: Rooted): Lend;
41
- export declare function facultyOf<F extends Faculty>(harbor: Harbor, name: string): F | undefined;
@@ -1,113 +0,0 @@
1
- import { isInvitation } from 'nervur';
2
- import { Faculty } from './faculty.js';
3
- import { Console, CONSOLE } from './console.js';
4
- import { Local } from './local.js';
5
- import { Location } from './location.js';
6
- import { Notifications } from './notifications.js';
7
- import { Push } from './push.js';
8
- import { Timer } from './timer.js';
9
- export { Console, CONSOLE } from './console.js';
10
- export { Faculty, join, settled } from './faculty.js';
11
- export { Local, LOCAL } from './local.js';
12
- export { Location } from './location.js';
13
- export { Notifications } from './notifications.js';
14
- export { Push, isToken, bellOf, banner, sealBanner, SEAL } from './push.js';
15
- export { Timer } from './timer.js';
16
- // The three grounds the push faculty rings through, and the bell service
17
- // that holds a publisher's keys: what the box can do, beside the faculty.
18
- export { webPush, mintVapid, publicOf } from './webpush.js';
19
- export { apnsPush, pkcs8, HOSTS } from './apns.js';
20
- export { fcmPush, isAccount, DOORS } from './fcm.js';
21
- export { handedTo, ringing, routed } from './service.js';
22
- // The names this package lends under. They are conventions of the dock and
23
- // nothing of Quo's: a name is the harbor's namespace, and a being holds each
24
- // one under an id of her own choosing.
25
- export const LOCATION = 'location';
26
- export const NOTIFICATIONS = 'notifications';
27
- export const PUSH = 'push';
28
- export const TIMER = 'timer';
29
- // The classes every dock ward may hold, by class name. A terrain hands its
30
- // own subclasses under the same names and the harbor holds those instead.
31
- // `local` and `grants` are not among them: the box's way in and the box's
32
- // grants stand only where a terrain serves routes, and that terrain adds
33
- // them to what it lends. `Grants` is not in this file at all, so that a tab
34
- // bundling this index carries no route's notion: the terrain that serves
35
- // routes hands her class to its harbor, as the daemon hands its own push.
36
- export const FACULTIES = { [LOCATION]: 'Location', [NOTIFICATIONS]: 'Notifications', [PUSH]: 'Push', [TIMER]: 'Timer' };
37
- export const FACULTY_CLASSES = { Console, Local, Location, Notifications, Push, Timer };
38
- // The name a harbor keeps its own ward under. It is a ward in every other
39
- // respect, and this one is never adopted or dropped as a migration: its
40
- // beings are the box's and die with it.
41
- export const DOCK = 'dock';
42
- // Put the dock ward up and answer the `lend` this harbor hands its wards.
43
- // `lent` says which names this box offers and which class stands for each;
44
- // a name it does not hold is null, and so is every other kind of no.
45
- //
46
- // The harbor keeps this ward's ask pointer rather than handing it out, so it
47
- // is the root here, and the root may invite on any being of its ward. That
48
- // is the whole of what `lend` does: it never mints, it asks its own root to.
49
- export async function stand(harbor, lent = FACULTIES) {
50
- const kept = await harbor.store.load(DOCK);
51
- const ward = harbor.wards.get(DOCK) ?? (kept ? await harbor.host(DOCK, kept) : await harbor.create(DOCK, CONSOLE));
52
- return standOn(ward, lent);
53
- }
54
- // The faculties up in a ward already made, and the `lend` that reaches them.
55
- // Apart from `stand` so a suite can put its own stubs in its own ward and
56
- // get the same thing a box gets. It is run again at every boot and says the
57
- // same thing twice without harm: a key already booted is refused, and an
58
- // invitation on an id already held is null.
59
- export async function standOn(ward, lent = FACULTIES) {
60
- for (const [name, className] of Object.entries(lent))
61
- await ward.ask('boot', { key: name, class: className });
62
- // The box as one being, carrying what it stood. She is the page a device
63
- // that has joined no world shows, and she is placed by the root because
64
- // the root is the box: a standing at every faculty, minted and knocked in
65
- // one move, as an owner places any relation into a being of its ward.
66
- await ward.ask('boot', { key: CONSOLE, class: 'Console' });
67
- for (const name of Object.keys(lent)) {
68
- const inv = await ward.ask('invite', { being: name, id: `${CONSOLE}:${name}` });
69
- if (!isInvitation(inv))
70
- continue;
71
- await ward.ask('knock', { being: CONSOLE, id: name, invitation: inv });
72
- }
73
- await ward.save?.();
74
- return lending(ward);
75
- }
76
- // The root's half alone: a name and a taker in, whether the ward took it out.
77
- // It never mints; it asks the root of the ward it holds to, which is what a
78
- // root may do on any being of its ward. And it unmints: a taker that would
79
- // not take leaves an occupant on that faculty which nobody holds, so the root
80
- // removes it before answering, and a refused lend costs the box nothing.
81
- //
82
- // Any being of the dock ward is lent by her key, not only the ones a terrain
83
- // stood: the faculties the dock ships are examples, and an estate boots its
84
- // own into the dock ward, of a class in its own `classes/`, and lends it the
85
- // same way. A name nobody there answers to is refused at the invite.
86
- //
87
- // The id is drawn and never counted. A faculty's occupants are in her cells
88
- // and outlive the process, so a count that started again at every boot would
89
- // name, on the first lend after a restart, an id she already holds, and the
90
- // invite would be refused for the rest of the box's life.
91
- export function lending(ward) {
92
- return async (name, take) => {
93
- const id = `lent:${fresh()}`;
94
- const out = await ward.ask('invite', { being: name, id });
95
- if (!isInvitation(out))
96
- return false;
97
- const took = await take(out);
98
- if (!took)
99
- await ward.ask('remove', { being: name, id });
100
- await ward.save?.();
101
- return took;
102
- };
103
- }
104
- const fresh = () => Array.from(globalThis.crypto.getRandomValues(new Uint8Array(8)))
105
- .map((b) => b.toString(16).padStart(2, '0'))
106
- .join('');
107
- // One faculty of this box, as the device's own code holds it: the object the
108
- // harbor made, in this process. Not a path around a door, and nothing of it
109
- // crosses an edge; it is how a device fires its own timer at a moment it
110
- // names and how a suite reads what a stub recorded.
111
- export function facultyOf(harbor, name) {
112
- return harbor.wards.get(DOCK)?.being(name);
113
- }
@@ -1,44 +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.js';
17
- import { VERIFY } from '../beings/way.js';
18
- // The name the dock ward lends her under. A box with no local side lends no
19
- // such name, and the desk there holds no such standing.
20
- export const LOCAL = 'local';
21
- export class Local extends Faculty {
22
- static asks = { verify: VERIFY };
23
- #nonces = new Map();
24
- // A nonce for one client this box saw itself, spent by the first proof
25
- // that names it. The caller drops it when the admit is over: a nonce that
26
- // was never spent is not a way in that stays open.
27
- mint(nonce, who) {
28
- this.#nonces.set(nonce, who);
29
- }
30
- drop(nonce) {
31
- this.#nonces.delete(nonce);
32
- }
33
- verify(args) {
34
- const proof = args.proof;
35
- if (!proof || typeof proof !== 'object' || Array.isArray(proof))
36
- return null;
37
- const nonce = proof.nonce;
38
- const who = typeof nonce === 'string' ? this.#nonces.get(nonce) : undefined;
39
- if (!who)
40
- return null;
41
- this.#nonces.delete(nonce);
42
- return who;
43
- }
44
- }
@@ -1,11 +0,0 @@
1
- import { Faculty } from './faculty.js';
2
- export class Location extends Faculty {
3
- static asks = {
4
- where: { description: 'where this box is', input: { type: 'object' } },
5
- };
6
- // The platform's own permission prompt is the first ask's, and a refusal is
7
- // the error object every ask after it answers.
8
- where() {
9
- return Promise.resolve({ error: 'this box cannot say where it is' });
10
- }
11
- }
package/dist/dock/push.js DELETED
@@ -1,132 +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 { Faculty } from './faculty.js';
18
- import { mintVapid } from './webpush.js';
19
- export const isToken = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && typeof v.kind === 'string';
20
- // The bell a token names, when it names one a ring can reach.
21
- export function bellOf(token) {
22
- if (typeof token.bell !== 'string')
23
- return undefined;
24
- try {
25
- const u = new URL(token.bell);
26
- return u.protocol === 'https:' || u.protocol === 'http:' ? token.bell : undefined;
27
- }
28
- catch {
29
- return undefined;
30
- }
31
- }
32
- // What a platform will carry, so a seal too big is refused where a human
33
- // can still read why rather than two hops later by a service nobody reads.
34
- export const SEAL = 4096;
35
- // The line a device shows when it was woken while nobody was looking,
36
- // composed from one object. A pushed object is whatever the far being sent,
37
- // so the two names a notification has are taken when they are there and the
38
- // object is shown as it is when they are not.
39
- export function banner(o) {
40
- const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : '');
41
- const title = text(o.title) || text(o.from) || 'Nervur';
42
- const body = text(o.body) ||
43
- text(o.said) ||
44
- Object.entries(o)
45
- .filter(([k]) => k !== 'title' && k !== 'from')
46
- .map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
47
- .join(', ');
48
- return { title, body };
49
- }
50
- // A banner sealed to a device's banner key, as hex, or null when it will
51
- // not seal or will not fit. The world seals and the process that draws the
52
- // banner opens it; no doorbell, no bell service and no platform between
53
- // them holds either half, which is why a ring may carry a line at all.
54
- export async function sealBanner(line, key) {
55
- if (!/^[0-9a-f]{64}$/.test(key))
56
- return null;
57
- try {
58
- const boxed = await arithmetic.box(new TextEncoder().encode(JSON.stringify(line)), arithmetic.unhex(key), globalThis.crypto.getRandomValues(new Uint8Array(32)));
59
- const wire = arithmetic.hex(boxed.bytes);
60
- return wire.length > SEAL ? null : wire;
61
- }
62
- catch {
63
- return null;
64
- }
65
- }
66
- // The root and nobody else: where a box hands a phone's ring is the box's own.
67
- const root = (_occ, asker) => asker.id === OWNER;
68
- export class Push extends Faculty {
69
- // What she keeps, beside every faculty's: the world's doorbell key, a P-256
70
- // JWK she mints herself the first time a terrain asks for one, so a Web
71
- // Push is signed under a key that lives with the wards and in no file; and
72
- // the bell service a phone's ring is handed to when its token names none,
73
- // a row the root keeps, since whose service it is belongs to an estate.
74
- static cells = { watched: {}, minted: 0, key: null, service: null };
75
- static asks = {
76
- 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'] } },
77
- kinds: { description: 'which kinds this box can ring', input: { type: 'object' } },
78
- 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 },
79
- };
80
- // The doorbell key, minted once and kept. A terrain asks for it when it
81
- // serves a web route; a box that never does holds none and rings no tabs.
82
- async mint() {
83
- if (this.cells.key === null)
84
- this.cells.key = (await mintVapid()).key;
85
- return this.cells.key;
86
- }
87
- get key() {
88
- return this.cells.key;
89
- }
90
- get service() {
91
- return typeof this.cells.service === 'string' ? this.cells.service : undefined;
92
- }
93
- keep(args) {
94
- const service = args.service;
95
- if (service !== null && (typeof service !== 'string' || !/^https?:\/\/\S+$/.test(service)))
96
- return { error: 'a bell service is a URL over http or https, or null' };
97
- this.cells.service = service === null ? null : service.replace(/\/$/, '');
98
- return { service: this.cells.service };
99
- }
100
- // What this box can ring. A terrain overrides it and builds its rings from
101
- // its environment and this being's cells; the base rings nothing.
102
- get rings() {
103
- return {};
104
- }
105
- kinds() {
106
- return { kinds: Object.keys(this.rings) };
107
- }
108
- async ring(args) {
109
- const token = args.token;
110
- if (!isToken(token))
111
- return { error: 'a token names its kind' };
112
- const sealed = args.sealed;
113
- if (sealed !== undefined && typeof sealed !== 'string')
114
- return { error: 'a seal is text' };
115
- if (typeof sealed === 'string' && sealed.length > SEAL)
116
- return { error: `a seal is at most ${SEAL} bytes` };
117
- const said = args.line;
118
- if (said !== undefined && (said === null || typeof said !== 'object' || Array.isArray(said) || typeof said.title !== 'string' || typeof said.body !== 'string'))
119
- return { error: 'a line is a title and a body' };
120
- const line = said;
121
- const r = Object.hasOwn(this.rings, token.kind) ? this.rings[token.kind] : undefined;
122
- if (!r)
123
- return { error: `this box cannot ring a ${token.kind}` };
124
- try {
125
- await r.ring(token, sealed, line);
126
- return { rang: token.kind };
127
- }
128
- catch (e) {
129
- return { error: e instanceof Error ? e.message : 'the ring threw' };
130
- }
131
- }
132
- }