@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
package/beings/envoy.ts DELETED
@@ -1,218 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The envoy base: what every envoy is and no envoy has alone. An envoy is
3
- // a being of the org's standing in for a ward she does not run, an account
4
- // at a thing that speaks no Quo, and the trunk's envoy chapter says the
5
- // seven things she is. This is those seven as one class an author extends,
6
- // so that a vendor's envoy is her asks out, her signature and her decoding,
7
- // and nothing else.
8
- //
9
- // Two gates, one credential, one way out, one way in, one memory, one way
10
- // back. `credential` and `forget` are the owner's; `deliver`, for an envoy
11
- // that is called in, is the owner's too, and the daemon asks it with the
12
- // vendor's raw bytes and headers. What an occupant may ask is the class's
13
- // asks out, gated by `occupant` or by her own reading of the notes.
14
- //
15
- // Nothing here knows a vendor's name. The signature is `verify`, the
16
- // vocabulary is `decode`, and what a delivery wakes is `wake`, each a
17
- // method the class fills; the base keeps the discipline around them: the
18
- // bytes checked before anything is parsed, the id kept bounded so a retry
19
- // lands once, `{ ok }` answered before the wake goes out, a wake that meets
20
- // silence written down and never retried.
21
- import { Being, isSilence, isWord, wordOf } from 'nervur';
22
- import type { Asker, AskSpec, JsonObject, OccupantRecord } from 'nervur';
23
-
24
- // The two gates. The owner is the ward's ask pointer, `{ id: 'OWNER' }`,
25
- // which is how the daemon asks `deliver` and how the root places a
26
- // credential; an occupant is anyone holding a standing at her.
27
- export const owner = (_rec: OccupantRecord | undefined, asker: Asker): boolean => asker.id === 'OWNER';
28
- export const occupant = (rec: OccupantRecord | undefined): boolean => rec !== undefined;
29
-
30
- // The third gate: who may place what the vendor gave this account. WORLDS
31
- // has placing an envoy as an ask on home, shown by the gate to the org's
32
- // presences and to its model, with the box never logged into for it, so
33
- // this cannot be the owner alone. It is the owner or an occupant whose
34
- // note says she may, placed with her standing the way every other note is,
35
- // and it is never a default: an org that marked nobody has the root's
36
- // shell and nothing else, which is where this began.
37
- export const places = (rec: OccupantRecord | undefined, asker: Asker): boolean => asker.id === 'OWNER' || rec?.notes.credential === true;
38
-
39
- // A credential as a form, for a class that wants one. `at` is never among
40
- // the fields and cannot be: it is where this box sends the org's key, so a
41
- // form that set it would be a form that points the box at anywhere and
42
- // makes it fetch there with the account's own secret. Where a vendor
43
- // answers is the class's, since the class is the thing that speaks that
44
- // vendor, and only the owner may name another.
45
- export const CREDENTIAL = (properties: Record<string, JsonObject>, required: string[]): AskSpec => ({
46
- description: 'what the vendor gave this account: kept in her cells, and never shown again',
47
- input: { type: 'object', properties, required },
48
- for: places,
49
- });
50
-
51
- // One delivery, decoded by the class into what the base needs: the
52
- // vendor's id for it, its kind in the vendor's words, which vendor id it
53
- // is about, and the values the class read off it.
54
- export type Delivery = { id: string; kind: string; about: string | null; data: JsonObject };
55
- // What a delivery wakes: one ask on the way back to whoever the vendor id
56
- // was remembered for, or nothing.
57
- export type Wake = { method: string; args: JsonObject };
58
- // What a way out heard: the status, and the body as an object, `{}` when
59
- // there was none or it was not JSON.
60
- export type Heard = { ok: boolean; status: number; body: JsonObject };
61
-
62
- const SEEN = 200;
63
- const FRESH = 5 * 60_000;
64
-
65
- export class Envoy extends Being {
66
- static override cells: JsonObject = { credential: null, about: {}, seen: [], unwoken: [] };
67
- static override asks: Record<string, AskSpec> = {
68
- credential: { description: 'what the vendor gave the account: kept in her cells, never shown', input: { type: 'object' }, for: places },
69
- forget: { description: 'forget the credential', input: { type: 'object' }, for: places },
70
- };
71
- // The one ask in, for an envoy the vendor calls: a class that is called
72
- // in spreads it into her asks, `{ ...Envoy.asks, ...Envoy.IN, ...hers }`,
73
- // and a one-way envoy shows no deliver at all.
74
- static IN: Record<string, AskSpec> = {
75
- deliver: { description: 'the vendor delivers: its raw bytes and headers, from the daemon as the owner', input: { type: 'object', properties: { body: { type: 'string' }, headers: { type: 'object' } }, required: ['body', 'headers'] }, for: owner },
76
- };
77
-
78
- // 1. Her credential is in her cells, and she gets it herself. The owner
79
- // places what the vendor gave, values only; `check` is the class's word
80
- // on what it must hold, a reason or null.
81
- protected check(c: JsonObject): string | null {
82
- for (const [k, v] of Object.entries(c)) if (typeof v !== 'string' || v === '') return `${k} is a non-empty string`;
83
- return Object.keys(c).length ? null : 'a credential has at least one field';
84
- }
85
- // Where the vendor answers, as the class that speaks that vendor knows
86
- // it. A class that leaves it null has an owner who must name one once.
87
- static at: string | null = null;
88
-
89
- credential(args: JsonObject, asker: Asker): JsonObject {
90
- // `at` is the one field a form may not carry. From anyone but the
91
- // owner it is dropped rather than refused, since a human pasting a key
92
- // has said nothing about it either way, and what stands is the one
93
- // already placed or the class's own.
94
- const { at: named, ...rest } = args;
95
- const at = asker.id === 'OWNER' && typeof named === 'string' ? named : ((this.held?.at as string | undefined) ?? (this.constructor as typeof Envoy).at);
96
- const c: JsonObject = { ...rest, ...(typeof at === 'string' ? { at } : {}) };
97
- const why = this.check(c);
98
- if (why) return { error: why };
99
- this.cells.credential = c;
100
- return { kept: true };
101
- }
102
- forget(): JsonObject {
103
- this.cells.credential = null;
104
- return { kept: false };
105
- }
106
- protected get held(): JsonObject | null {
107
- return (this.cells.credential as JsonObject | null) ?? null;
108
- }
109
-
110
- // 2. Her asks out are the vendor's surface, narrowed: one fetch, from
111
- // `at` in her credential with `key` as the bearer, the answer as values.
112
- // The class reshapes what it heard and passes nothing through raw.
113
- protected async out(path: string, init: { method?: string; body?: JsonObject; headers?: Record<string, string> } = {}): Promise<Heard> {
114
- const c = this.held;
115
- if (!c || typeof c.at !== 'string') return { ok: false, status: 0, body: { error: 'no credential: the owner has not placed one' } };
116
- let r: Response;
117
- try {
118
- r = await fetch(`${c.at}${path}`, {
119
- method: init.method ?? (init.body ? 'POST' : 'GET'),
120
- headers: { ...(typeof c.key === 'string' ? { authorization: `Bearer ${c.key}` } : {}), ...(init.body ? { 'content-type': 'application/json' } : {}), ...(init.headers ?? {}) },
121
- ...(init.body ? { body: JSON.stringify(init.body) } : {}),
122
- });
123
- } catch {
124
- return { ok: false, status: 0, body: { error: 'the vendor is unreachable' } };
125
- }
126
- const v = (await r.json().catch(() => ({}))) as unknown;
127
- const body = v !== null && typeof v === 'object' && !Array.isArray(v) ? (v as JsonObject) : {};
128
- return { ok: r.ok, status: r.status, body };
129
- }
130
- // The vendor's no, as an error object in her own words.
131
- protected refused(h: Heard): JsonObject {
132
- return h.status === 0 ? h.body : { error: `the vendor refused: ${h.status}${typeof h.body.message === 'string' ? ` ${h.body.message}` : ''}` };
133
- }
134
-
135
- // 4. Her cells are what the vendor told her: the map from the vendor's id
136
- // to whom it is about, written when she asked out, read when called in.
137
- protected remember(vendorId: string, by: string, more: JsonObject = {}): void {
138
- (this.cells.about as Record<string, JsonObject>)[vendorId] = { by, ...more };
139
- }
140
- protected recall(vendorId: string): (JsonObject & { by: string }) | undefined {
141
- const m = this.cells.about as Record<string, JsonObject>;
142
- return Object.hasOwn(m, vendorId) ? (m[vendorId] as JsonObject & { by: string }) : undefined;
143
- }
144
-
145
- // 3. `deliver` is her one ask in. The daemon asks it as the owner with
146
- // the raw body and the headers; an empty body is a redirect landing,
147
- // the query in the headers under `:query`. The signature is checked
148
- // before anything is parsed; a delivery she has seen is `{ ok }` at once
149
- // and wakes nobody twice; and 6, `{ ok }` goes back before the wake.
150
- async deliver(args: JsonObject): Promise<JsonObject> {
151
- const body = typeof args.body === 'string' ? args.body : '';
152
- const headers = (args.headers && typeof args.headers === 'object' && !Array.isArray(args.headers) ? args.headers : {}) as Record<string, string>;
153
- if (body === '') return this.landed(headers[':query'] ?? '', headers);
154
- if (!(await this.verify(body, headers))) return { error: 'bad signature' };
155
- const d = this.decode(body, headers);
156
- if ('error' in d) return d as JsonObject;
157
- const seen = this.cells.seen as string[];
158
- const mark = `${d.kind}:${d.id}`;
159
- if (seen.includes(mark)) return { ok: true };
160
- seen.push(mark);
161
- while (seen.length > SEEN) seen.shift();
162
- void this.#wake(d);
163
- return { ok: true };
164
- }
165
- // The class's word on the vendor's signature, over the bytes as sent.
166
- protected verify(_body: string, _headers: Record<string, string>): Promise<boolean> | boolean {
167
- return false;
168
- }
169
- // The class's reading of the vendor's vocabulary.
170
- protected decode(_body: string, _headers: Record<string, string>): Delivery | { error: string } {
171
- return { error: 'not called in' };
172
- }
173
- // Where a redirect lands: an OAuth code in the query, for a class that
174
- // begins its account that way. The base takes nothing from it.
175
- protected landed(_query: string, _headers: Record<string, string>): JsonObject | Promise<JsonObject> {
176
- return { ok: true };
177
- }
178
- // A signature over text with a shared secret, hex, the shape most
179
- // vendors sign in: here once so a class writes the vendor's framing only.
180
- protected async hmac(secret: string, text: string): Promise<string> {
181
- const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
182
- return [...new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(text)))].map((x) => x.toString(16).padStart(2, '0')).join('');
183
- }
184
- // Whether a vendor's timestamp is within five minutes of now, in seconds
185
- // or in milliseconds, since vendors sign in either and a class should
186
- // not have to know which.
187
- protected fresh(ts: string | number): boolean {
188
- const t = Number(ts);
189
- if (!Number.isFinite(t)) return false;
190
- const ms = t < 1e11 ? t * 1000 : t;
191
- return Math.abs(Date.now() - ms) <= FRESH;
192
- }
193
-
194
- // 5. Her way back is a standing placed like any other, `to:<id>` for the
195
- // occupant a vendor id was remembered for, handed to her at the knock.
196
- // A delivery is one ask on it, in her own words, said by `wake`; the
197
- // being woken cannot tell it from a clock.
198
- protected wake(_d: Delivery, _about: (JsonObject & { by: string }) | undefined): Wake | null {
199
- return null;
200
- }
201
- async #wake(d: Delivery): Promise<void> {
202
- const about = d.about === null ? undefined : this.recall(d.about);
203
- const w = this.wake(d, about);
204
- if (!w) return;
205
- const unwoken = this.cells.unwoken as JsonObject[];
206
- // a wake the class shaped wrong is written down, never dropped on the floor
207
- if (typeof w.method !== 'string' || !w.method || w.args === null || typeof w.args !== 'object') return void unwoken.push({ id: d.id, kind: d.kind, why: 'a wake is { method, args }' });
208
- const back = about ? this.standings[`to:${about.by}`] : undefined;
209
- if (!back) return void unwoken.push({ id: d.id, kind: d.kind, why: about ? `no way back to ${about.by}` : 'about nobody she remembers' });
210
- const out = await back.ask(w.method, w.args);
211
- if (isSilence(out) || out === undefined) unwoken.push({ id: d.id, kind: d.kind, why: 'silence' });
212
- else if (isWord(out)) unwoken.push({ id: d.id, kind: d.kind, why: wordOf(out) });
213
- }
214
- // What she could not wake, for the device's own code and a suite.
215
- get unwoken(): JsonObject[] {
216
- return this.cells.unwoken as JsonObject[];
217
- }
218
- }
@@ -1,54 +0,0 @@
1
- import { Being } from 'nervur';
2
- import type { Asker, AskSpec, JsonObject, OccupantRecord } from 'nervur';
3
- export declare const owner: (_rec: OccupantRecord | undefined, asker: Asker) => boolean;
4
- export declare const occupant: (rec: OccupantRecord | undefined) => boolean;
5
- export declare const places: (rec: OccupantRecord | undefined, asker: Asker) => boolean;
6
- export declare const CREDENTIAL: (properties: Record<string, JsonObject>, required: string[]) => AskSpec;
7
- export type Delivery = {
8
- id: string;
9
- kind: string;
10
- about: string | null;
11
- data: JsonObject;
12
- };
13
- export type Wake = {
14
- method: string;
15
- args: JsonObject;
16
- };
17
- export type Heard = {
18
- ok: boolean;
19
- status: number;
20
- body: JsonObject;
21
- };
22
- export declare class Envoy extends Being {
23
- #private;
24
- static cells: JsonObject;
25
- static asks: Record<string, AskSpec>;
26
- static IN: Record<string, AskSpec>;
27
- protected check(c: JsonObject): string | null;
28
- static at: string | null;
29
- credential(args: JsonObject, asker: Asker): JsonObject;
30
- forget(): JsonObject;
31
- protected get held(): JsonObject | null;
32
- protected out(path: string, init?: {
33
- method?: string;
34
- body?: JsonObject;
35
- headers?: Record<string, string>;
36
- }): Promise<Heard>;
37
- protected refused(h: Heard): JsonObject;
38
- protected remember(vendorId: string, by: string, more?: JsonObject): void;
39
- protected recall(vendorId: string): (JsonObject & {
40
- by: string;
41
- }) | undefined;
42
- deliver(args: JsonObject): Promise<JsonObject>;
43
- protected verify(_body: string, _headers: Record<string, string>): Promise<boolean> | boolean;
44
- protected decode(_body: string, _headers: Record<string, string>): Delivery | {
45
- error: string;
46
- };
47
- protected landed(_query: string, _headers: Record<string, string>): JsonObject | Promise<JsonObject>;
48
- protected hmac(secret: string, text: string): Promise<string>;
49
- protected fresh(ts: string | number): boolean;
50
- protected wake(_d: Delivery, _about: (JsonObject & {
51
- by: string;
52
- }) | undefined): Wake | null;
53
- get unwoken(): JsonObject[];
54
- }
@@ -1,211 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The envoy base: what every envoy is and no envoy has alone. An envoy is
3
- // a being of the org's standing in for a ward she does not run, an account
4
- // at a thing that speaks no Quo, and the trunk's envoy chapter says the
5
- // seven things she is. This is those seven as one class an author extends,
6
- // so that a vendor's envoy is her asks out, her signature and her decoding,
7
- // and nothing else.
8
- //
9
- // Two gates, one credential, one way out, one way in, one memory, one way
10
- // back. `credential` and `forget` are the owner's; `deliver`, for an envoy
11
- // that is called in, is the owner's too, and the daemon asks it with the
12
- // vendor's raw bytes and headers. What an occupant may ask is the class's
13
- // asks out, gated by `occupant` or by her own reading of the notes.
14
- //
15
- // Nothing here knows a vendor's name. The signature is `verify`, the
16
- // vocabulary is `decode`, and what a delivery wakes is `wake`, each a
17
- // method the class fills; the base keeps the discipline around them: the
18
- // bytes checked before anything is parsed, the id kept bounded so a retry
19
- // lands once, `{ ok }` answered before the wake goes out, a wake that meets
20
- // silence written down and never retried.
21
- import { Being, isSilence, isWord, wordOf } from 'nervur';
22
- // The two gates. The owner is the ward's ask pointer, `{ id: 'OWNER' }`,
23
- // which is how the daemon asks `deliver` and how the root places a
24
- // credential; an occupant is anyone holding a standing at her.
25
- export const owner = (_rec, asker) => asker.id === 'OWNER';
26
- export const occupant = (rec) => rec !== undefined;
27
- // The third gate: who may place what the vendor gave this account. WORLDS
28
- // has placing an envoy as an ask on home, shown by the gate to the org's
29
- // presences and to its model, with the box never logged into for it, so
30
- // this cannot be the owner alone. It is the owner or an occupant whose
31
- // note says she may, placed with her standing the way every other note is,
32
- // and it is never a default: an org that marked nobody has the root's
33
- // shell and nothing else, which is where this began.
34
- export const places = (rec, asker) => asker.id === 'OWNER' || rec?.notes.credential === true;
35
- // A credential as a form, for a class that wants one. `at` is never among
36
- // the fields and cannot be: it is where this box sends the org's key, so a
37
- // form that set it would be a form that points the box at anywhere and
38
- // makes it fetch there with the account's own secret. Where a vendor
39
- // answers is the class's, since the class is the thing that speaks that
40
- // vendor, and only the owner may name another.
41
- export const CREDENTIAL = (properties, required) => ({
42
- description: 'what the vendor gave this account: kept in her cells, and never shown again',
43
- input: { type: 'object', properties, required },
44
- for: places,
45
- });
46
- const SEEN = 200;
47
- const FRESH = 5 * 60_000;
48
- export class Envoy extends Being {
49
- static cells = { credential: null, about: {}, seen: [], unwoken: [] };
50
- static asks = {
51
- credential: { description: 'what the vendor gave the account: kept in her cells, never shown', input: { type: 'object' }, for: places },
52
- forget: { description: 'forget the credential', input: { type: 'object' }, for: places },
53
- };
54
- // The one ask in, for an envoy the vendor calls: a class that is called
55
- // in spreads it into her asks, `{ ...Envoy.asks, ...Envoy.IN, ...hers }`,
56
- // and a one-way envoy shows no deliver at all.
57
- static IN = {
58
- deliver: { description: 'the vendor delivers: its raw bytes and headers, from the daemon as the owner', input: { type: 'object', properties: { body: { type: 'string' }, headers: { type: 'object' } }, required: ['body', 'headers'] }, for: owner },
59
- };
60
- // 1. Her credential is in her cells, and she gets it herself. The owner
61
- // places what the vendor gave, values only; `check` is the class's word
62
- // on what it must hold, a reason or null.
63
- check(c) {
64
- for (const [k, v] of Object.entries(c))
65
- if (typeof v !== 'string' || v === '')
66
- return `${k} is a non-empty string`;
67
- return Object.keys(c).length ? null : 'a credential has at least one field';
68
- }
69
- // Where the vendor answers, as the class that speaks that vendor knows
70
- // it. A class that leaves it null has an owner who must name one once.
71
- static at = null;
72
- credential(args, asker) {
73
- // `at` is the one field a form may not carry. From anyone but the
74
- // owner it is dropped rather than refused, since a human pasting a key
75
- // has said nothing about it either way, and what stands is the one
76
- // already placed or the class's own.
77
- const { at: named, ...rest } = args;
78
- const at = asker.id === 'OWNER' && typeof named === 'string' ? named : (this.held?.at ?? this.constructor.at);
79
- const c = { ...rest, ...(typeof at === 'string' ? { at } : {}) };
80
- const why = this.check(c);
81
- if (why)
82
- return { error: why };
83
- this.cells.credential = c;
84
- return { kept: true };
85
- }
86
- forget() {
87
- this.cells.credential = null;
88
- return { kept: false };
89
- }
90
- get held() {
91
- return this.cells.credential ?? null;
92
- }
93
- // 2. Her asks out are the vendor's surface, narrowed: one fetch, from
94
- // `at` in her credential with `key` as the bearer, the answer as values.
95
- // The class reshapes what it heard and passes nothing through raw.
96
- async out(path, init = {}) {
97
- const c = this.held;
98
- if (!c || typeof c.at !== 'string')
99
- return { ok: false, status: 0, body: { error: 'no credential: the owner has not placed one' } };
100
- let r;
101
- try {
102
- r = await fetch(`${c.at}${path}`, {
103
- method: init.method ?? (init.body ? 'POST' : 'GET'),
104
- headers: { ...(typeof c.key === 'string' ? { authorization: `Bearer ${c.key}` } : {}), ...(init.body ? { 'content-type': 'application/json' } : {}), ...(init.headers ?? {}) },
105
- ...(init.body ? { body: JSON.stringify(init.body) } : {}),
106
- });
107
- }
108
- catch {
109
- return { ok: false, status: 0, body: { error: 'the vendor is unreachable' } };
110
- }
111
- const v = (await r.json().catch(() => ({})));
112
- const body = v !== null && typeof v === 'object' && !Array.isArray(v) ? v : {};
113
- return { ok: r.ok, status: r.status, body };
114
- }
115
- // The vendor's no, as an error object in her own words.
116
- refused(h) {
117
- return h.status === 0 ? h.body : { error: `the vendor refused: ${h.status}${typeof h.body.message === 'string' ? ` ${h.body.message}` : ''}` };
118
- }
119
- // 4. Her cells are what the vendor told her: the map from the vendor's id
120
- // to whom it is about, written when she asked out, read when called in.
121
- remember(vendorId, by, more = {}) {
122
- this.cells.about[vendorId] = { by, ...more };
123
- }
124
- recall(vendorId) {
125
- const m = this.cells.about;
126
- return Object.hasOwn(m, vendorId) ? m[vendorId] : undefined;
127
- }
128
- // 3. `deliver` is her one ask in. The daemon asks it as the owner with
129
- // the raw body and the headers; an empty body is a redirect landing,
130
- // the query in the headers under `:query`. The signature is checked
131
- // before anything is parsed; a delivery she has seen is `{ ok }` at once
132
- // and wakes nobody twice; and 6, `{ ok }` goes back before the wake.
133
- async deliver(args) {
134
- const body = typeof args.body === 'string' ? args.body : '';
135
- const headers = (args.headers && typeof args.headers === 'object' && !Array.isArray(args.headers) ? args.headers : {});
136
- if (body === '')
137
- return this.landed(headers[':query'] ?? '', headers);
138
- if (!(await this.verify(body, headers)))
139
- return { error: 'bad signature' };
140
- const d = this.decode(body, headers);
141
- if ('error' in d)
142
- return d;
143
- const seen = this.cells.seen;
144
- const mark = `${d.kind}:${d.id}`;
145
- if (seen.includes(mark))
146
- return { ok: true };
147
- seen.push(mark);
148
- while (seen.length > SEEN)
149
- seen.shift();
150
- void this.#wake(d);
151
- return { ok: true };
152
- }
153
- // The class's word on the vendor's signature, over the bytes as sent.
154
- verify(_body, _headers) {
155
- return false;
156
- }
157
- // The class's reading of the vendor's vocabulary.
158
- decode(_body, _headers) {
159
- return { error: 'not called in' };
160
- }
161
- // Where a redirect lands: an OAuth code in the query, for a class that
162
- // begins its account that way. The base takes nothing from it.
163
- landed(_query, _headers) {
164
- return { ok: true };
165
- }
166
- // A signature over text with a shared secret, hex, the shape most
167
- // vendors sign in: here once so a class writes the vendor's framing only.
168
- async hmac(secret, text) {
169
- const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
170
- return [...new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(text)))].map((x) => x.toString(16).padStart(2, '0')).join('');
171
- }
172
- // Whether a vendor's timestamp is within five minutes of now, in seconds
173
- // or in milliseconds, since vendors sign in either and a class should
174
- // not have to know which.
175
- fresh(ts) {
176
- const t = Number(ts);
177
- if (!Number.isFinite(t))
178
- return false;
179
- const ms = t < 1e11 ? t * 1000 : t;
180
- return Math.abs(Date.now() - ms) <= FRESH;
181
- }
182
- // 5. Her way back is a standing placed like any other, `to:<id>` for the
183
- // occupant a vendor id was remembered for, handed to her at the knock.
184
- // A delivery is one ask on it, in her own words, said by `wake`; the
185
- // being woken cannot tell it from a clock.
186
- wake(_d, _about) {
187
- return null;
188
- }
189
- async #wake(d) {
190
- const about = d.about === null ? undefined : this.recall(d.about);
191
- const w = this.wake(d, about);
192
- if (!w)
193
- return;
194
- const unwoken = this.cells.unwoken;
195
- // a wake the class shaped wrong is written down, never dropped on the floor
196
- if (typeof w.method !== 'string' || !w.method || w.args === null || typeof w.args !== 'object')
197
- return void unwoken.push({ id: d.id, kind: d.kind, why: 'a wake is { method, args }' });
198
- const back = about ? this.standings[`to:${about.by}`] : undefined;
199
- if (!back)
200
- return void unwoken.push({ id: d.id, kind: d.kind, why: about ? `no way back to ${about.by}` : 'about nobody she remembers' });
201
- const out = await back.ask(w.method, w.args);
202
- if (isSilence(out) || out === undefined)
203
- unwoken.push({ id: d.id, kind: d.kind, why: 'silence' });
204
- else if (isWord(out))
205
- unwoken.push({ id: d.id, kind: d.kind, why: wordOf(out) });
206
- }
207
- // What she could not wake, for the device's own code and a suite.
208
- get unwoken() {
209
- return this.cells.unwoken;
210
- }
211
- }
@@ -1,6 +0,0 @@
1
- import { Carrier } from '../beings/carry.ts';
2
- import type { Asker, OccupantRecord } from 'nervur';
3
- export declare const CONSOLE = "console";
4
- export declare class Console extends Carrier {
5
- static carries(occupant: OccupantRecord | undefined, _asker: Asker): boolean;
6
- }
@@ -1,20 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The box, as one being. A device has no surface of its own, so a box that
3
- // has joined no world has nothing to draw: the Console is that page and only
4
- // that. She carries the faculties her harbor stood, showing each one's asks
5
- // as her own, and the ordinary screen draws her exactly as it draws a
6
- // world's user being, through one avatar holding one standing.
7
- //
8
- // She exists because a fresh install otherwise asks for location and
9
- // notifications in its manifest and shows nothing that ever asks for them,
10
- // which is a store reviewer's first question and a human's second.
11
- //
12
- // She carries for whoever the harbor's root let in, and for nobody else: the
13
- // root is the box, and it invites the one avatar its own screen runs.
14
- import { Carrier } from '../beings/carry.js';
15
- export const CONSOLE = 'console';
16
- export class Console extends Carrier {
17
- static carries(occupant, _asker) {
18
- return occupant !== undefined;
19
- }
20
- }
@@ -1,75 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // The box's dialers: `dialers`, one faculty of the dock ward, standing for
3
- // the sockets this box holds open to other harbors' `quo.` routes. A Mac
4
- // behind NAT, a phone on the mobile network, a droplet dialing home: each
5
- // is a dialer, and what it dials is a row in her cells, placed once by the
6
- // root with `keep` the way a schedule is placed on the clock and an agent
7
- // on the agents faculty, in no file.
8
- //
9
- // A row is one URL, the `quo.` route of the harbor dialed. She holds the
10
- // socket from her birth on, as every faculty holds what the box holds: one
11
- // line per row, announcing this harbor's wards on open, binding the far
12
- // side's pks, reconnecting with backoff when the line drops, and closed
13
- // when the row is dropped or the box goes down. The dialer itself is the
14
- // library's, written on the standard socket surface, so this faculty holds
15
- // on every terrain that has a socket; the daemon hands her the harbor to
16
- // dial for, as it hands the agents faculty how to run a row.
17
- import { OWNER } from 'nervur';
18
- import { dial } from 'nervur/harbor';
19
- import { Faculty } from './faculty.js';
20
- // The name the dock ward holds her under.
21
- export const DIALERS = 'dialers';
22
- // The root and nobody else: a line held open is the device's own to place.
23
- const root = (_occ, asker) => asker.id === OWNER;
24
- const URL_ = { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] };
25
- export class Dialers extends Faculty {
26
- static cells = { watched: {}, minted: 0, dialers: {} };
27
- static asks = {
28
- keep: { description: 'hold a socket to that quo. route, from now and from every boot, until it is dropped', input: URL_, for: root },
29
- drop: { description: 'close the socket to that route and forget it', input: URL_, for: root },
30
- list: { description: 'every route she dials, and whether the line is open now', input: { type: 'object' }, for: root },
31
- };
32
- harbor;
33
- open = new Map();
34
- get rows() {
35
- return this.cells.dialers;
36
- }
37
- // Every row dialed, once the daemon has handed the harbor to dial for.
38
- start(harbor) {
39
- this.harbor = harbor;
40
- for (const row of Object.values(this.rows))
41
- this.#dial(row.url);
42
- }
43
- #dial(url) {
44
- if (!this.harbor || this.open.has(url))
45
- return;
46
- this.open.set(url, dial(this.harbor, url));
47
- }
48
- keep(args) {
49
- const url = args.url;
50
- if (typeof url !== 'string' || !/^https?:\/\/\S+$/.test(url))
51
- return { error: 'url is the quo. route of the harbor dialed, over http or https' };
52
- const clean = url.replace(/\/$/, '');
53
- this.rows[clean] = { url: clean };
54
- this.#dial(clean);
55
- return { kept: clean };
56
- }
57
- drop(args) {
58
- const url = typeof args.url === 'string' ? args.url.replace(/\/$/, '') : '';
59
- if (!this.rows[url])
60
- return { error: 'no such route' };
61
- this.open.get(url)?.close();
62
- this.open.delete(url);
63
- delete this.rows[url];
64
- return { dropped: url };
65
- }
66
- list() {
67
- return { dialers: Object.keys(this.rows).map((url) => ({ url, open: this.open.get(url)?.socket !== null && this.open.has(url) })) };
68
- }
69
- // The device puts the box down: every line closes, the rows stay.
70
- close() {
71
- for (const d of this.open.values())
72
- d.close();
73
- this.open.clear();
74
- }
75
- }
@@ -1,24 +0,0 @@
1
- import { Being } from 'nervur';
2
- import type { Asker, JsonObject } from 'nervur';
3
- export type Watched = {
4
- by: string;
5
- method: string;
6
- args: JsonObject;
7
- };
8
- export type Woke = {
9
- id: string;
10
- by: string;
11
- out: unknown;
12
- };
13
- export declare class Faculty extends Being {
14
- static cells: JsonObject;
15
- watch(args: JsonObject, asker: Asker): Promise<JsonObject>;
16
- protected welcomed(_id: string): Promise<void>;
17
- protected woke(round?: (w: Watched) => JsonObject, only?: string): Promise<Woke[] | null>;
18
- get watching(): Record<string, Watched>;
19
- }
20
- export declare const settled: () => Promise<void>;
21
- export declare function join(b: Being, name: string, watch?: {
22
- method: string;
23
- args?: JsonObject;
24
- }, id?: string): Promise<string | null>;