@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/dist/values.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { fromBase64url, ID32_LENGTH, LfcpError, principalId, toBase64url, } from "@openlfcp/core";
|
|
2
|
-
import {
|
|
3
|
-
import { ProfileInvalidError } from "./profile-invalid.js";
|
|
2
|
+
import { deriveDomainActorId } from "./admission/actor.js";
|
|
4
3
|
/**
|
|
5
4
|
* Value-level rules of SHARED-OBJECTS-PROFILE-01: Principal references,
|
|
6
5
|
* Local Dates, timestamps, reverse-domain names, namespaced values, the
|
|
@@ -60,104 +59,17 @@ const NAMESPACED = new RegExp(`^x/${LABEL}(?:\\.${LABEL})+/[^/]+$`, "u");
|
|
|
60
59
|
export const isReverseDomain = (text) => typeof text === "string" && REVERSE_DOMAIN.test(text);
|
|
61
60
|
/** §33 ABNF namespaced-value: x/<reverse-domain>/<value>, value non-empty without "/". */
|
|
62
61
|
export const isNamespacedValue = (text) => typeof text === "string" && NAMESPACED.test(text);
|
|
63
|
-
|
|
62
|
+
/** §8: the actor domain of this profile. */
|
|
63
|
+
export const ACTOR_DOMAIN = "OPENLFCP-SHARED-OBJECTS-ACTOR-v1";
|
|
64
64
|
/**
|
|
65
65
|
* §8: the Automerge actor ID of `principal` editing `resource`:
|
|
66
66
|
* SHA-256(ASCII("OPENLFCP-SHARED-OBJECTS-ACTOR-v1") || resource_id || principal_id),
|
|
67
67
|
* from the raw 32-byte IDs.
|
|
68
68
|
*/
|
|
69
69
|
export function deriveActorId(resource, principal) {
|
|
70
|
-
|
|
71
|
-
["the Resource ID", resource],
|
|
72
|
-
["the Principal ID", principal],
|
|
73
|
-
]) {
|
|
74
|
-
if (!(id instanceof Uint8Array) || id.length !== ID32_LENGTH)
|
|
75
|
-
throw new LfcpError("INVALID_LENGTH", `${what} must be the raw 32-byte ID`);
|
|
76
|
-
}
|
|
77
|
-
const input = new Uint8Array(ACTOR_DOMAIN.length + 2 * ID32_LENGTH);
|
|
78
|
-
input.set(ACTOR_DOMAIN, 0);
|
|
79
|
-
input.set(resource, ACTOR_DOMAIN.length);
|
|
80
|
-
input.set(principal, ACTOR_DOMAIN.length + ID32_LENGTH);
|
|
81
|
-
return sha256(input);
|
|
82
|
-
}
|
|
83
|
-
/** §11, §13: the only framing version. */
|
|
84
|
-
export const FRAMING_VERSION = 1;
|
|
85
|
-
/** The deterministic CBOR head of a byte string of `length` bytes (major type 2, shortest form). */
|
|
86
|
-
function bstrHead(length) {
|
|
87
|
-
if (length < 24)
|
|
88
|
-
return [0x40 | length];
|
|
89
|
-
if (length < 0x100)
|
|
90
|
-
return [0x58, length];
|
|
91
|
-
if (length < 0x10000)
|
|
92
|
-
return [0x59, length >> 8, length & 0xff];
|
|
93
|
-
if (length <= 0xffffffff)
|
|
94
|
-
return [
|
|
95
|
-
0x5a,
|
|
96
|
-
(length >>> 24) & 0xff,
|
|
97
|
-
(length >>> 16) & 0xff,
|
|
98
|
-
(length >>> 8) & 0xff,
|
|
99
|
-
length & 0xff,
|
|
100
|
-
];
|
|
101
|
-
throw new LfcpError("OUT_OF_RANGE", "the framed bytes are too long");
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Deterministic CBOR of [1, bstr] (§11 shared-objects-change, §13
|
|
105
|
-
* shared-objects-snapshot): the profile plaintext of a Data Unit (one
|
|
106
|
-
* Automerge change) or a Snapshot (one Automerge full save). The bytes are
|
|
107
|
-
* framed as given; checking that they are a valid Automerge change or save
|
|
108
|
-
* is the Automerge binding (LFCP-031).
|
|
109
|
-
*/
|
|
110
|
-
export function frameProfilePayload(bytes) {
|
|
111
|
-
if (!(bytes instanceof Uint8Array))
|
|
112
|
-
throw new LfcpError("INVALID_LENGTH", "framing takes bytes");
|
|
113
|
-
const head = bstrHead(bytes.length);
|
|
114
|
-
const out = new Uint8Array(2 + head.length + bytes.length);
|
|
115
|
-
out.set([0x82, FRAMING_VERSION, ...head], 0);
|
|
116
|
-
out.set(bytes, 2 + head.length);
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* The inner bytes of a framed profile plaintext. §11: a receiver MUST
|
|
121
|
-
* reject plaintext that is not valid CBOR, not a two-element array, or
|
|
122
|
-
* uses an unsupported framing version; this also requires the
|
|
123
|
-
* deterministic encoding and no trailing bytes. Throws PROFILE_INVALID /
|
|
124
|
-
* INVALID_AUTOMERGE_BYTES (§74.1).
|
|
125
|
-
*/
|
|
126
|
-
export function unframeProfilePayload(framed) {
|
|
127
|
-
const fail = (why) => {
|
|
128
|
-
throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `invalid profile framing: ${why}`);
|
|
129
|
-
};
|
|
130
|
-
if (!(framed instanceof Uint8Array) || framed.length < 3)
|
|
131
|
-
return fail("too short");
|
|
132
|
-
if (framed[0] !== 0x82)
|
|
133
|
-
return fail("not a two-element array");
|
|
134
|
-
if (framed[1] !== FRAMING_VERSION)
|
|
135
|
-
return fail("unsupported framing version");
|
|
136
|
-
const first = framed[2];
|
|
137
|
-
if (first >> 5 !== 2)
|
|
138
|
-
return fail("the second element is not a byte string");
|
|
139
|
-
const info = first & 0x1f;
|
|
140
|
-
let length;
|
|
141
|
-
let offset;
|
|
142
|
-
const at = (i) => framed[i];
|
|
143
|
-
if (info < 24)
|
|
144
|
-
[length, offset] = [info, 3];
|
|
145
|
-
else if (info === 24)
|
|
146
|
-
[length, offset] = [at(3), 4];
|
|
147
|
-
else if (info === 25)
|
|
148
|
-
[length, offset] = [(at(3) << 8) | at(4), 5];
|
|
149
|
-
else if (info === 26)
|
|
150
|
-
[length, offset] = [((at(3) << 24) >>> 0) + (at(4) << 16) + (at(5) << 8) + at(6), 7];
|
|
151
|
-
else
|
|
152
|
-
return fail("unsupported byte string length");
|
|
153
|
-
if (offset > framed.length)
|
|
154
|
-
return fail("truncated");
|
|
155
|
-
const bytes = framed.subarray(offset);
|
|
156
|
-
if (bytes.length !== length)
|
|
157
|
-
return fail(bytes.length < length ? "truncated" : "trailing bytes");
|
|
158
|
-
const canonical = bstrHead(length);
|
|
159
|
-
if (canonical.length !== offset - 2)
|
|
160
|
-
return fail("non-shortest byte string length");
|
|
161
|
-
return Uint8Array.from(bytes);
|
|
70
|
+
return deriveDomainActorId(ACTOR_DOMAIN, resource, principal);
|
|
162
71
|
}
|
|
72
|
+
// §11, §13: the profile framing [1, bytes] is part of the admission module
|
|
73
|
+
// both profiles share (./admission/framing.ts); re-exported here unchanged.
|
|
74
|
+
export { FRAMING_VERSION, frameProfilePayload, unframeProfilePayload, } from "./admission/framing.js";
|
|
163
75
|
//# sourceMappingURL=values.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlfcp/shared-objects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.10",
|
|
4
4
|
"description": "SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1) implementation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openlfcp",
|
|
@@ -31,6 +31,14 @@
|
|
|
31
31
|
".": {
|
|
32
32
|
"types": "./dist/index.d.ts",
|
|
33
33
|
"import": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./admission": {
|
|
36
|
+
"types": "./dist/admission/index.d.ts",
|
|
37
|
+
"import": "./dist/admission/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./sections": {
|
|
40
|
+
"types": "./dist/sections/index.d.ts",
|
|
41
|
+
"import": "./dist/sections/index.js"
|
|
34
42
|
}
|
|
35
43
|
},
|
|
36
44
|
"files": [
|
|
@@ -40,13 +48,13 @@
|
|
|
40
48
|
],
|
|
41
49
|
"publishConfig": {
|
|
42
50
|
"access": "public",
|
|
43
|
-
"tag": "
|
|
51
|
+
"tag": "beta"
|
|
44
52
|
},
|
|
45
53
|
"dependencies": {
|
|
46
54
|
"@automerge/automerge": "3.5.0",
|
|
47
55
|
"fflate": "0.8.3",
|
|
48
|
-
"@openlfcp/
|
|
49
|
-
"@openlfcp/
|
|
56
|
+
"@openlfcp/crypto": "^0.2.0-beta.10",
|
|
57
|
+
"@openlfcp/core": "^0.2.0-beta.10"
|
|
50
58
|
},
|
|
51
59
|
"devDependencies": {
|
|
52
60
|
"fast-check": "4.10.2"
|
package/dist/automerge-bytes.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import * as A from "@automerge/automerge";
|
|
2
|
-
import { toHex } from "@openlfcp/core";
|
|
3
|
-
import { checkCanonicalChange } from "./canonical.js";
|
|
4
|
-
import { checkChangeExpansion } from "./chunk-limits.js";
|
|
5
|
-
import { ProfileInvalidError } from "./profile-invalid.js";
|
|
6
|
-
import { frameProfilePayload, unframeProfilePayload } from "./values.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
|
-
PARSED.set(checked, parsed);
|
|
68
|
-
return checked;
|
|
69
|
-
}
|
|
70
|
-
/** The operations checkChange read from the bytes (§11.3), for the references of §11.4. */
|
|
71
|
-
const PARSED = new WeakMap();
|
|
72
|
-
/** The change as its canonical bytes give it (§11.3), reusing the walk of checkChange. */
|
|
73
|
-
export function parsedOf(change) {
|
|
74
|
-
let p = PARSED.get(change);
|
|
75
|
-
if (p === undefined) {
|
|
76
|
-
p = checkCanonicalChange(change.bytes);
|
|
77
|
-
PARSED.set(change, p);
|
|
78
|
-
}
|
|
79
|
-
return p;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Checks that `bytes` starts as an Automerge full save (a document chunk,
|
|
83
|
-
* §13). Whether the whole image loads, checksums included, is decided by
|
|
84
|
-
* Automerge's load.
|
|
85
|
-
*/
|
|
86
|
-
export function checkSaveHeader(bytes) {
|
|
87
|
-
const type = chunkType(bytes, "the Snapshot image");
|
|
88
|
-
if (type !== CHUNK_DOCUMENT)
|
|
89
|
-
reject(`chunk type ${type} is not an Automerge document (full save, §13)`);
|
|
90
|
-
}
|
|
91
|
-
/** §11: the Data Unit plaintext [1, change] of one checked Automerge change. */
|
|
92
|
-
export function frameChange(change) {
|
|
93
|
-
checkChange(change);
|
|
94
|
-
return frameProfilePayload(change);
|
|
95
|
-
}
|
|
96
|
-
/** §11: the one Automerge change a Data Unit plaintext carries, checked. Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
|
|
97
|
-
export function unframeChange(plaintext) {
|
|
98
|
-
return checkChange(unframeProfilePayload(plaintext));
|
|
99
|
-
}
|
|
100
|
-
/** §13: the Snapshot plaintext [1, save] of an Automerge full save. */
|
|
101
|
-
export function frameSnapshot(save) {
|
|
102
|
-
checkSaveHeader(save);
|
|
103
|
-
return frameProfilePayload(save);
|
|
104
|
-
}
|
|
105
|
-
/** §13: the Automerge full save a Snapshot plaintext carries (header checked). Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
|
|
106
|
-
export function unframeSnapshot(plaintext) {
|
|
107
|
-
const save = unframeProfilePayload(plaintext);
|
|
108
|
-
checkSaveHeader(save);
|
|
109
|
-
return save;
|
|
110
|
-
}
|
|
111
|
-
//# sourceMappingURL=automerge-bytes.js.map
|