nervur 0.23.0 → 0.24.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 (157) hide show
  1. package/CONTRIBUTING.md +13 -0
  2. package/LICENSE +55 -202
  3. package/NOTICE +3 -8
  4. package/README.md +14 -156
  5. package/RELEASING.md +15 -0
  6. package/SECURITY.md +9 -0
  7. package/bin/nervur.js +6 -0
  8. package/package.json +26 -81
  9. package/AUTHORING.md +0 -935
  10. package/COMMAND.md +0 -179
  11. package/FACES.md +0 -280
  12. package/FACULTIES.md +0 -578
  13. package/GROUNDS.md +0 -315
  14. package/KIT-SPEC.md +0 -304
  15. package/WORLD.md +0 -195
  16. package/dist/app/app-ground.d.ts +0 -18
  17. package/dist/app/app-ground.js +0 -23
  18. package/dist/app/index.d.ts +0 -2
  19. package/dist/app/index.js +0 -5
  20. package/dist/app/native.d.ts +0 -11
  21. package/dist/app/native.js +0 -13
  22. package/dist/being/being.d.ts +0 -306
  23. package/dist/being/being.js +0 -20
  24. package/dist/being/covers.d.ts +0 -9
  25. package/dist/being/covers.js +0 -21
  26. package/dist/being/index.d.ts +0 -4
  27. package/dist/being/index.js +0 -6
  28. package/dist/being/need.d.ts +0 -49
  29. package/dist/being/need.js +0 -33
  30. package/dist/being/schema.d.ts +0 -100
  31. package/dist/being/schema.js +0 -132
  32. package/dist/being/table.d.ts +0 -48
  33. package/dist/being/table.js +0 -344
  34. package/dist/bench/bench-ground.d.ts +0 -73
  35. package/dist/bench/bench-ground.js +0 -153
  36. package/dist/bench/bench.d.ts +0 -101
  37. package/dist/bench/bench.js +0 -368
  38. package/dist/bench/fake-clock.d.ts +0 -17
  39. package/dist/bench/fake-clock.js +0 -34
  40. package/dist/bench/fake-memory.d.ts +0 -16
  41. package/dist/bench/fake-memory.js +0 -46
  42. package/dist/bench/fake-network.d.ts +0 -66
  43. package/dist/bench/fake-network.js +0 -222
  44. package/dist/bench/fake-unlock.d.ts +0 -6
  45. package/dist/bench/fake-unlock.js +0 -10
  46. package/dist/bench/index.d.ts +0 -3
  47. package/dist/bench/index.js +0 -5
  48. package/dist/bench/seeded.d.ts +0 -11
  49. package/dist/bench/seeded.js +0 -37
  50. package/dist/bench/settle.d.ts +0 -2
  51. package/dist/bench/settle.js +0 -49
  52. package/dist/bench/stewards.d.ts +0 -169
  53. package/dist/bench/stewards.js +0 -128
  54. package/dist/bodies/class-list.d.ts +0 -14
  55. package/dist/bodies/class-list.js +0 -33
  56. package/dist/bodies/joined-carry.d.ts +0 -29
  57. package/dist/bodies/joined-carry.js +0 -48
  58. package/dist/bodies/kept.d.ts +0 -44
  59. package/dist/bodies/kept.js +0 -91
  60. package/dist/bodies/noble-crypto.d.ts +0 -22
  61. package/dist/bodies/noble-crypto.js +0 -88
  62. package/dist/bodies/seed-keys.d.ts +0 -10
  63. package/dist/bodies/seed-keys.js +0 -42
  64. package/dist/bodies/strict-tools.d.ts +0 -10
  65. package/dist/bodies/strict-tools.js +0 -306
  66. package/dist/bodies/vouch.d.ts +0 -20
  67. package/dist/bodies/vouch.js +0 -71
  68. package/dist/bodies/web-carry.d.ts +0 -72
  69. package/dist/bodies/web-carry.js +0 -245
  70. package/dist/bodies/web-clock.d.ts +0 -12
  71. package/dist/bodies/web-clock.js +0 -33
  72. package/dist/browser/browser-ground.d.ts +0 -76
  73. package/dist/browser/browser-ground.js +0 -294
  74. package/dist/browser/index.d.ts +0 -4
  75. package/dist/browser/index.js +0 -7
  76. package/dist/browser/indexeddb-memory.d.ts +0 -17
  77. package/dist/browser/indexeddb-memory.js +0 -97
  78. package/dist/browser/locked-unlock.d.ts +0 -24
  79. package/dist/browser/locked-unlock.js +0 -85
  80. package/dist/browser/origin-classes.d.ts +0 -7
  81. package/dist/browser/origin-classes.js +0 -31
  82. package/dist/edge/durable-clock.d.ts +0 -19
  83. package/dist/edge/durable-clock.js +0 -61
  84. package/dist/edge/durable-memory.d.ts +0 -5
  85. package/dist/edge/durable-memory.js +0 -11
  86. package/dist/edge/durable.d.ts +0 -18
  87. package/dist/edge/durable.js +0 -24
  88. package/dist/edge/edge-ground.d.ts +0 -49
  89. package/dist/edge/edge-ground.js +0 -223
  90. package/dist/edge/index.d.ts +0 -7
  91. package/dist/edge/index.js +0 -9
  92. package/dist/edge/native-crypto.d.ts +0 -8
  93. package/dist/edge/native-crypto.js +0 -102
  94. package/dist/edge/secret-unlock.d.ts +0 -7
  95. package/dist/edge/secret-unlock.js +0 -13
  96. package/dist/edge/socket-carry.d.ts +0 -39
  97. package/dist/edge/socket-carry.js +0 -111
  98. package/dist/foundation.d.ts +0 -162
  99. package/dist/foundation.js +0 -4
  100. package/dist/ground/dock.d.ts +0 -1460
  101. package/dist/ground/dock.js +0 -1159
  102. package/dist/ground/ground.d.ts +0 -239
  103. package/dist/ground/ground.js +0 -787
  104. package/dist/ground/views.d.ts +0 -76
  105. package/dist/ground/views.js +0 -208
  106. package/dist/house/crossing.d.ts +0 -50
  107. package/dist/house/crossing.js +0 -153
  108. package/dist/house/house.d.ts +0 -88
  109. package/dist/house/house.js +0 -2069
  110. package/dist/house/rows-shape.d.ts +0 -110
  111. package/dist/house/rows-shape.js +0 -12
  112. package/dist/house/rows.d.ts +0 -27
  113. package/dist/house/rows.js +0 -185
  114. package/dist/index.d.ts +0 -33
  115. package/dist/index.js +0 -29
  116. package/dist/node/bridge.d.ts +0 -20
  117. package/dist/node/bridge.js +0 -284
  118. package/dist/node/cli.d.ts +0 -2
  119. package/dist/node/cli.js +0 -225
  120. package/dist/node/file-memory.d.ts +0 -13
  121. package/dist/node/file-memory.js +0 -86
  122. package/dist/node/folder-classes.d.ts +0 -5
  123. package/dist/node/folder-classes.js +0 -40
  124. package/dist/node/gone.d.ts +0 -2
  125. package/dist/node/gone.js +0 -7
  126. package/dist/node/hand.d.ts +0 -39
  127. package/dist/node/hand.js +0 -107
  128. package/dist/node/http.d.ts +0 -11
  129. package/dist/node/http.js +0 -90
  130. package/dist/node/index.d.ts +0 -11
  131. package/dist/node/index.js +0 -13
  132. package/dist/node/ledger-memory.d.ts +0 -17
  133. package/dist/node/ledger-memory.js +0 -152
  134. package/dist/node/lock.d.ts +0 -10
  135. package/dist/node/lock.js +0 -78
  136. package/dist/node/node-ground.d.ts +0 -26
  137. package/dist/node/node-ground.js +0 -228
  138. package/dist/node/notify.d.ts +0 -1
  139. package/dist/node/notify.js +0 -18
  140. package/dist/node/shell.d.ts +0 -11
  141. package/dist/node/shell.js +0 -35
  142. package/dist/node/sync-folder.d.ts +0 -6
  143. package/dist/node/sync-folder.js +0 -18
  144. package/dist/node/tcp-carry.d.ts +0 -42
  145. package/dist/node/tcp-carry.js +0 -193
  146. package/dist/node/unlock.d.ts +0 -17
  147. package/dist/node/unlock.js +0 -86
  148. package/dist/node/websocket.d.ts +0 -12
  149. package/dist/node/websocket.js +0 -134
  150. package/dist/quo/frame.d.ts +0 -33
  151. package/dist/quo/frame.js +0 -92
  152. package/dist/quo/index.d.ts +0 -1
  153. package/dist/quo/index.js +0 -5
  154. package/dist/quo/room.d.ts +0 -154
  155. package/dist/quo/room.js +0 -362
  156. package/dist/serve/index.d.ts +0 -45
  157. package/dist/serve/index.js +0 -71
package/dist/quo/room.js DELETED
@@ -1,362 +0,0 @@
1
- const SIZE = 1_048_576;
2
- const PK = 32;
3
- const TAG = 16;
4
- const SIG = 64;
5
- const CIPHERTEXT = 1088;
6
- const MAX_COUNT = 2 ** 53 - 1;
7
- const COUNT = /^[1-9][0-9]*$/;
8
- const PK_HEX = /^[0-9a-f]{64}$/;
9
- const DIGEST = /^[0-9a-f]{16}$/;
10
- const ZERO_PK = '0'.repeat(64);
11
- const WORDS = new Set(['removed', 'unannounced', 'repeated']);
12
- const concat = (...parts) => {
13
- const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
14
- let at = 0;
15
- for (const part of parts) {
16
- out.set(part, at);
17
- at += part.length;
18
- }
19
- return out;
20
- };
21
- export class Room {
22
- #keys;
23
- #crypto;
24
- #tools;
25
- constructor(keys, crypto, tools) {
26
- this.#keys = keys;
27
- this.#crypto = crypto;
28
- this.#tools = tools;
29
- }
30
- #hex(bytes) {
31
- return this.#tools.hex(bytes);
32
- }
33
- #bytes(hex) {
34
- const bytes = this.#tools.bytes(hex);
35
- if (bytes === null)
36
- throw new TypeError('a record holds hex that is not hex');
37
- return bytes;
38
- }
39
- async #cipher(ikm, label) {
40
- const out = await this.#crypto.hkdf(ikm, label, 44);
41
- return { key: out.subarray(0, 32), nonce: out.subarray(32) };
42
- }
43
- async #follow(edge, agreement) {
44
- return this.#crypto.hkdf(concat(edge, agreement), 'quo-edge', 32);
45
- }
46
- /** The ward's name: its signing pk, then its padlock, as 128 hex. */
47
- async ward() {
48
- const ward = await this.#keys.ward();
49
- return this.#hex(concat(ward.signing, ward.padlock));
50
- }
51
- /** A new heir: the invitation to give away, and the occupant to keep. */
52
- async invite(at) {
53
- const ward = await this.#keys.ward();
54
- const secret = this.#crypto.random(32);
55
- const heir = this.#hex(await this.#crypto.signingPublic(secret));
56
- const invitation = {
57
- ward: this.#hex(concat(ward.signing, ward.padlock)),
58
- heir,
59
- secret: this.#hex(secret),
60
- lock: this.#hex(ward.lock),
61
- ...(at === undefined ? {} : { at: [...at] }),
62
- };
63
- return { invitation, occupant: { heir, spent: false, removed: false, held: null, vouched: null, open: null, offered: null, highest: 0 } };
64
- }
65
- /** The occupant after the door stops holding it: gone while fresh, its keys kept while spent. */
66
- release(occupant) {
67
- return occupant.spent ? { ...occupant, removed: true } : null;
68
- }
69
- /** A value read as an invitation, or `null` where it is none. */
70
- async invitation(value) {
71
- if (typeof value !== 'object' || value === null || Array.isArray(value))
72
- return null;
73
- const { ward, heir, secret, lock, at } = value;
74
- const hex = (field, length) => typeof field === 'string' && field.length === length && this.#tools.bytes(field) !== null;
75
- if (!hex(ward, 128) || !hex(heir, 64) || !hex(secret, 64) || !hex(lock, 2368))
76
- return null;
77
- if ((await this.#crypto.encapsulate(this.#bytes(lock))) === null)
78
- return null;
79
- const addresses = Array.isArray(at) ? at.filter((entry) => typeof entry === 'string') : undefined;
80
- return { ward: ward, heir: heir, secret: secret, lock: lock, ...(addresses === undefined ? {} : { at: addresses }) };
81
- }
82
- /** A standing on an invitation, before its knock. */
83
- standing(invitation) {
84
- return { invitation, key: invitation.secret, edge: null, knock: null, bound: false, heard: false, knockedLast: false, sent: 0, moved: 0 };
85
- }
86
- // ---- the door ----
87
- /** A box at the door: refused with its reply, or admitted and waiting for a choice. */
88
- async arrive(box, lookup) {
89
- const refuse = async (why, lid) => ({ refused: true, reply: (await this.#reply(lid, '{"silence":true}')).box, why });
90
- const word = async (why, lid, name) => ({ refused: true, reply: (await this.#reply(lid, `{"quo":"${name}"}`)).box, why });
91
- const lid = box.length >= PK ? box.subarray(0, PK) : null;
92
- if (box.length > SIZE || lid === null)
93
- return refuse(1, lid);
94
- const agreement = await this.#keys.agree(lid);
95
- if (agreement === null)
96
- return refuse(1, lid);
97
- const headSeal = await this.#cipher(agreement, 'quo-seal');
98
- const head = await this.#crypto.open(headSeal.key, headSeal.nonce, box.subarray(PK, PK + PK + TAG), lid);
99
- if (head === null || head.length !== PK)
100
- return refuse(1, lid);
101
- const zero = head.every((byte) => byte === 0);
102
- const heir = zero ? null : this.#hex(head);
103
- const occupant = heir === null ? undefined : await lookup.occupant(heir);
104
- const fresh = occupant !== undefined && !occupant.spent;
105
- // The body, under each edge key the door takes for this head, in order.
106
- let rest = box.subarray(PK + PK + TAG);
107
- let edges;
108
- if (fresh) {
109
- if (rest.length < CIPHERTEXT)
110
- return refuse(1, lid);
111
- const shared = await this.#keys.decapsulate(rest.subarray(0, CIPHERTEXT));
112
- edges = [await this.#crypto.hkdf(shared, 'quo-lock', 32)];
113
- rest = rest.subarray(CIPHERTEXT);
114
- }
115
- else if (occupant === undefined)
116
- edges = [new Uint8Array(32)];
117
- else
118
- edges = [occupant.open, occupant.offered].filter((edge) => edge !== null).map((edge) => this.#bytes(edge));
119
- let body = null;
120
- let under = null;
121
- for (const edge of edges) {
122
- const seal = await this.#cipher(concat(agreement, edge), 'quo-edge-seal');
123
- body = await this.#crypto.open(seal.key, seal.nonce, rest, lid);
124
- if (body !== null) {
125
- under = edge;
126
- break;
127
- }
128
- }
129
- if (body === null || under === null || body.length <= SIG)
130
- return refuse(1, lid);
131
- const payloadBytes = body.subarray(0, body.length - SIG);
132
- const signature = body.subarray(body.length - SIG);
133
- const text = this.#tools.text(payloadBytes);
134
- const parsed = text === null ? null : this.#tools.parse(text);
135
- if (parsed === null || parsed.fields === null || parsed.duplicate)
136
- return refuse(2, lid);
137
- const payload = parsed.value;
138
- const fields = parsed.fields;
139
- const pk = (value) => typeof value === 'string' && PK_HEX.test(value) && value !== ZERO_PK;
140
- const seqText = fields.seq;
141
- const wellFormed = 'to' in fields &&
142
- 'by' in fields &&
143
- 'next' in fields &&
144
- seqText !== undefined &&
145
- (zero ? payload.to === null : payload.to === heir) &&
146
- pk(payload.by) &&
147
- (payload.next === null || pk(payload.next)) &&
148
- COUNT.test(seqText) &&
149
- Number(seqText) <= MAX_COUNT &&
150
- (!('method' in fields) || typeof payload.method === 'string') &&
151
- (!('args' in fields) || (typeof payload.args === 'object' && payload.args !== null && !Array.isArray(payload.args)));
152
- if (!wellFormed)
153
- return refuse(3, lid);
154
- const by = payload.by;
155
- const next = payload.next;
156
- const seq = Number(seqText);
157
- const signedBy = this.#bytes(by);
158
- const admitted = {
159
- refused: false,
160
- heir,
161
- by,
162
- seq,
163
- method: payload.method,
164
- args: fields.args ?? '{}',
165
- strange: parsed.nestedDuplicate,
166
- call: typeof payload.call === 'string' ? payload.call : undefined,
167
- within: Number.isSafeInteger(payload.within) && payload.within >= 0 ? payload.within : undefined,
168
- after: typeof payload.after === 'string' && DIGEST.test(payload.after) ? payload.after : undefined,
169
- signature: this.#hex(signature),
170
- };
171
- if (zero) {
172
- if (!lookup.zero)
173
- return refuse(4, lid);
174
- if (!(await this.#crypto.verify(signedBy, payloadBytes, signature)))
175
- return refuse(5, lid);
176
- return { ...admitted, choose: async (choice) => ({ reply: (await this.#reply(lid, this.#write(choice))).box, occupant: null }) };
177
- }
178
- if (occupant === undefined || (occupant.removed && occupant.held === null))
179
- return refuse(6, lid);
180
- const keys = fresh ? [occupant.heir] : [occupant.held, occupant.vouched].filter((key) => key !== null);
181
- if (!keys.includes(by))
182
- return refuse(7, lid);
183
- if (!(await this.#crypto.verify(signedBy, payloadBytes, signature)))
184
- return refuse(8, lid);
185
- if (occupant.removed)
186
- return word(9, lid, 'removed');
187
- const announced = next === null || next === occupant.heir || next === by ? null : next;
188
- if (fresh && announced === null)
189
- return word(10, lid, 'unannounced');
190
- if (!fresh && seq <= occupant.highest)
191
- return word(11, lid, 'repeated');
192
- return {
193
- ...admitted,
194
- choose: async (choice) => {
195
- const { box: reply, agreement: replied } = await this.#reply(lid, this.#write(choice));
196
- const offered = this.#hex(await this.#follow(under, replied));
197
- const open = this.#hex(under);
198
- const moved = fresh
199
- ? { ...occupant, spent: true, held: announced, vouched: null, open, offered, highest: seq }
200
- : { ...occupant, held: by, vouched: announced ?? (by === occupant.held ? occupant.vouched : null), open, offered, highest: seq };
201
- return { reply, occupant: moved };
202
- },
203
- };
204
- }
205
- #write(choice) {
206
- if ('silence' in choice)
207
- return '{"silence":true}';
208
- const at = choice.at === undefined ? '' : `,"at":${JSON.stringify(choice.at)}`;
209
- return `{"object":${choice.object},"seen":${JSON.stringify(choice.seen)}${at}}`;
210
- }
211
- // A reply sealed to a lid, or to a lid nobody holds where the lid takes no seal.
212
- async #reply(lid, text) {
213
- const secret = this.#crypto.random(32);
214
- const pk = await this.#crypto.agreePublic(secret);
215
- let to = lid;
216
- let agreement = to === null ? null : await this.#crypto.agree(secret, to);
217
- if (agreement === null) {
218
- to = this.#crypto.random(32);
219
- agreement = await this.#crypto.agree(secret, to);
220
- if (agreement === null) {
221
- to = await this.#crypto.agreePublic(this.#crypto.random(32));
222
- agreement = (await this.#crypto.agree(secret, to));
223
- }
224
- }
225
- const textBytes = this.#tools.utf8(text);
226
- const signature = await this.#keys.sign(concat(to, textBytes));
227
- const seal = await this.#cipher(agreement, 'quo-seal');
228
- const sealed = await this.#crypto.seal(seal.key, seal.nonce, concat(textBytes, signature), pk);
229
- return { box: concat(pk, sealed), agreement };
230
- }
231
- // ---- the standing ----
232
- /** The next box on a relation: its knock, its knock again, or an ask. */
233
- async seal(standing, ask) {
234
- const invitation = standing.invitation;
235
- const ward = this.#bytes(invitation.ward);
236
- const padlock = ward.subarray(32);
237
- // A knock no door heard binds nothing, so it goes again alone. After one
238
- // that may have been heard and brought no object back, the knock again
239
- // and an ask under its own key take turns, until the door is known to
240
- // have bound it.
241
- if (standing.knock !== null && !standing.bound && (!standing.heard || (!standing.knockedLast && standing.sent > 1))) {
242
- const kept = JSON.parse(standing.knock);
243
- return { box: this.#bytes(kept.box), pending: kept.pending, standing: { ...standing, knockedLast: true } };
244
- }
245
- const knock = standing.edge === null;
246
- const seq = standing.sent + 1;
247
- const nextSecret = this.#crypto.random(32);
248
- const next = this.#hex(await this.#crypto.signingPublic(nextSecret));
249
- const signing = this.#bytes(standing.key);
250
- const by = this.#hex(await this.#crypto.signingPublic(signing));
251
- const payload = `{"to":"${invitation.heir}","by":"${by}","next":"${next}","seq":${seq}${ask.method === undefined ? '' : `,"method":${JSON.stringify(ask.method)}`}${ask.args === undefined ? '' : `,"args":${ask.args}`}${ask.call === undefined ? '' : `,"call":${JSON.stringify(ask.call)}`}${ask.within === undefined ? '' : `,"within":${ask.within}`}${ask.after === undefined ? '' : `,"after":"${ask.after}"`}}`;
252
- const payloadBytes = this.#tools.utf8(payload);
253
- const signature = await this.#crypto.sign(signing, payloadBytes);
254
- const lidSecret = this.#crypto.random(32);
255
- const lid = await this.#crypto.agreePublic(lidSecret);
256
- const agreement = await this.#crypto.agree(lidSecret, padlock);
257
- if (agreement === null)
258
- throw new TypeError('the ward’s padlock takes no seal');
259
- const headSeal = await this.#cipher(agreement, 'quo-seal');
260
- const head = await this.#crypto.seal(headSeal.key, headSeal.nonce, this.#bytes(invitation.heir), lid);
261
- let edge;
262
- let ciphertext = new Uint8Array(0);
263
- if (knock) {
264
- const encapsulated = await this.#crypto.encapsulate(this.#bytes(invitation.lock));
265
- if (encapsulated === null)
266
- throw new TypeError('the invitation’s lock is refused');
267
- ciphertext = encapsulated.ciphertext;
268
- edge = await this.#crypto.hkdf(encapsulated.shared, 'quo-lock', 32);
269
- }
270
- else
271
- edge = this.#bytes(standing.edge);
272
- const bodySeal = await this.#cipher(concat(agreement, edge), 'quo-edge-seal');
273
- const body = await this.#crypto.seal(bodySeal.key, bodySeal.nonce, concat(payloadBytes, signature), lid);
274
- const box = concat(lid, head, ciphertext, body);
275
- const pending = { seq, lid: this.#hex(lidSecret), announced: this.#hex(nextSecret), under: this.#hex(edge), knock };
276
- // A knock is the one ask after which the door admits one key: the one it
277
- // announces, under the knock's edge key. The standing asks under both.
278
- const moved = knock
279
- ? { ...standing, key: this.#hex(nextSecret), edge: this.#hex(edge), knock: JSON.stringify({ box: this.#hex(box), pending }), sent: seq, knockedLast: false }
280
- : { ...standing, sent: seq, knockedLast: false };
281
- return { box, pending, standing: moved };
282
- }
283
- /** A reply read, and the standing moved where an object came back above every ask it moved on. */
284
- /** `heard` says whether a box that brought nothing may have reached the door; a reply was heard. */
285
- async read(given, pending, reply, heard = true) {
286
- const standing = pending.knock && (reply !== null || heard) ? { ...given, heard: true } : given;
287
- const { read, agreement } = await this.#reading(standing.invitation.ward, pending.lid, reply);
288
- if ('quo' in read) {
289
- // A word is said only to an admitted key, so the door holds the relation.
290
- return { read, standing: { ...standing, bound: pending.knock ? standing.bound : true } };
291
- }
292
- if (!('object' in read))
293
- return { read, standing };
294
- if (pending.seq <= standing.moved)
295
- return { read, standing: { ...standing, bound: true } };
296
- const edge = this.#hex(await this.#follow(this.#bytes(pending.under), agreement));
297
- return { read, standing: { ...standing, key: pending.announced ?? standing.key, edge, moved: pending.seq, bound: true, knock: null } };
298
- }
299
- // A reply opened under the lid's secret and held to the ward's signature.
300
- async #reading(ward, lidHex, reply) {
301
- if (reply === null)
302
- return { read: { nothing: true }, agreement: null };
303
- const silence = { read: { silence: true }, agreement: null };
304
- if (reply.length > SIZE || reply.length < PK + TAG + SIG)
305
- return silence;
306
- const pk = reply.subarray(0, PK);
307
- const lidSecret = this.#bytes(lidHex);
308
- const agreement = await this.#crypto.agree(lidSecret, pk);
309
- if (agreement === null)
310
- return silence;
311
- const seal = await this.#cipher(agreement, 'quo-seal');
312
- const body = await this.#crypto.open(seal.key, seal.nonce, reply.subarray(PK), pk);
313
- if (body === null || body.length < SIG)
314
- return silence;
315
- const textBytes = body.subarray(0, body.length - SIG);
316
- const lid = await this.#crypto.agreePublic(lidSecret);
317
- const signing = this.#bytes(ward).subarray(0, 32);
318
- if (!(await this.#crypto.verify(signing, concat(lid, textBytes), body.subarray(body.length - SIG))))
319
- return silence;
320
- const text = this.#tools.text(textBytes);
321
- const parsed = text === null ? null : this.#tools.parse(text);
322
- if (parsed === null || parsed.fields === null || parsed.duplicate)
323
- return silence;
324
- const value = parsed.value;
325
- const keys = Object.keys(parsed.fields);
326
- const only = (...names) => keys.length === names.length && names.every((name) => keys.includes(name));
327
- if (only('silence') && value.silence === true)
328
- return silence;
329
- if (only('quo') && typeof value.quo === 'string' && WORDS.has(value.quo))
330
- return { read: { quo: value.quo }, agreement };
331
- if ((only('object', 'seen') || only('object', 'seen', 'at')) && (value.seen === null || typeof value.seen === 'string')) {
332
- // An `at` is read as an invitation's is: no array reads as absent, and an entry that is no string is skipped.
333
- const at = Array.isArray(value.at) ? value.at.filter((entry) => typeof entry === 'string') : undefined;
334
- return { read: { object: value.object, text: parsed.fields.object, seen: value.seen, ...(at === undefined ? {} : { at }) }, agreement };
335
- }
336
- return silence;
337
- }
338
- // ---- a stranger ----
339
- /** An ask on the zero head of a ward, signed by `secret`, which the door keeps nothing of. */
340
- async stranger(ward, secret, ask) {
341
- const padlock = this.#bytes(ward).subarray(32);
342
- const signing = this.#bytes(secret);
343
- const by = this.#hex(await this.#crypto.signingPublic(signing));
344
- const payload = `{"to":null,"by":"${by}","next":null,"seq":1${ask.method === undefined ? '' : `,"method":${JSON.stringify(ask.method)}`}${ask.args === undefined ? '' : `,"args":${ask.args}`}}`;
345
- const payloadBytes = this.#tools.utf8(payload);
346
- const signature = await this.#crypto.sign(signing, payloadBytes);
347
- const lidSecret = this.#crypto.random(32);
348
- const lid = await this.#crypto.agreePublic(lidSecret);
349
- const agreement = await this.#crypto.agree(lidSecret, padlock);
350
- if (agreement === null)
351
- throw new TypeError('the ward’s padlock takes no seal');
352
- const headSeal = await this.#cipher(agreement, 'quo-seal');
353
- const head = await this.#crypto.seal(headSeal.key, headSeal.nonce, new Uint8Array(32), lid);
354
- const bodySeal = await this.#cipher(concat(agreement, new Uint8Array(32)), 'quo-edge-seal');
355
- const body = await this.#crypto.seal(bodySeal.key, bodySeal.nonce, concat(payloadBytes, signature), lid);
356
- return { box: concat(lid, head, body), lid: this.#hex(lidSecret) };
357
- }
358
- /** A stranger's reply read. */
359
- async strangerRead(ward, lid, reply) {
360
- return (await this.#reading(ward, lid, reply)).read;
361
- }
362
- }
@@ -1,45 +0,0 @@
1
- import type { Json } from '../being/being.ts';
2
- type Answered = {
3
- result: Json;
4
- } | {
5
- error: {
6
- message: string;
7
- };
8
- };
9
- /** The faculty's memory, as its lines reach it: every entry's bytes in lowercase hex. */
10
- interface ServedMemory {
11
- read(place: string): Promise<{
12
- entries: Record<string, string>;
13
- version: string | null;
14
- }>;
15
- list(): Promise<string[]>;
16
- /** Each written place's new version, or `null` where the versions moved and nothing landed. */
17
- write(writes: Record<string, Record<string, string | null>>, expect: Record<string, string | null>): Promise<Record<string, string | null> | null>;
18
- }
19
- /** What a served method receives beside its args. */
20
- interface ServeContext {
21
- /** The call id: the same on every attempt of one call, so a repeat acts once. */
22
- readonly id: string;
23
- /**
24
- * A handle the program was handed, called; its answer, or its error. Its
25
- * call id is `id` where given, and otherwise this call's own id and the
26
- * count of handles it has called, so the same on every attempt of this
27
- * call, in any life of the program.
28
- */
29
- call(token: string, args?: Json, id?: string): Promise<Answered>;
30
- /** The faculty's own memory, sealed by the ground; a line the ground refuses throws. */
31
- readonly memory: ServedMemory;
32
- }
33
- type Method = (args: {
34
- readonly [key: string]: Json;
35
- }, context: ServeContext) => Json | Promise<Json>;
36
- /**
37
- * Serves `methods` on the standard streams under `need`'s blueprint, and
38
- * `window`, how long the program remembers a call id. A method that
39
- * throws answers an error, which is final; a program that must be asked
40
- * again exits instead, and the ground starts it again.
41
- */
42
- export declare const serve: (need: unknown, methods: Readonly<Record<string, Method>>, { window }?: {
43
- window?: number;
44
- }) => void;
45
- export {};
@@ -1,71 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // `nervur/serve`: the program's side of the bridge, for a faculty written
3
- // in JavaScript and run beside a ground. It answers `describe` with the
4
- // blueprint, and every other line with the method it names. A method
5
- // receives its args and a context: the call id, a way to call a handle it
6
- // was handed, and the faculty's memory.
7
- import { createInterface } from 'node:readline';
8
- import { blueprintOf } from '../being/need.js';
9
- /**
10
- * Serves `methods` on the standard streams under `need`'s blueprint, and
11
- * `window`, how long the program remembers a call id. A method that
12
- * throws answers an error, which is final; a program that must be asked
13
- * again exits instead, and the ground starts it again.
14
- */
15
- export const serve = (need, methods, { window } = {}) => {
16
- const blueprint = blueprintOf(need);
17
- if (blueprint === undefined)
18
- throw new TypeError('serve takes a need');
19
- const write = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
20
- const waiting = new Map();
21
- let next = 0;
22
- // One line of the program's own, and the ground's answer to it.
23
- const asked = (line) => new Promise((resolve) => {
24
- const sent = `p${++next}`;
25
- waiting.set(sent, resolve);
26
- write({ id: sent, ...line });
27
- });
28
- const remembered = async (op, args) => {
29
- const answer = await asked({ memory: op, args });
30
- if ('error' in answer)
31
- throw new Error(answer.error.message);
32
- return answer.result;
33
- };
34
- const memory = {
35
- read: async (place) => (await remembered('read', { place })),
36
- list: async () => (await remembered('list', {})),
37
- write: async (writes, expect) => (await remembered('write', { writes, expect })),
38
- };
39
- createInterface({ input: process.stdin, crlfDelay: Infinity }).on('line', (line) => {
40
- const message = JSON.parse(line);
41
- // The house's answer to a handle this program called.
42
- if (message.method === undefined) {
43
- const answered = waiting.get(String(message.id));
44
- waiting.delete(String(message.id));
45
- answered?.('error' in message && message.error !== undefined ? { error: message.error } : { result: message.result ?? null });
46
- return;
47
- }
48
- if (message.method === 'describe') {
49
- write({ id: message.id, result: { blueprint: JSON.parse(JSON.stringify(blueprint)), ...(window === undefined ? {} : { window }) } });
50
- return;
51
- }
52
- const method = methods[message.method];
53
- const served = message.call ?? '';
54
- let called = 0;
55
- const context = {
56
- id: served,
57
- call: (token, args = {}, id = served === '' ? '' : `${served}:${++called}`) => id === '' ? Promise.resolve({ error: { message: 'a handle is called with a call id of the program’s own' } }) : asked({ token, args, call: id }),
58
- memory,
59
- };
60
- void (async () => {
61
- if (method === undefined)
62
- return write({ id: message.id, error: { message: `no method ${message.method}` } });
63
- try {
64
- write({ id: message.id, result: await method(message.args ?? {}, context) });
65
- }
66
- catch (error) {
67
- write({ id: message.id, error: { message: error instanceof Error ? error.message : String(error) } });
68
- }
69
- })();
70
- });
71
- };