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.
- package/CHANGELOG.md +1062 -0
- package/LICENSE +202 -0
- package/LICENSE-CC-BY-4.0.txt +396 -0
- package/README.md +689 -0
- package/bin/provared-check.mjs +565 -0
- package/bin/provared-headers.mjs +110 -0
- package/docs/threat-model.md +997 -0
- package/net/headers.js +403 -0
- package/package.json +57 -0
- package/page/check-page.js +162 -0
- package/page/index.html +47 -0
- package/page/render.js +530 -0
- package/page/style.css +150 -0
- package/samples/office-supplies.expected.json +82 -0
- package/samples/office-supplies.jsonl +10 -0
- package/samples/office-supplies.show.json +59 -0
- package/spec/provared-format.md +1672 -0
- package/src/actions.js +90 -0
- package/src/approval.js +73 -0
- package/src/blockstamp.js +198 -0
- package/src/book.js +2232 -0
- package/src/check.d.ts +21 -0
- package/src/check.js +15 -0
- package/src/compare.js +276 -0
- package/src/cover.js +198 -0
- package/src/der.js +192 -0
- package/src/encoding.js +286 -0
- package/src/fields.js +94 -0
- package/src/guard.js +171 -0
- package/src/headers.js +380 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +37 -0
- package/src/jws.js +235 -0
- package/src/keys.js +149 -0
- package/src/pass.js +67 -0
- package/src/passkey-browser.d.ts +20 -0
- package/src/passkey-browser.js +171 -0
- package/src/recorder.js +1115 -0
- package/src/seal.js +133 -0
- package/src/service.js +124 -0
- package/src/signatures.js +147 -0
- package/src/slip.js +401 -0
- package/src/standing.js +181 -0
- package/src/stub.js +140 -0
- package/src/timestamp.js +351 -0
- package/src/tools.js +192 -0
- package/src/tree.js +143 -0
- package/src/types.d.ts +628 -0
- package/src/webauthn.js +119 -0
package/src/recorder.js
ADDED
|
@@ -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
|
+
}
|