provared 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/seal.js CHANGED
@@ -25,7 +25,7 @@ export const CLOCK_ALLOWANCE_MS = 300 * 1000;
25
25
  * @param {any} c
26
26
  */
27
27
  export function validateSealContent(c) {
28
- f.members(c, ['by', 'id', 'root', 'size', 'type', 'when'], ['previous'], 'seal');
28
+ f.members(c, ['by', 'id', 'root', 'size', 'type', 'when'], ['book', 'previous'], 'seal');
29
29
  f.id(c.id, 'id');
30
30
  f.members(c.by, ['keys', 'name'], [], 'by');
31
31
  f.label(c.by.name, 'by.name');
@@ -34,6 +34,13 @@ export function validateSealContent(c) {
34
34
  if (c.size < 1) throw f.fail('size', 'must be 1 or more.');
35
35
  f.fingerprintText(c.root, 'root');
36
36
  if (Object.hasOwn(c, 'previous')) f.fingerprintText(c.previous, 'previous');
37
+ // The number of the book among the books a recorder keeps under one permission (section 21.6). Absent for the
38
+ // first book, so that every seal written before numbers existed reads as it did; written only from 2.
39
+ if (Object.hasOwn(c, 'book')) {
40
+ f.wholeNumber(c.book, 'book');
41
+ if (c.book < 2) throw f.fail('book', 'must be 2 or more: the first book has no number.');
42
+ if (!Object.hasOwn(c, 'previous')) throw f.fail('previous', 'is missing: every seal of a book after the first names the seal before it, and the first names the last seal of the book before.');
43
+ }
37
44
  f.time(c.when, 'when');
38
45
  }
39
46
 
@@ -102,7 +109,10 @@ export async function keySetFingerprint(keys) {
102
109
  * @param {string} book the book so far
103
110
  * @param {object} fields
104
111
  * @param {{keys: object[], name: string}} fields.by the recorder's public key set and its name (a label)
105
- * @param {string} [fields.previous] the fingerprint of the seal before this one in the book, if there is one
112
+ * @param {string} [fields.previous] the fingerprint of the seal before this one in the book, if there is one; for
113
+ * the first seal of a book after the first, the fingerprint of the last seal of the book before
114
+ * @param {number} [fields.book] the book's number among the books the recorder keeps under one permission (section
115
+ * 21.6): 2 or more for a book after the first; 1 or none for the first, which carries no number
106
116
  * @param {number|Date} [fields.when] defaults to now
107
117
  * @param {any[]} privateKeys the recorder's private keys, or signers (signatures.js), in the order of the key set. A signer must hold the key "by" names in its place
108
118
  * @returns {Promise<{record: object, fingerprint: string, fingerprintBytes: Uint8Array}>}
@@ -111,15 +121,33 @@ export async function keySetFingerprint(keys) {
111
121
  export async function writeSeal(book, fields, privateKeys) {
112
122
  const lines = book.split('\n');
113
123
  if (lines[lines.length - 1] === '') lines.pop();
124
+ return sealOver(lines.length, await treeRoot(lines.map(utf8)), fields, privateKeys);
125
+ }
126
+
127
+ /**
128
+ * Sign a seal over a number of entries and their top fingerprint, worked out
129
+ * by the caller: writeSeal from the book, and the central sealer (sealer.js)
130
+ * from the tree it built from the leaf fingerprints it was sent. One path, so
131
+ * that the two give the same seal for the same book. Not part of the public
132
+ * interface: a size and a top fingerprint handed in from outside are never
133
+ * signed (choice 388).
134
+ * @param {number} size
135
+ * @param {Uint8Array} root
136
+ * @param {object} fields as writeSeal takes them
137
+ * @param {any[]} privateKeys
138
+ */
139
+ export async function sealOver(size, root, fields, privateKeys) {
114
140
  const content = {
115
141
  type: KINDS.seal.type,
116
142
  id: fields.id ?? randomId(),
117
143
  by: fields.by,
118
- size: lines.length,
119
- root: toBase64url(await treeRoot(lines.map(utf8))),
144
+ size,
145
+ root: toBase64url(root),
120
146
  when: formatTime(fields.when ?? Date.now()),
121
147
  };
122
148
  if (fields.previous) content.previous = fields.previous;
149
+ // The book's number: 1, or none, for the first book, which then carries no number.
150
+ if (fields.book !== undefined && fields.book !== null && fields.book !== 1) content.book = fields.book;
123
151
  validateSealContent(content);
124
152
  const { contentBytes, payloadB64 } = encodeContent(content);
125
153
  const headers = protectedHeaders('seal');