@noy-db/on-webauthn 0.1.0-pre.10

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,267 @@
1
+ import { UnlockedKeyring, SlotRewrapContext, EnrollAuthenticatorOptions } from '@noy-db/hub';
2
+ export { ValidationError } from '@noy-db/hub';
3
+
4
+ /**
5
+ * @noy-db/on-webauthn —
6
+ *
7
+ * Hardware-key keyring for noy-db using the WebAuthn API.
8
+ *
9
+ * Covers every form factor:
10
+ * - Platform authenticators: Touch ID, Face ID, Windows Hello, Android biometric
11
+ * - Roaming authenticators: YubiKey (5C NFC, Bio), SoloKey, Titan, any FIDO2 key
12
+ * - Passkey-capable platform authenticators: iCloud Keychain, Google Password Manager
13
+ *
14
+ * Key derivation model
15
+ * ────────────────────
16
+ * This package uses the **PRF (Pseudo-Random Function) extension** when
17
+ * available to derive a deterministic wrapping key from the WebAuthn
18
+ * credential. The PRF output is consistent across assertions on the same
19
+ * device/credential, enabling unlock-without-passphrase while keeping the
20
+ * derived key bound to the physical authenticator.
21
+ *
22
+ * When PRF is not supported by the authenticator (common on older hardware),
23
+ * the package falls back to HKDF-SHA256 over the credential's `rawId` —
24
+ * the same approach as the pre-existing `@noy-db/core` biometric module.
25
+ *
26
+ * The derived key is NEVER persisted. It exists only in memory during the
27
+ * unlock operation. What IS persisted (in the noy-db adapter, not in browser
28
+ * storage) is the wrapped KEK: `encrypt(KEK, derivedKey)`.
29
+ *
30
+ * BE-flag guards
31
+ * ──────────────
32
+ * The backup-eligibility (BE) flag in a WebAuthn authenticator data signals
33
+ * that the credential is (or can be) synced across devices — e.g. stored in
34
+ * iCloud Keychain. For single-device security policies (air-gapped USB sticks,
35
+ * high-security terminals), this is a threat: the credential is available on
36
+ * any device where the user's iCloud account is signed in.
37
+ *
38
+ * The `requireSingleDevice: true` option rejects credentials with the BE flag
39
+ * set during enrollment. Existing enrollments are checked at assertion time —
40
+ * if the authenticator data shows BE=1 but `requireSingleDevice` was set at
41
+ * enrollment, the assertion throws `WebAuthnMultiDeviceError`.
42
+ *
43
+ * Enrollment flow
44
+ * ───────────────
45
+ * 1. User is already authenticated (passphrase or existing session).
46
+ * 2. Call `enrollWebAuthn(keyring, options)`.
47
+ * 3. WebAuthn credential is created; PRF or rawId-derived key wraps the KEK.
48
+ * 4. Returns a `WebAuthnEnrollment` — persist this to the noy-db adapter
49
+ * via `saveEnrollment()`, or store it yourself in any encrypted collection.
50
+ *
51
+ * Unlock flow
52
+ * ───────────
53
+ * 1. Load the `WebAuthnEnrollment` via `loadEnrollment()`.
54
+ * 2. Call `unlockWebAuthn(enrollment, keyring)` — triggers the WebAuthn
55
+ * assertion prompt.
56
+ * 3. On success, returns the unwrapped `CryptoKey` (the KEK) — use it to
57
+ * re-hydrate the session via `createSession()`.
58
+ */
59
+
60
+ /**
61
+ * Thrown when the WebAuthn API is not available in the current environment.
62
+ *
63
+ * Check `isWebAuthnAvailable()` before calling `enrollWebAuthn()` or
64
+ * `unlockWebAuthn()` and show a fallback UI (passphrase entry) if this
65
+ * returns false. Common scenarios: Node.js environments, older browsers,
66
+ * non-HTTPS origins (WebAuthn requires a Secure Context).
67
+ */
68
+ declare class WebAuthnNotAvailableError extends Error {
69
+ readonly code = "WEBAUTHN_NOT_AVAILABLE";
70
+ constructor();
71
+ }
72
+ /**
73
+ * Thrown when the user dismisses the WebAuthn prompt without completing it.
74
+ *
75
+ * The `op` field distinguishes enrollment cancellation (user chose not to
76
+ * enroll a hardware key) from assertion cancellation (user dismissed the
77
+ * unlock prompt). Treat this as a user-initiated action, not an error — show
78
+ * a "use passphrase instead" option rather than an error message.
79
+ */
80
+ declare class WebAuthnCancelledError extends Error {
81
+ readonly code = "WEBAUTHN_CANCELLED";
82
+ constructor(op: 'enrollment' | 'assertion');
83
+ }
84
+ /**
85
+ * Thrown when the authenticator has the backup-eligible (BE) flag set but
86
+ * the vault requires a single-device credential (`requireSingleDevice: true`).
87
+ *
88
+ * A BE credential is synced across devices (e.g. iCloud Keychain, Google
89
+ * Password Manager), which violates the single-device security model. The
90
+ * user must enroll a hardware security key (YubiKey, Titan, SoloKey) instead.
91
+ */
92
+ declare class WebAuthnMultiDeviceError extends Error {
93
+ readonly code = "WEBAUTHN_MULTI_DEVICE";
94
+ constructor();
95
+ }
96
+ /**
97
+ * Thrown (as a non-fatal warning, caught internally) when the PRF extension
98
+ * is not supported by the authenticator.
99
+ *
100
+ * NOYDB prefers PRF for key derivation because it produces a
101
+ * credential-bound output that is deterministic and not extractable from
102
+ * the authenticator. When PRF is unavailable, enrollment falls back to
103
+ * HKDF over the credential's `rawId` — weaker binding, but still functional.
104
+ * This error is caught at enrollment time; callers only see it if they
105
+ * explicitly opt into strict PRF-only mode.
106
+ */
107
+ declare class WebAuthnPRFUnavailableError extends Error {
108
+ readonly code = "WEBAUTHN_PRF_UNAVAILABLE";
109
+ constructor();
110
+ }
111
+ /**
112
+ * A persisted WebAuthn enrollment record. Store this in a noy-db
113
+ * collection (encrypted like any other record) or return it from
114
+ * `saveEnrollment()` / `loadEnrollment()` helpers.
115
+ */
116
+ interface WebAuthnEnrollment {
117
+ /** Enrollment format version. */
118
+ readonly _noydb_webauthn: 1;
119
+ /** The vault this enrollment was created for. */
120
+ readonly vault: string;
121
+ /** The user ID this enrollment belongs to. */
122
+ readonly userId: string;
123
+ /** WebAuthn credential ID (base64). Use for allowCredentials in assertions. */
124
+ readonly credentialId: string;
125
+ /** Whether PRF was used for key derivation (vs rawId HKDF fallback). */
126
+ readonly prfUsed: boolean;
127
+ /** Whether the BE (backup-eligibility) flag was present at enrollment time. */
128
+ readonly beFlag: boolean;
129
+ /** Whether single-device was required at enrollment time. */
130
+ readonly requireSingleDevice: boolean;
131
+ /** The wrapped KEK: encrypt(exportedDekMap, derivedKey). Base64. */
132
+ readonly wrappedPayload: string;
133
+ /** IV used for the wrapping. Base64. */
134
+ readonly wrapIv: string;
135
+ /** ISO timestamp of enrollment. */
136
+ readonly enrolledAt: string;
137
+ }
138
+ /** Options for `enrollWebAuthn()`. */
139
+ interface WebAuthnEnrollOptions {
140
+ /**
141
+ * Relying party ID and name for the WebAuthn credential.
142
+ * Defaults to `{ id: window.location.hostname, name: 'NOYDB' }`.
143
+ */
144
+ rp?: {
145
+ id?: string;
146
+ name: string;
147
+ };
148
+ /**
149
+ * If `true`, refuse to enroll credentials with the BE flag set
150
+ * (multi-device / syncable passkeys). Defaults to `false`.
151
+ *
152
+ * Set to `true` for high-security deployments where the credential
153
+ * must be bound to a single physical device (YubiKey, Titan, etc.).
154
+ */
155
+ requireSingleDevice?: boolean;
156
+ /**
157
+ * WebAuthn timeout in milliseconds. Default: 60_000.
158
+ */
159
+ timeout?: number;
160
+ /**
161
+ * If `true`, prefer a cross-platform authenticator (roaming security key).
162
+ * If `false`, prefer a platform authenticator (Touch ID, Face ID).
163
+ * If undefined, let the browser choose.
164
+ */
165
+ preferCrossPlatform?: boolean;
166
+ }
167
+ /** Options for `unlockWebAuthn()`. */
168
+ interface WebAuthnUnlockOptions {
169
+ /** WebAuthn timeout in milliseconds. Default: 60_000. */
170
+ timeout?: number;
171
+ }
172
+ /**
173
+ * Returns `true` if WebAuthn is available and can be used for enrollment or unlock.
174
+ *
175
+ * Checks for `navigator.credentials`, `window.PublicKeyCredential`, and a
176
+ * Secure Context (`window.isSecureContext`). Call this before rendering the
177
+ * "Register hardware key" button to avoid showing options that will fail.
178
+ */
179
+ declare function isWebAuthnAvailable(): boolean;
180
+ /**
181
+ * Enroll a WebAuthn credential for the given keyring.
182
+ *
183
+ * The caller must already have an unlocked keyring (from passphrase auth or
184
+ * an existing session). The WebAuthn credential creation prompt is triggered
185
+ * by this call.
186
+ *
187
+ * Returns a `WebAuthnEnrollment` that should be persisted — typically via
188
+ * `saveEnrollment()` into a noy-db collection.
189
+ *
190
+ * @throws `WebAuthnNotAvailableError` if the environment doesn't support WebAuthn.
191
+ * @throws `WebAuthnCancelledError` if the user cancels the credential creation.
192
+ * @throws `WebAuthnMultiDeviceError` if `requireSingleDevice` is true and the
193
+ * authenticator returned a credential with the BE flag set.
194
+ */
195
+ declare function enrollWebAuthn(keyring: UnlockedKeyring, vault: string, options?: WebAuthnEnrollOptions): Promise<WebAuthnEnrollment>;
196
+ /**
197
+ * Unlock a vault using a previously enrolled WebAuthn credential.
198
+ *
199
+ * Triggers the WebAuthn assertion prompt. On success, decrypts the keyring
200
+ * payload from the enrollment record and returns an `UnlockedKeyring`.
201
+ *
202
+ * The returned keyring has the same DEKs as at enrollment time. If DEKs
203
+ * have been rotated since enrollment, this will return stale DEKs — the
204
+ * caller should detect decryption failures and prompt for re-enrollment.
205
+ *
206
+ * @throws `WebAuthnNotAvailableError` if the environment doesn't support WebAuthn.
207
+ * @throws `WebAuthnCancelledError` if the user cancels the assertion.
208
+ * @throws `WebAuthnMultiDeviceError` if `requireSingleDevice` was set at
209
+ * enrollment and the authenticator data now shows BE=1.
210
+ * @throws `ValidationError` if decryption of the keyring payload fails.
211
+ */
212
+ declare function unlockWebAuthn(enrollment: WebAuthnEnrollment, options?: WebAuthnUnlockOptions): Promise<UnlockedKeyring>;
213
+ /**
214
+ * `SlotRewrapCeremony` for WebAuthn slots — used by hub's
215
+ * `rotatePassphrase({ slotCeremonies: { [slotId]: webAuthnSlotRewrapCeremony } })`
216
+ * to preserve a tier-2 WebAuthn enrollment across a tier-1 phrase
217
+ * rotation without requiring re-enrollment of the credential (#56).
218
+ *
219
+ * The credential itself is unaffected by phrase rotation — the
220
+ * wrapping key derived from PRF (or rawId fallback) is bound to the
221
+ * authenticator, not to the passphrase. What needs to change is the
222
+ * **wrapped payload**: the encrypted blob the slot's `wrapped_kek`
223
+ * field holds. After rotation, the old payload still has the old
224
+ * DEKs (now stale because rotatePassphrase rewrapped them under a
225
+ * fresh KEK); the new payload must hold the freshly rewrapped
226
+ * `ctx.newDeks`.
227
+ *
228
+ * Single ceremony, two operations:
229
+ * 1. Trigger one WebAuthn assertion to derive the wrapping key.
230
+ * 2. Decrypt the OLD `wrapped_kek` to extract identity fields
231
+ * (userId, displayName, role, permissions, salt) — these don't
232
+ * change on rotation, so they're carried verbatim into the new
233
+ * payload.
234
+ * 3. Encrypt the NEW payload (same identity + `ctx.newDeks`) under
235
+ * the same wrapping key, with a fresh IV.
236
+ * 4. Return `EnrollAuthenticatorOptions` preserving `oldSlot.id`
237
+ * and `method: 'webauthn'` (hub validates these to prevent
238
+ * slot-type swap mid-rotation).
239
+ *
240
+ * Niwat (consumer) shipped a workaround at #44 — detect dropped slots
241
+ * after rotate and offer "Re-enrol Touch ID" inline. This ceremony
242
+ * eliminates that step.
243
+ *
244
+ * Out of scope: tier-3 PIN. PIN state lives in `QuickUnlockStore`
245
+ * (RAM-only), not in `KeyringFile.authenticators[]`, so
246
+ * `slotCeremonies` doesn't apply. Clear PIN state on rotate; let
247
+ * the user set a fresh PIN via `db.enrollUnlock` immediately after.
248
+ *
249
+ * @throws {WebAuthnNotAvailableError} when the environment lacks WebAuthn.
250
+ * @throws {WebAuthnCancelledError} when the user dismisses the assertion.
251
+ * @throws {WebAuthnMultiDeviceError} when `meta.requireSingleDevice` was
252
+ * set at enrollment and the authenticator now reports BE=1.
253
+ * @throws {ValidationError} when `oldSlot.method !== 'webauthn'`,
254
+ * when required `meta` fields are missing, or when the old
255
+ * payload fails to decrypt (credential changed / payload
256
+ * tampered).
257
+ *
258
+ * @see #56 #29 — the ceremony plumbing this fills in.
259
+ */
260
+ declare function webAuthnSlotRewrapCeremony(ctx: SlotRewrapContext, options?: WebAuthnUnlockOptions): Promise<EnrollAuthenticatorOptions>;
261
+ /**
262
+ * Check whether a `WebAuthnEnrollment` record looks well-formed.
263
+ * Does not perform any cryptographic verification.
264
+ */
265
+ declare function isValidEnrollment(value: unknown): value is WebAuthnEnrollment;
266
+
267
+ export { WebAuthnCancelledError, type WebAuthnEnrollOptions, type WebAuthnEnrollment, WebAuthnMultiDeviceError, WebAuthnNotAvailableError, WebAuthnPRFUnavailableError, type WebAuthnUnlockOptions, enrollWebAuthn, isValidEnrollment, isWebAuthnAvailable, unlockWebAuthn, webAuthnSlotRewrapCeremony };
@@ -0,0 +1,267 @@
1
+ import { UnlockedKeyring, SlotRewrapContext, EnrollAuthenticatorOptions } from '@noy-db/hub';
2
+ export { ValidationError } from '@noy-db/hub';
3
+
4
+ /**
5
+ * @noy-db/on-webauthn —
6
+ *
7
+ * Hardware-key keyring for noy-db using the WebAuthn API.
8
+ *
9
+ * Covers every form factor:
10
+ * - Platform authenticators: Touch ID, Face ID, Windows Hello, Android biometric
11
+ * - Roaming authenticators: YubiKey (5C NFC, Bio), SoloKey, Titan, any FIDO2 key
12
+ * - Passkey-capable platform authenticators: iCloud Keychain, Google Password Manager
13
+ *
14
+ * Key derivation model
15
+ * ────────────────────
16
+ * This package uses the **PRF (Pseudo-Random Function) extension** when
17
+ * available to derive a deterministic wrapping key from the WebAuthn
18
+ * credential. The PRF output is consistent across assertions on the same
19
+ * device/credential, enabling unlock-without-passphrase while keeping the
20
+ * derived key bound to the physical authenticator.
21
+ *
22
+ * When PRF is not supported by the authenticator (common on older hardware),
23
+ * the package falls back to HKDF-SHA256 over the credential's `rawId` —
24
+ * the same approach as the pre-existing `@noy-db/core` biometric module.
25
+ *
26
+ * The derived key is NEVER persisted. It exists only in memory during the
27
+ * unlock operation. What IS persisted (in the noy-db adapter, not in browser
28
+ * storage) is the wrapped KEK: `encrypt(KEK, derivedKey)`.
29
+ *
30
+ * BE-flag guards
31
+ * ──────────────
32
+ * The backup-eligibility (BE) flag in a WebAuthn authenticator data signals
33
+ * that the credential is (or can be) synced across devices — e.g. stored in
34
+ * iCloud Keychain. For single-device security policies (air-gapped USB sticks,
35
+ * high-security terminals), this is a threat: the credential is available on
36
+ * any device where the user's iCloud account is signed in.
37
+ *
38
+ * The `requireSingleDevice: true` option rejects credentials with the BE flag
39
+ * set during enrollment. Existing enrollments are checked at assertion time —
40
+ * if the authenticator data shows BE=1 but `requireSingleDevice` was set at
41
+ * enrollment, the assertion throws `WebAuthnMultiDeviceError`.
42
+ *
43
+ * Enrollment flow
44
+ * ───────────────
45
+ * 1. User is already authenticated (passphrase or existing session).
46
+ * 2. Call `enrollWebAuthn(keyring, options)`.
47
+ * 3. WebAuthn credential is created; PRF or rawId-derived key wraps the KEK.
48
+ * 4. Returns a `WebAuthnEnrollment` — persist this to the noy-db adapter
49
+ * via `saveEnrollment()`, or store it yourself in any encrypted collection.
50
+ *
51
+ * Unlock flow
52
+ * ───────────
53
+ * 1. Load the `WebAuthnEnrollment` via `loadEnrollment()`.
54
+ * 2. Call `unlockWebAuthn(enrollment, keyring)` — triggers the WebAuthn
55
+ * assertion prompt.
56
+ * 3. On success, returns the unwrapped `CryptoKey` (the KEK) — use it to
57
+ * re-hydrate the session via `createSession()`.
58
+ */
59
+
60
+ /**
61
+ * Thrown when the WebAuthn API is not available in the current environment.
62
+ *
63
+ * Check `isWebAuthnAvailable()` before calling `enrollWebAuthn()` or
64
+ * `unlockWebAuthn()` and show a fallback UI (passphrase entry) if this
65
+ * returns false. Common scenarios: Node.js environments, older browsers,
66
+ * non-HTTPS origins (WebAuthn requires a Secure Context).
67
+ */
68
+ declare class WebAuthnNotAvailableError extends Error {
69
+ readonly code = "WEBAUTHN_NOT_AVAILABLE";
70
+ constructor();
71
+ }
72
+ /**
73
+ * Thrown when the user dismisses the WebAuthn prompt without completing it.
74
+ *
75
+ * The `op` field distinguishes enrollment cancellation (user chose not to
76
+ * enroll a hardware key) from assertion cancellation (user dismissed the
77
+ * unlock prompt). Treat this as a user-initiated action, not an error — show
78
+ * a "use passphrase instead" option rather than an error message.
79
+ */
80
+ declare class WebAuthnCancelledError extends Error {
81
+ readonly code = "WEBAUTHN_CANCELLED";
82
+ constructor(op: 'enrollment' | 'assertion');
83
+ }
84
+ /**
85
+ * Thrown when the authenticator has the backup-eligible (BE) flag set but
86
+ * the vault requires a single-device credential (`requireSingleDevice: true`).
87
+ *
88
+ * A BE credential is synced across devices (e.g. iCloud Keychain, Google
89
+ * Password Manager), which violates the single-device security model. The
90
+ * user must enroll a hardware security key (YubiKey, Titan, SoloKey) instead.
91
+ */
92
+ declare class WebAuthnMultiDeviceError extends Error {
93
+ readonly code = "WEBAUTHN_MULTI_DEVICE";
94
+ constructor();
95
+ }
96
+ /**
97
+ * Thrown (as a non-fatal warning, caught internally) when the PRF extension
98
+ * is not supported by the authenticator.
99
+ *
100
+ * NOYDB prefers PRF for key derivation because it produces a
101
+ * credential-bound output that is deterministic and not extractable from
102
+ * the authenticator. When PRF is unavailable, enrollment falls back to
103
+ * HKDF over the credential's `rawId` — weaker binding, but still functional.
104
+ * This error is caught at enrollment time; callers only see it if they
105
+ * explicitly opt into strict PRF-only mode.
106
+ */
107
+ declare class WebAuthnPRFUnavailableError extends Error {
108
+ readonly code = "WEBAUTHN_PRF_UNAVAILABLE";
109
+ constructor();
110
+ }
111
+ /**
112
+ * A persisted WebAuthn enrollment record. Store this in a noy-db
113
+ * collection (encrypted like any other record) or return it from
114
+ * `saveEnrollment()` / `loadEnrollment()` helpers.
115
+ */
116
+ interface WebAuthnEnrollment {
117
+ /** Enrollment format version. */
118
+ readonly _noydb_webauthn: 1;
119
+ /** The vault this enrollment was created for. */
120
+ readonly vault: string;
121
+ /** The user ID this enrollment belongs to. */
122
+ readonly userId: string;
123
+ /** WebAuthn credential ID (base64). Use for allowCredentials in assertions. */
124
+ readonly credentialId: string;
125
+ /** Whether PRF was used for key derivation (vs rawId HKDF fallback). */
126
+ readonly prfUsed: boolean;
127
+ /** Whether the BE (backup-eligibility) flag was present at enrollment time. */
128
+ readonly beFlag: boolean;
129
+ /** Whether single-device was required at enrollment time. */
130
+ readonly requireSingleDevice: boolean;
131
+ /** The wrapped KEK: encrypt(exportedDekMap, derivedKey). Base64. */
132
+ readonly wrappedPayload: string;
133
+ /** IV used for the wrapping. Base64. */
134
+ readonly wrapIv: string;
135
+ /** ISO timestamp of enrollment. */
136
+ readonly enrolledAt: string;
137
+ }
138
+ /** Options for `enrollWebAuthn()`. */
139
+ interface WebAuthnEnrollOptions {
140
+ /**
141
+ * Relying party ID and name for the WebAuthn credential.
142
+ * Defaults to `{ id: window.location.hostname, name: 'NOYDB' }`.
143
+ */
144
+ rp?: {
145
+ id?: string;
146
+ name: string;
147
+ };
148
+ /**
149
+ * If `true`, refuse to enroll credentials with the BE flag set
150
+ * (multi-device / syncable passkeys). Defaults to `false`.
151
+ *
152
+ * Set to `true` for high-security deployments where the credential
153
+ * must be bound to a single physical device (YubiKey, Titan, etc.).
154
+ */
155
+ requireSingleDevice?: boolean;
156
+ /**
157
+ * WebAuthn timeout in milliseconds. Default: 60_000.
158
+ */
159
+ timeout?: number;
160
+ /**
161
+ * If `true`, prefer a cross-platform authenticator (roaming security key).
162
+ * If `false`, prefer a platform authenticator (Touch ID, Face ID).
163
+ * If undefined, let the browser choose.
164
+ */
165
+ preferCrossPlatform?: boolean;
166
+ }
167
+ /** Options for `unlockWebAuthn()`. */
168
+ interface WebAuthnUnlockOptions {
169
+ /** WebAuthn timeout in milliseconds. Default: 60_000. */
170
+ timeout?: number;
171
+ }
172
+ /**
173
+ * Returns `true` if WebAuthn is available and can be used for enrollment or unlock.
174
+ *
175
+ * Checks for `navigator.credentials`, `window.PublicKeyCredential`, and a
176
+ * Secure Context (`window.isSecureContext`). Call this before rendering the
177
+ * "Register hardware key" button to avoid showing options that will fail.
178
+ */
179
+ declare function isWebAuthnAvailable(): boolean;
180
+ /**
181
+ * Enroll a WebAuthn credential for the given keyring.
182
+ *
183
+ * The caller must already have an unlocked keyring (from passphrase auth or
184
+ * an existing session). The WebAuthn credential creation prompt is triggered
185
+ * by this call.
186
+ *
187
+ * Returns a `WebAuthnEnrollment` that should be persisted — typically via
188
+ * `saveEnrollment()` into a noy-db collection.
189
+ *
190
+ * @throws `WebAuthnNotAvailableError` if the environment doesn't support WebAuthn.
191
+ * @throws `WebAuthnCancelledError` if the user cancels the credential creation.
192
+ * @throws `WebAuthnMultiDeviceError` if `requireSingleDevice` is true and the
193
+ * authenticator returned a credential with the BE flag set.
194
+ */
195
+ declare function enrollWebAuthn(keyring: UnlockedKeyring, vault: string, options?: WebAuthnEnrollOptions): Promise<WebAuthnEnrollment>;
196
+ /**
197
+ * Unlock a vault using a previously enrolled WebAuthn credential.
198
+ *
199
+ * Triggers the WebAuthn assertion prompt. On success, decrypts the keyring
200
+ * payload from the enrollment record and returns an `UnlockedKeyring`.
201
+ *
202
+ * The returned keyring has the same DEKs as at enrollment time. If DEKs
203
+ * have been rotated since enrollment, this will return stale DEKs — the
204
+ * caller should detect decryption failures and prompt for re-enrollment.
205
+ *
206
+ * @throws `WebAuthnNotAvailableError` if the environment doesn't support WebAuthn.
207
+ * @throws `WebAuthnCancelledError` if the user cancels the assertion.
208
+ * @throws `WebAuthnMultiDeviceError` if `requireSingleDevice` was set at
209
+ * enrollment and the authenticator data now shows BE=1.
210
+ * @throws `ValidationError` if decryption of the keyring payload fails.
211
+ */
212
+ declare function unlockWebAuthn(enrollment: WebAuthnEnrollment, options?: WebAuthnUnlockOptions): Promise<UnlockedKeyring>;
213
+ /**
214
+ * `SlotRewrapCeremony` for WebAuthn slots — used by hub's
215
+ * `rotatePassphrase({ slotCeremonies: { [slotId]: webAuthnSlotRewrapCeremony } })`
216
+ * to preserve a tier-2 WebAuthn enrollment across a tier-1 phrase
217
+ * rotation without requiring re-enrollment of the credential (#56).
218
+ *
219
+ * The credential itself is unaffected by phrase rotation — the
220
+ * wrapping key derived from PRF (or rawId fallback) is bound to the
221
+ * authenticator, not to the passphrase. What needs to change is the
222
+ * **wrapped payload**: the encrypted blob the slot's `wrapped_kek`
223
+ * field holds. After rotation, the old payload still has the old
224
+ * DEKs (now stale because rotatePassphrase rewrapped them under a
225
+ * fresh KEK); the new payload must hold the freshly rewrapped
226
+ * `ctx.newDeks`.
227
+ *
228
+ * Single ceremony, two operations:
229
+ * 1. Trigger one WebAuthn assertion to derive the wrapping key.
230
+ * 2. Decrypt the OLD `wrapped_kek` to extract identity fields
231
+ * (userId, displayName, role, permissions, salt) — these don't
232
+ * change on rotation, so they're carried verbatim into the new
233
+ * payload.
234
+ * 3. Encrypt the NEW payload (same identity + `ctx.newDeks`) under
235
+ * the same wrapping key, with a fresh IV.
236
+ * 4. Return `EnrollAuthenticatorOptions` preserving `oldSlot.id`
237
+ * and `method: 'webauthn'` (hub validates these to prevent
238
+ * slot-type swap mid-rotation).
239
+ *
240
+ * Niwat (consumer) shipped a workaround at #44 — detect dropped slots
241
+ * after rotate and offer "Re-enrol Touch ID" inline. This ceremony
242
+ * eliminates that step.
243
+ *
244
+ * Out of scope: tier-3 PIN. PIN state lives in `QuickUnlockStore`
245
+ * (RAM-only), not in `KeyringFile.authenticators[]`, so
246
+ * `slotCeremonies` doesn't apply. Clear PIN state on rotate; let
247
+ * the user set a fresh PIN via `db.enrollUnlock` immediately after.
248
+ *
249
+ * @throws {WebAuthnNotAvailableError} when the environment lacks WebAuthn.
250
+ * @throws {WebAuthnCancelledError} when the user dismisses the assertion.
251
+ * @throws {WebAuthnMultiDeviceError} when `meta.requireSingleDevice` was
252
+ * set at enrollment and the authenticator now reports BE=1.
253
+ * @throws {ValidationError} when `oldSlot.method !== 'webauthn'`,
254
+ * when required `meta` fields are missing, or when the old
255
+ * payload fails to decrypt (credential changed / payload
256
+ * tampered).
257
+ *
258
+ * @see #56 #29 — the ceremony plumbing this fills in.
259
+ */
260
+ declare function webAuthnSlotRewrapCeremony(ctx: SlotRewrapContext, options?: WebAuthnUnlockOptions): Promise<EnrollAuthenticatorOptions>;
261
+ /**
262
+ * Check whether a `WebAuthnEnrollment` record looks well-formed.
263
+ * Does not perform any cryptographic verification.
264
+ */
265
+ declare function isValidEnrollment(value: unknown): value is WebAuthnEnrollment;
266
+
267
+ export { WebAuthnCancelledError, type WebAuthnEnrollOptions, type WebAuthnEnrollment, WebAuthnMultiDeviceError, WebAuthnNotAvailableError, WebAuthnPRFUnavailableError, type WebAuthnUnlockOptions, enrollWebAuthn, isValidEnrollment, isWebAuthnAvailable, unlockWebAuthn, webAuthnSlotRewrapCeremony };