provared 0.1.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 (49) hide show
  1. package/CHANGELOG.md +1062 -0
  2. package/LICENSE +202 -0
  3. package/LICENSE-CC-BY-4.0.txt +396 -0
  4. package/README.md +689 -0
  5. package/bin/provared-check.mjs +565 -0
  6. package/bin/provared-headers.mjs +110 -0
  7. package/docs/threat-model.md +997 -0
  8. package/net/headers.js +403 -0
  9. package/package.json +57 -0
  10. package/page/check-page.js +162 -0
  11. package/page/index.html +47 -0
  12. package/page/render.js +530 -0
  13. package/page/style.css +150 -0
  14. package/samples/office-supplies.expected.json +82 -0
  15. package/samples/office-supplies.jsonl +10 -0
  16. package/samples/office-supplies.show.json +59 -0
  17. package/spec/provared-format.md +1672 -0
  18. package/src/actions.js +90 -0
  19. package/src/approval.js +73 -0
  20. package/src/blockstamp.js +198 -0
  21. package/src/book.js +2232 -0
  22. package/src/check.d.ts +21 -0
  23. package/src/check.js +15 -0
  24. package/src/compare.js +276 -0
  25. package/src/cover.js +198 -0
  26. package/src/der.js +192 -0
  27. package/src/encoding.js +286 -0
  28. package/src/fields.js +94 -0
  29. package/src/guard.js +171 -0
  30. package/src/headers.js +380 -0
  31. package/src/index.d.ts +2 -0
  32. package/src/index.js +37 -0
  33. package/src/jws.js +235 -0
  34. package/src/keys.js +149 -0
  35. package/src/pass.js +67 -0
  36. package/src/passkey-browser.d.ts +20 -0
  37. package/src/passkey-browser.js +171 -0
  38. package/src/recorder.js +1115 -0
  39. package/src/seal.js +133 -0
  40. package/src/service.js +124 -0
  41. package/src/signatures.js +147 -0
  42. package/src/slip.js +401 -0
  43. package/src/standing.js +181 -0
  44. package/src/stub.js +140 -0
  45. package/src/timestamp.js +351 -0
  46. package/src/tools.js +192 -0
  47. package/src/tree.js +143 -0
  48. package/src/types.d.ts +628 -0
  49. package/src/webauthn.js +119 -0
@@ -0,0 +1,1115 @@
1
+ // The stub writer beside an agent. It keeps the agent's place in its chain,
2
+ // asks the check before acting (guard.js) before each action, and writes a
3
+ // stub for each action that is taken. It holds the book as text; where the
4
+ // text is kept is the caller's business.
5
+ //
6
+ // It makes the two things an agent's software must do into one call, so
7
+ // that they are not forgotten: ask first, and write the receipt after.
8
+ //
9
+ // One writer serves one chain: an agent under its slip, or a helper under
10
+ // its pass. Its calls wait for one another, so two actions asked for at the
11
+ // same moment are taken one after the other, each against the book as the
12
+ // one before left it. Two writers on copies of one book know nothing of
13
+ // each other: keeping them apart is the caller's business.
14
+ //
15
+ // The writer never puts into its book a line that would make the book fail
16
+ // its check. Before an action is taken, its stub is written and checked
17
+ // with the whole book; what the other side hands back is checked before it
18
+ // joins the stub, and left out if it does not check.
19
+ //
20
+ // The book is read once, when the writer is opened, and is then carried on
21
+ // (book.js): each new line is read by itself, against what was worked out
22
+ // from the lines before it. So no signature is checked twice, and what an
23
+ // action costs grows only slowly with the length of the book. Every line
24
+ // is tried on a reader split off the book's own, which takes the book's
25
+ // place only if the line is written, and is thrown away otherwise. So the
26
+ // book's own reader always stands for exactly the text of the book,
27
+ // whatever fails on the way.
28
+ //
29
+ // What is handed to the writer is copied when it is handed over: the
30
+ // request, the approval, what the other side hands back. The copy is what
31
+ // is checked and what is written, so a later change to the caller's own
32
+ // objects changes nothing.
33
+
34
+ import { MAX_LINE_BYTES, answerOf, carryOn, checkCountersignature, entryLine, fixedOptions, forkCarried, latestWhen, promoteCarried, startCarried } from './book.js';
35
+ import { Refusal, canonicalJson, fingerprint, fromBase64url, parseTime, problemFrom, utf8 } from './encoding.js';
36
+ import { checkBeforeRead } from './guard.js';
37
+ import { CLOCK_ALLOWANCE_MS, MAX_STAMPS } from './seal.js';
38
+ import { writeAcknowledgement } from './standing.js';
39
+ import { writeStub } from './stub.js';
40
+
41
+ /** How long the other side is given to hand back its countersignature, unless the caller says otherwise: 30 seconds. */
42
+ export const COUNTERSIGN_WITHIN_MS = 30000;
43
+ // The longest wait a timer can hold.
44
+ const MAX_WAIT_MS = 2 ** 31 - 1;
45
+ // The most cancellations a check may be handed beside a book (book.js).
46
+ const MAX_HANDED = 16;
47
+
48
+ // Where the platform can tell (Node.js), a call to the writer made by an
49
+ // action that the writer is taking is refused at once. It would otherwise
50
+ // wait for the action to end, while the action waits for it.
51
+ const hooks = globalThis.process && typeof globalThis.process.getBuiltinModule === 'function' ? globalThis.process.getBuiltinModule('node:async_hooks') : null;
52
+
53
+ function within(promise, ms) {
54
+ return new Promise((resolve, reject) => {
55
+ const timer = setTimeout(() => reject(new Error('no answer in time')), ms);
56
+ promise.then(
57
+ (value) => {
58
+ clearTimeout(timer);
59
+ resolve(value);
60
+ },
61
+ (e) => {
62
+ clearTimeout(timer);
63
+ reject(e);
64
+ },
65
+ );
66
+ });
67
+ }
68
+
69
+ const notWritten = (why) => new Refusal('record-not-sound', why);
70
+
71
+ // A copy that nothing else holds.
72
+ function plain(value, what) {
73
+ try {
74
+ return structuredClone(value);
75
+ } catch {
76
+ throw new Refusal('bad-field', `${what} must be plain data: text, numbers, lists and objects.`);
77
+ }
78
+ }
79
+
80
+ // The deepest a copied value may nest: far more than a line of a book can hold.
81
+ const MAX_COPY_DEPTH = 64;
82
+
83
+ // A copy that nothing else holds, made in one pass: each member and each
84
+ // place is read once, and copied as it is read. So a member whose value
85
+ // changes from one reading to the next is read only once. An object of any
86
+ // kind, a Proxy among them, is copied as its own listed members; a list as
87
+ // its length and the places that hold an item, so that a gap stays a gap
88
+ // (and a check refuses it), and a faulty item beside a sound one leaves the
89
+ // sound one as it is. A value met twice is copied once. Throws where it
90
+ // cannot be done: a function, a list whose length is not a whole number, a
91
+ // value that holds itself (which nests without end), or a value that,
92
+ // written out, would be longer than a line of a book may be.
93
+ //
94
+ // The copies one copier makes share one count, so that several values,
95
+ // each short enough for a line, cannot together cost more than a line.
96
+ function copier() {
97
+ const copies = new Map();
98
+ let total = 0;
99
+ // The copy, and the fewest characters it takes written out: a text its
100
+ // characters and two quotation marks; any other single value one; a list
101
+ // or an object its brackets and commas, and each member's name with its
102
+ // quotation marks and colon. A value met again is counted again, as it is
103
+ // written out again.
104
+ const walk = (v, depth) => {
105
+ if (typeof v === 'function' || typeof v === 'symbol' || typeof v === 'bigint') throw new TypeError('not plain data');
106
+ if (typeof v === 'string') return [v, v.length + 2];
107
+ if (v === null || typeof v !== 'object') return [v, 1];
108
+ if (copies.has(v)) return copies.get(v);
109
+ if (depth > MAX_COPY_DEPTH) throw new TypeError('the value nests too deeply, or holds itself');
110
+ const list = Array.isArray(v);
111
+ let made = {};
112
+ if (list) {
113
+ const length = v.length;
114
+ if (!(Number.isInteger(length) && length >= 0 && length <= MAX_LIST_LENGTH)) throw new TypeError('not plain data');
115
+ made = new Array(length);
116
+ }
117
+ let size = 2;
118
+ let members = 0;
119
+ for (const name of Object.keys(v)) {
120
+ const [item, count] = walk(v[name], depth + 1);
121
+ size += count + (members++ > 0 ? 1 : 0) + (list ? 0 : name.length + 3);
122
+ if (size > MAX_LINE_BYTES) throw new TypeError('more than a line can hold');
123
+ Object.defineProperty(made, name, { value: item, enumerable: true, writable: true, configurable: true });
124
+ }
125
+ copies.set(v, [made, size]);
126
+ return [made, size];
127
+ };
128
+ return (value) => {
129
+ const [made, size] = walk(value, 0);
130
+ total += size;
131
+ if (total > MAX_LINE_BYTES) throw new TypeError('more than a line can hold');
132
+ return made;
133
+ };
134
+ }
135
+
136
+ const copyOf = (value) => copier()(value);
137
+
138
+ // The longest a list can be.
139
+ const MAX_LIST_LENGTH = 2 ** 32 - 1;
140
+
141
+ // A copy of a cancellation given when the writer is opened, as plain data:
142
+ // its three members as a check reads them (by name, whether or not they are
143
+ // enumerable), and any other member as a check lists it. So what
144
+ // "cancellations" hands back is what was checked, and the writer never
145
+ // changes the caller's own object. An object of a class is copied in the
146
+ // same way: a check reads only its own members. One that cannot be copied
147
+ // so is refused when the writer is opened. Each of the three may be as long
148
+ // as a line; the other members share one line's length.
149
+ function plainHeld(copy) {
150
+ if (copy === null || typeof copy !== 'object' || Array.isArray(copy)) return copy;
151
+ try {
152
+ const made = {};
153
+ const rest = copier();
154
+ for (const name of Object.getOwnPropertyNames(copy)) {
155
+ const known = name === 'cancellation' || name === 'stamps' || name === 'acknowledgements';
156
+ if (!known && Object.getOwnPropertyDescriptor(copy, name)?.enumerable !== true) continue;
157
+ Object.defineProperty(made, name, { value: known ? copyOf(copy[name]) : rest(copy[name]), enumerable: true, writable: true, configurable: true });
158
+ }
159
+ return made;
160
+ } catch {
161
+ throw new Refusal('bad-field', 'options.cancellations: a copy of a cancellation could not be read as plain data.');
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Open the stub writer for one agent under one slip.
167
+ *
168
+ * @param {object} o
169
+ * @param {string} o.book the book so far: at least the slip
170
+ * @param {string} o.slip the fingerprint of the slip
171
+ * @param {CryptoKey[]} o.privateKeys the agent's private keys: Ed25519, then ML-DSA-87
172
+ * @param {string[]} o.issuerKeys the thumbprints of the passkeys this agent's software trusts; a slip from any other key allows nothing
173
+ * @param {string} [o.pass] for a helper agent: the fingerprint of the pass it acts under
174
+ * @param {object} [o.options] further options for the checker (sealKeys, stampServices, blocks, vouchers, cancellations).
175
+ * They are read once, as a whole check reads them, and fixed
176
+ * @param {() => number} [o.now] the clock, in milliseconds; defaults to the device's. "act" takes an action at the clock's time, and nothing is written that is dated more than 300 seconds ahead of it
177
+ * @param {number} [o.countersignWithin] how long the other side is given to countersign, in milliseconds; defaults to 30,000
178
+ * @returns {Promise<object>} the writer: before, record, act, book, check, cancellations, add
179
+ * @throws {Refusal} "record-not-sound" if the book so far has a problem
180
+ */
181
+ export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass, options = {}, now = Date.now, countersignWithin = COUNTERSIGN_WITHIN_MS }) {
182
+ if (typeof now !== 'function') throw new Refusal('bad-field', 'now: must be a function that gives the time in milliseconds.');
183
+ // A clock that gives anything but a time is a clock that failed: no comparison with it would hold.
184
+ const device = now;
185
+ now = () => {
186
+ const time = device();
187
+ if (!Number.isFinite(time)) throw new Refusal('bad-field', 'now: the clock did not give a time in milliseconds.');
188
+ return time;
189
+ };
190
+ if (!(Number.isFinite(countersignWithin) && countersignWithin >= 1 && countersignWithin <= MAX_WAIT_MS)) {
191
+ throw new Refusal('bad-field', 'countersignWithin: must be a number of milliseconds from 1 to 2,147,483,647.');
192
+ }
193
+ // Whom the writer trusts is fixed when it is opened: read once, as a whole
194
+ // check reads it, and copied. The passkeys it trusts are its own
195
+ // "issuerKeys", whatever the options say, read through the same fixed
196
+ // copy. An option that cannot be read is refused here.
197
+ const fixed = fixedOptions(options);
198
+ const own = fixedOptions({ issuerKeys });
199
+ let base;
200
+ try {
201
+ base = {};
202
+ for (const name of Object.keys(fixed)) if (name !== 'issuerKeys') base[name] = fixed[name];
203
+ Object.assign(base, own);
204
+ // A value that cannot even be looked into (a revoked Proxy) is refused here.
205
+ for (const value of Object.values(base)) Array.isArray(value);
206
+ // A list longer than a check accepts is refused by the check; it is not walked through here.
207
+ if (Array.isArray(base.cancellations) && base.cancellations.length <= MAX_HANDED) base.cancellations = base.cancellations.map(plainHeld);
208
+ } catch (e) {
209
+ if (e instanceof Refusal) throw e;
210
+ throw new Refusal('bad-field', 'options: a member could not be read.');
211
+ }
212
+ // The person's cancellations that the writer holds beside its book: those
213
+ // given when it was opened, and those of its own slip that were handed
214
+ // to it since and could not yet be written into the book ("kept"). A
215
+ // cancellation of another slip that cannot be added is nothing to this
216
+ // writer and is not kept, so no number of them can crowd out one that
217
+ // matters. Each is written into the book as soon as it can be
218
+ // (writeWaiting). Until then it lives only in this writer:
219
+ // "cancellations" hands it back, so that the caller can keep it with the
220
+ // book. A check may be handed 16 at most: see "beside".
221
+ const given = Array.isArray(base.cancellations) ? base.cancellations : [];
222
+ /** @type {{entry: any, when: number, fingerprint: string, tried: boolean}[]} */
223
+ const kept = [];
224
+ // Those given at opening that the book does not hold: written into it as the kept ones are.
225
+ /** @type {{entry: any, slip: string, when: number, fingerprint: string, tried: boolean}[]} */
226
+ const unwritten = [];
227
+ // The copies given at opening that already hold an acknowledgement of
228
+ // this writer's own agent: the writer puts no second one with them.
229
+ const ownAcked = new Set();
230
+ // Whether the person's cancellation of this writer's slip was handed to
231
+ // it. From then on it takes no action, whatever becomes of the
232
+ // cancellation itself.
233
+ let stopped = false;
234
+ // The acknowledgements that this writer's own agent has signed, by the
235
+ // fingerprint of the cancellation each is for: those the book holds
236
+ // ("written"), and those signed since the writer was opened. One
237
+ // cancellation is acknowledged once.
238
+ const acknowledged = new Map();
239
+ const written = new Set();
240
+ // Those that could not yet follow their cancellation into the book, by
241
+ // the fingerprint of the cancellation, with the cancellation itself.
242
+ const waiting = new Map();
243
+ // An acknowledgement that waits to follow its cancellation into the book
244
+ // is handed back too, with the cancellation, unless a copy given at
245
+ // opening already carries it. So a writer opened again from what this
246
+ // one hands back writes the same acknowledgement, not a second one.
247
+ // What the writer holds beside its book, the most needed first: the
248
+ // person's cancellations of its own slip that the book does not hold
249
+ // (those kept, then those given at opening), which stop the writer; then
250
+ // the acknowledgements that wait, in the order in which they began to
251
+ // wait (each carried by a copy given at opening, or handed back by
252
+ // itself); then every other copy given at opening. A check may be handed
253
+ // sixteen at most: where there are more, the last give way, so that
254
+ // copies of other slips, or of cancellations the book holds, can never
255
+ // crowd out one that matters.
256
+ const ownUnwritten = () => unwritten.filter((u) => u.slip === slip).map((u) => u.entry);
257
+ const waitingPart = (own = ownUnwritten()) => {
258
+ const part = [];
259
+ for (const [cancellation, record] of waiting) {
260
+ const carriers = given.filter((g) => g && g.cancellation && g.cancellation.payload === record.payload);
261
+ if (carriers.length === 0) part.push({ cancellation: record, acknowledgements: [acknowledged.get(cancellation)] });
262
+ for (const g of carriers) if (!own.includes(g) && !part.includes(g)) part.push(g);
263
+ }
264
+ return part;
265
+ };
266
+ const room = () => {
267
+ const own = ownUnwritten();
268
+ return MAX_HANDED - kept.length - own.length - waitingPart(own).length;
269
+ };
270
+ const beside = () => {
271
+ if (!kept.length && !waiting.size) return given;
272
+ const own = ownUnwritten();
273
+ const waits = waitingPart(own);
274
+ const rest = given.filter((g) => !own.includes(g) && !waits.includes(g));
275
+ return [...kept.map((k) => k.entry), ...own, ...waits, ...rest].slice(0, MAX_HANDED);
276
+ };
277
+ const checking = () => (kept.length || waiting.size ? { ...base, cancellations: beside() } : base);
278
+ const STOPPED = { code: 'after-cancellation', message: 'The person cancelled the slip: the cancellation was handed to this stub writer.' };
279
+ const whileStopped = (answer) => {
280
+ if (stopped && answer.allowed) {
281
+ answer.allowed = false;
282
+ answer.breaches.push({ ...STOPPED });
283
+ }
284
+ return answer;
285
+ };
286
+ const without = Array.isArray(base.withoutMethods) ? base.withoutMethods : [];
287
+ let text = typeof book === 'string' && book !== '' && !book.endsWith('\n') ? book + '\n' : book;
288
+
289
+ // Where the agent's own chain stands: the last stub under this slip, and
290
+ // under this pass if it acts under one.
291
+ const lastStub = (entries) => {
292
+ let last = null;
293
+ for (const e of entries) {
294
+ if (e.kind === 'stub' && e.slip === slip && (e.pass ?? null) === (pass ?? null)) last = { seq: e.content.seq, fingerprint: e.fingerprint };
295
+ }
296
+ return last;
297
+ };
298
+ // The book is read once, here. "carried" always stands for exactly the
299
+ // text of the book: every line that joins the text is read into it.
300
+ let carried = startCarried(base);
301
+ await carryOn(carried, text);
302
+ // The answer for the book as it stands, or for a reader split off it, with the cancellations held beside the book.
303
+ const standing = (of = carried) => answerOf(of, checking());
304
+ const { result } = await standing();
305
+ if (result.summary.problemFound) throw notWritten('A problem was found in the record so far. It must be looked at before the agent acts again.');
306
+ let after = lastStub(result.entries);
307
+ // What was given at opening: the first sound acknowledgement of this
308
+ // writer's own agent that came with a cancellation is the one it goes on
309
+ // handing back; a cancellation that the book does not hold waits to be
310
+ // written.
311
+ result.held.forEach((h, i) => {
312
+ if (!h.used) return;
313
+ // The person's cancellation of this writer's own slip, given when it was opened, stops it from the start.
314
+ if (h.slip === slip) stopped = true;
315
+ h.acknowledgements.forEach((a, j) => {
316
+ if (a.state !== 'valid' || h.slip !== slip || a.pass !== (pass ?? null)) return;
317
+ ownAcked.add(given[i]);
318
+ if (!acknowledged.has(h.fingerprint)) acknowledged.set(h.fingerprint, given[i].acknowledgements[j]);
319
+ });
320
+ if (h.inBook === null) unwritten.push({ entry: given[i], slip: h.slip, when: parseTime(h.content.when), fingerprint: h.fingerprint, tried: false });
321
+ });
322
+ {
323
+ // The first acknowledgement of this writer's own agent that the book
324
+ // holds for a cancellation is the one it goes on handing back.
325
+ const lines = typeof text === 'string' ? text.split('\n') : [];
326
+ for (const e of result.entries) {
327
+ if (e.kind !== 'acknowledgement' || e.problems.length > 0 || e.slip !== slip || (e.pass ?? null) !== (pass ?? null)) continue;
328
+ if (written.has(e.content.cancellation)) continue;
329
+ acknowledged.set(e.content.cancellation, JSON.parse(lines[e.index]).acknowledgement);
330
+ written.add(e.content.cancellation);
331
+ }
332
+ }
333
+ // An acknowledgement given at opening, for a cancellation that the book
334
+ // holds without it, follows the cancellation into the book: it was still
335
+ // waiting when the writer that signed it handed it back.
336
+ result.held.forEach((h, i) => {
337
+ if (h.used && h.slip === slip && h.inBook !== null && acknowledged.has(h.fingerprint) && !written.has(h.fingerprint)) waiting.set(h.fingerprint, given[i].cancellation);
338
+ });
339
+ // The services the slip names, to check a countersignature before it goes into the book.
340
+ const slipEntry = result.entries.find((e) => e.kind === 'slip' && e.fingerprint === slip);
341
+ const services = slipEntry && slipEntry.content ? slipEntry.content.with : [];
342
+
343
+ // One call at a time. The answer to "would this be outside the slip?"
344
+ // holds only until the next stub is written.
345
+ const inside = hooks ? new hooks.AsyncLocalStorage() : null;
346
+ let turn = Promise.resolve();
347
+ const inTurn = (work) => {
348
+ const mark = inside ? inside.getStore() : undefined;
349
+ if (mark && mark.writer === inTurn && mark.running) {
350
+ return Promise.reject(new Error('The stub writer was called from inside an action it is taking. Call it once the action has ended.'));
351
+ }
352
+ const run = turn.then(work);
353
+ turn = run.catch(() => {});
354
+ return run;
355
+ };
356
+ // What the caller hands in to be run (the action, the request for a
357
+ // countersignature) is run under a mark, so that the call above is
358
+ // caught. The mark holds only while the writer is waiting for that work:
359
+ // until it ends or, for a countersignature, until its time runs out.
360
+ const runInside = async (work) => {
361
+ if (!inside) return work();
362
+ const mark = { writer: inTurn, running: true };
363
+ try {
364
+ return await inside.run(mark, work);
365
+ } finally {
366
+ mark.running = false;
367
+ }
368
+ };
369
+
370
+ // What a call hands in, copied at the moment of the call.
371
+ const asked = (request, more) => {
372
+ if (request === null || typeof request !== 'object') throw new Refusal('bad-field', 'The request must be an object.');
373
+ if (more === null || typeof more !== 'object') more = {};
374
+ const call = {
375
+ request: plain(request, 'The request'),
376
+ when: more.when === undefined || more.when === null ? undefined : new Date(more.when).getTime(),
377
+ terms: more.terms,
378
+ countersign: more.countersign,
379
+ hasApproval: more.approval !== undefined && more.approval !== null,
380
+ approval: undefined,
381
+ };
382
+ if (call.hasApproval) {
383
+ try {
384
+ call.approval = structuredClone(more.approval.record);
385
+ } catch {
386
+ call.approval = undefined; // not a record: refused when the stub is written
387
+ }
388
+ }
389
+ return call;
390
+ };
391
+ const calling = (request, more, work) => {
392
+ let call;
393
+ try {
394
+ call = asked(request, more);
395
+ } catch (e) {
396
+ return Promise.reject(e);
397
+ }
398
+ return inTurn(() => work(call));
399
+ };
400
+
401
+ const proposalFrom = (call, when) => {
402
+ const { request } = call;
403
+ const proposal = { slip, action: request.action };
404
+ for (const name of ['amount', 'with', 'details']) {
405
+ // An empty list of documents is the same as none, as the stub itself is written.
406
+ if (name === 'details' && Array.isArray(request.details) && request.details.length === 0) continue;
407
+ if (request[name] !== undefined) proposal[name] = request[name];
408
+ }
409
+ if (pass) proposal.pass = pass;
410
+ proposal.when = when;
411
+ if (call.terms !== undefined) proposal.terms = call.terms;
412
+ if (call.hasApproval) proposal.approval = call.approval;
413
+ return proposal;
414
+ };
415
+ const aheadOfClock = (when) => when > now() + CLOCK_ALLOWANCE_MS;
416
+ const behindClock = (when) => when < now() - CLOCK_ALLOWANCE_MS;
417
+
418
+ // The stub for an action, checked with the whole book before anything is
419
+ // done with it. Nothing on its line may be dated ahead of the clock: the
420
+ // stub's own date is looked at before this, and the approval's here. It
421
+ // is read by a reader split off the book's own, which is handed back: if
422
+ // the stub is not written after all, that reader is simply not used.
423
+ const fitsBook = async (made, otherwise) => {
424
+ const tried = forkCarried(carried);
425
+ await carryOn(tried, made.line + '\n');
426
+ const checked = (await standing(tried)).result;
427
+ if (checked.summary.problemFound) throw notWritten(otherwise);
428
+ if (latestWhen(checked.entries.at(-1)) > now() + CLOCK_ALLOWANCE_MS) throw notWritten('The approval that goes with this action is dated ahead of the clock, so nothing was written.');
429
+ return tried;
430
+ };
431
+
432
+ // The fingerprint of an approval is worked out here, from the record
433
+ // itself. One handed over with it is not relied on.
434
+ const approvalFingerprint = async (record) => {
435
+ try {
436
+ return await fingerprint(fromBase64url(record.payload));
437
+ } catch {
438
+ throw new Refusal('approval-invalid', 'The approval handed over is not a signed record.');
439
+ }
440
+ };
441
+
442
+ // The stub for an action, with the approval that goes with it, as a line.
443
+ const composeStub = async (call, when) => {
444
+ const { request } = call;
445
+ const stub = await writeStub(
446
+ {
447
+ slip,
448
+ after,
449
+ action: request.action,
450
+ amount: request.amount,
451
+ with: request.with,
452
+ details: request.details,
453
+ approval: call.hasApproval ? await approvalFingerprint(call.approval) : undefined,
454
+ terms: call.terms,
455
+ pass,
456
+ when,
457
+ },
458
+ privateKeys,
459
+ );
460
+ const entry = { stub: stub.record };
461
+ if (call.hasApproval) entry.approval = call.approval;
462
+ const line = entryLine(entry);
463
+ if (utf8(line).length > MAX_LINE_BYTES) throw new Refusal('too-large', 'The record of this action is longer than a line of a book may be.');
464
+ return { stub, entry, line, countersignature: { accepted: false, problem: null } };
465
+ };
466
+
467
+ // Ask the other side for its countersignature, and put it with the stub
468
+ // if it checks: the right keys, the right stub, not dated ahead of the
469
+ // clock, and a line that still fits. One that does not check is left
470
+ // out, and the stub stands one-sided, which is what happened.
471
+ const countersigned = async (made, call) => {
472
+ if (typeof call.countersign !== 'function') return made;
473
+ const countersignature = { accepted: false, problem: null };
474
+ let line = made.line;
475
+ try {
476
+ // The other side is handed a copy: nothing it does to it changes the stub.
477
+ const given = await runInside(() => within(Promise.resolve(call.countersign(structuredClone(made.stub.record))), countersignWithin));
478
+ if (given) {
479
+ // What it hands back is copied at once. The copy is what is checked, and what joins the stub.
480
+ let kept;
481
+ try {
482
+ kept = structuredClone(given);
483
+ } catch {
484
+ throw new Refusal('countersignature-invalid', 'What the other side handed back is not a signed record.');
485
+ }
486
+ const service = typeof call.request.with === 'string' ? services.find((s) => s.id === call.request.with) : undefined;
487
+ const checked = await checkCountersignature(kept, made.stub.fingerprint, service, without);
488
+ if (checked.state !== 'valid') throw new Refusal('countersignature-invalid', 'This device could not check the countersignature.');
489
+ if (parseTime(checked.when) > now() + CLOCK_ALLOWANCE_MS) {
490
+ throw new Refusal('countersignature-invalid', 'The countersignature is dated ahead of the clock.');
491
+ }
492
+ const withIt = entryLine({ ...made.entry, countersignature: kept });
493
+ if (utf8(withIt).length > MAX_LINE_BYTES) throw new Refusal('too-large', 'With the countersignature, the line would be longer than a line of a book may be.');
494
+ line = withIt;
495
+ countersignature.accepted = true;
496
+ }
497
+ } catch (e) {
498
+ countersignature.problem = e instanceof Refusal ? problemFrom(e) : { code: 'countersignature-missing', message: 'Asking the other side for its countersignature failed, or took too long.' };
499
+ }
500
+ return { ...made, line, countersignature };
501
+ };
502
+
503
+ // The line joins the book. The reader that takes the book's place is
504
+ // the one the stub was tried on, if the line is still the one tried.
505
+ // With a countersignature the line is another: it is read from where
506
+ // the book stood, and the reader that tried the stub alone is not used.
507
+ const commit = async ({ stub, line, countersignature }, made, tried) => {
508
+ let next = tried;
509
+ if (line !== made.line) {
510
+ next = forkCarried(carried);
511
+ await carryOn(next, line + '\n');
512
+ }
513
+ carried = promoteCarried(next);
514
+ text += line + '\n';
515
+ after = { seq: stub.seq, fingerprint: stub.fingerprint };
516
+ return { ...stub, line, countersignature };
517
+ };
518
+
519
+ // A line that someone else made is tried on a reader split off the
520
+ // book's own, which may read a line of any kind. If the book passes its
521
+ // check with the line, and the line is not dated ahead of the clock, that
522
+ // reader takes the book's place and the line joins the text. Otherwise it
523
+ // is thrown away, and the book's own reader is as it was. "at" is the
524
+ // clock, read once by the caller.
525
+ const takeLine = async (line, at, options = checking()) => {
526
+ const tried = forkCarried(carried, true);
527
+ await carryOn(tried, line + '\n');
528
+ const checked = (await answerOf(tried, options)).result;
529
+ if (checked.summary.problemFound) return { fits: false, ahead: false, why: 'with it, the book would not pass its check.' };
530
+ if (latestWhen(checked.entries.at(-1)) > at + CLOCK_ALLOWANCE_MS) return { fits: false, ahead: true, why: 'it is dated ahead of the clock.' };
531
+ carried = promoteCarried(tried);
532
+ text += line + '\n';
533
+ return { fits: true, checked };
534
+ };
535
+
536
+ // A cancellation held beside the book is written into the book as soon as
537
+ // it can be: once the date it gives is no longer ahead of the clock. It
538
+ // goes in as the entry it is, without the acknowledgements that came with
539
+ // it. If the book would not pass its check with it even then, it is not
540
+ // tried again, and the writer goes on holding it.
541
+ const writeWaiting = async (at) => {
542
+ for (const list of [kept, unwritten]) {
543
+ for (const item of [...list]) {
544
+ if (item.tried || item.when > at + CLOCK_ALLOWANCE_MS) continue;
545
+ const entry = { cancellation: item.entry.cancellation };
546
+ if (Object.hasOwn(item.entry, 'stamps')) entry.stamps = item.entry.stamps;
547
+ const taken = await takeLine(entryLine(entry), at);
548
+ if (!taken.fits) {
549
+ item.tried = true;
550
+ continue;
551
+ }
552
+ // In the book, it is no longer one that waits.
553
+ list.splice(list.indexOf(item), 1);
554
+ // The acknowledgement that was signed when it was handed over follows it into the book.
555
+ if (acknowledged.has(item.fingerprint)) await acknowledge(item.fingerprint, item.entry.cancellation, true, at);
556
+ }
557
+ }
558
+ // An acknowledgement that could not follow its cancellation into the
559
+ // book, because it was dated ahead of the clock, is tried again.
560
+ for (const [cancellation] of [...waiting]) {
561
+ const taken = await takeLine(entryLine({ acknowledgement: acknowledged.get(cancellation) }), at);
562
+ if (taken.fits) written.add(cancellation);
563
+ if (taken.fits || !taken.ahead) waiting.delete(cancellation);
564
+ }
565
+ };
566
+ // The acknowledgement goes with the cancellation that the writer holds
567
+ // beside its book, so that "cancellations" hands the two back together.
568
+ // A copy holds four at most. The writer's own is always kept: where the
569
+ // copy already holds four, the last of the others gives way to it.
570
+ // Another agent's is put with the copy only where there is room.
571
+ const putWith = (entry, record, own = true) => {
572
+ if (!record) return;
573
+ let list = Array.isArray(entry.acknowledgements) ? entry.acknowledgements : [];
574
+ if (list.some((a) => a && a.payload === record.payload)) return;
575
+ // A copy given at opening that already holds one of the agent's own takes no second one.
576
+ if (own && ownAcked.has(entry)) return;
577
+ if (list.length >= 4) {
578
+ if (!own) return;
579
+ list = list.slice(0, 3);
580
+ }
581
+ entry.acknowledgements = [...list, structuredClone(record)];
582
+ };
583
+
584
+ // The agent's side says that it was handed the person's cancellation of
585
+ // its slip, and when: signed with this writer's own keys, once for each
586
+ // cancellation, and dated by the writer's clock. Where the book holds
587
+ // the cancellation, the acknowledgement follows it into the book. It is
588
+ // handed back to the caller, to be given to the person. If it cannot be
589
+ // signed, or does not check (the writer was opened with keys that are
590
+ // not the agent's), there is none; the cancellation stops the writer all
591
+ // the same.
592
+ //
593
+ // It is not dated before the writer's own last stub, nor before a pass
594
+ // that its agent handed on. Either may be dated up to 300 seconds ahead
595
+ // of the clock, and the agent was not told before it made them: an
596
+ // acknowledgement dated by the clock alone would have them read as made
597
+ // after the agent was told.
598
+ const acknowledge = async (cancellation, cancellationRecord, inBook, at) => {
599
+ let record = acknowledged.get(cancellation);
600
+ if (!record) {
601
+ let when = at;
602
+ for (const e of (await standing()).result.entries) {
603
+ if (e.slip !== slip || !e.content) continue;
604
+ const own =
605
+ (e.kind === 'stub' && (e.pass ?? null) === (pass ?? null)) ||
606
+ (e.kind === 'pass' && (Object.hasOwn(e.content, 'from') ? e.content.from : null) === (pass ?? null));
607
+ if (own && parseTime(e.content.when) > when) when = parseTime(e.content.when);
608
+ }
609
+ let made;
610
+ try {
611
+ made = (await writeAcknowledgement({ slip, cancellation, pass, when }, privateKeys)).record;
612
+ } catch {
613
+ return undefined;
614
+ }
615
+ // Checked as a reader will check it, beside the person's own copy of the cancellation.
616
+ const [beside] = (await answerOf(carried, { ...base, cancellations: [{ cancellation: cancellationRecord, acknowledgements: [made] }] })).result.held;
617
+ if (!beside || beside.acknowledgements.length !== 1 || beside.acknowledgements[0].state !== 'valid') return undefined;
618
+ record = made;
619
+ acknowledged.set(cancellation, record);
620
+ }
621
+ if (inBook && !written.has(cancellation)) {
622
+ const taken = await takeLine(entryLine({ acknowledgement: record }), at);
623
+ if (taken.fits) written.add(cancellation);
624
+ else if (taken.ahead) waiting.set(cancellation, cancellationRecord);
625
+ }
626
+ // It goes with the cancellation wherever the writer holds that beside its book.
627
+ const held = kept.find((k) => k.fingerprint === cancellation);
628
+ if (held) putWith(held.entry, record);
629
+ const opened = given.find((h) => h && h.cancellation && h.cancellation.payload === cancellationRecord.payload);
630
+ if (opened) putWith(opened, record);
631
+ return structuredClone(record);
632
+ };
633
+ const refused = (message, acknowledgement) => {
634
+ const e = notWritten(message);
635
+ if (acknowledgement) e.acknowledgement = acknowledgement;
636
+ return e;
637
+ };
638
+
639
+ // The acknowledgements handed over with the person's cancellation of this
640
+ // writer's slip. Each is checked by itself, so that one which does not
641
+ // check is never taken for one which does. The first sound one of this
642
+ // writer's own agent is the one the writer goes on handing back, unless
643
+ // it already has one; any further one of its own agent is let go, so that
644
+ // a copy never holds two. The sound ones of other agents are handed back,
645
+ // to stay with the copy. At most sixteen are looked at.
646
+ const sortHanded = async (cancellation, cancellationRecord, handed) => {
647
+ const others = [];
648
+ if (!Array.isArray(handed)) return others;
649
+ for (const record of handed.slice(0, MAX_HANDED)) {
650
+ const [beside] = (await answerOf(carried, { ...base, cancellations: [{ cancellation: cancellationRecord, acknowledgements: [record] }] })).result.held;
651
+ if (!beside || !beside.used || beside.acknowledgements.length !== 1 || beside.acknowledgements[0].state !== 'valid') continue;
652
+ if (beside.acknowledgements[0].pass !== (pass ?? null)) others.push(record);
653
+ else if (!acknowledged.has(cancellation)) acknowledged.set(cancellation, record);
654
+ }
655
+ return others;
656
+ };
657
+ // The writer's own acknowledgement, and the sound ones of other agents, go with a copy the writer holds.
658
+ const putHanded = (entry, cancellation, others) => {
659
+ putWith(entry, acknowledged.get(cancellation));
660
+ for (const other of others) putWith(entry, other, false);
661
+ };
662
+
663
+ // The time-stamps that a cancellation should go with: those a copy of it
664
+ // already holds, and those handed over now, none twice. Each is judged by
665
+ // itself, beside the book: one with which the cancellation does not pass
666
+ // its check is left out. Those that this writer counts come first, then
667
+ // the others, four at most, so that a time-stamp the writer counts is
668
+ // never crowded out by one it does not. Only a set with which the book
669
+ // still passes its check is taken. Null where nothing changes.
670
+ const mergedStamps = async (cancellationRecord, holds, offered) => {
671
+ const keyOf = (s) => {
672
+ try {
673
+ return canonicalJson(s);
674
+ } catch {
675
+ return null;
676
+ }
677
+ };
678
+ const held = new Set(holds.map(keyOf));
679
+ const all = [...holds];
680
+ for (const s of offered) {
681
+ const key = keyOf(s);
682
+ if (key === null || held.has(key)) continue;
683
+ held.add(key);
684
+ all.push(s);
685
+ }
686
+ if (all.length === holds.length) return null;
687
+ const counted = [];
688
+ const uncounted = [];
689
+ for (const s of all) {
690
+ const [one] = (await answerOf(carried, { ...base, cancellations: [{ cancellation: cancellationRecord, stamps: [s] }] })).result.held;
691
+ if (!one || !one.used || one.problems.length > 0) continue;
692
+ if (one.stampedAt) counted.push({ s, at: parseTime(one.stampedAt) });
693
+ else uncounted.push(s);
694
+ }
695
+ // Of those it counts, the earliest come first: they place the cancellation earliest in time.
696
+ counted.sort((a, b) => a.at - b.at);
697
+ const stamps = [...counted.map((c) => c.s), ...uncounted].slice(0, MAX_STAMPS);
698
+ if (stamps.length === 0 || keyOf(stamps) === keyOf(holds)) return null;
699
+ let form;
700
+ try {
701
+ form = entryLine({ cancellation: cancellationRecord, stamps });
702
+ } catch {
703
+ return null;
704
+ }
705
+ const tried = forkCarried(carried, true);
706
+ await carryOn(tried, form + '\n');
707
+ if ((await answerOf(tried, checking())).result.summary.problemFound) return null;
708
+ return stamps;
709
+ };
710
+
711
+ const writer = {
712
+ /**
713
+ * Ask whether an action would be outside the slip. Nothing is written.
714
+ * The answer holds only until the next stub is written: use "act" to
715
+ * ask and act in one step.
716
+ * @param {{action: string, amount?: object, with?: string, details?: object[]}} request
717
+ * @param {{when?: number|Date, terms?: string, approval?: {record: object}}} [more]
718
+ * @returns {Promise<{allowed: boolean, problems: object[], breaches: object[], needs: string[]}>}
719
+ */
720
+ before(request, more = {}) {
721
+ return calling(request, more, async (call) => whileStopped(await checkBeforeRead(() => standing(), proposalFrom(call, call.when ?? now()), checking())));
722
+ },
723
+
724
+ /**
725
+ * Write the stub for an action that was taken, and add it to the book.
726
+ * It does not ask first: use "act" for that. An action that was taken
727
+ * is recorded whether or not it was inside the slip, because the record
728
+ * is of what happened. But a stub is written only if the book still
729
+ * passes its check with it: one dated ahead of the clock, or before
730
+ * the stub ahead of it, or with an approval that does not fit, is
731
+ * refused, and nothing is written. The other side is asked to
732
+ * countersign only once the stub has passed that check, so a stub
733
+ * that is refused has gone nowhere.
734
+ *
735
+ * @param {{action: string, amount?: object, with?: string, details?: object[]}} request
736
+ * @param {object} [more]
737
+ * @param {number|Date} [more.when] when the action was taken; defaults to the clock
738
+ * @param {string} [more.terms] the fingerprint of the service's terms relied on
739
+ * @param {{record: object}} [more.approval] the person's signed approval of this action
740
+ * @param {(stub: object) => Promise<object|null>} [more.countersign] asks the other side for its countersignature; null if it gives none
741
+ * @returns {Promise<{record: object, fingerprint: string, seq: number, line: string, countersignature: {accepted: boolean, problem: object|null}}>}
742
+ * "countersignature.accepted" says whether a countersignature was given, checked and put with the stub.
743
+ * Where one was given and did not check, or asking for it failed, "problem" says why, and the stub is one-sided.
744
+ * @throws {Refusal} "record-not-sound" if the book would not pass its check with the stub; nothing is written
745
+ */
746
+ record(request, more = {}) {
747
+ return calling(request, more, async (call) => {
748
+ await writeWaiting(now());
749
+ const when = call.when ?? now();
750
+ if (aheadOfClock(when)) throw notWritten('The stub was not written: it is dated ahead of the clock.');
751
+ const made = await composeStub(call, when);
752
+ const tried = await fitsBook(made, 'The stub was not written: with it, the book would not pass its check.');
753
+ return commit(await countersigned(made, call), made, tried);
754
+ });
755
+ },
756
+
757
+ /**
758
+ * Ask first; if the action is allowed, take it and write its stub.
759
+ *
760
+ * If the action is not allowed it is not taken and nothing is written.
761
+ * The action is taken now: a date handed in ("when") must be within 300
762
+ * seconds of the writer's clock, or the action is not taken. Before the
763
+ * action is taken, its stub is written and checked with the whole
764
+ * book, so that the receipt is known to fit before there is anything
765
+ * to give a receipt for. Where the slip asks for a countersignature,
766
+ * the action is taken only if a way to ask for one ("countersign") is
767
+ * given. If taking the action fails with an error, the error is passed
768
+ * on and no stub is written: there is then no receipt for whatever
769
+ * part of the action did happen, and the caller must deal with that.
770
+ * If taking the action never ends, the writer waits: it cannot know
771
+ * what happened.
772
+ *
773
+ * @param {{action: string, amount?: object, with?: string, details?: object[]}} request
774
+ * @param {() => Promise<any>} perform takes the action; it must not call this writer
775
+ * @param {object} [more] as for "record"; "when" defaults to the clock and must be within 300 seconds of it
776
+ * @returns {Promise<{done: boolean, answer: object, result?: any, stub?: object}>}
777
+ */
778
+ act(request, perform, more = {}) {
779
+ return calling(request, more, async (call) => {
780
+ await writeWaiting(now());
781
+ const when = call.when ?? now();
782
+ const answer = whileStopped(await checkBeforeRead(() => standing(), proposalFrom(call, when), checking()));
783
+ const stop = (problem) => {
784
+ answer.allowed = false;
785
+ answer.problems.push(problem);
786
+ };
787
+ if (answer.allowed && aheadOfClock(when)) stop({ code: 'record-not-sound', message: 'The action is dated ahead of the clock.' });
788
+ if (answer.allowed && behindClock(when)) {
789
+ stop({
790
+ code: 'record-not-sound',
791
+ message: 'The action is dated behind the clock. An action is taken now, so its date may not be more than 300 seconds before the stub writer\'s clock. An action that was taken earlier is written with "record".',
792
+ });
793
+ }
794
+ // Said whether or not something else stands in the way too, so that whoever reads the answer sees all of it.
795
+ if (answer.needs.includes('countersignature') && typeof call.countersign !== 'function' && !answer.breaches.some((b) => b.code === 'countersignature-missing')) {
796
+ answer.allowed = false;
797
+ answer.breaches.push({
798
+ code: 'countersignature-missing',
799
+ message: 'The slip asks for the other side to countersign this action, and the stub writer was given no way to ask for a countersignature.',
800
+ });
801
+ }
802
+ let made;
803
+ let tried;
804
+ if (answer.allowed) {
805
+ try {
806
+ made = await composeStub(call, when);
807
+ tried = await fitsBook(made, 'The stub for this action would not pass the check of the book, so the action was not taken.');
808
+ } catch (e) {
809
+ stop(problemFrom(e));
810
+ }
811
+ }
812
+ if (!answer.allowed) return { done: false, answer };
813
+ const outcome = await runInside(perform);
814
+ const stub = await commit(await countersigned(made, call), made, tried);
815
+ return { done: true, answer, result: outcome, stub };
816
+ });
817
+ },
818
+
819
+ /** The book as it now stands. */
820
+ book() {
821
+ return text;
822
+ },
823
+
824
+ /**
825
+ * What the record shows as it stands: the answer that a whole check of
826
+ * the book gives, with the cancellations the writer holds beside it.
827
+ * Nothing is read again.
828
+ * @returns {Promise<object>} as checkBook returns; a copy of its own each time
829
+ */
830
+ check() {
831
+ return inTurn(async () => structuredClone((await standing()).result));
832
+ },
833
+
834
+ /**
835
+ * The person's cancellations that the writer holds beside its book:
836
+ * those it was given when it was opened, and those of its own slip
837
+ * that it could not yet write into the book, each with the
838
+ * acknowledgement the writer signed for it; and a cancellation whose
839
+ * acknowledgement still waits to follow it into the book, with that
840
+ * acknowledgement. They live only in this
841
+ * writer. Keep them with the book, and hand them in again
842
+ * ("options.cancellations") when a writer is opened again from the
843
+ * book: a writer opened without them knows nothing of them.
844
+ * @returns {object[]} copies, each as it is handed to a check: {cancellation, stamps?, acknowledgements?}
845
+ */
846
+ cancellations() {
847
+ return structuredClone(beside());
848
+ },
849
+
850
+ /**
851
+ * Add an entry that someone else made (a seal, a cancellation, a
852
+ * refusal) to the book. It is added only if the book still passes its
853
+ * check with it, and it is not dated ahead of the clock.
854
+ *
855
+ * A cancellation is a special case: whatever is wrong with what stands
856
+ * beside it, a cancellation that the person signed is not lost.
857
+ * - If the entry cannot be added as it was handed over (a faulty
858
+ * time-stamp beside it, a member that cannot be written as a line),
859
+ * the cancellation is added with the time-stamps that pass their
860
+ * check, or by itself, without what else stood beside it, if the
861
+ * book passes its check with that.
862
+ * - If it cannot be added even so (a date ahead of the clock), but is
863
+ * signed with the passkey the slip names, the writer keeps it as the
864
+ * person's own copy and allows nothing more under the slip. It
865
+ * writes it into the book, at its next call, once the clock has
866
+ * reached its date.
867
+ * In both cases the call still fails, and the error says what was done.
868
+ *
869
+ * Whenever the person's cancellation of this writer's own slip is
870
+ * handed to "add", the writer signs an acknowledgement: its agent's
871
+ * statement that it was handed the cancellation, and when. It is
872
+ * handed back, to be given to the person, and it follows the
873
+ * cancellation into the book. One cancellation is acknowledged once:
874
+ * the same acknowledgement is handed back each time, also by a writer
875
+ * opened again and given what "cancellations" handed back.
876
+ *
877
+ * @param {object} entry
878
+ * @returns {Promise<{acknowledgement?: object}>} for the person's cancellation of this writer's slip: the acknowledgement
879
+ * @throws {Refusal} "record-not-sound" if the entry cannot be added as it was handed over.
880
+ * Nothing is added, except a cancellation by itself as set out above. Where the person's
881
+ * cancellation of this writer's slip was handed over, the error holds the "acknowledgement"
882
+ */
883
+ add(entry) {
884
+ // What is handed over is read once, member by member, when it is
885
+ // handed over: the members a line of a book lists, and the three of a
886
+ // cancellation by their names. Each is copied as it is read, so a
887
+ // member whose value changes from one reading to the next cannot make
888
+ // the forms below differ. A member that cannot be read or copied makes
889
+ // the entry one that cannot be written as a line. The three members of
890
+ // a cancellation are read first, each with room for a line of its own,
891
+ // so that nothing handed over beside them can keep them from being
892
+ // read. The other members share one line's room: once one of them
893
+ // cannot be read or copied, the entry cannot be a line, and the rest
894
+ // are not read. Anything but an object (a list, or a value that cannot
895
+ // even be looked into) cannot be a line, and is not written out.
896
+ const read = new Map();
897
+ let listed = null;
898
+ let object = false;
899
+ try {
900
+ object = entry !== null && typeof entry === 'object' && !Array.isArray(entry);
901
+ } catch {
902
+ object = false;
903
+ }
904
+ if (object) {
905
+ try {
906
+ listed = Object.keys(entry);
907
+ const known = ['cancellation', 'stamps', 'acknowledgements'].filter((name) => Object.hasOwn(entry, name));
908
+ const rest = copier();
909
+ let whole = true;
910
+ for (const name of [...known, ...listed.filter((name) => !known.includes(name))]) {
911
+ const own = known.includes(name);
912
+ if (!own && !whole) {
913
+ read.set(name, null);
914
+ continue;
915
+ }
916
+ try {
917
+ read.set(name, { value: own ? copyOf(entry[name]) : rest(entry[name]) });
918
+ } catch {
919
+ read.set(name, null);
920
+ if (!own) whole = false;
921
+ }
922
+ }
923
+ } catch {
924
+ listed = [];
925
+ read.clear();
926
+ }
927
+ }
928
+ const member = (name) => read.get(name) ?? null;
929
+ // The entry is written out once, as text. The text is what is checked, and what is added.
930
+ let line = null;
931
+ try {
932
+ if (listed !== null && listed.length > 0 && listed.every((name) => member(name) !== null)) line = entryLine(Object.fromEntries(listed.map((name) => [name, member(name).value])));
933
+ } catch {
934
+ line = null; // it cannot be a line of a book
935
+ }
936
+ // A cancellation is also written out by itself, without what stands beside it.
937
+ let alone = null;
938
+ try {
939
+ if (member('cancellation') !== null) alone = entryLine({ cancellation: member('cancellation').value });
940
+ } catch {
941
+ alone = null; // not a record at all
942
+ }
943
+ // And with its time-stamps alone, where more than those stands beside
944
+ // it; and the time-stamps themselves, each as a line would hold it:
945
+ // every one that can be written, from the first sixteen places of the
946
+ // list, so that a gap or a faulty item beside a sound time-stamp does
947
+ // not lose it.
948
+ let stamped = null;
949
+ const offered = [];
950
+ const stamps = alone !== null ? member('stamps') : null;
951
+ if (stamps !== null) {
952
+ try {
953
+ if (listed.length > 2) stamped = entryLine({ cancellation: member('cancellation').value, stamps: stamps.value });
954
+ } catch {
955
+ stamped = null;
956
+ }
957
+ if (Array.isArray(stamps.value)) {
958
+ for (let i = 0; i < Math.min(stamps.value.length, MAX_HANDED); i++) {
959
+ if (!Object.hasOwn(stamps.value, i)) continue;
960
+ try {
961
+ offered.push(JSON.parse(canonicalJson(stamps.value[i])));
962
+ } catch {
963
+ // not a time-stamp
964
+ }
965
+ }
966
+ }
967
+ }
968
+ // The acknowledgements handed over with it, as "cancellations" hands them back.
969
+ let handed = null;
970
+ const acknowledgements = alone !== null ? member('acknowledgements') : null;
971
+ try {
972
+ if (acknowledgements !== null && Array.isArray(acknowledgements.value)) handed = JSON.parse(JSON.stringify(acknowledgements.value));
973
+ } catch {
974
+ handed = null;
975
+ }
976
+ return inTurn(async () => {
977
+ // The person's cancellation of this writer's own slip stops the
978
+ // writer before anything else is done, the reading of the clock
979
+ // included: whatever fails after this, no action is taken. The
980
+ // acknowledgements handed over with it are sorted at once too, so
981
+ // that a clock that fails loses none of them.
982
+ let early = null;
983
+ if (alone !== null) {
984
+ const candidate = JSON.parse(alone);
985
+ const one = (await answerOf(carried, { ...base, cancellations: [candidate] })).result.held[0];
986
+ if (one && one.used && one.slip === slip) {
987
+ stopped = true;
988
+ early = { candidate, one, others: await sortHanded(one.fingerprint, candidate.cancellation, handed) };
989
+ }
990
+ }
991
+ // Where the writer holds a copy of this cancellation beside its book,
992
+ // the copy takes the acknowledgements handed over now and, where the
993
+ // book does not hold the cancellation, its sound time-stamps: before
994
+ // anything is written into the book. A line of a book is not changed.
995
+ // "keep": where the writer holds no copy, and the book does not hold
996
+ // the cancellation, the writer keeps one, with the sound time-stamps.
997
+ // True where the writer holds a copy afterwards.
998
+ const takeHanded = async (keep, inBook = early !== null && early.one.inBook !== null) => {
999
+ if (!early) return false;
1000
+ const { candidate, one, others } = early;
1001
+ const holds = kept.find((k) => k.fingerprint === one.fingerprint);
1002
+ const atOpening = holds ? undefined : given.find((h) => h && h.cancellation && h.cancellation.payload === candidate.cancellation.payload);
1003
+ const copy = holds ? holds.entry : atOpening;
1004
+ if (!copy && (inBook || !keep || room() <= 0)) return false;
1005
+ const stamps = inBook ? null : await mergedStamps(candidate.cancellation, copy && Object.hasOwn(copy, 'stamps') && Array.isArray(copy.stamps) ? copy.stamps : [], offered);
1006
+ if (holds) {
1007
+ if (stamps) {
1008
+ holds.entry = { ...holds.entry, stamps };
1009
+ holds.tried = false;
1010
+ }
1011
+ putHanded(holds.entry, one.fingerprint, others);
1012
+ } else if (atOpening) {
1013
+ if (stamps) {
1014
+ atOpening.stamps = stamps;
1015
+ const item = unwritten.find((u) => u.entry === atOpening);
1016
+ if (item) item.tried = false;
1017
+ }
1018
+ putHanded(atOpening, one.fingerprint, others);
1019
+ } else {
1020
+ const copied = { cancellation: candidate.cancellation };
1021
+ if (stamps) copied.stamps = stamps;
1022
+ putHanded(copied, one.fingerprint, others);
1023
+ kept.push({ entry: copied, when: parseTime(one.content.when), fingerprint: one.fingerprint, tried: false });
1024
+ }
1025
+ return true;
1026
+ };
1027
+ // The clock is read once for the whole call.
1028
+ let at;
1029
+ try {
1030
+ at = now();
1031
+ } catch (e) {
1032
+ // The cancellation is kept, with its sound time-stamps and the
1033
+ // acknowledgements handed over with it, so that none is lost with
1034
+ // the call.
1035
+ const holding = await takeHanded(true);
1036
+ // An acknowledgement of the agent's own, taken for a cancellation
1037
+ // the book holds, waits to follow it into the book, and is handed
1038
+ // back meanwhile, as one that could not be written at once does.
1039
+ const fingerprint = early ? early.one.fingerprint : null;
1040
+ if (early && early.one.inBook !== null && acknowledged.has(fingerprint) && !written.has(fingerprint)) waiting.set(fingerprint, early.candidate.cancellation);
1041
+ if (early && early.one.inBook === null && !holding) {
1042
+ const full = refused(
1043
+ 'The clock failed, so nothing was written into the book. The cancellation is signed with the passkey the slip names, so the stub writer allows nothing more under that slip. It already holds as many cancellations beside its book as it can, so this one is not kept: hand it over again later.',
1044
+ );
1045
+ full.cause = e;
1046
+ throw full;
1047
+ }
1048
+ throw e;
1049
+ }
1050
+ await takeHanded(false);
1051
+ await writeWaiting(at);
1052
+ let why = 'it cannot be written as a line of a book.';
1053
+ if (line !== null) {
1054
+ const whole = await takeLine(line, at);
1055
+ if (whole.fits) {
1056
+ // An added stub of this writer's own chain moves its place in the chain on.
1057
+ after = lastStub(whole.checked.entries);
1058
+ const last = whole.checked.entries.at(-1);
1059
+ if (last.kind === 'cancellation' && last.slip === slip && last.verified === 'all') {
1060
+ stopped = true;
1061
+ return { acknowledgement: await acknowledge(last.fingerprint, JSON.parse(line).cancellation, true, at) };
1062
+ }
1063
+ return {};
1064
+ }
1065
+ why = whole.why;
1066
+ }
1067
+ if (alone === null) throw notWritten(`The entry was not added: ${why}`);
1068
+ // The cancellation with its time-stamps, then with those of them that
1069
+ // pass their check, then by itself, where it was handed over with more.
1070
+ const cancellationRecord = JSON.parse(alone).cancellation;
1071
+ const sound = await mergedStamps(cancellationRecord, [], offered);
1072
+ const withSound = sound ? entryLine({ cancellation: cancellationRecord, stamps: sound }) : null;
1073
+ let added = null;
1074
+ const tried = new Set([line]);
1075
+ for (const form of [stamped, withSound, alone]) {
1076
+ if (form === null || tried.has(form) || added !== null) continue;
1077
+ tried.add(form);
1078
+ if ((await takeLine(form, at)).fits) added = form;
1079
+ }
1080
+ // Whether the person signed it, for this writer's own slip, and
1081
+ // whether the book now holds it: checked without any other cancellation.
1082
+ const candidate = JSON.parse(added ?? alone);
1083
+ const one = (await answerOf(carried, { ...base, cancellations: [candidate] })).result.held[0];
1084
+ const own = Boolean(one && one.used && one.slip === slip);
1085
+ const stops = own ? ' It is signed with the passkey the slip names, so the stub writer allows nothing more under that slip.' : '';
1086
+ if (own) stopped = true;
1087
+ if (added !== null) {
1088
+ const acknowledgement = own ? await acknowledge(one.fingerprint, candidate.cancellation, true, at) : undefined;
1089
+ const how =
1090
+ added === stamped
1091
+ ? 'The cancellation was added to the book with its time-stamps, without what else stood beside it.'
1092
+ : added === withSound
1093
+ ? 'The cancellation was added to the book with those of its time-stamps that pass their check, without what else stood beside it.'
1094
+ : 'The cancellation by itself was added to the book, without what stood beside it.';
1095
+ throw refused(`The cancellation was not added as it was handed over: ${why} ${how}${stops}`, acknowledgement);
1096
+ }
1097
+ if (own) {
1098
+ const inBook = one.inBook !== null;
1099
+ const acknowledgement = await acknowledge(one.fingerprint, candidate.cancellation, inBook, at);
1100
+ const holding = await takeHanded(true, inBook);
1101
+ if (inBook) {
1102
+ const how = early && early.one.inBook === null ? ' It was written into the book in this call, from the copy the stub writer held beside it, with the time-stamps that copy holds.' : '';
1103
+ throw refused(`The book already holds this cancellation, so it was not added again.${how}${stops}`, acknowledgement);
1104
+ }
1105
+ if (!holding) {
1106
+ throw refused(`The cancellation was not added: ${why}${stops} The stub writer already holds as many cancellations beside its book as it can, so this one is not kept: hand it over again later.`, acknowledgement);
1107
+ }
1108
+ throw refused(`The cancellation was not added: ${why}${stops}`, acknowledgement);
1109
+ }
1110
+ throw notWritten(`The entry was not added: ${why}`);
1111
+ });
1112
+ },
1113
+ };
1114
+ return writer;
1115
+ }