@openlfcp/shared-objects 0.4.0 → 0.5.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.
- package/README.md +1 -1
- package/dist/identity/index.d.ts +1 -1
- package/dist/identity/replica.d.ts +21 -2
- package/dist/identity/replica.js +39 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ without notice until the profile is in a baseline.
|
|
|
40
40
|
## Identity profile (MVP 0.4 Working Draft)
|
|
41
41
|
|
|
42
42
|
`@openlfcp/shared-objects/identity` implements IDENTITY-PROFILE-01 §§2–4
|
|
43
|
-
(`org.openlfcp.identity.v1`, `mvp-0.4-baseline.
|
|
43
|
+
(`org.openlfcp.identity.v1`, `mvp-0.4-baseline.4`, LFCP-04-409 and 409b).
|
|
44
44
|
`IdentityReplica` admits changes with AI0–AI5, validates every concurrent
|
|
45
45
|
field value and exposes `roster()`, `devices()` and `requests()`. Invalid
|
|
46
46
|
entries are omitted; an invalid root makes all three readers empty.
|
package/dist/identity/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** IDENTITY-PROFILE-01 §§2–4. No authority is inferred from document metadata. */
|
|
2
2
|
export { checkIdentityUnit, IDENTITY_ADMISSION_ORDER, IdentityAdmission, type IdentityAdmissionDiagnostic, IdentityAdmissionError, } from "./admission.js";
|
|
3
|
-
export { type IdentityIntent, type IdentityOptions, type IdentityReceiveResult, type IdentityRefusal, IdentityReplica, type IdentityUnit, identityCodec, identityCodecFor, identitySnapshotCodec, identityUnitProfile, } from "./replica.js";
|
|
3
|
+
export { type IdentityIntent, type IdentityOptions, type IdentityReceiveResult, type IdentityRefusal, IdentityReplica, type IdentityUnit, type IdentityWriteOptions, identityCodec, identityCodecFor, identitySnapshotCodec, identityUnitProfile, } from "./replica.js";
|
|
4
4
|
export { IDENTITY_DIAGNOSTIC_ORDER, type IdentityDiagnostic, type IdentityProblem, type IdentityValidation, validateIdentity, } from "./schema.js";
|
|
5
5
|
export { type DeviceEntry, deriveIdentityActorId, IDENTITY_PROFILE_ID, type IdentityDocument, type IdentityEpoch, type IdentityWriter, type IdentityWriterClass, isResourceRef, type PairingRequest, type ResourceRef, type RosterEntry, } from "./values.js";
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -25,6 +25,15 @@ export interface IdentityReceiveResult {
|
|
|
25
25
|
readonly waiting: readonly string[];
|
|
26
26
|
readonly refused: readonly IdentityRefusal[];
|
|
27
27
|
}
|
|
28
|
+
/** Options of IdentityReplica.write. */
|
|
29
|
+
export interface IdentityWriteOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Device entries (PrincipalRefs) whose `revoked` flag the change sets to
|
|
32
|
+
* `true` after the intents, writing no other field of them: a retired
|
|
33
|
+
* entry (IDENTITY-PROFILE-01 §7.1 step 4, §8.2). Each entry must exist.
|
|
34
|
+
*/
|
|
35
|
+
readonly retire?: readonly string[];
|
|
36
|
+
}
|
|
28
37
|
export type IdentityIntent = {
|
|
29
38
|
readonly intent: "identity.initialize";
|
|
30
39
|
} | {
|
|
@@ -91,13 +100,23 @@ export declare class IdentityReplica {
|
|
|
91
100
|
readonly hash: string;
|
|
92
101
|
readonly signer: PrincipalId;
|
|
93
102
|
}[];
|
|
103
|
+
/**
|
|
104
|
+
* Every value the scalar field `table[key].field` holds now: one, or
|
|
105
|
+
* several under concurrent writes (identical values once, SOP §45); none
|
|
106
|
+
* when the field is absent or `table[key]` is not one map. Strings, uints
|
|
107
|
+
* and booleans as JavaScript values. `devices()` shows one of them; this
|
|
108
|
+
* is what IDENTITY-PROFILE-01 §7.1 step 4 reads of a retired entry's
|
|
109
|
+
* `revoked` ("some value is not true").
|
|
110
|
+
*/
|
|
111
|
+
fieldValues(table: "roster" | "devices" | "requests", key: string, field: string): readonly unknown[];
|
|
94
112
|
/**
|
|
95
113
|
* One transaction (one change) for the intent, or for several intents in
|
|
96
114
|
* order, e.g. the fill change of IDENTITY-PROFILE-01 §5.1 or the devices
|
|
97
115
|
* entry and the request deletion of §5.3 step 6; scalar strings/uints,
|
|
98
|
-
* unknown fields preserved on updates.
|
|
116
|
+
* unknown fields preserved on updates. `options.retire` adds the
|
|
117
|
+
* `revoked = true` flag of retired entries, field by field.
|
|
99
118
|
*/
|
|
100
|
-
write(writer: IdentityWriter, intent: IdentityIntent | readonly IdentityIntent[]): CheckedChange;
|
|
119
|
+
write(writer: IdentityWriter, intent: IdentityIntent | readonly IdentityIntent[], options?: IdentityWriteOptions): CheckedChange;
|
|
101
120
|
}
|
|
102
121
|
export declare const identityCodec: Readonly<{
|
|
103
122
|
dataProfile: "org.openlfcp.identity.v1";
|
package/dist/identity/replica.js
CHANGED
|
@@ -8,6 +8,7 @@ import { admitBatch, admitChange } from "../admission/sequence.js";
|
|
|
8
8
|
import { isWasmTrap } from "../replica.js";
|
|
9
9
|
import { principalRef } from "../values.js";
|
|
10
10
|
import { checkIdentityUnit, IdentityAdmission, refuseIdentity } from "./admission.js";
|
|
11
|
+
import { IdentityReader } from "./reader.js";
|
|
11
12
|
import { validateIdentity } from "./schema.js";
|
|
12
13
|
import { deriveIdentityActorId, IDENTITY_PROFILE_ID, } from "./values.js";
|
|
13
14
|
const refusal = (hash, error, held = false) => ({
|
|
@@ -245,17 +246,35 @@ export class IdentityReplica {
|
|
|
245
246
|
}
|
|
246
247
|
return Object.freeze(out);
|
|
247
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Every value the scalar field `table[key].field` holds now: one, or
|
|
251
|
+
* several under concurrent writes (identical values once, SOP §45); none
|
|
252
|
+
* when the field is absent or `table[key]` is not one map. Strings, uints
|
|
253
|
+
* and booleans as JavaScript values. `devices()` shows one of them; this
|
|
254
|
+
* is what IDENTITY-PROFILE-01 §7.1 step 4 reads of a retired entry's
|
|
255
|
+
* `revoked` ("some value is not true").
|
|
256
|
+
*/
|
|
257
|
+
fieldValues(table, key, field) {
|
|
258
|
+
const reader = new IdentityReader(this.#doc);
|
|
259
|
+
const container = reader.map("_root", table);
|
|
260
|
+
const entry = container === undefined ? undefined : reader.map(container, key);
|
|
261
|
+
if (entry === undefined)
|
|
262
|
+
return Object.freeze([]);
|
|
263
|
+
return Object.freeze(reader.all(entry, field).map(([, value]) => value));
|
|
264
|
+
}
|
|
248
265
|
/**
|
|
249
266
|
* One transaction (one change) for the intent, or for several intents in
|
|
250
267
|
* order, e.g. the fill change of IDENTITY-PROFILE-01 §5.1 or the devices
|
|
251
268
|
* entry and the request deletion of §5.3 step 6; scalar strings/uints,
|
|
252
|
-
* unknown fields preserved on updates.
|
|
269
|
+
* unknown fields preserved on updates. `options.retire` adds the
|
|
270
|
+
* `revoked = true` flag of retired entries, field by field.
|
|
253
271
|
*/
|
|
254
|
-
write(writer, intent) {
|
|
272
|
+
write(writer, intent, options = {}) {
|
|
255
273
|
const intents = Array.isArray(intent)
|
|
256
274
|
? intent
|
|
257
275
|
: [intent];
|
|
258
|
-
|
|
276
|
+
const retire = options.retire ?? [];
|
|
277
|
+
if (intents.length === 0 && retire.length === 0)
|
|
259
278
|
throw new LfcpError("PROFILE_INVALID", "no intent to write");
|
|
260
279
|
// AI5 (§3) refuses the change anyway; say why before building it.
|
|
261
280
|
for (const i of intents)
|
|
@@ -270,14 +289,19 @@ export class IdentityReplica {
|
|
|
270
289
|
const doc = A.change(fork, { time: 0 }, (d) => {
|
|
271
290
|
for (const i of intents)
|
|
272
291
|
applyIntent(d, i, this.#options.root, A.getBackend(d));
|
|
292
|
+
for (const key of retire)
|
|
293
|
+
retireEntry(d, key);
|
|
273
294
|
});
|
|
274
295
|
const last = A.getLastLocalChange(doc);
|
|
275
296
|
if (last === null || last === undefined)
|
|
276
297
|
throw new LfcpError("PROFILE_INVALID", "the intent did not create a change");
|
|
277
298
|
bytes = last;
|
|
278
299
|
const validation = validateIdentity(doc, this.#options.root);
|
|
279
|
-
|
|
280
|
-
|
|
300
|
+
const written = [
|
|
301
|
+
...intents.map((i) => ("key" in i ? `/${tableOf(i)}/${i.key}` : "")),
|
|
302
|
+
...retire.map((key) => `/devices/${key}`),
|
|
303
|
+
];
|
|
304
|
+
for (const base of written) {
|
|
281
305
|
const problem = validation.problems.find((p) => p.diagnostic === "INVALID_ROOT" ||
|
|
282
306
|
p.pointer === base ||
|
|
283
307
|
p.pointer.startsWith(`${base}/`));
|
|
@@ -343,6 +367,16 @@ function applyIntent(d, intent, root, backend) {
|
|
|
343
367
|
if (value !== undefined)
|
|
344
368
|
putValue(entry, key, value, backend);
|
|
345
369
|
}
|
|
370
|
+
/** `devices[key].revoked = true`, and nothing else of the entry (§7.1 step 4, §8.2). */
|
|
371
|
+
function retireEntry(d, key) {
|
|
372
|
+
const devices = d.devices;
|
|
373
|
+
if (devices === undefined)
|
|
374
|
+
refuseIdentity("INVALID_ROOT", "initialize the identity first");
|
|
375
|
+
const entry = Object.hasOwn(devices, key) ? devices[key] : undefined;
|
|
376
|
+
if (entry === null || typeof entry !== "object")
|
|
377
|
+
throw new LfcpError("PROFILE_INVALID", `no device entry ${key} to retire`);
|
|
378
|
+
entry.revoked = true;
|
|
379
|
+
}
|
|
346
380
|
function putValue(target, key, value, backend) {
|
|
347
381
|
const map = target;
|
|
348
382
|
if (typeof value === "string")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlfcp/shared-objects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1) implementation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openlfcp",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@automerge/automerge": "3.5.0",
|
|
59
59
|
"fflate": "0.8.3",
|
|
60
|
-
"@openlfcp/
|
|
61
|
-
"@openlfcp/
|
|
60
|
+
"@openlfcp/core": "^0.5.0",
|
|
61
|
+
"@openlfcp/crypto": "^0.5.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"fast-check": "4.10.2"
|