@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.
- package/README.md +15 -2
- package/dist/admission/actor.d.ts +16 -0
- package/dist/admission/actor.js +35 -0
- package/dist/{canonical.d.ts → admission/canonical.d.ts} +9 -0
- package/dist/{canonical.js → admission/canonical.js} +43 -3
- package/dist/{automerge-bytes.d.ts → admission/framing.d.ts} +41 -1
- package/dist/admission/framing.js +242 -0
- package/dist/admission/index.d.ts +26 -0
- package/dist/admission/index.js +26 -0
- package/dist/{chunk-limits.d.ts → admission/limits.d.ts} +11 -1
- package/dist/{chunk-limits.js → admission/limits.js} +25 -10
- package/dist/{references.d.ts → admission/references.d.ts} +1 -1
- package/dist/{references.js → admission/references.js} +9 -3
- package/dist/admission/sequence.d.ts +81 -0
- package/dist/admission/sequence.js +147 -0
- package/dist/concurrent.d.ts +15 -0
- package/dist/concurrent.js +55 -0
- package/dist/data-profile.d.ts +1 -1
- package/dist/data-profile.js +3 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/replica.d.ts +32 -2
- package/dist/replica.js +165 -209
- package/dist/sections/admission.d.ts +35 -0
- package/dist/sections/admission.js +328 -0
- package/dist/sections/data-profile.d.ts +195 -0
- package/dist/sections/data-profile.js +383 -0
- package/dist/sections/dispatch.d.ts +41 -0
- package/dist/sections/dispatch.js +33 -0
- package/dist/sections/document.d.ts +54 -0
- package/dist/sections/document.js +101 -0
- package/dist/sections/index.d.ts +18 -0
- package/dist/sections/index.js +18 -0
- package/dist/sections/replica.d.ts +406 -0
- package/dist/sections/replica.js +1227 -0
- package/dist/sections/schema.d.ts +77 -0
- package/dist/sections/schema.js +548 -0
- package/dist/sections/tree.d.ts +83 -0
- package/dist/sections/tree.js +401 -0
- package/dist/sections/values.d.ts +30 -0
- package/dist/sections/values.js +36 -0
- package/dist/values.d.ts +3 -18
- package/dist/values.js +7 -95
- package/package.json +12 -4
- 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.
|
|
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.
|
|
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 "
|
|
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({
|
|
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
|
-
|
|
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=
|
|
68
|
+
//# sourceMappingURL=limits.d.ts.map
|