@openlfcp/shared-objects 0.1.4 → 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.
- 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} +2 -0
- package/dist/{canonical.js → admission/canonical.js} +9 -2
- package/dist/{automerge-bytes.d.ts → admission/framing.d.ts} +32 -1
- package/dist/admission/framing.js +231 -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} +11 -0
- package/dist/admission/sequence.d.ts +81 -0
- package/dist/admission/sequence.js +133 -0
- package/dist/data-profile.d.ts +1 -1
- package/dist/data-profile.js +3 -7
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/replica.d.ts +38 -2
- package/dist/replica.js +177 -214
- 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 +412 -0
- package/dist/sections/replica.js +1149 -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 +290 -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 {
|
|
@@ -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
|
};
|
|
@@ -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({
|
|
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
|
-
|
|
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=
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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";
|
|
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,6 +138,12 @@ export class ReferenceHistory {
|
|
|
137
138
|
};
|
|
138
139
|
for (const [i, op] of change.ops.entries()) {
|
|
139
140
|
const id = `${change.startOp + i}@${change.actor}`;
|
|
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";
|
|
145
|
+
if (op.action === 6)
|
|
146
|
+
return "R10";
|
|
140
147
|
// R3: the root, or an object an operation made; its key form.
|
|
141
148
|
let sequence;
|
|
142
149
|
if (op.obj === "_root")
|
|
@@ -171,6 +178,10 @@ export class ReferenceHistory {
|
|
|
171
178
|
// R7: a deletion has a predecessor.
|
|
172
179
|
if (op.action === 3 && op.pred.length === 0)
|
|
173
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";
|
|
174
185
|
own.push(targetOf(op, id));
|
|
175
186
|
}
|
|
176
187
|
return null;
|