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

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 (43) hide show
  1. package/README.md +15 -2
  2. package/dist/admission/actor.d.ts +16 -0
  3. package/dist/admission/actor.js +35 -0
  4. package/dist/{canonical.d.ts → admission/canonical.d.ts} +2 -0
  5. package/dist/{canonical.js → admission/canonical.js} +9 -2
  6. package/dist/{automerge-bytes.d.ts → admission/framing.d.ts} +32 -1
  7. package/dist/admission/framing.js +231 -0
  8. package/dist/admission/index.d.ts +26 -0
  9. package/dist/admission/index.js +26 -0
  10. package/dist/{chunk-limits.d.ts → admission/limits.d.ts} +11 -1
  11. package/dist/{chunk-limits.js → admission/limits.js} +25 -10
  12. package/dist/{references.d.ts → admission/references.d.ts} +1 -1
  13. package/dist/{references.js → admission/references.js} +9 -3
  14. package/dist/admission/sequence.d.ts +81 -0
  15. package/dist/admission/sequence.js +133 -0
  16. package/dist/data-profile.d.ts +1 -1
  17. package/dist/data-profile.js +3 -7
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +2 -2
  20. package/dist/replica.d.ts +32 -2
  21. package/dist/replica.js +154 -195
  22. package/dist/sections/admission.d.ts +35 -0
  23. package/dist/sections/admission.js +328 -0
  24. package/dist/sections/data-profile.d.ts +195 -0
  25. package/dist/sections/data-profile.js +383 -0
  26. package/dist/sections/dispatch.d.ts +41 -0
  27. package/dist/sections/dispatch.js +33 -0
  28. package/dist/sections/document.d.ts +54 -0
  29. package/dist/sections/document.js +101 -0
  30. package/dist/sections/index.d.ts +18 -0
  31. package/dist/sections/index.js +18 -0
  32. package/dist/sections/replica.d.ts +412 -0
  33. package/dist/sections/replica.js +1149 -0
  34. package/dist/sections/schema.d.ts +77 -0
  35. package/dist/sections/schema.js +548 -0
  36. package/dist/sections/tree.d.ts +83 -0
  37. package/dist/sections/tree.js +290 -0
  38. package/dist/sections/values.d.ts +30 -0
  39. package/dist/sections/values.js +36 -0
  40. package/dist/values.d.ts +3 -18
  41. package/dist/values.js +7 -95
  42. package/package.json +12 -4
  43. package/dist/automerge-bytes.js +0 -111
package/README.md CHANGED
@@ -20,9 +20,22 @@ reaches the replica (§95). The replica has no Markdown or editor concepts.
20
20
 
21
21
  Part of the OpenLFCP TypeScript SDK, which implements the OpenLFCP MVP 0.1
22
22
  subset of [LFCP-WIRE-01](https://github.com/openlfcp/spec/blob/main/wire/LFCP-WIRE-01.md)
23
- at `mvp-0.1-baseline.10`, not every deferred WIRE-01 feature. It does not
23
+ at `mvp-0.1-baseline.9`, not every deferred WIRE-01 feature. It does not
24
24
  claim full LFCP-WIRE-01 conformance. This is the MVP 0.1 release
25
- (`0.1.4`, npm dist-tag `latest`). Until 1.0, minor versions may change APIs.
25
+ (`0.1.3`, npm dist-tag `latest`). Until 1.0, minor versions may change APIs.
26
+
27
+ ## Shared sections (Working Draft)
28
+
29
+ `@openlfcp/shared-objects/sections` implements part of
30
+ SHARED-SECTIONS-PROFILE-01 (`org.openlfcp.shared-sections.v1`, Working
31
+ Draft 0.3, not in any implementation baseline): profile dispatch by a
32
+ Resource's Genesis profile (`profileModel`), the section actor binding
33
+ (`deriveSectionActorId`) and schema validation of a section document
34
+ (`validateSection`, `SectionDocument`), and the writer of a section's
35
+ creation and Task intents (`SectionReplica`). Moves, lifecycle, Text
36
+ edits, the effective tree and the section admission rules are not there
37
+ yet. It may change
38
+ without notice until the profile is in a baseline.
26
39
 
27
40
  ## Automerge and WebAssembly
28
41
 
@@ -0,0 +1,16 @@
1
+ import { type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ import type { CheckedChange } from "./framing.js";
3
+ /**
4
+ * SHARED-OBJECTS-PROFILE-01 §8, the actor binding, for any profile that
5
+ * inherits it with its own domain (SHARED-SECTIONS-PROFILE-01 §2):
6
+ * SHA-256(ASCII(domain) || resource_id || principal_id), from the raw
7
+ * 32-byte IDs.
8
+ */
9
+ export declare function deriveDomainActorId(domain: string, resource: ResourceId, principal: PrincipalId): Uint8Array;
10
+ /**
11
+ * §8, §11: a Data Unit carries only changes of its signer's actor. Throws
12
+ * PROFILE_INVALID / CHANGE_ACTOR_MISMATCH otherwise; `actor` is the hex
13
+ * actor ID the signer's binding gives.
14
+ */
15
+ export declare function checkChangeActor(change: CheckedChange, actor: string): CheckedChange;
16
+ //# sourceMappingURL=actor.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { ID32_LENGTH, LfcpError } from "@openlfcp/core";
2
+ import { sha256 } from "@openlfcp/crypto";
3
+ import { ProfileInvalidError } from "../profile-invalid.js";
4
+ /**
5
+ * SHARED-OBJECTS-PROFILE-01 §8, the actor binding, for any profile that
6
+ * inherits it with its own domain (SHARED-SECTIONS-PROFILE-01 §2):
7
+ * SHA-256(ASCII(domain) || resource_id || principal_id), from the raw
8
+ * 32-byte IDs.
9
+ */
10
+ export function deriveDomainActorId(domain, resource, principal) {
11
+ for (const [what, id] of [
12
+ ["the Resource ID", resource],
13
+ ["the Principal ID", principal],
14
+ ]) {
15
+ if (!(id instanceof Uint8Array) || id.length !== ID32_LENGTH)
16
+ throw new LfcpError("INVALID_LENGTH", `${what} must be the raw 32-byte ID`);
17
+ }
18
+ const prefix = Uint8Array.from(domain, (c) => c.charCodeAt(0));
19
+ const input = new Uint8Array(prefix.length + 2 * ID32_LENGTH);
20
+ input.set(prefix, 0);
21
+ input.set(resource, prefix.length);
22
+ input.set(principal, prefix.length + ID32_LENGTH);
23
+ return sha256(input);
24
+ }
25
+ /**
26
+ * §8, §11: a Data Unit carries only changes of its signer's actor. Throws
27
+ * PROFILE_INVALID / CHANGE_ACTOR_MISMATCH otherwise; `actor` is the hex
28
+ * actor ID the signer's binding gives.
29
+ */
30
+ export function checkChangeActor(change, actor) {
31
+ if (change.actor !== actor)
32
+ throw new ProfileInvalidError("CHANGE_ACTOR_MISMATCH", `the change's Automerge actor ${change.actor} is not the §8 actor ${actor} of the unit's signer (§11, SO-SEC1)`);
33
+ return change;
34
+ }
35
+ //# sourceMappingURL=actor.js.map
@@ -17,6 +17,8 @@ export interface ParsedOp {
17
17
  readonly action: number;
18
18
  /** Predecessor IDs "counter@actorhex", in order. */
19
19
  readonly pred: readonly string[];
20
+ /** The value's type, the low 4 bits of its metadata (8: a counter). */
21
+ readonly valueType: number;
20
22
  }
21
23
  /** A change read from its bytes (§11.3), without the engine. */
22
24
  export interface ParsedChange {
@@ -12,7 +12,7 @@
12
12
  // the same bytes. Everything else is checked as stated: the header, the
13
13
  // columns and their presence, row counts, value metadata and bytes,
14
14
  // actions, IDs and counters below 2^32, sorted predecessors.
15
- import { ProfileInvalidError } from "./profile-invalid.js";
15
+ import { ProfileInvalidError } from "../profile-invalid.js";
16
16
  const refuse = (why) => {
17
17
  throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `the change is not in its canonical encoding (§11.3): ${why}`);
18
18
  };
@@ -575,7 +575,14 @@ export function checkCanonicalChange(bytes) {
575
575
  pred.push(`${ctr}@${id}`);
576
576
  anyPred = true;
577
577
  }
578
- ops.push(Object.freeze({ obj, key, insert: ins, action: act, pred: Object.freeze(pred) }));
578
+ ops.push(Object.freeze({
579
+ obj,
580
+ key,
581
+ insert: ins,
582
+ action: act,
583
+ pred: Object.freeze(pred),
584
+ valueType: valueTypes[i],
585
+ }));
579
586
  }
580
587
  // Rule 3: each column present exactly when the table says.
581
588
  const presence = [
@@ -1,3 +1,4 @@
1
+ import * as A from "@automerge/automerge";
1
2
  import { type ParsedChange } from "./canonical.js";
2
3
  /** A checked Automerge change: its bytes and decoded header. */
3
4
  export interface CheckedChange {
@@ -21,8 +22,20 @@ export interface CheckedChange {
21
22
  * a change without verifying its checksum, so the check is done here.
22
23
  */
23
24
  export declare function checkChange(bytes: Uint8Array): CheckedChange;
25
+ /**
26
+ * The change hash of bytes checkChange refused, read without decoding
27
+ * them: the SHA-256 of the chunk from its type byte on (the Automerge
28
+ * change hash). A change above a §11.1 limit is named this way, so a
29
+ * receiver can report it without expanding it. Undefined when the bytes
30
+ * are not one change chunk that could be read: no magic bytes, another
31
+ * chunk type, a length that does not cover them exactly, or a header or
32
+ * column cut short.
33
+ */
34
+ export declare function refusedChangeHash(bytes: Uint8Array, error: unknown): string | undefined;
24
35
  /** The change as its canonical bytes give it (§11.3), reusing the walk of checkChange. */
25
36
  export declare function parsedOf(change: CheckedChange): ParsedChange;
37
+ /** The decoded change, reusing the decode of checkChange when there was one. */
38
+ export declare function decodedOf(change: CheckedChange): A.DecodedChange;
26
39
  /**
27
40
  * Checks that `bytes` starts as an Automerge full save (a document chunk,
28
41
  * §13). Whether the whole image loads, checksums included, is decided by
@@ -37,4 +50,22 @@ export declare function unframeChange(plaintext: Uint8Array): CheckedChange;
37
50
  export declare function frameSnapshot(save: Uint8Array): Uint8Array;
38
51
  /** §13: the Automerge full save a Snapshot plaintext carries (header checked). Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
39
52
  export declare function unframeSnapshot(plaintext: Uint8Array): Uint8Array;
40
- //# sourceMappingURL=automerge-bytes.d.ts.map
53
+ /** §11, §13: the only framing version. */
54
+ export declare const FRAMING_VERSION = 1;
55
+ /**
56
+ * Deterministic CBOR of [1, bstr] (§11 shared-objects-change, §13
57
+ * shared-objects-snapshot): the profile plaintext of a Data Unit (one
58
+ * Automerge change) or a Snapshot (one Automerge full save). The bytes are
59
+ * framed as given; checking that they are a valid Automerge change or save
60
+ * is the Automerge binding (LFCP-031).
61
+ */
62
+ export declare function frameProfilePayload(bytes: Uint8Array): Uint8Array;
63
+ /**
64
+ * The inner bytes of a framed profile plaintext. §11: a receiver MUST
65
+ * reject plaintext that is not valid CBOR, not a two-element array, or
66
+ * uses an unsupported framing version; this also requires the
67
+ * deterministic encoding and no trailing bytes. Throws PROFILE_INVALID /
68
+ * INVALID_AUTOMERGE_BYTES (§74.1).
69
+ */
70
+ export declare function unframeProfilePayload(framed: Uint8Array): Uint8Array;
71
+ //# sourceMappingURL=framing.d.ts.map
@@ -0,0 +1,231 @@
1
+ import * as A from "@automerge/automerge";
2
+ import { LfcpError, toHex } from "@openlfcp/core";
3
+ import { sha256 } from "@openlfcp/crypto";
4
+ import { ProfileInvalidError } from "../profile-invalid.js";
5
+ import { checkCanonicalChange } from "./canonical.js";
6
+ import { checkChangeExpansion, MalformedChunkError } from "./limits.js";
7
+ /**
8
+ * Automerge byte checks of the SHARED-OBJECTS-PROFILE-01 payloads (LFCP-031):
9
+ * a Data Unit plaintext carries exactly one Automerge change (§11, §12), a
10
+ * Snapshot plaintext one Automerge full save (§13). The framing itself is
11
+ * frameProfilePayload / unframeProfilePayload (LFCP-030); this adds the
12
+ * check that the framed bytes are what they claim to be.
13
+ *
14
+ * Automerge storage chunks start with the magic bytes 85 6f 4a 83, a 4-byte
15
+ * checksum (the first 4 bytes of the chunk hash) and a chunk type: 0 for a
16
+ * document (a full save), 1 for a change, 2 for a compressed change.
17
+ */
18
+ const MAGIC = [0x85, 0x6f, 0x4a, 0x83];
19
+ const CHUNK_DOCUMENT = 0;
20
+ const CHUNK_CHANGE = 1;
21
+ const CHUNK_COMPRESSED_CHANGE = 2;
22
+ const HEADER = 9; // magic, checksum, chunk type
23
+ /** §11, §13, §74.1: PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
24
+ const reject = (why) => {
25
+ throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", why);
26
+ };
27
+ function chunkType(bytes, what) {
28
+ if (bytes.length < HEADER || MAGIC.some((b, i) => bytes[i] !== b))
29
+ return reject(`${what} is not an Automerge chunk`);
30
+ return bytes[8];
31
+ }
32
+ /**
33
+ * Checks that `bytes` is exactly one valid Automerge change (§11, §12) and
34
+ * returns it decoded. Rejects with PROFILE_INVALID / INVALID_AUTOMERGE_BYTES: a chunk that is not a
35
+ * change (a document chunk is a full save, §13), bytes Automerge cannot
36
+ * parse, trailing bytes such as a second concatenated change, and a
37
+ * checksum that does not match the change hash. Automerge JS 3.5.0 parses
38
+ * a change without verifying its checksum, so the check is done here.
39
+ */
40
+ export function checkChange(bytes) {
41
+ const type = chunkType(bytes, "the change");
42
+ if (type === CHUNK_COMPRESSED_CHANGE)
43
+ reject("a compressed Automerge change (chunk type 2): a change is uncompressed (§11)");
44
+ if (type !== CHUNK_CHANGE)
45
+ reject(`chunk type ${type} is not an Automerge change (§11)`);
46
+ // §11.1: what the change expands to and its structure, before Automerge decodes it.
47
+ const expansion = checkChangeExpansion(bytes);
48
+ // §11.3: the canonical encoding, by the format's properties, still before the engine.
49
+ const parsed = checkCanonicalChange(bytes);
50
+ let decoded;
51
+ try {
52
+ decoded = A.decodeChange(bytes);
53
+ }
54
+ catch (e) {
55
+ return reject(`invalid Automerge change bytes (§11): ${e.message}`);
56
+ }
57
+ if (toHex(bytes.subarray(4, 8)) !== decoded.hash.slice(0, 8))
58
+ reject("the Automerge change checksum does not match its hash (§11)");
59
+ const checked = Object.freeze({
60
+ bytes: Uint8Array.from(bytes),
61
+ hash: decoded.hash,
62
+ actor: decoded.actor,
63
+ seq: decoded.seq,
64
+ deps: Object.freeze([...decoded.deps]),
65
+ otherActors: expansion.otherActors,
66
+ });
67
+ DECODED.set(checked, decoded);
68
+ PARSED.set(checked, parsed);
69
+ return checked;
70
+ }
71
+ /**
72
+ * The change hash of bytes checkChange refused, read without decoding
73
+ * them: the SHA-256 of the chunk from its type byte on (the Automerge
74
+ * change hash). A change above a §11.1 limit is named this way, so a
75
+ * receiver can report it without expanding it. Undefined when the bytes
76
+ * are not one change chunk that could be read: no magic bytes, another
77
+ * chunk type, a length that does not cover them exactly, or a header or
78
+ * column cut short.
79
+ */
80
+ export function refusedChangeHash(bytes, error) {
81
+ if (error instanceof MalformedChunkError)
82
+ return undefined;
83
+ if (bytes.length < HEADER || MAGIC.some((b, i) => bytes[i] !== b))
84
+ return undefined;
85
+ if (bytes[8] !== CHUNK_CHANGE)
86
+ return undefined;
87
+ // The ULEB128 chunk length after the type byte must cover the rest exactly.
88
+ let length = 0;
89
+ let scale = 1;
90
+ let pos = HEADER;
91
+ for (;;) {
92
+ if (pos >= bytes.length || pos - HEADER >= 8)
93
+ return undefined;
94
+ const b = bytes[pos++];
95
+ length += (b & 0x7f) * scale;
96
+ scale *= 128;
97
+ if ((b & 0x80) === 0)
98
+ break;
99
+ }
100
+ if (pos + length !== bytes.length)
101
+ return undefined;
102
+ return toHex(sha256(bytes.subarray(8)));
103
+ }
104
+ /** The operations checkChange read from the bytes (§11.3), for the references of §11.4. */
105
+ const PARSED = new WeakMap();
106
+ /** The change as its canonical bytes give it (§11.3), reusing the walk of checkChange. */
107
+ export function parsedOf(change) {
108
+ let p = PARSED.get(change);
109
+ if (p === undefined) {
110
+ p = checkCanonicalChange(change.bytes);
111
+ PARSED.set(change, p);
112
+ }
113
+ return p;
114
+ }
115
+ /** The decoded form checkChange made, kept for the change's lifetime (decoding is not cheap). */
116
+ const DECODED = new WeakMap();
117
+ /** The decoded change, reusing the decode of checkChange when there was one. */
118
+ export function decodedOf(change) {
119
+ return DECODED.get(change) ?? A.decodeChange(change.bytes);
120
+ }
121
+ /**
122
+ * Checks that `bytes` starts as an Automerge full save (a document chunk,
123
+ * §13). Whether the whole image loads, checksums included, is decided by
124
+ * Automerge's load.
125
+ */
126
+ export function checkSaveHeader(bytes) {
127
+ const type = chunkType(bytes, "the Snapshot image");
128
+ if (type !== CHUNK_DOCUMENT)
129
+ reject(`chunk type ${type} is not an Automerge document (full save, §13)`);
130
+ }
131
+ /** §11: the Data Unit plaintext [1, change] of one checked Automerge change. */
132
+ export function frameChange(change) {
133
+ checkChange(change);
134
+ return frameProfilePayload(change);
135
+ }
136
+ /** §11: the one Automerge change a Data Unit plaintext carries, checked. Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
137
+ export function unframeChange(plaintext) {
138
+ return checkChange(unframeProfilePayload(plaintext));
139
+ }
140
+ /** §13: the Snapshot plaintext [1, save] of an Automerge full save. */
141
+ export function frameSnapshot(save) {
142
+ checkSaveHeader(save);
143
+ return frameProfilePayload(save);
144
+ }
145
+ /** §13: the Automerge full save a Snapshot plaintext carries (header checked). Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
146
+ export function unframeSnapshot(plaintext) {
147
+ const save = unframeProfilePayload(plaintext);
148
+ checkSaveHeader(save);
149
+ return save;
150
+ }
151
+ /** §11, §13: the only framing version. */
152
+ export const FRAMING_VERSION = 1;
153
+ /** The deterministic CBOR head of a byte string of `length` bytes (major type 2, shortest form). */
154
+ function bstrHead(length) {
155
+ if (length < 24)
156
+ return [0x40 | length];
157
+ if (length < 0x100)
158
+ return [0x58, length];
159
+ if (length < 0x10000)
160
+ return [0x59, length >> 8, length & 0xff];
161
+ if (length <= 0xffffffff)
162
+ return [
163
+ 0x5a,
164
+ (length >>> 24) & 0xff,
165
+ (length >>> 16) & 0xff,
166
+ (length >>> 8) & 0xff,
167
+ length & 0xff,
168
+ ];
169
+ throw new LfcpError("OUT_OF_RANGE", "the framed bytes are too long");
170
+ }
171
+ /**
172
+ * Deterministic CBOR of [1, bstr] (§11 shared-objects-change, §13
173
+ * shared-objects-snapshot): the profile plaintext of a Data Unit (one
174
+ * Automerge change) or a Snapshot (one Automerge full save). The bytes are
175
+ * framed as given; checking that they are a valid Automerge change or save
176
+ * is the Automerge binding (LFCP-031).
177
+ */
178
+ export function frameProfilePayload(bytes) {
179
+ if (!(bytes instanceof Uint8Array))
180
+ throw new LfcpError("INVALID_LENGTH", "framing takes bytes");
181
+ const head = bstrHead(bytes.length);
182
+ const out = new Uint8Array(2 + head.length + bytes.length);
183
+ out.set([0x82, FRAMING_VERSION, ...head], 0);
184
+ out.set(bytes, 2 + head.length);
185
+ return out;
186
+ }
187
+ /**
188
+ * The inner bytes of a framed profile plaintext. §11: a receiver MUST
189
+ * reject plaintext that is not valid CBOR, not a two-element array, or
190
+ * uses an unsupported framing version; this also requires the
191
+ * deterministic encoding and no trailing bytes. Throws PROFILE_INVALID /
192
+ * INVALID_AUTOMERGE_BYTES (§74.1).
193
+ */
194
+ export function unframeProfilePayload(framed) {
195
+ const fail = (why) => {
196
+ throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `invalid profile framing: ${why}`);
197
+ };
198
+ if (!(framed instanceof Uint8Array) || framed.length < 3)
199
+ return fail("too short");
200
+ if (framed[0] !== 0x82)
201
+ return fail("not a two-element array");
202
+ if (framed[1] !== FRAMING_VERSION)
203
+ return fail("unsupported framing version");
204
+ const first = framed[2];
205
+ if (first >> 5 !== 2)
206
+ return fail("the second element is not a byte string");
207
+ const info = first & 0x1f;
208
+ let length;
209
+ let offset;
210
+ const at = (i) => framed[i];
211
+ if (info < 24)
212
+ [length, offset] = [info, 3];
213
+ else if (info === 24)
214
+ [length, offset] = [at(3), 4];
215
+ else if (info === 25)
216
+ [length, offset] = [(at(3) << 8) | at(4), 5];
217
+ else if (info === 26)
218
+ [length, offset] = [((at(3) << 24) >>> 0) + (at(4) << 16) + (at(5) << 8) + at(6), 7];
219
+ else
220
+ return fail("unsupported byte string length");
221
+ if (offset > framed.length)
222
+ return fail("truncated");
223
+ const bytes = framed.subarray(offset);
224
+ if (bytes.length !== length)
225
+ return fail(bytes.length < length ? "truncated" : "trailing bytes");
226
+ const canonical = bstrHead(length);
227
+ if (canonical.length !== offset - 2)
228
+ return fail("non-shortest byte string length");
229
+ return Uint8Array.from(bytes);
230
+ }
231
+ //# sourceMappingURL=framing.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The admission of SHARED-OBJECTS-PROFILE-01 §§7–18 that every profile
3
+ * inheriting them shares (SHARED-SECTIONS-PROFILE-01 §2; LFCP-02-085),
4
+ * checked before the Automerge engine sees a change or a Snapshot:
5
+ *
6
+ * - framing (§11, §13): [1, change] and [1, full save], one uncompressed
7
+ * change chunk with a verified checksum;
8
+ * - limits (§11.1, §11.2, §13.1): the exact change expansion limits, the
9
+ * document depth bound, the Snapshot limits with their floor;
10
+ * - the canonical change encoding (§11.3), by the format's properties;
11
+ * - operation references (§11.4) against the change's causal history;
12
+ * - actor binding (§8), with the profile's domain;
13
+ * - sequence admission (§14.1): duplicates, dependencies, the sequence
14
+ * check, and holding a change whose actor and sequence another change
15
+ * has (POST-001).
16
+ *
17
+ * It knows no profile semantics: a profile's own rules (its values, §74.1)
18
+ * run after it, on admitted changes only. `@openlfcp/shared-objects/admission`.
19
+ */
20
+ export { checkChangeActor, deriveDomainActorId } from "./actor.js";
21
+ export { checkCanonicalChange, type ParsedChange, type ParsedOp } from "./canonical.js";
22
+ export { type CheckedChange, checkChange, checkSaveHeader, FRAMING_VERSION, frameChange, frameProfilePayload, frameSnapshot, parsedOf, refusedChangeHash, unframeChange, unframeProfilePayload, unframeSnapshot, } from "./framing.js";
23
+ export { CHANGE_LIMITS, type ChunkExpansion, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, type SnapshotLimits, } from "./limits.js";
24
+ export { ReferenceHistory, type ReferenceRule } from "./references.js";
25
+ export { admitBatch, admitChange, type BatchAdmission, type ChangeAdmission, type DocumentSequences, type RefusedChange, } from "./sequence.js";
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The admission of SHARED-OBJECTS-PROFILE-01 §§7–18 that every profile
3
+ * inheriting them shares (SHARED-SECTIONS-PROFILE-01 §2; LFCP-02-085),
4
+ * checked before the Automerge engine sees a change or a Snapshot:
5
+ *
6
+ * - framing (§11, §13): [1, change] and [1, full save], one uncompressed
7
+ * change chunk with a verified checksum;
8
+ * - limits (§11.1, §11.2, §13.1): the exact change expansion limits, the
9
+ * document depth bound, the Snapshot limits with their floor;
10
+ * - the canonical change encoding (§11.3), by the format's properties;
11
+ * - operation references (§11.4) against the change's causal history;
12
+ * - actor binding (§8), with the profile's domain;
13
+ * - sequence admission (§14.1): duplicates, dependencies, the sequence
14
+ * check, and holding a change whose actor and sequence another change
15
+ * has (POST-001).
16
+ *
17
+ * It knows no profile semantics: a profile's own rules (its values, §74.1)
18
+ * run after it, on admitted changes only. `@openlfcp/shared-objects/admission`.
19
+ */
20
+ export { checkChangeActor, deriveDomainActorId } from "./actor.js";
21
+ export { checkCanonicalChange } from "./canonical.js";
22
+ export { checkChange, checkSaveHeader, FRAMING_VERSION, frameChange, frameProfilePayload, frameSnapshot, parsedOf, refusedChangeHash, unframeChange, unframeProfilePayload, unframeSnapshot, } from "./framing.js";
23
+ export { CHANGE_LIMITS, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, } from "./limits.js";
24
+ export { ReferenceHistory } from "./references.js";
25
+ export { admitBatch, admitChange, } from "./sequence.js";
26
+ //# sourceMappingURL=index.js.map
@@ -1,3 +1,4 @@
1
+ import { ProfileInvalidError } from "../profile-invalid.js";
1
2
  /** The exact limits of a change (§11.1): writers stay within them, receivers reject above. */
2
3
  export declare const CHANGE_LIMITS: Readonly<{
3
4
  maxRows: 16384;
@@ -42,6 +43,15 @@ export interface ChunkExpansion {
42
43
  /** A Snapshot's deepest object (§11.2); 0 for a change (its depths depend on the document). */
43
44
  readonly maxDepth: number;
44
45
  }
46
+ /**
47
+ * A chunk refused because it cannot be read as one: no magic bytes, a
48
+ * length that does not cover it exactly, a truncated number or field. A
49
+ * chunk refused for anything else (a limit, a deflated or duplicate column,
50
+ * an out-of-range value) was read in full and still names its change hash.
51
+ */
52
+ export declare class MalformedChunkError extends ProfileInvalidError {
53
+ constructor(why: string);
54
+ }
45
55
  /**
46
56
  * §11.1: checks an uncompressed change chunk (type 1) against the exact
47
57
  * change limits before the engine sees it. Throws PROFILE_INVALID /
@@ -55,4 +65,4 @@ export declare function checkChangeExpansion(bytes: Uint8Array): ChunkExpansion;
55
65
  * INVALID_AUTOMERGE_BYTES.
56
66
  */
57
67
  export declare function checkSnapshotExpansion(bytes: Uint8Array, limits?: SnapshotLimits): ChunkExpansion;
58
- //# sourceMappingURL=chunk-limits.d.ts.map
68
+ //# sourceMappingURL=limits.d.ts.map
@@ -22,7 +22,7 @@
22
22
  // actors);
23
23
  // - for a Snapshot, the bytes of all column data after inflation.
24
24
  import { Inflate } from "fflate";
25
- import { ProfileInvalidError } from "./profile-invalid.js";
25
+ import { ProfileInvalidError } from "../profile-invalid.js";
26
26
  /** The exact limits of a change (§11.1): writers stay within them, receivers reject above. */
27
27
  export const CHANGE_LIMITS = Object.freeze({
28
28
  maxRows: 16_384,
@@ -63,6 +63,21 @@ const INFLATE_SLICE = 1024;
63
63
  const refuse = (why) => {
64
64
  throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `Automerge chunk refused (§11.1): ${why}`);
65
65
  };
66
+ /**
67
+ * A chunk refused because it cannot be read as one: no magic bytes, a
68
+ * length that does not cover it exactly, a truncated number or field. A
69
+ * chunk refused for anything else (a limit, a deflated or duplicate column,
70
+ * an out-of-range value) was read in full and still names its change hash.
71
+ */
72
+ export class MalformedChunkError extends ProfileInvalidError {
73
+ constructor(why) {
74
+ super("INVALID_AUTOMERGE_BYTES", `Automerge chunk refused (§11.1): ${why}`);
75
+ this.name = "MalformedChunkError";
76
+ }
77
+ }
78
+ const malformed = (why) => {
79
+ throw new MalformedChunkError(why);
80
+ };
66
81
  class Cursor {
67
82
  bytes;
68
83
  end;
@@ -76,7 +91,7 @@ class Cursor {
76
91
  }
77
92
  take(n) {
78
93
  if (n > this.end - this.pos)
79
- refuse("a length runs past the end");
94
+ malformed("a length runs past the end");
80
95
  const out = this.bytes.subarray(this.pos, this.pos + n);
81
96
  this.pos += n;
82
97
  return out;
@@ -87,14 +102,14 @@ class Cursor {
87
102
  let scale = 1;
88
103
  for (let i = 0; i < 10; i++) {
89
104
  if (this.pos >= this.end)
90
- refuse("a LEB128 number runs past the end");
105
+ malformed("a LEB128 number runs past the end");
91
106
  const b = this.bytes[this.pos++];
92
107
  value += (b & 0x7f) * scale;
93
108
  if ((b & 0x80) === 0)
94
109
  return value >= 2 ** 53 ? Number.POSITIVE_INFINITY : value;
95
110
  scale *= 128;
96
111
  }
97
- return refuse("a LEB128 number is longer than 10 bytes");
112
+ return malformed("a LEB128 number is longer than 10 bytes");
98
113
  }
99
114
  /** Signed LEB128 as a number; magnitudes at or above 2^53 come back as +/-Infinity. */
100
115
  sleb() {
@@ -102,7 +117,7 @@ class Cursor {
102
117
  let scale = 1;
103
118
  for (let i = 0; i < 10; i++) {
104
119
  if (this.pos >= this.end)
105
- refuse("a LEB128 number runs past the end");
120
+ malformed("a LEB128 number runs past the end");
106
121
  const b = this.bytes[this.pos++];
107
122
  value += (b & 0x7f) * scale;
108
123
  scale *= 128;
@@ -112,7 +127,7 @@ class Cursor {
112
127
  return Math.abs(value) >= 2 ** 53 ? Math.sign(value) * Number.POSITIVE_INFINITY : value;
113
128
  }
114
129
  }
115
- return refuse("a LEB128 number is longer than 10 bytes");
130
+ return malformed("a LEB128 number is longer than 10 bytes");
116
131
  }
117
132
  }
118
133
  class Tally {
@@ -199,7 +214,7 @@ function countColumn(type, data, tally, bounds) {
199
214
  function columnMetas(c) {
200
215
  const count = c.uleb();
201
216
  if (count > (c.end - c.pos) / 2)
202
- refuse("the column metadata runs past the end");
217
+ malformed("the column metadata runs past the end");
203
218
  const out = [];
204
219
  const specs = new Set();
205
220
  for (let i = 0; i < count; i++) {
@@ -236,14 +251,14 @@ function body(bytes, want, what) {
236
251
  const c = new Cursor(bytes);
237
252
  const magic = c.take(4);
238
253
  if (!MAGIC.every((b, i) => magic[i] === b))
239
- refuse(`${what} has no Automerge magic bytes`);
254
+ malformed(`${what} has no Automerge magic bytes`);
240
255
  c.take(4); // checksum: verified elsewhere (checkChange; the engine for a Snapshot)
241
256
  const type = c.take(1)[0];
242
257
  if (type !== want)
243
258
  refuse(`${what} is chunk type ${type}, not ${want}`);
244
259
  const length = c.uleb();
245
260
  if (length !== bytes.length - c.pos)
246
- refuse(`${what} must be exactly one chunk with nothing after it`);
261
+ malformed(`${what} must be exactly one chunk with nothing after it`);
247
262
  return c;
248
263
  }
249
264
  /**
@@ -454,4 +469,4 @@ export function checkSnapshotExpansion(bytes, limits = SNAPSHOT_LIMITS_FLOOR) {
454
469
  maxDepth,
455
470
  });
456
471
  }
457
- //# sourceMappingURL=chunk-limits.js.map
472
+ //# sourceMappingURL=limits.js.map
@@ -1,7 +1,7 @@
1
1
  import * as A from "@automerge/automerge";
2
2
  import { type ParsedChange } from "./canonical.js";
3
3
  /** A §11.4 rule a change breaks. */
4
- export type ReferenceRule = "R1" | "R2" | "R3" | "R4" | "R5" | "R6" | "R7" | "R10";
4
+ export type ReferenceRule = "R1" | "R2" | "R3" | "R4" | "R5" | "R6" | "R7" | "R8" | "R9" | "R10";
5
5
  /**
6
6
  * The changes of a document as §11.4 reads them. A replica keeps it current
7
7
  * as changes are admitted and committed, or builds it again from its
@@ -31,6 +31,7 @@ const targetOf = (op, id) => ({
31
31
  insert: op.insert,
32
32
  del: op.action === 3,
33
33
  make: MAKE.has(op.action) ? op.action : null,
34
+ counterPut: op.action === 1 && op.valueType === 8,
34
35
  });
35
36
  function splitId(id) {
36
37
  const at = id.indexOf("@");
@@ -137,9 +138,10 @@ export class ReferenceHistory {
137
138
  };
138
139
  for (const [i, op] of change.ops.entries()) {
139
140
  const id = `${change.startOp + i}@${change.actor}`;
140
- // R10: no operation makes a table. Automerge JS 3.5.0 cannot write a
141
- // table back out and aborts applying one (differential fuzzing D1); a
142
- // writer of this profile never makes one.
141
+ // R9: no operation is a mark. R10: no operation makes a table (an engine
142
+ // cannot write a table back out, and writing into one aborts the apply).
143
+ if (op.action === 7)
144
+ return "R9";
143
145
  if (op.action === 6)
144
146
  return "R10";
145
147
  // R3: the root, or an object an operation made; its key form.
@@ -176,6 +178,10 @@ export class ReferenceHistory {
176
178
  // R7: a deletion has a predecessor.
177
179
  if (op.action === 3 && op.pred.length === 0)
178
180
  return "R7";
181
+ // R8: an increment names at least one put of a counter, and only such puts (R6 put
182
+ // them on the same object and key).
183
+ if (op.action === 5 && (op.pred.length === 0 || op.pred.some((p) => !target(p)?.counterPut)))
184
+ return "R8";
179
185
  own.push(targetOf(op, id));
180
186
  }
181
187
  return null;