@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 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.2`, LFCP-04-409 and 409b).
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.
@@ -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";
@@ -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
- if (intents.length === 0)
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
- for (const i of intents) {
280
- const base = "key" in i ? `/${tableOf(i)}/${i.key}` : "";
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.4.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/crypto": "^0.4.0",
61
- "@openlfcp/core": "^0.4.0"
60
+ "@openlfcp/core": "^0.5.0",
61
+ "@openlfcp/crypto": "^0.5.0"
62
62
  },
63
63
  "devDependencies": {
64
64
  "fast-check": "4.10.2"