@kubun/credential 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/errors.js ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Failures a caller acts on differently.
3
+ *
4
+ * The distinctions are not cosmetic. "This device cannot supply the factor" is a
5
+ * prompt-the-user or use-another-device outcome; "the descriptors were tampered
6
+ * with" is a security event; "wrong secret" is a retry. Collapsing them into one
7
+ * decrypt error is what makes a tampered wrapping indistinguishable from a
8
+ * mistyped PIN.
9
+ *
10
+ * `code` is required rather than defaulted: a subclass that inherited a generic
11
+ * code would compile, ship, and reach a client as an untyped failure, which is
12
+ * the one outcome this hierarchy exists to prevent.
13
+ */ export class CredentialError extends Error {
14
+ #code;
15
+ constructor(message, options){
16
+ super(message, options);
17
+ this.name = 'CredentialError';
18
+ this.#code = options.code;
19
+ }
20
+ get code() {
21
+ return this.#code;
22
+ }
23
+ }
24
+ /** A wrapping needs a factor kind this device cannot supply. */ export class NoAvailableFactor extends CredentialError {
25
+ #kind;
26
+ constructor(kind){
27
+ super(`No available credential factor of kind "${kind}" on this device`, {
28
+ code: 'CREDENTIAL_FACTOR_UNAVAILABLE'
29
+ });
30
+ this.name = 'NoAvailableFactor';
31
+ this.#kind = kind;
32
+ }
33
+ get kind() {
34
+ return this.#kind;
35
+ }
36
+ }
37
+ /**
38
+ * A factor list a wrapping may not be built from.
39
+ *
40
+ * Raised at creation rather than at first unwrap: an unopenable or ambiguous
41
+ * wrapping that only fails on read is a key nobody notices is broken until they
42
+ * need it.
43
+ */ export class InvalidCredentialFactors extends CredentialError {
44
+ constructor(reason){
45
+ super(`Invalid credential factor list: ${reason}`, {
46
+ code: 'CREDENTIAL_FACTORS_INVALID'
47
+ });
48
+ this.name = 'InvalidCredentialFactors';
49
+ }
50
+ }
51
+ /** A key names a ciphersuite this build does not implement. */ export class UnsupportedCredentialSuite extends CredentialError {
52
+ #suite;
53
+ constructor(suite){
54
+ super(`Unsupported credential suite ${suite}`, {
55
+ code: 'CREDENTIAL_SUITE_UNSUPPORTED'
56
+ });
57
+ this.name = 'UnsupportedCredentialSuite';
58
+ this.#suite = suite;
59
+ }
60
+ get suite() {
61
+ return this.#suite;
62
+ }
63
+ }
64
+ /**
65
+ * A wrapping did not authenticate, and no user-supplied secret was involved.
66
+ *
67
+ * Raised for a wrapping whose factors are all `did`, where nothing the user
68
+ * typed could have been wrong. The causes left are an edited descriptor list,
69
+ * corrupted ciphertext, a wrapping copied onto another row — **and a wrapping
70
+ * addressed to someone else**. X25519 agreement never fails: a foreign recipient
71
+ * recovers different bytes rather than erroring, so the tag fails identically. A
72
+ * wrapping this device is not addressed to normally never reaches an unwrap at
73
+ * all, because `satisfiableFactors` filters it out first.
74
+ */ export class CredentialTampered extends CredentialError {
75
+ constructor(message, options){
76
+ super(message, {
77
+ ...options,
78
+ code: 'CREDENTIAL_TAMPERED'
79
+ });
80
+ this.name = 'CredentialTampered';
81
+ }
82
+ }
83
+ /** An entry belongs to a branch superseded by the key's resolved winner. */ export class CredentialSupersededBranch extends CredentialError {
84
+ #entryID;
85
+ #entryBranchID;
86
+ #winnerBranchID;
87
+ constructor(entryID, entryBranchID, winnerBranchID){
88
+ super(`Credential entry ${entryID} belongs to superseded branch ${entryBranchID}; winner is ${winnerBranchID}`, {
89
+ code: 'CREDENTIAL_SUPERSEDED_BRANCH'
90
+ });
91
+ this.name = 'CredentialSupersededBranch';
92
+ this.#entryID = entryID;
93
+ this.#entryBranchID = entryBranchID;
94
+ this.#winnerBranchID = winnerBranchID;
95
+ }
96
+ get entryID() {
97
+ return this.#entryID;
98
+ }
99
+ get entryBranchID() {
100
+ return this.#entryBranchID;
101
+ }
102
+ get winnerBranchID() {
103
+ return this.#winnerBranchID;
104
+ }
105
+ }
106
+ /**
107
+ * A wrapping carrying a user-supplied factor did not authenticate.
108
+ *
109
+ * Attribution to a wrong secret is policy, not a cryptographic fact: one tag
110
+ * failure is all that is observable, and a tampered descriptor produces the same
111
+ * one. This is the retry path; the `pin` throttle bounds repeated attempts.
112
+ */ export class CredentialSecretRejected extends CredentialError {
113
+ #kinds;
114
+ constructor(kinds, options){
115
+ const keyID = options?.keyID;
116
+ super(`Credential ${kinds.join(' and ')} rejected${keyID == null ? '' : ` for key ${keyID}`}. If the secret is known to be correct, the wrapping's factor descriptors may have been altered — the two are indistinguishable at this point.`, {
117
+ ...options,
118
+ code: options?.code ?? 'CREDENTIAL_SECRET_REJECTED'
119
+ });
120
+ this.name = 'CredentialSecretRejected';
121
+ this.#kinds = kinds;
122
+ }
123
+ /** The user-supplied factor kinds in the wrapping, any of which could be wrong. */ get kinds() {
124
+ return this.#kinds;
125
+ }
126
+ }
127
+ /**
128
+ * A PIN attempt was wrong, or refused because too many already were.
129
+ *
130
+ * Raised by the manager rather than the suite: the counter is per-key and the
131
+ * suite holds none. A refusal is issued before the KDF runs, so a throttled
132
+ * attempt costs the attacker time and the defender nothing.
133
+ */ export class CredentialPinRejected extends CredentialSecretRejected {
134
+ #attemptsRemaining;
135
+ #retryAfter;
136
+ constructor(keyID, options){
137
+ super([
138
+ 'pin'
139
+ ], {
140
+ cause: options.cause,
141
+ keyID,
142
+ code: 'CREDENTIAL_PIN_REJECTED'
143
+ });
144
+ this.name = 'CredentialPinRejected';
145
+ this.#attemptsRemaining = options.attemptsRemaining;
146
+ this.#retryAfter = options.retryAfter;
147
+ }
148
+ /** Attempts left before a delay begins. */ get attemptsRemaining() {
149
+ return this.#attemptsRemaining;
150
+ }
151
+ /** Milliseconds until another attempt is accepted. Zero means now. */ get retryAfter() {
152
+ return this.#retryAfter;
153
+ }
154
+ }
155
+ /**
156
+ * This identity holds no authority to do that to that key.
157
+ *
158
+ * Named rather than collapsed into {@link CredentialNotFound}, which is what a
159
+ * verb with no ownership check reports once it goes looking for a wrapping it
160
+ * was never entitled to touch. "There is no such key here" and "that key is not
161
+ * yours to administer" want opposite things from a caller, and only one of them
162
+ * is worth retrying on another device.
163
+ */ export class CredentialNotAuthorized extends CredentialError {
164
+ #authorization;
165
+ constructor(authorization, keyID){
166
+ super(`Not authorized to ${authorization} credential key ${keyID}`, {
167
+ code: 'CREDENTIAL_NOT_AUTHORIZED'
168
+ });
169
+ this.name = 'CredentialNotAuthorized';
170
+ this.#authorization = authorization;
171
+ }
172
+ get authorization() {
173
+ return this.#authorization;
174
+ }
175
+ }
176
+ /** No key, wrapping or entry under that id. */ export class CredentialNotFound extends CredentialError {
177
+ constructor(what, id){
178
+ super(`${what} not found: ${id}`, {
179
+ code: 'CREDENTIAL_NOT_FOUND'
180
+ });
181
+ this.name = 'CredentialNotFound';
182
+ }
183
+ }
184
+ /**
185
+ * More than one wrapping on a key is addressed to this device.
186
+ *
187
+ * Thrown rather than resolved by picking: re-wrapping one of them would leave
188
+ * the other un-gated and look like success.
189
+ */ export class AmbiguousCredentialWrapping extends CredentialError {
190
+ constructor(keyID, count){
191
+ super(`Credential key ${keyID} has ${count} wrappings addressed to this device; which one to re-wrap is ambiguous`, {
192
+ code: 'CREDENTIAL_WRAPPING_AMBIGUOUS'
193
+ });
194
+ this.name = 'AmbiguousCredentialWrapping';
195
+ }
196
+ }
197
+ /**
198
+ * The key is closed, and a factor this device can supply would open it.
199
+ *
200
+ * Distinct from {@link CredentialUnopenable} because the two need opposite
201
+ * responses: this one is a prompt, that one is "use another device". Collapsing
202
+ * them makes the UI ask for a PIN that will never work.
203
+ */ export class CredentialKeyLocked extends CredentialError {
204
+ #keyID;
205
+ constructor(keyID, kinds){
206
+ super(`Credential key ${keyID} is locked; unlock it with: ${kinds.join(', ')}`, {
207
+ code: 'CREDENTIAL_KEY_LOCKED'
208
+ });
209
+ this.name = 'CredentialKeyLocked';
210
+ this.#keyID = keyID;
211
+ }
212
+ get keyID() {
213
+ return this.#keyID;
214
+ }
215
+ }
216
+ /**
217
+ * An entry was written under a key version the key has moved past.
218
+ *
219
+ * Rotation re-encrypts every entry, so this is a rotation that crashed partway.
220
+ * Loud by design: falling back to the older version would read a value the
221
+ * rotation was meant to retire, and silently.
222
+ */ export class CredentialKeyVersionMismatch extends CredentialError {
223
+ constructor(entryID, entryVersion, keyVersion){
224
+ super(`Credential entry ${entryID} is at key version ${entryVersion} but its key is at ${keyVersion}: a rotation did not complete`, {
225
+ code: 'CREDENTIAL_KEY_VERSION_MISMATCH'
226
+ });
227
+ this.name = 'CredentialKeyVersionMismatch';
228
+ }
229
+ }
230
+ /**
231
+ * Every wrapping for the key was tried and none opened.
232
+ *
233
+ * Reported rather than skipped: a key whose only wrapping uses an unimplemented
234
+ * suite, or whose factors this device lacks, is a state the caller must be able
235
+ * to show. Silently returning "no entries" would read as an empty vault.
236
+ */ export class CredentialUnopenable extends CredentialError {
237
+ #keyID;
238
+ constructor(keyID, reason){
239
+ super(`Credential key ${keyID} cannot be opened on this device: ${reason}`, {
240
+ code: 'CREDENTIAL_UNOPENABLE'
241
+ });
242
+ this.name = 'CredentialUnopenable';
243
+ this.#keyID = keyID;
244
+ }
245
+ get keyID() {
246
+ return this.#keyID;
247
+ }
248
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ export type { CredentialAuthority, CredentialAuthorityParams, CredentialAuthorization, } from './authority.js';
2
+ export { createDeviceAuthority } from './authority.js';
3
+ export type { BuildEntryParams, BuildKeyParams, BuiltEntry, BuiltKey, BuiltWrapping, CredentialKeyMaterial, } from './builder.js';
4
+ export { buildCredentialEntry, buildCredentialKey } from './builder.js';
5
+ export type { CredentialEntryDigestInput, CredentialKeyDigestInput, CredentialWrappingDigestInput, } from './digest.js';
6
+ export { credentialEntriesDigest, credentialEntryDigest, credentialKeyDigest, credentialWrappingDigest, digestParts, } from './digest.js';
7
+ export type { CredentialErrorCode, CredentialErrorOptions } from './errors.js';
8
+ export { AmbiguousCredentialWrapping, CredentialError, CredentialKeyLocked, CredentialKeyVersionMismatch, CredentialNotAuthorized, CredentialNotFound, CredentialPinRejected, CredentialSecretRejected, CredentialSupersededBranch, CredentialTampered, CredentialUnopenable, InvalidCredentialFactors, NoAvailableFactor, UnsupportedCredentialSuite, } from './errors.js';
9
+ export type { CreateCredentialManagerParams, CreateKeyParams, CredentialGrantResult, CredentialKeyStatus, CredentialManager, CredentialUnopenableReason, FactorChangeParams, GrantKeyParams, ReKeyEntryParams, RotateKeyParams, } from './manager.js';
10
+ export { createCredentialManager } from './manager.js';
11
+ export type { CredentialOpClaim, CredentialOpSigner, CredentialOpSubject, CredentialPutSubject, CredentialRowSubject, VerifiedCredentialOp, } from './op.js';
12
+ export { compareBranchIDs, createCredentialOpSigner, credentialOpMatchesPut, credentialOpMatchesRow, credentialPutDigest, credentialRowDigest, selectWinningBranch, verifyCredentialOp, } from './op.js';
13
+ export type { PinThrottle, PinThrottleParams } from './pin-throttle.js';
14
+ export { createPinThrottle, DEFAULT_BASE_DELAY, DEFAULT_FREE_ATTEMPTS, DEFAULT_MAX_DELAY, } from './pin-throttle.js';
15
+ export type { CredentialEntryProvenance } from './provenance.js';
16
+ export { entryProvenanceFields } from './provenance.js';
17
+ export type { Argon2Cost, CredentialSuite, FactorInput, FactorInputs, SecretFactorKind, } from './suite.js';
18
+ export { DEFAULT_ARGON2_COST, getSuite, isSupportedSuite, SUITE_KUBUN_CRED_1 } from './suite.js';
19
+ export { CREDENTIAL_ADMINISTER_ACTION, credentialUserResource } from './vocabulary.js';
package/lib/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { createDeviceAuthority } from './authority.js';
2
+ export { buildCredentialEntry, buildCredentialKey } from './builder.js';
3
+ export { credentialEntriesDigest, credentialEntryDigest, credentialKeyDigest, credentialWrappingDigest, digestParts } from './digest.js';
4
+ export { AmbiguousCredentialWrapping, CredentialError, CredentialKeyLocked, CredentialKeyVersionMismatch, CredentialNotAuthorized, CredentialNotFound, CredentialPinRejected, CredentialSecretRejected, CredentialSupersededBranch, CredentialTampered, CredentialUnopenable, InvalidCredentialFactors, NoAvailableFactor, UnsupportedCredentialSuite } from './errors.js';
5
+ export { createCredentialManager } from './manager.js';
6
+ export { compareBranchIDs, createCredentialOpSigner, credentialOpMatchesPut, credentialOpMatchesRow, credentialPutDigest, credentialRowDigest, selectWinningBranch, verifyCredentialOp } from './op.js';
7
+ export { createPinThrottle, DEFAULT_BASE_DELAY, DEFAULT_FREE_ATTEMPTS, DEFAULT_MAX_DELAY } from './pin-throttle.js';
8
+ export { entryProvenanceFields } from './provenance.js';
9
+ export { DEFAULT_ARGON2_COST, getSuite, isSupportedSuite, SUITE_KUBUN_CRED_1 } from './suite.js';
10
+ export { CREDENTIAL_ADMINISTER_ACTION, credentialUserResource } from './vocabulary.js';
@@ -0,0 +1,34 @@
1
+ /** 15 minutes. Long enough for a working session, short enough to matter. */
2
+ export declare const DEFAULT_UNLOCK_TTL: number;
3
+ export type KeyCacheParams = {
4
+ /**
5
+ * How long an unlocked key stays in memory. Eviction is lazy — checked on
6
+ * read — because a timer would keep the host process alive and make every
7
+ * test wait on a clock it does not control.
8
+ */
9
+ ttl?: number;
10
+ /** Injectable so a TTL test does not sleep. */
11
+ now?: () => number;
12
+ };
13
+ /**
14
+ * The unlocked content keys, and the only place lock state exists.
15
+ *
16
+ * It is deliberately not a column. Locked, unlocked and unopenable are derived
17
+ * from what is here plus which wrappings exist; a persisted `unlocked` would be
18
+ * a belief that survives a restart it should not.
19
+ */
20
+ export type KeyCache = {
21
+ /**
22
+ * The cached key and the branch it resolved to, or null if absent, expired,
23
+ * or from a superseded version. A cache hit still surfaces the branch so a
24
+ * post-fork write joins the same branch the first open resolved.
25
+ */
26
+ get(keyID: string, keyVersion: number): {
27
+ contentKey: Uint8Array;
28
+ branchID: string;
29
+ } | null;
30
+ set(keyID: string, contentKey: Uint8Array, keyVersion: number, branchID: string): void;
31
+ evict(keyID: string): void;
32
+ evictAll(): void;
33
+ };
34
+ export declare function createKeyCache(params?: KeyCacheParams): KeyCache;
@@ -0,0 +1,55 @@
1
+ /** 15 minutes. Long enough for a working session, short enough to matter. */ export const DEFAULT_UNLOCK_TTL = 15 * 60 * 1000;
2
+ export function createKeyCache(params = {}) {
3
+ const ttl = params.ttl ?? DEFAULT_UNLOCK_TTL;
4
+ const now = params.now ?? Date.now;
5
+ const entries = new Map();
6
+ /**
7
+ * Overwrite the key material before dropping the entry.
8
+ *
9
+ * Dropping the entry alone leaves the bytes wherever they are until a
10
+ * collector reaches them. Zeroing is the part that can be guaranteed;
11
+ * JavaScript cannot promise no interpreter-level copy survives, and the design
12
+ * spec records that limit rather than implying hygiene it cannot deliver.
13
+ */ function evict(keyID) {
14
+ const cached = entries.get(keyID);
15
+ if (cached != null) {
16
+ cached.contentKey.fill(0);
17
+ entries.delete(keyID);
18
+ }
19
+ }
20
+ return {
21
+ get (keyID, keyVersion) {
22
+ const cached = entries.get(keyID);
23
+ if (cached == null) {
24
+ return null;
25
+ }
26
+ // A rotation invalidates the cache as surely as time does: serving the
27
+ // pre-rotation key would decrypt entries the rotation was retiring.
28
+ if (now() - cached.unlockedAt >= ttl || cached.keyVersion !== keyVersion) {
29
+ evict(keyID);
30
+ return null;
31
+ }
32
+ return {
33
+ contentKey: cached.contentKey,
34
+ branchID: cached.branchID
35
+ };
36
+ },
37
+ set (keyID, contentKey, keyVersion, branchID) {
38
+ evict(keyID);
39
+ entries.set(keyID, {
40
+ contentKey,
41
+ keyVersion,
42
+ branchID,
43
+ unlockedAt: now()
44
+ });
45
+ },
46
+ evict,
47
+ evictAll () {
48
+ for (const keyID of [
49
+ ...entries.keys()
50
+ ]){
51
+ evict(keyID);
52
+ }
53
+ }
54
+ };
55
+ }
@@ -0,0 +1,280 @@
1
+ import { type MethodRegistry, type SigningIdentity } from '@kokuin/token';
2
+ import { HLC } from '@kubun/hlc';
3
+ import type { CredentialStoreAPI } from '@kubun/store-credential';
4
+ import type { Runtime } from '@sozai/runtime';
5
+ import { type CredentialAuthority } from './authority.js';
6
+ import { type PinThrottleParams } from './pin-throttle.js';
7
+ import { type CredentialEntryProvenance } from './provenance.js';
8
+ import { type FactorInput, type FactorInputs, type SecretFactorKind } from './suite.js';
9
+ export type CreateCredentialManagerParams = {
10
+ store: CredentialStoreAPI;
11
+ /**
12
+ * This device's identity. NOT required to be a `KeyAgreementIdentity`: a device
13
+ * that cannot decrypt can still read metadata, and will be able to unwrap by
14
+ * passphrase once that factor exists. What it cannot do is take the `did`
15
+ * default in {@link CredentialManager.createKey}.
16
+ *
17
+ * It MUST be able to sign, which a reading identity need not: every row this
18
+ * manager writes carries a signed op, and a device that could write rows no
19
+ * peer can authenticate would be writing into a lane that has to drop them.
20
+ */
21
+ identity: SigningIdentity;
22
+ /**
23
+ * Host runtime, propagated rather than constructed here. This package runs on
24
+ * React Native and in browsers as well as on node, and content keys, IVs and
25
+ * factor secrets are the values that most need the platform's own CSPRNG
26
+ * rather than whatever `crypto` a bundler resolved.
27
+ */
28
+ runtime: Runtime;
29
+ /**
30
+ * The device's one clock, passed in rather than built here.
31
+ *
32
+ * Required, and deliberately not defaulted: a connector builds one manager per
33
+ * transaction, so a manager that constructed its own would start from
34
+ * `lastWallTime = 0` every time and two of them writing in the same
35
+ * millisecond would emit an identical `(wallTime, counter, nodeID)` — a
36
+ * colliding merkle leaf and an order tie with no tiebreak.
37
+ */
38
+ hlc: HLC;
39
+ /**
40
+ * Who this identity is relative to a key. Defaults to the device answer:
41
+ * it administers what it owns and nothing else.
42
+ *
43
+ * The same port the plugin's query context takes, deliberately — a device
44
+ * authority refuses a grant recipient gating someone else's key, and a
45
+ * delegation authority will let an agent gate its controller's, without
46
+ * either layer growing a predicate of its own.
47
+ */
48
+ authority?: CredentialAuthority;
49
+ /**
50
+ * How long an unlocked key stays in memory. Eviction is lazy — checked when
51
+ * the cache is read — because a timer would keep the host process alive and
52
+ * make every test wait on a clock it does not control.
53
+ */
54
+ unlockTTL?: number;
55
+ /**
56
+ * Failed-PIN backoff. Its clock comes from `now`, so a caller cannot give the
57
+ * throttle a different notion of time from the cache's.
58
+ */
59
+ pinThrottle?: Omit<PinThrottleParams, 'now'>;
60
+ /** Injectable so a TTL or backoff test does not sleep. */
61
+ now?: () => number;
62
+ };
63
+ export type CreateKeyParams = {
64
+ suite?: number;
65
+ /** One wrapping, whose factors are an AND. Shorthand for a single-entry `wrappings`. */
66
+ factors?: Array<FactorInput>;
67
+ /**
68
+ * Several wrappings — an OR of ANDs, one per way of opening the key.
69
+ *
70
+ * A key addressed to two recipients is not two keys and is not a grant: both
71
+ * wrappings exist from the first write, which is what a connector needs when
72
+ * the account owner and the server that runs sync are different DIDs.
73
+ */
74
+ wrappings?: Array<Array<FactorInput>>;
75
+ /**
76
+ * Who may later grant, rotate and revoke. Defaults to this device, which is
77
+ * the peer case. A server creating a key on a viewer's behalf passes the
78
+ * viewer, and stays a recipient rather than the owner.
79
+ */
80
+ ownerDID?: string;
81
+ /**
82
+ * Resolvers for recipient DIDs that carry no key material of their own
83
+ * (`did:kokuin:`). Passed per call rather than held on the manager, so a
84
+ * request-scoped resolver never outlives its request in the cached manager.
85
+ */
86
+ methods?: MethodRegistry;
87
+ };
88
+ export type RotateKeyParams = {
89
+ /**
90
+ * Migrate to a different ciphersuite. Rotation is the only path between
91
+ * suites: it already re-encrypts every entry and re-makes every wrapping,
92
+ * which is exactly what a suite change needs.
93
+ */
94
+ suite?: number;
95
+ /**
96
+ * Secrets for the surviving factors, keyed by kind — the same prompt
97
+ * `addFactor` needs, for the same reason: a descriptor holds a salt and a
98
+ * cost, never the secret, so a wrapping carrying one cannot be re-made at the
99
+ * new version without it.
100
+ *
101
+ * Only the rotating identity's own wrappings can carry a secret factor, since
102
+ * `addFactor` refuses one on a key it does not administer — so the prompt is
103
+ * always one the caller is present for.
104
+ */
105
+ inputs?: FactorInputs;
106
+ /** Resolvers for recipient DIDs that need one (`did:kokuin:`), for re-made `did` wrappings. */
107
+ methods?: MethodRegistry;
108
+ /**
109
+ * Overrides the constructed authority for this one call, so a provider-cached
110
+ * manager holds no request state. No production caller feeds one yet — the
111
+ * group-grant wiring that will is a follow-up.
112
+ */
113
+ authority?: CredentialAuthority;
114
+ };
115
+ /** Optionals shared by the factor-editing verbs `addFactor` and `removeFactor`. */
116
+ export type FactorChangeParams = {
117
+ /**
118
+ * Secrets for the factors that SURVIVE the change, keyed by kind — a
119
+ * descriptor holds a salt, never a secret, so the surviving wrapping's KEK
120
+ * cannot be re-derived without them.
121
+ */
122
+ inputs?: FactorInputs;
123
+ /** Resolvers for recipient DIDs that need one (`did:kokuin:`), for re-made `did` wrappings. */
124
+ methods?: MethodRegistry;
125
+ /** Per-call authority overriding the constructed default. See `RotateKeyParams.authority`. */
126
+ authority?: CredentialAuthority;
127
+ };
128
+ /** See `grant`. */
129
+ export type GrantKeyParams = {
130
+ /** Resolvers for recipient DIDs that carry no key material of their own (`did:kokuin:`). */
131
+ methods?: MethodRegistry;
132
+ /** Per-call authority overriding the constructed default. See `RotateKeyParams.authority`. */
133
+ authority?: CredentialAuthority;
134
+ };
135
+ /** See `reKeyEntry`. */
136
+ export type ReKeyEntryParams = {
137
+ /** Per-call authority overriding the constructed default. See `RotateKeyParams.authority`. */
138
+ authority?: CredentialAuthority;
139
+ };
140
+ /**
141
+ * Why a key cannot be opened here, as opposed to not being open yet.
142
+ *
143
+ * Carried so that `status` can answer rather than throw: the one call whose job
144
+ * is to report a key as unreadable must not fail on the key that is unreadable.
145
+ */
146
+ export type CredentialUnopenableReason =
147
+ /** A newer peer's ciphersuite. A different build could open it; this one cannot. */
148
+ 'unsupported-suite'
149
+ /** No wrapping at the key's current version — a rotation that did not finish. */
150
+ | 'no-wrapping'
151
+ /** Every wrapping belongs to another recipient. Another device can open it. */
152
+ | 'not-addressed';
153
+ /**
154
+ * Derived, never stored. A persisted `unlocked` column would be a belief that
155
+ * survives a restart it should not.
156
+ *
157
+ * `available` is separated from `unlocked` because they differ in what the
158
+ * caller must do, which is nothing, and in what the machine must do, which is
159
+ * one unwrap. A `did`-only key is `available` until first read, and a TTL
160
+ * eviction returns it there rather than to `locked` — no prompt ever becomes
161
+ * necessary.
162
+ */
163
+ export type CredentialKeyStatus = {
164
+ state: 'unlocked';
165
+ } | {
166
+ state: 'available';
167
+ } | {
168
+ state: 'locked';
169
+ /**
170
+ * One entry per wrapping that could be opened here: an OR of ANDs. Nested
171
+ * because flattening loses the difference between a key openable by a pin
172
+ * OR a passphrase and one demanding both, and those are different prompts.
173
+ * `did` factors are absent — this device supplies them without asking.
174
+ */
175
+ unlockOptions: Array<Array<SecretFactorKind>>;
176
+ } | {
177
+ state: 'unopenable';
178
+ reason: CredentialUnopenableReason;
179
+ };
180
+ export type CredentialGrantResult = {
181
+ /** The key version the recipient's wrapping sits at. */
182
+ version: number;
183
+ /**
184
+ * False when the recipient already held a wrapping and nothing was written.
185
+ * A caller broadcasting the grant emits its frame either way — that is what
186
+ * makes a repeat grant the retry path for a frame that never arrived.
187
+ */
188
+ created: boolean;
189
+ };
190
+ export type CredentialManager = {
191
+ /** Factor kinds this device can supply. Empty means it can hold no credentials. */
192
+ availableFactors(): Array<string>;
193
+ /**
194
+ * Open a key and hold its content key in memory.
195
+ *
196
+ * A `did`-only key needs no call: it opens on demand. This is for the ones
197
+ * carrying a factor only the user can supply.
198
+ */
199
+ unlock(keyID: string, inputs?: FactorInputs): Promise<void>;
200
+ /** Zero the cached content key. TTL eviction does the same thing. */
201
+ lock(keyID: string): void;
202
+ lockAll(): void;
203
+ status(keyID: string): Promise<CredentialKeyStatus>;
204
+ /**
205
+ * Add a factor to THIS device's wrapping, leaving every other recipient's
206
+ * alone. Adding a `pin` therefore gates this device and no other, which is the
207
+ * intended granularity.
208
+ *
209
+ * `params.inputs` must carry the secret for every factor that SURVIVES the
210
+ * change, not only the one being added: the new wrapping's key is derived from
211
+ * all of them, and a descriptor holds a salt, never a secret.
212
+ */
213
+ addFactor(keyID: string, factor: FactorInput, params?: FactorChangeParams): Promise<void>;
214
+ /** Remove a factor kind from this device's wrapping. See `addFactor` on `params.inputs`. */
215
+ removeFactor(keyID: string, kind: string, params?: FactorChangeParams): Promise<void>;
216
+ createKey(params?: CreateKeyParams): Promise<string>;
217
+ /**
218
+ * Retire this key's content key: mint a fresh one at `version + 1`,
219
+ * re-encrypt every entry under it and re-make every wrapping. Returns the new
220
+ * version, which a caller has to carry in the `credential:key-rotate` frame.
221
+ *
222
+ * `keyID` and every `entryID` survive, so documents referencing them do not
223
+ * move. A wrapping survives only if this device can re-make it: `did`-only
224
+ * ones re-make unattended, and one carrying a secret factor needs that secret
225
+ * back in `params.inputs`, because re-making a wrapping derives its KEK from
226
+ * every surviving factor and a descriptor holds a salt, never a secret.
227
+ *
228
+ * NOT atomic. This manager holds a resolved store, not a `StoreProvider`, so
229
+ * it has no transaction to open — a caller wanting all-or-nothing has to build
230
+ * the manager inside one.
231
+ */
232
+ rotate(keyID: string, params?: RotateKeyParams): Promise<number>;
233
+ /**
234
+ * Address this key to one more recipient: a `did` wrapping over the SAME
235
+ * content key at the key's current version. Every entry already written stays
236
+ * as it is — the recipient opens the key, not a copy of it — and no entry
237
+ * plaintext is touched. Returns the version the wrapping was made at, which a
238
+ * caller has to carry in the `credential:key-grant` frame.
239
+ *
240
+ * A recipient that already holds a wrapping is a no-op, so the version alone
241
+ * cannot tell a fresh grant from a re-grant — `created` is what distinguishes
242
+ * them.
243
+ *
244
+ * `recipientWrappableDID` must be RESOLVABLE, not merely a name: a
245
+ * `did:peer:4` short form carries no document and so no agreement key. Working
246
+ * out which string that is for a given party is a group question, and this
247
+ * package does not know what a group is — so it takes the answer rather than
248
+ * asking.
249
+ *
250
+ * `administer`, and the device must be able to open the key: a locked one
251
+ * fails before the first write rather than persisting half a grant.
252
+ */
253
+ grant(keyID: string, recipientWrappableDID: string, params?: GrantKeyParams): Promise<CredentialGrantResult>;
254
+ listKeys(): Promise<Array<string>>;
255
+ /** How many wrappings a key carries — one per way of opening it. */
256
+ countWrappings(keyID: string): Promise<number>;
257
+ deleteKey(keyID: string): Promise<void>;
258
+ putEntry(keyID: string, plaintext: Uint8Array): Promise<string>;
259
+ updateEntry(entryID: string, plaintext: Uint8Array): Promise<void>;
260
+ readEntry(entryID: string): Promise<Uint8Array>;
261
+ getEntryProvenance(entryID: string): Promise<CredentialEntryProvenance>;
262
+ listEntries(keyID: string): Promise<Array<string>>;
263
+ deleteEntry(entryID: string): Promise<void>;
264
+ /**
265
+ * Move an entry onto another key: decrypt under the one it sits on, re-encrypt
266
+ * under the target's current version, keep the entry id.
267
+ *
268
+ * The key is the sharing unit, so sharing one secret means moving it onto a
269
+ * shared key rather than sharing the key it already sits under, which would
270
+ * share every other entry beneath that one too. The entry id survives because
271
+ * it never embedded a key id — every document referencing it stays valid. The
272
+ * ciphertext does not survive: it is bound to its key, so this is a genuine
273
+ * re-encryption and the target must be open, not merely named.
274
+ *
275
+ * `administer` on BOTH keys. Writing under the target changes what the target
276
+ * holds, and taking an entry out of the source changes what the source holds.
277
+ */
278
+ reKeyEntry(entryID: string, toKeyID: string, params?: ReKeyEntryParams): Promise<void>;
279
+ };
280
+ export declare function createCredentialManager(params: CreateCredentialManagerParams): CredentialManager;