@noy-db/hub 0.1.0-pre.7 → 0.1.0-pre.9

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 (89) hide show
  1. package/dist/blobs/index.cjs.map +1 -1
  2. package/dist/blobs/index.d.cts +2 -2
  3. package/dist/blobs/index.d.ts +2 -2
  4. package/dist/blobs/index.js +2 -2
  5. package/dist/bundle/index.d.cts +2 -2
  6. package/dist/bundle/index.d.ts +2 -2
  7. package/dist/bundle/index.js +3 -3
  8. package/dist/{chunk-KPF2HHPI.js → chunk-2CSJGFCB.js} +2 -2
  9. package/dist/{chunk-INSJBB5W.js → chunk-4PWAI7Q4.js} +3 -3
  10. package/dist/{chunk-CL37QSND.js → chunk-AVVPZ4BC.js} +2 -2
  11. package/dist/{chunk-FAAWLVTF.js → chunk-EXHNQEV4.js} +2 -2
  12. package/dist/{chunk-NZ4XCIKS.js → chunk-MDDTIZUO.js} +3 -3
  13. package/dist/{chunk-GILMPJXB.js → chunk-PTVMYYON.js} +2 -2
  14. package/dist/{chunk-N2LMZKLR.js → chunk-QAVUREFT.js} +2 -2
  15. package/dist/{chunk-3WCRU7TI.js → chunk-QGZRWRSL.js} +2 -2
  16. package/dist/{chunk-B6HF6NTZ.js → chunk-RKJ6OL7K.js} +1 -1
  17. package/dist/chunk-RKJ6OL7K.js.map +1 -0
  18. package/dist/{chunk-XCL3WP6J.js → chunk-SCZXXXU4.js} +2 -1
  19. package/dist/{chunk-XCL3WP6J.js.map → chunk-SCZXXXU4.js.map} +1 -1
  20. package/dist/{chunk-UFL4DUEV.js → chunk-VQBTTTUN.js} +1 -1
  21. package/dist/chunk-VQBTTTUN.js.map +1 -0
  22. package/dist/{chunk-6IJQ27XN.js → chunk-WDM5XGGS.js} +51 -4
  23. package/dist/chunk-WDM5XGGS.js.map +1 -0
  24. package/dist/consent/index.d.cts +2 -2
  25. package/dist/consent/index.d.ts +2 -2
  26. package/dist/{delegation-XDJCBTI2.js → delegation-2DBS2EOH.js} +2 -2
  27. package/dist/{dev-unlock-CcJ1qIi7.d.ts → dev-unlock-BdPp68qn.d.ts} +1 -1
  28. package/dist/{dev-unlock-Dk14V6lX.d.cts → dev-unlock-Da1B0TIK.d.cts} +1 -1
  29. package/dist/{hash-h_2U3TFb.d.cts → hash-BEfzPKwo.d.cts} +1 -1
  30. package/dist/{hash-1Xsqx1jl.d.ts → hash-lsoL3eEW.d.ts} +1 -1
  31. package/dist/history/index.cjs.map +1 -1
  32. package/dist/history/index.d.cts +3 -3
  33. package/dist/history/index.d.ts +3 -3
  34. package/dist/history/index.js +2 -2
  35. package/dist/i18n/index.cjs +11 -0
  36. package/dist/i18n/index.cjs.map +1 -1
  37. package/dist/i18n/index.d.cts +2 -2
  38. package/dist/i18n/index.d.ts +2 -2
  39. package/dist/i18n/index.js +3 -3
  40. package/dist/{index-DZn6Yick.d.ts → index-8QDuznDr.d.ts} +1 -1
  41. package/dist/{index-Cvb0efA_.d.cts → index-CywCC1qZ.d.cts} +1 -1
  42. package/dist/index.cjs +590 -59
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +5 -5
  45. package/dist/index.d.ts +5 -5
  46. package/dist/index.js +548 -74
  47. package/dist/index.js.map +1 -1
  48. package/dist/{ledger-5V67MAIL.js → ledger-QZTTHQAQ.js} +3 -3
  49. package/dist/periods/index.cjs.map +1 -1
  50. package/dist/periods/index.d.cts +2 -2
  51. package/dist/periods/index.d.ts +2 -2
  52. package/dist/periods/index.js +3 -3
  53. package/dist/{public-envelope-DFJZHXVH.js → public-envelope-6JTACYJV.js} +3 -3
  54. package/dist/session/index.cjs.map +1 -1
  55. package/dist/session/index.d.cts +3 -3
  56. package/dist/session/index.d.ts +3 -3
  57. package/dist/session/index.js +1 -1
  58. package/dist/shadow/index.d.cts +2 -2
  59. package/dist/shadow/index.d.ts +2 -2
  60. package/dist/store/index.d.cts +2 -2
  61. package/dist/store/index.d.ts +2 -2
  62. package/dist/sync/index.cjs.map +1 -1
  63. package/dist/sync/index.d.cts +1 -1
  64. package/dist/sync/index.d.ts +1 -1
  65. package/dist/sync/index.js +2 -2
  66. package/dist/team/index.cjs +11 -0
  67. package/dist/team/index.cjs.map +1 -1
  68. package/dist/team/index.d.cts +2 -2
  69. package/dist/team/index.d.ts +2 -2
  70. package/dist/team/index.js +4 -4
  71. package/dist/tx/index.d.cts +2 -2
  72. package/dist/tx/index.d.ts +2 -2
  73. package/dist/{types-D3QLmhlk.d.cts → types-Bnb82f5R.d.cts} +818 -74
  74. package/dist/{types-D-6bmD2c.d.ts → types-Bo7NSXJr.d.ts} +818 -74
  75. package/package.json +1 -1
  76. package/dist/chunk-6IJQ27XN.js.map +0 -1
  77. package/dist/chunk-B6HF6NTZ.js.map +0 -1
  78. package/dist/chunk-UFL4DUEV.js.map +0 -1
  79. /package/dist/{chunk-KPF2HHPI.js.map → chunk-2CSJGFCB.js.map} +0 -0
  80. /package/dist/{chunk-INSJBB5W.js.map → chunk-4PWAI7Q4.js.map} +0 -0
  81. /package/dist/{chunk-CL37QSND.js.map → chunk-AVVPZ4BC.js.map} +0 -0
  82. /package/dist/{chunk-FAAWLVTF.js.map → chunk-EXHNQEV4.js.map} +0 -0
  83. /package/dist/{chunk-NZ4XCIKS.js.map → chunk-MDDTIZUO.js.map} +0 -0
  84. /package/dist/{chunk-GILMPJXB.js.map → chunk-PTVMYYON.js.map} +0 -0
  85. /package/dist/{chunk-N2LMZKLR.js.map → chunk-QAVUREFT.js.map} +0 -0
  86. /package/dist/{chunk-3WCRU7TI.js.map → chunk-QGZRWRSL.js.map} +0 -0
  87. /package/dist/{delegation-XDJCBTI2.js.map → delegation-2DBS2EOH.js.map} +0 -0
  88. /package/dist/{ledger-5V67MAIL.js.map → ledger-QZTTHQAQ.js.map} +0 -0
  89. /package/dist/{public-envelope-DFJZHXVH.js.map → public-envelope-6JTACYJV.js.map} +0 -0
@@ -1770,6 +1770,49 @@ interface PassphrasePolicy {
1770
1770
  readonly minWordLength?: number;
1771
1771
  /** Reject adjacent identical words ("the the"). Default true. */
1772
1772
  readonly rejectRepeatedAdjacent?: boolean;
1773
+ /**
1774
+ * Override the default character-class rule (`/^[a-z]+( [a-z]+)*$/`).
1775
+ *
1776
+ * The hub's strict default is lowercase-letters-and-single-spaces
1777
+ * because that's what the EFF wordlist generator emits and what
1778
+ * most attacker password lists are keyed on. Use this knob to allow
1779
+ * digits, uppercase, hyphens, or non-Latin scripts when the
1780
+ * consumer's audience needs them — e.g.:
1781
+ *
1782
+ * ```ts
1783
+ * // Thai + English mix with digits permitted
1784
+ * pattern: /^[\p{L}0-9 ]+( [\p{L}0-9 ]+)*$/u
1785
+ *
1786
+ * // Allow uppercase + hyphens (passphrase-with-hyphens style)
1787
+ * pattern: /^[A-Za-z]+([- ][A-Za-z]+)*$/
1788
+ * ```
1789
+ *
1790
+ * The OTHER structural rules still apply (min-words split by space,
1791
+ * min-word-length, repeated-adjacent, leading/trailing whitespace,
1792
+ * double-space). For non-space-delimited word semantics, use
1793
+ * {@link customValidator} instead.
1794
+ *
1795
+ * Added in pre.8 (#31).
1796
+ */
1797
+ readonly pattern?: RegExp;
1798
+ /**
1799
+ * Replace ALL validation entirely with a custom function. When set,
1800
+ * none of the other PassphrasePolicy fields apply — the consumer
1801
+ * owns every rule (word splitting, character classes, entropy
1802
+ * thresholds, allowlist/denylist). Use sparingly; this is the
1803
+ * escape hatch for domain-specific phrase formats:
1804
+ *
1805
+ * - Localized wordlists with non-space word boundaries
1806
+ * - BIP-39 seed phrases (24 words, fixed wordlist, etc.)
1807
+ * - Organization-specific HR password policies
1808
+ *
1809
+ * The returned `PassphraseValidationResult` is what
1810
+ * {@link assertStrongPassphrase} dispatches on — `ok: true` accepts;
1811
+ * `ok: false` throws `WeakPassphraseError` with the supplied reason.
1812
+ *
1813
+ * Added in pre.8 (#31).
1814
+ */
1815
+ readonly customValidator?: (phrase: string) => PassphraseValidationResult;
1773
1816
  }
1774
1817
  /** Result of a check. Discriminated union — compile-time exhaustive. */
1775
1818
  type PassphraseValidationResult = {
@@ -1876,7 +1919,28 @@ interface UnlockedKeyring {
1876
1919
  readonly role: Role;
1877
1920
  readonly permissions: Permissions;
1878
1921
  readonly deks: Map<string, CryptoKey>;
1879
- readonly kek: CryptoKey;
1922
+ /**
1923
+ * The KEK, when this keyring was unlocked via tier 1 (passphrase) or
1924
+ * a wrap-KEK tier-2 method (WebAuthn / OIDC). `null` when the
1925
+ * keyring was opened via:
1926
+ *
1927
+ * - Unencrypted mode (no KEK exists)
1928
+ * - Tier-3 PIN quick-resume (`@noy-db/on-pin`)
1929
+ * - Wrap-DEKs tier-2 unlock (`@noy-db/on-password`'s
1930
+ * `verifyPasswordSlot` after #26 Path C)
1931
+ * - Session-state restore (`session/session.ts`)
1932
+ * - Dev-unlock fixture (`session/dev-unlock.ts`)
1933
+ *
1934
+ * Consumers performing tier-1 operations that need the KEK
1935
+ * (DEK rewrap, keyring persist, delegation issue/unwrap) must
1936
+ * null-check and throw a clear error if absent — re-authenticate
1937
+ * at tier 1 first to recover the KEK.
1938
+ *
1939
+ * Tightened from `CryptoKey` to `CryptoKey | null` in pre.8 (#41).
1940
+ * The runtime contract has always allowed null; the type now
1941
+ * matches reality.
1942
+ */
1943
+ readonly kek: CryptoKey | null;
1880
1944
  readonly salt: Uint8Array;
1881
1945
  /**
1882
1946
  * `@noy-db/as-*` export capability. Absent when the
@@ -2882,6 +2946,82 @@ declare class SyncEngine {
2882
2946
  private persistMeta;
2883
2947
  }
2884
2948
 
2949
+ /**
2950
+ * Tier-2 authenticator slot management — issue #11.
2951
+ *
2952
+ * Each slot independently wraps the SAME KEK under a method-specific
2953
+ * derived key (LUKS pattern). Enrolling adds a slot; removing drops
2954
+ * one. Both are constant-time keyring writes — no DEK re-keying.
2955
+ *
2956
+ * The crypto for each method lives in its `@noy-db/on-*` package
2957
+ * (`on-webauthn`, `on-oidc`, `on-password`); this module accepts the
2958
+ * package's `wrapped_kek` ciphertext + `meta` payload and persists it.
2959
+ *
2960
+ * @see docs/subsystems/session-tiers.md → Tier 2 — Authenticate
2961
+ *
2962
+ * @module
2963
+ */
2964
+
2965
+ /** Fields shared across both wrap-KEK and wrap-DEKs enroll inputs. */
2966
+ interface EnrollAuthenticatorBase {
2967
+ readonly id: string;
2968
+ readonly method: KeyringAuthenticator['method'];
2969
+ /** Method-specific metadata (cred id, salt, …). */
2970
+ readonly meta: Record<string, unknown>;
2971
+ /** Tier the active session held when enrolling. Defaults to 1. */
2972
+ readonly enrolled_via_tier?: 1 | 2;
2973
+ }
2974
+ /** Wrap-KEK enroll input (WebAuthn, OIDC). */
2975
+ interface EnrollAuthenticatorWrappingKEKOptions extends EnrollAuthenticatorBase {
2976
+ /** Already-wrapped KEK ciphertext (base64) — produced by the on-* package. */
2977
+ readonly wrapped_kek: string;
2978
+ readonly wrapKind?: 'kek';
2979
+ }
2980
+ /** Wrap-DEKs enroll input (password, future on-* using the unified wrap-DEKs primitive). */
2981
+ interface EnrollAuthenticatorWrappingDEKsOptions extends EnrollAuthenticatorBase {
2982
+ readonly wrapKind: 'deks';
2983
+ /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */
2984
+ readonly wrapped_deks: string;
2985
+ /** Base64 AES-GCM IV used for the `wrapped_deks` ciphertext. */
2986
+ readonly iv: string;
2987
+ }
2988
+ /** Discriminated union over the two enroll input shapes. */
2989
+ type EnrollAuthenticatorOptions = EnrollAuthenticatorWrappingKEKOptions | EnrollAuthenticatorWrappingDEKsOptions;
2990
+ /**
2991
+ * Append a new authenticator slot to the keyring file. Throws
2992
+ * `ValidationError` if a slot with the same id already exists — the
2993
+ * caller decides whether to remove + re-enroll.
2994
+ *
2995
+ * Accepts either wrap-KEK (WebAuthn, OIDC) or wrap-DEKs (password)
2996
+ * input. The variant is preserved verbatim into `KeyringAuthenticator`.
2997
+ */
2998
+ declare function enrollAuthenticator(store: NoydbStore, vault: string, keyring: UnlockedKeyring, options: EnrollAuthenticatorOptions): Promise<UnlockedKeyring>;
2999
+ /**
3000
+ * Caller payload for {@link updateAuthenticator} (#55). Mutates only
3001
+ * `meta` — the slot's id, method, and wrap material are immutable
3002
+ * through this primitive, preserving the anti-slot-swap guard.
3003
+ *
3004
+ * `meta` is **merged** at the top level: keys absent from the patch
3005
+ * are preserved, keys present overwrite. To clear a meta key, pass
3006
+ * `null` for that key explicitly. (Same semantics as #57's
3007
+ * `UserApi.updateMe`, scoped to this top-level merge — no recursion
3008
+ * into nested meta values.)
3009
+ */
3010
+ interface UpdateAuthenticatorOptions {
3011
+ readonly meta?: Record<string, unknown>;
3012
+ }
3013
+ /**
3014
+ * Drop a slot by id. No-op if the slot doesn't exist (idempotent —
3015
+ * removing a non-existent slot is a recoverable retry, not an error).
3016
+ */
3017
+ declare function removeAuthenticator(store: NoydbStore, vault: string, keyring: UnlockedKeyring, slotId: string): Promise<UnlockedKeyring>;
3018
+ /**
3019
+ * Look up a slot by id. Returns `undefined` when no slot matches.
3020
+ * Used by tier-2 unlock dispatchers to fetch the wrapped KEK + meta
3021
+ * before invoking the method-specific verifier.
3022
+ */
3023
+ declare function findAuthenticator(keyring: UnlockedKeyring, slotId: string): KeyringAuthenticator | undefined;
3024
+
2885
3025
  /**
2886
3026
  * Tier-1 change flows — `rotatePassphrase` (user remembers old) and
2887
3027
  * `recoverPassphrase` (user supplies a recovery proof). Issue #10.
@@ -2901,24 +3041,86 @@ declare class SyncEngine {
2901
3041
  * @module
2902
3042
  */
2903
3043
 
3044
+ /**
3045
+ * Context handed to a {@link SlotRewrapCeremony} when `rotatePassphrase`
3046
+ * preserves a tier-2 slot. The ceremony's job is to re-derive its
3047
+ * method-specific wrapping material (PRF assertion, PBKDF2 of a
3048
+ * daily-password, etc.) and wrap the freshly rewrapped DEK set under
3049
+ * the new wrapping key.
3050
+ *
3051
+ * Two surfaces are exposed:
3052
+ *
3053
+ * - `newDeks` — the rewrapped (extractable) DEK set the slot will
3054
+ * wrap. This is what `mintPaperRecoveryEntry` / `enrollPassword-
3055
+ * Authenticator` / `wrapKeyringSummary` (in `@noy-db/on-webauthn`)
3056
+ * all consume; effectively the canonical input for every
3057
+ * post-Path C tier-2 ceremony.
3058
+ *
3059
+ * - `newKek` — the freshly-derived KEK (extractable for the
3060
+ * ceremony scope only). Only relevant for forward-compatibility
3061
+ * with a hypothetical future on-* package that wants to wrap the
3062
+ * KEK itself under a method-derived key. None of the shipped
3063
+ * on-* packages need this; they all operate on `newDeks`.
3064
+ *
3065
+ * The ceremony MUST preserve `oldSlot.id` and `oldSlot.method` in the
3066
+ * returned `EnrollAuthenticatorOptions`. Hub validates these — a
3067
+ * mismatch throws `ValidationError` (prevents slot-type swap mid-
3068
+ * rotation, e.g. converting a webauthn slot to a password slot under
3069
+ * cover of preservation).
3070
+ */
3071
+ interface SlotRewrapContext {
3072
+ readonly newKek: CryptoKey;
3073
+ readonly newDeks: Map<string, CryptoKey>;
3074
+ readonly oldSlot: KeyringAuthenticator;
3075
+ }
3076
+ /**
3077
+ * Callback that re-enrolls one tier-2 slot during `rotatePassphrase`.
3078
+ * Returns the new slot's `EnrollAuthenticatorOptions` — same shape
3079
+ * the consumer would pass to `db.enrollAuthenticator` for a fresh
3080
+ * enrollment. Hub persists the result atomically with the rotation.
3081
+ */
3082
+ type SlotRewrapCeremony = (ctx: SlotRewrapContext) => Promise<EnrollAuthenticatorOptions>;
2904
3083
  /** Caller payload for {@link rotatePassphrase}. */
2905
3084
  interface RotatePassphraseInput {
2906
3085
  readonly oldPassphrase: string;
2907
3086
  readonly newPassphrase: string;
2908
3087
  readonly passphrasePolicy?: PassphrasePolicy;
2909
3088
  readonly allowWeakPassphrase?: boolean;
3089
+ /**
3090
+ * Map of slot id → re-enrolment ceremony. Slots whose id appears
3091
+ * here are PRESERVED across rotation (the ceremony re-derives the
3092
+ * method-specific wrapping under the new keyring); slots whose id
3093
+ * is absent are DROPPED (the pre-#29 behavior).
3094
+ *
3095
+ * Without this map, `rotatePassphrase` retains the pre-pre.8
3096
+ * behavior of wiping every tier-2 slot. Consumers building a
3097
+ * "rotate without losing my biometric" flow supply ceremonies for
3098
+ * each slot they want to keep.
3099
+ *
3100
+ * If a ceremony throws, the entire rotation throws — no partial
3101
+ * state. Callers wrap individual ceremonies in try/catch + return
3102
+ * a sentinel if they want graceful degradation per slot.
3103
+ *
3104
+ * Added in pre.8 (#29).
3105
+ */
3106
+ readonly slotCeremonies?: {
3107
+ readonly [slotId: string]: SlotRewrapCeremony;
3108
+ };
2910
3109
  }
2911
3110
  /**
2912
3111
  * Re-derive the user's KEK from `oldPassphrase`, rewrap every DEK
2913
3112
  * under a freshly-derived KEK from `newPassphrase`, and persist.
2914
3113
  *
2915
- * Tier-2 authenticator slots are NOT preserved each slot wraps the
2916
- * old KEK and would need the user's per-slot derivation key to
2917
- * re-wrap; the hub doesn't hold that. The user re-enrols any slots
2918
- * after rotation. v0.1.0-pre.5 limitation.
3114
+ * Tier-2 authenticator slots are dropped UNLESS the caller supplies
3115
+ * a `slotCeremonies` map (#29) each ceremony re-derives its
3116
+ * method-specific wrapping under the new keyring, and hub persists
3117
+ * the rewrapped slots atomically with the rotation. Slots whose id
3118
+ * isn't in the map are still dropped (pre-pre.8 behavior).
2919
3119
  *
2920
3120
  * @throws `InvalidKeyError` if `oldPassphrase` does not unwrap the keyring.
2921
3121
  * @throws `WeakPassphraseError` if `newPassphrase` fails the strength rule.
3122
+ * @throws `ValidationError` if a ceremony's result mismatches the
3123
+ * slot's id or method (anti-slot-swap guard).
2922
3124
  */
2923
3125
  declare function rotatePassphrase(store: NoydbStore, vault: string, userId: string, input: RotatePassphraseInput): Promise<UnlockedKeyring>;
2924
3126
  /** Caller payload for {@link recoverPassphrase}. */
@@ -2949,6 +3151,58 @@ interface RecoverPassphraseInput {
2949
3151
  readonly recoveryProof: RecoveryProof;
2950
3152
  readonly passphrasePolicy?: PassphrasePolicy;
2951
3153
  readonly allowWeakPassphrase?: boolean;
3154
+ /**
3155
+ * After a successful paper-recovery, replace ALL remaining recovery
3156
+ * entries with freshly-minted ones. Defaults to `true` (defensive).
3157
+ *
3158
+ * Rationale (issue #36): the user just demonstrated they had access
3159
+ * to AT LEAST one code. The remaining codes from the same printed
3160
+ * sheet may also be compromised — photographed, leaked via a
3161
+ * screen-share slip, or in the hands of whoever stole the sheet.
3162
+ * Auto-rotation closes the window without requiring consumer action.
3163
+ *
3164
+ * Set to `false` to preserve the original behavior (only the matched
3165
+ * code is burned; the rest stay valid).
3166
+ *
3167
+ * Hub-side orchestration is non-atomic with the recovery itself:
3168
+ * if the rotation step fails after a successful burn, the user
3169
+ * falls back to the pre-rotation state (remaining codes still
3170
+ * valid). Strictly safer than the previous default — a failed
3171
+ * rotation degrades gracefully rather than leaving the vault
3172
+ * locked or codes dual-existing.
3173
+ */
3174
+ readonly rotateRemainingCodes?: boolean;
3175
+ /**
3176
+ * Number of fresh codes to mint when `rotateRemainingCodes` is on.
3177
+ * Defaults to the count of remaining entries POST-burn (e.g. if
3178
+ * the user enrolled 8 originally and just consumed 1, defaults to
3179
+ * 7). Pass an explicit number to mint a different count — useful
3180
+ * when the consumer wants to refresh to a target N regardless of
3181
+ * how many were left.
3182
+ */
3183
+ readonly newCodeCount?: number;
3184
+ /**
3185
+ * Override the default raw-code generator. The default is hub's
3186
+ * {@link generateULID} — uppercase Crockford-Base32, 26 chars,
3187
+ * passes through `normalizePaperCode` untouched.
3188
+ *
3189
+ * Pass `() => generateRawCode()` from `@noy-db/on-recovery` when
3190
+ * the consumer prefers the Base32 + checksum format with hyphenated
3191
+ * display. The `mintPaperRecoveryEntry` helper accepts any string —
3192
+ * the generator just needs to produce a high-entropy unique value.
3193
+ */
3194
+ readonly codeGenerator?: () => string;
3195
+ }
3196
+ /**
3197
+ * Return shape of `db.recoverPassphrase`. `newCodes` is populated when
3198
+ * `rotateRemainingCodes` was enabled and at least one entry was
3199
+ * rotated; an empty array means no rotation happened (rotation
3200
+ * disabled, or no remaining codes after burn). Show the codes to the
3201
+ * user once — they are the canonical credential for future recovery
3202
+ * and CANNOT be retrieved again.
3203
+ */
3204
+ interface RecoverPassphraseResult {
3205
+ readonly newCodes: readonly string[];
2952
3206
  }
2953
3207
  /**
2954
3208
  * Reset the user's passphrase using a recovery proof. v0.1.0-pre.5
@@ -2961,6 +3215,172 @@ interface RecoverPassphraseInput {
2961
3215
  */
2962
3216
  declare function recoverPassphrase(store: NoydbStore, vault: string, userId: string, input: RecoverPassphraseInput): Promise<UnlockedKeyring>;
2963
3217
 
3218
+ /**
3219
+ * Atomic peer-recovery primitive — issues #33 + #34.
3220
+ *
3221
+ * `recoverUser` is a SEPARATE operation from `revoke + grant`. It
3222
+ * exists because peer-recovery has different semantics than account
3223
+ * removal-then-reissue:
3224
+ *
3225
+ * 1. **Same identity preserved.** `userId`, `role`, `permissions`,
3226
+ * capability bits, user envelope (if any), policy override (if
3227
+ * any) all survive. Only the wrapping changes.
3228
+ * 2. **No key rotation.** The existing DEKs stay valid — every
3229
+ * OTHER principal in the vault keeps their access. Rotating
3230
+ * keys would invalidate every co-user's wrapping.
3231
+ * 3. **Atomic by construction.** A single `store.put` overwrites
3232
+ * `_keyring/<userId>` with the recovered file. No revoke step
3233
+ * means no partial-failure window.
3234
+ * 4. **Owner→owner natively allowed.** Two co-owners recovering
3235
+ * each other is the explicitly-intentional case (a partner
3236
+ * forgot the master phrase). The existing `canRevoke` rule that
3237
+ * blocks owner→owner is correct for `revoke` (which is account
3238
+ * *removal*) and intentionally NOT replicated here. The policy
3239
+ * gate `peer-recover-user` carries the freshness requirement.
3240
+ * 5. **Tier-2 slots dropped.** The slots wrap the OLD KEK under
3241
+ * method-derived keys; after recovery the KEK is re-derived
3242
+ * from the new temp passphrase. Match `rotatePassphrase`'s
3243
+ * precedent — the recovered user re-enrols slots after picking
3244
+ * their own phrase.
3245
+ *
3246
+ * Caller must be at least as privileged as the target. The hub
3247
+ * `db.recoverUser` method gates this with the `peer-recover-user`
3248
+ * policy gate (#33's factor-proof requirement); the function below
3249
+ * enforces only the role + anti-privilege-escalation invariants.
3250
+ *
3251
+ * @module
3252
+ */
3253
+
3254
+ /** Input shape for {@link recoverUser}. */
3255
+ interface RecoverUserOptions {
3256
+ /** Target user id whose keyring is being recovered. */
3257
+ readonly userId: string;
3258
+ /**
3259
+ * Temporary passphrase under which the new keyring is wrapped.
3260
+ * The recipient should call `db.rotatePassphrase` immediately on
3261
+ * acceptance to choose their own phrase — this temp acts as a
3262
+ * single-use bridge in invite / peer-recovery flows.
3263
+ */
3264
+ readonly passphrase: string;
3265
+ /** Override the target's role. Defaults to the existing target's role. */
3266
+ readonly role?: Role;
3267
+ /** Override the target's display name. Defaults to existing. */
3268
+ readonly displayName?: string;
3269
+ /** Validate phrase strength against the configured policy. */
3270
+ readonly validatePassphrase?: boolean;
3271
+ /**
3272
+ * Skip phrase strength validation even when `validatePassphrase` is
3273
+ * set. The escape hatch matches `grant`'s shape — used when the
3274
+ * temp phrase is a high-entropy one-shot string that doesn't need
3275
+ * to satisfy the human-typeable rules.
3276
+ */
3277
+ readonly allowWeakPassphrase?: boolean;
3278
+ /**
3279
+ * Optional explicit phrase policy override (passed through to
3280
+ * `assertStrongPassphrase`). Mirrors how `grant` accepts a custom
3281
+ * `PassphrasePolicy` for app-specific tightening.
3282
+ */
3283
+ readonly passphrasePolicy?: PassphrasePolicy;
3284
+ }
3285
+ /**
3286
+ * Atomically rewrap the target user's keyring under a fresh temp
3287
+ * passphrase. Single store write; no revoke step; no key rotation.
3288
+ *
3289
+ * Caller's responsibilities (NOT enforced here):
3290
+ * - Run the `peer-recover-user` policy gate first via
3291
+ * `Noydb.checkGate` to enforce the freshness factor proof.
3292
+ * - Communicate the temp passphrase to the recipient via a secure
3293
+ * channel (URL fragment, in-person, etc.) — the hub does not
3294
+ * transport secrets.
3295
+ */
3296
+ declare function recoverUser(store: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: RecoverUserOptions): Promise<void>;
3297
+
3298
+ /**
3299
+ * **Wrap-DEKs primitive (#44)** — a single canonical shape for the
3300
+ * pattern of "serialize a DEK set, encrypt it under a credential-derived
3301
+ * AES-GCM key." Used by:
3302
+ *
3303
+ * - **tier-0** — paper recovery entries (`_meta/recovery-paper`),
3304
+ * credential = the printed code.
3305
+ * - **tier-2** — password authenticator slots (`KeyringFile.authenticators`,
3306
+ * `wrapKind: 'deks'`), credential = the daily password.
3307
+ *
3308
+ * **Not** used by `@noy-db/on-pin` — tier-3 wraps the DEK set under
3309
+ * the same conceptual pattern but at **100,000 PBKDF2 iterations**
3310
+ * (vs the 600,000 here), because the protection window for a PIN
3311
+ * slot is short (idle-timeout-bounded, typically 15 min) and 600k
3312
+ * iterations would make every PIN-resume noticeably slow. The wire
3313
+ * formats are deliberately incompatible. See `@noy-db/on-pin`'s
3314
+ * `PIN_PBKDF2_ITERATIONS` and the threat-model rationale in its
3315
+ * module docstring.
3316
+ *
3317
+ * Before #44, the same crypto lived in two places: `mintPaperRecoveryEntry`
3318
+ * (in `team/recovery.ts`) and `enrollPasswordAuthenticator` (in
3319
+ * `@noy-db/on-password`). Both functions did identical work — PBKDF2
3320
+ * the credential, AES-GCM-encrypt the JSON-serialized DEK set — but
3321
+ * their implementations had drifted apart enough that fixing a bug
3322
+ * in one wouldn't fix the other.
3323
+ *
3324
+ * This module owns the canonical implementation. Consumers compose:
3325
+ *
3326
+ * - `mintPaperRecoveryEntry` is now a thin wrapper that calls
3327
+ * `mintWrappedDeksBlob` and adds `{ codeId, enrolledAt }`.
3328
+ * - `enrollPasswordAuthenticator` calls `mintWrappedDeksBlob` and
3329
+ * wraps the result in the slot envelope.
3330
+ *
3331
+ * @module
3332
+ */
3333
+ /**
3334
+ * The wrap-DEKs primitive — a serialized + AES-GCM-encrypted DEK set
3335
+ * keyed under a credential-derived key.
3336
+ *
3337
+ * All three fields are base64-encoded so the blob is JSON-safe and
3338
+ * round-trips through `_meta/*` envelopes (which carry plaintext
3339
+ * JSON in `_data`).
3340
+ *
3341
+ * Composition: `PaperRecoveryEntry extends WrappedDeksBlob` plus
3342
+ * `{ codeId, enrolledAt }`. `KeyringAuthenticatorWrappingDEKs`
3343
+ * carries the same three fields with `salt` stored in `meta` for
3344
+ * slot-format back-compat (#44 defers moving it to top-level).
3345
+ */
3346
+ interface WrappedDeksBlob {
3347
+ /** Base64 PBKDF2 salt for the credential-derived wrapping key. */
3348
+ readonly salt: string;
3349
+ /** Base64 AES-GCM IV used for the `wrappedDeks` ciphertext. */
3350
+ readonly iv: string;
3351
+ /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */
3352
+ readonly wrappedDeks: string;
3353
+ }
3354
+ /**
3355
+ * Mint a fresh `WrappedDeksBlob` from a DEK set + a string credential.
3356
+ *
3357
+ * Generates a random salt + IV, derives a 256-bit AES-GCM key via
3358
+ * PBKDF2-SHA256(credential, salt, 600K), serializes the DEK set as
3359
+ * `{ deks: { coll: rawBase64 } }`, and AES-GCM-encrypts.
3360
+ *
3361
+ * The `credential` is the user-typed string (recovery code, daily
3362
+ * password, PIN). Caller normalization rules apply (e.g. paper
3363
+ * recovery uppercase-strips the code before reaching this function).
3364
+ *
3365
+ * @param deks - DEK set to wrap. Each DEK must be exportable via
3366
+ * `subtle.exportKey('raw', dek)` (the hub mints DEKs
3367
+ * this way; consumers feeding non-extractable keys
3368
+ * will get `InvalidAccessError` from WebCrypto).
3369
+ * @param credential - String input the consumer minted (paper code,
3370
+ * password, PIN). Treated as opaque bytes by PBKDF2.
3371
+ */
3372
+ declare function mintWrappedDeksBlob(deks: Map<string, CryptoKey>, credential: string): Promise<WrappedDeksBlob>;
3373
+ /**
3374
+ * Reverse of {@link mintWrappedDeksBlob}. Re-derives the wrapping key
3375
+ * from the credential + stored salt, AES-GCM-decrypts the wrapped DEK
3376
+ * set, and re-imports each DEK as an extractable AES-GCM CryptoKey.
3377
+ *
3378
+ * Throws (AES-GCM auth tag failure) when the credential doesn't
3379
+ * match the blob. Callers iterating over multiple blobs (e.g. paper
3380
+ * recovery's "try every entry until one matches") should catch.
3381
+ */
3382
+ declare function unwrapDeksFromBlob(blob: WrappedDeksBlob, credential: string): Promise<Map<string, CryptoKey>>;
3383
+
2964
3384
  /**
2965
3385
  * Recovery profile persistence + dispatch — issue #10.
2966
3386
  *
@@ -3000,15 +3420,15 @@ declare function recoverPassphrase(store: NoydbStore, vault: string, userId: str
3000
3420
  * resume — the cryptographic guarantee is identical (AES-GCM with a
3001
3421
  * PBKDF2-derived key), and it sidesteps the non-extractable-KEK
3002
3422
  * constraint cleanly.
3423
+ *
3424
+ * Type-level composition (#44): `PaperRecoveryEntry extends
3425
+ * WrappedDeksBlob` — the three crypto fields (`salt`, `iv`,
3426
+ * `wrappedDeks`) come from the shared primitive; `codeId` and
3427
+ * `enrolledAt` are paper-recovery's own metadata. Wire format
3428
+ * unchanged.
3003
3429
  */
3004
- interface PaperRecoveryEntry {
3430
+ interface PaperRecoveryEntry extends WrappedDeksBlob {
3005
3431
  readonly codeId: string;
3006
- /** Base64 PBKDF2 salt. */
3007
- readonly salt: string;
3008
- /** Base64 AES-GCM IV used for the wrapped-DEK ciphertext. */
3009
- readonly iv: string;
3010
- /** Base64 AES-GCM ciphertext — JSON `{ deks: Record<string, base64> }`. */
3011
- readonly wrappedDeks: string;
3012
3432
  readonly enrolledAt: string;
3013
3433
  }
3014
3434
  interface PaperRecoveryDoc {
@@ -3024,6 +3444,33 @@ declare function savePaperRecoveryEntries(store: NoydbStore, vault: string, entr
3024
3444
  declare function burnPaperRecoveryEntry(store: NoydbStore, vault: string, codeId: string): Promise<void>;
3025
3445
  /** Whether at least one recovery profile has any enrolled entries. */
3026
3446
  declare function hasRecoveryEnrolled(store: NoydbStore, vault: string): Promise<boolean>;
3447
+ /**
3448
+ * Generate one paper-recovery entry from an unlocked DEK set.
3449
+ *
3450
+ * Returns the serializable entry (persisted via
3451
+ * {@link savePaperRecoveryEntries}). The recovery flow unwraps the
3452
+ * DEK set, then mints a fresh KEK from the user's new passphrase.
3453
+ *
3454
+ * Thin wrapper over {@link mintWrappedDeksBlob} (#44) — the crypto
3455
+ * lives in the shared primitive; this function just adds paper-
3456
+ * recovery's own metadata (`codeId`, `enrolledAt`).
3457
+ *
3458
+ * @param deks Map of collection-name → DEK (extractable).
3459
+ * @param code The plaintext recovery code (caller-supplied;
3460
+ * pair this with `@noy-db/on-recovery`'s code
3461
+ * generator/parser if available).
3462
+ * @param codeId Stable id used by `burnPaperRecoveryEntry`.
3463
+ */
3464
+ declare function mintPaperRecoveryEntry(deks: Map<string, CryptoKey>, code: string, codeId: string): Promise<PaperRecoveryEntry>;
3465
+ /**
3466
+ * Decrypt a recovery entry to recover the raw DEK set. Used by the
3467
+ * `recoverPassphrase` flow after the user's code has been parsed.
3468
+ *
3469
+ * Thin wrapper over {@link unwrapDeksFromBlob} (#44).
3470
+ *
3471
+ * @throws when the code does not match the entry (AES-GCM auth tag fail).
3472
+ */
3473
+ declare function unwrapDeksFromPaperEntry(entry: PaperRecoveryEntry, code: string): Promise<Map<string, CryptoKey>>;
3027
3474
 
3028
3475
  /**
3029
3476
  * Public envelope — owner-curated plaintext metadata, readable
@@ -3130,51 +3577,6 @@ declare function validatePublicEnvelopeInput(input: SetPublicEnvelopeInput, sche
3130
3577
  */
3131
3578
  declare function isPublicEnvelope(x: unknown): x is PublicEnvelope;
3132
3579
 
3133
- /**
3134
- * Tier-2 authenticator slot management — issue #11.
3135
- *
3136
- * Each slot independently wraps the SAME KEK under a method-specific
3137
- * derived key (LUKS pattern). Enrolling adds a slot; removing drops
3138
- * one. Both are constant-time keyring writes — no DEK re-keying.
3139
- *
3140
- * The crypto for each method lives in its `@noy-db/on-*` package
3141
- * (`on-webauthn`, `on-oidc`, `on-password`); this module accepts the
3142
- * package's `wrapped_kek` ciphertext + `meta` payload and persists it.
3143
- *
3144
- * @see docs/subsystems/session-tiers.md → Tier 2 — Authenticate
3145
- *
3146
- * @module
3147
- */
3148
-
3149
- /** Input shape for `enrollAuthenticator`. */
3150
- interface EnrollAuthenticatorOptions {
3151
- readonly id: string;
3152
- readonly method: KeyringAuthenticator['method'];
3153
- /** Already-wrapped KEK ciphertext (base64) — produced by the on-* package. */
3154
- readonly wrapped_kek: string;
3155
- /** Method-specific metadata (cred id, salt, …). */
3156
- readonly meta: Record<string, unknown>;
3157
- /** Tier the active session held when enrolling. Defaults to 1. */
3158
- readonly enrolled_via_tier?: 1 | 2;
3159
- }
3160
- /**
3161
- * Append a new authenticator slot to the keyring file. Throws
3162
- * `ValidationError` if a slot with the same id already exists — the
3163
- * caller decides whether to remove + re-enroll.
3164
- */
3165
- declare function enrollAuthenticator(store: NoydbStore, vault: string, keyring: UnlockedKeyring, options: EnrollAuthenticatorOptions): Promise<UnlockedKeyring>;
3166
- /**
3167
- * Drop a slot by id. No-op if the slot doesn't exist (idempotent —
3168
- * removing a non-existent slot is a recoverable retry, not an error).
3169
- */
3170
- declare function removeAuthenticator(store: NoydbStore, vault: string, keyring: UnlockedKeyring, slotId: string): Promise<UnlockedKeyring>;
3171
- /**
3172
- * Look up a slot by id. Returns `undefined` when no slot matches.
3173
- * Used by tier-2 unlock dispatchers to fetch the wrapped KEK + meta
3174
- * before invoking the method-specific verifier.
3175
- */
3176
- declare function findAuthenticator(keyring: UnlockedKeyring, slotId: string): KeyringAuthenticator | undefined;
3177
-
3178
3580
  /**
3179
3581
  * Per-vault tier-3 (PIN / quick-resume) state — issue #11.
3180
3582
  *
@@ -3375,8 +3777,35 @@ declare function runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T>
3375
3777
  * @module
3376
3778
  */
3377
3779
 
3378
- /** A single off-device factor surface — the proof an actor presents at gate time. */
3379
- type FactorKind = 'totp' | 'email-otp' | 'recovery' | 'shamir' | 'webauthn-roaming';
3780
+ /**
3781
+ * A single factor surface the proof an actor presents at gate time.
3782
+ *
3783
+ * | Kind | Source | Off-device? |
3784
+ * |---|---|---|
3785
+ * | `totp` | RFC 6238 authenticator app (Google Auth, 1Password) | yes |
3786
+ * | `email-otp` | one-time code mailed to the user | yes |
3787
+ * | `recovery` | printable Base32 code (`@noy-db/on-recovery`) | yes (paper) |
3788
+ * | `shamir` | k-of-n threshold share (`@noy-db/on-shamir`) | yes |
3789
+ * | `webauthn-roaming` | hardware key (YubiKey, SoloKey, Titan) | yes (key portable) |
3790
+ * | `webauthn-platform` | platform passkey (Touch ID, Face ID, Hello) | no (device-bound) |
3791
+ * | `password` | tier-2 daily password (`@noy-db/on-password`) | no |
3792
+ * | `pin` | tier-3 quick-resume PIN (`@noy-db/on-pin`) | no |
3793
+ *
3794
+ * Off-device kinds (TOTP, email-OTP, recovery, shamir, roaming WebAuthn)
3795
+ * are the strongest factor proofs because they require something
3796
+ * separate from the device the user just unlocked. Platform / password /
3797
+ * PIN are useful for "fresh proof of *this* user" but don't bind across
3798
+ * devices — policies can require ANY of them or insist on a count of 2
3799
+ * to force a mix.
3800
+ *
3801
+ * Added in pre.8 (#30): `webauthn-platform`, `password`, `pin` —
3802
+ * previously consumers with no off-device infrastructure (no TOTP,
3803
+ * no email-OTP, paper recovery not enrolled) had to disable the
3804
+ * factor requirement entirely on `rotate-passphrase`. Now they can
3805
+ * pin "any second factor I have wired" without losing the freshness
3806
+ * guarantee.
3807
+ */
3808
+ type FactorKind = 'totp' | 'email-otp' | 'recovery' | 'shamir' | 'webauthn-roaming' | 'webauthn-platform' | 'password' | 'pin';
3380
3809
  /**
3381
3810
  * One factor requirement entry. The default is "any one of the listed
3382
3811
  * factors, fresh within the last 5 minutes". Bumping `count` requires N
@@ -3415,11 +3844,39 @@ interface GatePolicy {
3415
3844
  * and use the same engine; the engine treats unknown names with no
3416
3845
  * configured policy as "no gate" (no-op).
3417
3846
  */
3418
- type BuiltInGateName = 'rotate-passphrase' | 'recover-passphrase' | 'enroll-authenticator' | 'remove-authenticator' | 'rotate-unlock' | 'enroll-user' | 'revoke-user' | 'export-bundle' | 'export-plaintext' | 'view-user-auth'
3847
+ type BuiltInGateName = 'rotate-passphrase' | 'recover-passphrase' | 'enroll-authenticator' | 'remove-authenticator'
3848
+ /**
3849
+ * Authorize a meta-only mutation on an existing authenticator slot —
3850
+ * `db.updateAuthenticator` (#55). The slot's wrap material, id, and
3851
+ * method are immutable through this gate; only the `meta` blob
3852
+ * (nicknames, method-specific labels) can change. Anti-slot-swap
3853
+ * guard is preserved structurally regardless of this gate's
3854
+ * settings.
3855
+ */
3856
+ | 'update-authenticator' | 'rotate-unlock' | 'enroll-user' | 'revoke-user' | 'export-bundle' | 'export-plaintext' | 'view-user-auth'
3419
3857
  /** Authorize a write to one's own user envelope (#22). */
3420
3858
  | 'edit-own-profile'
3421
3859
  /** Authorize reading other principals' user envelopes (#22). */
3422
- | 'view-team-profiles';
3860
+ | 'view-team-profiles'
3861
+ /**
3862
+ * Authorize an atomic peer-recovery — `db.recoverUser` (#33, #34).
3863
+ * Distinct from `revoke-user` because peer-recovery is intentional
3864
+ * re-issuance of someone's keyring under a temp passphrase, NOT
3865
+ * removal. Allows owner→owner natively (matches the threat model:
3866
+ * a co-owner explicitly recovering another co-owner). Ships with a
3867
+ * factor-proof default in `STRICT_POLICY` so the issuer must
3868
+ * affirmatively prove identity at the moment of recovery.
3869
+ */
3870
+ | 'peer-recover-user'
3871
+ /**
3872
+ * Authorize a post-grant identity mutation — `db.updateUser` (#54).
3873
+ * Covers `role`, `displayName`, `permissions` changes on an existing
3874
+ * keyring. Pure plaintext-header rewrite — no DEKs touched, no KEK
3875
+ * required. The role-elevation guard inside the implementation
3876
+ * mirrors `db.grant`'s hierarchy (admin cannot promote to owner)
3877
+ * regardless of this gate's settings.
3878
+ */
3879
+ | 'update-user';
3423
3880
  /** Either a built-in gate name or an `app:*` custom gate. */
3424
3881
  type GateName = BuiltInGateName | `app:${string}`;
3425
3882
  /**
@@ -3519,6 +3976,52 @@ declare class Noydb {
3519
3976
  grant(vault: string, options: GrantOptions): Promise<void>;
3520
3977
  /** Revoke a user's access to a vault. */
3521
3978
  revoke(vault: string, options: RevokeOptions): Promise<void>;
3979
+ /**
3980
+ * Mutate post-grant identity fields on an existing keyring — `role`,
3981
+ * `displayName`, and/or `permissions`. Pure plaintext-header rewrite:
3982
+ * no DEK rewrap, no KEK required, no authenticator slots touched.
3983
+ * Tier-2 enrollments and recovery codes survive.
3984
+ *
3985
+ * Different from `db.revoke + db.grant`:
3986
+ *
3987
+ * - Same `userId`, same DEK wrappings, same `granted_by`, same
3988
+ * `_users/<keyringId>` envelope. Only the specified header
3989
+ * fields move. Last-write-wins via the standard keyring put.
3990
+ * - No cascade on role demotion (admins demoted to operator keep
3991
+ * the keyrings they previously granted; the cascade rules are
3992
+ * a `db.revoke` concern, not `db.updateUser`).
3993
+ * - Tier-2 slots NOT dropped — the wrapping is unaffected.
3994
+ *
3995
+ * Role-elevation guard: BOTH the old and new role must satisfy
3996
+ * `db.grant`'s hierarchy. Owner can do anything; admin manages
3997
+ * admin/operator/viewer/client laterally; admin cannot promote to
3998
+ * owner OR demote from owner. The guard runs regardless of the
3999
+ * `update-user` policy gate's settings — gates can only be more
4000
+ * permissive than the structural floor, never less.
4001
+ *
4002
+ * Gated by `update-user`. `STRICT_POLICY` requires a TOTP/email-OTP
4003
+ * factor proof so the operator affirmatively re-asserts identity at
4004
+ * the moment of mutation; `PERSONAL_POLICY` accepts a tier-1 unlock
4005
+ * alone.
4006
+ *
4007
+ * ```ts
4008
+ * await db.updateUser('acme', {
4009
+ * userId: 'bob',
4010
+ * role: 'operator', // promote
4011
+ * permissions: { invoices: 'rw' },
4012
+ * }, { factors: [{ kind: 'totp' }] })
4013
+ * ```
4014
+ *
4015
+ * @throws `NoAccessError` when no keyring exists for the target.
4016
+ * @throws `PermissionDeniedError` when the role hierarchy rejects.
4017
+ * @throws `ValidationError` when no field is provided.
4018
+ *
4019
+ * @see #54
4020
+ */
4021
+ updateUser(vault: string, options: UpdateUserOptions, factors?: {
4022
+ factors?: ReadonlyArray<FactorProof>;
4023
+ sharedDevice?: boolean;
4024
+ }): Promise<void>;
3522
4025
  /**
3523
4026
  * Rotate the DEKs for the given collections in a vault.
3524
4027
  *
@@ -3817,6 +4320,38 @@ declare class Noydb {
3817
4320
  }): Promise<void>;
3818
4321
  /** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
3819
4322
  listAuthenticators(vault: string): Promise<ReadonlyArray<KeyringAuthenticator>>;
4323
+ /**
4324
+ * Mutate the `meta` blob on an existing authenticator slot — slot
4325
+ * rename, label change, attachment of UI hints. The slot's `id`,
4326
+ * `method`, and wrap material (`wrapped_kek` / `wrapped_deks` + `iv`)
4327
+ * are immutable through this method. Anti-slot-swap is structural,
4328
+ * not gate-driven.
4329
+ *
4330
+ * `meta` patch semantics (#57-aligned):
4331
+ * - Top-level merge — absent keys preserved
4332
+ * - `null` value — delete that meta key
4333
+ * - Other values — replace verbatim
4334
+ *
4335
+ * Use case: per-slot nickname for "iPhone Touch ID" vs "MacBook
4336
+ * Touch ID" disambiguation in admin UIs. The slot id (auto-derived
4337
+ * from credentialId prefix) is not human-friendly; `meta.nickname`
4338
+ * is.
4339
+ *
4340
+ * Gated by `update-authenticator`. PERSONAL_POLICY: tier-1 unlock
4341
+ * alone (matches enroll/remove). STRICT_POLICY: tier-1 +
4342
+ * TOTP/email-OTP factor proof — a malicious rename on a shared
4343
+ * workstation could mislead the user about which device a slot
4344
+ * corresponds to, so STRICT requires fresh factor binding.
4345
+ *
4346
+ * @throws `NoAccessError` when no slot with the given id exists.
4347
+ * @throws `ValidationError` when no patch field is provided.
4348
+ *
4349
+ * @see #55
4350
+ */
4351
+ updateAuthenticator(vault: string, slotId: string, options: UpdateAuthenticatorOptions, presented?: {
4352
+ factors?: ReadonlyArray<FactorProof>;
4353
+ sharedDevice?: boolean;
4354
+ }): Promise<void>;
3820
4355
  /**
3821
4356
  * Native WebAuthn enrollment using the **real** internal keyring (#16).
3822
4357
  *
@@ -3968,20 +4503,84 @@ declare class Noydb {
3968
4503
  recoverPassphrase(vault: string, input: RecoverPassphraseInput, factors?: {
3969
4504
  factors?: ReadonlyArray<FactorProof>;
3970
4505
  sharedDevice?: boolean;
4506
+ }): Promise<RecoverPassphraseResult>;
4507
+ /**
4508
+ * Atomic peer-recovery — re-wraps an EXISTING user's keyring under
4509
+ * a fresh temp passphrase in a single store write. Closes #34's
4510
+ * partial-failure window (the previous compose-from-primitives
4511
+ * pattern was `db.revoke + db.grant`, two writes — if the issuer
4512
+ * cancelled between them the target was locked out entirely).
4513
+ *
4514
+ * Different from `db.revoke + db.grant`:
4515
+ *
4516
+ * - Same `userId`, role, permissions, capabilities preserved.
4517
+ * - DEKs unchanged → every other principal in the vault keeps
4518
+ * access. No key rotation.
4519
+ * - Allows owner→owner natively (#33). The existing
4520
+ * `db.revoke` retains its block — peer-recovery is a separate,
4521
+ * intentionally-named operation.
4522
+ * - Tier-2 slots dropped (they wrap the old KEK).
4523
+ *
4524
+ * Gated by `peer-recover-user`; `STRICT_POLICY` requires a
4525
+ * recovery / TOTP / email-OTP factor proof at the moment of
4526
+ * recovery, so the issuer affirmatively re-asserts identity.
4527
+ *
4528
+ * The recipient should call `db.rotatePassphrase` on first session
4529
+ * to choose their own phrase — the temp acts as a single-use
4530
+ * bridge.
4531
+ *
4532
+ * ```ts
4533
+ * await db.recoverUser('acme', {
4534
+ * userId: 'bob',
4535
+ * passphrase: 'temporary-correct-horse-battery-staple-printer',
4536
+ * }, { factors: [{ kind: 'recovery' }] })
4537
+ * // Bob opens createNoydb({ user: 'bob', secret: tempPhrase })
4538
+ * // and immediately calls db.rotatePassphrase to set his own.
4539
+ * ```
4540
+ *
4541
+ * @throws `NoAccessError` when no keyring exists for the target.
4542
+ * @throws `PermissionDeniedError` when the caller's role can't
4543
+ * recover the target's role (admin→owner is blocked even
4544
+ * under recovery).
4545
+ * @throws `PrivilegeEscalationError` when the caller lacks a DEK
4546
+ * the target previously had access to.
4547
+ *
4548
+ * @see #33 #34 — the issues this method closes.
4549
+ */
4550
+ recoverUser(vault: string, options: RecoverUserOptions, factors?: {
4551
+ factors?: ReadonlyArray<FactorProof>;
4552
+ sharedDevice?: boolean;
3971
4553
  }): Promise<void>;
3972
4554
  /**
3973
4555
  * Persist a recovery enrollment. v0.1.0-pre.5 accepts the `'paper'`
3974
- * profile — the developer first calls
3975
- * `@noy-db/on-recovery/generateRecoveryCodeSet` to mint codes +
3976
- * entries, shows the codes to the user once, then hands the entries
3977
- * here.
4556
+ * profile.
4557
+ *
4558
+ * The hub wraps the user's DEK set (not the KEK) under a code-derived
4559
+ * AES-GCM key — see `team/recovery.ts` for the rationale. The mint
4560
+ * helper {@link mintPaperRecoveryEntry} is the canonical primitive;
4561
+ * pair it with `db.getKeyring(vault)` to obtain the live DEK set:
3978
4562
  *
3979
4563
  * ```ts
3980
- * import { generateRecoveryCodeSet } from '@noy-db/on-recovery'
3981
- * const { codes, entries } = await generateRecoveryCodeSet({ kek, count: 10 })
4564
+ * import { mintPaperRecoveryEntry } from '@noy-db/hub'
4565
+ *
4566
+ * const keyring = await db.getKeyring('acme')
4567
+ * const codes: string[] = ['CORRECT-HORSE-1', 'BATTERY-STAPLE-2', ...]
4568
+ * const entries = await Promise.all(
4569
+ * codes.map((code, i) => mintPaperRecoveryEntry(keyring.deks, code, `code-${i}`)),
4570
+ * )
3982
4571
  * await db.enrollRecovery('acme', { profile: 'paper', entries })
3983
4572
  * showCodesToUser(codes)
3984
4573
  * ```
4574
+ *
4575
+ * As of pre.8, `@noy-db/on-recovery`'s `generateRecoveryCodeSet`
4576
+ * delegates to `mintPaperRecoveryEntry` internally — its output is
4577
+ * fed directly to this API. Pick whichever fits your code-gen layer:
4578
+ *
4579
+ * ```ts
4580
+ * import { generateRecoveryCodeSet } from '@noy-db/on-recovery'
4581
+ * const { codes, entries } = await generateRecoveryCodeSet({ deks: keyring.deks, count: 8 })
4582
+ * await db.enrollRecovery('acme', { profile: 'paper', entries })
4583
+ * ```
3985
4584
  */
3986
4585
  enrollRecovery(vault: string, enrollment: {
3987
4586
  profile: 'paper';
@@ -4016,8 +4615,30 @@ declare class Noydb {
4016
4615
  unlockViaPin(vault: string, resume: (state: QuickUnlockState) => Promise<UnlockedKeyring>): Promise<UnlockedKeyring | undefined>;
4017
4616
  /** Drop the tier-3 state for a vault — explicit logout. */
4018
4617
  clearQuickUnlock(vault: string): void;
4019
- /** Get or load the keyring for a vault. */
4020
- private getKeyring;
4618
+ /**
4619
+ * Public accessor for the unlocked keyring of a vault — issue #28.
4620
+ *
4621
+ * Returns the cached `UnlockedKeyring` (already in memory after
4622
+ * `createNoydb` + first vault touch); loads it on demand if absent.
4623
+ * Used by `@noy-db/on-*` ceremonies that need the live DEK set
4624
+ * (paper recovery via {@link mintPaperRecoveryEntry}, tier-3 PIN
4625
+ * enrolment via on-pin's `enrollPin`, custom on-* ceremonies that
4626
+ * don't have a hub-side wrapper).
4627
+ *
4628
+ * No new permission gate — this is an accessor over already-unlocked
4629
+ * state. The keyring is materialized only after the calling session
4630
+ * has unlocked the vault at tier 1, 2, or 3, so exposing it does not
4631
+ * widen access. Throws `ValidationError` when encryption is enabled
4632
+ * and no `secret` / `getKeyring` is configured.
4633
+ *
4634
+ * ```ts
4635
+ * const keyring = await db.getKeyring('acme')
4636
+ * // keyring.deks: Map<collection, CryptoKey>
4637
+ * // keyring.kek: CryptoKey (non-extractable; null for tier-3 sessions)
4638
+ * // keyring.role / .permissions / .authenticators
4639
+ * ```
4640
+ */
4641
+ getKeyring(vault: string): Promise<UnlockedKeyring>;
4021
4642
  }
4022
4643
  /** Create a new NOYDB instance. */
4023
4644
  declare function createNoydb(options: NoydbOptions): Promise<Noydb>;
@@ -4509,6 +5130,24 @@ declare function isMagicLinkGrantExpired(payload: MagicLinkGrantPayload, now?: D
4509
5130
  type DeepPartial<T> = T extends object ? {
4510
5131
  [P in keyof T]?: DeepPartial<T[P]>;
4511
5132
  } : T;
5133
+ /**
5134
+ * Recursive partial with `null` allowed at every level — used by
5135
+ * `updateMe` (#57) to express deletion intent in addition to merge.
5136
+ *
5137
+ * Semantics inside `updateMe`:
5138
+ * - `undefined` (or absent key) — skip; source value preserved
5139
+ * - `null` — delete the key from the resulting envelope
5140
+ * - any other value — overwrite (deep-merge for plain objects,
5141
+ * replace for primitives / arrays)
5142
+ *
5143
+ * Matches lodash `_.merge` behavior on `null` and Firestore's
5144
+ * `FieldValue.delete()` semantics. Loosened from `DeepPartial<T>` per
5145
+ * #57; consumers wanting the original "merge-only" surface can keep
5146
+ * importing `DeepPartial` and avoid passing `null`.
5147
+ */
5148
+ type DeepPartialOrNull<T> = T extends object ? {
5149
+ [P in keyof T]?: DeepPartialOrNull<T[P]> | null;
5150
+ } : T;
4512
5151
  /** Cancel a previously-registered subscription. */
4513
5152
  type Unsubscribe = () => void;
4514
5153
  /**
@@ -4575,11 +5214,22 @@ declare class UserApi {
4575
5214
  * the envelope on first call. Optimistic-concurrency safe — a stale
4576
5215
  * `_v` (parallel writer on another device) throws `ConflictError`.
4577
5216
  *
5217
+ * Patch semantics (#57):
5218
+ * - `undefined` (or omitted key) — skip; existing value preserved
5219
+ * - `null` — delete the field from the merged result
5220
+ * - any other value — overwrite (deep-merge for plain objects,
5221
+ * replace for primitives / arrays)
5222
+ *
5223
+ * To clear a field, pass `null` rather than `undefined`. Callers
5224
+ * with shape `T = string | null` where `null` is a meaningful value
5225
+ * should use `setMe` for that specific field instead — `null` here
5226
+ * always means delete.
5227
+ *
4578
5228
  * Gated by the `edit-own-profile` policy gate (default `minTier: 3`).
4579
5229
  * Pass `presented` to satisfy tightened policies that require a
4580
5230
  * factor proof (e.g. STRICT_POLICY's TOTP requirement).
4581
5231
  */
4582
- updateMe<T extends object = Record<string, unknown>>(patch: DeepPartial<T>, presented?: UserEnvelopePresented): Promise<UserEnvelope<T>>;
5232
+ updateMe<T extends object = Record<string, unknown>>(patch: DeepPartialOrNull<T>, presented?: UserEnvelopePresented): Promise<UserEnvelope<T>>;
4583
5233
  /**
4584
5234
  * Replace the writer's own envelope with `payload`. Use sparingly —
4585
5235
  * `updateMe` is the canonical mutation. No `expectedVersion` check;
@@ -7588,7 +8238,13 @@ type RecoveryEnrollment = {
7588
8238
  *
7589
8239
  * @see docs/subsystems/session-tiers.md → Tier 2 — Authenticate (multi-slot)
7590
8240
  */
7591
- interface KeyringAuthenticator {
8241
+ /**
8242
+ * Shared fields across all authenticator slot variants. The variant
8243
+ * (`KeyringAuthenticatorWrappingKEK` vs `KeyringAuthenticatorWrappingDEKs`)
8244
+ * carries the actual wrapped material; everything below is identity +
8245
+ * metadata only.
8246
+ */
8247
+ interface KeyringAuthenticatorBase {
7592
8248
  /** Caller-chosen identifier — e.g. `'webauthn-yubikey-blue'`, `'oidc-google'`, `'password-daily'`. */
7593
8249
  readonly id: string;
7594
8250
  /** Method family — selects which `@noy-db/on-*` package handles unlock. */
@@ -7600,8 +8256,6 @@ interface KeyringAuthenticator {
7600
8256
  * tier 2 may add a sibling slot when the active policy permits.
7601
8257
  */
7602
8258
  readonly enrolled_via_tier: 1 | 2;
7603
- /** Base64 wrapped-KEK ciphertext under the method-derived key. */
7604
- readonly wrapped_kek: string;
7605
8259
  /**
7606
8260
  * Method-specific metadata: WebAuthn cred id, OIDC issuer/sub, PBKDF2
7607
8261
  * salt for `on-password`, etc. The schema is open by design — the
@@ -7609,6 +8263,63 @@ interface KeyringAuthenticator {
7609
8263
  */
7610
8264
  readonly meta: Record<string, unknown>;
7611
8265
  }
8266
+ /**
8267
+ * Slot that wraps the KEK directly under a method-derived AES-KW key.
8268
+ * Used by ceremonies where the on-* package can produce/recover an
8269
+ * extractable KEK from its own credential — WebAuthn (PRF-derived
8270
+ * wrapping key) and split-key OIDC.
8271
+ *
8272
+ * `wrapKind` is optional/absent on slots written before pre.8 — those
8273
+ * legacy slots are treated as wrap-KEK by default at unlock time.
8274
+ */
8275
+ interface KeyringAuthenticatorWrappingKEK extends KeyringAuthenticatorBase {
8276
+ readonly wrapKind?: 'kek';
8277
+ /** Base64 wrapped-KEK ciphertext under the method-derived key. */
8278
+ readonly wrapped_kek: string;
8279
+ /** XOR guard — wrap-KEK slots must NOT carry wrap-DEKs material. */
8280
+ readonly wrapped_deks?: never;
8281
+ /** XOR guard — wrap-KEK slots must NOT carry wrap-DEKs material. */
8282
+ readonly iv?: never;
8283
+ }
8284
+ /**
8285
+ * Slot that wraps the DEK set (not the KEK) under a method-derived
8286
+ * AES-GCM key — sidesteps the non-extractable-KEK constraint by
8287
+ * encrypting the serialized `{ deks: { collection: rawDekBase64 } }`
8288
+ * directly. Mirrors the format used by `mintPaperRecoveryEntry`
8289
+ * (`PaperRecoveryEntry`) and `@noy-db/on-pin`'s `PinResumeState` —
8290
+ * the unified wrap-DEKs primitive across tier-0 / tier-2 / tier-3.
8291
+ *
8292
+ * Trade-off: a slot of this kind reconstructs `UnlockedKeyring` with
8293
+ * `kek: null` after unlock. That is semantically correct for tier-2
8294
+ * (sensitive ops like `enrollAuthenticator` / `rotatePassphrase`
8295
+ * require a tier-1 unlock anyway) and matches how `@noy-db/on-pin`
8296
+ * already behaves at tier 3.
8297
+ *
8298
+ * @see `mintPaperRecoveryEntry` in `team/recovery.ts` — same shape on
8299
+ * a different on-disk path (`_meta/recovery-paper`).
8300
+ */
8301
+ interface KeyringAuthenticatorWrappingDEKs extends KeyringAuthenticatorBase {
8302
+ readonly wrapKind: 'deks';
8303
+ /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */
8304
+ readonly wrapped_deks: string;
8305
+ /** Base64 AES-GCM IV used for the `wrapped_deks` ciphertext. */
8306
+ readonly iv: string;
8307
+ /** XOR guard — wrap-DEKs slots must NOT carry wrap-KEK material. */
8308
+ readonly wrapped_kek?: never;
8309
+ }
8310
+ /**
8311
+ * Discriminated union over the two wrap-format variants. Reads from
8312
+ * disk should always go through this type so the variant is preserved.
8313
+ *
8314
+ * Discriminator: `wrapKind`. Absent → wrap-KEK (legacy / WebAuthn /
8315
+ * OIDC). Present and `'deks'` → wrap-DEKs (password / future on-* that
8316
+ * want to sidestep extractable-KEK).
8317
+ *
8318
+ * The type-level XOR enforces "exactly one of `wrapped_kek` /
8319
+ * `wrapped_deks` is present" — a structural guarantee that the runtime
8320
+ * dispatch is safe.
8321
+ */
8322
+ type KeyringAuthenticator = KeyringAuthenticatorWrappingKEK | KeyringAuthenticatorWrappingDEKs;
7612
8323
  interface KeyringFile {
7613
8324
  readonly _noydb_keyring: typeof NOYDB_KEYRING_VERSION;
7614
8325
  readonly user_id: string;
@@ -8014,6 +8725,39 @@ interface GrantOptions {
8014
8725
  */
8015
8726
  readonly initialProfile?: unknown;
8016
8727
  }
8728
+ /**
8729
+ * Caller payload for `db.updateUser` (#54). Mutate one or more
8730
+ * identity fields on an existing keyring without rotating any keys.
8731
+ *
8732
+ * `role`, `displayName`, and `permissions` live in the plaintext header
8733
+ * of `_keyring/<userId>` (the sync engine reads them without keys).
8734
+ * Mutating them is a JSON header swap — no DEK rewrap, no KEK
8735
+ * required, no authenticator slots touched. Tier-2 slots and recovery
8736
+ * enrollments survive unchanged. Last-write-wins through the existing
8737
+ * keyring put (same concurrency story as `db.grant` / `db.revoke`).
8738
+ *
8739
+ * Top-level fields are partial-merge: absent fields are not modified.
8740
+ * `permissions`, however, is a **full replacement** at the map level —
8741
+ * passing `{ invoices: 'rw' }` REPLACES the entire permissions map,
8742
+ * silently dropping any other entries. To partially update, read the
8743
+ * current keyring and merge: `permissions: { ...current, invoices: 'rw' }`.
8744
+ * To clear all permissions, pass `permissions: {}` explicitly.
8745
+ *
8746
+ * Role-elevation guard: the same hierarchy as `db.grant`. Admins can
8747
+ * change `admin` / `operator` / `viewer` / `client` to and from each
8748
+ * other; admins cannot promote to or demote from `owner`. Owners can
8749
+ * do anything. Non-admin callers (operator/viewer/client) cannot call
8750
+ * `db.updateUser` at all — for self-displayName changes, use
8751
+ * `vault.user.updateMe` (the user-envelope API).
8752
+ *
8753
+ * @see #54
8754
+ */
8755
+ interface UpdateUserOptions {
8756
+ readonly userId: string;
8757
+ readonly role?: Role;
8758
+ readonly displayName?: string;
8759
+ readonly permissions?: Permissions;
8760
+ }
8017
8761
  interface RevokeOptions {
8018
8762
  readonly userId: string;
8019
8763
  readonly rotateKeys?: boolean;
@@ -8830,4 +9574,4 @@ interface DeleteManyResult {
8830
9574
  }>;
8831
9575
  }
8832
9576
 
8833
- export { type ConsentAuditEntry as $, type BlobObject as A, type BlobStrategy as B, type BlobPutOptions as C, DICT_COLLECTION_PREFIX as D, type BlobResponseOptions as E, BlobSet as F, type BlobStrategyOpenArgs as G, type CompactRunOptions as H, type I18nStrategy as I, type CompactionContext as J, type CompactionResult as K, DEFAULT_CHUNK_SIZE as L, EXPORT_AUDIT_COLLECTION as M, ExportBlobsAbortedError as N, type ExportBlobsAuditEntry as O, PolicyEnforcer as P, type ExportBlobsHandle as Q, type ExportBlobsOptions as R, type SessionStrategy as S, type ExportedBlob as T, type SlotInfo as U, type SlotRecord as V, type VersionRecord as W, createExportBlobsHandle as X, runCompaction as Y, type ConsentStrategy as Z, CONSENT_AUDIT_COLLECTION as _, type DictEntry as a, type BuiltInGateName as a$, type ConsentAuditFilter as a0, type ConsentContext as a1, type ConsentOp as a2, loadConsentEntries as a3, writeConsentEntry as a4, type PeriodsStrategy as a5, type CarryForwardContext as a6, type ClosePeriodOptions as a7, type OpenPeriodOptions as a8, PERIODS_COLLECTION as a9, type DiffEntry as aA, type JsonPatch as aB, type JsonPatchOp as aC, type LedgerEntry as aD, LedgerStore as aE, type VaultEngine as aF, VaultInstant as aG, type VerifyResult as aH, applyPatch as aI, canonicalJson as aJ, computePatch as aK, diff as aL, formatDiff as aM, hashEntry as aN, paddedIndex as aO, parseIndex as aP, sha256Hex as aQ, type UserEnvelope as aR, type PublicEnvelope as aS, type GateName as aT, type GatePolicy as aU, type VaultPolicy as aV, type ActiveTier as aW, type FactorProof as aX, Vault as aY, type AccessibleVault as aZ, BUNDLE_STORE_POLICY as a_, type PeriodRecord as aa, type ReadOnlyCollection as ab, appendPeriodLedgerEntry as ac, assertTsWritable as ad, chainAnchor as ae, loadPeriods as af, validatePeriodName as ag, type ShadowStrategy as ah, CollectionFrame as ai, VaultFrame as aj, type TxStrategy as ak, TxCollection as al, TxContext as am, TxVault as an, runTransaction as ao, type SyncStrategy as ap, type Role as aq, type UnlockedKeyring as ar, type HistoryStrategy as as, type NoydbStore as at, type HistoryOptions as au, type EncryptedEnvelope as av, type PruneOptions as aw, type AppendInput as ax, type ChangeType as ay, CollectionInstant as az, type DictKeyDescriptor as b, type Permissions as b$, type BundleRecipient as b0, type CacheOptions as b1, type CacheStats as b2, type ChangeEvent as b3, Collection as b4, type CollectionChangeEvent as b5, type CollectionConflictResolver as b6, type Conflict as b7, type ConflictPolicy as b8, type ConflictStrategy as b9, type KeyringFile as bA, type ListAccessibleVaultsOptions as bB, type ListPageResult as bC, type LiveUserEnvelope as bD, type LocaleReadOptions as bE, Lru as bF, type LruOptions as bG, type LruStats as bH, MAGIC_LINK_CONTENT_INFO_PREFIX as bI, MAGIC_LINK_GRANTS_COLLECTION as bJ, MAGIC_LINK_KEK_INFO_PREFIX as bK, type MagicLinkGrantPayload as bL, type MagicLinkGrantRecord as bM, NOYDB_BACKUP_VERSION as bN, NOYDB_FORMAT_VERSION as bO, NOYDB_KEYRING_VERSION as bP, NOYDB_SYNC_VERSION as bQ, Noydb as bR, type NoydbBundleStore as bS, type NoydbEventMap as bT, type NoydbOptions as bU, PUBLIC_ENVELOPE_FIELDS as bV, type PaperRecoveryDoc as bW, type PaperRecoveryEntry as bX, type PassphrasePolicy as bY, type PassphraseValidationResult as bZ, type Permission as b_, type CrossTierAccessEvent as ba, DEFAULT_PUBLIC_ENVELOPE_SCHEMA as bb, DELEGATIONS_COLLECTION as bc, type DeepPartial as bd, type DelegationToken as be, type DeleteManyResult as bf, type DirtyEntry as bg, ELEVATION_AUDIT_COLLECTION as bh, ElevatedHandle as bi, type EnrollAuthenticatorOptions as bj, type ExportCapability as bk, type ExportChunk as bl, type ExportFormat as bm, type ExportStreamOptions as bn, type FactorKind as bo, type FactorRequirement as bp, type GhostRecord as bq, type GrantOptions as br, type HistoryConfig as bs, type HistoryEntry as bt, INDEXED_STORE_POLICY as bu, type ImportCapability as bv, type InferOutput as bw, type IssueDelegationOptions as bx, type IssueMagicLinkGrantOptions as by, type KeyringAuthenticator as bz, DictionaryHandle as c, assertStrongPassphrase as c$, type PlaintextTranslatorContext as c0, type PlaintextTranslatorFn as c1, PresenceHandle as c2, type PresencePeer as c3, type PublicEnvelopeField as c4, type PublicEnvelopeSchema as c5, type PublicEnvelopeText as c6, type PullMode as c7, type PullOptions as c8, type PullPolicy as c9, SyncEngine as cA, type SyncMetadata as cB, type SyncPolicy as cC, SyncScheduler as cD, type SyncSchedulerStatus as cE, type SyncStatus as cF, type SyncTarget as cG, type SyncTargetRole as cH, SyncTransaction as cI, type SyncTransactionResult as cJ, type TierMode as cK, type TranslatorAuditEntry as cL, type TxOp as cM, USER_ENVELOPE_COLLECTION as cN, USER_ENVELOPE_MAX_BYTES as cO, type Unsubscribe as cP, UserApi as cQ, type UserEnvelopeCheckGate as cR, UserEnvelopeOversizedError as cS, type UserEnvelopePresented as cT, type UserInfo as cU, type VaultBackup as cV, type VaultPolicyOnDisk as cW, type VaultSnapshot as cX, type WarningRules as cY, WeakPassphraseError as cZ, type WeakPassphraseReason as c_, type PullResult as ca, type PushMode as cb, type PushOptions as cc, type PushPolicy as cd, type PushResult as ce, type PutManyItemOptions as cf, type PutManyOptions as cg, type PutManyResult as ch, type QueryAcrossOptions as ci, type QueryAcrossResult as cj, type QuickUnlockState as ck, QuickUnlockStore as cl, type ReAuthOperation as cm, type RecoverPassphraseInput as cn, type RecoveryProof as co, type ResolvedPublicEnvelopeSchema as cp, type RevokeOptions as cq, type RotatePassphraseInput as cr, type SessionPolicy as cs, type SetPublicEnvelopeInput as ct, type StandardSchemaV1 as cu, type StandardSchemaV1Issue as cv, type StandardSchemaV1SyncResult as cw, type StoreAuth as cx, type StoreAuthKind as cy, type StoreCapabilities as cz, type DictionaryOptions as d, buildRecipientKeyringFile as d0, burnPaperRecoveryEntry as d1, createNoydb as d2, createStore as d3, deriveMagicLinkContentKey as d4, enrollAuthenticator as d5, estimateEntropy as d6, evaluateExportCapability as d7, evaluateImportCapability as d8, findAuthenticator as d9, writeMagicLinkGrant as dA, hasExportCapability as da, hasImportCapability as db, hasRecoveryEnrolled as dc, isMagicLinkGrantExpired as dd, isPublicEnvelope as de, issueDelegation as df, recoverPassphrase as dg, rotatePassphrase as dh, listMagicLinkGrants as di, listUsers as dj, listUsersWithEnvelopes as dk, loadActiveDelegations as dl, loadPaperRecoveryEntries as dm, magicLinkGrantRecordId as dn, readMagicLinkGrantRecord as dp, removeAuthenticator as dq, resolveSchema as dr, revokeDelegation as ds, revokeMagicLinkGrant as dt, savePaperRecoveryEntries as du, unwrapMagicLinkGrant as dv, validatePassphrase as dw, validatePublicEnvelopeInput as dx, validateSchemaInput as dy, validateSchemaOutput as dz, type I18nTextDescriptor as e, type I18nTextOptions as f, applyI18nLocale as g, dictCollectionName as h, dictKey as i, i18nText as j, isDictCollectionName as k, isDictKeyDescriptor as l, isI18nTextDescriptor as m, createEnforcer as n, validateSessionPolicy as o, BLOB_CHUNKS_COLLECTION as p, BLOB_COLLECTION as q, resolveI18nText as r, BLOB_EVICTION_AUDIT_COLLECTION as s, BLOB_INDEX_COLLECTION as t, BLOB_SLOTS_PREFIX as u, validateI18nTextValue as v, BLOB_VERSIONS_PREFIX as w, type BlobEvictionEntry as x, type BlobFieldPolicy as y, type BlobFieldsConfig as z };
9577
+ export { type ConsentAuditEntry as $, type BlobObject as A, type BlobStrategy as B, type BlobPutOptions as C, DICT_COLLECTION_PREFIX as D, type BlobResponseOptions as E, BlobSet as F, type BlobStrategyOpenArgs as G, type CompactRunOptions as H, type I18nStrategy as I, type CompactionContext as J, type CompactionResult as K, DEFAULT_CHUNK_SIZE as L, EXPORT_AUDIT_COLLECTION as M, ExportBlobsAbortedError as N, type ExportBlobsAuditEntry as O, PolicyEnforcer as P, type ExportBlobsHandle as Q, type ExportBlobsOptions as R, type SessionStrategy as S, type ExportedBlob as T, type SlotInfo as U, type SlotRecord as V, type VersionRecord as W, createExportBlobsHandle as X, runCompaction as Y, type ConsentStrategy as Z, CONSENT_AUDIT_COLLECTION as _, type DictEntry as a, type BuiltInGateName as a$, type ConsentAuditFilter as a0, type ConsentContext as a1, type ConsentOp as a2, loadConsentEntries as a3, writeConsentEntry as a4, type PeriodsStrategy as a5, type CarryForwardContext as a6, type ClosePeriodOptions as a7, type OpenPeriodOptions as a8, PERIODS_COLLECTION as a9, type DiffEntry as aA, type JsonPatch as aB, type JsonPatchOp as aC, type LedgerEntry as aD, LedgerStore as aE, type VaultEngine as aF, VaultInstant as aG, type VerifyResult as aH, applyPatch as aI, canonicalJson as aJ, computePatch as aK, diff as aL, formatDiff as aM, hashEntry as aN, paddedIndex as aO, parseIndex as aP, sha256Hex as aQ, type UserEnvelope as aR, type PublicEnvelope as aS, type GateName as aT, type GatePolicy as aU, type VaultPolicy as aV, type ActiveTier as aW, type FactorProof as aX, Vault as aY, type AccessibleVault as aZ, BUNDLE_STORE_POLICY as a_, type PeriodRecord as aa, type ReadOnlyCollection as ab, appendPeriodLedgerEntry as ac, assertTsWritable as ad, chainAnchor as ae, loadPeriods as af, validatePeriodName as ag, type ShadowStrategy as ah, CollectionFrame as ai, VaultFrame as aj, type TxStrategy as ak, TxCollection as al, TxContext as am, TxVault as an, runTransaction as ao, type SyncStrategy as ap, type Role as aq, type UnlockedKeyring as ar, type HistoryStrategy as as, type NoydbStore as at, type HistoryOptions as au, type EncryptedEnvelope as av, type PruneOptions as aw, type AppendInput as ax, type ChangeType as ay, CollectionInstant as az, type DictKeyDescriptor as b, type Permission as b$, type BundleRecipient as b0, type CacheOptions as b1, type CacheStats as b2, type ChangeEvent as b3, Collection as b4, type CollectionChangeEvent as b5, type CollectionConflictResolver as b6, type Conflict as b7, type ConflictPolicy as b8, type ConflictStrategy as b9, type KeyringAuthenticator as bA, type KeyringFile as bB, type ListAccessibleVaultsOptions as bC, type ListPageResult as bD, type LiveUserEnvelope as bE, type LocaleReadOptions as bF, Lru as bG, type LruOptions as bH, type LruStats as bI, MAGIC_LINK_CONTENT_INFO_PREFIX as bJ, MAGIC_LINK_GRANTS_COLLECTION as bK, MAGIC_LINK_KEK_INFO_PREFIX as bL, type MagicLinkGrantPayload as bM, type MagicLinkGrantRecord as bN, NOYDB_BACKUP_VERSION as bO, NOYDB_FORMAT_VERSION as bP, NOYDB_KEYRING_VERSION as bQ, NOYDB_SYNC_VERSION as bR, Noydb as bS, type NoydbBundleStore as bT, type NoydbEventMap as bU, type NoydbOptions as bV, PUBLIC_ENVELOPE_FIELDS as bW, type PaperRecoveryDoc as bX, type PaperRecoveryEntry as bY, type PassphrasePolicy as bZ, type PassphraseValidationResult as b_, type CrossTierAccessEvent as ba, DEFAULT_PUBLIC_ENVELOPE_SCHEMA as bb, DELEGATIONS_COLLECTION as bc, type DeepPartial as bd, type DeepPartialOrNull as be, type DelegationToken as bf, type DeleteManyResult as bg, type DirtyEntry as bh, ELEVATION_AUDIT_COLLECTION as bi, ElevatedHandle as bj, type EnrollAuthenticatorOptions as bk, type ExportCapability as bl, type ExportChunk as bm, type ExportFormat as bn, type ExportStreamOptions as bo, type FactorKind as bp, type FactorRequirement as bq, type GhostRecord as br, type GrantOptions as bs, type HistoryConfig as bt, type HistoryEntry as bu, INDEXED_STORE_POLICY as bv, type ImportCapability as bw, type InferOutput as bx, type IssueDelegationOptions as by, type IssueMagicLinkGrantOptions as bz, DictionaryHandle as c, type UserInfo as c$, type Permissions as c0, type PlaintextTranslatorContext as c1, type PlaintextTranslatorFn as c2, PresenceHandle as c3, type PresencePeer as c4, type PublicEnvelopeField as c5, type PublicEnvelopeSchema as c6, type PublicEnvelopeText as c7, type PullMode as c8, type PullOptions as c9, type StandardSchemaV1Issue as cA, type StandardSchemaV1SyncResult as cB, type StoreAuth as cC, type StoreAuthKind as cD, type StoreCapabilities as cE, SyncEngine as cF, type SyncMetadata as cG, type SyncPolicy as cH, SyncScheduler as cI, type SyncSchedulerStatus as cJ, type SyncStatus as cK, type SyncTarget as cL, type SyncTargetRole as cM, SyncTransaction as cN, type SyncTransactionResult as cO, type TierMode as cP, type TranslatorAuditEntry as cQ, type TxOp as cR, USER_ENVELOPE_COLLECTION as cS, USER_ENVELOPE_MAX_BYTES as cT, type Unsubscribe as cU, type UpdateAuthenticatorOptions as cV, type UpdateUserOptions as cW, UserApi as cX, type UserEnvelopeCheckGate as cY, UserEnvelopeOversizedError as cZ, type UserEnvelopePresented as c_, type PullPolicy as ca, type PullResult as cb, type PushMode as cc, type PushOptions as cd, type PushPolicy as ce, type PushResult as cf, type PutManyItemOptions as cg, type PutManyOptions as ch, type PutManyResult as ci, type QueryAcrossOptions as cj, type QueryAcrossResult as ck, type QuickUnlockState as cl, QuickUnlockStore as cm, type ReAuthOperation as cn, type RecoverPassphraseInput as co, type RecoverPassphraseResult as cp, type RecoverUserOptions as cq, type RecoveryProof as cr, type ResolvedPublicEnvelopeSchema as cs, type RevokeOptions as ct, type RotatePassphraseInput as cu, type SessionPolicy as cv, type SetPublicEnvelopeInput as cw, type SlotRewrapCeremony as cx, type SlotRewrapContext as cy, type StandardSchemaV1 as cz, type DictionaryOptions as d, type VaultBackup as d0, type VaultPolicyOnDisk as d1, type VaultSnapshot as d2, type WarningRules as d3, WeakPassphraseError as d4, type WeakPassphraseReason as d5, type WrappedDeksBlob as d6, assertStrongPassphrase as d7, buildRecipientKeyringFile as d8, burnPaperRecoveryEntry as d9, recoverUser as dA, removeAuthenticator as dB, resolveSchema as dC, revokeDelegation as dD, revokeMagicLinkGrant as dE, savePaperRecoveryEntries as dF, unwrapDeksFromBlob as dG, unwrapDeksFromPaperEntry as dH, unwrapMagicLinkGrant as dI, validatePassphrase as dJ, validatePublicEnvelopeInput as dK, validateSchemaInput as dL, validateSchemaOutput as dM, writeMagicLinkGrant as dN, createNoydb as da, createStore as db, deriveMagicLinkContentKey as dc, enrollAuthenticator as dd, estimateEntropy as de, evaluateExportCapability as df, evaluateImportCapability as dg, findAuthenticator as dh, hasExportCapability as di, hasImportCapability as dj, hasRecoveryEnrolled as dk, isMagicLinkGrantExpired as dl, isPublicEnvelope as dm, issueDelegation as dn, recoverPassphrase as dp, rotatePassphrase as dq, listMagicLinkGrants as dr, listUsers as ds, listUsersWithEnvelopes as dt, loadActiveDelegations as du, loadPaperRecoveryEntries as dv, magicLinkGrantRecordId as dw, mintPaperRecoveryEntry as dx, mintWrappedDeksBlob as dy, readMagicLinkGrantRecord as dz, type I18nTextDescriptor as e, type I18nTextOptions as f, applyI18nLocale as g, dictCollectionName as h, dictKey as i, i18nText as j, isDictCollectionName as k, isDictKeyDescriptor as l, isI18nTextDescriptor as m, createEnforcer as n, validateSessionPolicy as o, BLOB_CHUNKS_COLLECTION as p, BLOB_COLLECTION as q, resolveI18nText as r, BLOB_EVICTION_AUDIT_COLLECTION as s, BLOB_INDEX_COLLECTION as t, BLOB_SLOTS_PREFIX as u, validateI18nTextValue as v, BLOB_VERSIONS_PREFIX as w, type BlobEvictionEntry as x, type BlobFieldPolicy as y, type BlobFieldsConfig as z };