@kokuin/controller 0.1.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.
@@ -0,0 +1,121 @@
1
+ import { DID_PREFIX } from './events.js';
2
+ import { authoritativeStates } from './history.js';
3
+ import { allStatesAsync } from './state.js';
4
+ import { agreementKeysFrom, DID_METHOD, signingKeyFrom } from './state-resolver.js';
5
+ const CONTEXT = 'Controller resolver';
6
+ /**
7
+ * A `did:kokuin:` resolver for `@kokuin/token`. Injected rather than imported by token: this package
8
+ * depends on token for signing, so the reverse import would cycle. Token resolves `iss` through the
9
+ * interface without knowing the fold exists.
10
+ */ export function createControllerResolver(options) {
11
+ // The fold currently under way for each DID, so overlapping resolutions of one DID share it (two
12
+ // tokens from one issuer verified in parallel is ordinary — token caches only `did:peer:4`).
13
+ //
14
+ // Also the last line against a `loadLog` that re-enters resolution for the DID it is loading. The
15
+ // fold no longer does that itself, but a caller's `loadLog` can still reach `resolve` for the same
16
+ // DID: sharing the pending fold makes that a quiet deadlock the caller's request timeout catches,
17
+ // rather than an await-chained Ed25519 loop that starves the timer queue.
18
+ //
19
+ // Not a cache: removed as soon as the fold settles, so a later resolution re-folds.
20
+ const inFlight = new Map();
21
+ // Shared by every member: load, fold, throw `Unknown DID` when absent. The whole state array
22
+ // rather than the head, because `resolveHistoric` answers about rotated-away keys; head-only
23
+ // members take `states[states.length - 1]`. Always the async fold: it differs from `foldLog` only
24
+ // in awaiting a capability-authorised revoke's verifier, so a mode-selecting option would choose
25
+ // nothing — the verifier is the only thing a caller supplies.
26
+ async function computeStates(did) {
27
+ const events = await options.loadLog(did);
28
+ if (events == null || events.length === 0) {
29
+ throw new Error(`Unknown DID: ${did}`);
30
+ }
31
+ const foldOptions = {
32
+ verifyCapability: options.verifyCapability
33
+ };
34
+ if (options.history == null) {
35
+ return await allStatesAsync({
36
+ did,
37
+ events,
38
+ context: CONTEXT,
39
+ options: foldOptions
40
+ });
41
+ }
42
+ // Compared against what this party last accepted, so a truncated log — folding cleanly, missing
43
+ // the revoke that matters — is refused. See `LogStore`.
44
+ const seen = await options.history.get(did);
45
+ const { log, states } = await authoritativeStates({
46
+ did,
47
+ loaded: events,
48
+ seen,
49
+ context: CONTEXT,
50
+ options: foldOptions
51
+ });
52
+ // Only after it has folded, and only in the memory-keeping direction: a log that lost never
53
+ // reaches here, so the store never moves backwards.
54
+ await options.history.set(did, log);
55
+ return states;
56
+ }
57
+ async function loadStates(did) {
58
+ if (!did.startsWith(DID_PREFIX)) {
59
+ throw new Error(`Unknown DID: ${did}`);
60
+ }
61
+ const pending = inFlight.get(did);
62
+ if (pending != null) {
63
+ // Awaiting an existing fold, never clearing it — the call that created it owns removal, so a
64
+ // joiner cannot evict an entry a third caller is about to join.
65
+ return await pending;
66
+ }
67
+ // Deferred by a microtask so the entry is registered *before* `loadLog` runs — otherwise a
68
+ // `loadLog` that re-enters `resolve` before its own first `await` recurses to a stack overflow
69
+ // while the map is still empty.
70
+ const fold = Promise.resolve().then(()=>computeStates(did));
71
+ inFlight.set(did, fold);
72
+ try {
73
+ return await fold;
74
+ } finally{
75
+ // Removal is what keeps this from being a cache: the next resolution re-reads and re-folds, so
76
+ // a revoke landing between two resolutions takes effect. In `finally`, so a rejected fold
77
+ // clears too.
78
+ inFlight.delete(did);
79
+ }
80
+ }
81
+ return {
82
+ method: DID_METHOD,
83
+ // The head's key set only: a profile that rotated away from a key answers "no" for it here, which
84
+ // is what makes `rotate` retire a leaked authority key rather than add one beside it.
85
+ async resolve (did, header = {}) {
86
+ return signingKeyFrom({
87
+ did,
88
+ states: await loadStates(did),
89
+ header
90
+ });
91
+ },
92
+ // Every key set within the current generation, for a caller verifying past-issued material. A
93
+ // `rotate` must not invalidate those; a `reset` still does, and the scan stops at the generation
94
+ // boundary. See `signingKeyFrom` in `state-resolver.ts`.
95
+ async resolveHistoric (did, header = {}) {
96
+ return signingKeyFrom({
97
+ did,
98
+ states: await loadStates(did),
99
+ header,
100
+ historic: true
101
+ });
102
+ },
103
+ async resolveDenySet (did) {
104
+ // The head's set, deliberately, not the state at any position a capability names: `iat` is
105
+ // author-supplied and backdatable, so anchoring to it would let a revoked holder pick a
106
+ // position at which it was not yet denied. The set is position-dependent *within the fold*; the
107
+ // question a verifier asks is "is this grant valid now". Both spellings ride in it (see
108
+ // `KeyState.deny`); the key half is enforced by `signingKeyFrom`, and the two forms cannot
109
+ // collide. `loadStates` throws `Unknown DID` rather than answering an empty (= "nobody
110
+ // revoked") set.
111
+ const states = await loadStates(did);
112
+ return states[states.length - 1].deny;
113
+ },
114
+ async resolveAgreementKey (did) {
115
+ // The head's set only, unlike `resolve`. A retired agreement key is a downgrade, not a grant
116
+ // the profile already made: the recipient decrypts old ciphertexts by re-deriving from the
117
+ // seed, never by resolution.
118
+ return agreementKeysFrom(did, await loadStates(did));
119
+ }
120
+ };
121
+ }
@@ -0,0 +1,60 @@
1
+ import { type DIDMethodResolver, type ResolvedAgreementKey, type ResolvedSigningKey, type ResolveIssuerHeader } from '@kokuin/token';
2
+ import type { KeyState } from './fold.js';
3
+ /** The method segment of {@link DID_PREFIX}, so the two cannot drift. */
4
+ export declare const DID_METHOD: string;
5
+ /**
6
+ * What a token signed by a revoked key is rejected with. The `#`-prefixed key follows after `: `, so
7
+ * match with `startsWith`.
8
+ */
9
+ export declare const KEY_REVOKED = "kid names a key the controller has revoked";
10
+ /**
11
+ * The signing key `header` names, out of an already-folded log. `historic` picks which question above
12
+ * is asked, and defaults to the safe one.
13
+ *
14
+ * **A revoked key answers neither question.** A `rev` naming `#<key>` retires a key for material it
15
+ * has *already* signed, and this is where that bites — every `did:kokuin:` signature check reaches a
16
+ * resolved key through here, so the denial covers `verifyToken` on both `historic` settings, every
17
+ * capability and revocation record `@kokuin/capability` verifies, and the fold's own cap-authorised
18
+ * revoke. The historic arm is the live one: a revoked key is never in the head's `k` (the
19
+ * `KeyState.deny` invariant), so only the backwards scan can select one, and without this
20
+ * `resolveHistoric` would accept a leaked key forever.
21
+ *
22
+ * The head arm cannot be reached from a log this package folded, but is not decoration: `states`
23
+ * arrives from the caller, and the invariant is the fold's property, not the array's — a
24
+ * caller-built state whose head publishes a key it also denies resolves to nothing here.
25
+ *
26
+ * **Evaluated against the head's deny set, never the position the key was selected at.** A denial is
27
+ * about the key, not one event, and the position an artefact carries is author-supplied — else a
28
+ * thief presents a token resolving at a position before the revoke. Deliberately *not* how a DID
29
+ * denial behaves inside the fold, where `states[i]` is consulted at position `i` because "was this
30
+ * author allowed to act *here*" is the whole question; the two rules answer different questions.
31
+ * Inside the fold they coincide anyway: `createStateResolver` gets the prefix `states[0..i-1]`, so
32
+ * its head *is* the position being verified.
33
+ */
34
+ export declare function signingKeyFrom({ did, states, header, historic, }: {
35
+ did: string;
36
+ states: Array<KeyState>;
37
+ header: ResolveIssuerHeader;
38
+ historic?: boolean;
39
+ }): ResolvedSigningKey;
40
+ /**
41
+ * The head's key agreement set, minus anything it has revoked. A `rev` may name an agreement key as
42
+ * readily as a signing one (the target spelling is the key, not its purpose), and these are the keys
43
+ * a *sender* encrypts to — encrypting to a revoked key is the one outcome a denial must prevent.
44
+ *
45
+ * Like the signing side, unreachable from a log this package folded (the `KeyState.deny` invariant)
46
+ * but `states` arrives from the caller. Filtering to empty is the honest answer for a state that
47
+ * denies everything it publishes: `@kokuin/jwe` then encrypts nothing, the fail-closed direction.
48
+ */
49
+ export declare function agreementKeysFrom(did: string, states: Array<KeyState>): Array<ResolvedAgreementKey>;
50
+ /**
51
+ * A `DIDMethodResolver` answering for exactly one profile, out of already-folded state. The fold
52
+ * hands one to a capability-authorised revoke's verifier, built from the states *before* the event
53
+ * being verified — the prefix contract made unforgettable: the verifier no longer needs a
54
+ * position-aware registry, because the fold supplies the answer. See `FoldOptions.verifyCapability`.
55
+ *
56
+ * Answers only for `did`; any other identifier is `Unknown DID`, so a caller merging this into a
57
+ * wider registry falls back to its own resolver for another `did:kokuin:` delegate and never serves
58
+ * one profile's state for another's. Not a cache: the states are the caller's, fixed at construction.
59
+ */
60
+ export declare function createStateResolver(did: string, states: Array<KeyState>): DIDMethodResolver;
@@ -0,0 +1,186 @@
1
+ import { IssuerKeyNotFoundError } from '@kokuin/token';
2
+ import { DID_PREFIX, keyTarget } from './events.js';
3
+ import { decodeKey } from './keys.js';
4
+ /** The method segment of {@link DID_PREFIX}, so the two cannot drift. */ export const DID_METHOD = DID_PREFIX.slice('did:'.length, -1);
5
+ /**
6
+ * The `kid` fragment's body, or `null` when the header names no key. Format is `#<the multibase key
7
+ * exactly as it appears in `k`>`, matched against the folded key sets by membership — an index-based
8
+ * fragment would mean whatever the key set said at the time, and a token outlives that. The bare key
9
+ * without `#` is rejected, not accepted as a second spelling.
10
+ *
11
+ * Every rejection on this path is an `IssuerKeyNotFoundError`, not the plain error the rest of this
12
+ * file throws: the DID resolved and its log folded — what failed is the key. Load-bearing, because
13
+ * `resolveIssuerWithDoc` retypes everything else as `UnresolvableIssuerError`, on which
14
+ * `@kokuin/capability` *denies* a capability; since `kid` is unauthenticated, a fabricated record
15
+ * naming this DID and any invented key would otherwise deny every capability this controller issued.
16
+ */ function keyFromKid(did, kid) {
17
+ if (kid == null) {
18
+ return null;
19
+ }
20
+ if (!kid.startsWith('#')) {
21
+ throw new IssuerKeyNotFoundError(`Controller ${did} kid is not a key fragment: ${kid}`);
22
+ }
23
+ return kid.slice(1);
24
+ }
25
+ /**
26
+ * The key a token's `kid` names, out of the **head's** key set alone — what `resolve` answers, and
27
+ * the only question authenticating a live signer may ask ("can this profile sign with this key
28
+ * *now*"). A key the profile rotated away is not an answer, which is what makes a `rotate` retire a
29
+ * leaked authority key for new issuance — the middle rung of the `revoke → rotate → reset` ladder.
30
+ *
31
+ * A `kid` naming a key not in the head's `k` (including one published earlier) is an error, **never
32
+ * a fall back to `keys[0]`**, which would check the signature against a key the token never claimed.
33
+ * An absent `kid` resolves to the head's first key: volunteering one is not the same as accepting one
34
+ * the token names. Past-issued material is {@link historicSigningKey}, via explicit `resolveHistoric`.
35
+ */ function headSigningKey(did, states, kid) {
36
+ const head = states[states.length - 1];
37
+ const key = keyFromKid(did, kid);
38
+ if (key == null) {
39
+ return head.keys[0];
40
+ }
41
+ if (!head.keys.includes(key)) {
42
+ throw new IssuerKeyNotFoundError(`Controller ${did} kid names a key that is not current: ${kid}`);
43
+ }
44
+ return key;
45
+ }
46
+ /**
47
+ * The key a token's `kid` names, out of **every position in the current generation** — what
48
+ * `resolveHistoric` answers. It accepts a key the profile has since rotated away, so it establishes
49
+ * "this profile did once hold this key", never "holds". The right answer for material minted in the
50
+ * past (an already-issued capability, a revocation record), because a routine `rotate` must not
51
+ * invalidate what the profile issued to third parties who cannot know it rotated; the wrong answer
52
+ * for a live signer.
53
+ *
54
+ * A `reset` is what invalidates (it discards everything under the prior generation), and `gen` never
55
+ * decreases, so the backwards scan stops dead at the first earlier-generation state. Runs backwards
56
+ * because the head's own key set is the common answer. A `kid` naming a never-published key, or one
57
+ * from a superseded generation, is an error — never a fall back to `keys[0]`.
58
+ */ function historicSigningKey(did, states, kid) {
59
+ const head = states[states.length - 1];
60
+ const key = keyFromKid(did, kid);
61
+ if (key == null) {
62
+ return head.keys[0];
63
+ }
64
+ for(let i = states.length - 1; i >= 0 && states[i].gen === head.gen; i--){
65
+ if (states[i].keys.includes(key)) {
66
+ return key;
67
+ }
68
+ }
69
+ throw new IssuerKeyNotFoundError(`Controller ${did} kid names a key outside the current generation: ${kid}`);
70
+ }
71
+ /**
72
+ * What a token signed by a revoked key is rejected with. The `#`-prefixed key follows after `: `, so
73
+ * match with `startsWith`.
74
+ */ export const KEY_REVOKED = 'kid names a key the controller has revoked';
75
+ /**
76
+ * The signing key `header` names, out of an already-folded log. `historic` picks which question above
77
+ * is asked, and defaults to the safe one.
78
+ *
79
+ * **A revoked key answers neither question.** A `rev` naming `#<key>` retires a key for material it
80
+ * has *already* signed, and this is where that bites — every `did:kokuin:` signature check reaches a
81
+ * resolved key through here, so the denial covers `verifyToken` on both `historic` settings, every
82
+ * capability and revocation record `@kokuin/capability` verifies, and the fold's own cap-authorised
83
+ * revoke. The historic arm is the live one: a revoked key is never in the head's `k` (the
84
+ * `KeyState.deny` invariant), so only the backwards scan can select one, and without this
85
+ * `resolveHistoric` would accept a leaked key forever.
86
+ *
87
+ * The head arm cannot be reached from a log this package folded, but is not decoration: `states`
88
+ * arrives from the caller, and the invariant is the fold's property, not the array's — a
89
+ * caller-built state whose head publishes a key it also denies resolves to nothing here.
90
+ *
91
+ * **Evaluated against the head's deny set, never the position the key was selected at.** A denial is
92
+ * about the key, not one event, and the position an artefact carries is author-supplied — else a
93
+ * thief presents a token resolving at a position before the revoke. Deliberately *not* how a DID
94
+ * denial behaves inside the fold, where `states[i]` is consulted at position `i` because "was this
95
+ * author allowed to act *here*" is the whole question; the two rules answer different questions.
96
+ * Inside the fold they coincide anyway: `createStateResolver` gets the prefix `states[0..i-1]`, so
97
+ * its head *is* the position being verified.
98
+ */ export function signingKeyFrom({ did, states, header, historic = false }) {
99
+ const head = states[states.length - 1];
100
+ if (head.keys.length === 0) {
101
+ throw new Error(`Controller ${did} has no signing key`);
102
+ }
103
+ const selected = historic ? historicSigningKey(did, states, header.kid) : headSigningKey(did, states, header.kid);
104
+ // `IssuerKeyNotFoundError` like every rejection here: the DID resolved and the log folded, and what
105
+ // failed is the key. Retyping it unresolvable would let a `kid` (unauthenticated) make this
106
+ // controller read as unresolvable to every fail-closed caller. See {@link keyFromKid}.
107
+ if (head.deny.has(keyTarget(selected))) {
108
+ throw new IssuerKeyNotFoundError(`Controller ${did} ${KEY_REVOKED}: ${keyTarget(selected)}`);
109
+ }
110
+ const key = decodeKey(selected);
111
+ if (key.alg === 'X25519') {
112
+ throw new Error(`Controller ${did} signing key is not a signature algorithm: ${key.alg}`);
113
+ }
114
+ return {
115
+ alg: key.alg,
116
+ publicKey: key.publicKey
117
+ };
118
+ }
119
+ /**
120
+ * The head's key agreement set, minus anything it has revoked. A `rev` may name an agreement key as
121
+ * readily as a signing one (the target spelling is the key, not its purpose), and these are the keys
122
+ * a *sender* encrypts to — encrypting to a revoked key is the one outcome a denial must prevent.
123
+ *
124
+ * Like the signing side, unreachable from a log this package folded (the `KeyState.deny` invariant)
125
+ * but `states` arrives from the caller. Filtering to empty is the honest answer for a state that
126
+ * denies everything it publishes: `@kokuin/jwe` then encrypts nothing, the fail-closed direction.
127
+ */ export function agreementKeysFrom(did, states) {
128
+ const head = states[states.length - 1];
129
+ return head.agreement.flatMap((value)=>{
130
+ if (head.deny.has(keyTarget(value))) {
131
+ return [];
132
+ }
133
+ const key = decodeKey(value);
134
+ if (key.alg !== 'X25519') {
135
+ throw new Error(`Controller ${did} publishes an unsupported agreement key: ${key.alg}`);
136
+ }
137
+ return {
138
+ alg: key.alg,
139
+ publicKey: key.publicKey
140
+ };
141
+ });
142
+ }
143
+ /**
144
+ * A `DIDMethodResolver` answering for exactly one profile, out of already-folded state. The fold
145
+ * hands one to a capability-authorised revoke's verifier, built from the states *before* the event
146
+ * being verified — the prefix contract made unforgettable: the verifier no longer needs a
147
+ * position-aware registry, because the fold supplies the answer. See `FoldOptions.verifyCapability`.
148
+ *
149
+ * Answers only for `did`; any other identifier is `Unknown DID`, so a caller merging this into a
150
+ * wider registry falls back to its own resolver for another `did:kokuin:` delegate and never serves
151
+ * one profile's state for another's. Not a cache: the states are the caller's, fixed at construction.
152
+ */ export function createStateResolver(did, states) {
153
+ function statesFor(asked) {
154
+ if (asked !== did) {
155
+ throw new Error(`Unknown DID: ${asked}`);
156
+ }
157
+ return states;
158
+ }
159
+ return {
160
+ method: DID_METHOD,
161
+ async resolve (asked, header = {}) {
162
+ return signingKeyFrom({
163
+ did: asked,
164
+ states: statesFor(asked),
165
+ header
166
+ });
167
+ },
168
+ // The head of *this prefix*, not the log: these states end at the position the fold is verifying.
169
+ // A capability issued before that position is the archived material `resolveHistoric` exists for.
170
+ async resolveHistoric (asked, header = {}) {
171
+ return signingKeyFrom({
172
+ did: asked,
173
+ states: statesFor(asked),
174
+ header,
175
+ historic: true
176
+ });
177
+ },
178
+ async resolveDenySet (asked) {
179
+ const known = statesFor(asked);
180
+ return known[known.length - 1].deny;
181
+ },
182
+ async resolveAgreementKey (asked) {
183
+ return agreementKeysFrom(asked, statesFor(asked));
184
+ }
185
+ };
186
+ }
package/lib/state.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { SignedEvent } from './events.js';
2
+ import { type FoldOptions, type KeyState } from './fold.js';
3
+ /**
4
+ * Current state via the synchronous {@link foldLog}. A capability-authorised revoke fails closed
5
+ * here — see {@link currentStateAsync}.
6
+ */
7
+ export declare function currentState(did: string, events: Array<SignedEvent>, context: string): KeyState;
8
+ /**
9
+ * Every per-position state, via {@link foldLogAsync}. What the resolver needs and the identity does
10
+ * not: a verifier has to answer about keys the log has since rotated away, since a token outlives the
11
+ * state that gave its `kid` meaning.
12
+ */
13
+ export declare function allStatesAsync({ did, events, context, options, }: {
14
+ did: string;
15
+ events: Array<SignedEvent>;
16
+ context: string;
17
+ options?: FoldOptions;
18
+ }): Promise<Array<KeyState>>;
19
+ /**
20
+ * Current state via {@link foldLogAsync}, which can await `options.verifyCapability` for a
21
+ * capability-authorised revoke instead of failing closed on it.
22
+ */
23
+ export declare function currentStateAsync({ did, events, context, options, }: {
24
+ did: string;
25
+ events: Array<SignedEvent>;
26
+ context: string;
27
+ options?: FoldOptions;
28
+ }): Promise<KeyState>;
package/lib/state.js ADDED
@@ -0,0 +1,34 @@
1
+ import { foldLog, foldLogAsync } from './fold.js';
2
+ /**
3
+ * Fold results to states, turning a fold failure into a throw. The identity and the resolver both
4
+ * need the same fold-reject-take-last sequence, sync and async, so it lives here once. `context`
5
+ * names the caller so its diagnostics survive the factoring.
6
+ */ function foldedStates(did, context, result) {
7
+ if (!result.ok) {
8
+ throw new Error(`${context}: invalid log for ${did}: ${result.reason} at event ${result.index}`);
9
+ }
10
+ return result.states;
11
+ }
12
+ function lastState(did, context, result) {
13
+ const states = foldedStates(did, context, result);
14
+ return states[states.length - 1];
15
+ }
16
+ /**
17
+ * Current state via the synchronous {@link foldLog}. A capability-authorised revoke fails closed
18
+ * here — see {@link currentStateAsync}.
19
+ */ export function currentState(did, events, context) {
20
+ return lastState(did, context, foldLog(did, events));
21
+ }
22
+ /**
23
+ * Every per-position state, via {@link foldLogAsync}. What the resolver needs and the identity does
24
+ * not: a verifier has to answer about keys the log has since rotated away, since a token outlives the
25
+ * state that gave its `kid` meaning.
26
+ */ export async function allStatesAsync({ did, events, context, options }) {
27
+ return foldedStates(did, context, await foldLogAsync(did, events, options));
28
+ }
29
+ /**
30
+ * Current state via {@link foldLogAsync}, which can await `options.verifyCapability` for a
31
+ * capability-authorised revoke instead of failing closed on it.
32
+ */ export async function currentStateAsync({ did, events, context, options }) {
33
+ return lastState(did, context, await foldLogAsync(did, events, options));
34
+ }
@@ -0,0 +1,97 @@
1
+ import { type SignedEvent } from './events.js';
2
+ import { type FoldOptions, type FoldResult } from './fold.js';
3
+ export type Duplicity = {
4
+ /** Position of the divergence, from the folded state of the first conflicting event. */
5
+ gen: number;
6
+ seq: number;
7
+ /**
8
+ * Digests of the two conflicting events *at the divergence point*, sorted — not of the branch
9
+ * heads, which for branches of unequal length say nothing about where they disagree.
10
+ */
11
+ digests: [string, string];
12
+ };
13
+ /**
14
+ * Why branch selection could not name a winner.
15
+ *
16
+ * - `duplicity` — two events the controller cannot both have authored at one position. The only one
17
+ * that says anything is wrong with the log.
18
+ * - `no-valid-branch` — nothing presented folded at all.
19
+ * - `needs-capability-verification` — **every** branch carries a capability-authorised revoke this
20
+ * call was not equipped to check, so there is nothing left to compare. The log is fine; use
21
+ * {@link resolveBranchesAsync} with a `verifyCapability`. A call where only *some* branches are
22
+ * unverifiable answers with the rest and reports the count as {@link ResolveResult.unverified}.
23
+ *
24
+ * Added rather than swapped in: `duplicity` still carries the `gen: -1` sentinel for the two
25
+ * non-duplicity arms, so a caller written against the previous shape still tells them apart.
26
+ */
27
+ export type ResolveFailure = 'duplicity' | 'no-valid-branch' | 'needs-capability-verification';
28
+ /**
29
+ * How many presented branches this call could not check, and therefore did not compare.
30
+ *
31
+ * Non-zero means the answer is **provisional**: an unfoldable branch may have superseded the one
32
+ * reported, so a caller acting on a winner must re-resolve through {@link resolveBranchesAsync} with
33
+ * a `verifyCapability` or treat the result as unconfirmed. Zero for every correctly configured call.
34
+ *
35
+ * Reported rather than fatal because the alternative is an unauthenticated DoS on duplicity
36
+ * detection. A cap-bearing revoke reaches the verifier path *before* any signature check (the
37
+ * capability names the signer), so a keyless peer can copy the public inception, append a `rev` with
38
+ * `sigs: []` and arbitrary `cap` bytes, and present it — and refusing the whole resolution on that
39
+ * would turn a genuine duplicity report between two honest branches into a refusal, for every
40
+ * profile. Dropping such a branch cannot hand an attacker a win (a branch that does not fold is not a
41
+ * contender); the only exposure left is a *stale* answer, which this count marks.
42
+ */
43
+ export type ResolveResult = {
44
+ ok: true;
45
+ winner: Array<SignedEvent>;
46
+ superseded: number;
47
+ unverified: number;
48
+ } | {
49
+ ok: false;
50
+ failure: ResolveFailure;
51
+ duplicity: Duplicity;
52
+ unverified: number;
53
+ };
54
+ /**
55
+ * Whether a branch failed to fold only because this call could not check a capability. Filtering such
56
+ * a branch out as invalid is what switched duplicity detection off for the management tier — a
57
+ * `cap`-bearing revoke is the tier working as designed, not a forgery. Matched by prefix against the
58
+ * fold's exported reasons, the contract those constants exist for.
59
+ */
60
+ export declare function needsCapabilityVerification(result: FoldResult): boolean;
61
+ /**
62
+ * Pick the authoritative branch. Two rules, in order:
63
+ *
64
+ * 1. The highest folded generation wins outright. Only a reset raises the generation, and only the
65
+ * committed recovery key can author a reset — the root's override, unreachable by a thief.
66
+ * 2. Within that generation, branches are compared **at the point they diverge**, not at their heads:
67
+ * a rotate signed by the pre-committed next keys supersedes the current-key event it forked from
68
+ * and everything that branch accumulated. Two diverging events with no rotate to settle them are
69
+ * duplicity, surfaced rather than merged.
70
+ *
71
+ * Comparing heads instead would make branch *length* decisive below the top generation — the one
72
+ * resource a thief has in abundance (a stolen current key cannot rotate but can sign unlimited
73
+ * revokes). A longer branch still wins when it is the same history carried further, which the
74
+ * divergence walk sees as a prefix.
75
+ *
76
+ * `superseded` counts the state-advancing events discarded from losing branches (what a cache must
77
+ * invalidate — folded state is not append-only under supersession); skipped events are not counted.
78
+ * Branches that do not fold are filtered out before comparison, so a thief who cannot produce a valid
79
+ * event cannot create a duplicity report.
80
+ *
81
+ * This form folds synchronously and cannot check a capability-authorised revoke: such a branch is
82
+ * counted in {@link ResolveResult.unverified} and left out, and only a call where *every* branch is
83
+ * one answers `failure: 'needs-capability-verification'`. See {@link ResolveResult.unverified} for
84
+ * why the count is reported rather than fatal; the remedy is {@link resolveBranchesAsync} with a
85
+ * verifier.
86
+ */
87
+ export declare function resolveBranches(did: string, branches: Array<Array<SignedEvent>>): ResolveResult;
88
+ /**
89
+ * Async counterpart of {@link resolveBranches}, and the form the management tier needs — the sync
90
+ * fold fails closed on a capability-authorised revoke, so resolving through it would filter every
91
+ * branch of such a log away as invalid and report "no valid history" for a healthy profile.
92
+ *
93
+ * Branches are folded one at a time: `verifyCapability` is caller-supplied code that may do I/O, and
94
+ * branch counts in a duplicity report are small, so there is nothing to win against calling a
95
+ * caller's verifier concurrently.
96
+ */
97
+ export declare function resolveBranchesAsync(did: string, branches: Array<Array<SignedEvent>>, options?: FoldOptions): Promise<ResolveResult>;