@openlfcp/shared-objects 0.2.0-beta.1 → 0.2.0-beta.2

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.
@@ -30,6 +30,13 @@ export interface ParsedChange {
30
30
  /** Hex other actors, ascending: actor index i is otherActors[i - 1]. */
31
31
  readonly otherActors: readonly string[];
32
32
  readonly ops: readonly ParsedOp[];
33
+ /**
34
+ * §14.1: the extra bytes after the columns (which rule 4 leaves free) begin
35
+ * with an Automerge author — an unsigned LEB128 1, a length L, then at least
36
+ * L bytes. automerge 0.12 reads it and asserts the change's sequence number
37
+ * is 1, so a later change carrying one is refused (finding D5).
38
+ */
39
+ readonly beginsWithAuthor: boolean;
33
40
  }
34
41
  /**
35
42
  * §11.3: checks that `bytes` (one change chunk within the §11.1 limits) is
@@ -349,6 +349,36 @@ function checkValue(type, bytes) {
349
349
  if (type === 6)
350
350
  utf8(bytes, "a string value");
351
351
  }
352
+ /**
353
+ * §14.1: whether the extra bytes after a change's columns begin with an
354
+ * Automerge author (finding D5). Rule 4 leaves these bytes free, but
355
+ * automerge 0.12 reads an author there — an unsigned LEB128 1, a length L,
356
+ * then L bytes — and asserts the change's sequence number is 1. The LEB128 is
357
+ * read as automerge reads it: up to ten bytes, under 2^64, in any encoding
358
+ * (not the shortest-form `Reader` the rest of the walk uses).
359
+ */
360
+ function beginsWithAuthor(extra) {
361
+ let pos = 0;
362
+ const leb = () => {
363
+ let value = 0n;
364
+ let shift = 0n;
365
+ for (;;) {
366
+ if (pos >= extra.length)
367
+ return null;
368
+ const b = extra[pos++];
369
+ if (shift === 63n && b !== 0 && b !== 1)
370
+ return null;
371
+ value |= BigInt(b & 0x7f) << shift;
372
+ if ((b & 0x80) === 0)
373
+ return value;
374
+ shift += 7n;
375
+ }
376
+ };
377
+ if (leb() !== 1n)
378
+ return false;
379
+ const length = leb();
380
+ return length !== null && BigInt(extra.length - pos) >= length;
381
+ }
352
382
  /**
353
383
  * §11.3: checks that `bytes` (one change chunk within the §11.1 limits) is
354
384
  * the canonical encoding of its change, and returns the change's operations
@@ -403,7 +433,9 @@ export function checkCanonicalChange(bytes) {
403
433
  const data = new Map();
404
434
  for (const m of metas)
405
435
  data.set(m.spec, r.take(m.length));
406
- // What follows is the change's extra bytes.
436
+ // What follows is the change's extra bytes (§11.3 rule 4), free except that
437
+ // automerge reads an author from their start (§14.1, finding D5).
438
+ const authored = beginsWithAuthor(bytes.subarray(r.pos));
407
439
  const rle = (spec) => {
408
440
  const d = data.get(spec);
409
441
  return d === undefined ? undefined : column(spec, d);
@@ -615,6 +647,7 @@ export function checkCanonicalChange(bytes) {
615
647
  deps: Object.freeze(deps.map(hex)),
616
648
  otherActors: Object.freeze(others.map(hex)),
617
649
  ops: Object.freeze(ops),
650
+ beginsWithAuthor: authored,
618
651
  });
619
652
  }
620
653
  //# sourceMappingURL=canonical.js.map
@@ -12,6 +12,12 @@ export interface CheckedChange {
12
12
  readonly deps: readonly string[];
13
13
  /** Hex IDs of the other actors the change refers to (§11.1: each must be known to the document). */
14
14
  readonly otherActors: readonly string[];
15
+ /**
16
+ * §14.1: the change's extra bytes begin with an Automerge author, allowed
17
+ * only in an actor's first change (finding D5). See
18
+ * {@link ParsedChange.beginsWithAuthor}.
19
+ */
20
+ readonly beginsWithAuthor: boolean;
15
21
  }
16
22
  /**
17
23
  * Checks that `bytes` is exactly one valid Automerge change (§11, §12) and
@@ -63,6 +63,7 @@ export function checkChange(bytes) {
63
63
  seq: decoded.seq,
64
64
  deps: Object.freeze([...decoded.deps]),
65
65
  otherActors: expansion.otherActors,
66
+ beginsWithAuthor: parsed.beginsWithAuthor,
66
67
  });
67
68
  DECODED.set(checked, decoded);
68
69
  PARSED.set(checked, parsed);
@@ -69,6 +69,14 @@ export function admitBatch(changes, doc, accept = () => undefined) {
69
69
  });
70
70
  continue;
71
71
  }
72
+ else if (c.beginsWithAuthor && c.seq !== 1) {
73
+ refused.push({
74
+ change: c,
75
+ error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `the change's extra bytes begin with an author but its sequence number is ${c.seq}, not 1 (§14.1)`),
76
+ held: false,
77
+ });
78
+ continue;
79
+ }
72
80
  else {
73
81
  const references = referenceRefusal(c, doc);
74
82
  if (references !== null) {
@@ -124,6 +132,12 @@ export function admitChange(change, doc) {
124
132
  kind: "invalid",
125
133
  error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `actor ${change.actor} sequence ${change.seq} skips sequence ${latest + 1} (§14.1)`),
126
134
  };
135
+ // §14.1: an author belongs only in an actor's first change (finding D5).
136
+ if (change.beginsWithAuthor && change.seq !== 1)
137
+ return {
138
+ kind: "invalid",
139
+ error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `the change's extra bytes begin with an author but its sequence number is ${change.seq}, not 1 (§14.1)`),
140
+ };
127
141
  // §11.4: checked only; the caller adds the change once the engine applied it.
128
142
  const references = referenceRefusal(change, doc);
129
143
  if (references !== null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlfcp/shared-objects",
3
- "version": "0.2.0-beta.1",
3
+ "version": "0.2.0-beta.2",
4
4
  "description": "SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1) implementation.",
5
5
  "keywords": [
6
6
  "openlfcp",
@@ -53,8 +53,8 @@
53
53
  "dependencies": {
54
54
  "@automerge/automerge": "3.5.0",
55
55
  "fflate": "0.8.3",
56
- "@openlfcp/core": "^0.2.0-beta.1",
57
- "@openlfcp/crypto": "^0.2.0-beta.1"
56
+ "@openlfcp/core": "^0.2.0-beta.2",
57
+ "@openlfcp/crypto": "^0.2.0-beta.2"
58
58
  },
59
59
  "devDependencies": {
60
60
  "fast-check": "4.10.2"