@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/dist/values.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { fromBase64url, ID32_LENGTH, LfcpError, principalId, toBase64url, } from "@openlfcp/core";
2
- import { sha256 } from "@openlfcp/crypto";
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
- const ACTOR_DOMAIN = Uint8Array.from("OPENLFCP-SHARED-OBJECTS-ACTOR-v1", (c) => c.charCodeAt(0));
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
- for (const [what, id] of [
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.1.5",
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": "latest"
51
+ "tag": "beta"
44
52
  },
45
53
  "dependencies": {
46
54
  "@automerge/automerge": "3.5.0",
47
55
  "fflate": "0.8.3",
48
- "@openlfcp/core": "^0.1.5",
49
- "@openlfcp/crypto": "^0.1.5"
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"
@@ -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