@openlfcp/client 0.3.1 → 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/dist/identity/context.d.ts +33 -1
- package/dist/identity/context.js +98 -11
- package/dist/identity/devices.d.ts +18 -5
- package/dist/identity/document.d.ts +23 -0
- package/dist/identity/index.d.ts +4 -4
- package/dist/identity/index.js +2 -2
- package/dist/identity/pairing-request.d.ts +12 -0
- package/dist/identity/pairing-request.js +40 -9
- package/dist/identity/pairing.d.ts +11 -0
- package/dist/identity/pairing.js +20 -2
- package/dist/identity/reconcile.d.ts +73 -3
- package/dist/identity/reconcile.js +165 -36
- package/dist/identity/revocation.d.ts +11 -1
- package/dist/identity/revocation.js +69 -13
- package/dist/identity/rotation.d.ts +20 -7
- package/dist/identity/rotation.js +35 -8
- package/package.json +5 -5
|
@@ -4,7 +4,7 @@ import { type LfcpStorage, type SecretStore } from "@openlfcp/storage";
|
|
|
4
4
|
import { type DataUnitPayload, type Parsed, type PrincipalDescriptor, type Signer } from "@openlfcp/wire";
|
|
5
5
|
import { type IdentityDocumentPort, type IdentityIntent } from "./document.js";
|
|
6
6
|
import { type JournalPhase, type OperationJournal, type PackageLedger } from "./journal.js";
|
|
7
|
-
import type
|
|
7
|
+
import { type IdentityChannel } from "./network.js";
|
|
8
8
|
import type { LinearChain, PhaseState } from "./phases.js";
|
|
9
9
|
/** The storage the identity flows use. */
|
|
10
10
|
export type IdentityStorage = Pick<LfcpStorage, "control" | "commit" | "localMarks" | "actorSequences">;
|
|
@@ -61,6 +61,13 @@ export interface PhaseOutcome {
|
|
|
61
61
|
* with the same ACK, W §47), a record rebuilt on a new head otherwise.
|
|
62
62
|
*/
|
|
63
63
|
export declare function runControlPhase(storage: IdentityStorage, channel: IdentityChannel, journal: OperationJournal, operation: string, spec: ControlPhaseSpec): Promise<PhaseOutcome>;
|
|
64
|
+
/**
|
|
65
|
+
* The client-local refusal code of a device record or same-root Key Package
|
|
66
|
+
* this client does not send to a server whose READY lacks `identity-01`
|
|
67
|
+
* (LFCP-WIRE-01 §37): the Resource is hosted on a server without identity
|
|
68
|
+
* support (IDENTITY-PROFILE-01 §6). No wire code.
|
|
69
|
+
*/
|
|
70
|
+
export declare const IDENTITY_UNSUPPORTED = "IDENTITY_UNSUPPORTED";
|
|
64
71
|
/** The DEKs this client holds for `chain`'s epochs, each checked against its commitment. */
|
|
65
72
|
export declare function heldDeks(secrets: SecretStore, chain: LinearChain): Promise<Map<string, ResourceDEK>>;
|
|
66
73
|
/** Stores the DEK of `epoch` (secret first) and links the epoch rows to it (LFCP-034). */
|
|
@@ -72,6 +79,16 @@ export declare function storeDek(storage: IdentityStorage, secrets: SecretStore,
|
|
|
72
79
|
export declare function fetchDeks(storage: IdentityStorage, secrets: SecretStore, channel: IdentityChannel, chain: LinearChain, recipient: Installation): Promise<Map<string, ResourceDEK>>;
|
|
73
80
|
/** Every Principal with `holds(P, 1)` at the head (W §19, §25.2): the reader set, by ID. */
|
|
74
81
|
export declare function readersAt(state: LinearChain["state"]): readonly PrincipalDescriptor[];
|
|
82
|
+
/**
|
|
83
|
+
* IDENTITY-PROFILE-01 §7.1 step 1: whether `principal`, a reader of the
|
|
84
|
+
* target state, is a device that the Identity Resource `identity` has
|
|
85
|
+
* revoked, or whose current chain in the target passes through such a
|
|
86
|
+
* device. Until the pair is propagated the target still counts the device
|
|
87
|
+
* as active (an unpropagated pair), but the installations of `identity`'s
|
|
88
|
+
* root treat it as revoked there and seal nothing to it, in a rotation as
|
|
89
|
+
* in step 3.
|
|
90
|
+
*/
|
|
91
|
+
export declare function revokedByIdentity(identity: LinearChain, target: LinearChain["state"], principal: PrincipalId): boolean;
|
|
75
92
|
export interface KeyDelivery {
|
|
76
93
|
/** Packages sealed and acknowledged now, as (epoch, recipient hex). */
|
|
77
94
|
readonly sent: readonly {
|
|
@@ -136,6 +153,14 @@ export interface IdentityDocumentLoad {
|
|
|
136
153
|
}
|
|
137
154
|
/** The Genesis owner of a chain: the root of an Identity Resource (§2.3). */
|
|
138
155
|
export declare function genesisOwner(chain: LinearChain): PrincipalId;
|
|
156
|
+
/**
|
|
157
|
+
* Whether the retired entry `devices[key]` needs its flag (§7.1 step 4):
|
|
158
|
+
* some value of its `revoked` is not `true`, or the field is absent. A port
|
|
159
|
+
* without `retirement` shows one value. False when there is no valid entry.
|
|
160
|
+
*/
|
|
161
|
+
export declare function retiredFlagMissing(entries: ReadonlyMap<string, {
|
|
162
|
+
readonly revoked?: boolean;
|
|
163
|
+
}>, key: string, revokedValues?: (key: string) => readonly unknown[]): boolean;
|
|
139
164
|
/**
|
|
140
165
|
* A new unit of `signer` in the Identity Resource carrying `intents` as
|
|
141
166
|
* one change: the next actor sequence from the storage's reservation
|
|
@@ -150,6 +175,13 @@ export declare function sealIdentityUnit(options: {
|
|
|
150
175
|
readonly doc: IdentityDocumentPort;
|
|
151
176
|
readonly latest: IdentityDocumentLoad["latest"];
|
|
152
177
|
readonly intents: readonly IdentityIntent[];
|
|
178
|
+
/**
|
|
179
|
+
* Retired entries whose `revoked` flag the change sets (§7.1 step 4,
|
|
180
|
+
* §8.2): field by field when the port has `retirement`, else with
|
|
181
|
+
* `device.put` of the entry's display fields (its `missing_keys` hint is
|
|
182
|
+
* the device's own, §3 AI5).
|
|
183
|
+
*/
|
|
184
|
+
readonly retire?: readonly string[];
|
|
153
185
|
/** The sequence to use instead of a reservation (a pairing device shared by installations, §5.3). */
|
|
154
186
|
readonly seq?: bigint;
|
|
155
187
|
}): Promise<Uint8Array>;
|
package/dist/identity/context.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { actorSequence, bytesEqual, controlRecordId, dataEpoch, fromHex, hash32, LfcpError, toHex, } from "@openlfcp/core";
|
|
2
2
|
import { dekCommitment, exportSecretKeyBytes, importResourceDEK, sha256, } from "@openlfcp/crypto";
|
|
3
3
|
import { dekSecretRef } from "@openlfcp/storage";
|
|
4
|
-
import { ABILITY, hasAbility, InMemorySeenUnits, parseDataUnit, parseKeyPackage, receiveDataUnit, receiveKeyPackage, sealDataUnit, sealKeyPackage, validateControlChain, verifyKeyPackage, verifySignedObject, } from "@openlfcp/wire";
|
|
4
|
+
import { ABILITY, decodeControlRecord, hasAbility, InMemorySeenUnits, parseDataUnit, parseKeyPackage, receiveDataUnit, receiveKeyPackage, revocationOf, sealDataUnit, sealKeyPackage, validateControlChain, verifyKeyPackage, verifySignedObject, } from "@openlfcp/wire";
|
|
5
5
|
import { adoptStoredDeks, checkWriteBarrier, loadControlChain, saveControlChain, saveControlConflict, } from "../storage.js";
|
|
6
6
|
import { identityPlaintextCodec, identityWriterClass, } from "./document.js";
|
|
7
7
|
import { phaseOf, } from "./journal.js";
|
|
8
|
-
import { IdentityUnavailable } from "./network.js";
|
|
8
|
+
import { IDENTITY_EXTENSION, IdentityUnavailable } from "./network.js";
|
|
9
9
|
/** The Resource or the Identity Resource is forked (§9.7): approval, reconciliation and rotation are suspended. */
|
|
10
10
|
export class IdentityConflict extends Error {
|
|
11
11
|
resource;
|
|
@@ -104,11 +104,22 @@ export async function runControlPhase(storage, channel, journal, operation, spec
|
|
|
104
104
|
if (attempt >= MAX_HEAD_RETRIES)
|
|
105
105
|
throw new IdentityUnavailable(`the head kept moving while proposing ${spec.id}`);
|
|
106
106
|
const head = toHex(chain.state.head);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
bytes
|
|
107
|
+
const journaled = spec.resend !== false && before?.bytes !== undefined && before.expectedHead === head;
|
|
108
|
+
const bytes = journaled ? fromHex(before.bytes) : await spec.build(chain);
|
|
109
|
+
if (!channel.extensions.includes(IDENTITY_EXTENSION) && isDeviceRecord(bytes)) {
|
|
110
|
+
// W §37: no record of type 9 or 10 to a server whose READY lacks identity-01,
|
|
111
|
+
// journaled bytes included (a resumed operation checks the current READY).
|
|
112
|
+
await journal.putPhase(operation, {
|
|
113
|
+
...(before ?? {}),
|
|
114
|
+
id: spec.id,
|
|
115
|
+
kind: spec.kind,
|
|
116
|
+
resource: toHex(R),
|
|
117
|
+
state: "refused",
|
|
118
|
+
code: IDENTITY_UNSUPPORTED,
|
|
119
|
+
});
|
|
120
|
+
return Object.freeze({ state: "refused", chain, code: IDENTITY_UNSUPPORTED });
|
|
121
|
+
}
|
|
122
|
+
if (!journaled) {
|
|
112
123
|
const id = toHex(validateRecordId(bytes));
|
|
113
124
|
await journal.putPhase(operation, {
|
|
114
125
|
id: spec.id,
|
|
@@ -136,6 +147,18 @@ export async function runControlPhase(storage, channel, journal, operation, spec
|
|
|
136
147
|
return Object.freeze({ state: "refused", chain, code: answer.code });
|
|
137
148
|
}
|
|
138
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* The client-local refusal code of a device record or same-root Key Package
|
|
152
|
+
* this client does not send to a server whose READY lacks `identity-01`
|
|
153
|
+
* (LFCP-WIRE-01 §37): the Resource is hosted on a server without identity
|
|
154
|
+
* support (IDENTITY-PROFILE-01 §6). No wire code.
|
|
155
|
+
*/
|
|
156
|
+
export const IDENTITY_UNSUPPORTED = "IDENTITY_UNSUPPORTED";
|
|
157
|
+
/** A DEVICE_GRANT (type 9) or DEVICE_REVOKE (type 10). */
|
|
158
|
+
function isDeviceRecord(bytes) {
|
|
159
|
+
const type = decodeControlRecord(bytes).body.type;
|
|
160
|
+
return type === "DEVICE_GRANT" || type === "DEVICE_REVOKE";
|
|
161
|
+
}
|
|
139
162
|
/** A Control Record's ID: SHA-256 of its exact bytes (W §13). */
|
|
140
163
|
const validateRecordId = (bytes) => controlRecordId(sha256(bytes));
|
|
141
164
|
function ownRecordOn(chain, sent) {
|
|
@@ -219,6 +242,23 @@ export function readersAt(state) {
|
|
|
219
242
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
220
243
|
.map(([, d]) => d));
|
|
221
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* IDENTITY-PROFILE-01 §7.1 step 1: whether `principal`, a reader of the
|
|
247
|
+
* target state, is a device that the Identity Resource `identity` has
|
|
248
|
+
* revoked, or whose current chain in the target passes through such a
|
|
249
|
+
* device. Until the pair is propagated the target still counts the device
|
|
250
|
+
* as active (an unpropagated pair), but the installations of `identity`'s
|
|
251
|
+
* root treat it as revoked there and seal nothing to it, in a rotation as
|
|
252
|
+
* in step 3.
|
|
253
|
+
*/
|
|
254
|
+
export function revokedByIdentity(identity, target, principal) {
|
|
255
|
+
const R = genesisOwner(identity);
|
|
256
|
+
const revoked = (d) => revocationOf(identity.state, R, d) !== undefined;
|
|
257
|
+
if (revoked(principal))
|
|
258
|
+
return true;
|
|
259
|
+
const binding = target.devices.get(toHex(principal));
|
|
260
|
+
return binding !== undefined && bytesEqual(binding.root, R) && binding.issuers.some(revoked);
|
|
261
|
+
}
|
|
222
262
|
/**
|
|
223
263
|
* Seals every DEK of `deks` to each recipient at the chain's head and
|
|
224
264
|
* records each acknowledged package (§9.5). A package the ledger records is
|
|
@@ -233,6 +273,21 @@ export async function deliverKeys(options) {
|
|
|
233
273
|
const skipped = [];
|
|
234
274
|
let complete = 0;
|
|
235
275
|
const epochs = [...options.deks.keys()].sort((a, b) => (BigInt(a) < BigInt(b) ? -1 : 1));
|
|
276
|
+
// W §37: without identity-01 the server applies no same-root rule (W §25.2); a sender
|
|
277
|
+
// without key/distribute is authorized only by it, and sends nothing there.
|
|
278
|
+
const identity = options.channel.extensions.includes(IDENTITY_EXTENSION);
|
|
279
|
+
const sameRootOnly = !identity && !hasAbility(chain.state, sender.descriptor.principalId, ABILITY.KEY_DISTRIBUTE);
|
|
280
|
+
// The same for a pending package, at its own referenced head: a later key/distribute does
|
|
281
|
+
// not authorize it there (W §25.2), so it is re-sealed at the current head instead.
|
|
282
|
+
const resendable = (bytes) => {
|
|
283
|
+
const parsed = parseKeyPackage(bytes);
|
|
284
|
+
if (verifyKeyPackage(chain, parsed).kind !== "authorized")
|
|
285
|
+
return false;
|
|
286
|
+
if (identity)
|
|
287
|
+
return true;
|
|
288
|
+
const atHead = chain.stateAt(parsed.payload.controlHead);
|
|
289
|
+
return (atHead !== undefined && hasAbility(atHead, parsed.payload.sender, ABILITY.KEY_DISTRIBUTE));
|
|
290
|
+
};
|
|
236
291
|
for (const recipient of options.recipients) {
|
|
237
292
|
const rid = recipient.principalId;
|
|
238
293
|
const hex = toHex(rid);
|
|
@@ -242,6 +297,10 @@ export async function deliverKeys(options) {
|
|
|
242
297
|
skipped.push({ recipient: hex, reason: "not a reader at the head" });
|
|
243
298
|
continue;
|
|
244
299
|
}
|
|
300
|
+
if (sameRootOnly) {
|
|
301
|
+
skipped.push({ recipient: hex, reason: IDENTITY_UNSUPPORTED });
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
245
304
|
for (const e of epochs) {
|
|
246
305
|
const epoch = dataEpoch(BigInt(e));
|
|
247
306
|
if ((await ledger.has(R, epoch, rid)) && !(options.resend?.get(hex)?.has(e) ?? false)) {
|
|
@@ -250,8 +309,7 @@ export async function deliverKeys(options) {
|
|
|
250
309
|
}
|
|
251
310
|
// §9.1: a package sent before is sent again as the same bytes, while it is still authorized.
|
|
252
311
|
let bytes = await ledger.pending(R, epoch, rid);
|
|
253
|
-
if (bytes !== undefined &&
|
|
254
|
-
verifyKeyPackage(chain, parseKeyPackage(bytes)).kind !== "authorized")
|
|
312
|
+
if (bytes !== undefined && !resendable(bytes))
|
|
255
313
|
bytes = undefined;
|
|
256
314
|
if (bytes === undefined) {
|
|
257
315
|
const sealed = await sealKeyPackage({
|
|
@@ -368,6 +426,28 @@ export function genesisOwner(chain) {
|
|
|
368
426
|
const g = chain.records[0];
|
|
369
427
|
return g?.body.type === "GENESIS" ? g.body.owner.principalId : chain.state.owner.principalId;
|
|
370
428
|
}
|
|
429
|
+
/** 0.4's flag write on a port without `retirement`: the entry's display fields and the flag. */
|
|
430
|
+
function retiredPut(doc, key) {
|
|
431
|
+
const entry = doc.devices().get(key);
|
|
432
|
+
if (entry === undefined)
|
|
433
|
+
return [];
|
|
434
|
+
const { missing_keys: _hint, ...display } = entry;
|
|
435
|
+
return [{ intent: "device.put", key, entry: { ...display, revoked: true } }];
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Whether the retired entry `devices[key]` needs its flag (§7.1 step 4):
|
|
439
|
+
* some value of its `revoked` is not `true`, or the field is absent. A port
|
|
440
|
+
* without `retirement` shows one value. False when there is no valid entry.
|
|
441
|
+
*/
|
|
442
|
+
export function retiredFlagMissing(entries, key, revokedValues) {
|
|
443
|
+
const entry = entries.get(key);
|
|
444
|
+
if (entry === undefined)
|
|
445
|
+
return false;
|
|
446
|
+
if (revokedValues === undefined)
|
|
447
|
+
return entry.revoked !== true;
|
|
448
|
+
const values = revokedValues(key);
|
|
449
|
+
return values.length === 0 || values.some((v) => v !== true);
|
|
450
|
+
}
|
|
371
451
|
/**
|
|
372
452
|
* A new unit of `signer` in the Identity Resource carrying `intents` as
|
|
373
453
|
* one change: the next actor sequence from the storage's reservation
|
|
@@ -390,11 +470,18 @@ export async function sealIdentityUnit(options) {
|
|
|
390
470
|
while (previous !== undefined && seq <= previous.seq)
|
|
391
471
|
seq = await options.storage.actorSequences.reserveNext(R, me);
|
|
392
472
|
}
|
|
393
|
-
const
|
|
473
|
+
const author = {
|
|
394
474
|
signer: me,
|
|
395
475
|
actorSeq: seq,
|
|
396
476
|
writerClass: identityWriterClass(chain.state, genesisOwner(chain), me),
|
|
397
|
-
}
|
|
477
|
+
};
|
|
478
|
+
const retire = options.retire ?? [];
|
|
479
|
+
const { doc } = options;
|
|
480
|
+
const plaintext = retire.length === 0
|
|
481
|
+
? doc.write(author, options.intents)
|
|
482
|
+
: doc.retirement !== undefined
|
|
483
|
+
? doc.retirement.write(author, options.intents, retire)
|
|
484
|
+
: doc.write(author, [...options.intents, ...retire.flatMap((key) => retiredPut(doc, key))]);
|
|
398
485
|
return sealDataUnit({
|
|
399
486
|
resourceId: R,
|
|
400
487
|
dataEpoch: epoch,
|
|
@@ -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
|
|
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
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
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
|
|
@@ -95,6 +95,29 @@ export interface IdentityDocumentPort {
|
|
|
95
95
|
readonly hash: string;
|
|
96
96
|
readonly signer: PrincipalId;
|
|
97
97
|
}[];
|
|
98
|
+
/**
|
|
99
|
+
* Optional (0.5.0): a device entry's `revoked` flag field by field, for
|
|
100
|
+
* a retired entry (§7.1 step 4, §8.2). With it, reconciliation sees every
|
|
101
|
+
* concurrent value of the flag and sets it without writing the entry's
|
|
102
|
+
* other fields, so it never resolves a conflict on them (a concurrent
|
|
103
|
+
* rename). A port without it shows one value per field (`devices()`), and
|
|
104
|
+
* the flag is written with `device.put` of the entry's display fields, as
|
|
105
|
+
* in 0.4. IdentityReplica provides both (`fieldValues`, `write`'s
|
|
106
|
+
* `retire` option).
|
|
107
|
+
*/
|
|
108
|
+
readonly retirement?: IdentityRetirementPort;
|
|
109
|
+
}
|
|
110
|
+
/** IdentityDocumentPort.retirement. */
|
|
111
|
+
export interface IdentityRetirementPort {
|
|
112
|
+
/** Every value `devices[key].revoked` holds now: several under concurrent writes, none when absent. */
|
|
113
|
+
revokedValues(key: string): readonly unknown[];
|
|
114
|
+
/**
|
|
115
|
+
* Like IdentityDocumentPort.write, the plaintext of one change by
|
|
116
|
+
* `author`: `intents` in order (possibly none), then `revoked = true` in
|
|
117
|
+
* each existing entry `devices[key]` of `retire`, no other field of those
|
|
118
|
+
* entries written.
|
|
119
|
+
*/
|
|
120
|
+
write(author: IdentityAuthor, intents: readonly IdentityIntent[], retire: readonly string[]): Uint8Array;
|
|
98
121
|
}
|
|
99
122
|
/** A passthrough Data Unit codec: the port produces and consumes the plaintext. */
|
|
100
123
|
export declare const identityPlaintextCodec: DataProfileCodec<Uint8Array>;
|
package/dist/identity/index.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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";
|
|
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";
|
|
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
|
+
export { type DeviceEntryValue, IDENTITY_PROFILE, type IdentityAuthor, type IdentityDocumentPort, type IdentityIntent, type IdentityRetirementPort, 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";
|
|
8
8
|
export { type ApprovalResult, approvedGrantRecord, approvePairing, type CancelResult, cancelPairing, cancelRecord, consumeRecord, createPairingSession, openPairingSessions, PairingLinkText, type PairingReview, type PairingSessionResult, pairingRequests, resumeApproval, resumePairingSession, } from "./pairing.js";
|
|
9
|
-
export { type CandidateEvidence, type CandidateResult, type CandidateStatus, candidateEvidence, completePairing, type PairingOutcome, type RequestPairingOptions, type RequestPairingResult, requestPairing, } from "./pairing-request.js";
|
|
9
|
+
export { type CandidateEvidence, type CandidateResult, type CandidateStatus, candidateEvidence, completePairing, type PairingOutcome, pairingLinkRejection, type RequestPairingOptions, type RequestPairingResult, requestPairing, } from "./pairing-request.js";
|
|
10
10
|
export { certificateInScope, classifyCancel, classifyConsume, classifyPackage, classifyRegister, classifyRevoke, classifyRotate, keyEpochAfter, type LinearChain, type PhaseState, type RegisterPhase, recordSeq, } from "./phases.js";
|
|
11
11
|
export { type RevocationStatus, revocationStatus } from "./proof.js";
|
|
12
|
-
export { planReconciliation, type ReconcileStep, reconcileDevices, reconcileRecord, type TargetReconciliation, } from "./reconcile.js";
|
|
12
|
+
export { type DisplayPlan, keyRecipients, planDisplayData, planReconciliation, planRevocations, type ReconcileStep, reconcileDevices, reconcileRecord, revocationRelevant, type TargetReconciliation, } from "./reconcile.js";
|
|
13
13
|
export { type CreatedIdentityResource, type CreateIdentityResourceOptions, createIdentityResource, type IdentityDocumentFactory, type IdentityFlowOptions, identityTarget, localIdentityResource, resumeIdentityCreation, setLocalIdentityResource, } from "./resource.js";
|
|
14
14
|
export { previewRevocation, type ResourcePreview, type RevocationPreview, type RevocationResult, type RevokeDeviceResult, revokeDevice, rotateIfExposed, } from "./revocation.js";
|
|
15
15
|
export { type AddToRosterOptions, type AddToRosterResult, addToRoster, type RosterWrite, writeRosterEntry, } from "./roster.js";
|
package/dist/identity/index.js
CHANGED
|
@@ -6,10 +6,10 @@ export { IDENTITY_PROFILE, identityPlaintextCodec, identityWriterClass, principa
|
|
|
6
6
|
export { OperationJournal, PackageLedger, pairingSecretRef, pendingDekRef, } from "./journal.js";
|
|
7
7
|
export { ChannelPool, connectionNetwork, IDENTITY_EXTENSION, IdentityRefused, IdentityUnavailable, } from "./network.js";
|
|
8
8
|
export { approvedGrantRecord, approvePairing, cancelPairing, cancelRecord, consumeRecord, createPairingSession, openPairingSessions, PairingLinkText, pairingRequests, resumeApproval, resumePairingSession, } from "./pairing.js";
|
|
9
|
-
export { candidateEvidence, completePairing, requestPairing, } from "./pairing-request.js";
|
|
9
|
+
export { candidateEvidence, completePairing, pairingLinkRejection, requestPairing, } from "./pairing-request.js";
|
|
10
10
|
export { certificateInScope, classifyCancel, classifyConsume, classifyPackage, classifyRegister, classifyRevoke, classifyRotate, keyEpochAfter, recordSeq, } from "./phases.js";
|
|
11
11
|
export { revocationStatus } from "./proof.js";
|
|
12
|
-
export { planReconciliation, reconcileDevices, reconcileRecord, } from "./reconcile.js";
|
|
12
|
+
export { keyRecipients, planDisplayData, planReconciliation, planRevocations, reconcileDevices, reconcileRecord, revocationRelevant, } from "./reconcile.js";
|
|
13
13
|
export { createIdentityResource, identityTarget, localIdentityResource, resumeIdentityCreation, setLocalIdentityResource, } from "./resource.js";
|
|
14
14
|
export { previewRevocation, revokeDevice, rotateIfExposed, } from "./revocation.js";
|
|
15
15
|
export { addToRoster, writeRosterEntry, } from "./roster.js";
|
|
@@ -81,6 +81,18 @@ export type RequestPairingResult = {
|
|
|
81
81
|
readonly operation?: string;
|
|
82
82
|
readonly reason: string;
|
|
83
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* §5.2, the checks after the link parses and its secret is its device's
|
|
86
|
+
* (`parsePairingUri`, `pairingDeviceSigner`), on what opening `I` as `J`
|
|
87
|
+
* gave: the session as `J` is refused (W §41), `I`'s Genesis profile is not
|
|
88
|
+
* `org.openlfcp.identity.v1`, or `J` is not a registered, active kind-2
|
|
89
|
+
* device of `I`'s owner in `I`'s chain. Returns why the link is rejected
|
|
90
|
+
* (a client-local outcome, the corpora's PAIRING_LINK_REJECTED), or
|
|
91
|
+
* undefined when it passes; a rejected link is not used any further.
|
|
92
|
+
*/
|
|
93
|
+
export declare function pairingLinkRejection(J: PrincipalId, opened: LinearChain | {
|
|
94
|
+
readonly refused: string;
|
|
95
|
+
}): string | undefined;
|
|
84
96
|
/** §5.3 steps 2–3. */
|
|
85
97
|
export declare function requestPairing(o: RequestPairingOptions): Promise<RequestPairingResult>;
|
|
86
98
|
/** §6: the status of one Resource for the new device, from its own observations only. */
|
|
@@ -5,6 +5,32 @@ import { IDENTITY_PROFILE, principalRefOf, resourceRef, } from "./document.js";
|
|
|
5
5
|
import { newOperationId, OperationJournal, pairingSecretRef, phaseOf } from "./journal.js";
|
|
6
6
|
import { ChannelPool, IDENTITY_EXTENSION, IdentityRefused, IdentityUnavailable, } from "./network.js";
|
|
7
7
|
import { setLocalIdentityResource } from "./resource.js";
|
|
8
|
+
/**
|
|
9
|
+
* §5.2, the checks after the link parses and its secret is its device's
|
|
10
|
+
* (`parsePairingUri`, `pairingDeviceSigner`), on what opening `I` as `J`
|
|
11
|
+
* gave: the session as `J` is refused (W §41), `I`'s Genesis profile is not
|
|
12
|
+
* `org.openlfcp.identity.v1`, or `J` is not a registered, active kind-2
|
|
13
|
+
* device of `I`'s owner in `I`'s chain. Returns why the link is rejected
|
|
14
|
+
* (a client-local outcome, the corpora's PAIRING_LINK_REJECTED), or
|
|
15
|
+
* undefined when it passes; a rejected link is not used any further.
|
|
16
|
+
*/
|
|
17
|
+
export function pairingLinkRejection(J, opened) {
|
|
18
|
+
if ("refused" in opened)
|
|
19
|
+
return `the session as the link's device is refused (${opened.refused})`;
|
|
20
|
+
const state = opened.state;
|
|
21
|
+
if (state.dataProfile !== IDENTITY_PROFILE)
|
|
22
|
+
return `the Resource is not an Identity Resource (its Genesis profile is ${state.dataProfile})`;
|
|
23
|
+
const R = genesisOwner(opened);
|
|
24
|
+
const binding = state.devices.get(toHex(J));
|
|
25
|
+
const j = activeDevice(state, J);
|
|
26
|
+
if (binding === undefined ||
|
|
27
|
+
!binding.registered ||
|
|
28
|
+
j === undefined ||
|
|
29
|
+
j.kind !== DEVICE_KIND.PAIRING ||
|
|
30
|
+
!bytesEqual(j.root, R))
|
|
31
|
+
return "the link's device is not a registered, active pairing device of the owner";
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
8
34
|
/** §5.3 steps 2–3. */
|
|
9
35
|
export async function requestPairing(o) {
|
|
10
36
|
let link;
|
|
@@ -39,16 +65,21 @@ export async function requestPairing(o) {
|
|
|
39
65
|
};
|
|
40
66
|
}
|
|
41
67
|
const channel = await pool.get(J, url);
|
|
42
|
-
|
|
68
|
+
let chain;
|
|
69
|
+
try {
|
|
70
|
+
chain = await syncChain(o.storage, channel, I);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
// §5.2 check 2: the coordinator refuses the session as J (W §41).
|
|
74
|
+
if (!(e instanceof IdentityRefused))
|
|
75
|
+
throw e;
|
|
76
|
+
const reason = pairingLinkRejection(J.descriptor.principalId, { refused: e.code });
|
|
77
|
+
return { kind: "not-a-session", reason: reason ?? e.message };
|
|
78
|
+
}
|
|
43
79
|
const R = genesisOwner(chain);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (j === undefined || j.kind !== DEVICE_KIND.PAIRING || !bytesEqual(j.root, R))
|
|
48
|
-
return {
|
|
49
|
-
kind: "not-a-session",
|
|
50
|
-
reason: "the link's device is not an active pairing device of the owner",
|
|
51
|
-
};
|
|
80
|
+
const rejected = pairingLinkRejection(J.descriptor.principalId, chain);
|
|
81
|
+
if (rejected !== undefined)
|
|
82
|
+
return { kind: "not-a-session", reason: rejected };
|
|
52
83
|
const pairing = { signer: J, agreement: link.secret.agreementKey };
|
|
53
84
|
const deks = await fetchDeks(o.storage, o.secrets, channel, chain, pairing);
|
|
54
85
|
const doc = o.document(I, R);
|
|
@@ -43,6 +43,17 @@ export type PairingSessionResult = {
|
|
|
43
43
|
| {
|
|
44
44
|
readonly kind: "not-authorized";
|
|
45
45
|
readonly reason: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `I`'s coordinator does not list `identity-01` in READY: it is hosted on
|
|
49
|
+
* a server without identity support (IDENTITY-PROFILE-01 §6). Nothing is
|
|
50
|
+
* proposed or uploaded there: no DEVICE_GRANT, no same-root Key Package
|
|
51
|
+
* (LFCP-WIRE-01 §37).
|
|
52
|
+
*/
|
|
53
|
+
| {
|
|
54
|
+
readonly kind: "unsupported";
|
|
55
|
+
readonly session?: string;
|
|
56
|
+
readonly reason: string;
|
|
46
57
|
} | {
|
|
47
58
|
readonly kind: "refused";
|
|
48
59
|
readonly code: string;
|
package/dist/identity/pairing.js
CHANGED
|
@@ -5,10 +5,11 @@ import { deliverKeys, fetchDeks, genesisOwner, IdentityConflict, loadIdentityDoc
|
|
|
5
5
|
import { checkIssuance, leafPayload, MASK_PRESETS, mayManageDevices, registerDevice, signChain, } from "./devices.js";
|
|
6
6
|
import { identityPlaintextCodec, principalOfRef, principalRefOf, } from "./document.js";
|
|
7
7
|
import { newOperationId, OperationJournal, PackageLedger, pairingSecretRef, phaseOf, } from "./journal.js";
|
|
8
|
-
import { ChannelPool, IdentityUnavailable } from "./network.js";
|
|
8
|
+
import { ChannelPool, IDENTITY_EXTENSION, 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
|
|
@@ -39,6 +40,8 @@ export class PairingLinkText {
|
|
|
39
40
|
return "[redacted]";
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
/** W §37: the reason of an `unsupported` result. */
|
|
44
|
+
const NO_IDENTITY_SUPPORT = "the Identity Resource is hosted on a server without identity support (its READY does not list identity-01)";
|
|
42
45
|
const PAIRING_MASK = [1n, 2n];
|
|
43
46
|
/** Wraps a flow body: transport failures and forks become results, the journal stays. */
|
|
44
47
|
async function guarded(pool, body, unavailable, conflict) {
|
|
@@ -69,6 +72,8 @@ export async function createPairingSession(o) {
|
|
|
69
72
|
const me = o.installation.signer;
|
|
70
73
|
return guarded(pool, async () => {
|
|
71
74
|
const channel = await pool.get(me, target.url);
|
|
75
|
+
if (!channel.extensions.includes(IDENTITY_EXTENSION))
|
|
76
|
+
return { kind: "unsupported", reason: NO_IDENTITY_SUPPORT };
|
|
72
77
|
const chain = await syncChain(o.storage, channel, target.resource);
|
|
73
78
|
const R = genesisOwner(chain);
|
|
74
79
|
if (!mayManageDevices(chain.state, me.descriptor.principalId, R))
|
|
@@ -142,7 +147,14 @@ async function missingMaterial(o, channel, chain) {
|
|
|
142
147
|
dek: (epoch) => deks.get(String(epoch)),
|
|
143
148
|
profile: identityPlaintextCodec,
|
|
144
149
|
});
|
|
145
|
-
|
|
150
|
+
// A unit held whose only failure is the missing DEK of a real epoch of I (every other
|
|
151
|
+
// check passed, and that DEK is listed above) is not missing itself. A unit naming an
|
|
152
|
+
// epoch I does not have, or failing any other check, is missing (§5.3 step 1).
|
|
153
|
+
const onlyDekMissing = opened?.kind === "local-failure" &&
|
|
154
|
+
opened.reason === "NO_DEK" &&
|
|
155
|
+
unit !== undefined &&
|
|
156
|
+
chain.state.epochs.has(String(parseDataUnit(unit).payload.dataEpoch));
|
|
157
|
+
if (opened?.kind !== "accepted" && !onlyDekMissing)
|
|
146
158
|
missing.push("the root's Data Unit with actor sequence 1");
|
|
147
159
|
return missing;
|
|
148
160
|
}
|
|
@@ -155,6 +167,9 @@ async function finishSession(o, pool, session) {
|
|
|
155
167
|
const chainJ = certificateChain(op.intent.certificates.map((c) => parseDeviceCertificate(fromHex(c))), R);
|
|
156
168
|
const me = o.installation.signer;
|
|
157
169
|
const channel = await pool.get(me, op.intent.url);
|
|
170
|
+
// A resumed session may meet a server that no longer lists identity-01 (W §37).
|
|
171
|
+
if (!channel.extensions.includes(IDENTITY_EXTENSION))
|
|
172
|
+
return { kind: "unsupported", session, reason: NO_IDENTITY_SUPPORT };
|
|
158
173
|
const grant = await runControlPhase(o.storage, channel, journal, session, {
|
|
159
174
|
id: "grant",
|
|
160
175
|
kind: "register",
|
|
@@ -323,6 +338,7 @@ export async function pairingRequests(o) {
|
|
|
323
338
|
* phase 1; phase 2 signs exactly that payload.
|
|
324
339
|
*/
|
|
325
340
|
export async function approvePairing(o) {
|
|
341
|
+
checkFlowProfiles(o);
|
|
326
342
|
const { I, R, J, url } = await readSession(o, o.review.session);
|
|
327
343
|
const pool = new ChannelPool(o.network);
|
|
328
344
|
const me = o.installation.signer;
|
|
@@ -413,6 +429,7 @@ export const approvedGrantRecord = (chain, signer, root, device, pairing) => sig
|
|
|
413
429
|
export const cancelRecord = (chain, signer, root, pairing) => signControlRecord(next(chain), { type: "DEVICE_REVOKE", root, device: pairing, reason: 0n }, signer).bytes;
|
|
414
430
|
/** Resumes a journaled approval from its next phase (§9.1: "If A crashes after step 5 …"). */
|
|
415
431
|
export async function resumeApproval(o, operation) {
|
|
432
|
+
checkFlowProfiles(o);
|
|
416
433
|
const journal = new OperationJournal(o.storage);
|
|
417
434
|
const pool = new ChannelPool(o.network);
|
|
418
435
|
const me = o.installation.signer;
|
|
@@ -565,6 +582,7 @@ export async function resumeApproval(o, operation) {
|
|
|
565
582
|
* human does not confirm is rejected).
|
|
566
583
|
*/
|
|
567
584
|
export async function cancelPairing(o) {
|
|
585
|
+
checkFlowProfiles(o);
|
|
568
586
|
const { I, R, J, url } = await readSession(o, o.session);
|
|
569
587
|
const journal = new OperationJournal(o.storage);
|
|
570
588
|
const pool = new ChannelPool(o.network);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type PrincipalId, type ResourceId } from "@openlfcp/core";
|
|
2
|
-
import { type CertificateChain, type Signer } from "@openlfcp/wire";
|
|
2
|
+
import { type CertificateChain, type ControlState, type DeviceBinding, type PrincipalDescriptor, type Signer } from "@openlfcp/wire";
|
|
3
3
|
import { type DocumentWrite, type KeyDelivery } from "./context.js";
|
|
4
|
+
import { type DeviceEntryValue, type IdentityIntent } from "./document.js";
|
|
4
5
|
import { type LinearChain, type PhaseState } from "./phases.js";
|
|
5
6
|
import { type IdentityFlowOptions } from "./resource.js";
|
|
6
7
|
import { type RotationOutcome } from "./rotation.js";
|
|
@@ -26,11 +27,40 @@ export type ReconcileStep = {
|
|
|
26
27
|
/** `I`'s chain for the device. */
|
|
27
28
|
readonly chain: CertificateChain;
|
|
28
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* §7.1 step 1: whether the revocation of `device` in `I` is relevant in the
|
|
32
|
+
* target `X`. It is when the device is bound in `X` (as a leaf or an
|
|
33
|
+
* intermediate, W §17.4 rule 9), or when it could become registered in `X`
|
|
34
|
+
* or issue a chain there: bound in `I` with kind 0, or with kind 1 and `X`
|
|
35
|
+
* in its scope, or not bound in `I` at all (its kind is unknown). A pairing
|
|
36
|
+
* device, or a service device whose scope does not list `X`, that is not
|
|
37
|
+
* bound in `X` can never act there (W §7.3 c3, c6; N8): its revocation
|
|
38
|
+
* would protect nothing and would take one of the 64 unbound pairs of the
|
|
39
|
+
* root that W §17.5 rule 5 allows in `X` and never frees.
|
|
40
|
+
*
|
|
41
|
+
* `binding` is the device's binding in `I` (undefined when unbound there).
|
|
42
|
+
*/
|
|
43
|
+
export declare function revocationRelevant(binding: Pick<DeviceBinding, "kind" | "scope"> | undefined, target: ControlState, device: PrincipalId): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* §7.1 step 1 for one target: the revocations of `I` to propose on `X`
|
|
46
|
+
* (the pairs of `I`'s `revoked` not in `X`'s that are relevant there, those
|
|
47
|
+
* whose device is bound in `X` first, each group in `I`'s chain order), and
|
|
48
|
+
* the devices whose revocation is not relevant in `X` and is not
|
|
49
|
+
* propagated. Empty for the target `I` itself.
|
|
50
|
+
*/
|
|
51
|
+
export declare function planRevocations(identity: LinearChain, target: LinearChain): {
|
|
52
|
+
readonly propose: readonly Extract<ReconcileStep, {
|
|
53
|
+
kind: "revoke";
|
|
54
|
+
}>[];
|
|
55
|
+
readonly notRelevant: readonly PrincipalId[];
|
|
56
|
+
};
|
|
29
57
|
/**
|
|
30
58
|
* §7.1 steps 1 and 2 for one target, in their normative order. For the
|
|
31
59
|
* target `I` itself both steps are vacuous. Registrations are planned on
|
|
32
|
-
* the target's state as the revocations leave it
|
|
33
|
-
* `
|
|
60
|
+
* the target's state as the revocations leave it, and every pair of `I`'s
|
|
61
|
+
* `revoked` counts as revoked in `X` whether its propagation succeeds or
|
|
62
|
+
* not (an unpropagated pair, refused by W §17.5 rule 5, or a pair that is
|
|
63
|
+
* not relevant): no device whose issuer `I` has revoked is ever registered.
|
|
34
64
|
*/
|
|
35
65
|
export declare function planReconciliation(identity: LinearChain, target: LinearChain): readonly ReconcileStep[];
|
|
36
66
|
/** The record of a step on the target's head, signed by the reconciler. */
|
|
@@ -43,6 +73,17 @@ export interface TargetReconciliation {
|
|
|
43
73
|
readonly step: ReconcileStep;
|
|
44
74
|
readonly state: PhaseState | "refused";
|
|
45
75
|
}[];
|
|
76
|
+
/**
|
|
77
|
+
* §7.1 step 1: devices whose revocation in `I` the target refused (the
|
|
78
|
+
* limit of W §17.5 rule 5, or the reconciler's authority there), to
|
|
79
|
+
* report to the human with the Resource. Each stays unpropagated: this
|
|
80
|
+
* run registered no device through it and sealed nothing to it. A later
|
|
81
|
+
* run proposes it again once the target's state changed since the
|
|
82
|
+
* refusal (its head moved, for one by the device's binding there); on the
|
|
83
|
+
* same state it reports the pair without sending anything. Empty when
|
|
84
|
+
* every relevant pair reached the target.
|
|
85
|
+
*/
|
|
86
|
+
readonly unpropagated?: readonly PrincipalId[];
|
|
46
87
|
readonly keys?: KeyDelivery;
|
|
47
88
|
readonly rotation?: RotationOutcome;
|
|
48
89
|
}
|
|
@@ -70,4 +111,33 @@ export declare function reconcileDevices(o: IdentityFlowOptions & {
|
|
|
70
111
|
*/
|
|
71
112
|
readonly document: DocumentWrite;
|
|
72
113
|
}>;
|
|
114
|
+
/** §7.1 step 4's writes in `I`'s document: new entries, and the retired entries to flag. */
|
|
115
|
+
export interface DisplayPlan {
|
|
116
|
+
readonly intents: readonly IdentityIntent[];
|
|
117
|
+
/** Retired entries (PrincipalRefs) whose `revoked` is set to `true`, and nothing else of them. */
|
|
118
|
+
readonly retire: readonly string[];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* §7.1 step 4: the display data reconciliation writes in `I`'s document,
|
|
122
|
+
* from `I`'s Control state and the valid device entries (§4). For every
|
|
123
|
+
* device of `R` of kind 0 or 1 bound in `I` as the leaf of an accepted
|
|
124
|
+
* chain (an intermediate-only binding issues and does not act in `I`):
|
|
125
|
+
* when it is not revoked in `I` and has no valid entry, an entry labelled
|
|
126
|
+
* with its fingerprint (§5.2); when it is revoked in `I` and some value of
|
|
127
|
+
* its entry's `revoked` (a retired entry) is not `true`, or the field is
|
|
128
|
+
* absent, that flag alone. `revokedValues` gives every concurrent value
|
|
129
|
+
* of the flag (IdentityDocumentPort.retirement); without it the entry's
|
|
130
|
+
* one shown value is read. No entry is created for a revoked device,
|
|
131
|
+
* nothing is written for a pairing device, nothing is relabelled or
|
|
132
|
+
* deleted, and a second run on the same states plans nothing.
|
|
133
|
+
*/
|
|
134
|
+
export declare function planDisplayData(identity: ControlState, entries: ReadonlyMap<string, DeviceEntryValue>, root: PrincipalId, revokedValues?: (key: string) => readonly unknown[]): DisplayPlan;
|
|
135
|
+
/**
|
|
136
|
+
* §7.1 step 3: the devices of `R` reconciliation seals the target's keys
|
|
137
|
+
* to: every reader of the target whose root is `R` (pairing devices of `I`
|
|
138
|
+
* included, in `I`), except a device revoked in `I` and a device whose
|
|
139
|
+
* current chain in the target passes through a device revoked in `I` (an
|
|
140
|
+
* unpropagated pair of step 1, which the target still counts as active).
|
|
141
|
+
*/
|
|
142
|
+
export declare function keyRecipients(identity: LinearChain, target: LinearChain): readonly PrincipalDescriptor[];
|
|
73
143
|
//# sourceMappingURL=reconcile.d.ts.map
|