@openlfcp/client 0.3.0 → 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.
- package/dist/identity/accept.js +4 -32
- package/dist/identity/devices.d.ts +19 -0
- package/dist/identity/index.d.ts +3 -2
- package/dist/identity/index.js +1 -0
- package/dist/identity/journal.d.ts +1 -1
- package/dist/identity/pairing.js +4 -0
- package/dist/identity/reconcile.js +2 -1
- package/dist/identity/revocation.js +3 -1
- package/dist/identity/roster.d.ts +89 -0
- package/dist/identity/roster.js +130 -0
- package/dist/identity/rotation.d.ts +38 -3
- package/dist/identity/rotation.js +91 -6
- package/package.json +5 -5
package/dist/identity/accept.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { bytesEqual, dataEpoch, fromHex, generateResourceId, principalId, resourceId, secureRandom, toHex, } from "@openlfcp/core";
|
|
2
2
|
import { dekCommitment, generateResourceDEK } from "@openlfcp/crypto";
|
|
3
3
|
import { ABILITY, activeDevice, DEVICE_ABILITY, DEVICE_KIND, hasAbility, invitationPrincipal, parseInviteUri, signControlRecord, validateControlChain, verifyInvitationSecret, } from "@openlfcp/wire";
|
|
4
|
-
import { deliverKeys, fetchDeks, genesisOwner, IdentityConflict,
|
|
4
|
+
import { deliverKeys, fetchDeks, genesisOwner, IdentityConflict, runControlPhase, storeDek, syncChain, } from "./context.js";
|
|
5
5
|
import { registerDevice } from "./devices.js";
|
|
6
|
-
import { resourceRef } from "./document.js";
|
|
7
6
|
import { newOperationId, OperationJournal, PackageLedger, phaseOf, } from "./journal.js";
|
|
8
7
|
import { ChannelPool, IdentityRefused, IdentityUnavailable } from "./network.js";
|
|
9
8
|
import { classifyRegister } from "./phases.js";
|
|
10
9
|
import { identityTarget } from "./resource.js";
|
|
10
|
+
import { writeRosterEntry } from "./roster.js";
|
|
11
11
|
/**
|
|
12
12
|
* Invitations accepted by a device and Resources created from a device
|
|
13
13
|
* (IDENTITY-PROFILE-01 §7.2, §7.3). Inviting is unchanged; a device that
|
|
@@ -77,36 +77,8 @@ async function keyAndList(o, pool, journal, operation, I, iUrl, X, url, entry) {
|
|
|
77
77
|
others.push(...r.results);
|
|
78
78
|
}
|
|
79
79
|
// §7.2 step 4, §7.3: the roster entry, so the human's other installations reach X.
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const chainI = await syncChain(o.storage, iChannel, I.state.resourceId);
|
|
83
|
-
const doc = o.document(I.state.resourceId, genesisOwner(chainI));
|
|
84
|
-
const deksI = await fetchDeks(o.storage, o.secrets, iChannel, chainI, o.installation);
|
|
85
|
-
const load = await loadIdentityDocument({ channel: iChannel, chain: chainI, deks: deksI, doc });
|
|
86
|
-
return { chainI, doc, deksI, load };
|
|
87
|
-
};
|
|
88
|
-
let roster = (await openI()).doc.roster().has(resourceRef(X));
|
|
89
|
-
if (!roster) {
|
|
90
|
-
const r = await runUnitPhase(iChannel, journal, operation, {
|
|
91
|
-
id: "roster",
|
|
92
|
-
resource: I.state.resourceId,
|
|
93
|
-
// A unit refused after a rotation of I is written again in the new epoch (W §19.1).
|
|
94
|
-
rebuildOn: ["STALE_DATA_EPOCH"],
|
|
95
|
-
build: async () => {
|
|
96
|
-
const { chainI, doc, deksI, load } = await openI();
|
|
97
|
-
return sealIdentityUnit({
|
|
98
|
-
storage: o.storage,
|
|
99
|
-
chain: chainI,
|
|
100
|
-
deks: deksI,
|
|
101
|
-
signer: me,
|
|
102
|
-
doc,
|
|
103
|
-
latest: load.latest,
|
|
104
|
-
intents: [{ intent: "roster.put", key: resourceRef(X), entry }],
|
|
105
|
-
});
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
roster = r.state === "committed-own";
|
|
109
|
-
}
|
|
80
|
+
const written = await writeRosterEntry(o, await pool.get(me, iUrl), journal, operation, I.state.resourceId, X, entry);
|
|
81
|
+
const roster = written.state !== "refused";
|
|
110
82
|
return { rootKeys, siblings: Object.freeze(others), roster };
|
|
111
83
|
}
|
|
112
84
|
/** §7.2: the claim of an invitation for the root, signed by the Invitation Principal (W §18.1 rule 5). */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type PrincipalId, type ResourceId } from "@openlfcp/core";
|
|
2
2
|
import type { SecretStore } from "@openlfcp/storage";
|
|
3
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";
|
|
@@ -31,7 +32,25 @@ export interface FlowOptions {
|
|
|
31
32
|
readonly secrets: SecretStore;
|
|
32
33
|
readonly network: IdentityNetwork;
|
|
33
34
|
readonly now: () => number;
|
|
35
|
+
/**
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
readonly profiles?: readonly UnitProfile[];
|
|
34
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">;
|
|
35
54
|
/**
|
|
36
55
|
* The certificates above a leaf that `issuer` signs for `root` in the
|
|
37
56
|
* state: none when the issuer is the root, else the issuer's own current
|
package/dist/identity/index.d.ts
CHANGED
|
@@ -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";
|
|
@@ -12,5 +12,6 @@ export { type RevocationStatus, revocationStatus } from "./proof.js";
|
|
|
12
12
|
export { planReconciliation, type ReconcileStep, reconcileDevices, reconcileRecord, 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
|
-
export { type
|
|
15
|
+
export { type AddToRosterOptions, type AddToRosterResult, addToRoster, type RosterWrite, writeRosterEntry, } from "./roster.js";
|
|
16
|
+
export { type ExposingRevocation, exposingRevocationsSince, type FrontierData, type LocalAccepted, type RotationOutcome, type RotationStatus, type RotatorTest, rotateAfter, rotatorTest, verifiedFrontier, } from "./rotation.js";
|
|
16
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/identity/index.js
CHANGED
|
@@ -12,5 +12,6 @@ export { revocationStatus } from "./proof.js";
|
|
|
12
12
|
export { planReconciliation, reconcileDevices, reconcileRecord, } from "./reconcile.js";
|
|
13
13
|
export { createIdentityResource, identityTarget, localIdentityResource, resumeIdentityCreation, setLocalIdentityResource, } from "./resource.js";
|
|
14
14
|
export { previewRevocation, revokeDevice, rotateIfExposed, } from "./revocation.js";
|
|
15
|
+
export { addToRoster, writeRosterEntry, } from "./roster.js";
|
|
15
16
|
export { exposingRevocationsSince, rotateAfter, rotatorTest, verifiedFrontier, } from "./rotation.js";
|
|
16
17
|
//# sourceMappingURL=index.js.map
|
|
@@ -21,7 +21,7 @@ import type { PhaseState } from "./phases.js";
|
|
|
21
21
|
* SecretStore only, under the references below, before the row that names
|
|
22
22
|
* them is written (LFCP-034 ordering).
|
|
23
23
|
*/
|
|
24
|
-
export type OperationKind = "create-identity" | "pairing-session" | "pairing-approval" | "pairing-request" | "register-device" | "revoke-device" | "rotate" | "reconcile" | "accept-invitation" | "create-resource";
|
|
24
|
+
export type OperationKind = "create-identity" | "pairing-session" | "pairing-approval" | "pairing-request" | "register-device" | "revoke-device" | "rotate" | "reconcile" | "accept-invitation" | "create-resource" | "add-to-roster";
|
|
25
25
|
/** The state of a whole operation. */
|
|
26
26
|
export type OperationStatus =
|
|
27
27
|
/** Phases remain; resume it. */
|
package/dist/identity/pairing.js
CHANGED
|
@@ -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)}`);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type ResourceId } from "@openlfcp/core";
|
|
2
|
+
import { type RosterEntryValue } from "./document.js";
|
|
3
|
+
import { OperationJournal } from "./journal.js";
|
|
4
|
+
import { type IdentityChannel } from "./network.js";
|
|
5
|
+
import { type IdentityFlowOptions } from "./resource.js";
|
|
6
|
+
/**
|
|
7
|
+
* The roster of the Identity Resource (IDENTITY-PROFILE-01 §2.1): how the
|
|
8
|
+
* human's other installations learn a Resource (§6). An entry is written as
|
|
9
|
+
* its own durable phase of an operation (§9.1): a unit refused as stale
|
|
10
|
+
* after a rotation of `I` is written again in the current epoch (W §19.1),
|
|
11
|
+
* and an entry already present, by this installation or another writer,
|
|
12
|
+
* is not written again.
|
|
13
|
+
*/
|
|
14
|
+
export type RosterWrite =
|
|
15
|
+
/** This installation's unit carrying the entry was accepted. */
|
|
16
|
+
{
|
|
17
|
+
readonly state: "written";
|
|
18
|
+
}
|
|
19
|
+
/** The roster already held an entry for the Resource. */
|
|
20
|
+
| {
|
|
21
|
+
readonly state: "present";
|
|
22
|
+
}
|
|
23
|
+
/** The coordinator refused the unit for another reason; the phase stays open. */
|
|
24
|
+
| {
|
|
25
|
+
readonly state: "refused";
|
|
26
|
+
readonly code: string;
|
|
27
|
+
};
|
|
28
|
+
/** Writes `roster[ref(resource)] = entry` in `I` as the phase `roster` of `operation`. */
|
|
29
|
+
export declare function writeRosterEntry(o: IdentityFlowOptions, channel: IdentityChannel, journal: OperationJournal, operation: string, identity: ResourceId, resource: ResourceId, entry: RosterEntryValue): Promise<RosterWrite>;
|
|
30
|
+
export interface AddToRosterOptions extends IdentityFlowOptions {
|
|
31
|
+
/** The Resource to list (a Resource of the human: created or joined here). */
|
|
32
|
+
readonly resource: ResourceId;
|
|
33
|
+
/** A display name for the roster entry. */
|
|
34
|
+
readonly name?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The entry's fields; by default taken from the Resource's Control Chain:
|
|
37
|
+
* its Data Profile, route endpoints and coordinator (stored here, or read
|
|
38
|
+
* from `url`).
|
|
39
|
+
*/
|
|
40
|
+
readonly entry?: RosterEntryValue;
|
|
41
|
+
/** The Resource's coordinator, when its chain is not stored here. */
|
|
42
|
+
readonly url?: string;
|
|
43
|
+
/** Resume this operation. */
|
|
44
|
+
readonly operation?: string;
|
|
45
|
+
}
|
|
46
|
+
export type AddToRosterResult =
|
|
47
|
+
/** This installation wrote the entry. */
|
|
48
|
+
{
|
|
49
|
+
readonly kind: "added";
|
|
50
|
+
readonly operation: string;
|
|
51
|
+
readonly resource: ResourceId;
|
|
52
|
+
}
|
|
53
|
+
/** The roster already lists the Resource (written earlier, or by another device). */
|
|
54
|
+
| {
|
|
55
|
+
readonly kind: "present";
|
|
56
|
+
readonly operation: string;
|
|
57
|
+
readonly resource: ResourceId;
|
|
58
|
+
}
|
|
59
|
+
/** This installation cannot write `I` (`holds(D, 2)` is false, e.g. a Read device, §7.2). */
|
|
60
|
+
| {
|
|
61
|
+
readonly kind: "needs-writer";
|
|
62
|
+
}
|
|
63
|
+
/** The coordinator refused the unit: the operation stays open; resume it with `operation`. */
|
|
64
|
+
| {
|
|
65
|
+
readonly kind: "document-incomplete";
|
|
66
|
+
readonly operation: string;
|
|
67
|
+
readonly resource: ResourceId;
|
|
68
|
+
readonly code: string;
|
|
69
|
+
} | {
|
|
70
|
+
readonly kind: "conflict";
|
|
71
|
+
} | {
|
|
72
|
+
readonly kind: "unavailable";
|
|
73
|
+
readonly operation?: string;
|
|
74
|
+
readonly reason: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Adds a Resource to the roster of the Identity Resource as one durable
|
|
78
|
+
* operation (IDENTITY-PROFILE-01 §2.1, §6, §9): the entry the human's other
|
|
79
|
+
* installations read to reach the Resource (pairing step 10, reconciliation
|
|
80
|
+
* §7.1). For a Resource created or joined after `I` was filled (§5.1), for
|
|
81
|
+
* example a section created later on any personal device. Idempotent: an
|
|
82
|
+
* entry already present is not written again; a unit refused as stale
|
|
83
|
+
* after a rotation of `I` is written again in the current epoch; any other
|
|
84
|
+
* refusal leaves the operation open. Only a writer of `I` (the root or a
|
|
85
|
+
* personal device holding data/write there) may add entries (§3 AI4: a
|
|
86
|
+
* pairing or service device never writes the roster).
|
|
87
|
+
*/
|
|
88
|
+
export declare function addToRoster(o: AddToRosterOptions): Promise<AddToRosterResult>;
|
|
89
|
+
//# sourceMappingURL=roster.d.ts.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { fromHex, LfcpError, resourceId, secureRandom, toHex, } from "@openlfcp/core";
|
|
2
|
+
import { ABILITY, hasAbility } from "@openlfcp/wire";
|
|
3
|
+
import { loadControlChain } from "../storage.js";
|
|
4
|
+
import { fetchDeks, genesisOwner, IdentityConflict, loadIdentityDocument, runUnitPhase, sealIdentityUnit, syncChain, } from "./context.js";
|
|
5
|
+
import { resourceRef } from "./document.js";
|
|
6
|
+
import { newOperationId, OperationJournal } from "./journal.js";
|
|
7
|
+
import { ChannelPool, IdentityRefused, IdentityUnavailable, } from "./network.js";
|
|
8
|
+
import { identityTarget } from "./resource.js";
|
|
9
|
+
/** Writes `roster[ref(resource)] = entry` in `I` as the phase `roster` of `operation`. */
|
|
10
|
+
export async function writeRosterEntry(o, channel, journal, operation, identity, resource, entry) {
|
|
11
|
+
const me = o.installation.signer;
|
|
12
|
+
const openI = async () => {
|
|
13
|
+
const chain = await syncChain(o.storage, channel, identity);
|
|
14
|
+
const doc = o.document(identity, genesisOwner(chain));
|
|
15
|
+
const deks = await fetchDeks(o.storage, o.secrets, channel, chain, o.installation);
|
|
16
|
+
const load = await loadIdentityDocument({ channel, chain, deks, doc });
|
|
17
|
+
return { chain, doc, deks, load };
|
|
18
|
+
};
|
|
19
|
+
if ((await openI()).doc.roster().has(resourceRef(resource)))
|
|
20
|
+
return { state: "present" };
|
|
21
|
+
const r = await runUnitPhase(channel, journal, operation, {
|
|
22
|
+
id: "roster",
|
|
23
|
+
resource: identity,
|
|
24
|
+
// A unit refused after a rotation of I is written again in the new epoch (W §19.1).
|
|
25
|
+
rebuildOn: ["STALE_DATA_EPOCH"],
|
|
26
|
+
// A resumed operation whose unit was refused writes the entry again on the
|
|
27
|
+
// current document, previous unit and actor sequence: the refused bytes may
|
|
28
|
+
// name a previous unit this actor has since moved past (W §26.2). Exact
|
|
29
|
+
// bytes are re-sent only after an unknown outcome (§9.1).
|
|
30
|
+
rebuildRefused: true,
|
|
31
|
+
build: async () => {
|
|
32
|
+
const { chain, doc, deks, load } = await openI();
|
|
33
|
+
// Another writer may have listed the Resource since the last check (a stale
|
|
34
|
+
// retry, a resume): its entry stands and the phase is complete (§9.2).
|
|
35
|
+
if (doc.roster().has(resourceRef(resource)))
|
|
36
|
+
return null;
|
|
37
|
+
return sealIdentityUnit({
|
|
38
|
+
storage: o.storage,
|
|
39
|
+
chain,
|
|
40
|
+
deks,
|
|
41
|
+
signer: me,
|
|
42
|
+
doc,
|
|
43
|
+
latest: load.latest,
|
|
44
|
+
intents: [{ intent: "roster.put", key: resourceRef(resource), entry }],
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
if (r.state === "committed-own")
|
|
49
|
+
return { state: "written" };
|
|
50
|
+
if (r.state === "complete")
|
|
51
|
+
return { state: "present" };
|
|
52
|
+
return { state: "refused", code: r.code ?? "refused" };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Adds a Resource to the roster of the Identity Resource as one durable
|
|
56
|
+
* operation (IDENTITY-PROFILE-01 §2.1, §6, §9): the entry the human's other
|
|
57
|
+
* installations read to reach the Resource (pairing step 10, reconciliation
|
|
58
|
+
* §7.1). For a Resource created or joined after `I` was filled (§5.1), for
|
|
59
|
+
* example a section created later on any personal device. Idempotent: an
|
|
60
|
+
* entry already present is not written again; a unit refused as stale
|
|
61
|
+
* after a rotation of `I` is written again in the current epoch; any other
|
|
62
|
+
* refusal leaves the operation open. Only a writer of `I` (the root or a
|
|
63
|
+
* personal device holding data/write there) may add entries (§3 AI4: a
|
|
64
|
+
* pairing or service device never writes the roster).
|
|
65
|
+
*/
|
|
66
|
+
export async function addToRoster(o) {
|
|
67
|
+
const target = await identityTarget(o);
|
|
68
|
+
const me = o.installation.signer;
|
|
69
|
+
const journal = new OperationJournal(o.storage);
|
|
70
|
+
const pool = new ChannelPool(o.network);
|
|
71
|
+
let operation = o.operation;
|
|
72
|
+
try {
|
|
73
|
+
const channel = await pool.get(me, target.url);
|
|
74
|
+
const I = await syncChain(o.storage, channel, target.resource);
|
|
75
|
+
if (!hasAbility(I.state, me.descriptor.principalId, ABILITY.DATA_WRITE))
|
|
76
|
+
return { kind: "needs-writer" };
|
|
77
|
+
if (operation === undefined || (await journal.get(operation)) === undefined) {
|
|
78
|
+
const entry = o.entry ?? (await entryOf(o, pool, o.resource));
|
|
79
|
+
operation = operation ?? newOperationId("add-to-roster", secureRandom(8));
|
|
80
|
+
await journal.begin({
|
|
81
|
+
id: operation,
|
|
82
|
+
kind: "add-to-roster",
|
|
83
|
+
createdAt: o.now(),
|
|
84
|
+
intent: {
|
|
85
|
+
identity: toHex(target.resource),
|
|
86
|
+
resource: toHex(o.resource),
|
|
87
|
+
entry: { ...entry, ...(o.name === undefined ? {} : { name: o.name }) },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const op = await journal.load(operation);
|
|
92
|
+
const resource = resourceId(fromHex(op.intent.resource));
|
|
93
|
+
const written = await writeRosterEntry(o, channel, journal, op.id, target.resource, resource, op.intent.entry);
|
|
94
|
+
if (written.state === "refused")
|
|
95
|
+
return { kind: "document-incomplete", operation: op.id, resource, code: written.code };
|
|
96
|
+
await journal.setStatus(op.id, "done");
|
|
97
|
+
return { kind: written.state === "written" ? "added" : "present", operation: op.id, resource };
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
if (e instanceof IdentityUnavailable || e instanceof IdentityRefused)
|
|
101
|
+
return {
|
|
102
|
+
kind: "unavailable",
|
|
103
|
+
...(operation === undefined ? {} : { operation }),
|
|
104
|
+
reason: e.message,
|
|
105
|
+
};
|
|
106
|
+
if (e instanceof IdentityConflict)
|
|
107
|
+
return { kind: "conflict" };
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
pool.closeAll();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** The roster entry of a Resource from its Control Chain (W §15, §20): profile, routes, coordinator. */
|
|
115
|
+
async function entryOf(o, pool, resource) {
|
|
116
|
+
const stored = await loadControlChain(o.storage, resource);
|
|
117
|
+
let chain;
|
|
118
|
+
if (stored?.kind === "linear")
|
|
119
|
+
chain = stored;
|
|
120
|
+
else if (o.url !== undefined)
|
|
121
|
+
chain = await syncChain(o.storage, await pool.get(o.installation.signer, o.url), resource);
|
|
122
|
+
else
|
|
123
|
+
throw new LfcpError("MISSING_DEPENDENCY", "the Resource's chain is not stored here: give its entry or its coordinator URL");
|
|
124
|
+
return {
|
|
125
|
+
profile: chain.state.dataProfile,
|
|
126
|
+
endpoints: chain.state.route.endpoints.map((e) => e.url),
|
|
127
|
+
coordinator: chain.state.route.coordinatorUrl,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=roster.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type ControlRecordId, type DataEpoch, type PrincipalId, type ResourceId } from "@openlfcp/core";
|
|
2
|
+
import { type ResourceDEK } from "@openlfcp/crypto";
|
|
2
3
|
import { type ControlState, type HaveVector, type PrincipalDescriptor } from "@openlfcp/wire";
|
|
3
4
|
import { type KeyDelivery } from "./context.js";
|
|
4
|
-
import type { FlowOptions } from "./devices.js";
|
|
5
|
+
import type { FlowOptions, UnitProfile } from "./devices.js";
|
|
5
6
|
import { type ChannelPool, type IdentityChannel } from "./network.js";
|
|
6
7
|
import { type LinearChain } from "./phases.js";
|
|
7
8
|
/**
|
|
@@ -40,15 +41,47 @@ export declare function exposingRevocationsSince(chain: LinearChain): readonly E
|
|
|
40
41
|
* §8.3: the final frontier of the closing epoch from this installation's
|
|
41
42
|
* locally verified accepted set, after a Data synchronisation: a server's
|
|
42
43
|
* Have Vector is a fetch hint; a sequence that cannot be fetched and
|
|
43
|
-
* verified, or that fails authorization, stays outside.
|
|
44
|
+
* verified, or that fails authorization, stays outside. With `data`, a unit
|
|
45
|
+
* counts only when recipients accept it as data (W §26.3 rules 6 and 7: it
|
|
46
|
+
* decrypts under the epoch's DEK and the Data Profile accepts it), so a unit
|
|
47
|
+
* no recipient can merge never pushes an honest later unit of its actor
|
|
48
|
+
* beyond the cutoff; rotateAfter always passes it. Without `data` only
|
|
49
|
+
* signature, authorization and the actor chain are checked (0.3.0).
|
|
44
50
|
*/
|
|
45
|
-
export declare function verifiedFrontier(channel: IdentityChannel, chain: LinearChain, local?: LocalAccepted): Promise<HaveVector>;
|
|
51
|
+
export declare function verifiedFrontier(channel: IdentityChannel, chain: LinearChain, local?: LocalAccepted, data?: FrontierData): Promise<HaveVector>;
|
|
46
52
|
/** This installation's stored units of a Resource and its verified Snapshots' coverage. */
|
|
47
53
|
export interface LocalAccepted {
|
|
48
54
|
/** Every signature-valid unit the storage holds for the Resource (exact bytes). */
|
|
49
55
|
readonly units: readonly Uint8Array[];
|
|
50
56
|
/** The sequences verified Snapshots cover (W §29.3). */
|
|
51
57
|
readonly snapshot: HaveVector;
|
|
58
|
+
/**
|
|
59
|
+
* Unit IDs (hex) whose bytes this installation proved bad as data: a failed
|
|
60
|
+
* AEAD or a Data Profile decode refusal. A missing or mismatching DEK is
|
|
61
|
+
* not such a proof: those units are verified again here.
|
|
62
|
+
*/
|
|
63
|
+
readonly refused?: ReadonlySet<string>;
|
|
64
|
+
/**
|
|
65
|
+
* Unit IDs (hex) this installation accepted on the wire (its actor chain
|
|
66
|
+
* links through them), including units its profile later refused to
|
|
67
|
+
* merge: recipients link through them the same way.
|
|
68
|
+
*/
|
|
69
|
+
readonly accepted?: ReadonlySet<string>;
|
|
70
|
+
}
|
|
71
|
+
/** What the rotating client needs to judge a unit as data (W §26.3 rules 6 and 7). */
|
|
72
|
+
export interface FrontierData {
|
|
73
|
+
/** The DEKs held for the Resource's epochs, by epoch number. */
|
|
74
|
+
readonly deks: ReadonlyMap<string, ResourceDEK>;
|
|
75
|
+
/**
|
|
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
|
|
80
|
+
* checked here, and a profile refusal this installation recorded still
|
|
81
|
+
* counts (LocalAccepted.refused). 0.3.1 took a bare codec, which cannot
|
|
82
|
+
* bind the actor: one without `codecFor` is refused (TypeError).
|
|
83
|
+
*/
|
|
84
|
+
readonly profile?: UnitProfile;
|
|
52
85
|
}
|
|
53
86
|
export type RotationStatus =
|
|
54
87
|
/** This installation committed the KEY_EPOCH and sealed the new DEK to every reader. */
|
|
@@ -87,6 +120,8 @@ export declare function rotateAfter(o: FlowOptions & {
|
|
|
87
120
|
readonly resource: ResourceId;
|
|
88
121
|
readonly revokedAt: bigint;
|
|
89
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;
|
|
90
125
|
/**
|
|
91
126
|
* Resumes every rotation a journaled operation started and did not adopt:
|
|
92
127
|
* a KEY_EPOCH whose ACK was lost leaves its DEK in the operation's pending
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bytesEqual, controlRecordId, fromHex, resourceId, toHex, } from "@openlfcp/core";
|
|
2
2
|
import { generateResourceDEK, sha256 } from "@openlfcp/crypto";
|
|
3
|
-
import { ABILITY, actsFor, addSequence, checkDataUnit, hasAbility, hasSequence, InMemorySeenUnits, isExposingRevocation, KEY_EPOCH_REASON, parseDataUnit, rotateEpoch, } from "@openlfcp/wire";
|
|
3
|
+
import { ABILITY, actsFor, addSequence, checkDataUnit, hasAbility, hasSequence, InMemorySeenUnits, isExposingRevocation, KEY_EPOCH_REASON, parseDataUnit, receiveDataUnit, rotateEpoch, } from "@openlfcp/wire";
|
|
4
4
|
import { snapshotFrontier } from "../outbound.js";
|
|
5
5
|
import { loadControlChain } from "../storage.js";
|
|
6
6
|
import { deliverKeys, fetchDeks, readersAt, runControlPhase, storeDek, withoutEquivocations, } from "./context.js";
|
|
@@ -50,9 +50,16 @@ export function exposingRevocationsSince(chain) {
|
|
|
50
50
|
* §8.3: the final frontier of the closing epoch from this installation's
|
|
51
51
|
* locally verified accepted set, after a Data synchronisation: a server's
|
|
52
52
|
* Have Vector is a fetch hint; a sequence that cannot be fetched and
|
|
53
|
-
* verified, or that fails authorization, stays outside.
|
|
53
|
+
* verified, or that fails authorization, stays outside. With `data`, a unit
|
|
54
|
+
* counts only when recipients accept it as data (W §26.3 rules 6 and 7: it
|
|
55
|
+
* decrypts under the epoch's DEK and the Data Profile accepts it), so a unit
|
|
56
|
+
* no recipient can merge never pushes an honest later unit of its actor
|
|
57
|
+
* beyond the cutoff; rotateAfter always passes it. Without `data` only
|
|
58
|
+
* signature, authorization and the actor chain are checked (0.3.0).
|
|
54
59
|
*/
|
|
55
|
-
export async function verifiedFrontier(channel, chain, local = { units: [], snapshot: [] }) {
|
|
60
|
+
export async function verifiedFrontier(channel, chain, local = { units: [], snapshot: [] }, data) {
|
|
61
|
+
if (data?.profile !== undefined)
|
|
62
|
+
checkUnitProfile(data.profile, "FrontierData.profile");
|
|
56
63
|
const R = chain.state.resourceId;
|
|
57
64
|
const opened = await channel.open(R);
|
|
58
65
|
const ranges = opened.kind === "nack"
|
|
@@ -90,10 +97,20 @@ export async function verifiedFrontier(channel, chain, local = { units: [], snap
|
|
|
90
97
|
const check = await checkDataUnit(chain, bytes, seen);
|
|
91
98
|
if (check.kind !== "valid")
|
|
92
99
|
continue;
|
|
100
|
+
const covered = hasSequence(local.snapshot, p.actor, p.actorSeq);
|
|
101
|
+
// W §26.3 rules 6 and 7: a unit recipients cannot decrypt, or whose plaintext
|
|
102
|
+
// the Data Profile refuses, is not accepted data. It is not counted and does
|
|
103
|
+
// not become the actor's latest unit, so the honest unit after it, which names
|
|
104
|
+
// the actor's last good unit (W §26.2), still links. Units a verified Snapshot
|
|
105
|
+
// covers are attested by it (W §29.3).
|
|
106
|
+
if (!covered &&
|
|
107
|
+
data !== undefined &&
|
|
108
|
+
!(await acceptedAsData(chain, bytes, toHex(check.unitId), p.actor, local, data)))
|
|
109
|
+
continue;
|
|
93
110
|
const actor = toHex(p.actor);
|
|
94
111
|
const last = latest.get(actor);
|
|
95
112
|
let links;
|
|
96
|
-
if (
|
|
113
|
+
if (covered)
|
|
97
114
|
links = true;
|
|
98
115
|
else {
|
|
99
116
|
// The actor's last accepted position: its latest held unit or the highest
|
|
@@ -137,6 +154,34 @@ function coveredTop(vector, actor) {
|
|
|
137
154
|
best = end;
|
|
138
155
|
return best;
|
|
139
156
|
}
|
|
157
|
+
const opaque = (dataProfile) => ({
|
|
158
|
+
dataProfile,
|
|
159
|
+
encode: (v) => v,
|
|
160
|
+
decode: (v) => v,
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* Whether recipients accept the unit as data: its DEK is held and matches
|
|
164
|
+
* the epoch's commitment, it decrypts, and the profile accepts the plaintext
|
|
165
|
+
* (receiveDataUnit's own steps, on a fresh accepted set: "held" for a link
|
|
166
|
+
* not decided here means those steps passed). A unit this client cannot
|
|
167
|
+
* decrypt cannot be verified and stays outside the frontier (W §19).
|
|
168
|
+
*/
|
|
169
|
+
async function acceptedAsData(chain, bytes, unitId, actor, local, data) {
|
|
170
|
+
if (local.accepted?.has(unitId) === true)
|
|
171
|
+
return true;
|
|
172
|
+
if (local.refused?.has(unitId) === true)
|
|
173
|
+
return false;
|
|
174
|
+
// The receiver's own check of this unit: bound to its Resource and signer (W §26.3 rule 7).
|
|
175
|
+
const profile = data.profile !== undefined && data.profile.dataProfile === chain.state.dataProfile
|
|
176
|
+
? data.profile.codecFor({ resourceId: chain.state.resourceId, actor })
|
|
177
|
+
: opaque(chain.state.dataProfile);
|
|
178
|
+
const r = await receiveDataUnit(chain, bytes, {
|
|
179
|
+
seen: new InMemorySeenUnits(),
|
|
180
|
+
dek: (epoch) => data.deks.get(String(epoch)),
|
|
181
|
+
profile,
|
|
182
|
+
});
|
|
183
|
+
return r.kind === "accepted" || r.kind === "held";
|
|
184
|
+
}
|
|
140
185
|
/**
|
|
141
186
|
* The Rotate phase after a revocation at control sequence `revokedAt`
|
|
142
187
|
* (§8.2, §8.4, §9.2–§9.4), journaled in `operation`. The KEY_EPOCH is
|
|
@@ -149,6 +194,7 @@ function coveredTop(vector, actor) {
|
|
|
149
194
|
* readers and receive nothing.
|
|
150
195
|
*/
|
|
151
196
|
export async function rotateAfter(o) {
|
|
197
|
+
checkFlowProfiles(o);
|
|
152
198
|
const journal = new OperationJournal(o.storage);
|
|
153
199
|
const me = o.installation.signer;
|
|
154
200
|
const R = o.resource;
|
|
@@ -177,7 +223,10 @@ export async function rotateAfter(o) {
|
|
|
177
223
|
: "awaiting-rotation";
|
|
178
224
|
},
|
|
179
225
|
build: async (chain) => {
|
|
180
|
-
const frontier = await verifiedFrontier(o.channel, chain, await localAccepted(o, chain)
|
|
226
|
+
const frontier = await verifiedFrontier(o.channel, chain, await localAccepted(o, chain), {
|
|
227
|
+
deks: await fetchDeks(o.storage, o.secrets, o.channel, chain, o.installation),
|
|
228
|
+
...profileFor(o, chain),
|
|
229
|
+
});
|
|
181
230
|
const dek = (await getPendingDek(o.secrets, R, o.operation)) ?? generateResourceDEK();
|
|
182
231
|
// The DEK before the record that commits to it (§9.1).
|
|
183
232
|
await putPendingDek(o.secrets, R, o.operation, dek);
|
|
@@ -233,6 +282,28 @@ export async function rotateAfter(o) {
|
|
|
233
282
|
* stored accepted units and verified Snapshots, when the storage keeps them),
|
|
234
283
|
* merged into the verified frontier (§8.3).
|
|
235
284
|
*/
|
|
285
|
+
/** The profile the application gave for the Resource's Data Profile, if any. */
|
|
286
|
+
const profileFor = (o, chain) => {
|
|
287
|
+
const profile = o.profiles?.find((p) => p.dataProfile === chain.state.dataProfile);
|
|
288
|
+
return profile === undefined ? {} : { profile };
|
|
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
|
+
}
|
|
236
307
|
async function localAccepted(o, chain) {
|
|
237
308
|
const st = o.storage;
|
|
238
309
|
if (st.dataUnits === undefined || st.snapshots === undefined)
|
|
@@ -256,7 +327,20 @@ async function localAccepted(o, chain) {
|
|
|
256
327
|
])
|
|
257
328
|
for (const u of await st.dataUnits.withStatus(R, status))
|
|
258
329
|
units.push(u.bytes);
|
|
259
|
-
|
|
330
|
+
// What this installation decided as a receiver. Proven bad bytes: AEAD and
|
|
331
|
+
// profile decode failures (a missing or wrong DEK is verified again). Accepted
|
|
332
|
+
// on the wire: the units its actor chain links through, a unit the profile
|
|
333
|
+
// refused when merging included (it keeps the accepted mark).
|
|
334
|
+
const refused = new Set();
|
|
335
|
+
for (const u of await st.dataUnits.withStatus(R, "local-failure"))
|
|
336
|
+
if (/^(AEAD|PROFILE_REJECTED):/.test(u.detail ?? ""))
|
|
337
|
+
refused.add(toHex(u.unitId));
|
|
338
|
+
const accepted = new Set();
|
|
339
|
+
for (const status of ["merged", "profile-pending", "profile-held", "profile-rejected"])
|
|
340
|
+
for (const u of await st.dataUnits.withStatus(R, status))
|
|
341
|
+
if (u.accepted)
|
|
342
|
+
accepted.add(toHex(u.unitId));
|
|
343
|
+
return { units, snapshot: await snapshotFrontier(st, R), refused, accepted };
|
|
260
344
|
}
|
|
261
345
|
/**
|
|
262
346
|
* Resumes every rotation a journaled operation started and did not adopt:
|
|
@@ -265,6 +349,7 @@ async function localAccepted(o, chain) {
|
|
|
265
349
|
* it (§9.1, §9.2). Run before planning new work on a Resource.
|
|
266
350
|
*/
|
|
267
351
|
export async function resumeRotations(o) {
|
|
352
|
+
checkFlowProfiles(o);
|
|
268
353
|
const journal = new OperationJournal(o.storage);
|
|
269
354
|
const out = [];
|
|
270
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
|
+
"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.
|
|
46
|
-
"@openlfcp/storage": "^0.
|
|
47
|
-
"@openlfcp/
|
|
48
|
-
"@openlfcp/
|
|
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
|
}
|