@nervur-org/dock 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +17 -15
  2. package/api/route.ts +4 -72
  3. package/beings/avatar.ts +11 -7
  4. package/beings/carry.ts +129 -116
  5. package/beings/clock.ts +11 -7
  6. package/beings/desk.ts +7 -6
  7. package/beings/doorbell.ts +36 -7
  8. package/beings/index.ts +17 -4
  9. package/beings/join.ts +7 -4
  10. package/beings/link.ts +1 -1
  11. package/beings/porter.ts +5 -1
  12. package/beings/presence.ts +15 -5
  13. package/beings/setup.ts +27 -21
  14. package/beings/user.ts +26 -17
  15. package/beings/way.ts +1 -1
  16. package/{dock → box}/agents.ts +37 -44
  17. package/{dock → box}/apns.ts +4 -4
  18. package/box/box.ts +31 -0
  19. package/box/contract.ts +41 -0
  20. package/box/dialers.ts +88 -0
  21. package/box/dock.ts +148 -0
  22. package/box/envoy.ts +96 -0
  23. package/box/faculty.ts +111 -0
  24. package/{dock → box}/fcm.ts +1 -1
  25. package/box/grants.ts +90 -0
  26. package/box/http.ts +93 -0
  27. package/{dock → box}/http2.ts +7 -7
  28. package/box/index.ts +201 -0
  29. package/box/join.ts +78 -0
  30. package/box/local.ts +37 -0
  31. package/{dock → box}/location.ts +15 -3
  32. package/{dock → box}/notifications.ts +17 -5
  33. package/box/push.ts +85 -0
  34. package/box/ring.ts +85 -0
  35. package/{dock → box}/service.ts +2 -2
  36. package/box/timer.ts +150 -0
  37. package/box/tree.ts +22 -0
  38. package/{dock → box}/webpush.ts +2 -2
  39. package/cli/client.ts +1 -1
  40. package/cli/daemon.ts +101 -103
  41. package/cli/estate.ts +2 -3
  42. package/cli/nervur.ts +44 -25
  43. package/cli/stand.ts +1 -1
  44. package/dist/api/route.d.ts +1 -2
  45. package/dist/api/route.js +4 -76
  46. package/dist/beings/avatar.d.ts +4 -3
  47. package/dist/beings/avatar.js +9 -6
  48. package/dist/beings/carry.d.ts +10 -12
  49. package/dist/beings/carry.js +119 -127
  50. package/dist/beings/clock.js +10 -7
  51. package/dist/beings/desk.js +8 -7
  52. package/dist/beings/doorbell.d.ts +3 -1
  53. package/dist/beings/doorbell.js +37 -4
  54. package/dist/beings/index.d.ts +24 -4
  55. package/dist/beings/index.js +15 -4
  56. package/dist/beings/join.d.ts +1 -0
  57. package/dist/beings/join.js +6 -4
  58. package/dist/beings/link.d.ts +0 -1
  59. package/dist/beings/link.js +1 -1
  60. package/dist/beings/porter.js +5 -1
  61. package/dist/beings/presence.d.ts +1 -0
  62. package/dist/beings/presence.js +12 -3
  63. package/dist/beings/setup.d.ts +5 -2
  64. package/dist/beings/setup.js +16 -15
  65. package/dist/beings/user.d.ts +4 -3
  66. package/dist/beings/user.js +18 -11
  67. package/dist/{dock → box}/agents.d.ts +7 -4
  68. package/dist/{dock → box}/agents.js +30 -37
  69. package/dist/{dock → box}/apns.d.ts +1 -1
  70. package/dist/{dock → box}/apns.js +3 -3
  71. package/dist/box/box.d.ts +9 -0
  72. package/dist/box/box.js +18 -0
  73. package/dist/box/contract.d.ts +6 -0
  74. package/dist/box/contract.js +35 -0
  75. package/dist/{dock → box}/dialers.d.ts +7 -4
  76. package/dist/box/dialers.js +76 -0
  77. package/dist/box/dock.d.ts +182 -0
  78. package/dist/box/dock.js +128 -0
  79. package/dist/box/envoy.d.ts +44 -0
  80. package/dist/box/envoy.js +75 -0
  81. package/dist/box/faculty.d.ts +68 -0
  82. package/dist/box/faculty.js +97 -0
  83. package/dist/{dock → box}/fcm.d.ts +1 -1
  84. package/dist/{dock → box}/grants.d.ts +4 -3
  85. package/dist/box/grants.js +78 -0
  86. package/dist/box/http.d.ts +26 -0
  87. package/dist/box/http.js +89 -0
  88. package/dist/{dock → box}/http2.d.ts +1 -1
  89. package/dist/{dock → box}/http2.js +6 -6
  90. package/dist/box/index.d.ts +46 -0
  91. package/dist/box/index.js +179 -0
  92. package/dist/box/join.d.ts +19 -0
  93. package/dist/box/join.js +67 -0
  94. package/dist/{dock → box}/local.d.ts +3 -2
  95. package/dist/box/local.js +27 -0
  96. package/dist/{dock → box}/location.d.ts +3 -0
  97. package/dist/box/location.js +21 -0
  98. package/dist/{dock → box}/notifications.d.ts +3 -0
  99. package/dist/{dock → box}/notifications.js +14 -4
  100. package/dist/{dock → box}/push.d.ts +12 -18
  101. package/dist/box/push.js +83 -0
  102. package/dist/box/ring.d.ts +18 -0
  103. package/dist/box/ring.js +55 -0
  104. package/dist/{dock → box}/service.d.ts +1 -1
  105. package/dist/{dock → box}/service.js +1 -1
  106. package/dist/{dock → box}/timer.d.ts +2 -0
  107. package/dist/box/timer.js +127 -0
  108. package/dist/box/tree.d.ts +5 -0
  109. package/dist/box/tree.js +14 -0
  110. package/dist/{dock → box}/webpush.d.ts +1 -1
  111. package/dist/cli/client.d.ts +1 -0
  112. package/dist/cli/daemon.d.ts +2 -8
  113. package/dist/cli/daemon.js +102 -107
  114. package/dist/cli/estate.js +2 -3
  115. package/dist/cli/nervur.js +44 -24
  116. package/dist/cli/stand.d.ts +0 -14
  117. package/dist/cli/stand.js +1 -1
  118. package/dist/harbor/browser.d.ts +8 -5
  119. package/dist/harbor/browser.js +20 -21
  120. package/dist/harbor/capacitor.d.ts +3 -3
  121. package/dist/harbor/capacitor.js +9 -8
  122. package/dist/harbor/disk.d.ts +9 -9
  123. package/dist/harbor/disk.js +56 -51
  124. package/dist/harbor/edge/alarm.d.ts +4 -2
  125. package/dist/harbor/edge/alarm.js +9 -19
  126. package/dist/harbor/edge/edge.d.ts +7 -6
  127. package/dist/harbor/edge/edge.js +58 -53
  128. package/dist/harbor/edge/exercise.js +4 -4
  129. package/dist/harbor/edge/given.d.ts +0 -1
  130. package/dist/harbor/edge/given.js +0 -29
  131. package/dist/harbor/edge/storage.d.ts +0 -2
  132. package/dist/harbor/edge/storage.js +2 -2
  133. package/dist/harbor/http.js +4 -2
  134. package/dist/harbor/sealed.d.ts +0 -5
  135. package/dist/harbor/sealed.js +0 -39
  136. package/dist/harbor/tauri.d.ts +2 -2
  137. package/dist/harbor/tauri.js +13 -10
  138. package/dist/human/app.d.ts +1 -1
  139. package/dist/human/app.js +10 -12
  140. package/dist/human/door.d.ts +0 -1
  141. package/dist/human/door.js +1 -1
  142. package/dist/human/html.js +1 -1
  143. package/dist/human/move.js +1 -1
  144. package/dist/human/tab.bundle.txt +3629 -2824
  145. package/dist/human/wake.d.ts +5 -2
  146. package/dist/human/wake.js +23 -14
  147. package/dist/human/web.d.ts +0 -1
  148. package/dist/human/web.js +3 -3
  149. package/dist/human/worker.bundle.txt +2945 -2118
  150. package/dist/human/worlds.js +3 -3
  151. package/dist/mcp/agent.d.ts +0 -1
  152. package/dist/mcp/agent.js +1 -1
  153. package/dist/mcp/allow.js +3 -2
  154. package/dist/mcp/http.d.ts +0 -1
  155. package/dist/mcp/http.js +1 -1
  156. package/dist/mcp/oauth.d.ts +3 -4
  157. package/dist/mcp/oauth.js +4 -4
  158. package/dist/mcp/route.d.ts +1 -1
  159. package/dist/mcp/runner.d.ts +0 -1
  160. package/dist/mcp/runner.js +1 -1
  161. package/dist/mcp/server.d.ts +0 -1
  162. package/dist/mcp/server.js +1 -1
  163. package/harbor/browser.ts +22 -24
  164. package/harbor/capacitor.ts +9 -8
  165. package/harbor/disk.ts +53 -48
  166. package/harbor/edge/alarm.ts +15 -12
  167. package/harbor/edge/edge.ts +61 -55
  168. package/harbor/edge/exercise.ts +4 -4
  169. package/harbor/edge/given.ts +3 -33
  170. package/harbor/edge/storage.ts +2 -2
  171. package/harbor/http.ts +5 -3
  172. package/harbor/sealed.ts +0 -35
  173. package/harbor/tauri.ts +13 -10
  174. package/human/app.ts +12 -13
  175. package/human/door.ts +1 -1
  176. package/human/html.ts +1 -1
  177. package/human/local.ts +1 -1
  178. package/human/move.ts +1 -1
  179. package/human/tab.bundle.txt +3629 -2824
  180. package/human/wake.ts +36 -16
  181. package/human/web.ts +4 -4
  182. package/human/worker.bundle.txt +2945 -2118
  183. package/human/worlds.ts +3 -3
  184. package/mcp/agent.ts +1 -1
  185. package/mcp/allow.ts +2 -1
  186. package/mcp/http.ts +1 -1
  187. package/mcp/oauth.ts +4 -4
  188. package/mcp/route.ts +1 -1
  189. package/mcp/runner.ts +1 -1
  190. package/mcp/server.ts +1 -1
  191. package/package.json +10 -18
  192. package/GETTING_STARTED.md +0 -138
  193. package/GLOSSARY.md +0 -118
  194. package/WORLDS.md +0 -569
  195. package/beings/envoy.ts +0 -218
  196. package/dist/beings/envoy.d.ts +0 -54
  197. package/dist/beings/envoy.js +0 -211
  198. package/dist/dock/console.d.ts +0 -6
  199. package/dist/dock/console.js +0 -20
  200. package/dist/dock/dialers.js +0 -75
  201. package/dist/dock/faculty.d.ts +0 -24
  202. package/dist/dock/faculty.js +0 -142
  203. package/dist/dock/grants.js +0 -99
  204. package/dist/dock/index.d.ts +0 -41
  205. package/dist/dock/index.js +0 -113
  206. package/dist/dock/local.js +0 -44
  207. package/dist/dock/location.js +0 -11
  208. package/dist/dock/push.js +0 -132
  209. package/dist/dock/timer.js +0 -149
  210. package/dock/console.ts +0 -23
  211. package/dock/dialers.ts +0 -85
  212. package/dock/faculty.ts +0 -148
  213. package/dock/grants.ts +0 -111
  214. package/dock/index.ts +0 -133
  215. package/dock/local.ts +0 -48
  216. package/dock/push.ts +0 -161
  217. package/dock/timer.ts +0 -174
  218. package/quo-api.md +0 -196
  219. package/quo-app-bells.md +0 -280
  220. package/quo-dock.md +0 -1945
  221. package/quo-harbor.md +0 -874
  222. package/quo-human.md +0 -1042
  223. package/quo-mcp.md +0 -343
  224. /package/dist/{dock → box}/fcm.js +0 -0
  225. /package/dist/{dock → box}/webpush.js +0 -0
@@ -0,0 +1,182 @@
1
+ import { Box } from 'nervur/harbor';
2
+ import type { Asker, JsonObject, OccupantRecord, Reply } from 'nervur';
3
+ export { BOX } from 'nervur/harbor';
4
+ declare const root: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
5
+ export type Routes = {
6
+ mcp: string;
7
+ web: string;
8
+ quo?: string;
9
+ api?: string;
10
+ };
11
+ export declare const DOMAIN: RegExp;
12
+ export declare function routesFor(domain: string): Required<Routes>;
13
+ export type Apps = {
14
+ apple: string[];
15
+ android: {
16
+ package: string;
17
+ fingerprints: string[];
18
+ }[];
19
+ };
20
+ export type BoxCells = {
21
+ routes: Routes | null;
22
+ apps: Apps | null;
23
+ default: string | null;
24
+ };
25
+ export declare class Dock extends Box {
26
+ #private;
27
+ static carries(occupant: OccupantRecord | undefined): boolean;
28
+ static skips(id: string): boolean;
29
+ static cells: JsonObject;
30
+ static asks: {
31
+ offer: {
32
+ description: string;
33
+ input: {
34
+ type: string;
35
+ properties: {
36
+ name: {
37
+ type: string;
38
+ };
39
+ };
40
+ required: string[];
41
+ };
42
+ for: (occ: OccupantRecord | undefined) => boolean;
43
+ };
44
+ retract: {
45
+ description: string;
46
+ input: {
47
+ type: string;
48
+ properties: {
49
+ name: {
50
+ type: string;
51
+ };
52
+ heir: {
53
+ type: string;
54
+ };
55
+ };
56
+ required: string[];
57
+ };
58
+ for: (occ: OccupantRecord | undefined) => boolean;
59
+ };
60
+ host: {
61
+ description: string;
62
+ input: {
63
+ type: string;
64
+ properties: {
65
+ seed: {
66
+ type: string;
67
+ };
68
+ };
69
+ required: string[];
70
+ };
71
+ for: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
72
+ };
73
+ roll: {
74
+ description: string;
75
+ input: {
76
+ type: string;
77
+ };
78
+ for: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
79
+ };
80
+ box: {
81
+ description: string;
82
+ input: {
83
+ type: string;
84
+ };
85
+ for: typeof root;
86
+ };
87
+ domain: {
88
+ description: string;
89
+ input: {
90
+ type: string;
91
+ properties: {
92
+ domain: {
93
+ type: string;
94
+ };
95
+ };
96
+ required: string[];
97
+ };
98
+ for: typeof root;
99
+ };
100
+ apps: {
101
+ description: string;
102
+ input: {
103
+ type: string;
104
+ properties: {
105
+ apple: {
106
+ type: string;
107
+ items: {
108
+ type: string;
109
+ };
110
+ };
111
+ android: {
112
+ type: string;
113
+ items: {
114
+ type: string;
115
+ properties: {
116
+ package: {
117
+ type: string;
118
+ };
119
+ fingerprints: {
120
+ type: string;
121
+ items: {
122
+ type: string;
123
+ };
124
+ };
125
+ };
126
+ required: string[];
127
+ };
128
+ };
129
+ };
130
+ };
131
+ for: typeof root;
132
+ };
133
+ default: {
134
+ description: string;
135
+ input: {
136
+ type: string;
137
+ properties: {
138
+ ward: {
139
+ type: string;
140
+ };
141
+ };
142
+ required: string[];
143
+ };
144
+ for: typeof root;
145
+ };
146
+ lends: {
147
+ description: string;
148
+ input: {
149
+ type: string;
150
+ };
151
+ for: typeof root;
152
+ };
153
+ stood: {
154
+ description: string;
155
+ input: {
156
+ type: string;
157
+ properties: {
158
+ lends: {
159
+ type: string;
160
+ };
161
+ titles: {
162
+ type: string;
163
+ };
164
+ clashes: {
165
+ type: string;
166
+ };
167
+ };
168
+ required: string[];
169
+ };
170
+ for: typeof root;
171
+ };
172
+ };
173
+ answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply>;
174
+ offer(args: JsonObject): Promise<JsonObject>;
175
+ retract(args: JsonObject): Promise<JsonObject>;
176
+ lends(): JsonObject;
177
+ box(): JsonObject;
178
+ domain(args: JsonObject): JsonObject;
179
+ apps(args: JsonObject): JsonObject;
180
+ default(args: JsonObject): JsonObject;
181
+ stood(args: JsonObject): JsonObject;
182
+ }
@@ -0,0 +1,128 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The box ward's own being, and so the harbor's. She is the kit's Box, the
3
+ // box as one standing that every hosted ward holds, and two things more.
4
+ //
5
+ // She lends by contract: a being asks her for a contract's digest, and she
6
+ // holds each faculty under its key, so her cells keep the index from digest
7
+ // to key, written whole by the root at every boot from what this run's
8
+ // classes say. A name she is asked to offer is a digest first and a key
9
+ // second, and then it is the kit's offer.
10
+ //
11
+ // And she carries: she holds a standing at every faculty her harbor stood
12
+ // and shows their asks as her own to whoever the root let in, so the one
13
+ // screen a device has with no world joined draws her the way it draws any
14
+ // being.
15
+ import { OWNER } from 'nervur';
16
+ import { Box, BOX_SEED } from 'nervur/harbor';
17
+ import { carry } from '../beings/carry.js';
18
+ export { BOX } from 'nervur/harbor';
19
+ const root = (_occ, asker) => asker.id === OWNER;
20
+ export const DOMAIN = /^[a-z0-9.-]+\.[a-z]{2,}$/i;
21
+ export function routesFor(domain) {
22
+ if (!DOMAIN.test(domain))
23
+ throw new Error(`${domain} is not a domain`);
24
+ return { mcp: `https://mcp.${domain}`, web: `https://web.${domain}`, quo: `https://quo.${domain}`, api: `https://api.${domain}` };
25
+ }
26
+ const APPS = { type: 'object', properties: { apple: { type: 'array', items: { type: 'string' } }, android: { type: 'array', items: { type: 'object', properties: { package: { type: 'string' }, fingerprints: { type: 'array', items: { type: 'string' } } }, required: ['package', 'fingerprints'] } } } };
27
+ export class Dock extends Box {
28
+ static carries(occupant) {
29
+ return occupant !== undefined;
30
+ }
31
+ // She holds every faculty twice: under its key, which is the lend seam
32
+ // and plumbing, and under the word the first contract it is lent under
33
+ // calls itself, which is the section a screen shows. A key is a path
34
+ // through the class tree and always carries a slash; a title never does.
35
+ // Both are this run's: `stood` writes the index whole and puts down any
36
+ // title standing this run does not say.
37
+ static skips(id) {
38
+ return id.includes('/');
39
+ }
40
+ static cells = { lends: {}, titles: {}, clashes: {}, hosted: [], routes: null, apps: null, default: null };
41
+ static asks = {
42
+ ...Box.asks,
43
+ box: { description: 'what this box is: its routes, its apps and its default ward, each null until said', input: { type: 'object' }, for: root },
44
+ domain: { description: 'the four routes by the convention, mcp., web., quo. and api. under this domain over https, kept until said again', input: { type: 'object', properties: { domain: { type: 'string' } }, required: ['domain'] }, for: root },
45
+ apps: { description: 'the apps this box’s links open, apple app ids and android packages with their fingerprints; neither named forgets them', input: APPS, for: root },
46
+ default: { description: 'the ward a caller means when naming none; the harbor holds it to the wards it hosts at boot', input: { type: 'object', properties: { ward: { type: 'string' } }, required: ['ward'] }, for: root },
47
+ lends: { description: 'which faculty is lent under each contract, by digest to key, and which contracts are lent to nobody because two classes clashed under them', input: { type: 'object' }, for: root },
48
+ stood: {
49
+ description: 'what this boot stood: each contract, by digest, to the faculty that fulfils it, each title to the faculty shown under it, and each clash to the keys that clashed; a title held that this does not say goes',
50
+ input: { type: 'object', properties: { lends: { type: 'object' }, titles: { type: 'object' }, clashes: { type: 'object' } }, required: ['lends', 'titles', 'clashes'] },
51
+ for: root,
52
+ },
53
+ };
54
+ answer(asker, method, args = {}) {
55
+ return carry(this, asker, method, args, (a, m, g) => super.answer(a, m, g));
56
+ }
57
+ #key(name) {
58
+ const index = this.cells.lends;
59
+ return Object.hasOwn(index, name) ? index[name] : name;
60
+ }
61
+ offer(args) {
62
+ return super.offer({ name: this.#key(args.name) });
63
+ }
64
+ retract(args) {
65
+ return super.retract({ ...args, name: this.#key(args.name) });
66
+ }
67
+ lends() {
68
+ return { lends: this.cells.lends, clashes: this.cells.clashes ?? {} };
69
+ }
70
+ // What the box is, placed by the root and read by the harbor: nothing here
71
+ // comes from a file.
72
+ box() {
73
+ return { routes: this.cells.routes ?? null, apps: this.cells.apps ?? null, default: this.cells.default ?? null };
74
+ }
75
+ domain(args) {
76
+ if (typeof args.domain !== 'string' || !DOMAIN.test(args.domain))
77
+ return { error: `${JSON.stringify(args.domain)} is not a domain` };
78
+ this.cells.routes = routesFor(args.domain);
79
+ return { routes: this.cells.routes };
80
+ }
81
+ apps(args) {
82
+ const apple = Array.isArray(args.apple) ? args.apple.filter((x) => typeof x === 'string') : [];
83
+ const android = Array.isArray(args.android)
84
+ ? args.android
85
+ .filter((a) => typeof a?.package === 'string' && Array.isArray(a.fingerprints))
86
+ .map((a) => ({ package: a.package, fingerprints: a.fingerprints.filter((f) => typeof f === 'string') }))
87
+ : [];
88
+ this.cells.apps = apple.length === 0 && android.length === 0 ? null : { apple, android };
89
+ return { apps: this.cells.apps };
90
+ }
91
+ default(args) {
92
+ if (typeof args.ward !== 'string' || args.ward === '' || args.ward === BOX_SEED)
93
+ return { error: 'a ward is a name, and the box ward is never the default' };
94
+ this.cells.default = args.ward;
95
+ return { default: args.ward };
96
+ }
97
+ // The index, written whole at every boot: what stands is what this run's
98
+ // classes say, and nothing an earlier run said, the clashes with it, by
99
+ // the word the contract calls itself to the keys that clashed. Her title
100
+ // standings are held to it: one under a title this run shows nobody
101
+ // under, or that now names another faculty, goes here, and the root
102
+ // removes the far end. She answers what she holds and what went, so the
103
+ // root places only what is not there.
104
+ stood(args) {
105
+ const { lends, titles, clashes } = args;
106
+ if (!isIndex(lends))
107
+ return { error: 'lends is a digest to a key' };
108
+ if (!isIndex(titles))
109
+ return { error: 'titles is a title to a key' };
110
+ if (clashes === null || typeof clashes !== 'object' || Array.isArray(clashes) || !Object.values(clashes).every((ks) => Array.isArray(ks) && ks.every((k) => typeof k === 'string')))
111
+ return { error: 'clashes is a word to keys' };
112
+ this.cells.clashes = clashes;
113
+ const was = (this.cells.titles ?? {});
114
+ const pruned = [];
115
+ for (const id of Object.keys(this.cells.standings)) {
116
+ if (id.includes('/'))
117
+ continue;
118
+ if (Object.hasOwn(titles, id) && was[id] === titles[id])
119
+ continue;
120
+ this.standings.remove(id);
121
+ pruned.push(id);
122
+ }
123
+ this.cells.lends = lends;
124
+ this.cells.titles = titles;
125
+ return { lends, held: Object.keys(this.cells.standings), pruned };
126
+ }
127
+ }
128
+ const isIndex = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && Object.values(v).every((k) => typeof k === 'string');
@@ -0,0 +1,44 @@
1
+ import type { Asker, JsonObject } from 'nervur';
2
+ import { Faculty } from './faculty.ts';
3
+ export declare const WATCH_ACCOUNT: {
4
+ description: string;
5
+ input: {
6
+ type: string;
7
+ properties: {
8
+ invitation: {
9
+ type: string;
10
+ };
11
+ method: {
12
+ type: string;
13
+ };
14
+ args: {
15
+ type: string;
16
+ };
17
+ account: {
18
+ type: string;
19
+ };
20
+ secret: {
21
+ type: string;
22
+ };
23
+ };
24
+ required: string[];
25
+ };
26
+ };
27
+ export type Delivery = {
28
+ id: string;
29
+ method: string;
30
+ args: JsonObject;
31
+ };
32
+ export declare class EnvoyFaculty extends Faculty {
33
+ #private;
34
+ static cells: JsonObject;
35
+ watch(args: JsonObject, asker: Asker): Promise<JsonObject>;
36
+ get waking(): Promise<void>;
37
+ inbound(args: JsonObject): Promise<JsonObject>;
38
+ protected account(_body: string, _headers: Record<string, string>): string | null;
39
+ protected verify(_body: string, _headers: Record<string, string>, _secret: string): Promise<boolean> | boolean;
40
+ protected decode(_body: string, _headers: Record<string, string>): Delivery | {
41
+ error: string;
42
+ };
43
+ protected hmac(secret: string, text: string): Promise<string>;
44
+ }
@@ -0,0 +1,75 @@
1
+ import { Faculty, WATCH } from './faculty.js';
2
+ // A watch for an account: the invitation back, the ask to wake her with,
3
+ // her account at the vendor and the secret the vendor signs with.
4
+ export const WATCH_ACCOUNT = {
5
+ description: 'wake me for my account: an invitation back, the ask to wake me with, the account id and its signing secret',
6
+ input: { type: 'object', properties: { ...WATCH.input.properties, account: { type: 'string' }, secret: { type: 'string' } }, required: ['invitation', 'method', 'account', 'secret'] },
7
+ };
8
+ const SEEN = 200;
9
+ export class EnvoyFaculty extends Faculty {
10
+ static cells = { watched: {}, minted: 0, seen: [] };
11
+ async watch(args, asker) {
12
+ if (typeof args.account !== 'string' || !args.account || typeof args.secret !== 'string' || !args.secret)
13
+ return { error: 'an account and its secret' };
14
+ const out = await super.watch(args, asker);
15
+ const id = out.watching;
16
+ if (typeof id === 'string')
17
+ Object.assign(this.cells.watched[id], { account: args.account, secret: args.secret });
18
+ return out;
19
+ }
20
+ #waking = Promise.resolve();
21
+ // The wake that went out after the last `{ ok }`, for the device's own
22
+ // code and a suite.
23
+ get waking() {
24
+ return this.#waking;
25
+ }
26
+ // The vendor's bytes, as they arrived on a path she claims, handed by the
27
+ // http faculty; nothing in them is trusted before `verify`.
28
+ async inbound(args) {
29
+ const body = typeof args.body === 'string' ? args.body : '';
30
+ const headers = (args.headers !== null && typeof args.headers === 'object' && !Array.isArray(args.headers) ? args.headers : {});
31
+ const account = this.account(body, headers);
32
+ const watched = this.cells.watched;
33
+ const mine = account === null ? [] : Object.entries(watched).filter(([, w]) => w.account === account);
34
+ if (mine.length === 0)
35
+ return { error: 'nobody watches that account' };
36
+ // every watcher of the account whose secret the bytes were signed with
37
+ const at = [];
38
+ for (const [id, w] of mine)
39
+ if (await this.verify(body, headers, w.secret))
40
+ at.push(id);
41
+ if (at.length === 0)
42
+ return { error: 'bad signature' };
43
+ const d = this.decode(body, headers);
44
+ if ('error' in d)
45
+ return d;
46
+ const seen = this.cells.seen;
47
+ if (seen.includes(d.id))
48
+ return { ok: true };
49
+ seen.push(d.id);
50
+ while (seen.length > SEEN)
51
+ seen.shift();
52
+ this.#waking = (async () => {
53
+ for (const id of at)
54
+ await this.standings[id]?.ask(d.method, d.args);
55
+ })();
56
+ return { ok: true };
57
+ }
58
+ // Which account the bytes are for: the one thing read before they are
59
+ // verified, since it names whose secret verifies them.
60
+ account(_body, _headers) {
61
+ return null;
62
+ }
63
+ verify(_body, _headers, _secret) {
64
+ return false;
65
+ }
66
+ decode(_body, _headers) {
67
+ return { error: 'this faculty is not called in' };
68
+ }
69
+ // A signature over text with a shared secret, hex: the shape most vendors
70
+ // sign in, here once so a leaf writes only the vendor's framing.
71
+ async hmac(secret, text) {
72
+ const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
73
+ return [...new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(text)))].map((x) => x.toString(16).padStart(2, '0')).join('');
74
+ }
75
+ }
@@ -0,0 +1,68 @@
1
+ import { Lent } from 'nervur';
2
+ import type { Asker, JsonObject, OccupantRecord, Stance } from 'nervur';
3
+ import type { Contract } from './contract.ts';
4
+ import { type Harborage } from './box.ts';
5
+ export type Watched = {
6
+ by: string;
7
+ method: string;
8
+ args: JsonObject;
9
+ };
10
+ export type Woke = {
11
+ id: string;
12
+ by: string;
13
+ out: unknown;
14
+ };
15
+ export declare const HTTP = "http:";
16
+ export declare const INBOUND: {
17
+ description: string;
18
+ input: {
19
+ type: string;
20
+ properties: {
21
+ body: {
22
+ type: string;
23
+ };
24
+ headers: {
25
+ type: string;
26
+ };
27
+ query: {
28
+ type: string;
29
+ };
30
+ method: {
31
+ type: string;
32
+ };
33
+ };
34
+ required: string[];
35
+ };
36
+ for: (occ: OccupantRecord | undefined) => boolean;
37
+ };
38
+ export declare const WATCH: {
39
+ description: string;
40
+ input: {
41
+ type: string;
42
+ properties: {
43
+ invitation: {
44
+ type: string;
45
+ };
46
+ method: {
47
+ type: string;
48
+ };
49
+ args: {
50
+ type: string;
51
+ };
52
+ };
53
+ required: string[];
54
+ };
55
+ };
56
+ export declare class Faculty extends Lent {
57
+ static cells: JsonObject;
58
+ static fulfils: Contract[];
59
+ static stands(): boolean;
60
+ static claims: string[];
61
+ readonly box: Harborage;
62
+ constructor(stance: Stance, box?: Harborage);
63
+ inbound(_args: JsonObject): Promise<JsonObject> | JsonObject;
64
+ watch(args: JsonObject, asker: Asker): Promise<JsonObject>;
65
+ protected welcomed(_id: string): Promise<void>;
66
+ protected woke(round?: (w: Watched) => JsonObject, only?: string): Promise<Woke[] | null>;
67
+ get watching(): Record<string, Watched>;
68
+ }
@@ -0,0 +1,97 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A faculty: a being of the box ward standing for one thing outside every
3
+ // being. She says everything about herself on her class: the contracts she
4
+ // fulfils, whether she can work on this runtime, and the paths she claims on
5
+ // the box's wire. She is born holding her harbor's box. What every faculty
6
+ // shares is here: `watch`, the one way a being asks to be woken, and
7
+ // `woke`, one round of waking whoever asked.
8
+ import { Lent, isInvitation, isSilence, isWord, wordOf } from 'nervur';
9
+ import { ALONE } from './box.js';
10
+ // The ask the http faculty makes of a faculty that claims a path, and only
11
+ // the http faculty: the root places her one occupant per claimed path,
12
+ // `http:<path>`.
13
+ export const HTTP = 'http:';
14
+ export const INBOUND = {
15
+ description: 'bytes that arrived on a path this faculty claims',
16
+ input: { type: 'object', properties: { body: { type: 'string' }, headers: { type: 'object' }, query: { type: 'string' }, method: { type: 'string' } }, required: ['body', 'headers'] },
17
+ for: (occ) => occ?.id.startsWith(HTTP) === true,
18
+ };
19
+ export const WATCH = {
20
+ description: 'wake me: an invitation back, the ask to wake me with, and what to add to it',
21
+ input: { type: 'object', properties: { invitation: { type: 'object' }, method: { type: 'string' }, args: { type: 'object' } }, required: ['invitation', 'method'] },
22
+ };
23
+ export class Faculty extends Lent {
24
+ static cells = { watched: {}, minted: 0 };
25
+ // The contracts she fulfils; the box lends her under each one's digest.
26
+ static fulfils = [];
27
+ // Whether she can work on the runtime reading this, answered honestly.
28
+ // One that cannot is never booted and never lent.
29
+ static stands() {
30
+ return true;
31
+ }
32
+ // The paths on the box's wire she answers. The http faculty hands her the
33
+ // bytes that arrive on one as `inbound`, and nobody else may ask it.
34
+ static claims = [];
35
+ // What her harbor is to her, handed at birth.
36
+ box;
37
+ constructor(stance, box = ALONE) {
38
+ super(stance);
39
+ this.box = box;
40
+ }
41
+ // Bytes on a path she claims: the body as text, the headers, the query and
42
+ // the method. This body is called in by nobody; a faculty that claims a
43
+ // path says what the bytes mean.
44
+ inbound(_args) {
45
+ return { error: 'this faculty is not called in' };
46
+ }
47
+ // Someone wants waking. She hands an invitation back and says what to ask
48
+ // her; this knocks it, takes it under an id of this being's own, and files
49
+ // it. The empty ask is the knock, because every being answers one.
50
+ async watch(args, asker) {
51
+ const inv = args.invitation;
52
+ if (!isInvitation(inv) || typeof args.method !== 'string')
53
+ return { error: 'an invitation and a method' };
54
+ const extra = args.args;
55
+ if (extra !== undefined && (extra === null || typeof extra !== 'object' || Array.isArray(extra)))
56
+ return { error: 'args is one object' };
57
+ const out = await this.knock(inv);
58
+ if (isSilence(out) || isWord(out))
59
+ return { error: 'no way back' };
60
+ const cells = this.cells;
61
+ const id = await this.take(`wake:${(cells.minted += 1)}`, inv);
62
+ if (id === null)
63
+ return { error: 'not taken' };
64
+ cells.watched[id] = { by: asker.id ?? '', method: args.method, args: extra ?? {} };
65
+ await this.welcomed(id);
66
+ return { watching: id };
67
+ }
68
+ // Somebody new is filed, and the answer to her `watch` waits for this. A
69
+ // faculty that owes a new subscriber something at once says it here.
70
+ async welcomed(_id) { }
71
+ // One round. Each subscriber is asked what she asked to be asked, with
72
+ // whatever this round adds; `only` is one of them. A subscriber who removed
73
+ // the occupant says `removed` once and her row goes. Null is this being
74
+ // herself taken out of her ward, so whoever holds an interval puts it down.
75
+ async woke(round = (w) => w.args, only) {
76
+ const watched = this.cells.watched;
77
+ const out = [];
78
+ for (const [id, w] of Object.entries(watched)) {
79
+ if (only !== undefined && id !== only)
80
+ continue;
81
+ const said = await this.standings[id]?.ask(w.method, round(w));
82
+ if (said === undefined || (isWord(said) && wordOf(said) === 'dropped'))
83
+ return null;
84
+ if (isWord(said) && wordOf(said) === 'removed') {
85
+ this.standings.remove(id);
86
+ delete watched[id];
87
+ continue;
88
+ }
89
+ out.push({ id, by: w.by, out: said });
90
+ }
91
+ return out;
92
+ }
93
+ // Who is waiting, by this being's own id for the relation.
94
+ get watching() {
95
+ return this.cells.watched;
96
+ }
97
+ }
@@ -1,4 +1,4 @@
1
- import type { Ring } from './push.ts';
1
+ import type { Ring } from './ring.ts';
2
2
  export type Account = {
3
3
  project_id: string;
4
4
  client_email: string;
@@ -1,5 +1,5 @@
1
1
  import { Faculty } from './faculty.ts';
2
- export declare const GRANTS = "grants";
2
+ export declare const GRANTS: import("nervur").Blueprint;
3
3
  export declare const ACCESS_TTL: number;
4
4
  export declare const REFRESH_TTL: number;
5
5
  export type Grant = {
@@ -25,9 +25,9 @@ export declare class Grants extends Faculty {
25
25
  refresh: {};
26
26
  };
27
27
  static asks: {};
28
- persist: () => Promise<void>;
29
- now: () => number;
28
+ static fulfils: import("nervur").Blueprint[];
30
29
  get kept(): Kept;
30
+ now(): number;
31
31
  issue(identity: string, ward: string, client: string): Promise<Tokens>;
32
32
  refresh(refresh_token: string): Promise<Tokens | null>;
33
33
  bearer(req: Request): {
@@ -37,5 +37,6 @@ export declare class Grants extends Faculty {
37
37
  revoke(identity: string, ward: string): Promise<void>;
38
38
  sweep(): void;
39
39
  }
40
+ export type Bearers = Pick<Grants, 'issue' | 'refresh' | 'bearer' | 'revoke' | 'now'>;
40
41
  export declare const offered: (req: Request) => string;
41
42
  export {};