@openlfcp/shared-objects 0.2.2 → 0.3.0

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,112 @@
1
+ import { type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ import { type CheckedChange, frameSnapshot, unframeChange, unframeSnapshot } from "../admission/framing.js";
3
+ import { type SnapshotLimits } from "../admission/limits.js";
4
+ import { type IdentityValidation } from "./schema.js";
5
+ import { type DeviceEntry, type IdentityWriter, type PairingRequest, type RosterEntry } from "./values.js";
6
+ export interface IdentityOptions {
7
+ readonly resource: ResourceId;
8
+ /** Genesis owner; not the current owner of a transferred resource. */
9
+ readonly root: PrincipalId;
10
+ }
11
+ /** Wire authorization/decryption and class derivation at control_ref happen before this boundary. */
12
+ export interface IdentityUnit extends IdentityWriter {
13
+ readonly bytes: Uint8Array;
14
+ }
15
+ export interface IdentityRefusal {
16
+ readonly hash: string | undefined;
17
+ readonly diagnostic: string;
18
+ readonly code: string;
19
+ readonly held: boolean;
20
+ readonly message: string;
21
+ }
22
+ export interface IdentityReceiveResult {
23
+ readonly admitted: readonly string[];
24
+ readonly duplicates: readonly string[];
25
+ readonly waiting: readonly string[];
26
+ readonly refused: readonly IdentityRefusal[];
27
+ }
28
+ export type IdentityIntent = {
29
+ readonly intent: "identity.initialize";
30
+ } | {
31
+ readonly intent: "roster.put";
32
+ readonly key: string;
33
+ readonly entry: RosterEntry;
34
+ } | {
35
+ readonly intent: "device.put";
36
+ readonly key: string;
37
+ readonly entry: DeviceEntry;
38
+ } | {
39
+ readonly intent: "request.put";
40
+ readonly key: string;
41
+ readonly entry: PairingRequest;
42
+ } | {
43
+ readonly intent: "roster.delete" | "device.delete" | "request.delete";
44
+ readonly key: string;
45
+ };
46
+ /** Small identity documents: deterministic replay, causal admission, per-entry readers (§§2–4). */
47
+ export declare class IdentityReplica {
48
+ #private;
49
+ private constructor();
50
+ static empty(options: IdentityOptions): IdentityReplica;
51
+ /** Both plaintext delivery orders are accepted; missing dependencies and held units are retained. */
52
+ static fromChanges(units: readonly IdentityUnit[], options: IdentityOptions): {
53
+ readonly replica: IdentityReplica;
54
+ readonly result: IdentityReceiveResult;
55
+ };
56
+ /**
57
+ * §2.2: no snapshot state is usable without the root's verified unit 1.
58
+ * Replays the snapshot against verified carrying units for EVERY change:
59
+ * this also checks historical writer classes and AI4 without trusting a snapshot's author.
60
+ * Callers fetch/decrypt missing units before retrying (KEY_BLOCKED included).
61
+ */
62
+ static fromSave(save: Uint8Array, options: IdentityOptions, verifiedUnits: readonly IdentityUnit[], limits?: SnapshotLimits): IdentityReplica;
63
+ static fromSnapshot(plaintext: Uint8Array, options: IdentityOptions, verifiedUnits: readonly IdentityUnit[], limits?: SnapshotLimits): IdentityReplica;
64
+ receive(plaintext: Uint8Array, writer: IdentityWriter): IdentityReceiveResult;
65
+ receiveChanges(units: readonly IdentityUnit[]): IdentityReceiveResult;
66
+ /** SOP §14: excluded units and their dependants disappear; held alternatives are retried. */
67
+ rebuildWithout(exclude: Iterable<string>): {
68
+ readonly replica: IdentityReplica;
69
+ readonly result: IdentityReceiveResult;
70
+ };
71
+ heads(): string[];
72
+ hasChange(hash: string): boolean;
73
+ changes(): Uint8Array[];
74
+ save(): Uint8Array;
75
+ snapshot(): Uint8Array;
76
+ validate(): IdentityValidation;
77
+ roster(): IdentityValidation["roster"];
78
+ devices(): IdentityValidation["devices"];
79
+ requests(): IdentityValidation["requests"];
80
+ /**
81
+ * The admitted changes that created or wrote into the value `table[key]`
82
+ * holds now (every value of a conflict, nested lists and maps included),
83
+ * in admission (causal) order, each with the signer of the unit that
84
+ * carried it. Changes to an earlier value since deleted or replaced are
85
+ * not its provenance. This is the provenance a
86
+ * managing device checks before it shows a pairing request
87
+ * (IDENTITY-PROFILE-01 §5.3 step 4: written only by the session's
88
+ * pairing device) and the change hashes it journals with an approval.
89
+ */
90
+ origins(table: "roster" | "devices" | "requests", key: string): readonly {
91
+ readonly hash: string;
92
+ readonly signer: PrincipalId;
93
+ }[];
94
+ /**
95
+ * One transaction (one change) for the intent, or for several intents in
96
+ * order, e.g. the fill change of IDENTITY-PROFILE-01 §5.1 or the devices
97
+ * entry and the request deletion of §5.3 step 6; scalar strings/uints,
98
+ * unknown fields preserved on updates.
99
+ */
100
+ write(writer: IdentityWriter, intent: IdentityIntent | readonly IdentityIntent[]): CheckedChange;
101
+ }
102
+ export declare const identityCodec: Readonly<{
103
+ dataProfile: "org.openlfcp.identity.v1";
104
+ encode: (change: CheckedChange) => Uint8Array;
105
+ decode: typeof unframeChange;
106
+ }>;
107
+ export declare const identitySnapshotCodec: Readonly<{
108
+ dataProfile: "org.openlfcp.identity.v1";
109
+ encode: typeof frameSnapshot;
110
+ decode: typeof unframeSnapshot;
111
+ }>;
112
+ //# sourceMappingURL=replica.d.ts.map
@@ -0,0 +1,384 @@
1
+ import * as A from "@automerge/automerge";
2
+ import { bytesEqual, dataEpoch, LfcpError, toHex, } from "@openlfcp/core";
3
+ import { checkChange, checkSaveHeader, frameChange, frameSnapshot, parsedOf, refusedChangeHash, unframeChange, unframeSnapshot, } from "../admission/framing.js";
4
+ import { checkSnapshotExpansion, SNAPSHOT_LIMITS_FLOOR, } from "../admission/limits.js";
5
+ import { ReferenceHistory } from "../admission/references.js";
6
+ import { admitBatch, admitChange } from "../admission/sequence.js";
7
+ import { isWasmTrap } from "../replica.js";
8
+ import { principalRef } from "../values.js";
9
+ import { checkIdentityUnit, IdentityAdmission, refuseIdentity } from "./admission.js";
10
+ import { validateIdentity } from "./schema.js";
11
+ import { deriveIdentityActorId, IDENTITY_PROFILE_ID, } from "./values.js";
12
+ const refusal = (hash, error, held = false) => ({
13
+ hash,
14
+ held,
15
+ code: error instanceof LfcpError ? error.code : "PROFILE_INVALID",
16
+ diagnostic: error !== null && typeof error === "object" && "diagnostic" in error
17
+ ? String(error.diagnostic)
18
+ : held
19
+ ? "ACTOR_EQUIVOCATION"
20
+ : "INVALID_AUTOMERGE_BYTES",
21
+ message: error instanceof Error ? error.message : String(error),
22
+ });
23
+ /** Small identity documents: deterministic replay, causal admission, per-entry readers (§§2–4). */
24
+ export class IdentityReplica {
25
+ #admission;
26
+ #options;
27
+ #accepted = new Map();
28
+ #waiting = new Map();
29
+ #sequences = new Map();
30
+ #references = new ReferenceHistory();
31
+ get #doc() {
32
+ return this.#admission.document;
33
+ }
34
+ constructor(options) {
35
+ this.#options = {
36
+ resource: options.resource.slice(),
37
+ root: options.root.slice(),
38
+ };
39
+ deriveIdentityActorId(options.resource, options.root); // Validate raw ID lengths.
40
+ const initial = A.init();
41
+ this.#admission = new IdentityAdmission(initial, this.#options.root);
42
+ A.free(initial);
43
+ }
44
+ #history() {
45
+ return {
46
+ hasChange: (hash) => this.#accepted.has(hash),
47
+ latestSeq: (actor) => this.#sequences.get(actor) ?? 0,
48
+ references: this.#references,
49
+ };
50
+ }
51
+ #record(change, unit) {
52
+ this.#accepted.set(change.hash, unit);
53
+ this.#sequences.set(change.actor, change.seq);
54
+ }
55
+ static empty(options) {
56
+ return new IdentityReplica(options);
57
+ }
58
+ /** Both plaintext delivery orders are accepted; missing dependencies and held units are retained. */
59
+ static fromChanges(units, options) {
60
+ const replica = IdentityReplica.empty(options);
61
+ return { replica, result: replica.receiveChanges(units) };
62
+ }
63
+ /**
64
+ * §2.2: no snapshot state is usable without the root's verified unit 1.
65
+ * Replays the snapshot against verified carrying units for EVERY change:
66
+ * this also checks historical writer classes and AI4 without trusting a snapshot's author.
67
+ * Callers fetch/decrypt missing units before retrying (KEY_BLOCKED included).
68
+ */
69
+ static fromSave(save, options, verifiedUnits, limits = SNAPSHOT_LIMITS_FLOOR) {
70
+ checkSaveHeader(save);
71
+ checkSnapshotExpansion(save, limits);
72
+ let changes;
73
+ try {
74
+ const loaded = A.load(save);
75
+ try {
76
+ changes = A.getAllChanges(loaded);
77
+ }
78
+ finally {
79
+ A.free(loaded);
80
+ }
81
+ }
82
+ catch (e) {
83
+ if (isWasmTrap(e))
84
+ throw e;
85
+ refuseIdentity("INVALID_AUTOMERGE_BYTES", `invalid snapshot: ${String(e)}`);
86
+ }
87
+ const checked = changes.map(checkChange);
88
+ const byHash = new Map(verifiedUnits.map((unit) => [checkChange(unit.bytes).hash, unit]));
89
+ const initial = checked.filter((c) => c.deps.length === 0);
90
+ if (initial.length !== 1)
91
+ refuseIdentity("INVALID_ROOT", "snapshot must contain one initial change");
92
+ const first = initial[0];
93
+ const proof = byHash.get(first.hash);
94
+ if (proof === undefined || !bytesEqual(proof.bytes, first.bytes))
95
+ refuseIdentity("INVALID_ROOT", "fetch and verify the root's unit 1 before using this snapshot");
96
+ checkIdentityUnit(first, proof, options.resource, options.root);
97
+ const units = checked.map((change) => {
98
+ const unit = byHash.get(change.hash);
99
+ if (unit === undefined || !bytesEqual(unit.bytes, change.bytes))
100
+ throw new LfcpError("MISSING_DEPENDENCY", `snapshot change ${change.hash} needs its verified carrying unit`);
101
+ return unit;
102
+ });
103
+ const { replica, result } = IdentityReplica.fromChanges(units, options);
104
+ if (result.refused.length > 0 || result.waiting.length > 0) {
105
+ const error = result.refused[0];
106
+ A.free(replica.#doc);
107
+ throw Object.assign(new LfcpError("PROFILE_INVALID", error?.message ?? "snapshot has missing changes"), {
108
+ diagnostic: error?.diagnostic ?? "INVALID_AUTOMERGE_BYTES",
109
+ });
110
+ }
111
+ return replica;
112
+ }
113
+ static fromSnapshot(plaintext, options, verifiedUnits, limits = SNAPSHOT_LIMITS_FLOOR) {
114
+ return IdentityReplica.fromSave(unframeSnapshot(plaintext), options, verifiedUnits, limits);
115
+ }
116
+ receive(plaintext, writer) {
117
+ return this.receiveChanges([{ ...writer, bytes: unframeChange(plaintext).bytes }]);
118
+ }
119
+ receiveChanges(units) {
120
+ const refused = [];
121
+ const pending = new Map(this.#waiting);
122
+ for (const unit of units) {
123
+ let change;
124
+ try {
125
+ change = checkChange(unit.bytes);
126
+ checkIdentityUnit(change, unit, this.#options.resource, this.#options.root);
127
+ if (!pending.has(change.hash))
128
+ pending.set(change.hash, {
129
+ change,
130
+ unit: { ...unit, signer: unit.signer.slice(), bytes: change.bytes },
131
+ });
132
+ }
133
+ catch (e) {
134
+ if (isWasmTrap(e))
135
+ throw e;
136
+ refused.push(refusal(change?.hash ?? refusedChangeHash(unit.bytes), e));
137
+ }
138
+ }
139
+ const result = admitBatch([...pending.values()].map((p) => p.change), this.#history(), (c) => this.#admission.accept(c, pending.get(c.hash).unit));
140
+ for (const refused of result.refused)
141
+ if (isWasmTrap(refused.error))
142
+ throw refused.error;
143
+ this.#waiting.clear();
144
+ for (const c of result.admitted)
145
+ this.#record(c, pending.get(c.hash).unit);
146
+ for (const c of result.waiting)
147
+ this.#waiting.set(c.hash, pending.get(c.hash));
148
+ for (const r of result.refused) {
149
+ refused.push(refusal(r.change.hash, r.error, r.held));
150
+ if (r.held)
151
+ this.#waiting.set(r.change.hash, pending.get(r.change.hash));
152
+ }
153
+ const hashes = (changes) => Object.freeze(changes.map((c) => c.hash));
154
+ return Object.freeze({
155
+ admitted: hashes(result.admitted),
156
+ duplicates: hashes(result.duplicates),
157
+ waiting: hashes(result.waiting),
158
+ refused: Object.freeze(refused),
159
+ });
160
+ }
161
+ /** SOP §14: excluded units and their dependants disappear; held alternatives are retried. */
162
+ rebuildWithout(exclude) {
163
+ const removed = new Set(exclude);
164
+ return IdentityReplica.fromChanges([
165
+ ...this.#accepted,
166
+ ...[...this.#waiting].map(([hash, pending]) => [hash, pending.unit]),
167
+ ]
168
+ .filter(([hash]) => !removed.has(hash))
169
+ .map(([, unit]) => unit), this.#options);
170
+ }
171
+ heads() {
172
+ return A.getHeads(this.#doc).sort();
173
+ }
174
+ hasChange(hash) {
175
+ return this.#accepted.has(hash);
176
+ }
177
+ changes() {
178
+ return A.getAllChanges(this.#doc);
179
+ }
180
+ save() {
181
+ return A.save(this.#doc);
182
+ }
183
+ snapshot() {
184
+ return frameSnapshot(this.save());
185
+ }
186
+ validate() {
187
+ return validateIdentity(this.#doc, this.#options.root);
188
+ }
189
+ roster() {
190
+ return this.validate().roster;
191
+ }
192
+ devices() {
193
+ return this.validate().devices;
194
+ }
195
+ requests() {
196
+ return this.validate().requests;
197
+ }
198
+ /**
199
+ * The admitted changes that created or wrote into the value `table[key]`
200
+ * holds now (every value of a conflict, nested lists and maps included),
201
+ * in admission (causal) order, each with the signer of the unit that
202
+ * carried it. Changes to an earlier value since deleted or replaced are
203
+ * not its provenance. This is the provenance a
204
+ * managing device checks before it shows a pairing request
205
+ * (IDENTITY-PROFILE-01 §5.3 step 4: written only by the session's
206
+ * pairing device) and the change hashes it journals with an approval.
207
+ */
208
+ origins(table, key) {
209
+ // The values `table[key]` holds now: one, or several under a conflict. Each
210
+ // is named by the op that set it (for a map, its object ID). The history of
211
+ // earlier values deleted or overwritten since is not their provenance.
212
+ const container = this.#doc[table];
213
+ const current = new Set();
214
+ if (container !== null && typeof container === "object" && Object.hasOwn(container, key)) {
215
+ const conflicts = A.getConflicts(container, key);
216
+ if (conflicts !== undefined)
217
+ for (const id of Object.keys(conflicts))
218
+ current.add(id);
219
+ else {
220
+ const value = container[key];
221
+ const id = value !== null && typeof value === "object" ? A.getObjectId(value) : null;
222
+ if (id !== null && id !== undefined)
223
+ current.add(id);
224
+ }
225
+ }
226
+ const entry = new Set(current);
227
+ const out = [];
228
+ if (current.size === 0)
229
+ return Object.freeze(out);
230
+ for (const [hash, unit] of this.#accepted) {
231
+ const change = A.decodeChange(unit.bytes);
232
+ let touched = false;
233
+ change.ops.forEach((op, i) => {
234
+ const id = `${change.startOp + i}@${change.actor}`;
235
+ const makes = op.action === "makeMap" || op.action === "makeList";
236
+ if (current.has(id) || entry.has(op.obj)) {
237
+ touched = true;
238
+ if (makes)
239
+ entry.add(id);
240
+ }
241
+ });
242
+ if (touched)
243
+ out.push({ hash, signer: unit.signer.slice() });
244
+ }
245
+ return Object.freeze(out);
246
+ }
247
+ /**
248
+ * One transaction (one change) for the intent, or for several intents in
249
+ * order, e.g. the fill change of IDENTITY-PROFILE-01 §5.1 or the devices
250
+ * entry and the request deletion of §5.3 step 6; scalar strings/uints,
251
+ * unknown fields preserved on updates.
252
+ */
253
+ write(writer, intent) {
254
+ const intents = Array.isArray(intent)
255
+ ? intent
256
+ : [intent];
257
+ if (intents.length === 0)
258
+ throw new LfcpError("PROFILE_INVALID", "no intent to write");
259
+ // AI5 (§3) refuses the change anyway; say why before building it.
260
+ for (const i of intents)
261
+ if (i.intent === "device.put" &&
262
+ i.entry.missing_keys !== undefined &&
263
+ i.key !== principalRef(writer.signer))
264
+ refuseIdentity("UNAUTHORIZED_WRITER", "a device writes only its own missing_keys hints (§3 AI5)");
265
+ const actor = toHex(deriveIdentityActorId(this.#options.resource, writer.signer));
266
+ const fork = A.clone(this.#doc, { actor });
267
+ let bytes;
268
+ try {
269
+ const doc = A.change(fork, { time: 0 }, (d) => {
270
+ for (const i of intents)
271
+ applyIntent(d, i, this.#options.root, A.getBackend(d));
272
+ });
273
+ const last = A.getLastLocalChange(doc);
274
+ if (last === null || last === undefined)
275
+ throw new LfcpError("PROFILE_INVALID", "the intent did not create a change");
276
+ bytes = last;
277
+ const validation = validateIdentity(doc, this.#options.root);
278
+ for (const i of intents) {
279
+ const base = "key" in i ? `/${tableOf(i)}/${i.key}` : "";
280
+ const problem = validation.problems.find((p) => p.diagnostic === "INVALID_ROOT" ||
281
+ p.pointer === base ||
282
+ p.pointer.startsWith(`${base}/`));
283
+ if (problem !== undefined)
284
+ throw Object.assign(new LfcpError("PROFILE_INVALID", problem.message), {
285
+ diagnostic: problem.diagnostic,
286
+ });
287
+ }
288
+ }
289
+ finally {
290
+ A.free(fork);
291
+ }
292
+ const change = checkChange(bytes);
293
+ // Check only this candidate against the current indexes. Failed local
294
+ // writes never enter the waiting queue or consume an actor sequence.
295
+ checkIdentityUnit(change, writer, this.#options.resource, this.#options.root);
296
+ const decision = admitChange(change, this.#history());
297
+ if (decision.kind === "invalid" || decision.kind === "held")
298
+ throw decision.error;
299
+ if (decision.kind === "missing")
300
+ throw new LfcpError("MISSING_DEPENDENCY", "local change is not admissible");
301
+ if (decision.kind === "next") {
302
+ this.#admission.accept(change, writer);
303
+ this.#references.add(change.hash, parsedOf(change));
304
+ this.#record(change, {
305
+ ...writer,
306
+ signer: writer.signer.slice(),
307
+ bytes: change.bytes,
308
+ });
309
+ if (this.#waiting.size > 0)
310
+ this.receiveChanges([]);
311
+ }
312
+ // Keep the byte log private even if callers mutate the returned buffer.
313
+ return checkChange(bytes);
314
+ }
315
+ }
316
+ const tableOf = (intent) => intent.intent.startsWith("roster.")
317
+ ? "roster"
318
+ : intent.intent.startsWith("device.")
319
+ ? "devices"
320
+ : "requests";
321
+ function applyIntent(d, intent, root, backend) {
322
+ if (intent.intent === "identity.initialize") {
323
+ if (Object.keys(d).length !== 0)
324
+ refuseIdentity("INVALID_ROOT", "identity is already initialized");
325
+ d.profile = new A.ImmutableString(IDENTITY_PROFILE_ID);
326
+ d.identity = { root: new A.ImmutableString(principalRef(root)) };
327
+ for (const key of ["roster", "devices", "requests", "extensions"])
328
+ d[key] = {};
329
+ return;
330
+ }
331
+ const map = d[tableOf(intent)];
332
+ if (map === undefined)
333
+ refuseIdentity("INVALID_ROOT", "initialize the identity first");
334
+ if (!("entry" in intent)) {
335
+ delete map[intent.key];
336
+ return;
337
+ }
338
+ if (!Object.hasOwn(map, intent.key))
339
+ map[intent.key] = {};
340
+ const entry = map[intent.key];
341
+ for (const [key, value] of Object.entries(intent.entry))
342
+ if (value !== undefined)
343
+ putValue(entry, key, value, backend);
344
+ }
345
+ function putValue(target, key, value, backend) {
346
+ const map = target;
347
+ if (typeof value === "string")
348
+ map[key] = new A.ImmutableString(value);
349
+ else if (typeof value === "number")
350
+ map[key] = new A.Uint(value);
351
+ else if (typeof value === "bigint") {
352
+ // Uint's JS wrapper is limited to safe numbers; the backend accepts the full uint64.
353
+ const id = A.getObjectId(target);
354
+ const epoch = dataEpoch(value);
355
+ if (Array.isArray(target) && Number(key) === backend.length(id))
356
+ backend.insert(id, Number(key), epoch, "uint");
357
+ else
358
+ backend.put(id, key, epoch, "uint");
359
+ }
360
+ else if (Array.isArray(value)) {
361
+ map[key] = [];
362
+ value.forEach((v, i) => {
363
+ putValue(map[key], i, v, backend);
364
+ });
365
+ }
366
+ else if (value !== null && typeof value === "object") {
367
+ map[key] = {};
368
+ for (const [k, v] of Object.entries(value))
369
+ putValue(map[key], k, v, backend);
370
+ }
371
+ else
372
+ map[key] = value;
373
+ }
374
+ export const identityCodec = Object.freeze({
375
+ dataProfile: IDENTITY_PROFILE_ID,
376
+ encode: (change) => frameChange(change.bytes),
377
+ decode: unframeChange,
378
+ });
379
+ export const identitySnapshotCodec = Object.freeze({
380
+ dataProfile: IDENTITY_PROFILE_ID,
381
+ encode: frameSnapshot,
382
+ decode: unframeSnapshot,
383
+ });
384
+ //# sourceMappingURL=replica.js.map
@@ -0,0 +1,22 @@
1
+ import type * as A from "@automerge/automerge";
2
+ import { type PrincipalId } from "@openlfcp/core";
3
+ import { type DeviceEntry, type PairingRequest, type RosterEntry } from "./values.js";
4
+ export declare const IDENTITY_DIAGNOSTIC_ORDER: readonly ["INVALID_ROOT", "MISSING_REQUIRED_FIELD", "INVALID_FIELD_TYPE", "INVALID_RESOURCE_REF", "INVALID_PRINCIPAL_REF", "INVALID_DESCRIPTOR", "INVALID_ENUM_VALUE", "INVALID_TIMESTAMP", "INVALID_EXTENSION_NAMESPACE"];
5
+ export type IdentityDiagnostic = (typeof IDENTITY_DIAGNOSTIC_ORDER)[number];
6
+ export interface IdentityProblem {
7
+ readonly code: "PROFILE_INVALID";
8
+ readonly diagnostic: IdentityDiagnostic;
9
+ readonly pointer: string;
10
+ readonly message: string;
11
+ }
12
+ export interface IdentityValidation {
13
+ /** False only for an invalid root; invalid entries do not poison other entries. */
14
+ readonly usable: boolean;
15
+ readonly problems: readonly IdentityProblem[];
16
+ readonly roster: Readonly<Record<string, RosterEntry>>;
17
+ readonly devices: Readonly<Record<string, DeviceEntry>>;
18
+ readonly requests: Readonly<Record<string, PairingRequest>>;
19
+ }
20
+ /** §4: validate every concurrent value; return only usable entries, never request winners. */
21
+ export declare function validateIdentity(doc: A.Doc<unknown>, root: PrincipalId): IdentityValidation;
22
+ //# sourceMappingURL=schema.d.ts.map