@openlfcp/client 0.3.1 → 0.4.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.
@@ -1,6 +1,7 @@
1
1
  import { type PrincipalId, type ResourceId } from "@openlfcp/core";
2
2
  import type { SecretStore } from "@openlfcp/storage";
3
- import { type CertificateChain, type ControlState, type DataProfileCodec, type DeviceCertificate, type DeviceCertificatePayload, type DeviceKind, type PrincipalDescriptor, type Signer } from "@openlfcp/wire";
3
+ import { type CertificateChain, type ControlState, type DeviceCertificate, type DeviceCertificatePayload, type DeviceKind, type PrincipalDescriptor, type Signer } from "@openlfcp/wire";
4
+ import type { DataProfileHandler } from "../apply.js";
4
5
  import { type IdentityStorage, type Installation, type KeyDelivery } from "./context.js";
5
6
  import { type IdentityNetwork } from "./network.js";
6
7
  import { type LinearChain } from "./phases.js";
@@ -32,12 +33,24 @@ export interface FlowOptions {
32
33
  readonly network: IdentityNetwork;
33
34
  readonly now: () => number;
34
35
  /**
35
- * The application's Data Profile codecs. A rotation uses the Resource's
36
- * codec to judge which units recipients accept as data (W §26.3 rule 7);
37
- * without one, only decryption and recorded refusals are checked.
36
+ * The application's Data Profiles, each as its receiver checks one unit:
37
+ * `codecFor({resourceId, actor})`, the codec bound to the unit's Resource
38
+ * and signer (the DataProfileHandler hook; e.g. shared-objects'
39
+ * sharedObjectsUnitProfile, sectionsUnitProfile, identityUnitProfile). A
40
+ * rotation judges each unit with its Resource's profile, as recipients do
41
+ * (W §26.3 rules 6 and 7, including the change's actor); without one, only
42
+ * decryption and recorded refusals are checked. Replaces 0.3.1's
43
+ * `dataProfiles` (bare codecs, which cannot bind the actor): a flow given
44
+ * `dataProfiles` throws instead of checking less.
38
45
  */
39
- readonly dataProfiles?: readonly DataProfileCodec<unknown>[];
46
+ readonly profiles?: readonly UnitProfile[];
40
47
  }
48
+ /**
49
+ * One Data Profile's per-unit check (the receiver's DataProfileHandler
50
+ * `dataProfile` and `codecFor`): `decode` of the codec it returns throws to
51
+ * reject the unit's plaintext.
52
+ */
53
+ export type UnitProfile = Pick<DataProfileHandler<unknown>, "dataProfile" | "codecFor">;
41
54
  /**
42
55
  * The certificates above a leaf that `issuer` signs for `root` in the
43
56
  * state: none when the issuer is the root, else the issuer's own current
@@ -1,7 +1,7 @@
1
1
  /** Identity flows (IDENTITY-PROFILE-01 §§5–9, LFCP-04-410). */
2
2
  export { acceptInvitationAsDevice, claimForRootRecord, createResourceAsDevice, type DeviceAcceptResult, type DeviceCreateResult, type DeviceResourceResult, type IncompleteDeviceResource, selfRegistrationRecord, } from "./accept.js";
3
3
  export { type DocumentWrite, deliverKeys, extendChain, fetchDeks, genesisOwner, heldDeks, IdentityConflict, type IdentityDocumentLoad, type IdentityStorage, type Installation, type KeyDelivery, loadIdentityDocument, type PhaseOutcome, readersAt, runControlPhase, runUnitPhase, sealIdentityUnit, storeDek, syncChain, } from "./context.js";
4
- export { checkIssuance, type Feasibility, type FlowOptions, issuerCertificates, leafPayload, MASK_PRESETS, type MaskPreset, mayManageDevices, type RegisterTarget, type RegistrationResult, type RegistrationStatus, registerDevice, signChain, } from "./devices.js";
4
+ export { checkIssuance, type Feasibility, type FlowOptions, issuerCertificates, leafPayload, MASK_PRESETS, type MaskPreset, mayManageDevices, type RegisterTarget, type RegistrationResult, type RegistrationStatus, registerDevice, signChain, type UnitProfile, } from "./devices.js";
5
5
  export { type DeviceEntryValue, IDENTITY_PROFILE, type IdentityAuthor, type IdentityDocumentPort, type IdentityIntent, type IdentityWriterClass, identityPlaintextCodec, identityWriterClass, type PairingRequestValue, principalOfRef, principalRefOf, type RosterEntryValue, resourceRef, } from "./document.js";
6
6
  export { type JournalOperation, type JournalPhase, type LedgerEntry, OperationJournal, type OperationKind, type OperationStatus, PackageLedger, pairingSecretRef, pendingDekRef, } from "./journal.js";
7
7
  export { ChannelPool, type ConnectionNetworkOptions, connectionNetwork, IDENTITY_EXTENSION, type IdentityAnswer, type IdentityChannel, type IdentityNetwork, IdentityRefused, IdentityUnavailable, type OpenAnswer, } from "./network.js";
@@ -9,6 +9,7 @@ import { ChannelPool, IdentityUnavailable } from "./network.js";
9
9
  import { classifyCancel, classifyConsume, classifyRegister } from "./phases.js";
10
10
  import { identityTarget } from "./resource.js";
11
11
  import { rotateIfExposed } from "./revocation.js";
12
+ import { checkFlowProfiles } from "./rotation.js";
12
13
  /**
13
14
  * Pairing a new installation, the managing side `A` (IDENTITY-PROFILE-01
14
15
  * §5.3 steps 1, 4–7, 9 and 10): a pairing device `J` registered in the
@@ -323,6 +324,7 @@ export async function pairingRequests(o) {
323
324
  * phase 1; phase 2 signs exactly that payload.
324
325
  */
325
326
  export async function approvePairing(o) {
327
+ checkFlowProfiles(o);
326
328
  const { I, R, J, url } = await readSession(o, o.review.session);
327
329
  const pool = new ChannelPool(o.network);
328
330
  const me = o.installation.signer;
@@ -413,6 +415,7 @@ export const approvedGrantRecord = (chain, signer, root, device, pairing) => sig
413
415
  export const cancelRecord = (chain, signer, root, pairing) => signControlRecord(next(chain), { type: "DEVICE_REVOKE", root, device: pairing, reason: 0n }, signer).bytes;
414
416
  /** Resumes a journaled approval from its next phase (§9.1: "If A crashes after step 5 …"). */
415
417
  export async function resumeApproval(o, operation) {
418
+ checkFlowProfiles(o);
416
419
  const journal = new OperationJournal(o.storage);
417
420
  const pool = new ChannelPool(o.network);
418
421
  const me = o.installation.signer;
@@ -565,6 +568,7 @@ export async function resumeApproval(o, operation) {
565
568
  * human does not confirm is rejected).
566
569
  */
567
570
  export async function cancelPairing(o) {
571
+ checkFlowProfiles(o);
568
572
  const { I, R, J, url } = await readSession(o, o.session);
569
573
  const journal = new OperationJournal(o.storage);
570
574
  const pool = new ChannelPool(o.network);
@@ -8,7 +8,7 @@ import { ChannelPool, IDENTITY_EXTENSION, IdentityRefused, IdentityUnavailable,
8
8
  import { classifyRegister, classifyRevoke } from "./phases.js";
9
9
  import { identityTarget } from "./resource.js";
10
10
  import { rotateIfExposed } from "./revocation.js";
11
- import { resumeRotations } from "./rotation.js";
11
+ import { checkFlowProfiles, resumeRotations } from "./rotation.js";
12
12
  /**
13
13
  * §7.1 steps 1 and 2 for one target, in their normative order. For the
14
14
  * target `I` itself both steps are vacuous. Registrations are planned on
@@ -98,6 +98,7 @@ export function reconcileRecord(target, step, signer) {
98
98
  * opened and this installation is the rotator, it rotates (§8.4).
99
99
  */
100
100
  export async function reconcileDevices(o) {
101
+ checkFlowProfiles(o);
101
102
  const target = await identityTarget(o);
102
103
  const me = o.installation.signer;
103
104
  const journal = new OperationJournal(o.storage);
@@ -7,7 +7,7 @@ import { newOperationId, OperationJournal, phaseOf } from "./journal.js";
7
7
  import { ChannelPool, IDENTITY_EXTENSION, IdentityRefused, IdentityUnavailable, } from "./network.js";
8
8
  import { classifyRevoke } from "./phases.js";
9
9
  import { identityTarget } from "./resource.js";
10
- import { exposingRevocationsSince, resumeRotations, rotateAfter, rotatorTest, } from "./rotation.js";
10
+ import { checkFlowProfiles, exposingRevocationsSince, resumeRotations, rotateAfter, rotatorTest, } from "./rotation.js";
11
11
  const toKeep = (state, root) => [...state.devices.entries()]
12
12
  .filter(([, d]) => bytesEqual(d.root, root))
13
13
  .map(([hex]) => principalId(fromHex(hex)))
@@ -131,6 +131,7 @@ class SelfDependent extends Error {
131
131
  * another reason is `complete` and the rotation test still runs.
132
132
  */
133
133
  export async function revokeDevice(o) {
134
+ checkFlowProfiles(o);
134
135
  const target = await identityTarget(o);
135
136
  const me = o.installation.signer.descriptor.principalId;
136
137
  const selfRetire = bytesEqual(me, o.device);
@@ -387,6 +388,7 @@ async function markRevoked(o, channel, journal, operation, I, device) {
387
388
  * nothing exposing happened since the last rotation.
388
389
  */
389
390
  export async function rotateIfExposed(o) {
391
+ checkFlowProfiles(o);
390
392
  // A rotation this operation already started resumes from its journaled phase: its
391
393
  // KEY_EPOCH may be committed while its DEK still waits in the pending slot (§9.1).
392
394
  const started = phaseOf(await new OperationJournal(o.storage).load(o.operation), `rotate:${toHex(o.resource)}`);
@@ -1,8 +1,8 @@
1
1
  import { type ControlRecordId, type DataEpoch, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
2
  import { type ResourceDEK } from "@openlfcp/crypto";
3
- import { type ControlState, type DataProfileCodec, type HaveVector, type PrincipalDescriptor } from "@openlfcp/wire";
3
+ import { type ControlState, type HaveVector, type PrincipalDescriptor } from "@openlfcp/wire";
4
4
  import { type KeyDelivery } from "./context.js";
5
- import type { FlowOptions } from "./devices.js";
5
+ import type { FlowOptions, UnitProfile } from "./devices.js";
6
6
  import { type ChannelPool, type IdentityChannel } from "./network.js";
7
7
  import { type LinearChain } from "./phases.js";
8
8
  /**
@@ -73,12 +73,15 @@ export interface FrontierData {
73
73
  /** The DEKs held for the Resource's epochs, by epoch number. */
74
74
  readonly deks: ReadonlyMap<string, ResourceDEK>;
75
75
  /**
76
- * The Resource's Data Profile codec, when the application gives it: its
77
- * `decode` is the profile's acceptance. Without it only decryption is
76
+ * The Resource's Data Profile, when the application gives it, as its
77
+ * receiver checks one unit: `codecFor({resourceId, actor})` is bound to the
78
+ * unit's Resource and signer, and the codec's `decode` is the profile's
79
+ * acceptance (the change's actor included). Without it only decryption is
78
80
  * checked here, and a profile refusal this installation recorded still
79
- * counts (LocalAccepted.refused).
81
+ * counts (LocalAccepted.refused). 0.3.1 took a bare codec, which cannot
82
+ * bind the actor: one without `codecFor` is refused (TypeError).
80
83
  */
81
- readonly profile?: DataProfileCodec<unknown>;
84
+ readonly profile?: UnitProfile;
82
85
  }
83
86
  export type RotationStatus =
84
87
  /** This installation committed the KEY_EPOCH and sealed the new DEK to every reader. */
@@ -117,6 +120,8 @@ export declare function rotateAfter(o: FlowOptions & {
117
120
  readonly resource: ResourceId;
118
121
  readonly revokedAt: bigint;
119
122
  }): Promise<RotationOutcome>;
123
+ /** The flow options of a rotation: 0.3.1's bare `dataProfiles` is refused, never checked less. */
124
+ export declare function checkFlowProfiles(o: FlowOptions): void;
120
125
  /**
121
126
  * Resumes every rotation a journaled operation started and did not adopt:
122
127
  * a KEY_EPOCH whose ACK was lost leaves its DEK in the operation's pending
@@ -58,6 +58,8 @@ export function exposingRevocationsSince(chain) {
58
58
  * signature, authorization and the actor chain are checked (0.3.0).
59
59
  */
60
60
  export async function verifiedFrontier(channel, chain, local = { units: [], snapshot: [] }, data) {
61
+ if (data?.profile !== undefined)
62
+ checkUnitProfile(data.profile, "FrontierData.profile");
61
63
  const R = chain.state.resourceId;
62
64
  const opened = await channel.open(R);
63
65
  const ranges = opened.kind === "nack"
@@ -103,7 +105,7 @@ export async function verifiedFrontier(channel, chain, local = { units: [], snap
103
105
  // covers are attested by it (W §29.3).
104
106
  if (!covered &&
105
107
  data !== undefined &&
106
- !(await acceptedAsData(chain, bytes, toHex(check.unitId), local, data)))
108
+ !(await acceptedAsData(chain, bytes, toHex(check.unitId), p.actor, local, data)))
107
109
  continue;
108
110
  const actor = toHex(p.actor);
109
111
  const last = latest.get(actor);
@@ -164,13 +166,14 @@ const opaque = (dataProfile) => ({
164
166
  * not decided here means those steps passed). A unit this client cannot
165
167
  * decrypt cannot be verified and stays outside the frontier (W §19).
166
168
  */
167
- async function acceptedAsData(chain, bytes, unitId, local, data) {
169
+ async function acceptedAsData(chain, bytes, unitId, actor, local, data) {
168
170
  if (local.accepted?.has(unitId) === true)
169
171
  return true;
170
172
  if (local.refused?.has(unitId) === true)
171
173
  return false;
174
+ // The receiver's own check of this unit: bound to its Resource and signer (W §26.3 rule 7).
172
175
  const profile = data.profile !== undefined && data.profile.dataProfile === chain.state.dataProfile
173
- ? data.profile
176
+ ? data.profile.codecFor({ resourceId: chain.state.resourceId, actor })
174
177
  : opaque(chain.state.dataProfile);
175
178
  const r = await receiveDataUnit(chain, bytes, {
176
179
  seen: new InMemorySeenUnits(),
@@ -191,6 +194,7 @@ async function acceptedAsData(chain, bytes, unitId, local, data) {
191
194
  * readers and receive nothing.
192
195
  */
193
196
  export async function rotateAfter(o) {
197
+ checkFlowProfiles(o);
194
198
  const journal = new OperationJournal(o.storage);
195
199
  const me = o.installation.signer;
196
200
  const R = o.resource;
@@ -278,11 +282,28 @@ export async function rotateAfter(o) {
278
282
  * stored accepted units and verified Snapshots, when the storage keeps them),
279
283
  * merged into the verified frontier (§8.3).
280
284
  */
281
- /** The codec the application gave for the Resource's Data Profile, if any. */
285
+ /** The profile the application gave for the Resource's Data Profile, if any. */
282
286
  const profileFor = (o, chain) => {
283
- const profile = o.dataProfiles?.find((p) => p.dataProfile === chain.state.dataProfile);
287
+ const profile = o.profiles?.find((p) => p.dataProfile === chain.state.dataProfile);
284
288
  return profile === undefined ? {} : { profile };
285
289
  };
290
+ /**
291
+ * A profile given where a per-unit check is needed must be one: a bare codec
292
+ * (0.3.1's FlowOptions.dataProfiles, FrontierData.profile) cannot bind the
293
+ * change's actor to the unit's signer, so it is refused, never used as a
294
+ * weaker check (W §26.3 rule 7).
295
+ */
296
+ function checkUnitProfile(profile, where) {
297
+ if (typeof profile.codecFor !== "function")
298
+ throw new TypeError(`${where} needs a per-unit profile (dataProfile and codecFor({resourceId, actor})), not a bare codec`);
299
+ }
300
+ /** The flow options of a rotation: 0.3.1's bare `dataProfiles` is refused, never checked less. */
301
+ export function checkFlowProfiles(o) {
302
+ if (o.dataProfiles !== undefined)
303
+ throw new TypeError("FlowOptions.dataProfiles (bare codecs, 0.3.1) is replaced by FlowOptions.profiles (codecFor per unit): a codec without the unit's signer cannot check the change's actor");
304
+ for (const p of o.profiles ?? [])
305
+ checkUnitProfile(p, "FlowOptions.profiles");
306
+ }
286
307
  async function localAccepted(o, chain) {
287
308
  const st = o.storage;
288
309
  if (st.dataUnits === undefined || st.snapshots === undefined)
@@ -328,6 +349,7 @@ async function localAccepted(o, chain) {
328
349
  * it (§9.1, §9.2). Run before planning new work on a Resource.
329
350
  */
330
351
  export async function resumeRotations(o) {
352
+ checkFlowProfiles(o);
331
353
  const journal = new OperationJournal(o.storage);
332
354
  const out = [];
333
355
  for (const op of await journal.list())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlfcp/client",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "LFCP client: session, Control Plane and Data Plane synchronization.",
5
5
  "keywords": [
6
6
  "openlfcp",
@@ -42,9 +42,9 @@
42
42
  "tag": "latest"
43
43
  },
44
44
  "dependencies": {
45
- "@openlfcp/core": "^0.3.1",
46
- "@openlfcp/crypto": "^0.3.1",
47
- "@openlfcp/storage": "^0.3.1",
48
- "@openlfcp/wire": "^0.3.1"
45
+ "@openlfcp/core": "^0.4.0",
46
+ "@openlfcp/storage": "^0.4.0",
47
+ "@openlfcp/wire": "^0.4.0",
48
+ "@openlfcp/crypto": "^0.4.0"
49
49
  }
50
50
  }