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

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 (45) 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} +9 -0
  5. package/dist/{canonical.js → admission/canonical.js} +43 -3
  6. package/dist/{automerge-bytes.d.ts → admission/framing.d.ts} +41 -1
  7. package/dist/admission/framing.js +242 -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 +147 -0
  16. package/dist/concurrent.d.ts +15 -0
  17. package/dist/concurrent.js +55 -0
  18. package/dist/data-profile.d.ts +1 -1
  19. package/dist/data-profile.js +3 -7
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +2 -2
  22. package/dist/replica.d.ts +32 -2
  23. package/dist/replica.js +165 -209
  24. package/dist/sections/admission.d.ts +35 -0
  25. package/dist/sections/admission.js +328 -0
  26. package/dist/sections/data-profile.d.ts +195 -0
  27. package/dist/sections/data-profile.js +383 -0
  28. package/dist/sections/dispatch.d.ts +41 -0
  29. package/dist/sections/dispatch.js +33 -0
  30. package/dist/sections/document.d.ts +54 -0
  31. package/dist/sections/document.js +101 -0
  32. package/dist/sections/index.d.ts +18 -0
  33. package/dist/sections/index.js +18 -0
  34. package/dist/sections/replica.d.ts +406 -0
  35. package/dist/sections/replica.js +1227 -0
  36. package/dist/sections/schema.d.ts +77 -0
  37. package/dist/sections/schema.js +548 -0
  38. package/dist/sections/tree.d.ts +83 -0
  39. package/dist/sections/tree.js +401 -0
  40. package/dist/sections/values.d.ts +30 -0
  41. package/dist/sections/values.js +36 -0
  42. package/dist/values.d.ts +3 -18
  43. package/dist/values.js +7 -95
  44. package/package.json +12 -4
  45. 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 {
@@ -28,6 +30,13 @@ export interface ParsedChange {
28
30
  /** Hex other actors, ascending: actor index i is otherActors[i - 1]. */
29
31
  readonly otherActors: readonly string[];
30
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;
31
40
  }
32
41
  /**
33
42
  * §11.3: checks that `bytes` (one change chunk within the §11.1 limits) is
@@ -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
  };
@@ -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);
@@ -575,7 +607,14 @@ export function checkCanonicalChange(bytes) {
575
607
  pred.push(`${ctr}@${id}`);
576
608
  anyPred = true;
577
609
  }
578
- ops.push(Object.freeze({ obj, key, insert: ins, action: act, pred: Object.freeze(pred) }));
610
+ ops.push(Object.freeze({
611
+ obj,
612
+ key,
613
+ insert: ins,
614
+ action: act,
615
+ pred: Object.freeze(pred),
616
+ valueType: valueTypes[i],
617
+ }));
579
618
  }
580
619
  // Rule 3: each column present exactly when the table says.
581
620
  const presence = [
@@ -608,6 +647,7 @@ export function checkCanonicalChange(bytes) {
608
647
  deps: Object.freeze(deps.map(hex)),
609
648
  otherActors: Object.freeze(others.map(hex)),
610
649
  ops: Object.freeze(ops),
650
+ beginsWithAuthor: authored,
611
651
  });
612
652
  }
613
653
  //# sourceMappingURL=canonical.js.map
@@ -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 {
@@ -11,6 +12,12 @@ export interface CheckedChange {
11
12
  readonly deps: readonly string[];
12
13
  /** Hex IDs of the other actors the change refers to (§11.1: each must be known to the document). */
13
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;
14
21
  }
15
22
  /**
16
23
  * Checks that `bytes` is exactly one valid Automerge change (§11, §12) and
@@ -21,8 +28,23 @@ export interface CheckedChange {
21
28
  * a change without verifying its checksum, so the check is done here.
22
29
  */
23
30
  export declare function checkChange(bytes: Uint8Array): CheckedChange;
31
+ /**
32
+ * The change hash of bytes checkChange refused, read without decoding them:
33
+ * the SHA-256 of the chunk from its type byte on (the Automerge change hash).
34
+ * §14.1 names a refused change by this hash whenever the bytes are one change
35
+ * chunk of type 1 whose length field covers them exactly and whose checksum
36
+ * matches — the inner header is never read, so a change above a §11.1 limit
37
+ * (named without being expanded) and one whose header holds a length running
38
+ * past its end (finding C3) are both named. Undefined when the bytes are not
39
+ * such a chunk: no magic bytes, another chunk type (a compressed chunk
40
+ * included), a length that does not cover them exactly, or a wrong checksum
41
+ * (whose name would be its intact copy's, finding C1).
42
+ */
43
+ export declare function refusedChangeHash(bytes: Uint8Array): string | undefined;
24
44
  /** The change as its canonical bytes give it (§11.3), reusing the walk of checkChange. */
25
45
  export declare function parsedOf(change: CheckedChange): ParsedChange;
46
+ /** The decoded change, reusing the decode of checkChange when there was one. */
47
+ export declare function decodedOf(change: CheckedChange): A.DecodedChange;
26
48
  /**
27
49
  * Checks that `bytes` starts as an Automerge full save (a document chunk,
28
50
  * §13). Whether the whole image loads, checksums included, is decided by
@@ -37,4 +59,22 @@ export declare function unframeChange(plaintext: Uint8Array): CheckedChange;
37
59
  export declare function frameSnapshot(save: Uint8Array): Uint8Array;
38
60
  /** §13: the Automerge full save a Snapshot plaintext carries (header checked). Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
39
61
  export declare function unframeSnapshot(plaintext: Uint8Array): Uint8Array;
40
- //# sourceMappingURL=automerge-bytes.d.ts.map
62
+ /** §11, §13: the only framing version. */
63
+ export declare const FRAMING_VERSION = 1;
64
+ /**
65
+ * Deterministic CBOR of [1, bstr] (§11 shared-objects-change, §13
66
+ * shared-objects-snapshot): the profile plaintext of a Data Unit (one
67
+ * Automerge change) or a Snapshot (one Automerge full save). The bytes are
68
+ * framed as given; checking that they are a valid Automerge change or save
69
+ * is the Automerge binding (LFCP-031).
70
+ */
71
+ export declare function frameProfilePayload(bytes: Uint8Array): Uint8Array;
72
+ /**
73
+ * The inner bytes of a framed profile plaintext. §11: a receiver MUST
74
+ * reject plaintext that is not valid CBOR, not a two-element array, or
75
+ * uses an unsupported framing version; this also requires the
76
+ * deterministic encoding and no trailing bytes. Throws PROFILE_INVALID /
77
+ * INVALID_AUTOMERGE_BYTES (§74.1).
78
+ */
79
+ export declare function unframeProfilePayload(framed: Uint8Array): Uint8Array;
80
+ //# sourceMappingURL=framing.d.ts.map
@@ -0,0 +1,242 @@
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 } 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
+ beginsWithAuthor: parsed.beginsWithAuthor,
67
+ });
68
+ DECODED.set(checked, decoded);
69
+ PARSED.set(checked, parsed);
70
+ return checked;
71
+ }
72
+ /**
73
+ * The change hash of bytes checkChange refused, read without decoding them:
74
+ * the SHA-256 of the chunk from its type byte on (the Automerge change hash).
75
+ * §14.1 names a refused change by this hash whenever the bytes are one change
76
+ * chunk of type 1 whose length field covers them exactly and whose checksum
77
+ * matches — the inner header is never read, so a change above a §11.1 limit
78
+ * (named without being expanded) and one whose header holds a length running
79
+ * past its end (finding C3) are both named. Undefined when the bytes are not
80
+ * such a chunk: no magic bytes, another chunk type (a compressed chunk
81
+ * included), a length that does not cover them exactly, or a wrong checksum
82
+ * (whose name would be its intact copy's, finding C1).
83
+ */
84
+ export function refusedChangeHash(bytes) {
85
+ if (bytes.length < HEADER || MAGIC.some((b, i) => bytes[i] !== b))
86
+ return undefined;
87
+ if (bytes[8] !== CHUNK_CHANGE)
88
+ return undefined;
89
+ // The ULEB128 chunk length after the type byte must cover the rest exactly.
90
+ let length = 0;
91
+ let scale = 1;
92
+ let pos = HEADER;
93
+ for (;;) {
94
+ if (pos >= bytes.length || pos - HEADER >= 8)
95
+ return undefined;
96
+ const b = bytes[pos++];
97
+ length += (b & 0x7f) * scale;
98
+ scale *= 128;
99
+ if ((b & 0x80) === 0)
100
+ break;
101
+ }
102
+ if (pos + length !== bytes.length)
103
+ return undefined;
104
+ // §14.1 (baseline.7, finding C1): name the bytes only when their checksum
105
+ // matches. The name is the SHA-256 of the chunk from its type byte on, and
106
+ // the checksum (bytes 4 to 8) is the first four bytes of that same hash; a
107
+ // chunk with a wrong checksum would be named by its intact copy's hash, so
108
+ // it is left unnamed and the intact copy is checked as any change is.
109
+ const hash = sha256(bytes.subarray(8));
110
+ for (let i = 0; i < 4; i++)
111
+ if (bytes[4 + i] !== hash[i])
112
+ return undefined;
113
+ return toHex(hash);
114
+ }
115
+ /** The operations checkChange read from the bytes (§11.3), for the references of §11.4. */
116
+ const PARSED = new WeakMap();
117
+ /** The change as its canonical bytes give it (§11.3), reusing the walk of checkChange. */
118
+ export function parsedOf(change) {
119
+ let p = PARSED.get(change);
120
+ if (p === undefined) {
121
+ p = checkCanonicalChange(change.bytes);
122
+ PARSED.set(change, p);
123
+ }
124
+ return p;
125
+ }
126
+ /** The decoded form checkChange made, kept for the change's lifetime (decoding is not cheap). */
127
+ const DECODED = new WeakMap();
128
+ /** The decoded change, reusing the decode of checkChange when there was one. */
129
+ export function decodedOf(change) {
130
+ return DECODED.get(change) ?? A.decodeChange(change.bytes);
131
+ }
132
+ /**
133
+ * Checks that `bytes` starts as an Automerge full save (a document chunk,
134
+ * §13). Whether the whole image loads, checksums included, is decided by
135
+ * Automerge's load.
136
+ */
137
+ export function checkSaveHeader(bytes) {
138
+ const type = chunkType(bytes, "the Snapshot image");
139
+ if (type !== CHUNK_DOCUMENT)
140
+ reject(`chunk type ${type} is not an Automerge document (full save, §13)`);
141
+ }
142
+ /** §11: the Data Unit plaintext [1, change] of one checked Automerge change. */
143
+ export function frameChange(change) {
144
+ checkChange(change);
145
+ return frameProfilePayload(change);
146
+ }
147
+ /** §11: the one Automerge change a Data Unit plaintext carries, checked. Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
148
+ export function unframeChange(plaintext) {
149
+ return checkChange(unframeProfilePayload(plaintext));
150
+ }
151
+ /** §13: the Snapshot plaintext [1, save] of an Automerge full save. */
152
+ export function frameSnapshot(save) {
153
+ checkSaveHeader(save);
154
+ return frameProfilePayload(save);
155
+ }
156
+ /** §13: the Automerge full save a Snapshot plaintext carries (header checked). Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
157
+ export function unframeSnapshot(plaintext) {
158
+ const save = unframeProfilePayload(plaintext);
159
+ checkSaveHeader(save);
160
+ return save;
161
+ }
162
+ /** §11, §13: the only framing version. */
163
+ export const FRAMING_VERSION = 1;
164
+ /** The deterministic CBOR head of a byte string of `length` bytes (major type 2, shortest form). */
165
+ function bstrHead(length) {
166
+ if (length < 24)
167
+ return [0x40 | length];
168
+ if (length < 0x100)
169
+ return [0x58, length];
170
+ if (length < 0x10000)
171
+ return [0x59, length >> 8, length & 0xff];
172
+ if (length <= 0xffffffff)
173
+ return [
174
+ 0x5a,
175
+ (length >>> 24) & 0xff,
176
+ (length >>> 16) & 0xff,
177
+ (length >>> 8) & 0xff,
178
+ length & 0xff,
179
+ ];
180
+ throw new LfcpError("OUT_OF_RANGE", "the framed bytes are too long");
181
+ }
182
+ /**
183
+ * Deterministic CBOR of [1, bstr] (§11 shared-objects-change, §13
184
+ * shared-objects-snapshot): the profile plaintext of a Data Unit (one
185
+ * Automerge change) or a Snapshot (one Automerge full save). The bytes are
186
+ * framed as given; checking that they are a valid Automerge change or save
187
+ * is the Automerge binding (LFCP-031).
188
+ */
189
+ export function frameProfilePayload(bytes) {
190
+ if (!(bytes instanceof Uint8Array))
191
+ throw new LfcpError("INVALID_LENGTH", "framing takes bytes");
192
+ const head = bstrHead(bytes.length);
193
+ const out = new Uint8Array(2 + head.length + bytes.length);
194
+ out.set([0x82, FRAMING_VERSION, ...head], 0);
195
+ out.set(bytes, 2 + head.length);
196
+ return out;
197
+ }
198
+ /**
199
+ * The inner bytes of a framed profile plaintext. §11: a receiver MUST
200
+ * reject plaintext that is not valid CBOR, not a two-element array, or
201
+ * uses an unsupported framing version; this also requires the
202
+ * deterministic encoding and no trailing bytes. Throws PROFILE_INVALID /
203
+ * INVALID_AUTOMERGE_BYTES (§74.1).
204
+ */
205
+ export function unframeProfilePayload(framed) {
206
+ const fail = (why) => {
207
+ throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `invalid profile framing: ${why}`);
208
+ };
209
+ if (!(framed instanceof Uint8Array) || framed.length < 3)
210
+ return fail("too short");
211
+ if (framed[0] !== 0x82)
212
+ return fail("not a two-element array");
213
+ if (framed[1] !== FRAMING_VERSION)
214
+ return fail("unsupported framing version");
215
+ const first = framed[2];
216
+ if (first >> 5 !== 2)
217
+ return fail("the second element is not a byte string");
218
+ const info = first & 0x1f;
219
+ let length;
220
+ let offset;
221
+ const at = (i) => framed[i];
222
+ if (info < 24)
223
+ [length, offset] = [info, 3];
224
+ else if (info === 24)
225
+ [length, offset] = [at(3), 4];
226
+ else if (info === 25)
227
+ [length, offset] = [(at(3) << 8) | at(4), 5];
228
+ else if (info === 26)
229
+ [length, offset] = [((at(3) << 24) >>> 0) + (at(4) << 16) + (at(5) << 8) + at(6), 7];
230
+ else
231
+ return fail("unsupported byte string length");
232
+ if (offset > framed.length)
233
+ return fail("truncated");
234
+ const bytes = framed.subarray(offset);
235
+ if (bytes.length !== length)
236
+ return fail(bytes.length < length ? "truncated" : "trailing bytes");
237
+ const canonical = bstrHead(length);
238
+ if (canonical.length !== offset - 2)
239
+ return fail("non-shortest byte string length");
240
+ return Uint8Array.from(bytes);
241
+ }
242
+ //# 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