@openlfcp/shared-objects 0.2.2 → 0.3.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.
@@ -0,0 +1,271 @@
1
+ import { UINT64_MAX } from "@openlfcp/core";
2
+ import { pointerToken } from "../validate.js";
3
+ import { isPrincipalRef, isReverseDomain, isUtcTimestamp, principalRef } from "../values.js";
4
+ import { IdentityReader } from "./reader.js";
5
+ import { IDENTITY_PROFILE_ID, isIdentityDescriptor, isResourceRef, } from "./values.js";
6
+ export const IDENTITY_DIAGNOSTIC_ORDER = Object.freeze([
7
+ "INVALID_ROOT",
8
+ "MISSING_REQUIRED_FIELD",
9
+ "INVALID_FIELD_TYPE",
10
+ "INVALID_RESOURCE_REF",
11
+ "INVALID_PRINCIPAL_REF",
12
+ "INVALID_DESCRIPTOR",
13
+ "INVALID_ENUM_VALUE",
14
+ "INVALID_TIMESTAMP",
15
+ "INVALID_EXTENSION_NAMESPACE",
16
+ ]);
17
+ const at = (base, key) => `${base}/${pointerToken(String(key))}`;
18
+ const uint = ([type, value]) => type === "uint" &&
19
+ ((typeof value === "number" && Number.isSafeInteger(value) && value >= 0) ||
20
+ (typeof value === "bigint" && value >= 0n && value <= UINT64_MAX));
21
+ const epochOrder = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
22
+ const textOrder = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
23
+ class Fields {
24
+ reader;
25
+ problems = new Map();
26
+ conflicts = new Set();
27
+ constructor(reader) {
28
+ this.reader = reader;
29
+ }
30
+ fail(diagnostic, pointer) {
31
+ const previous = this.problems.get(pointer);
32
+ if (previous !== undefined &&
33
+ IDENTITY_DIAGNOSTIC_ORDER.indexOf(previous.diagnostic) <=
34
+ IDENTITY_DIAGNOSTIC_ORDER.indexOf(diagnostic))
35
+ return;
36
+ this.problems.set(pointer, Object.freeze({
37
+ code: "PROFILE_INVALID",
38
+ diagnostic,
39
+ pointer,
40
+ message: `IDENTITY-PROFILE-01 §4: ${diagnostic} at ${pointer || "/"}`,
41
+ }));
42
+ }
43
+ field(obj, key, base, required, request) {
44
+ const values = this.reader.all(obj, key);
45
+ if (required && values.length === 0)
46
+ this.fail("MISSING_REQUIRED_FIELD", at(base, key));
47
+ if (request && values.length > 1)
48
+ this.conflicts.add(at(base, key));
49
+ return values;
50
+ }
51
+ finishEntry(base) {
52
+ const belongs = (p) => p === base || p.startsWith(`${base}/`);
53
+ // §4 rule 2: report invalid values first. A conflict is INVALID_FIELD_TYPE
54
+ // only when every concurrent value is valid on its own (including maps).
55
+ if ([...this.problems.keys()].some(belongs))
56
+ return;
57
+ for (const pointer of this.conflicts)
58
+ if (belongs(pointer))
59
+ this.fail("INVALID_FIELD_TYPE", pointer);
60
+ }
61
+ scalar(values, type, pointer, validate, diagnostic = "INVALID_FIELD_TYPE") {
62
+ for (const value of values) {
63
+ if (value[0] !== type)
64
+ this.fail("INVALID_FIELD_TYPE", pointer);
65
+ else if (validate !== undefined && !validate(value[1]))
66
+ this.fail(diagnostic, pointer);
67
+ }
68
+ return values[0]?.[1];
69
+ }
70
+ list(values, pointer, type, request, resource = false) {
71
+ const lists = [];
72
+ for (const [kind, id] of values) {
73
+ if (kind !== "list") {
74
+ this.fail("INVALID_FIELD_TYPE", pointer);
75
+ continue;
76
+ }
77
+ const result = [];
78
+ for (const [i, entries] of this.reader.list(String(id)).entries()) {
79
+ const pos = at(pointer, i);
80
+ if (request && entries.length > 1)
81
+ this.conflicts.add(pos);
82
+ for (const entry of entries) {
83
+ if (entry[0] !== type || (type === "uint" && !uint(entry)))
84
+ this.fail("INVALID_FIELD_TYPE", pos);
85
+ else if (resource && !isResourceRef(entry[1]))
86
+ this.fail("INVALID_RESOURCE_REF", pos);
87
+ }
88
+ result.push(entries[0]?.[1]);
89
+ }
90
+ if (resource &&
91
+ (result.length === 0 || result.length > 64 || new Set(result).size !== result.length))
92
+ this.fail("INVALID_FIELD_TYPE", pointer);
93
+ lists.push(result);
94
+ }
95
+ return lists[0] ?? [];
96
+ }
97
+ entry(obj, key, table) {
98
+ const base = at(`/${table}`, key);
99
+ const request = table === "requests";
100
+ const roster = table === "roster";
101
+ const output = {};
102
+ if (roster ? !isResourceRef(key) : !isPrincipalRef(key))
103
+ this.fail(roster ? "INVALID_RESOURCE_REF" : "INVALID_PRINCIPAL_REF", base);
104
+ const required = roster
105
+ ? ["profile", "endpoints", "coordinator"]
106
+ : request
107
+ ? ["descriptor", "pairing", "label", "kind"]
108
+ : ["label", "kind"];
109
+ const strings = roster
110
+ ? ["profile", "coordinator", "name"]
111
+ : request
112
+ ? ["descriptor", "pairing", "label", "platform", "requested_at"]
113
+ : ["label", "platform", "added_at"];
114
+ const field = (name) => this.field(obj, name, base, required.includes(name), request);
115
+ for (const name of strings) {
116
+ const values = field(name);
117
+ if (values.length === 0)
118
+ continue;
119
+ const pointer = at(base, name);
120
+ output[name] = this.scalar(values, "str", pointer);
121
+ for (const [type, value] of values) {
122
+ if (type !== "str")
123
+ continue;
124
+ if (name === "pairing" && !isPrincipalRef(value))
125
+ this.fail("INVALID_PRINCIPAL_REF", pointer);
126
+ if (name === "descriptor" && !isIdentityDescriptor(String(value), key))
127
+ this.fail("INVALID_DESCRIPTOR", pointer);
128
+ if ((name === "added_at" || name === "requested_at") && !isUtcTimestamp(value))
129
+ this.fail("INVALID_TIMESTAMP", pointer);
130
+ }
131
+ }
132
+ if (roster)
133
+ output.endpoints = this.list(field("endpoints"), at(base, "endpoints"), "str", false);
134
+ else {
135
+ const kinds = field("kind");
136
+ if (kinds.length > 1)
137
+ this.conflicts.add(at(base, "kind"));
138
+ output.kind = this.scalar(kinds, "uint", at(base, "kind"), (v) => v === 0 || v === 1, "INVALID_ENUM_VALUE");
139
+ if (request) {
140
+ const scope = this.field(obj, "scope", base, kinds.some((v) => v[1] === 1), true);
141
+ if (scope.length > 0) {
142
+ if (kinds.some((v) => v[1] === 0))
143
+ this.fail("INVALID_FIELD_TYPE", at(base, "scope"));
144
+ output.scope = this.list(scope, at(base, "scope"), "str", true, true);
145
+ }
146
+ }
147
+ }
148
+ const bool = roster ? "archived" : "revoked";
149
+ if (!request) {
150
+ const values = field(bool);
151
+ if (values.length > 0)
152
+ output[bool] = this.scalar(values, "boolean", at(base, bool));
153
+ }
154
+ if (table === "devices") {
155
+ const values = field("missing_keys");
156
+ const union = new Map();
157
+ for (const [type, id] of values) {
158
+ if (type !== "map") {
159
+ this.fail("INVALID_FIELD_TYPE", at(base, "missing_keys"));
160
+ continue;
161
+ }
162
+ for (const resource of this.reader.keys(String(id))) {
163
+ const pointer = at(at(base, "missing_keys"), resource);
164
+ if (!isResourceRef(resource))
165
+ this.fail("INVALID_RESOURCE_REF", pointer);
166
+ const epochs = union.get(resource) ?? new Set();
167
+ for (const list of this.reader.all(String(id), resource)) {
168
+ this.list([list], pointer, "uint", false);
169
+ if (list[0] === "list")
170
+ for (const items of this.reader.list(String(list[1])))
171
+ for (const item of items)
172
+ if (uint(item))
173
+ epochs.add(item[1]);
174
+ }
175
+ union.set(resource, epochs);
176
+ }
177
+ }
178
+ if (values.length > 0)
179
+ output.missing_keys = Object.freeze(Object.fromEntries([...union]
180
+ .sort(([a], [b]) => textOrder(a, b))
181
+ .map(([k, v]) => [k, Object.freeze([...v].sort(epochOrder))])));
182
+ }
183
+ // Unknown request fields are preserved/ignored too, but any conflict invalidates a request.
184
+ if (request)
185
+ for (const name of this.reader.keys(obj))
186
+ if (this.reader.all(obj, name).length > 1)
187
+ this.conflicts.add(at(base, name));
188
+ for (const value of Object.values(output))
189
+ if (Array.isArray(value))
190
+ Object.freeze(value);
191
+ return Object.freeze(output);
192
+ }
193
+ }
194
+ /** §4: validate every concurrent value; return only usable entries, never request winners. */
195
+ export function validateIdentity(doc, root) {
196
+ const r = new IdentityReader(doc);
197
+ const f = new Fields(r);
198
+ const profiles = r.all("_root", "profile");
199
+ if (profiles.length !== 1 || profiles[0]?.[0] !== "str" || profiles[0][1] !== IDENTITY_PROFILE_ID)
200
+ f.fail("INVALID_ROOT", "/profile");
201
+ for (const key of ["identity", "roster", "devices", "requests", "extensions"])
202
+ if (r.map("_root", key) === undefined)
203
+ f.fail("INVALID_ROOT", at("", key));
204
+ const identity = r.map("_root", "identity");
205
+ if (identity !== undefined) {
206
+ const roots = r.all(identity, "root");
207
+ if (roots.length !== 1 || roots[0]?.[0] !== "str" || roots[0][1] !== principalRef(root))
208
+ f.fail("INVALID_ROOT", "/identity/root");
209
+ }
210
+ const usable = ![...f.problems.values()].some((p) => p.diagnostic === "INVALID_ROOT");
211
+ const extensions = r.map("_root", "extensions");
212
+ if (extensions !== undefined)
213
+ for (const key of r.keys(extensions))
214
+ if (!isReverseDomain(key))
215
+ f.fail("INVALID_EXTENSION_NAMESPACE", at("/extensions", key));
216
+ const tables = {};
217
+ for (const table of ["roster", "devices", "requests"]) {
218
+ const entries = [];
219
+ const container = r.map("_root", table);
220
+ if (container !== undefined)
221
+ for (const key of r.keys(container)) {
222
+ const base = at(`/${table}`, key);
223
+ const values = r.all(container, key);
224
+ if (table === "requests" && values.length > 1)
225
+ f.conflicts.add(base);
226
+ const projections = [];
227
+ for (const [type, id] of values) {
228
+ if (type !== "map")
229
+ f.fail("INVALID_FIELD_TYPE", base);
230
+ else
231
+ projections.push(f.entry(String(id), key, table));
232
+ }
233
+ // Concurrent device maps also cannot disagree about kind; hints union across maps.
234
+ if (table === "devices" && new Set(projections.map((p) => p.kind)).size > 1)
235
+ f.conflicts.add(at(base, "kind"));
236
+ f.finishEntry(base);
237
+ if (usable && ![...f.problems.keys()].some((p) => p === base || p.startsWith(`${base}/`))) {
238
+ const first = projections[0];
239
+ if (first !== undefined) {
240
+ if (table === "devices" && projections.some((p) => p.missing_keys !== undefined)) {
241
+ const union = new Map();
242
+ for (const p of projections)
243
+ for (const [ref, epochs] of Object.entries((p.missing_keys ?? {})))
244
+ union.set(ref, new Set([...(union.get(ref) ?? []), ...epochs]));
245
+ entries.push([
246
+ key,
247
+ Object.freeze({
248
+ ...first,
249
+ missing_keys: Object.freeze(Object.fromEntries([...union]
250
+ .sort(([a], [b]) => textOrder(a, b))
251
+ .map(([k, v]) => [k, Object.freeze([...v].sort(epochOrder))]))),
252
+ }),
253
+ ]);
254
+ }
255
+ else
256
+ entries.push([key, first]);
257
+ }
258
+ }
259
+ }
260
+ tables[table] = Object.freeze(Object.fromEntries(entries));
261
+ }
262
+ return Object.freeze({
263
+ usable,
264
+ problems: Object.freeze([...f.problems.values()].sort((a, b) => IDENTITY_DIAGNOSTIC_ORDER.indexOf(a.diagnostic) -
265
+ IDENTITY_DIAGNOSTIC_ORDER.indexOf(b.diagnostic) || textOrder(a.pointer, b.pointer))),
266
+ roster: tables.roster,
267
+ devices: tables.devices,
268
+ requests: tables.requests,
269
+ });
270
+ }
271
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,56 @@
1
+ import { type PrincipalId } from "@openlfcp/core";
2
+ export declare const IDENTITY_PROFILE_ID = "org.openlfcp.identity.v1";
3
+ export { deriveActorId as deriveIdentityActorId } from "../values.js";
4
+ export type ResourceRef = string & {
5
+ readonly __resourceRef: true;
6
+ };
7
+ export declare function isResourceRef(value: unknown): value is ResourceRef;
8
+ /** W §7 has exactly one deterministic encoding: a3, then 0/1/2 + bstr32. */
9
+ export declare function isIdentityDescriptor(text: string, key: string): boolean;
10
+ /** Supplied from the verified Identity Resource Control state at control_ref (§2.3). */
11
+ export type IdentityWriterClass = "root" | "personal" | "pairing" | "other";
12
+ export interface IdentityWriter {
13
+ readonly signer: PrincipalId;
14
+ /** The LFCP Data Unit actor sequence, not the Automerge sequence. */
15
+ readonly actorSeq: bigint | number;
16
+ readonly writerClass: IdentityWriterClass;
17
+ }
18
+ export interface RosterEntry {
19
+ readonly profile: string;
20
+ readonly endpoints: readonly string[];
21
+ readonly coordinator: string;
22
+ readonly name?: string;
23
+ readonly archived?: boolean;
24
+ }
25
+ export interface DeviceEntry {
26
+ readonly label: string;
27
+ readonly kind: 0 | 1;
28
+ readonly platform?: string;
29
+ readonly added_at?: string;
30
+ readonly revoked?: boolean;
31
+ /** Safe values are numbers; larger uint64 epochs are lossless bigints. */
32
+ readonly missing_keys?: Readonly<Record<string, readonly IdentityEpoch[]>>;
33
+ }
34
+ export type IdentityEpoch = bigint | number;
35
+ export interface PairingRequest {
36
+ readonly descriptor: string;
37
+ readonly pairing: string;
38
+ readonly label: string;
39
+ readonly kind: 0 | 1;
40
+ readonly scope?: readonly string[];
41
+ readonly platform?: string;
42
+ readonly requested_at?: string;
43
+ }
44
+ /** Logical §2.1 document; the engine stores strings as ImmutableString, never Text. */
45
+ export interface IdentityDocument {
46
+ readonly profile: typeof IDENTITY_PROFILE_ID;
47
+ readonly identity: {
48
+ readonly root: string;
49
+ };
50
+ readonly roster: Readonly<Record<string, RosterEntry>>;
51
+ readonly devices: Readonly<Record<string, DeviceEntry>>;
52
+ readonly requests: Readonly<Record<string, PairingRequest>>;
53
+ readonly extensions: Readonly<Record<string, unknown>>;
54
+ readonly [unknownKey: string]: unknown;
55
+ }
56
+ //# sourceMappingURL=values.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { bytesEqual, fromBase64url, toBase64url } from "@openlfcp/core";
2
+ import { isValidEd25519PublicKey, sha256 } from "@openlfcp/crypto";
3
+ export const IDENTITY_PROFILE_ID = "org.openlfcp.identity.v1";
4
+ // IDENTITY-PROFILE-01 §2 inherits SOP §8 without changing its domain.
5
+ export { deriveActorId as deriveIdentityActorId } from "../values.js";
6
+ export function isResourceRef(value) {
7
+ if (typeof value !== "string" || value.length !== 43)
8
+ return false;
9
+ try {
10
+ return fromBase64url(value).length === 32;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ /** W §7 has exactly one deterministic encoding: a3, then 0/1/2 + bstr32. */
17
+ export function isIdentityDescriptor(text, key) {
18
+ try {
19
+ const bytes = fromBase64url(text);
20
+ if (bytes.length !== 106 || bytes[0] !== 0xa3)
21
+ return false;
22
+ for (let k = 0; k < 3; k++) {
23
+ const at = 1 + 35 * k;
24
+ if (bytes[at] !== k || bytes[at + 1] !== 0x58 || bytes[at + 2] !== 32)
25
+ return false;
26
+ }
27
+ const id = bytes.subarray(4, 36);
28
+ const ed = bytes.subarray(39, 71);
29
+ const x = bytes.subarray(74, 106);
30
+ const domain = Uint8Array.from("LFCP-PRINCIPAL-v1", (c) => c.charCodeAt(0));
31
+ const input = new Uint8Array(domain.length + 64);
32
+ input.set(domain);
33
+ input.set(ed, domain.length);
34
+ input.set(x, domain.length + 32);
35
+ return (key === `p:${toBase64url(id)}` && bytesEqual(id, sha256(input)) && isValidEd25519PublicKey(ed));
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ //# sourceMappingURL=values.js.map
package/dist/replica.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as A from "@automerge/automerge";
2
2
  import { fromHex, LfcpError, toHex, } from "@openlfcp/core";
3
- import { checkChange, checkSaveHeader, parsedOf, unframeChange, unframeSnapshot, } from "./admission/framing.js";
3
+ import { checkChange, checkSaveHeader, decodedOf, parsedOf, refusedChangeHash, unframeChange, unframeSnapshot, } from "./admission/framing.js";
4
4
  import { checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, } from "./admission/limits.js";
5
5
  import { referenceHistoryOf } from "./admission/references.js";
6
6
  import { admitBatch, admitChange } from "./admission/sequence.js";
@@ -540,7 +540,7 @@ export class SharedObjectsReplica {
540
540
  };
541
541
  }
542
542
  #createdDepths(change, prior) {
543
- const decoded = A.decodeChange(change.bytes);
543
+ const decoded = decodedOf(change);
544
544
  const created = new Map();
545
545
  decoded.ops.forEach((op, i) => {
546
546
  if (!MAKE_ACTIONS.has(op.action))
@@ -577,7 +577,7 @@ export class SharedObjectsReplica {
577
577
  return path.length;
578
578
  }
579
579
  for (const bytes of A.getAllChanges(this.#doc)) {
580
- const c = A.decodeChange(bytes);
580
+ const c = decodedOf(checkChange(bytes));
581
581
  c.ops.forEach((op, i) => {
582
582
  if (!MAKE_ACTIONS.has(op.action))
583
583
  return;
@@ -657,7 +657,7 @@ export class SharedObjectsReplica {
657
657
  */
658
658
  rebuildWithout(exclude) {
659
659
  const out = new Set(exclude);
660
- const kept = A.getAllChanges(this.#doc).filter((c) => !out.has(A.decodeChange(c).hash));
660
+ const kept = A.getAllChanges(this.#doc).filter((c) => !out.has(checkChange(c).hash));
661
661
  return SharedObjectsReplica.fromChanges(kept, {
662
662
  resource: this.resource,
663
663
  principal: this.#principal,
@@ -835,8 +835,8 @@ export class SharedObjectsReplica {
835
835
  catch (e) {
836
836
  // §11.1: a writer never emits a change over the limits. The handle
837
837
  // this replica held is outdated by A.change: rebuild it without the change.
838
- const hash = A.decodeChange(bytes).hash;
839
- this.#doc = A.applyChanges(A.init({ actor: this.#actor }), A.getAllChanges(next).filter((c) => A.decodeChange(c).hash !== hash))[0];
838
+ const hash = refusedChangeHash(bytes);
839
+ this.#doc = A.applyChanges(A.init({ actor: this.#actor }), A.getAllChanges(next).filter((c) => refusedChangeHash(c) !== hash))[0];
840
840
  this.#refs = null;
841
841
  throw new LfcpError("CHANGE_TOO_LARGE", `the transaction "${message}" is larger than one change may be (§11.1: ${e.message}); split it into several (§12)`);
842
842
  }
@@ -1,3 +1,4 @@
1
+ import { IDENTITY_PROFILE_ID } from "../identity/values.js";
1
2
  import { PROFILE_ID } from "../values.js";
2
3
  import { SECTIONS_PROFILE_ID } from "./values.js";
3
4
  /**
@@ -9,13 +10,16 @@ import { SECTIONS_PROFILE_ID } from "./values.js";
9
10
  * textual shape.
10
11
  */
11
12
  /** The models this package implements, by Data Profile. */
12
- export declare const SUPPORTED_DATA_PROFILES: readonly ["org.openlfcp.shared-objects.v1", "org.openlfcp.shared-sections.v1"];
13
+ export declare const SUPPORTED_DATA_PROFILES: readonly ["org.openlfcp.shared-objects.v1", "org.openlfcp.shared-sections.v1", "org.openlfcp.identity.v1"];
13
14
  export type ProfileModel = {
14
15
  readonly kind: "shared-objects";
15
16
  readonly dataProfile: typeof PROFILE_ID;
16
17
  } | {
17
18
  readonly kind: "shared-sections";
18
19
  readonly dataProfile: typeof SECTIONS_PROFILE_ID;
20
+ } | {
21
+ readonly kind: "identity";
22
+ readonly dataProfile: typeof IDENTITY_PROFILE_ID;
19
23
  }
20
24
  /**
21
25
  * LFCP-WIRE-01 §62: no model here reads this profile. The Resource is
@@ -1,3 +1,4 @@
1
+ import { IDENTITY_PROFILE_ID } from "../identity/values.js";
1
2
  import { PROFILE_ID } from "../values.js";
2
3
  import { SECTIONS_PROFILE_ID } from "./values.js";
3
4
  /**
@@ -9,12 +10,18 @@ import { SECTIONS_PROFILE_ID } from "./values.js";
9
10
  * textual shape.
10
11
  */
11
12
  /** The models this package implements, by Data Profile. */
12
- export const SUPPORTED_DATA_PROFILES = Object.freeze([PROFILE_ID, SECTIONS_PROFILE_ID]);
13
+ export const SUPPORTED_DATA_PROFILES = Object.freeze([
14
+ PROFILE_ID,
15
+ SECTIONS_PROFILE_ID,
16
+ IDENTITY_PROFILE_ID,
17
+ ]);
13
18
  /**
14
19
  * The model of a Resource whose validated Genesis (its Control Chain state)
15
20
  * names `dataProfile`. Exact match only: no prefix, version or case folding.
16
21
  */
17
22
  export function profileModel(dataProfile) {
23
+ if (dataProfile === IDENTITY_PROFILE_ID)
24
+ return Object.freeze({ kind: "identity", dataProfile });
18
25
  if (dataProfile === PROFILE_ID)
19
26
  return Object.freeze({ kind: "shared-objects", dataProfile });
20
27
  if (dataProfile === SECTIONS_PROFILE_ID)
@@ -28,6 +35,12 @@ export function profileModel(dataProfile) {
28
35
  * Shared Objects Task.
29
36
  */
30
37
  export function taskRefModel(resourceDataProfile) {
38
+ if (resourceDataProfile === IDENTITY_PROFILE_ID)
39
+ return Object.freeze({
40
+ kind: "profile-unsupported",
41
+ code: "PROFILE_UNSUPPORTED",
42
+ dataProfile: resourceDataProfile,
43
+ });
31
44
  return profileModel(resourceDataProfile);
32
45
  }
33
46
  //# sourceMappingURL=dispatch.js.map
@@ -101,7 +101,7 @@ const str = (v) => A.isImmutableString(v) ? v.toString() : typeof v === "string"
101
101
  const isListStyle = (s) => typeof s === "string" && LIST_STYLES.includes(s);
102
102
  const revisionOf = (doc) => [...A.getHeads(doc)].sort().join(",");
103
103
  /** `changes` without those of `batch` (by hash), in their order. */
104
- const withoutBatch = (changes, batch) => changes.filter((c) => !batch.has(A.decodeChange(c).hash));
104
+ const withoutBatch = (changes, batch) => changes.filter((c) => !batch.has(checkChange(c).hash));
105
105
  /** Rebuilds a section document from the changes it held before an aborted apply. */
106
106
  function restoreSection(changes, actor, revision) {
107
107
  let restored;
@@ -198,7 +198,7 @@ export class SectionReplica {
198
198
  */
199
199
  rebuildWithout(exclude) {
200
200
  const out = new Set(exclude);
201
- const kept = A.getAllChanges(this.#doc).filter((c) => !out.has(A.decodeChange(c).hash));
201
+ const kept = A.getAllChanges(this.#doc).filter((c) => !out.has(checkChange(c).hash));
202
202
  return SectionReplica.fromChanges(kept, {
203
203
  resource: this.resource,
204
204
  principal: this.#principal,
@@ -379,8 +379,7 @@ export class SectionReplica {
379
379
  #sequences() {
380
380
  if (this.#seqs === undefined) {
381
381
  this.#seqs = new Map();
382
- for (const bytes of A.getAllChanges(this.#doc)) {
383
- const c = A.decodeChange(bytes);
382
+ for (const c of A.getChangesMetaSince(this.#doc, [])) {
384
383
  if (c.seq > (this.#seqs.get(c.actor) ?? 0))
385
384
  this.#seqs.set(c.actor, c.seq);
386
385
  }
@@ -1,4 +1,5 @@
1
1
  import * as A from "@automerge/automerge";
2
+ import { checkChange, decodedOf } from "../admission/framing.js";
2
3
  import { distinctValues } from "../concurrent.js";
3
4
  import { validateSection } from "./schema.js";
4
5
  /** The Task scalar fields whose concurrent values tree() reports. */
@@ -183,7 +184,7 @@ export function deriveTree(doc, validation = validateSection(doc)) {
183
184
  });
184
185
  }
185
186
  const summarise = (bytes) => {
186
- const c = A.decodeChange(bytes);
187
+ const c = decodedOf(checkChange(bytes));
187
188
  return {
188
189
  hash: c.hash,
189
190
  deps: c.deps,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlfcp/shared-objects",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1) implementation.",
5
5
  "keywords": [
6
6
  "openlfcp",
@@ -39,6 +39,10 @@
39
39
  "./sections": {
40
40
  "types": "./dist/sections/index.d.ts",
41
41
  "import": "./dist/sections/index.js"
42
+ },
43
+ "./identity": {
44
+ "types": "./dist/identity/index.d.ts",
45
+ "import": "./dist/identity/index.js"
42
46
  }
43
47
  },
44
48
  "files": [
@@ -53,8 +57,8 @@
53
57
  "dependencies": {
54
58
  "@automerge/automerge": "3.5.0",
55
59
  "fflate": "0.8.3",
56
- "@openlfcp/core": "^0.2.2",
57
- "@openlfcp/crypto": "^0.2.2"
60
+ "@openlfcp/core": "^0.3.1",
61
+ "@openlfcp/crypto": "^0.3.1"
58
62
  },
59
63
  "devDependencies": {
60
64
  "fast-check": "4.10.2"