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

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-NZ4XCIKS.js → chunk-2WGMYBYS.js} +3 -3
  9. package/dist/{chunk-3WCRU7TI.js → chunk-7XBQS42M.js} +2 -2
  10. package/dist/{chunk-CL37QSND.js → chunk-HC7Z5EQZ.js} +2 -2
  11. package/dist/{chunk-B6HF6NTZ.js → chunk-PJK6IOBC.js} +1 -1
  12. package/dist/chunk-PJK6IOBC.js.map +1 -0
  13. package/dist/{chunk-KPF2HHPI.js → chunk-R2ZTGEVP.js} +2 -2
  14. package/dist/{chunk-GILMPJXB.js → chunk-RSPLI376.js} +2 -2
  15. package/dist/{chunk-XCL3WP6J.js → chunk-SCZXXXU4.js} +2 -1
  16. package/dist/{chunk-XCL3WP6J.js.map → chunk-SCZXXXU4.js.map} +1 -1
  17. package/dist/{chunk-INSJBB5W.js → chunk-TOQK4KAN.js} +3 -3
  18. package/dist/{chunk-UFL4DUEV.js → chunk-VQBTTTUN.js} +1 -1
  19. package/dist/chunk-VQBTTTUN.js.map +1 -0
  20. package/dist/{chunk-FAAWLVTF.js → chunk-WN6UK7PM.js} +2 -2
  21. package/dist/{chunk-N2LMZKLR.js → chunk-Y4CMTMUW.js} +2 -2
  22. package/dist/{chunk-6IJQ27XN.js → chunk-YVFTBQHL.js} +14 -4
  23. package/dist/chunk-YVFTBQHL.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-Dk14V6lX.d.cts → dev-unlock-BZKx666y.d.cts} +1 -1
  28. package/dist/{dev-unlock-CcJ1qIi7.d.ts → dev-unlock-BygpnIWe.d.ts} +1 -1
  29. package/dist/{hash-1Xsqx1jl.d.ts → hash-B0eU2Qv9.d.ts} +1 -1
  30. package/dist/{hash-h_2U3TFb.d.cts → hash-CIyfmKsg.d.cts} +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-Dp4tKCjX.d.ts} +1 -1
  41. package/dist/{index-Cvb0efA_.d.cts → index-DsVbTDZI.d.cts} +1 -1
  42. package/dist/index.cjs +384 -57
  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 +377 -72
  47. package/dist/index.js.map +1 -1
  48. package/dist/{ledger-5V67MAIL.js → ledger-UQIMMKO5.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-3QTQADDW.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-D-6bmD2c.d.ts → types-DD9eKKNc.d.ts} +644 -72
  74. package/dist/{types-D3QLmhlk.d.cts → types-arFMsCtn.d.cts} +644 -72
  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-NZ4XCIKS.js.map → chunk-2WGMYBYS.js.map} +0 -0
  80. /package/dist/{chunk-3WCRU7TI.js.map → chunk-7XBQS42M.js.map} +0 -0
  81. /package/dist/{chunk-CL37QSND.js.map → chunk-HC7Z5EQZ.js.map} +0 -0
  82. /package/dist/{chunk-KPF2HHPI.js.map → chunk-R2ZTGEVP.js.map} +0 -0
  83. /package/dist/{chunk-GILMPJXB.js.map → chunk-RSPLI376.js.map} +0 -0
  84. /package/dist/{chunk-INSJBB5W.js.map → chunk-TOQK4KAN.js.map} +0 -0
  85. /package/dist/{chunk-FAAWLVTF.js.map → chunk-WN6UK7PM.js.map} +0 -0
  86. /package/dist/{chunk-N2LMZKLR.js.map → chunk-Y4CMTMUW.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-UQIMMKO5.js.map} +0 -0
  89. /package/dist/{public-envelope-DFJZHXVH.js.map → public-envelope-3QTQADDW.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,68 @@ 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
+ * Drop a slot by id. No-op if the slot doesn't exist (idempotent —
3001
+ * removing a non-existent slot is a recoverable retry, not an error).
3002
+ */
3003
+ declare function removeAuthenticator(store: NoydbStore, vault: string, keyring: UnlockedKeyring, slotId: string): Promise<UnlockedKeyring>;
3004
+ /**
3005
+ * Look up a slot by id. Returns `undefined` when no slot matches.
3006
+ * Used by tier-2 unlock dispatchers to fetch the wrapped KEK + meta
3007
+ * before invoking the method-specific verifier.
3008
+ */
3009
+ declare function findAuthenticator(keyring: UnlockedKeyring, slotId: string): KeyringAuthenticator | undefined;
3010
+
2885
3011
  /**
2886
3012
  * Tier-1 change flows — `rotatePassphrase` (user remembers old) and
2887
3013
  * `recoverPassphrase` (user supplies a recovery proof). Issue #10.
@@ -2901,24 +3027,86 @@ declare class SyncEngine {
2901
3027
  * @module
2902
3028
  */
2903
3029
 
3030
+ /**
3031
+ * Context handed to a {@link SlotRewrapCeremony} when `rotatePassphrase`
3032
+ * preserves a tier-2 slot. The ceremony's job is to re-derive its
3033
+ * method-specific wrapping material (PRF assertion, PBKDF2 of a
3034
+ * daily-password, etc.) and wrap the freshly rewrapped DEK set under
3035
+ * the new wrapping key.
3036
+ *
3037
+ * Two surfaces are exposed:
3038
+ *
3039
+ * - `newDeks` — the rewrapped (extractable) DEK set the slot will
3040
+ * wrap. This is what `mintPaperRecoveryEntry` / `enrollPassword-
3041
+ * Authenticator` / `wrapKeyringSummary` (in `@noy-db/on-webauthn`)
3042
+ * all consume; effectively the canonical input for every
3043
+ * post-Path C tier-2 ceremony.
3044
+ *
3045
+ * - `newKek` — the freshly-derived KEK (extractable for the
3046
+ * ceremony scope only). Only relevant for forward-compatibility
3047
+ * with a hypothetical future on-* package that wants to wrap the
3048
+ * KEK itself under a method-derived key. None of the shipped
3049
+ * on-* packages need this; they all operate on `newDeks`.
3050
+ *
3051
+ * The ceremony MUST preserve `oldSlot.id` and `oldSlot.method` in the
3052
+ * returned `EnrollAuthenticatorOptions`. Hub validates these — a
3053
+ * mismatch throws `ValidationError` (prevents slot-type swap mid-
3054
+ * rotation, e.g. converting a webauthn slot to a password slot under
3055
+ * cover of preservation).
3056
+ */
3057
+ interface SlotRewrapContext {
3058
+ readonly newKek: CryptoKey;
3059
+ readonly newDeks: Map<string, CryptoKey>;
3060
+ readonly oldSlot: KeyringAuthenticator;
3061
+ }
3062
+ /**
3063
+ * Callback that re-enrolls one tier-2 slot during `rotatePassphrase`.
3064
+ * Returns the new slot's `EnrollAuthenticatorOptions` — same shape
3065
+ * the consumer would pass to `db.enrollAuthenticator` for a fresh
3066
+ * enrollment. Hub persists the result atomically with the rotation.
3067
+ */
3068
+ type SlotRewrapCeremony = (ctx: SlotRewrapContext) => Promise<EnrollAuthenticatorOptions>;
2904
3069
  /** Caller payload for {@link rotatePassphrase}. */
2905
3070
  interface RotatePassphraseInput {
2906
3071
  readonly oldPassphrase: string;
2907
3072
  readonly newPassphrase: string;
2908
3073
  readonly passphrasePolicy?: PassphrasePolicy;
2909
3074
  readonly allowWeakPassphrase?: boolean;
3075
+ /**
3076
+ * Map of slot id → re-enrolment ceremony. Slots whose id appears
3077
+ * here are PRESERVED across rotation (the ceremony re-derives the
3078
+ * method-specific wrapping under the new keyring); slots whose id
3079
+ * is absent are DROPPED (the pre-#29 behavior).
3080
+ *
3081
+ * Without this map, `rotatePassphrase` retains the pre-pre.8
3082
+ * behavior of wiping every tier-2 slot. Consumers building a
3083
+ * "rotate without losing my biometric" flow supply ceremonies for
3084
+ * each slot they want to keep.
3085
+ *
3086
+ * If a ceremony throws, the entire rotation throws — no partial
3087
+ * state. Callers wrap individual ceremonies in try/catch + return
3088
+ * a sentinel if they want graceful degradation per slot.
3089
+ *
3090
+ * Added in pre.8 (#29).
3091
+ */
3092
+ readonly slotCeremonies?: {
3093
+ readonly [slotId: string]: SlotRewrapCeremony;
3094
+ };
2910
3095
  }
2911
3096
  /**
2912
3097
  * Re-derive the user's KEK from `oldPassphrase`, rewrap every DEK
2913
3098
  * under a freshly-derived KEK from `newPassphrase`, and persist.
2914
3099
  *
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.
3100
+ * Tier-2 authenticator slots are dropped UNLESS the caller supplies
3101
+ * a `slotCeremonies` map (#29) each ceremony re-derives its
3102
+ * method-specific wrapping under the new keyring, and hub persists
3103
+ * the rewrapped slots atomically with the rotation. Slots whose id
3104
+ * isn't in the map are still dropped (pre-pre.8 behavior).
2919
3105
  *
2920
3106
  * @throws `InvalidKeyError` if `oldPassphrase` does not unwrap the keyring.
2921
3107
  * @throws `WeakPassphraseError` if `newPassphrase` fails the strength rule.
3108
+ * @throws `ValidationError` if a ceremony's result mismatches the
3109
+ * slot's id or method (anti-slot-swap guard).
2922
3110
  */
2923
3111
  declare function rotatePassphrase(store: NoydbStore, vault: string, userId: string, input: RotatePassphraseInput): Promise<UnlockedKeyring>;
2924
3112
  /** Caller payload for {@link recoverPassphrase}. */
@@ -2949,6 +3137,58 @@ interface RecoverPassphraseInput {
2949
3137
  readonly recoveryProof: RecoveryProof;
2950
3138
  readonly passphrasePolicy?: PassphrasePolicy;
2951
3139
  readonly allowWeakPassphrase?: boolean;
3140
+ /**
3141
+ * After a successful paper-recovery, replace ALL remaining recovery
3142
+ * entries with freshly-minted ones. Defaults to `true` (defensive).
3143
+ *
3144
+ * Rationale (issue #36): the user just demonstrated they had access
3145
+ * to AT LEAST one code. The remaining codes from the same printed
3146
+ * sheet may also be compromised — photographed, leaked via a
3147
+ * screen-share slip, or in the hands of whoever stole the sheet.
3148
+ * Auto-rotation closes the window without requiring consumer action.
3149
+ *
3150
+ * Set to `false` to preserve the original behavior (only the matched
3151
+ * code is burned; the rest stay valid).
3152
+ *
3153
+ * Hub-side orchestration is non-atomic with the recovery itself:
3154
+ * if the rotation step fails after a successful burn, the user
3155
+ * falls back to the pre-rotation state (remaining codes still
3156
+ * valid). Strictly safer than the previous default — a failed
3157
+ * rotation degrades gracefully rather than leaving the vault
3158
+ * locked or codes dual-existing.
3159
+ */
3160
+ readonly rotateRemainingCodes?: boolean;
3161
+ /**
3162
+ * Number of fresh codes to mint when `rotateRemainingCodes` is on.
3163
+ * Defaults to the count of remaining entries POST-burn (e.g. if
3164
+ * the user enrolled 8 originally and just consumed 1, defaults to
3165
+ * 7). Pass an explicit number to mint a different count — useful
3166
+ * when the consumer wants to refresh to a target N regardless of
3167
+ * how many were left.
3168
+ */
3169
+ readonly newCodeCount?: number;
3170
+ /**
3171
+ * Override the default raw-code generator. The default is hub's
3172
+ * {@link generateULID} — uppercase Crockford-Base32, 26 chars,
3173
+ * passes through `normalizePaperCode` untouched.
3174
+ *
3175
+ * Pass `() => generateRawCode()` from `@noy-db/on-recovery` when
3176
+ * the consumer prefers the Base32 + checksum format with hyphenated
3177
+ * display. The `mintPaperRecoveryEntry` helper accepts any string —
3178
+ * the generator just needs to produce a high-entropy unique value.
3179
+ */
3180
+ readonly codeGenerator?: () => string;
3181
+ }
3182
+ /**
3183
+ * Return shape of `db.recoverPassphrase`. `newCodes` is populated when
3184
+ * `rotateRemainingCodes` was enabled and at least one entry was
3185
+ * rotated; an empty array means no rotation happened (rotation
3186
+ * disabled, or no remaining codes after burn). Show the codes to the
3187
+ * user once — they are the canonical credential for future recovery
3188
+ * and CANNOT be retrieved again.
3189
+ */
3190
+ interface RecoverPassphraseResult {
3191
+ readonly newCodes: readonly string[];
2952
3192
  }
2953
3193
  /**
2954
3194
  * Reset the user's passphrase using a recovery proof. v0.1.0-pre.5
@@ -2961,6 +3201,172 @@ interface RecoverPassphraseInput {
2961
3201
  */
2962
3202
  declare function recoverPassphrase(store: NoydbStore, vault: string, userId: string, input: RecoverPassphraseInput): Promise<UnlockedKeyring>;
2963
3203
 
3204
+ /**
3205
+ * Atomic peer-recovery primitive — issues #33 + #34.
3206
+ *
3207
+ * `recoverUser` is a SEPARATE operation from `revoke + grant`. It
3208
+ * exists because peer-recovery has different semantics than account
3209
+ * removal-then-reissue:
3210
+ *
3211
+ * 1. **Same identity preserved.** `userId`, `role`, `permissions`,
3212
+ * capability bits, user envelope (if any), policy override (if
3213
+ * any) all survive. Only the wrapping changes.
3214
+ * 2. **No key rotation.** The existing DEKs stay valid — every
3215
+ * OTHER principal in the vault keeps their access. Rotating
3216
+ * keys would invalidate every co-user's wrapping.
3217
+ * 3. **Atomic by construction.** A single `store.put` overwrites
3218
+ * `_keyring/<userId>` with the recovered file. No revoke step
3219
+ * means no partial-failure window.
3220
+ * 4. **Owner→owner natively allowed.** Two co-owners recovering
3221
+ * each other is the explicitly-intentional case (a partner
3222
+ * forgot the master phrase). The existing `canRevoke` rule that
3223
+ * blocks owner→owner is correct for `revoke` (which is account
3224
+ * *removal*) and intentionally NOT replicated here. The policy
3225
+ * gate `peer-recover-user` carries the freshness requirement.
3226
+ * 5. **Tier-2 slots dropped.** The slots wrap the OLD KEK under
3227
+ * method-derived keys; after recovery the KEK is re-derived
3228
+ * from the new temp passphrase. Match `rotatePassphrase`'s
3229
+ * precedent — the recovered user re-enrols slots after picking
3230
+ * their own phrase.
3231
+ *
3232
+ * Caller must be at least as privileged as the target. The hub
3233
+ * `db.recoverUser` method gates this with the `peer-recover-user`
3234
+ * policy gate (#33's factor-proof requirement); the function below
3235
+ * enforces only the role + anti-privilege-escalation invariants.
3236
+ *
3237
+ * @module
3238
+ */
3239
+
3240
+ /** Input shape for {@link recoverUser}. */
3241
+ interface RecoverUserOptions {
3242
+ /** Target user id whose keyring is being recovered. */
3243
+ readonly userId: string;
3244
+ /**
3245
+ * Temporary passphrase under which the new keyring is wrapped.
3246
+ * The recipient should call `db.rotatePassphrase` immediately on
3247
+ * acceptance to choose their own phrase — this temp acts as a
3248
+ * single-use bridge in invite / peer-recovery flows.
3249
+ */
3250
+ readonly passphrase: string;
3251
+ /** Override the target's role. Defaults to the existing target's role. */
3252
+ readonly role?: Role;
3253
+ /** Override the target's display name. Defaults to existing. */
3254
+ readonly displayName?: string;
3255
+ /** Validate phrase strength against the configured policy. */
3256
+ readonly validatePassphrase?: boolean;
3257
+ /**
3258
+ * Skip phrase strength validation even when `validatePassphrase` is
3259
+ * set. The escape hatch matches `grant`'s shape — used when the
3260
+ * temp phrase is a high-entropy one-shot string that doesn't need
3261
+ * to satisfy the human-typeable rules.
3262
+ */
3263
+ readonly allowWeakPassphrase?: boolean;
3264
+ /**
3265
+ * Optional explicit phrase policy override (passed through to
3266
+ * `assertStrongPassphrase`). Mirrors how `grant` accepts a custom
3267
+ * `PassphrasePolicy` for app-specific tightening.
3268
+ */
3269
+ readonly passphrasePolicy?: PassphrasePolicy;
3270
+ }
3271
+ /**
3272
+ * Atomically rewrap the target user's keyring under a fresh temp
3273
+ * passphrase. Single store write; no revoke step; no key rotation.
3274
+ *
3275
+ * Caller's responsibilities (NOT enforced here):
3276
+ * - Run the `peer-recover-user` policy gate first via
3277
+ * `Noydb.checkGate` to enforce the freshness factor proof.
3278
+ * - Communicate the temp passphrase to the recipient via a secure
3279
+ * channel (URL fragment, in-person, etc.) — the hub does not
3280
+ * transport secrets.
3281
+ */
3282
+ declare function recoverUser(store: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: RecoverUserOptions): Promise<void>;
3283
+
3284
+ /**
3285
+ * **Wrap-DEKs primitive (#44)** — a single canonical shape for the
3286
+ * pattern of "serialize a DEK set, encrypt it under a credential-derived
3287
+ * AES-GCM key." Used by:
3288
+ *
3289
+ * - **tier-0** — paper recovery entries (`_meta/recovery-paper`),
3290
+ * credential = the printed code.
3291
+ * - **tier-2** — password authenticator slots (`KeyringFile.authenticators`,
3292
+ * `wrapKind: 'deks'`), credential = the daily password.
3293
+ *
3294
+ * **Not** used by `@noy-db/on-pin` — tier-3 wraps the DEK set under
3295
+ * the same conceptual pattern but at **100,000 PBKDF2 iterations**
3296
+ * (vs the 600,000 here), because the protection window for a PIN
3297
+ * slot is short (idle-timeout-bounded, typically 15 min) and 600k
3298
+ * iterations would make every PIN-resume noticeably slow. The wire
3299
+ * formats are deliberately incompatible. See `@noy-db/on-pin`'s
3300
+ * `PIN_PBKDF2_ITERATIONS` and the threat-model rationale in its
3301
+ * module docstring.
3302
+ *
3303
+ * Before #44, the same crypto lived in two places: `mintPaperRecoveryEntry`
3304
+ * (in `team/recovery.ts`) and `enrollPasswordAuthenticator` (in
3305
+ * `@noy-db/on-password`). Both functions did identical work — PBKDF2
3306
+ * the credential, AES-GCM-encrypt the JSON-serialized DEK set — but
3307
+ * their implementations had drifted apart enough that fixing a bug
3308
+ * in one wouldn't fix the other.
3309
+ *
3310
+ * This module owns the canonical implementation. Consumers compose:
3311
+ *
3312
+ * - `mintPaperRecoveryEntry` is now a thin wrapper that calls
3313
+ * `mintWrappedDeksBlob` and adds `{ codeId, enrolledAt }`.
3314
+ * - `enrollPasswordAuthenticator` calls `mintWrappedDeksBlob` and
3315
+ * wraps the result in the slot envelope.
3316
+ *
3317
+ * @module
3318
+ */
3319
+ /**
3320
+ * The wrap-DEKs primitive — a serialized + AES-GCM-encrypted DEK set
3321
+ * keyed under a credential-derived key.
3322
+ *
3323
+ * All three fields are base64-encoded so the blob is JSON-safe and
3324
+ * round-trips through `_meta/*` envelopes (which carry plaintext
3325
+ * JSON in `_data`).
3326
+ *
3327
+ * Composition: `PaperRecoveryEntry extends WrappedDeksBlob` plus
3328
+ * `{ codeId, enrolledAt }`. `KeyringAuthenticatorWrappingDEKs`
3329
+ * carries the same three fields with `salt` stored in `meta` for
3330
+ * slot-format back-compat (#44 defers moving it to top-level).
3331
+ */
3332
+ interface WrappedDeksBlob {
3333
+ /** Base64 PBKDF2 salt for the credential-derived wrapping key. */
3334
+ readonly salt: string;
3335
+ /** Base64 AES-GCM IV used for the `wrappedDeks` ciphertext. */
3336
+ readonly iv: string;
3337
+ /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */
3338
+ readonly wrappedDeks: string;
3339
+ }
3340
+ /**
3341
+ * Mint a fresh `WrappedDeksBlob` from a DEK set + a string credential.
3342
+ *
3343
+ * Generates a random salt + IV, derives a 256-bit AES-GCM key via
3344
+ * PBKDF2-SHA256(credential, salt, 600K), serializes the DEK set as
3345
+ * `{ deks: { coll: rawBase64 } }`, and AES-GCM-encrypts.
3346
+ *
3347
+ * The `credential` is the user-typed string (recovery code, daily
3348
+ * password, PIN). Caller normalization rules apply (e.g. paper
3349
+ * recovery uppercase-strips the code before reaching this function).
3350
+ *
3351
+ * @param deks - DEK set to wrap. Each DEK must be exportable via
3352
+ * `subtle.exportKey('raw', dek)` (the hub mints DEKs
3353
+ * this way; consumers feeding non-extractable keys
3354
+ * will get `InvalidAccessError` from WebCrypto).
3355
+ * @param credential - String input the consumer minted (paper code,
3356
+ * password, PIN). Treated as opaque bytes by PBKDF2.
3357
+ */
3358
+ declare function mintWrappedDeksBlob(deks: Map<string, CryptoKey>, credential: string): Promise<WrappedDeksBlob>;
3359
+ /**
3360
+ * Reverse of {@link mintWrappedDeksBlob}. Re-derives the wrapping key
3361
+ * from the credential + stored salt, AES-GCM-decrypts the wrapped DEK
3362
+ * set, and re-imports each DEK as an extractable AES-GCM CryptoKey.
3363
+ *
3364
+ * Throws (AES-GCM auth tag failure) when the credential doesn't
3365
+ * match the blob. Callers iterating over multiple blobs (e.g. paper
3366
+ * recovery's "try every entry until one matches") should catch.
3367
+ */
3368
+ declare function unwrapDeksFromBlob(blob: WrappedDeksBlob, credential: string): Promise<Map<string, CryptoKey>>;
3369
+
2964
3370
  /**
2965
3371
  * Recovery profile persistence + dispatch — issue #10.
2966
3372
  *
@@ -3000,15 +3406,15 @@ declare function recoverPassphrase(store: NoydbStore, vault: string, userId: str
3000
3406
  * resume — the cryptographic guarantee is identical (AES-GCM with a
3001
3407
  * PBKDF2-derived key), and it sidesteps the non-extractable-KEK
3002
3408
  * constraint cleanly.
3409
+ *
3410
+ * Type-level composition (#44): `PaperRecoveryEntry extends
3411
+ * WrappedDeksBlob` — the three crypto fields (`salt`, `iv`,
3412
+ * `wrappedDeks`) come from the shared primitive; `codeId` and
3413
+ * `enrolledAt` are paper-recovery's own metadata. Wire format
3414
+ * unchanged.
3003
3415
  */
3004
- interface PaperRecoveryEntry {
3416
+ interface PaperRecoveryEntry extends WrappedDeksBlob {
3005
3417
  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
3418
  readonly enrolledAt: string;
3013
3419
  }
3014
3420
  interface PaperRecoveryDoc {
@@ -3024,6 +3430,33 @@ declare function savePaperRecoveryEntries(store: NoydbStore, vault: string, entr
3024
3430
  declare function burnPaperRecoveryEntry(store: NoydbStore, vault: string, codeId: string): Promise<void>;
3025
3431
  /** Whether at least one recovery profile has any enrolled entries. */
3026
3432
  declare function hasRecoveryEnrolled(store: NoydbStore, vault: string): Promise<boolean>;
3433
+ /**
3434
+ * Generate one paper-recovery entry from an unlocked DEK set.
3435
+ *
3436
+ * Returns the serializable entry (persisted via
3437
+ * {@link savePaperRecoveryEntries}). The recovery flow unwraps the
3438
+ * DEK set, then mints a fresh KEK from the user's new passphrase.
3439
+ *
3440
+ * Thin wrapper over {@link mintWrappedDeksBlob} (#44) — the crypto
3441
+ * lives in the shared primitive; this function just adds paper-
3442
+ * recovery's own metadata (`codeId`, `enrolledAt`).
3443
+ *
3444
+ * @param deks Map of collection-name → DEK (extractable).
3445
+ * @param code The plaintext recovery code (caller-supplied;
3446
+ * pair this with `@noy-db/on-recovery`'s code
3447
+ * generator/parser if available).
3448
+ * @param codeId Stable id used by `burnPaperRecoveryEntry`.
3449
+ */
3450
+ declare function mintPaperRecoveryEntry(deks: Map<string, CryptoKey>, code: string, codeId: string): Promise<PaperRecoveryEntry>;
3451
+ /**
3452
+ * Decrypt a recovery entry to recover the raw DEK set. Used by the
3453
+ * `recoverPassphrase` flow after the user's code has been parsed.
3454
+ *
3455
+ * Thin wrapper over {@link unwrapDeksFromBlob} (#44).
3456
+ *
3457
+ * @throws when the code does not match the entry (AES-GCM auth tag fail).
3458
+ */
3459
+ declare function unwrapDeksFromPaperEntry(entry: PaperRecoveryEntry, code: string): Promise<Map<string, CryptoKey>>;
3027
3460
 
3028
3461
  /**
3029
3462
  * Public envelope — owner-curated plaintext metadata, readable
@@ -3130,51 +3563,6 @@ declare function validatePublicEnvelopeInput(input: SetPublicEnvelopeInput, sche
3130
3563
  */
3131
3564
  declare function isPublicEnvelope(x: unknown): x is PublicEnvelope;
3132
3565
 
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
3566
  /**
3179
3567
  * Per-vault tier-3 (PIN / quick-resume) state — issue #11.
3180
3568
  *
@@ -3375,8 +3763,35 @@ declare function runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T>
3375
3763
  * @module
3376
3764
  */
3377
3765
 
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';
3766
+ /**
3767
+ * A single factor surface the proof an actor presents at gate time.
3768
+ *
3769
+ * | Kind | Source | Off-device? |
3770
+ * |---|---|---|
3771
+ * | `totp` | RFC 6238 authenticator app (Google Auth, 1Password) | yes |
3772
+ * | `email-otp` | one-time code mailed to the user | yes |
3773
+ * | `recovery` | printable Base32 code (`@noy-db/on-recovery`) | yes (paper) |
3774
+ * | `shamir` | k-of-n threshold share (`@noy-db/on-shamir`) | yes |
3775
+ * | `webauthn-roaming` | hardware key (YubiKey, SoloKey, Titan) | yes (key portable) |
3776
+ * | `webauthn-platform` | platform passkey (Touch ID, Face ID, Hello) | no (device-bound) |
3777
+ * | `password` | tier-2 daily password (`@noy-db/on-password`) | no |
3778
+ * | `pin` | tier-3 quick-resume PIN (`@noy-db/on-pin`) | no |
3779
+ *
3780
+ * Off-device kinds (TOTP, email-OTP, recovery, shamir, roaming WebAuthn)
3781
+ * are the strongest factor proofs because they require something
3782
+ * separate from the device the user just unlocked. Platform / password /
3783
+ * PIN are useful for "fresh proof of *this* user" but don't bind across
3784
+ * devices — policies can require ANY of them or insist on a count of 2
3785
+ * to force a mix.
3786
+ *
3787
+ * Added in pre.8 (#30): `webauthn-platform`, `password`, `pin` —
3788
+ * previously consumers with no off-device infrastructure (no TOTP,
3789
+ * no email-OTP, paper recovery not enrolled) had to disable the
3790
+ * factor requirement entirely on `rotate-passphrase`. Now they can
3791
+ * pin "any second factor I have wired" without losing the freshness
3792
+ * guarantee.
3793
+ */
3794
+ type FactorKind = 'totp' | 'email-otp' | 'recovery' | 'shamir' | 'webauthn-roaming' | 'webauthn-platform' | 'password' | 'pin';
3380
3795
  /**
3381
3796
  * One factor requirement entry. The default is "any one of the listed
3382
3797
  * factors, fresh within the last 5 minutes". Bumping `count` requires N
@@ -3419,7 +3834,17 @@ type BuiltInGateName = 'rotate-passphrase' | 'recover-passphrase' | 'enroll-auth
3419
3834
  /** Authorize a write to one's own user envelope (#22). */
3420
3835
  | 'edit-own-profile'
3421
3836
  /** Authorize reading other principals' user envelopes (#22). */
3422
- | 'view-team-profiles';
3837
+ | 'view-team-profiles'
3838
+ /**
3839
+ * Authorize an atomic peer-recovery — `db.recoverUser` (#33, #34).
3840
+ * Distinct from `revoke-user` because peer-recovery is intentional
3841
+ * re-issuance of someone's keyring under a temp passphrase, NOT
3842
+ * removal. Allows owner→owner natively (matches the threat model:
3843
+ * a co-owner explicitly recovering another co-owner). Ships with a
3844
+ * factor-proof default in `STRICT_POLICY` so the issuer must
3845
+ * affirmatively prove identity at the moment of recovery.
3846
+ */
3847
+ | 'peer-recover-user';
3423
3848
  /** Either a built-in gate name or an `app:*` custom gate. */
3424
3849
  type GateName = BuiltInGateName | `app:${string}`;
3425
3850
  /**
@@ -3968,20 +4393,84 @@ declare class Noydb {
3968
4393
  recoverPassphrase(vault: string, input: RecoverPassphraseInput, factors?: {
3969
4394
  factors?: ReadonlyArray<FactorProof>;
3970
4395
  sharedDevice?: boolean;
4396
+ }): Promise<RecoverPassphraseResult>;
4397
+ /**
4398
+ * Atomic peer-recovery — re-wraps an EXISTING user's keyring under
4399
+ * a fresh temp passphrase in a single store write. Closes #34's
4400
+ * partial-failure window (the previous compose-from-primitives
4401
+ * pattern was `db.revoke + db.grant`, two writes — if the issuer
4402
+ * cancelled between them the target was locked out entirely).
4403
+ *
4404
+ * Different from `db.revoke + db.grant`:
4405
+ *
4406
+ * - Same `userId`, role, permissions, capabilities preserved.
4407
+ * - DEKs unchanged → every other principal in the vault keeps
4408
+ * access. No key rotation.
4409
+ * - Allows owner→owner natively (#33). The existing
4410
+ * `db.revoke` retains its block — peer-recovery is a separate,
4411
+ * intentionally-named operation.
4412
+ * - Tier-2 slots dropped (they wrap the old KEK).
4413
+ *
4414
+ * Gated by `peer-recover-user`; `STRICT_POLICY` requires a
4415
+ * recovery / TOTP / email-OTP factor proof at the moment of
4416
+ * recovery, so the issuer affirmatively re-asserts identity.
4417
+ *
4418
+ * The recipient should call `db.rotatePassphrase` on first session
4419
+ * to choose their own phrase — the temp acts as a single-use
4420
+ * bridge.
4421
+ *
4422
+ * ```ts
4423
+ * await db.recoverUser('acme', {
4424
+ * userId: 'bob',
4425
+ * passphrase: 'temporary-correct-horse-battery-staple-printer',
4426
+ * }, { factors: [{ kind: 'recovery' }] })
4427
+ * // Bob opens createNoydb({ user: 'bob', secret: tempPhrase })
4428
+ * // and immediately calls db.rotatePassphrase to set his own.
4429
+ * ```
4430
+ *
4431
+ * @throws `NoAccessError` when no keyring exists for the target.
4432
+ * @throws `PermissionDeniedError` when the caller's role can't
4433
+ * recover the target's role (admin→owner is blocked even
4434
+ * under recovery).
4435
+ * @throws `PrivilegeEscalationError` when the caller lacks a DEK
4436
+ * the target previously had access to.
4437
+ *
4438
+ * @see #33 #34 — the issues this method closes.
4439
+ */
4440
+ recoverUser(vault: string, options: RecoverUserOptions, factors?: {
4441
+ factors?: ReadonlyArray<FactorProof>;
4442
+ sharedDevice?: boolean;
3971
4443
  }): Promise<void>;
3972
4444
  /**
3973
4445
  * 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.
4446
+ * profile.
4447
+ *
4448
+ * The hub wraps the user's DEK set (not the KEK) under a code-derived
4449
+ * AES-GCM key — see `team/recovery.ts` for the rationale. The mint
4450
+ * helper {@link mintPaperRecoveryEntry} is the canonical primitive;
4451
+ * pair it with `db.getKeyring(vault)` to obtain the live DEK set:
3978
4452
  *
3979
4453
  * ```ts
3980
- * import { generateRecoveryCodeSet } from '@noy-db/on-recovery'
3981
- * const { codes, entries } = await generateRecoveryCodeSet({ kek, count: 10 })
4454
+ * import { mintPaperRecoveryEntry } from '@noy-db/hub'
4455
+ *
4456
+ * const keyring = await db.getKeyring('acme')
4457
+ * const codes: string[] = ['CORRECT-HORSE-1', 'BATTERY-STAPLE-2', ...]
4458
+ * const entries = await Promise.all(
4459
+ * codes.map((code, i) => mintPaperRecoveryEntry(keyring.deks, code, `code-${i}`)),
4460
+ * )
3982
4461
  * await db.enrollRecovery('acme', { profile: 'paper', entries })
3983
4462
  * showCodesToUser(codes)
3984
4463
  * ```
4464
+ *
4465
+ * As of pre.8, `@noy-db/on-recovery`'s `generateRecoveryCodeSet`
4466
+ * delegates to `mintPaperRecoveryEntry` internally — its output is
4467
+ * fed directly to this API. Pick whichever fits your code-gen layer:
4468
+ *
4469
+ * ```ts
4470
+ * import { generateRecoveryCodeSet } from '@noy-db/on-recovery'
4471
+ * const { codes, entries } = await generateRecoveryCodeSet({ deks: keyring.deks, count: 8 })
4472
+ * await db.enrollRecovery('acme', { profile: 'paper', entries })
4473
+ * ```
3985
4474
  */
3986
4475
  enrollRecovery(vault: string, enrollment: {
3987
4476
  profile: 'paper';
@@ -4016,8 +4505,30 @@ declare class Noydb {
4016
4505
  unlockViaPin(vault: string, resume: (state: QuickUnlockState) => Promise<UnlockedKeyring>): Promise<UnlockedKeyring | undefined>;
4017
4506
  /** Drop the tier-3 state for a vault — explicit logout. */
4018
4507
  clearQuickUnlock(vault: string): void;
4019
- /** Get or load the keyring for a vault. */
4020
- private getKeyring;
4508
+ /**
4509
+ * Public accessor for the unlocked keyring of a vault — issue #28.
4510
+ *
4511
+ * Returns the cached `UnlockedKeyring` (already in memory after
4512
+ * `createNoydb` + first vault touch); loads it on demand if absent.
4513
+ * Used by `@noy-db/on-*` ceremonies that need the live DEK set
4514
+ * (paper recovery via {@link mintPaperRecoveryEntry}, tier-3 PIN
4515
+ * enrolment via on-pin's `enrollPin`, custom on-* ceremonies that
4516
+ * don't have a hub-side wrapper).
4517
+ *
4518
+ * No new permission gate — this is an accessor over already-unlocked
4519
+ * state. The keyring is materialized only after the calling session
4520
+ * has unlocked the vault at tier 1, 2, or 3, so exposing it does not
4521
+ * widen access. Throws `ValidationError` when encryption is enabled
4522
+ * and no `secret` / `getKeyring` is configured.
4523
+ *
4524
+ * ```ts
4525
+ * const keyring = await db.getKeyring('acme')
4526
+ * // keyring.deks: Map<collection, CryptoKey>
4527
+ * // keyring.kek: CryptoKey (non-extractable; null for tier-3 sessions)
4528
+ * // keyring.role / .permissions / .authenticators
4529
+ * ```
4530
+ */
4531
+ getKeyring(vault: string): Promise<UnlockedKeyring>;
4021
4532
  }
4022
4533
  /** Create a new NOYDB instance. */
4023
4534
  declare function createNoydb(options: NoydbOptions): Promise<Noydb>;
@@ -7588,7 +8099,13 @@ type RecoveryEnrollment = {
7588
8099
  *
7589
8100
  * @see docs/subsystems/session-tiers.md → Tier 2 — Authenticate (multi-slot)
7590
8101
  */
7591
- interface KeyringAuthenticator {
8102
+ /**
8103
+ * Shared fields across all authenticator slot variants. The variant
8104
+ * (`KeyringAuthenticatorWrappingKEK` vs `KeyringAuthenticatorWrappingDEKs`)
8105
+ * carries the actual wrapped material; everything below is identity +
8106
+ * metadata only.
8107
+ */
8108
+ interface KeyringAuthenticatorBase {
7592
8109
  /** Caller-chosen identifier — e.g. `'webauthn-yubikey-blue'`, `'oidc-google'`, `'password-daily'`. */
7593
8110
  readonly id: string;
7594
8111
  /** Method family — selects which `@noy-db/on-*` package handles unlock. */
@@ -7600,8 +8117,6 @@ interface KeyringAuthenticator {
7600
8117
  * tier 2 may add a sibling slot when the active policy permits.
7601
8118
  */
7602
8119
  readonly enrolled_via_tier: 1 | 2;
7603
- /** Base64 wrapped-KEK ciphertext under the method-derived key. */
7604
- readonly wrapped_kek: string;
7605
8120
  /**
7606
8121
  * Method-specific metadata: WebAuthn cred id, OIDC issuer/sub, PBKDF2
7607
8122
  * salt for `on-password`, etc. The schema is open by design — the
@@ -7609,6 +8124,63 @@ interface KeyringAuthenticator {
7609
8124
  */
7610
8125
  readonly meta: Record<string, unknown>;
7611
8126
  }
8127
+ /**
8128
+ * Slot that wraps the KEK directly under a method-derived AES-KW key.
8129
+ * Used by ceremonies where the on-* package can produce/recover an
8130
+ * extractable KEK from its own credential — WebAuthn (PRF-derived
8131
+ * wrapping key) and split-key OIDC.
8132
+ *
8133
+ * `wrapKind` is optional/absent on slots written before pre.8 — those
8134
+ * legacy slots are treated as wrap-KEK by default at unlock time.
8135
+ */
8136
+ interface KeyringAuthenticatorWrappingKEK extends KeyringAuthenticatorBase {
8137
+ readonly wrapKind?: 'kek';
8138
+ /** Base64 wrapped-KEK ciphertext under the method-derived key. */
8139
+ readonly wrapped_kek: string;
8140
+ /** XOR guard — wrap-KEK slots must NOT carry wrap-DEKs material. */
8141
+ readonly wrapped_deks?: never;
8142
+ /** XOR guard — wrap-KEK slots must NOT carry wrap-DEKs material. */
8143
+ readonly iv?: never;
8144
+ }
8145
+ /**
8146
+ * Slot that wraps the DEK set (not the KEK) under a method-derived
8147
+ * AES-GCM key — sidesteps the non-extractable-KEK constraint by
8148
+ * encrypting the serialized `{ deks: { collection: rawDekBase64 } }`
8149
+ * directly. Mirrors the format used by `mintPaperRecoveryEntry`
8150
+ * (`PaperRecoveryEntry`) and `@noy-db/on-pin`'s `PinResumeState` —
8151
+ * the unified wrap-DEKs primitive across tier-0 / tier-2 / tier-3.
8152
+ *
8153
+ * Trade-off: a slot of this kind reconstructs `UnlockedKeyring` with
8154
+ * `kek: null` after unlock. That is semantically correct for tier-2
8155
+ * (sensitive ops like `enrollAuthenticator` / `rotatePassphrase`
8156
+ * require a tier-1 unlock anyway) and matches how `@noy-db/on-pin`
8157
+ * already behaves at tier 3.
8158
+ *
8159
+ * @see `mintPaperRecoveryEntry` in `team/recovery.ts` — same shape on
8160
+ * a different on-disk path (`_meta/recovery-paper`).
8161
+ */
8162
+ interface KeyringAuthenticatorWrappingDEKs extends KeyringAuthenticatorBase {
8163
+ readonly wrapKind: 'deks';
8164
+ /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */
8165
+ readonly wrapped_deks: string;
8166
+ /** Base64 AES-GCM IV used for the `wrapped_deks` ciphertext. */
8167
+ readonly iv: string;
8168
+ /** XOR guard — wrap-DEKs slots must NOT carry wrap-KEK material. */
8169
+ readonly wrapped_kek?: never;
8170
+ }
8171
+ /**
8172
+ * Discriminated union over the two wrap-format variants. Reads from
8173
+ * disk should always go through this type so the variant is preserved.
8174
+ *
8175
+ * Discriminator: `wrapKind`. Absent → wrap-KEK (legacy / WebAuthn /
8176
+ * OIDC). Present and `'deks'` → wrap-DEKs (password / future on-* that
8177
+ * want to sidestep extractable-KEK).
8178
+ *
8179
+ * The type-level XOR enforces "exactly one of `wrapped_kek` /
8180
+ * `wrapped_deks` is present" — a structural guarantee that the runtime
8181
+ * dispatch is safe.
8182
+ */
8183
+ type KeyringAuthenticator = KeyringAuthenticatorWrappingKEK | KeyringAuthenticatorWrappingDEKs;
7612
8184
  interface KeyringFile {
7613
8185
  readonly _noydb_keyring: typeof NOYDB_KEYRING_VERSION;
7614
8186
  readonly user_id: string;
@@ -8830,4 +9402,4 @@ interface DeleteManyResult {
8830
9402
  }>;
8831
9403
  }
8832
9404
 
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 };
9405
+ 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, WeakPassphraseError 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, type StoreAuthKind as cA, type StoreCapabilities as cB, SyncEngine as cC, type SyncMetadata as cD, type SyncPolicy as cE, SyncScheduler as cF, type SyncSchedulerStatus as cG, type SyncStatus as cH, type SyncTarget as cI, type SyncTargetRole as cJ, SyncTransaction as cK, type SyncTransactionResult as cL, type TierMode as cM, type TranslatorAuditEntry as cN, type TxOp as cO, USER_ENVELOPE_COLLECTION as cP, USER_ENVELOPE_MAX_BYTES as cQ, type Unsubscribe as cR, UserApi as cS, type UserEnvelopeCheckGate as cT, UserEnvelopeOversizedError as cU, type UserEnvelopePresented as cV, type UserInfo as cW, type VaultBackup as cX, type VaultPolicyOnDisk as cY, type VaultSnapshot as cZ, type WarningRules 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 RecoverPassphraseResult as co, type RecoverUserOptions as cp, type RecoveryProof as cq, type ResolvedPublicEnvelopeSchema as cr, type RevokeOptions as cs, type RotatePassphraseInput as ct, type SessionPolicy as cu, type SetPublicEnvelopeInput as cv, type StandardSchemaV1 as cw, type StandardSchemaV1Issue as cx, type StandardSchemaV1SyncResult as cy, type StoreAuth as cz, type DictionaryOptions as d, type WeakPassphraseReason as d0, type WrappedDeksBlob as d1, assertStrongPassphrase as d2, buildRecipientKeyringFile as d3, burnPaperRecoveryEntry as d4, createNoydb as d5, createStore as d6, deriveMagicLinkContentKey as d7, enrollAuthenticator as d8, estimateEntropy as d9, savePaperRecoveryEntries as dA, unwrapDeksFromBlob as dB, unwrapDeksFromPaperEntry as dC, unwrapMagicLinkGrant as dD, validatePassphrase as dE, validatePublicEnvelopeInput as dF, validateSchemaInput as dG, validateSchemaOutput as dH, writeMagicLinkGrant as dI, evaluateExportCapability as da, evaluateImportCapability as db, findAuthenticator as dc, hasExportCapability as dd, hasImportCapability as de, hasRecoveryEnrolled as df, isMagicLinkGrantExpired as dg, isPublicEnvelope as dh, issueDelegation as di, recoverPassphrase as dj, rotatePassphrase as dk, listMagicLinkGrants as dl, listUsers as dm, listUsersWithEnvelopes as dn, loadActiveDelegations as dp, loadPaperRecoveryEntries as dq, magicLinkGrantRecordId as dr, mintPaperRecoveryEntry as ds, mintWrappedDeksBlob as dt, readMagicLinkGrantRecord as du, recoverUser as dv, removeAuthenticator as dw, resolveSchema as dx, revokeDelegation as dy, revokeMagicLinkGrant 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 };