@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,422 @@
1
+ import { bytesEqual, fromBase64url, fromHex, hash32, principalId, resourceId, secureRandom, toBase64url, toHex, } from "@openlfcp/core";
2
+ import { activeDevice, certificateChain, DEVICE_KIND, decodeInviteSecret, deviceFingerprint, encodeInviteSecret, encodePrincipalDescriptor, invitationPrincipal, pairingDeviceSigner, parseDataUnit, parseDeviceCertificate, parsePairingUri, revocationOf, } from "@openlfcp/wire";
3
+ import { fetchDeks, genesisOwner, IdentityConflict, loadIdentityDocument, runUnitPhase, sealIdentityUnit, syncChain, } from "./context.js";
4
+ import { IDENTITY_PROFILE, principalRefOf, resourceRef, } from "./document.js";
5
+ import { newOperationId, OperationJournal, pairingSecretRef, phaseOf } from "./journal.js";
6
+ import { ChannelPool, IDENTITY_EXTENSION, IdentityRefused, IdentityUnavailable, } from "./network.js";
7
+ import { setLocalIdentityResource } from "./resource.js";
8
+ /** §5.3 steps 2–3. */
9
+ export async function requestPairing(o) {
10
+ let link;
11
+ let J;
12
+ try {
13
+ link = parsePairingUri(typeof o.link === "string" ? o.link : o.link.reveal());
14
+ J = pairingDeviceSigner(link);
15
+ }
16
+ catch (e) {
17
+ return { kind: "invalid-link", reason: e instanceof Error ? e.message : String(e) };
18
+ }
19
+ const I = link.identityResourceId;
20
+ const url = link.endpoints[0];
21
+ const B = o.installation.signer.descriptor;
22
+ const pool = new ChannelPool(o.network);
23
+ const journal = new OperationJournal(o.storage);
24
+ let operation;
25
+ try {
26
+ // A request already journaled for this session and installation is resumed (§9.1).
27
+ const open = (await journal.list({ kind: "pairing-request", status: "open" })).find((op) => op.intent.pairingDevice === toHex(J.descriptor.principalId) &&
28
+ op.intent.device === toHex(B.principalId));
29
+ if (open !== undefined) {
30
+ operation = open.id;
31
+ const sent = await sendRequest(o, pool, open.id);
32
+ if (sent !== "committed-own")
33
+ return { kind: "refused", operation, code: sent.code };
34
+ return {
35
+ kind: "requested",
36
+ operation,
37
+ identity: I,
38
+ fingerprint: deviceFingerprint(B.principalId),
39
+ };
40
+ }
41
+ const channel = await pool.get(J, url);
42
+ const chain = await syncChain(o.storage, channel, I);
43
+ const R = genesisOwner(chain);
44
+ if (chain.state.dataProfile !== IDENTITY_PROFILE)
45
+ return { kind: "not-a-session", reason: "the Resource is not an Identity Resource" };
46
+ const j = activeDevice(chain.state, J.descriptor.principalId);
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
+ };
52
+ const pairing = { signer: J, agreement: link.secret.agreementKey };
53
+ const deks = await fetchDeks(o.storage, o.secrets, channel, chain, pairing);
54
+ const doc = o.document(I, R);
55
+ await loadIdentityDocument({ channel, chain, deks, doc });
56
+ if (!doc.initialized())
57
+ return {
58
+ kind: "missing-material",
59
+ missing: deks.size === 0 ? ["the DEKs of the Identity Resource"] : ["the root's unit 1"],
60
+ };
61
+ // Step 3: the candidates B may be registered in, persisted before the request.
62
+ const chosen = new Set((o.scope ?? []).map((r) => resourceRef(r)));
63
+ const offered = [...doc.roster()].filter(([ref, e]) => e.archived !== true && ref !== resourceRef(I));
64
+ if (o.sections !== undefined) {
65
+ const unknown = [];
66
+ for (const name of o.sections) {
67
+ const hits = offered.filter(([, e]) => e.name === name);
68
+ if (hits.length === 1)
69
+ chosen.add(hits[0][0]);
70
+ else
71
+ unknown.push(name);
72
+ }
73
+ if (unknown.length > 0)
74
+ return {
75
+ kind: "unknown-sections",
76
+ unknown,
77
+ available: offered
78
+ .map(([, e]) => e.name)
79
+ .filter((n) => n !== undefined)
80
+ .sort(),
81
+ };
82
+ }
83
+ const candidates = offered
84
+ .filter(([ref]) => o.kind === 0 || chosen.has(ref))
85
+ .map(([ref, e]) => ({
86
+ resource: toHex(fromBase64url(ref)),
87
+ url: e.coordinator,
88
+ profile: e.profile,
89
+ ...(e.name === undefined ? {} : { name: e.name }),
90
+ }));
91
+ const secret = encodeInviteSecret(link.secret);
92
+ try {
93
+ await o.secrets.put(pairingSecretRef(J.descriptor.principalId), secret);
94
+ }
95
+ finally {
96
+ secret.fill(0);
97
+ }
98
+ // The whole request is durable before anything is sent (§9.1): a restart
99
+ // resumes it from the journal (completePairing), never a second request.
100
+ operation = newOperationId("pairing-request", secureRandom(8));
101
+ await journal.begin({
102
+ id: operation,
103
+ kind: "pairing-request",
104
+ createdAt: o.now(),
105
+ intent: {
106
+ identity: toHex(I),
107
+ url,
108
+ root: toHex(R),
109
+ pairingDevice: toHex(J.descriptor.principalId),
110
+ device: toHex(B.principalId),
111
+ kind: o.kind,
112
+ label: o.label,
113
+ candidates,
114
+ request: {
115
+ descriptor: toBase64url(encodePrincipalDescriptor(B)),
116
+ pairing: principalRefOf(J.descriptor.principalId),
117
+ label: o.label,
118
+ kind: o.kind,
119
+ ...(o.kind === 1
120
+ ? { scope: candidates.map((c) => toBase64url(fromHex(c.resource))) }
121
+ : {}),
122
+ ...(o.platform === undefined ? {} : { platform: o.platform }),
123
+ ...(o.requestedAt === undefined ? {} : { requested_at: o.requestedAt }),
124
+ },
125
+ },
126
+ });
127
+ const sent = await sendRequest(o, pool, operation);
128
+ if (sent !== "committed-own")
129
+ return { kind: "refused", operation, code: sent.code };
130
+ return {
131
+ kind: "requested",
132
+ operation,
133
+ identity: I,
134
+ fingerprint: deviceFingerprint(B.principalId),
135
+ };
136
+ }
137
+ catch (e) {
138
+ if (e instanceof IdentityUnavailable ||
139
+ e instanceof IdentityRefused ||
140
+ e instanceof IdentityConflict)
141
+ return {
142
+ kind: "unavailable",
143
+ ...(operation === undefined ? {} : { operation }),
144
+ reason: e.message,
145
+ };
146
+ throw e;
147
+ }
148
+ finally {
149
+ pool.closeAll();
150
+ }
151
+ }
152
+ /** The pairing device of a journaled request, from the SecretStore (§9.8). */
153
+ async function pairingSigner(o, J) {
154
+ const bytes = await o.secrets.get(pairingSecretRef(J));
155
+ if (bytes === undefined)
156
+ return undefined;
157
+ try {
158
+ const secret = decodeInviteSecret(bytes);
159
+ return {
160
+ signer: { key: secret.signingKey, descriptor: invitationPrincipal(secret) },
161
+ agreement: secret.agreementKey,
162
+ };
163
+ }
164
+ finally {
165
+ bytes.fill(0);
166
+ }
167
+ }
168
+ /**
169
+ * Sends the journaled request as the pairing device: the journaled exact
170
+ * bytes when they exist, else a unit built from the journaled request at the
171
+ * next sequence the pairing device has on the coordinator (§5.3 step 3).
172
+ */
173
+ async function sendRequest(o, pool, operation) {
174
+ const journal = new OperationJournal(o.storage);
175
+ const op = await journal.load(operation);
176
+ if (phaseOf(op, "request")?.state === "committed-own")
177
+ return "committed-own";
178
+ const I = resourceId(fromHex(op.intent.identity));
179
+ const R = principalId(fromHex(op.intent.root));
180
+ const pairing = await pairingSigner(o, principalId(fromHex(op.intent.pairingDevice)));
181
+ if (pairing === undefined)
182
+ return { code: "PAIRING_SECRET_GONE" };
183
+ const channel = await pool.get(pairing.signer, op.intent.url);
184
+ const B = principalId(fromHex(op.intent.device));
185
+ const unit = await runUnitPhase(channel, journal, operation, {
186
+ id: "request",
187
+ resource: I,
188
+ // A request refused as stale (I rotated before it arrived) is written again
189
+ // in the current epoch (W §19.1); its refused bytes stay in the phase.
190
+ rebuildOn: ["STALE_DATA_EPOCH"],
191
+ build: async () => {
192
+ // Never the sequence of a unit already signed for this request (W §26.2).
193
+ const signed = phaseOf(await journal.load(operation), "request")?.bytes;
194
+ const used = signed === undefined ? 0n : parseDataUnit(fromHex(signed)).payload.actorSeq;
195
+ const chain = await syncChain(o.storage, channel, I);
196
+ const deks = await fetchDeks(o.storage, o.secrets, channel, chain, pairing);
197
+ const doc = o.document(I, R);
198
+ const load = await loadIdentityDocument({ channel, chain, deks, doc });
199
+ const previous = load.latest.get(toHex(pairing.signer.descriptor.principalId));
200
+ return sealIdentityUnit({
201
+ storage: o.storage,
202
+ chain,
203
+ deks,
204
+ signer: pairing.signer,
205
+ doc,
206
+ latest: load.latest,
207
+ // J may be used by another installation: the next sequence after the one it sees (§5.3).
208
+ seq: ((previous?.seq ?? 0n) > used ? (previous?.seq ?? 0n) : used) + 1n,
209
+ intents: [
210
+ {
211
+ intent: "request.put",
212
+ key: principalRefOf(B),
213
+ entry: op.intent.request,
214
+ },
215
+ ],
216
+ });
217
+ },
218
+ });
219
+ return unit.state === "committed-own" ? "committed-own" : { code: unit.code ?? "refused" };
220
+ }
221
+ /** The accepted DEVICE_GRANT leaves naming `device` in a chain's history, oldest first. */
222
+ function leavesOf(chain, device) {
223
+ return chain.records
224
+ .filter((r) => r.body.type === "DEVICE_GRANT" &&
225
+ bytesEqual(r.body.chain.leaf.payload.device.principalId, device))
226
+ .map((r) => r.body.chain);
227
+ }
228
+ const sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
229
+ export function candidateEvidence(chain, device, digest, result) {
230
+ const leaves = leavesOf(chain, device);
231
+ const proven = leaves.some((l) => bytesEqual(l.leaf.payloadDigest, digest));
232
+ const d = chain.state.devices.get(toHex(device));
233
+ const active = d?.registered === true && activeDevice(chain.state, device) !== undefined;
234
+ const same = d !== undefined &&
235
+ d.kind === result.kind &&
236
+ sameSet(d.mask.map(String), result.mask.map(String)) &&
237
+ sameSet(d.scope.map(toHex), result.scope.map(toHex));
238
+ return Object.freeze({
239
+ leaves: Object.freeze(leaves.map((l) => l.leaf.id)),
240
+ proven,
241
+ registration: active ? "active" : "pending",
242
+ usable: proven && active && same,
243
+ });
244
+ }
245
+ /**
246
+ * §5.3 steps 8 and 11: learns the session's result and the status of
247
+ * every candidate. Resumable at any point from the persisted candidates
248
+ * (and while `J` is active, from the link's secret in the SecretStore).
249
+ */
250
+ export async function completePairing(o) {
251
+ const journal = new OperationJournal(o.storage);
252
+ let op = await journal.load(o.operation);
253
+ const I = resourceId(fromHex(op.intent.identity));
254
+ const R = principalId(fromHex(op.intent.root));
255
+ const Jid = principalId(fromHex(op.intent.pairingDevice));
256
+ const B = o.installation.signer;
257
+ const D = B.descriptor.principalId;
258
+ const kind = op.intent.kind;
259
+ const pool = new ChannelPool(o.network);
260
+ try {
261
+ // Step 8: the session's state first, from J's view of I (W §41 after its
262
+ // revocation): a closed session's result is never hidden by a failed send.
263
+ if (op.intent.result === undefined) {
264
+ const bytes = await o.secrets.get(pairingSecretRef(Jid));
265
+ if (bytes === undefined)
266
+ return { kind: "unknown", reason: "the pairing secret is gone" };
267
+ let J;
268
+ try {
269
+ const secret = decodeInviteSecret(bytes);
270
+ J = { key: secret.signingKey, descriptor: invitationPrincipal(secret) };
271
+ }
272
+ finally {
273
+ bytes.fill(0);
274
+ }
275
+ let chain;
276
+ try {
277
+ chain = await syncChain(o.storage, await pool.get(J, op.intent.url), I);
278
+ }
279
+ catch (e) {
280
+ if (e instanceof IdentityRefused)
281
+ return {
282
+ kind: "unknown",
283
+ reason: "the pairing device can no longer read and no proof is served",
284
+ };
285
+ throw e;
286
+ }
287
+ const r = revocationOf(chain.state, R, Jid);
288
+ if (r === undefined) {
289
+ // J is active: a request that never reached the coordinator is sent now,
290
+ // from the journal, or written again in the current epoch (§9.1).
291
+ if (phaseOf(op, "request")?.state !== "committed-own") {
292
+ const sent = await sendRequest(o, pool, o.operation);
293
+ if (sent !== "committed-own")
294
+ return { kind: "unknown", reason: `the request was refused: ${sent.code}` };
295
+ }
296
+ return { kind: "waiting" };
297
+ }
298
+ if (r.reason === 3n && r.approval !== null)
299
+ op = await journal.updateIntent(o.operation, {
300
+ result: "approved",
301
+ digest: toHex(r.approval),
302
+ });
303
+ else {
304
+ await journal.updateIntent(o.operation, { result: "rejected" });
305
+ await journal.setStatus(o.operation, "abandoned");
306
+ await o.secrets.delete(pairingSecretRef(Jid));
307
+ return { kind: "rejected" };
308
+ }
309
+ await o.secrets.delete(pairingSecretRef(Jid));
310
+ }
311
+ if (op.intent.result === "rejected")
312
+ return { kind: "rejected" };
313
+ const digest = hash32(fromHex(op.intent.digest));
314
+ const candidates = op.intent.candidates.map((c) => ({
315
+ resource: resourceId(fromHex(c.resource)),
316
+ url: c.url,
317
+ ...(c.name === undefined ? {} : { name: c.name }),
318
+ }));
319
+ // A personal B is registered in I itself; a service B only in its candidates.
320
+ const places = kind === 0 ? [{ resource: I, url: op.intent.url }, ...candidates] : candidates;
321
+ let certificate = op.intent.certificate === undefined
322
+ ? null
323
+ : certificateChain(op.intent.certificate.map((c) => parseDeviceCertificate(fromHex(c))), R);
324
+ let foreign = 0;
325
+ const seen = new Map();
326
+ for (const p of places) {
327
+ try {
328
+ const channel = await pool.get(B, p.url);
329
+ if (!channel.extensions.includes(IDENTITY_EXTENSION)) {
330
+ seen.set(toHex(p.resource), "unsupported");
331
+ continue;
332
+ }
333
+ const chain = await syncChain(o.storage, channel, p.resource);
334
+ seen.set(toHex(p.resource), { chain });
335
+ for (const leaf of leavesOf(chain, D)) {
336
+ if (!bytesEqual(leaf.leaf.payloadDigest, digest)) {
337
+ foreign++;
338
+ continue;
339
+ }
340
+ // The first verified certificate decides (§5.3 step 8).
341
+ if (certificate === null &&
342
+ bytesEqual(leaf.root, R) &&
343
+ bytesEqual(leaf.leaf.payload.root, R) &&
344
+ bytesEqual(leaf.leaf.payload.device.principalId, D)) {
345
+ certificate = leaf;
346
+ op = await journal.updateIntent(o.operation, {
347
+ certificate: leaf.certificates.map((c) => toHex(c.signed.bytes)),
348
+ });
349
+ if (kind === 0)
350
+ await setLocalIdentityResource(o.storage, I);
351
+ }
352
+ }
353
+ }
354
+ catch (e) {
355
+ if (e instanceof IdentityUnavailable)
356
+ seen.set(toHex(p.resource), "unreachable");
357
+ else if (e instanceof IdentityRefused)
358
+ seen.set(toHex(p.resource), "closed");
359
+ else if (e instanceof IdentityConflict)
360
+ seen.set(toHex(p.resource), "unreachable");
361
+ else
362
+ throw e;
363
+ }
364
+ }
365
+ if (certificate === null)
366
+ // "approved, waiting for registration": no candidate's outcome is known yet.
367
+ return { kind: "approved", certificate: null, candidates: [], foreignCertificates: foreign };
368
+ const result = certificate.leaf.payload;
369
+ const out = [];
370
+ for (const p of places) {
371
+ const s = seen.get(toHex(p.resource));
372
+ const status = async () => {
373
+ if (result.kind !== DEVICE_KIND.PERSONAL &&
374
+ !result.scope.some((r) => bytesEqual(r, p.resource)))
375
+ return bytesEqual(p.resource, I) ? "pending-registration" : "not-granted";
376
+ if (s === "unreachable" || s === undefined)
377
+ return "unreachable";
378
+ if (s === "unsupported")
379
+ return "unsupported";
380
+ if (s === "closed")
381
+ return "pending-registration";
382
+ const evidence = candidateEvidence(s.chain, D, digest, result);
383
+ if (evidence.registration === "pending")
384
+ return "pending-registration";
385
+ if (!evidence.usable)
386
+ return "unproven";
387
+ const channel = await pool.get(B, p.url);
388
+ const deks = await fetchDeks(o.storage, o.secrets, channel, s.chain, o.installation);
389
+ return [...s.chain.state.epochs.keys()].every((e) => deks.has(e))
390
+ ? "ready"
391
+ : "waiting-for-keys";
392
+ };
393
+ const named = p.name === undefined ? {} : { name: p.name };
394
+ try {
395
+ out.push({ resource: p.resource, status: await status(), ...named });
396
+ }
397
+ catch (e) {
398
+ if (e instanceof IdentityUnavailable || e instanceof IdentityRefused)
399
+ out.push({ resource: p.resource, status: "unreachable", ...named });
400
+ else
401
+ throw e;
402
+ }
403
+ }
404
+ if (out.every((c) => c.status === "ready" || c.status === "not-granted"))
405
+ await journal.setStatus(o.operation, "done");
406
+ return {
407
+ kind: "approved",
408
+ certificate,
409
+ candidates: Object.freeze(out),
410
+ foreignCertificates: foreign,
411
+ };
412
+ }
413
+ catch (e) {
414
+ if (e instanceof IdentityUnavailable || e instanceof IdentityConflict)
415
+ return { kind: "unavailable", reason: e.message };
416
+ throw e;
417
+ }
418
+ finally {
419
+ pool.closeAll();
420
+ }
421
+ }
422
+ //# sourceMappingURL=pairing-request.js.map
@@ -0,0 +1,225 @@
1
+ import { type Hash32, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ import { type CertificateChain, type Signer } from "@openlfcp/wire";
3
+ import { type FlowOptions, type MaskPreset, type RegistrationResult } from "./devices.js";
4
+ import { type LinearChain } from "./phases.js";
5
+ import { type IdentityFlowOptions } from "./resource.js";
6
+ import type { RotationOutcome } from "./rotation.js";
7
+ /**
8
+ * Pairing a new installation, the managing side `A` (IDENTITY-PROFILE-01
9
+ * §5.3 steps 1, 4–7, 9 and 10): a pairing device `J` registered in the
10
+ * Identity Resource for one session, the request it carries validated and
11
+ * checked against the complete future certificate chain, then the two
12
+ * phases: consume `J` with the approved payload's digest, sign and register
13
+ * exactly that payload. Every phase is journaled and classified by §9.2, so
14
+ * a crash or a lost answer resumes without signing anything else; a
15
+ * pairing session yields at most one approved payload.
16
+ */
17
+ /** A bearer link (§5.2): its text is revealed on demand and never logged. */
18
+ export declare class PairingLinkText {
19
+ #private;
20
+ constructor(uri: string);
21
+ /** The lfcp://pair URI with its #secret= fragment. Show it to the human; never log it. */
22
+ reveal(): string;
23
+ toJSON(): string;
24
+ toString(): string;
25
+ }
26
+ export type PairingSessionResult = {
27
+ readonly kind: "created";
28
+ readonly session: string;
29
+ readonly pairingDevice: PrincipalId;
30
+ readonly link: PairingLinkText;
31
+ }
32
+ /** §5.3 step 1: the root already has 32 live devices in I; revoke one first. */
33
+ | {
34
+ readonly kind: "limit";
35
+ readonly reason: string;
36
+ }
37
+ /** A lacks the root's unit 1 or a DEK of I: no link (§5.3 step 1). */
38
+ | {
39
+ readonly kind: "missing-material";
40
+ readonly missing: readonly string[];
41
+ }
42
+ /** This installation is not the root or an active managing device of it. */
43
+ | {
44
+ readonly kind: "not-authorized";
45
+ readonly reason: string;
46
+ } | {
47
+ readonly kind: "refused";
48
+ readonly code: string;
49
+ } | {
50
+ readonly kind: "conflict";
51
+ } | {
52
+ readonly kind: "unavailable";
53
+ readonly session?: string;
54
+ readonly reason: string;
55
+ };
56
+ /**
57
+ * §5.3 step 1: creates a pairing device `J` (kind 2, mask {1, 2}, scope
58
+ * [I]), registers it in `I`, seals to it the DEK of every epoch of `I`, and
59
+ * returns the link. Before anything is committed `A` must hold, verified,
60
+ * the root's unit 1 and the DEK of every epoch of `I` (otherwise no link),
61
+ * and the DEVICE_GRANT must fit the root's 32 live devices (W §17.4 rule 8).
62
+ */
63
+ export declare function createPairingSession(o: IdentityFlowOptions & {
64
+ readonly endpoints?: readonly string[];
65
+ }): Promise<PairingSessionResult>;
66
+ /**
67
+ * §5.3 step 1 after a crash before the link was shown: completes the
68
+ * registration of `J` and its packages from the journal, then returns the
69
+ * link again.
70
+ */
71
+ export declare function resumePairingSession(o: IdentityFlowOptions, session: string): Promise<PairingSessionResult>;
72
+ /** A pairing request `A` may show (§5.3 step 4). */
73
+ export interface PairingReview {
74
+ readonly session: string;
75
+ /** `requests[k]`: the PrincipalRef of `D_B`. */
76
+ readonly requestKey: string;
77
+ readonly device: PrincipalId;
78
+ /** base64url of the exact descriptor bytes. */
79
+ readonly descriptor: string;
80
+ readonly label: string;
81
+ readonly platform?: string;
82
+ readonly kind: 0 | 1;
83
+ /** The requested scope (kind 1), the Identity Resource already removed. */
84
+ readonly requestedScope: readonly ResourceId[];
85
+ /** §5.2 device fingerprint of `D_B`, for the human to compare with B's screen. */
86
+ readonly fingerprint: string;
87
+ /** Each preset of §5.4 for the request's kind, and whether the future chain passes (§5.3 step 4). */
88
+ readonly presets: readonly {
89
+ readonly preset: MaskPreset;
90
+ readonly offered: boolean;
91
+ readonly reason?: string;
92
+ }[];
93
+ /**
94
+ * The hashes of the changes that wrote the request, in causal order: the
95
+ * request change hash §5.3 step 4 journals with the approval.
96
+ */
97
+ readonly requestChanges: readonly string[];
98
+ /** The confirmed tuple: when the entry or any change to it differs, the confirmation is asked again. */
99
+ readonly tuple: string;
100
+ }
101
+ /** §5.3 step 4: the valid requests of a session, each with the presets whose future chain passes. */
102
+ export declare function pairingRequests(o: IdentityFlowOptions & {
103
+ readonly session: string;
104
+ }): Promise<readonly PairingReview[]>;
105
+ export type ApprovalResult = {
106
+ readonly kind: "approved";
107
+ readonly operation: string;
108
+ readonly device: PrincipalId;
109
+ readonly chain: CertificateChain;
110
+ /** §5.3 step 10: the Resources of the roster (or the approved scope). */
111
+ readonly registrations: readonly RegistrationResult[];
112
+ /** §5.3 step 9: the consumed pairing device was a reader of I (§8.4). */
113
+ readonly rotation: RotationOutcome;
114
+ }
115
+ /** The entry changed since the human confirmed it: ask again (§5.3 step 4). */
116
+ | {
117
+ readonly kind: "reconfirm";
118
+ readonly review: PairingReview | undefined;
119
+ }
120
+ /** The preset's future chain does not pass on the current head (§5.3 step 4). */
121
+ | {
122
+ readonly kind: "infeasible";
123
+ readonly reason: string;
124
+ }
125
+ /** The session was consumed by another approval or cancelled: nothing is signed (§9.2). */
126
+ | {
127
+ readonly kind: "superseded";
128
+ readonly operation: string;
129
+ }
130
+ /** Phase 2 meets the 32-device limit: retry after a revocation; the approval stays consumed (§5.3 step 6). */
131
+ | {
132
+ readonly kind: "limit";
133
+ readonly operation: string;
134
+ } | {
135
+ readonly kind: "refused";
136
+ readonly operation: string;
137
+ readonly code: string;
138
+ }
139
+ /**
140
+ * Approved in the chain (phases 1 and 2, keys, rotation and registrations
141
+ * as for `approved`), but the coordinator refused the document unit of
142
+ * §5.3 step 6 with `code`: the device entry is not written and the request
143
+ * not removed. The operation stays open; resumeApproval writes the unit
144
+ * again on the current state.
145
+ */
146
+ | {
147
+ readonly kind: "document-incomplete";
148
+ readonly operation: string;
149
+ readonly device: PrincipalId;
150
+ readonly chain: CertificateChain;
151
+ readonly registrations: readonly RegistrationResult[];
152
+ readonly rotation: RotationOutcome;
153
+ readonly code: string;
154
+ } | {
155
+ readonly kind: "conflict";
156
+ } | {
157
+ readonly kind: "unavailable";
158
+ readonly operation?: string;
159
+ readonly reason: string;
160
+ };
161
+ /**
162
+ * §5.3 steps 4–7 and 10: approves the confirmed request with a preset
163
+ * (and, for a service device, a scope within the requested one). The
164
+ * complete certificate payload and its digest are journaled before
165
+ * phase 1; phase 2 signs exactly that payload.
166
+ */
167
+ export declare function approvePairing(o: IdentityFlowOptions & {
168
+ readonly review: PairingReview;
169
+ readonly preset: MaskPreset;
170
+ /** Service only: the approved scope, a subset of the requested one. */
171
+ readonly scope?: readonly ResourceId[];
172
+ /** RFC 3339 UTC for `devices[...].added_at`. */
173
+ readonly addedAt?: string;
174
+ }): Promise<ApprovalResult>;
175
+ /** Phase 1 (§5.3 step 5): `DEVICE_REVOKE {R, J, 3, digest}` on the chain's head. */
176
+ export declare const consumeRecord: (chain: LinearChain, signer: Signer, root: PrincipalId, pairing: PrincipalId, digest: Hash32) => Uint8Array;
177
+ /** Phase 2 (§5.3 step 6): `DEVICE_GRANT {R, chain_B, 2: J}` on the chain's head. */
178
+ export declare const approvedGrantRecord: (chain: LinearChain, signer: Signer, root: PrincipalId, device: CertificateChain, pairing: PrincipalId) => Uint8Array;
179
+ /** §5.3 step 9: `DEVICE_REVOKE {R, J, 0}` on the chain's head. */
180
+ export declare const cancelRecord: (chain: LinearChain, signer: Signer, root: PrincipalId, pairing: PrincipalId) => Uint8Array;
181
+ /** Resumes a journaled approval from its next phase (§9.1: "If A crashes after step 5 …"). */
182
+ export declare function resumeApproval(o: IdentityFlowOptions, operation: string): Promise<ApprovalResult>;
183
+ export type CancelResult = {
184
+ readonly kind: "cancelled";
185
+ readonly rotation: RotationOutcome;
186
+ }
187
+ /**
188
+ * Cancelled in the chain, but the coordinator refused the unit that
189
+ * deletes the rejected request (§5.3 step 4) with `code`: the request
190
+ * entry stays. The session stays open; cancelPairing with the same
191
+ * `reject` writes the unit again.
192
+ */
193
+ | {
194
+ readonly kind: "document-incomplete";
195
+ readonly rotation: RotationOutcome;
196
+ readonly code: string;
197
+ }
198
+ /** The session was consumed by an approval: not cancelled (§9.2 Cancel: superseded). */
199
+ | {
200
+ readonly kind: "consumed";
201
+ } | {
202
+ readonly kind: "refused";
203
+ readonly code: string;
204
+ } | {
205
+ readonly kind: "conflict";
206
+ } | {
207
+ readonly kind: "unavailable";
208
+ readonly reason: string;
209
+ };
210
+ /**
211
+ * §5.3 step 9 (rejection, timeout, cancel): `DEVICE_REVOKE {R, J, 0}`.
212
+ * With `reject`, the request entry is deleted first (step 4: a request the
213
+ * human does not confirm is rejected).
214
+ */
215
+ export declare function cancelPairing(o: IdentityFlowOptions & {
216
+ readonly session: string;
217
+ readonly reject?: string;
218
+ }): Promise<CancelResult>;
219
+ /** Open pairing sessions of this installation, oldest first. */
220
+ export declare function openPairingSessions(o: Pick<FlowOptions, "storage">): Promise<{
221
+ session: string;
222
+ identity: ResourceId;
223
+ pairingDevice: PrincipalId;
224
+ }[]>;
225
+ //# sourceMappingURL=pairing.d.ts.map