@openlfcp/client 0.2.2 → 0.3.1

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.
Files changed (43) hide show
  1. package/dist/data-unit.d.ts +11 -0
  2. package/dist/data-unit.js +9 -0
  3. package/dist/identity/accept.d.ts +115 -0
  4. package/dist/identity/accept.js +335 -0
  5. package/dist/identity/context.d.ts +214 -0
  6. package/dist/identity/context.js +495 -0
  7. package/dist/identity/devices.d.ts +127 -0
  8. package/dist/identity/devices.js +221 -0
  9. package/dist/identity/document.d.ts +112 -0
  10. package/dist/identity/document.js +54 -0
  11. package/dist/identity/index.d.ts +17 -0
  12. package/dist/identity/index.js +17 -0
  13. package/dist/identity/journal.d.ts +139 -0
  14. package/dist/identity/journal.js +202 -0
  15. package/dist/identity/network.d.ts +88 -0
  16. package/dist/identity/network.js +243 -0
  17. package/dist/identity/pairing-request.d.ts +147 -0
  18. package/dist/identity/pairing-request.js +422 -0
  19. package/dist/identity/pairing.d.ts +225 -0
  20. package/dist/identity/pairing.js +624 -0
  21. package/dist/identity/phases.d.ts +75 -0
  22. package/dist/identity/phases.js +104 -0
  23. package/dist/identity/proof.d.ts +45 -0
  24. package/dist/identity/proof.js +71 -0
  25. package/dist/identity/reconcile.d.ts +73 -0
  26. package/dist/identity/reconcile.js +273 -0
  27. package/dist/identity/resource.d.ts +74 -0
  28. package/dist/identity/resource.js +201 -0
  29. package/dist/identity/revocation.d.ts +106 -0
  30. package/dist/identity/revocation.js +403 -0
  31. package/dist/identity/roster.d.ts +89 -0
  32. package/dist/identity/roster.js +130 -0
  33. package/dist/identity/rotation.d.ts +131 -0
  34. package/dist/identity/rotation.js +363 -0
  35. package/dist/index.d.ts +2 -1
  36. package/dist/index.js +2 -1
  37. package/dist/revoke.d.ts +8 -0
  38. package/dist/revoke.js +12 -8
  39. package/dist/storage.d.ts +18 -0
  40. package/dist/storage.js +26 -0
  41. package/dist/sync-client.d.ts +2 -0
  42. package/dist/sync-client.js +1 -0
  43. package/package.json +5 -5
@@ -0,0 +1,221 @@
1
+ import { bytesEqual, LfcpError, secureRandom, toHex, } from "@openlfcp/core";
2
+ import { generateAgreementKeyPair, generateSigningKeyPair } from "@openlfcp/crypto";
3
+ import { activeDevice, authorizeDeviceGrant, certificateChain, checkDeviceCertificatePayload, DEVICE_ABILITY, DEVICE_KIND, newDeviceCertificateSerial, ownerAction, principalDescriptorFromKeys, signControlRecord, signDeviceCertificate, validateControlChain, } from "@openlfcp/wire";
4
+ import { deliverKeys, fetchDeks, IdentityConflict, runControlPhase, } from "./context.js";
5
+ import { newOperationId, OperationJournal, PackageLedger } from "./journal.js";
6
+ import { ChannelPool, IDENTITY_EXTENSION, IdentityRefused, IdentityUnavailable, } from "./network.js";
7
+ import { classifyRegister } from "./phases.js";
8
+ /** The mask presets of IDENTITY-PROFILE-01 §5.4 (decision I6). Codes 9 and 11 are never listed. */
9
+ export const MASK_PRESETS = Object.freeze({
10
+ full: Object.freeze({
11
+ kind: DEVICE_KIND.PERSONAL,
12
+ mask: [1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n, 10n, 12n, 13n],
13
+ }),
14
+ edit: Object.freeze({ kind: DEVICE_KIND.PERSONAL, mask: [1n, 2n, 3n] }),
15
+ read: Object.freeze({ kind: DEVICE_KIND.PERSONAL, mask: [1n] }),
16
+ service: Object.freeze({ kind: DEVICE_KIND.SERVICE, mask: [1n, 2n] }),
17
+ });
18
+ const hasCode = (mask, code) => mask.includes(code);
19
+ /**
20
+ * The certificates above a leaf that `issuer` signs for `root` in the
21
+ * state: none when the issuer is the root, else the issuer's own current
22
+ * chain, which must be active there (W §7.3 (c3), §17.4 rule 6).
23
+ */
24
+ export function issuerCertificates(state, issuer, root) {
25
+ if (bytesEqual(issuer, root))
26
+ return [];
27
+ const d = activeDevice(state, issuer);
28
+ if (d === undefined || d.chain === null || !bytesEqual(d.root, root))
29
+ return undefined;
30
+ return d.chain.certificates;
31
+ }
32
+ /** A leaf payload for `device` with a fresh serial (W §7.3). */
33
+ export function leafPayload(root, device, kind, mask, scope) {
34
+ return Object.freeze({
35
+ root,
36
+ device,
37
+ kind,
38
+ mask: Object.freeze([...mask].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))),
39
+ scope: Object.freeze([...scope]),
40
+ serial: newDeviceCertificateSerial(),
41
+ });
42
+ }
43
+ /** An Ed25519/X25519 Principal used only to give a not-yet-signed leaf a certificate shape. */
44
+ function placeholderSigner() {
45
+ const key = generateSigningKeyPair();
46
+ return { key, descriptor: principalDescriptorFromKeys(key, generateAgreementKeyPair()) };
47
+ }
48
+ /**
49
+ * IDENTITY-PROFILE-01 §5.3 step 4: whether the DEVICE_GRANT that would
50
+ * register (or bind) `leaf` through `issuer` would be accepted by the
51
+ * Resource's current state, before anything is committed or signed for
52
+ * the device (N9). The complete future chain is built with the leaf's
53
+ * signature taken as given (a placeholder never leaves this function) and
54
+ * checked with the validators' own rules: structure (kind, mask, scope,
55
+ * c1, c5, c7), c3 and c4 against the issuer's chain, and W §17.4 rules 4
56
+ * to 8 on the state after `consume` when a pairing session is given (its
57
+ * device then counts as revoked, rule 8), else on the current state.
58
+ */
59
+ export function checkIssuance(options) {
60
+ const { chain, issuer, leaf } = options;
61
+ const root = leaf.root;
62
+ const no = (reason) => ({ feasible: false, reason });
63
+ const issuers = issuerCertificates(chain.state, issuer.descriptor.principalId, root);
64
+ if (issuers === undefined)
65
+ return no("the issuer is not the root or an active device of it");
66
+ let future;
67
+ try {
68
+ checkDeviceCertificatePayload(leaf);
69
+ future = certificateChain([signDeviceCertificate(leaf, placeholderSigner()), ...issuers], root);
70
+ }
71
+ catch (e) {
72
+ if (e instanceof LfcpError)
73
+ return no(e.message);
74
+ throw e;
75
+ }
76
+ let state = chain.state;
77
+ if (options.pairing !== undefined) {
78
+ const consume = signControlRecord({ resourceId: state.resourceId, controlSeq: state.seq + 1n, prevControlId: state.head }, {
79
+ type: "DEVICE_REVOKE",
80
+ root,
81
+ device: options.pairing,
82
+ reason: 3n,
83
+ approval: future.leaf.payloadDigest,
84
+ }, issuer);
85
+ const after = validateControlChain([
86
+ ...chain.records.map((r) => r.signed.bytes),
87
+ consume.bytes,
88
+ ]);
89
+ if (after.kind !== "linear")
90
+ return no(`the session cannot be consumed by this issuer (${after.kind === "invalid" ? after.error.message : after.kind})`);
91
+ state = after.state;
92
+ }
93
+ const a = authorizeDeviceGrant(state, issuer.descriptor.principalId, {
94
+ type: "DEVICE_GRANT",
95
+ root,
96
+ chain: future,
97
+ ...(options.pairing === undefined ? {} : { pairingDevice: options.pairing }),
98
+ });
99
+ return a.allowed ? { feasible: true } : no(a.reason);
100
+ }
101
+ /** Signs `leaf` through `issuer` and returns the complete chain (§5.3 step 6). */
102
+ export function signChain(state, issuer, leaf) {
103
+ const issuers = issuerCertificates(state, issuer.descriptor.principalId, leaf.root);
104
+ if (issuers === undefined)
105
+ throw new LfcpError("AUTHORIZATION_FAILED", "the issuer is not the root or an active device of it");
106
+ return certificateChain([signDeviceCertificate(leaf, issuer), ...issuers], leaf.root);
107
+ }
108
+ /**
109
+ * Whether `actor` may propose device records for `root`'s devices in the
110
+ * Resource (§5.3 step 10, §7.1): it is the root, an active device of the
111
+ * root with `12`, or, when the root owns the Resource, an active device
112
+ * with `13`.
113
+ */
114
+ export function mayManageDevices(state, actor, root) {
115
+ if (bytesEqual(actor, root))
116
+ return true;
117
+ const d = activeDevice(state, actor);
118
+ if (d === undefined || !bytesEqual(d.root, root))
119
+ return false;
120
+ if (hasCode(d.mask, DEVICE_ABILITY.DEVICE_MANAGE))
121
+ return true;
122
+ return bytesEqual(state.owner.principalId, root) && ownerAction(state, actor);
123
+ }
124
+ /**
125
+ * Registers a device in Resources (§5.3 step 10, §7.2, §7.3, §7.1 step
126
+ * 2): `DEVICE_GRANT {R, chain}` on each target where this installation may
127
+ * act, then a Key Package of every epoch DEK it holds there to the device,
128
+ * each recorded in the ledger. One journaled operation; each Resource is
129
+ * a phase classified by §9.2 before anything is proposed, so a
130
+ * registration that exists by another hand is `complete` and is not
131
+ * proposed again. Resources this installation cannot reach are listed.
132
+ */
133
+ export async function registerDevice(options) {
134
+ const journal = new OperationJournal(options.storage);
135
+ const ledger = new PackageLedger(options.storage);
136
+ const device = options.chain.leaf.payload.device;
137
+ const root = options.chain.root;
138
+ const operation = options.operation ?? newOperationId("register-device", secureRandom(8));
139
+ if ((await journal.get(operation)) === undefined)
140
+ await journal.begin({
141
+ id: operation,
142
+ kind: "register-device",
143
+ createdAt: options.now(),
144
+ intent: {
145
+ device: toHex(device.principalId),
146
+ root: toHex(root),
147
+ leaf: toHex(options.chain.leaf.id),
148
+ targets: options.targets.map((t) => ({ resource: toHex(t.resource), url: t.url })),
149
+ },
150
+ });
151
+ const pool = new ChannelPool(options.network);
152
+ const results = [];
153
+ const me = options.installation.signer;
154
+ try {
155
+ for (const t of options.targets) {
156
+ const result = (status, extra = {}) => results.push(Object.freeze({ resource: t.resource, status, ...extra }));
157
+ try {
158
+ const channel = await pool.get(me, t.url);
159
+ if (!channel.extensions.includes(IDENTITY_EXTENSION)) {
160
+ result("unsupported");
161
+ continue;
162
+ }
163
+ const outcome = await runControlPhase(options.storage, channel, journal, operation, {
164
+ id: `register:${toHex(t.resource)}`,
165
+ kind: "register",
166
+ resource: t.resource,
167
+ classify: (chain, isOwn) => {
168
+ const s = classifyRegister(chain, { root, chain: options.chain, isOwnRecord: isOwn });
169
+ if (s === "pending" && !mayManageDevices(chain.state, me.descriptor.principalId, root))
170
+ throw new NotAuthorizedHere();
171
+ return s;
172
+ },
173
+ build: (chain) => signControlRecord({
174
+ resourceId: t.resource,
175
+ controlSeq: chain.state.seq + 1n,
176
+ prevControlId: chain.state.head,
177
+ }, { type: "DEVICE_GRANT", root, chain: options.chain }, me).bytes,
178
+ });
179
+ if (outcome.state === "superseded") {
180
+ result("superseded");
181
+ continue;
182
+ }
183
+ if (outcome.state === "refused") {
184
+ result("refused", { code: outcome.code ?? "refused" });
185
+ continue;
186
+ }
187
+ const deks = await fetchDeks(options.storage, options.secrets, channel, outcome.chain, options.installation);
188
+ const keys = await deliverKeys({
189
+ channel,
190
+ ledger,
191
+ chain: outcome.chain,
192
+ sender: me,
193
+ recipients: [device],
194
+ deks,
195
+ });
196
+ result("registered", { keys });
197
+ }
198
+ catch (e) {
199
+ if (e instanceof NotAuthorizedHere)
200
+ result("not-authorized");
201
+ else if (e instanceof IdentityUnavailable)
202
+ result("unreachable");
203
+ else if (e instanceof IdentityRefused)
204
+ result("not-authorized", { code: e.code });
205
+ else if (e instanceof IdentityConflict)
206
+ result("conflict");
207
+ else
208
+ throw e;
209
+ }
210
+ }
211
+ }
212
+ finally {
213
+ pool.closeAll();
214
+ }
215
+ const open = results.some((r) => r.status === "unreachable" || r.status === "conflict");
216
+ await journal.setStatus(operation, open ? "open" : "done");
217
+ return Object.freeze({ operation, results: Object.freeze(results) });
218
+ }
219
+ class NotAuthorizedHere extends Error {
220
+ }
221
+ //# sourceMappingURL=devices.js.map
@@ -0,0 +1,112 @@
1
+ import { type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ import { type ControlState, type DataProfileCodec } from "@openlfcp/wire";
3
+ /**
4
+ * The Identity Resource's document, as the identity flows see it
5
+ * (IDENTITY-PROFILE-01 §§2–4).
6
+ *
7
+ * @openlfcp/client never binds to the CRDT engine (LFCP-031): the
8
+ * application passes an IdentityDocumentPort, normally backed by
9
+ * @openlfcp/shared-objects' IdentityReplica, which owns admission (§3) and
10
+ * state validation (§4). The flows only produce intents, carry the
11
+ * resulting plaintext in Data Units and read the validated entries back.
12
+ * Nothing in the document confers authority (§4 rule 5): every decision
13
+ * of a flow is taken from a Control Chain.
14
+ */
15
+ /** IDENTITY-PROFILE-01's Data Profile identifier. */
16
+ export declare const IDENTITY_PROFILE = "org.openlfcp.identity.v1";
17
+ /** §2.3: the writer's class at the Data Unit's control_ref. */
18
+ export type IdentityWriterClass = "root" | "personal" | "pairing" | "other";
19
+ export interface IdentityAuthor {
20
+ readonly signer: PrincipalId;
21
+ /** The LFCP Data Unit actor sequence of the unit that carries the change. */
22
+ readonly actorSeq: bigint;
23
+ readonly writerClass: IdentityWriterClass;
24
+ }
25
+ export interface RosterEntryValue {
26
+ readonly profile: string;
27
+ readonly endpoints: readonly string[];
28
+ readonly coordinator: string;
29
+ readonly name?: string;
30
+ readonly archived?: boolean;
31
+ }
32
+ export interface DeviceEntryValue {
33
+ readonly label: string;
34
+ readonly kind: 0 | 1;
35
+ readonly platform?: string;
36
+ readonly added_at?: string;
37
+ readonly revoked?: boolean;
38
+ readonly missing_keys?: Readonly<Record<string, readonly (bigint | number)[]>>;
39
+ }
40
+ export interface PairingRequestValue {
41
+ /** base64url of the deterministic CBOR principal descriptor (W §7). */
42
+ readonly descriptor: string;
43
+ /** PrincipalRef of the pairing device that wrote the request. */
44
+ readonly pairing: string;
45
+ readonly label: string;
46
+ readonly kind: 0 | 1;
47
+ readonly scope?: readonly string[];
48
+ readonly platform?: string;
49
+ readonly requested_at?: string;
50
+ }
51
+ export type IdentityIntent = {
52
+ readonly intent: "identity.initialize";
53
+ } | {
54
+ readonly intent: "roster.put";
55
+ readonly key: string;
56
+ readonly entry: RosterEntryValue;
57
+ } | {
58
+ readonly intent: "device.put";
59
+ readonly key: string;
60
+ readonly entry: DeviceEntryValue;
61
+ } | {
62
+ readonly intent: "request.put";
63
+ readonly key: string;
64
+ readonly entry: PairingRequestValue;
65
+ } | {
66
+ readonly intent: "roster.delete" | "device.delete" | "request.delete";
67
+ readonly key: string;
68
+ };
69
+ export interface IdentityDocumentPort {
70
+ /**
71
+ * The plaintext of one new change by `author` carrying `intents` in one
72
+ * transaction, already applied to the local document. Throws when the
73
+ * document refuses it (the actor sequence is then not used).
74
+ */
75
+ write(author: IdentityAuthor, intents: readonly IdentityIntent[]): Uint8Array;
76
+ /** Applies a received unit's plaintext (§3 admission); false when it is refused or held. */
77
+ receive(plaintext: Uint8Array, author: IdentityAuthor): boolean;
78
+ /** Whether the initial change (§2.2) is admitted. */
79
+ initialized(): boolean;
80
+ /** The valid roster entries (§4 rule 1), by ResourceRef. */
81
+ roster(): ReadonlyMap<string, RosterEntryValue>;
82
+ /** The valid device entries, by PrincipalRef. */
83
+ devices(): ReadonlyMap<string, DeviceEntryValue>;
84
+ /** The valid pairing requests, by PrincipalRef. */
85
+ requests(): ReadonlyMap<string, PairingRequestValue>;
86
+ /**
87
+ * The admitted changes that created, wrote into or deleted `table[key]`,
88
+ * in causal order, with the signer of each (IdentityReplica.origins).
89
+ * Taken from the changes themselves, never inferred from the merged
90
+ * state: §5.3 step 4 accepts a request only when every change that
91
+ * wrote it was signed by the session's pairing device, and the approval
92
+ * journals these change hashes.
93
+ */
94
+ origins(table: "roster" | "devices" | "requests", key: string): readonly {
95
+ readonly hash: string;
96
+ readonly signer: PrincipalId;
97
+ }[];
98
+ }
99
+ /** A passthrough Data Unit codec: the port produces and consumes the plaintext. */
100
+ export declare const identityPlaintextCodec: DataProfileCodec<Uint8Array>;
101
+ /** ResourceRef (§2.1): unpadded base64url of the 32-byte Resource ID. */
102
+ export declare const resourceRef: (id: ResourceId) => string;
103
+ /** PrincipalRef (SOP §27): "p:" + unpadded base64url of the Principal ID. */
104
+ export declare const principalRefOf: (id: PrincipalId) => string;
105
+ /** The Principal ID of a PrincipalRef; undefined when it is malformed. */
106
+ export declare function principalOfRef(ref: string): PrincipalId | undefined;
107
+ /**
108
+ * §2.3: the writer class of `signer` from the Identity Resource's Control
109
+ * state at the unit's control_ref; `root` is the Genesis owner.
110
+ */
111
+ export declare function identityWriterClass(state: ControlState, root: PrincipalId, signer: PrincipalId): IdentityWriterClass;
112
+ //# sourceMappingURL=document.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { bytesEqual, fromBase64url, principalId, toBase64url, toHex, } from "@openlfcp/core";
2
+ import { DEVICE_KIND } from "@openlfcp/wire";
3
+ /**
4
+ * The Identity Resource's document, as the identity flows see it
5
+ * (IDENTITY-PROFILE-01 §§2–4).
6
+ *
7
+ * @openlfcp/client never binds to the CRDT engine (LFCP-031): the
8
+ * application passes an IdentityDocumentPort, normally backed by
9
+ * @openlfcp/shared-objects' IdentityReplica, which owns admission (§3) and
10
+ * state validation (§4). The flows only produce intents, carry the
11
+ * resulting plaintext in Data Units and read the validated entries back.
12
+ * Nothing in the document confers authority (§4 rule 5): every decision
13
+ * of a flow is taken from a Control Chain.
14
+ */
15
+ /** IDENTITY-PROFILE-01's Data Profile identifier. */
16
+ export const IDENTITY_PROFILE = "org.openlfcp.identity.v1";
17
+ /** A passthrough Data Unit codec: the port produces and consumes the plaintext. */
18
+ export const identityPlaintextCodec = Object.freeze({
19
+ dataProfile: IDENTITY_PROFILE,
20
+ encode: (plaintext) => plaintext,
21
+ decode: (plaintext) => plaintext,
22
+ });
23
+ /** ResourceRef (§2.1): unpadded base64url of the 32-byte Resource ID. */
24
+ export const resourceRef = (id) => toBase64url(id);
25
+ /** PrincipalRef (SOP §27): "p:" + unpadded base64url of the Principal ID. */
26
+ export const principalRefOf = (id) => `p:${toBase64url(id)}`;
27
+ /** The Principal ID of a PrincipalRef; undefined when it is malformed. */
28
+ export function principalOfRef(ref) {
29
+ if (!ref.startsWith("p:") || ref.length !== 45)
30
+ return undefined;
31
+ try {
32
+ return principalId(fromBase64url(ref.slice(2)));
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ /**
39
+ * §2.3: the writer class of `signer` from the Identity Resource's Control
40
+ * state at the unit's control_ref; `root` is the Genesis owner.
41
+ */
42
+ export function identityWriterClass(state, root, signer) {
43
+ if (bytesEqual(signer, root))
44
+ return "root";
45
+ const d = state.devices.get(toHex(signer));
46
+ if (d === undefined || !bytesEqual(d.root, root))
47
+ return "other";
48
+ if (d.kind === DEVICE_KIND.PERSONAL)
49
+ return "personal";
50
+ if (d.kind === DEVICE_KIND.PAIRING)
51
+ return "pairing";
52
+ return "other";
53
+ }
54
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1,17 @@
1
+ /** Identity flows (IDENTITY-PROFILE-01 §§5–9, LFCP-04-410). */
2
+ export { acceptInvitationAsDevice, claimForRootRecord, createResourceAsDevice, type DeviceAcceptResult, type DeviceCreateResult, type DeviceResourceResult, type IncompleteDeviceResource, selfRegistrationRecord, } from "./accept.js";
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";
6
+ export { type JournalOperation, type JournalPhase, type LedgerEntry, OperationJournal, type OperationKind, type OperationStatus, PackageLedger, pairingSecretRef, pendingDekRef, } from "./journal.js";
7
+ export { ChannelPool, type ConnectionNetworkOptions, connectionNetwork, IDENTITY_EXTENSION, type IdentityAnswer, type IdentityChannel, type IdentityNetwork, IdentityRefused, IdentityUnavailable, type OpenAnswer, } from "./network.js";
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";
10
+ export { certificateInScope, classifyCancel, classifyConsume, classifyPackage, classifyRegister, classifyRevoke, classifyRotate, keyEpochAfter, type LinearChain, type PhaseState, type RegisterPhase, recordSeq, } from "./phases.js";
11
+ export { type RevocationStatus, revocationStatus } from "./proof.js";
12
+ export { planReconciliation, type ReconcileStep, reconcileDevices, reconcileRecord, type TargetReconciliation, } from "./reconcile.js";
13
+ export { type CreatedIdentityResource, type CreateIdentityResourceOptions, createIdentityResource, type IdentityDocumentFactory, type IdentityFlowOptions, identityTarget, localIdentityResource, resumeIdentityCreation, setLocalIdentityResource, } from "./resource.js";
14
+ export { previewRevocation, type ResourcePreview, type RevocationPreview, type RevocationResult, type RevokeDeviceResult, revokeDevice, rotateIfExposed, } from "./revocation.js";
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";
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,17 @@
1
+ /** Identity flows (IDENTITY-PROFILE-01 §§5–9, LFCP-04-410). */
2
+ export { acceptInvitationAsDevice, claimForRootRecord, createResourceAsDevice, selfRegistrationRecord, } from "./accept.js";
3
+ export { deliverKeys, extendChain, fetchDeks, genesisOwner, heldDeks, IdentityConflict, loadIdentityDocument, readersAt, runControlPhase, runUnitPhase, sealIdentityUnit, storeDek, syncChain, } from "./context.js";
4
+ export { checkIssuance, issuerCertificates, leafPayload, MASK_PRESETS, mayManageDevices, registerDevice, signChain, } from "./devices.js";
5
+ export { IDENTITY_PROFILE, identityPlaintextCodec, identityWriterClass, principalOfRef, principalRefOf, resourceRef, } from "./document.js";
6
+ export { OperationJournal, PackageLedger, pairingSecretRef, pendingDekRef, } from "./journal.js";
7
+ export { ChannelPool, connectionNetwork, IDENTITY_EXTENSION, IdentityRefused, IdentityUnavailable, } from "./network.js";
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";
10
+ export { certificateInScope, classifyCancel, classifyConsume, classifyPackage, classifyRegister, classifyRevoke, classifyRotate, keyEpochAfter, recordSeq, } from "./phases.js";
11
+ export { revocationStatus } from "./proof.js";
12
+ export { planReconciliation, reconcileDevices, reconcileRecord, } from "./reconcile.js";
13
+ export { createIdentityResource, identityTarget, localIdentityResource, resumeIdentityCreation, setLocalIdentityResource, } from "./resource.js";
14
+ export { previewRevocation, revokeDevice, rotateIfExposed, } from "./revocation.js";
15
+ export { addToRoster, writeRosterEntry, } from "./roster.js";
16
+ export { exposingRevocationsSince, rotateAfter, rotatorTest, verifiedFrontier, } from "./rotation.js";
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,139 @@
1
+ import { type DataEpoch, type Hash32, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ import { type ResourceDEK } from "@openlfcp/crypto";
3
+ import { type LfcpStorage, type SecretStore } from "@openlfcp/storage";
4
+ import type { PhaseState } from "./phases.js";
5
+ /**
6
+ * The durable operation journal and the package ledger
7
+ * (IDENTITY-PROFILE-01 §9.1, §9.5, §9.8).
8
+ *
9
+ * Pairing, accepting an invitation, creating a Resource, revocation with
10
+ * rotation and reconciliation each commit several records and packages,
11
+ * possibly on several Resources. Before any record or package is sent the
12
+ * client persists the operation's intent and the exact signed bytes of the
13
+ * next record; after each ACK it persists the committed record ID. A phase
14
+ * is repeated only by re-sending the same exact bytes; after a
15
+ * CONTROL_HEAD_MISMATCH or a restart it is classified against the accepted
16
+ * chain (phases.ts) and rebuilt only when it is still `pending` on a new
17
+ * head.
18
+ *
19
+ * Journal rows are local marks (`identity-op:<id>`) and hold no secrets
20
+ * (§9.8): a bearer link, a pairing device's keys and a DEK go to the
21
+ * SecretStore only, under the references below, before the row that names
22
+ * them is written (LFCP-034 ordering).
23
+ */
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
+ /** The state of a whole operation. */
26
+ export type OperationStatus =
27
+ /** Phases remain; resume it. */
28
+ "open"
29
+ /** Every phase is committed or complete. */
30
+ | "done"
31
+ /** A phase was superseded: the operation ended without its effect (§9.2). */
32
+ | "superseded"
33
+ /** The human or the flow abandoned it (a cancelled session, a refused request). */
34
+ | "abandoned";
35
+ /** A journaled phase. Byte strings are hex; every value is public. */
36
+ export interface JournalPhase {
37
+ /** Unique within the operation, e.g. `consume`, `register:<resource hex>`. */
38
+ readonly id: string;
39
+ readonly kind: "host" | "unit" | "consume" | "register" | "cancel" | "revoke" | "rotate" | "claim" | "document";
40
+ /** The Resource the phase acts on (hex). */
41
+ readonly resource: string;
42
+ readonly state: PhaseState | "refused";
43
+ /** The exact signed bytes of the record or unit to (re)send, persisted before sending. */
44
+ readonly bytes?: string;
45
+ /** The head those bytes were built on (their prev_control_id), for a Control Record. */
46
+ readonly expectedHead?: string;
47
+ /** Every record ID this installation built for the phase, in order (§9.2 committed-own). */
48
+ readonly sent?: readonly string[];
49
+ /** The committed record ID, after the ACK (or after classification found it). */
50
+ readonly recordId?: string;
51
+ /** The NACK code of a refused phase. */
52
+ readonly code?: string;
53
+ /** Phase-specific public facts (a digest, a revocation sequence, an epoch). */
54
+ readonly facts?: Readonly<Record<string, string>>;
55
+ }
56
+ export interface JournalOperation {
57
+ readonly v: 1;
58
+ readonly id: string;
59
+ readonly kind: OperationKind;
60
+ readonly status: OperationStatus;
61
+ /** Milliseconds since the epoch, from the caller's clock; display only. */
62
+ readonly createdAt: number;
63
+ /** The operation's intent: public facts only (§9.8). */
64
+ readonly intent: Readonly<Record<string, unknown>>;
65
+ readonly phases: readonly JournalPhase[];
66
+ }
67
+ type JournalStorage = Pick<LfcpStorage, "localMarks" | "commit">;
68
+ /** The durable operation journal (§9.1). */
69
+ export declare class OperationJournal {
70
+ #private;
71
+ constructor(storage: JournalStorage);
72
+ /** Persists a new operation before any of its records is sent. */
73
+ begin(op: Omit<JournalOperation, "v" | "status" | "phases"> & {
74
+ readonly phases?: readonly JournalPhase[];
75
+ }): Promise<JournalOperation>;
76
+ get(id: string): Promise<JournalOperation | undefined>;
77
+ /** Every journaled operation, optionally of one kind and status, oldest first. */
78
+ list(filter?: {
79
+ readonly kind?: OperationKind;
80
+ readonly status?: OperationStatus;
81
+ }): Promise<readonly JournalOperation[]>;
82
+ /** Writes the phase (replacing the one with its ID), durably. */
83
+ putPhase(id: string, phase: JournalPhase): Promise<JournalOperation>;
84
+ /** Updates the intent with more public facts (e.g. the session's record IDs). */
85
+ updateIntent(id: string, patch: Readonly<Record<string, unknown>>): Promise<JournalOperation>;
86
+ setStatus(id: string, status: OperationStatus): Promise<JournalOperation>;
87
+ /** Removes a finished operation's row. */
88
+ remove(id: string): Promise<void>;
89
+ load(id: string): Promise<JournalOperation>;
90
+ }
91
+ /** The phase `id` of `op`, if journaled. */
92
+ export declare const phaseOf: (op: JournalOperation, id: string) => JournalPhase | undefined;
93
+ /** One sent Key Package (§9.5). */
94
+ export interface LedgerEntry {
95
+ readonly resource: ResourceId;
96
+ readonly epoch: DataEpoch;
97
+ readonly recipient: PrincipalId;
98
+ readonly packageId: Hash32;
99
+ }
100
+ /**
101
+ * The package ledger (§9.5): every Key Package this installation sent, as
102
+ * `(resource, epoch, recipient, package ID)`. It drives reconciliation's
103
+ * key step (§7.1 step 3); delivery is never inferred from the absence of a
104
+ * complaint, and a cleared entry causes a re-send.
105
+ */
106
+ export declare class PackageLedger {
107
+ #private;
108
+ constructor(storage: JournalStorage);
109
+ /** Records an acknowledged package and drops its pending bytes, in one commit. */
110
+ record(entry: LedgerEntry): Promise<void>;
111
+ /** The exact bytes of a package sent but not yet acknowledged (§9.1). */
112
+ pending(resource: ResourceId, epoch: DataEpoch, recipient: PrincipalId): Promise<Uint8Array | undefined>;
113
+ /** Persists a package's exact bytes before it is sent. */
114
+ putPending(resource: ResourceId, epoch: DataEpoch, recipient: PrincipalId, bytes: Uint8Array): Promise<void>;
115
+ has(resource: ResourceId, epoch: DataEpoch, recipient: PrincipalId): Promise<boolean>;
116
+ /** Every entry for `resource`. */
117
+ list(resource: ResourceId): Promise<readonly LedgerEntry[]>;
118
+ /**
119
+ * "Re-send keys" (§7.1 step 3): forgets every package sent to
120
+ * `recipient`, in `resource` or everywhere, so the next reconciliation
121
+ * re-seals them.
122
+ */
123
+ clear(recipient: PrincipalId, resource?: ResourceId): Promise<number>;
124
+ }
125
+ /**
126
+ * The SecretStore slot of a DEK generated for a KEY_EPOCH that is not
127
+ * committed yet (§9.1: persisted before the record is sent). It is not the
128
+ * epoch's DEK slot: until the record is accepted, another rotator may
129
+ * commit that epoch number with another DEK.
130
+ */
131
+ export declare const pendingDekRef: (resource: ResourceId, operation: string) => import("@openlfcp/storage").SecretRef;
132
+ /** The SecretStore slot of a pairing device's invite-secret (§5.2, §9.8). */
133
+ export declare const pairingSecretRef: (pairingDevice: PrincipalId) => import("@openlfcp/storage").SecretRef;
134
+ export declare function putPendingDek(secrets: SecretStore, resource: ResourceId, operation: string, dek: ResourceDEK): Promise<void>;
135
+ export declare function getPendingDek(secrets: SecretStore, resource: ResourceId, operation: string): Promise<ResourceDEK | undefined>;
136
+ /** A fresh, unique operation ID. */
137
+ export declare function newOperationId(kind: OperationKind, random: Uint8Array): string;
138
+ export {};
139
+ //# sourceMappingURL=journal.d.ts.map