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

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 (95) hide show
  1. package/dist/blobs/index.cjs.map +1 -1
  2. package/dist/blobs/index.d.cts +3 -3
  3. package/dist/blobs/index.d.ts +3 -3
  4. package/dist/blobs/index.js +2 -2
  5. package/dist/bundle/index.cjs +26 -3
  6. package/dist/bundle/index.cjs.map +1 -1
  7. package/dist/bundle/index.d.cts +3 -3
  8. package/dist/bundle/index.d.ts +3 -3
  9. package/dist/bundle/index.js +3 -1
  10. package/dist/{chunk-LSZHBNDG.js → chunk-3WCRU7TI.js} +2 -2
  11. package/dist/{chunk-PSHTHSIX.js → chunk-6IJQ27XN.js} +213 -10
  12. package/dist/chunk-6IJQ27XN.js.map +1 -0
  13. package/dist/{chunk-O5GK62FJ.js → chunk-B6HF6NTZ.js} +1 -1
  14. package/dist/chunk-B6HF6NTZ.js.map +1 -0
  15. package/dist/{chunk-AVWFLPNR.js → chunk-CL37QSND.js} +2 -2
  16. package/dist/chunk-EMIGCR7X.js +39 -0
  17. package/dist/chunk-EMIGCR7X.js.map +1 -0
  18. package/dist/{chunk-GJILMRPO.js → chunk-FAAWLVTF.js} +42 -4
  19. package/dist/chunk-FAAWLVTF.js.map +1 -0
  20. package/dist/chunk-GILMPJXB.js +155 -0
  21. package/dist/chunk-GILMPJXB.js.map +1 -0
  22. package/dist/{chunk-L77MEFCH.js → chunk-INSJBB5W.js} +3 -3
  23. package/dist/{chunk-QZIACZZU.js → chunk-KPF2HHPI.js} +2 -2
  24. package/dist/{chunk-NK2NSXXK.js → chunk-N2LMZKLR.js} +2 -2
  25. package/dist/{chunk-EARQCIL7.js → chunk-NZ4XCIKS.js} +3 -3
  26. package/dist/{chunk-E445ICYI.js → chunk-UFL4DUEV.js} +5 -3
  27. package/dist/chunk-UFL4DUEV.js.map +1 -0
  28. package/dist/consent/index.d.cts +3 -3
  29. package/dist/consent/index.d.ts +3 -3
  30. package/dist/{dev-unlock-XOUecfQ9.d.ts → dev-unlock-CcJ1qIi7.d.ts} +1 -1
  31. package/dist/{dev-unlock-5SmCVGyx.d.cts → dev-unlock-Dk14V6lX.d.cts} +1 -1
  32. package/dist/{hash-Bxud16vM.d.ts → hash-1Xsqx1jl.d.ts} +1 -1
  33. package/dist/{hash-CvuKN2gH.d.cts → hash-h_2U3TFb.d.cts} +1 -1
  34. package/dist/history/index.cjs.map +1 -1
  35. package/dist/history/index.d.cts +4 -4
  36. package/dist/history/index.d.ts +4 -4
  37. package/dist/history/index.js +2 -2
  38. package/dist/i18n/index.cjs +3 -1
  39. package/dist/i18n/index.cjs.map +1 -1
  40. package/dist/i18n/index.d.cts +3 -3
  41. package/dist/i18n/index.d.ts +3 -3
  42. package/dist/i18n/index.js +3 -3
  43. package/dist/{index-DN-J-5wT.d.cts → index-6xNpPsxR.d.cts} +1 -1
  44. package/dist/{index-Cy-MKrdK.d.ts → index-Cvb0efA_.d.cts} +39 -5
  45. package/dist/{index-BRHBCmLt.d.ts → index-DJTf9yxn.d.ts} +1 -1
  46. package/dist/{index-BvUiM47h.d.cts → index-DZn6Yick.d.ts} +39 -5
  47. package/dist/index.cjs +2001 -58
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +315 -19
  50. package/dist/index.d.ts +315 -19
  51. package/dist/index.js +1503 -41
  52. package/dist/index.js.map +1 -1
  53. package/dist/{ledger-HWXYGUIQ.js → ledger-5V67MAIL.js} +3 -3
  54. package/dist/periods/index.cjs.map +1 -1
  55. package/dist/periods/index.d.cts +3 -3
  56. package/dist/periods/index.d.ts +3 -3
  57. package/dist/periods/index.js +3 -3
  58. package/dist/public-envelope-DFJZHXVH.js +31 -0
  59. package/dist/public-envelope-DFJZHXVH.js.map +1 -0
  60. package/dist/query/index.d.cts +1 -1
  61. package/dist/query/index.d.ts +1 -1
  62. package/dist/session/index.cjs +4 -2
  63. package/dist/session/index.cjs.map +1 -1
  64. package/dist/session/index.d.cts +4 -4
  65. package/dist/session/index.d.ts +4 -4
  66. package/dist/session/index.js +1 -1
  67. package/dist/shadow/index.d.cts +3 -3
  68. package/dist/shadow/index.d.ts +3 -3
  69. package/dist/store/index.d.cts +3 -3
  70. package/dist/store/index.d.ts +3 -3
  71. package/dist/sync/index.cjs.map +1 -1
  72. package/dist/sync/index.d.cts +2 -2
  73. package/dist/sync/index.d.ts +2 -2
  74. package/dist/sync/index.js +2 -2
  75. package/dist/team/index.cjs +3 -1
  76. package/dist/team/index.cjs.map +1 -1
  77. package/dist/team/index.d.cts +3 -3
  78. package/dist/team/index.d.ts +3 -3
  79. package/dist/team/index.js +4 -4
  80. package/dist/tx/index.d.cts +3 -3
  81. package/dist/tx/index.d.ts +3 -3
  82. package/dist/{types-Dmi7nrC9.d.ts → types-D-6bmD2c.d.ts} +1271 -3
  83. package/dist/{types-BVSfkYg6.d.cts → types-D3QLmhlk.d.cts} +1271 -3
  84. package/package.json +1 -1
  85. package/dist/chunk-E445ICYI.js.map +0 -1
  86. package/dist/chunk-GJILMRPO.js.map +0 -1
  87. package/dist/chunk-O5GK62FJ.js.map +0 -1
  88. package/dist/chunk-PSHTHSIX.js.map +0 -1
  89. /package/dist/{chunk-LSZHBNDG.js.map → chunk-3WCRU7TI.js.map} +0 -0
  90. /package/dist/{chunk-AVWFLPNR.js.map → chunk-CL37QSND.js.map} +0 -0
  91. /package/dist/{chunk-L77MEFCH.js.map → chunk-INSJBB5W.js.map} +0 -0
  92. /package/dist/{chunk-QZIACZZU.js.map → chunk-KPF2HHPI.js.map} +0 -0
  93. /package/dist/{chunk-NK2NSXXK.js.map → chunk-N2LMZKLR.js.map} +0 -0
  94. /package/dist/{chunk-EARQCIL7.js.map → chunk-NZ4XCIKS.js.map} +0 -0
  95. /package/dist/{ledger-HWXYGUIQ.js.map → ledger-5V67MAIL.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { I as IndexStrategy, d as LazyQuery } from './lazy-builder-BwEoBQZ9.js';
2
2
  import { A as AggregateStrategy } from './strategy-D-SrOLCl.js';
3
3
  import { C as CrdtStrategy, a as CrdtMode, b as CrdtState } from './strategy-BSxFXGzb.js';
4
- import { U as RefDescriptor, p as JoinableSource, Z as RefViolation, Q as Query, $ as ScanBuilder } from './index-BRHBCmLt.js';
4
+ import { N as NoydbError, U as RefDescriptor, p as JoinableSource, Z as RefViolation, Q as Query, $ as ScanBuilder } from './index-DJTf9yxn.js';
5
5
  import { I as IndexDef, C as CollectionIndexes } from './predicate-SBHmi6D0.js';
6
6
 
7
7
  /**
@@ -1739,6 +1739,136 @@ declare class NoydbEventEmitter {
1739
1739
  removeAllListeners(): void;
1740
1740
  }
1741
1741
 
1742
+ /**
1743
+ * Passphrase validation — phrase format (per the three-tier session-tiers
1744
+ * design, locked 2026-05-04).
1745
+ *
1746
+ * Passphrases are **phrases**: multiple simple words, easy to remember,
1747
+ * structurally constrained so a weak choice cannot silently collapse the
1748
+ * security floor. The format is intentionally narrow: lowercase letters
1749
+ * and single spaces only, no punctuation, no symbols, no digits.
1750
+ *
1751
+ * - Default minimum: 6 words (~77 bits with the 7,776-word EFF list).
1752
+ * - Strict minimum: 8 words (~103 bits).
1753
+ * - Per-word minimum: 3 characters (excludes "a", "is", "of").
1754
+ * - Adjacent repeats rejected ("the the").
1755
+ *
1756
+ * The hub runs validation default-on at every passphrase ingress
1757
+ * (`createOwnerKeyring`, `grant`, `rotatePassphrase`); test fixtures and
1758
+ * CLI scripts override via `{ allowWeakPassphrase: true }`.
1759
+ *
1760
+ * @module
1761
+ */
1762
+
1763
+ /** All reasons a phrase can be rejected. */
1764
+ type WeakPassphraseReason = 'empty' | 'invalid-chars' | 'leading-or-trailing-space' | 'double-space' | 'too-few-words' | 'word-too-short' | 'repeated-adjacent';
1765
+ /** Per-vault knobs. Aligns with `VaultPolicy.passphrase`. */
1766
+ interface PassphrasePolicy {
1767
+ /** Minimum number of words. Default 6. Strict policy uses 8. */
1768
+ readonly minWords?: number;
1769
+ /** Minimum characters per word. Default 3. */
1770
+ readonly minWordLength?: number;
1771
+ /** Reject adjacent identical words ("the the"). Default true. */
1772
+ readonly rejectRepeatedAdjacent?: boolean;
1773
+ }
1774
+ /** Result of a check. Discriminated union — compile-time exhaustive. */
1775
+ type PassphraseValidationResult = {
1776
+ readonly ok: true;
1777
+ readonly words: number;
1778
+ } | {
1779
+ readonly ok: false;
1780
+ readonly reason: WeakPassphraseReason;
1781
+ readonly minimum?: number;
1782
+ readonly got?: number;
1783
+ };
1784
+ /**
1785
+ * Thrown by `assertStrongPassphrase()` and by every hub ingress
1786
+ * point (`createOwnerKeyring`, `grant`, `rotatePassphrase`) when a
1787
+ * supplied phrase fails the structural rules above.
1788
+ */
1789
+ declare class WeakPassphraseError extends NoydbError {
1790
+ readonly reason: WeakPassphraseReason;
1791
+ readonly suggestion: string;
1792
+ constructor(reason: WeakPassphraseReason, suggestion: string);
1793
+ }
1794
+ /**
1795
+ * Inspect a phrase against the format rules and return a structured
1796
+ * verdict. Never throws — callers either branch on `ok` or pass the
1797
+ * result to {@link assertStrongPassphrase} for the throwing flavour.
1798
+ */
1799
+ declare function validatePassphrase(s: string, opts?: PassphrasePolicy): PassphraseValidationResult;
1800
+ /**
1801
+ * Throw {@link WeakPassphraseError} when the phrase fails. Used by
1802
+ * `createOwnerKeyring`, `grant`, and `rotatePassphrase` at ingress.
1803
+ *
1804
+ * Pass `{ allowWeakPassphrase: true }` to bypass — intended for test
1805
+ * fixtures, CLI scripts, and dev environments. The override never
1806
+ * loosens the cryptographic key derivation; it only relaxes the
1807
+ * structural-strength gate.
1808
+ */
1809
+ declare function assertStrongPassphrase(s: string, opts?: PassphrasePolicy & {
1810
+ allowWeakPassphrase?: boolean;
1811
+ }): void;
1812
+ /**
1813
+ * Estimate the entropy of a phrase, given the EFF 7,776-word list as
1814
+ * the assumed wordlist. ~12.9 bits per word.
1815
+ *
1816
+ * Returns 0 for any input that fails the phrase format — character-class
1817
+ * estimates aren't comparable to phrase entropy, and surfacing 0 makes
1818
+ * weak inputs visible in any UI that displays an entropy meter.
1819
+ */
1820
+ declare function estimateEntropy(passphrase: string): number;
1821
+
1822
+ /**
1823
+ * Type surface for the per-principal user envelope subsystem.
1824
+ *
1825
+ * @see docs/superpowers/specs/2026-05-05-user-envelope-design.md
1826
+ *
1827
+ * @module
1828
+ */
1829
+
1830
+ /**
1831
+ * Thin reader view of a user envelope. The on-disk shape is the standard
1832
+ * {@link import('../../types.js').EncryptedEnvelope}; this is what callers
1833
+ * see after the storage layer has decrypted the payload.
1834
+ *
1835
+ * Hub commits to the `keyringId` ⇔ `userId` identity and the `_v` / `_ts`
1836
+ * envelope metadata. The `data` payload is fully app-defined — hub does
1837
+ * not introspect, validate, or reserve any keys inside it.
1838
+ */
1839
+ interface UserEnvelope<T> {
1840
+ /** The principal id this envelope belongs to. Equals the keyring `user_id`. */
1841
+ readonly keyringId: string;
1842
+ /** App-owned payload. Opaque to hub. */
1843
+ readonly data: T;
1844
+ /** Optimistic-concurrency version. Increments on every write. */
1845
+ readonly _v: number;
1846
+ /** ISO timestamp of the last write. */
1847
+ readonly _ts: string;
1848
+ }
1849
+ /**
1850
+ * Soft cap on the JSON-serialized payload size. Generous (a typical
1851
+ * profile + preferences + small app annex is ~1 KiB); rejects accidental
1852
+ * "stuff app state in here" anti-patterns.
1853
+ */
1854
+ declare const USER_ENVELOPE_MAX_BYTES: number;
1855
+ /**
1856
+ * Reserved store collection name for user envelopes. Starts with `_` so the
1857
+ * keyring grant machinery propagates the DEK to every granted user via the
1858
+ * existing system-collection DEK propagation path in `team/keyring.ts`.
1859
+ */
1860
+ declare const USER_ENVELOPE_COLLECTION = "_users";
1861
+ /**
1862
+ * Thrown when a user-envelope payload exceeds {@link USER_ENVELOPE_MAX_BYTES}
1863
+ * after JSON-serialization. The error carries the actual size so callers
1864
+ * can decide whether to trim or split.
1865
+ */
1866
+ declare class UserEnvelopeOversizedError extends NoydbError {
1867
+ readonly bytes: number;
1868
+ readonly limit: number;
1869
+ constructor(bytes: number, limit?: number);
1870
+ }
1871
+
1742
1872
  /** In-memory representation of an unlocked keyring. */
1743
1873
  interface UnlockedKeyring {
1744
1874
  readonly userId: string;
@@ -1761,6 +1891,20 @@ interface UnlockedKeyring {
1761
1891
  * bundle import granted, regardless of role).
1762
1892
  */
1763
1893
  readonly importCapability?: ImportCapability;
1894
+ /**
1895
+ * Tier-2 authenticator slots — readonly snapshot loaded from the
1896
+ * keyring file. Mutations go through `enrollAuthenticator` /
1897
+ * `removeAuthenticator` (issue #11), which write back via
1898
+ * `persistKeyring`. Always defined; loads with an empty array for
1899
+ * keyrings written before the multi-slot extension landed.
1900
+ */
1901
+ readonly authenticators: readonly KeyringAuthenticator[];
1902
+ /**
1903
+ * Reserved per-keyring policy override (forward-compat for Option C
1904
+ * — see {@link VaultPolicyOnDisk}). v1.0 round-trips this field but
1905
+ * never enforces it; the gate engine uses `_meta/policy` only.
1906
+ */
1907
+ readonly policy?: VaultPolicyOnDisk;
1764
1908
  }
1765
1909
  /**
1766
1910
  * Recipient slot in a re-keyed `.noydb` bundle. Each slot becomes its
@@ -1817,6 +1961,29 @@ interface BundleRecipient {
1817
1961
  * @internal
1818
1962
  */
1819
1963
  declare function buildRecipientKeyringFile(callerKeyring: UnlockedKeyring, recipient: BundleRecipient): Promise<KeyringFile>;
1964
+ /** List all users with access to a vault. */
1965
+ declare function listUsers(adapter: NoydbStore, vault: string): Promise<UserInfo[]>;
1966
+ /**
1967
+ * Joined enumeration: every keyring + its `_users/<keyringId>`
1968
+ * envelope side by side. Convenience for admin UIs that want to
1969
+ * render team-member lists with profile data ("Bob — operator —
1970
+ * 'Bob the Auditor' avatar X locale fr-FR") in a single pass.
1971
+ *
1972
+ * `userEnvelopeDek` is the vault's `_users` collection DEK
1973
+ * (`vault.getDEK('_users')`); used to decrypt every envelope.
1974
+ *
1975
+ * Principals without a persisted envelope (legacy keyrings predating
1976
+ * the user-envelope feature) come back with `envelope: null`. The
1977
+ * caller chooses how to render — usually "fall back to keyring's
1978
+ * `displayName`".
1979
+ *
1980
+ * Order matches `listUsers()` (store-defined; sort if you need a
1981
+ * stable display order).
1982
+ */
1983
+ declare function listUsersWithEnvelopes<T = unknown>(adapter: NoydbStore, vault: string, userEnvelopeDek: CryptoKey): Promise<Array<{
1984
+ user: UserInfo;
1985
+ envelope: UserEnvelope<T> | null;
1986
+ }>>;
1820
1987
  /**
1821
1988
  * Check whether a keyring is authorised for a given `@noy-db/as-*`
1822
1989
  * export tier.
@@ -2715,6 +2882,347 @@ declare class SyncEngine {
2715
2882
  private persistMeta;
2716
2883
  }
2717
2884
 
2885
+ /**
2886
+ * Tier-1 change flows — `rotatePassphrase` (user remembers old) and
2887
+ * `recoverPassphrase` (user supplies a recovery proof). Issue #10.
2888
+ *
2889
+ * The two flows share the post-verification half — fresh salt, fresh
2890
+ * KEK, rewrap every DEK — and differ only in how they re-derive the
2891
+ * old KEK:
2892
+ *
2893
+ * - **Rotate**: derive from the supplied `oldPassphrase`.
2894
+ * - **Recover (paper)**: unwrap from a `RecoveryCodeEntry` using a
2895
+ * user-supplied recovery code. The entry is burned on success.
2896
+ *
2897
+ * The non-paper recovery profiles (Shamir, multi-channel,
2898
+ * admin-mediated) are not yet wired — calling them throws
2899
+ * {@link RecoveryProfileNotImplementedError} with a tracking link.
2900
+ *
2901
+ * @module
2902
+ */
2903
+
2904
+ /** Caller payload for {@link rotatePassphrase}. */
2905
+ interface RotatePassphraseInput {
2906
+ readonly oldPassphrase: string;
2907
+ readonly newPassphrase: string;
2908
+ readonly passphrasePolicy?: PassphrasePolicy;
2909
+ readonly allowWeakPassphrase?: boolean;
2910
+ }
2911
+ /**
2912
+ * Re-derive the user's KEK from `oldPassphrase`, rewrap every DEK
2913
+ * under a freshly-derived KEK from `newPassphrase`, and persist.
2914
+ *
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.
2919
+ *
2920
+ * @throws `InvalidKeyError` if `oldPassphrase` does not unwrap the keyring.
2921
+ * @throws `WeakPassphraseError` if `newPassphrase` fails the strength rule.
2922
+ */
2923
+ declare function rotatePassphrase(store: NoydbStore, vault: string, userId: string, input: RotatePassphraseInput): Promise<UnlockedKeyring>;
2924
+ /** Caller payload for {@link recoverPassphrase}. */
2925
+ type RecoveryProof = {
2926
+ readonly profile: 'paper';
2927
+ readonly payload: {
2928
+ readonly code: string;
2929
+ };
2930
+ } | {
2931
+ readonly profile: 'shamir';
2932
+ readonly payload: {
2933
+ readonly shares: ReadonlyArray<string>;
2934
+ };
2935
+ } | {
2936
+ readonly profile: 'multi-channel';
2937
+ readonly payload: {
2938
+ readonly proofs: ReadonlyArray<unknown>;
2939
+ };
2940
+ } | {
2941
+ readonly profile: 'admin-mediated';
2942
+ readonly payload: {
2943
+ readonly token: string;
2944
+ readonly factor?: unknown;
2945
+ };
2946
+ };
2947
+ interface RecoverPassphraseInput {
2948
+ readonly newPassphrase: string;
2949
+ readonly recoveryProof: RecoveryProof;
2950
+ readonly passphrasePolicy?: PassphrasePolicy;
2951
+ readonly allowWeakPassphrase?: boolean;
2952
+ }
2953
+ /**
2954
+ * Reset the user's passphrase using a recovery proof. v0.1.0-pre.5
2955
+ * supports the `'paper'` profile via `@noy-db/on-recovery` entries
2956
+ * persisted in `_meta/recovery-paper`. The other three profiles throw
2957
+ * {@link RecoveryProfileNotImplementedError}.
2958
+ *
2959
+ * On success, the used recovery entry is burned (deleted from the
2960
+ * stored set).
2961
+ */
2962
+ declare function recoverPassphrase(store: NoydbStore, vault: string, userId: string, input: RecoverPassphraseInput): Promise<UnlockedKeyring>;
2963
+
2964
+ /**
2965
+ * Recovery profile persistence + dispatch — issue #10.
2966
+ *
2967
+ * v0.1.0-pre.5 wires the **paper** profile end-to-end through
2968
+ * `@noy-db/on-recovery`. The other three profiles (Shamir,
2969
+ * multi-channel, admin-mediated) ship the API surface and throw
2970
+ * {@link RecoveryProfileNotImplementedError} during use; per-profile
2971
+ * dispatch lands in follow-up issues.
2972
+ *
2973
+ * Storage layout:
2974
+ *
2975
+ * ```
2976
+ * _meta/recovery-paper — JSON { entries: RecoveryCodeEntry[] } produced by `on-recovery`.
2977
+ * _meta/recovery-shamir — reserved
2978
+ * _meta/recovery-multi — reserved
2979
+ * _meta/recovery-admin — reserved
2980
+ * ```
2981
+ *
2982
+ * Like `_meta/policy` and `_meta/handle`, the documents are plain JSON
2983
+ * with empty `_iv` — the recovery-code wrapping is what protects the
2984
+ * KEK; the entries themselves are inert without the user's code.
2985
+ *
2986
+ * @module
2987
+ */
2988
+
2989
+ /**
2990
+ * One paper recovery code as persisted in `_meta/recovery-paper`.
2991
+ *
2992
+ * The hub's KEK is intentionally non-extractable (see `crypto.ts`),
2993
+ * so the recovery entry can't AES-KW-wrap the KEK directly. Instead
2994
+ * we wrap a serialized DEK set: the entry holds the AES-GCM
2995
+ * ciphertext of `{ deks: { collection: rawDekBase64 } }`. Recovery
2996
+ * deserializes the DEK set, then mints a fresh KEK from the new
2997
+ * passphrase and rewraps the DEKs under it.
2998
+ *
2999
+ * This is the same pattern `@noy-db/on-pin` uses for tier-3 quick
3000
+ * resume — the cryptographic guarantee is identical (AES-GCM with a
3001
+ * PBKDF2-derived key), and it sidesteps the non-extractable-KEK
3002
+ * constraint cleanly.
3003
+ */
3004
+ interface PaperRecoveryEntry {
3005
+ 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
+ readonly enrolledAt: string;
3013
+ }
3014
+ interface PaperRecoveryDoc {
3015
+ readonly _noydb_recovery: 1;
3016
+ readonly profile: 'paper';
3017
+ readonly entries: ReadonlyArray<PaperRecoveryEntry>;
3018
+ }
3019
+ /** Read the paper-recovery entries. Returns empty array when absent. */
3020
+ declare function loadPaperRecoveryEntries(store: NoydbStore, vault: string): Promise<ReadonlyArray<PaperRecoveryEntry>>;
3021
+ /** Replace the paper-recovery entries (used after burn-on-recovery). */
3022
+ declare function savePaperRecoveryEntries(store: NoydbStore, vault: string, entries: ReadonlyArray<PaperRecoveryEntry>): Promise<void>;
3023
+ /** Drop a single paper-recovery entry (burn-on-use). */
3024
+ declare function burnPaperRecoveryEntry(store: NoydbStore, vault: string, codeId: string): Promise<void>;
3025
+ /** Whether at least one recovery profile has any enrolled entries. */
3026
+ declare function hasRecoveryEnrolled(store: NoydbStore, vault: string): Promise<boolean>;
3027
+
3028
+ /**
3029
+ * Public envelope — owner-curated plaintext metadata, readable
3030
+ * before vault unlock or bundle decryption.
3031
+ *
3032
+ * @see docs/subsystems/public-envelope.md
3033
+ *
3034
+ * @module
3035
+ */
3036
+ /**
3037
+ * Either a single string (used when the developer's app is
3038
+ * single-locale) or a locale → string map for i18n. Mirrors the
3039
+ * shape `@noy-db/hub/i18n` already uses for record fields, so the
3040
+ * existing `resolveI18nText` resolver applies.
3041
+ */
3042
+ type PublicEnvelopeText = string | Record<string, string>;
3043
+ /**
3044
+ * Persisted shape — both `_meta/public-envelope` and the bundle
3045
+ * header carry this. The version number is monotonic per vault and
3046
+ * helps cache invalidators detect change without hashing the JSON.
3047
+ */
3048
+ interface PublicEnvelope {
3049
+ readonly _noydb_public: 1;
3050
+ readonly version: number;
3051
+ readonly name?: PublicEnvelopeText;
3052
+ readonly description?: PublicEnvelopeText;
3053
+ /** Inline `data:` URL (`data:image/png;base64,…` or `data:image/svg+xml;base64,…`). */
3054
+ readonly icon?: string;
3055
+ /** ISO-8601 timestamp; auto-set on first envelope write, immutable thereafter. */
3056
+ readonly createdAt?: string;
3057
+ /** ISO-8601 timestamp; auto-updated on every `setPublicEnvelope` call. */
3058
+ readonly updatedAt?: string;
3059
+ /** BCP-47 fallback locale for renderers when the user's locale isn't covered. */
3060
+ readonly defaultLocale?: string;
3061
+ }
3062
+ /** Field names the developer can allow in `PublicEnvelopeSchema.fields`. */
3063
+ declare const PUBLIC_ENVELOPE_FIELDS: readonly ["name", "description", "icon", "createdAt", "updatedAt", "defaultLocale"];
3064
+ type PublicEnvelopeField = (typeof PUBLIC_ENVELOPE_FIELDS)[number];
3065
+ /**
3066
+ * Build-time schema. The developer enables the feature and bounds
3067
+ * what the owner can set. `true` is shorthand for "all defaults" —
3068
+ * gives the owner the full field set with the standard caps.
3069
+ */
3070
+ interface PublicEnvelopeSchema {
3071
+ /**
3072
+ * Allowed field names. Setting `name`/`description`/`icon`/`defaultLocale` is
3073
+ * gated on the field being listed here. `createdAt` / `updatedAt` are managed
3074
+ * by the hub; including them is a no-op (the owner cannot set them
3075
+ * directly). Default: every field above.
3076
+ */
3077
+ readonly fields?: ReadonlyArray<PublicEnvelopeField>;
3078
+ /**
3079
+ * Maximum icon size — measured as the length of the data-URL
3080
+ * string. Default 256 KB.
3081
+ */
3082
+ readonly maxIconBytes?: number;
3083
+ /** Allowed icon MIME types. Default ['image/png', 'image/svg+xml']. */
3084
+ readonly iconMimeTypes?: ReadonlyArray<string>;
3085
+ /** Maximum length of `name` / `description` per locale. Default 200. */
3086
+ readonly maxStringChars?: number;
3087
+ }
3088
+ /** Default schema values; merged onto every developer-supplied schema. */
3089
+ declare const DEFAULT_PUBLIC_ENVELOPE_SCHEMA: {
3090
+ readonly fields: readonly ["name", "description", "icon", "createdAt", "updatedAt", "defaultLocale"];
3091
+ readonly maxIconBytes: number;
3092
+ readonly iconMimeTypes: readonly ["image/png", "image/svg+xml"];
3093
+ readonly maxStringChars: 200;
3094
+ };
3095
+ /** Resolved schema after merging developer override onto defaults. */
3096
+ interface ResolvedPublicEnvelopeSchema {
3097
+ readonly fields: ReadonlyArray<PublicEnvelopeField>;
3098
+ readonly maxIconBytes: number;
3099
+ readonly iconMimeTypes: ReadonlyArray<string>;
3100
+ readonly maxStringChars: number;
3101
+ }
3102
+ /**
3103
+ * Merge developer schema onto the defaults. The shorthand `true`
3104
+ * resolves to the full default schema; an explicit object only
3105
+ * overrides the keys it provides.
3106
+ */
3107
+ declare function resolveSchema(schema: true | PublicEnvelopeSchema | undefined): ResolvedPublicEnvelopeSchema | undefined;
3108
+
3109
+ /** Owner-supplied input — the subset of {@link PublicEnvelope} the owner can set. */
3110
+ interface SetPublicEnvelopeInput {
3111
+ readonly name?: PublicEnvelopeText;
3112
+ readonly description?: PublicEnvelopeText;
3113
+ readonly icon?: string;
3114
+ readonly defaultLocale?: string;
3115
+ }
3116
+ /**
3117
+ * Validate an owner-supplied envelope input against the developer's
3118
+ * resolved schema. Throws `ValidationError` on the first violation;
3119
+ * returns void on success.
3120
+ *
3121
+ * The validator is deliberately strict: every fail mode is a hard
3122
+ * error rather than a silent drop, so the owner finds out immediately
3123
+ * which field they oversized rather than discovering a truncated
3124
+ * label months later.
3125
+ */
3126
+ declare function validatePublicEnvelopeInput(input: SetPublicEnvelopeInput, schema: ResolvedPublicEnvelopeSchema): void;
3127
+ /**
3128
+ * Lightweight runtime predicate — used by the bundle header
3129
+ * validator to recognise a public envelope without requiring it.
3130
+ */
3131
+ declare function isPublicEnvelope(x: unknown): x is PublicEnvelope;
3132
+
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
+ /**
3179
+ * Per-vault tier-3 (PIN / quick-resume) state — issue #11.
3180
+ *
3181
+ * The hub holds a `PinResumeState`-shaped record in memory, keyed by
3182
+ * vault. `enrollUnlock` populates it; `unlockViaPin` consumes it via
3183
+ * `@noy-db/on-pin`'s `resumePin`. The cached state is wiped when the
3184
+ * idle timer fires or `db.close()` is called.
3185
+ *
3186
+ * Importantly, this module does NOT depend on `@noy-db/on-pin` — the
3187
+ * caller passes the already-built state in. That keeps the hub's
3188
+ * `peerDependencies` empty for tier-3 and lets developers swap the
3189
+ * primitive (e.g. an OS biometric in place of a PIN).
3190
+ *
3191
+ * @module
3192
+ */
3193
+ /**
3194
+ * Opaque `PinResumeState`-compatible record. Mirrored from
3195
+ * `@noy-db/on-pin/PinResumeState`. The hub treats the contents as
3196
+ * a black box.
3197
+ */
3198
+ interface QuickUnlockState {
3199
+ readonly _noydb_on_pin: 1;
3200
+ readonly salt: string;
3201
+ readonly iv: string;
3202
+ readonly wrappedKeyring: string;
3203
+ readonly expiresAt: string;
3204
+ readonly maxAttempts: number;
3205
+ attempts: number;
3206
+ }
3207
+ /** In-memory store for tier-3 unlock state, keyed by vault. */
3208
+ declare class QuickUnlockStore {
3209
+ private readonly states;
3210
+ private readonly timers;
3211
+ /**
3212
+ * Register a quick-unlock state for a vault. Replaces any existing
3213
+ * state. Schedules an automatic clear when the state's `expiresAt`
3214
+ * elapses.
3215
+ */
3216
+ set(vault: string, state: QuickUnlockState): void;
3217
+ /** Read the state for a vault. Returns undefined when none is registered. */
3218
+ get(vault: string): QuickUnlockState | undefined;
3219
+ /** Drop the state for a vault. Cancels the auto-clear timer. */
3220
+ delete(vault: string): void;
3221
+ /** Drop every cached state. Called on `db.close()`. */
3222
+ clear(): void;
3223
+ private clearTimer;
3224
+ }
3225
+
2718
3226
  /**
2719
3227
  * Multi-record atomic transactions.
2720
3228
  *
@@ -2853,6 +3361,88 @@ declare class TxCollection<T> {
2853
3361
  */
2854
3362
  declare function runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T> | T): Promise<T>;
2855
3363
 
3364
+ /**
3365
+ * Policy gate DSL types — issue #9.
3366
+ *
3367
+ * Sensitive operations (rotate the passphrase, enroll an authenticator,
3368
+ * export plaintext, grant a user, …) are gated by a typed policy
3369
+ * object. The developer supplies a {@link VaultPolicy} at vault
3370
+ * creation; the hub merges it onto a built-in preset and persists the
3371
+ * merged document at `_meta/policy`.
3372
+ *
3373
+ * @see docs/subsystems/session-tiers.md → Policy gates DSL
3374
+ *
3375
+ * @module
3376
+ */
3377
+
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';
3380
+ /**
3381
+ * One factor requirement entry. The default is "any one of the listed
3382
+ * factors, fresh within the last 5 minutes". Bumping `count` requires N
3383
+ * distinct fresh proofs; bumping `freshnessMs` widens the acceptance
3384
+ * window.
3385
+ */
3386
+ interface FactorRequirement {
3387
+ readonly anyOf: ReadonlyArray<FactorKind>;
3388
+ /** Number of distinct factors required. Default 1. */
3389
+ readonly count?: number;
3390
+ /** How recent each proof must be. Default 5 minutes. */
3391
+ readonly freshnessMs?: number;
3392
+ }
3393
+ /** Soft signals layered on top of the gate verdict — never block on their own. */
3394
+ interface WarningRules {
3395
+ /** Behavior on shared-device tier-1 ops. `'block'` raises a `PolicyDeniedError`. */
3396
+ readonly sharedDevice?: 'warn' | 'block';
3397
+ /** Behavior on weak tier-2 (e.g. password-only) for sensitive ops. */
3398
+ readonly weakAuthenticator?: 'warn' | 'block';
3399
+ }
3400
+ /**
3401
+ * Policy applied to one named gate. `enabled: false` disables the
3402
+ * action entirely (useful in managed-passphrase mode where rotation is
3403
+ * impossible by construction).
3404
+ */
3405
+ interface GatePolicy {
3406
+ /** Minimum tier the active session must hold. */
3407
+ readonly minTier: 1 | 2 | 3;
3408
+ /** Extra freshness-bound proofs required at gate time. */
3409
+ readonly factors?: ReadonlyArray<FactorRequirement>;
3410
+ readonly warn?: WarningRules;
3411
+ readonly enabled?: boolean;
3412
+ }
3413
+ /**
3414
+ * Built-in gate names. App-defined gates live in the `app:*` namespace
3415
+ * and use the same engine; the engine treats unknown names with no
3416
+ * configured policy as "no gate" (no-op).
3417
+ */
3418
+ type BuiltInGateName = 'rotate-passphrase' | 'recover-passphrase' | 'enroll-authenticator' | 'remove-authenticator' | 'rotate-unlock' | 'enroll-user' | 'revoke-user' | 'export-bundle' | 'export-plaintext' | 'view-user-auth'
3419
+ /** Authorize a write to one's own user envelope (#22). */
3420
+ | 'edit-own-profile'
3421
+ /** Authorize reading other principals' user envelopes (#22). */
3422
+ | 'view-team-profiles';
3423
+ /** Either a built-in gate name or an `app:*` custom gate. */
3424
+ type GateName = BuiltInGateName | `app:${string}`;
3425
+ /**
3426
+ * Top-level policy object. Persisted at `_meta/policy` once at vault
3427
+ * creation. The `passphrase` block configures the strength rules
3428
+ * applied at every passphrase ingress (issue #7); `gates` configures
3429
+ * the action-level requirements.
3430
+ */
3431
+ interface VaultPolicy {
3432
+ readonly passphrase?: PassphrasePolicy;
3433
+ readonly gates: Partial<Record<GateName, GatePolicy>>;
3434
+ }
3435
+ /** Concrete proof an actor presents to {@link checkGate}. */
3436
+ interface FactorProof {
3437
+ readonly kind: FactorKind;
3438
+ /** ISO-8601 timestamp the proof was minted at. Compared against `freshnessMs`. */
3439
+ readonly mintedAt?: string;
3440
+ /** Method-specific payload. The engine treats it as opaque — verification is delegated. */
3441
+ readonly payload?: unknown;
3442
+ }
3443
+ /** Active session tier — what the engine compares against `gate.minTier`. */
3444
+ type ActiveTier = 1 | 2 | 3;
3445
+
2856
3446
  /** The top-level NOYDB instance. */
2857
3447
  declare class Noydb {
2858
3448
  private readonly options;
@@ -2860,6 +3450,25 @@ declare class Noydb {
2860
3450
  private readonly vaultCache;
2861
3451
  private readonly keyringCache;
2862
3452
  private readonly syncEngines;
3453
+ /**
3454
+ * Per-vault active session tier — defaults to `1` after a passphrase
3455
+ * unlock; tier-2 / tier-3 unlocks (issue #11) downgrade it. Used by
3456
+ * {@link checkGate} to evaluate `gate.minTier`.
3457
+ */
3458
+ private readonly activeTier;
3459
+ /**
3460
+ * Per-vault loaded policy. Cached after the first
3461
+ * `_meta/policy` load; replaced by `db.updatePolicy()`.
3462
+ */
3463
+ private readonly policyCache;
3464
+ /** Per-vault tier-3 (PIN / quick-resume) state — issue #11. */
3465
+ private readonly quickUnlock;
3466
+ /**
3467
+ * Resolved public-envelope schema. Lazily computed once from
3468
+ * `NoydbOptions.publicEnvelope`; `undefined` when the developer
3469
+ * didn't opt in.
3470
+ */
3471
+ private readonly publicEnvelopeSchema;
2863
3472
  private closed;
2864
3473
  private sessionTimer;
2865
3474
  /** Per-vault policy enforcers. */
@@ -3095,6 +3704,28 @@ declare class Noydb {
3095
3704
  private getSyncEngine;
3096
3705
  on<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
3097
3706
  off<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
3707
+ /**
3708
+ * Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
3709
+ * instance, sync engine, policy enforcer, and active-tier entry —
3710
+ * WITHOUT destroying the `Noydb` instance.
3711
+ *
3712
+ * Designed for "lock screen" UX: the user taps **Lock** and DEKs are
3713
+ * scrubbed from memory immediately, but the same `Noydb` instance can
3714
+ * be re-unlocked via {@link unlockViaAuthenticator} (tier 2) or
3715
+ * {@link unlockViaPin} (tier 3) without re-running `createNoydb`.
3716
+ *
3717
+ * **QuickUnlock state is preserved.** That's the whole point — the
3718
+ * user can still resume via PIN without a full credential re-prompt.
3719
+ * The on-disk `_meta/policy` document is also kept in cache (it
3720
+ * survives lock; nothing about it changes when DEKs are scrubbed).
3721
+ *
3722
+ * No-op when `vault` is not currently in cache (idempotent).
3723
+ *
3724
+ * Unblocks vLannaAi/niwat#33.
3725
+ *
3726
+ * @see #17
3727
+ */
3728
+ lockVault(vault: string): void;
3098
3729
  close(): void;
3099
3730
  /**
3100
3731
  * Returns a snapshot of all translator invocations since the last
@@ -3113,6 +3744,278 @@ declare class Noydb {
3113
3744
  * @internal — not part of the public API surface
3114
3745
  */
3115
3746
  invokeTranslator(text: string, from: string, to: string, field: string, collection: string): Promise<string>;
3747
+ /**
3748
+ * Read the active policy for a vault. Loads from `_meta/policy` on
3749
+ * first call; subsequent calls hit the in-memory cache. Throws
3750
+ * `ValidationError` if the vault has not been opened.
3751
+ */
3752
+ getPolicy(vault: string): Promise<VaultPolicy>;
3753
+ /**
3754
+ * Replace the policy document at `_meta/policy` and update the
3755
+ * in-memory cache. Gated by the `enroll-user` policy (a policy
3756
+ * change is fundamentally a privilege-management action).
3757
+ */
3758
+ updatePolicy(vault: string, override: Partial<VaultPolicy>): Promise<VaultPolicy>;
3759
+ /**
3760
+ * Evaluate a policy gate against the active session tier and the
3761
+ * presented factor proofs. Throws {@link PolicyDeniedError} on
3762
+ * denial; resolves with `void` on success.
3763
+ *
3764
+ * @param vault The vault whose policy applies.
3765
+ * @param gate Gate name — built-in (e.g. `'rotate-passphrase'`)
3766
+ * or app-defined (`app:*`).
3767
+ * @param presented Caller-supplied factor proofs.
3768
+ */
3769
+ checkGate(vault: string, gate: GateName, presented?: {
3770
+ factors?: ReadonlyArray<FactorProof>;
3771
+ sharedDevice?: boolean;
3772
+ }): Promise<void>;
3773
+ /** Read or persist the vault policy at `_meta/policy` on first open. */
3774
+ private bootstrapPolicy;
3775
+ /**
3776
+ * Throw {@link RecoveryNotEnrolledError} when the developer
3777
+ * explicitly opts into strict mandatory-recovery enforcement
3778
+ * (`createNoydb({ requireRecovery: true })`) and no recovery
3779
+ * entries are persisted.
3780
+ *
3781
+ * The default behavior is lenient — `recover-passphrase` is enabled
3782
+ * in `PERSONAL_POLICY` but the hub does not block vault open on
3783
+ * missing enrollment. v1.0 will flip the default to strict; for now,
3784
+ * apps that want the spec-mandated check turn it on per-vault.
3785
+ */
3786
+ private assertRecoveryEnrolled;
3787
+ /**
3788
+ * Internal accessor used by tier-2/tier-3 unlock paths (issue #11)
3789
+ * to mark the active session tier.
3790
+ * @internal
3791
+ */
3792
+ _setActiveTier(vault: string, tier: ActiveTier): void;
3793
+ /**
3794
+ * Add a tier-2 authenticator slot to the calling user's keyring.
3795
+ * Each slot independently wraps the SAME KEK under a method-specific
3796
+ * key — adding a slot is a constant-time keyring write.
3797
+ *
3798
+ * The wrapping ciphertext is produced by the corresponding
3799
+ * `@noy-db/on-*` package (e.g. `enrollPasswordAuthenticator` from
3800
+ * `@noy-db/on-password`); the hub persists the result.
3801
+ *
3802
+ * Gated by `enroll-authenticator`; `presented` carries any factor
3803
+ * proofs the active policy demands.
3804
+ */
3805
+ enrollAuthenticator(vault: string, options: EnrollAuthenticatorOptions, presented?: {
3806
+ factors?: ReadonlyArray<FactorProof>;
3807
+ sharedDevice?: boolean;
3808
+ }): Promise<void>;
3809
+ /**
3810
+ * Remove a tier-2 authenticator slot. Idempotent — removing a
3811
+ * non-existent slot is a successful no-op. Gated by
3812
+ * `remove-authenticator`.
3813
+ */
3814
+ removeAuthenticator(vault: string, slotId: string, presented?: {
3815
+ factors?: ReadonlyArray<FactorProof>;
3816
+ sharedDevice?: boolean;
3817
+ }): Promise<void>;
3818
+ /** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
3819
+ listAuthenticators(vault: string): Promise<ReadonlyArray<KeyringAuthenticator>>;
3820
+ /**
3821
+ * Native WebAuthn enrollment using the **real** internal keyring (#16).
3822
+ *
3823
+ * Why this exists: when a consumer is using `createNoydb({ secret })`,
3824
+ * they cannot reach the live `UnlockedKeyring` to feed it to
3825
+ * `enrollWebAuthn(keyring, vault, opts)` from `@noy-db/on-webauthn`.
3826
+ * Constructing a synthetic keyring (the previous workaround) produces
3827
+ * a slot whose `wrapped_kek` references the synthetic payload, not
3828
+ * the live session — so `unlockViaAuthenticator()` later replaces the
3829
+ * live DEK map with stale wrapped DEKs and every decrypt fails.
3830
+ *
3831
+ * This method runs `ceremony` with the REAL keyring (still in
3832
+ * `keyringCache`). The ceremony performs the WebAuthn enrollment and
3833
+ * returns the slot options that hub then persists via the standard
3834
+ * tier-2 enrollAuthenticator path.
3835
+ *
3836
+ * Layering note: hub does not import `@noy-db/on-webauthn` (that
3837
+ * would invert the dep graph). The consumer wires it in:
3838
+ *
3839
+ * ```ts
3840
+ * import { enrollWebAuthn } from '@noy-db/on-webauthn'
3841
+ *
3842
+ * await db.enrollWebAuthn('demo', async (keyring) => {
3843
+ * const e = await enrollWebAuthn(keyring, 'demo', { rp: {...} })
3844
+ * return {
3845
+ * id: `webauthn-${e.credentialId.slice(0, 8)}`,
3846
+ * method: 'webauthn',
3847
+ * wrapped_kek: e.wrappedPayload,
3848
+ * meta: {
3849
+ * credentialId: e.credentialId,
3850
+ * wrapIv: e.wrapIv,
3851
+ * prfUsed: e.prfUsed,
3852
+ * beFlag: e.beFlag,
3853
+ * requireSingleDevice: e.requireSingleDevice,
3854
+ * },
3855
+ * }
3856
+ * })
3857
+ * ```
3858
+ *
3859
+ * Returns the WebAuthn `credentialId` (extracted from `meta.credentialId`)
3860
+ * for the caller's lookup index (a bootstrap vault, a PublicEnvelope,
3861
+ * a server-side allowlist).
3862
+ *
3863
+ * Gated by `enroll-authenticator` like `enrollAuthenticator()` itself.
3864
+ *
3865
+ * @see #16
3866
+ */
3867
+ enrollWebAuthn(vault: string, ceremony: (keyring: UnlockedKeyring) => Promise<EnrollAuthenticatorOptions>, presented?: {
3868
+ factors?: ReadonlyArray<FactorProof>;
3869
+ sharedDevice?: boolean;
3870
+ }): Promise<{
3871
+ credentialId: string;
3872
+ }>;
3873
+ /**
3874
+ * Filter the slot list to webauthn-method slots only. Useful for
3875
+ * "you have N WebAuthn credentials enrolled" UI surfaces and for
3876
+ * deciding when a new device prompt should appear. Identity is
3877
+ * `id` + `enrolled_at`; the `meta.credentialId` (base64) is used by
3878
+ * `allowCredentials` at unlock time.
3879
+ *
3880
+ * @see #16
3881
+ */
3882
+ listWebAuthnSlots(vault: string): Promise<ReadonlyArray<{
3883
+ id: string;
3884
+ enrolledAt: string;
3885
+ credentialId: string;
3886
+ }>>;
3887
+ /**
3888
+ * Resolve a slot by id, then hand the wrapped-KEK ciphertext + meta
3889
+ * to the caller-supplied verifier. The verifier is the
3890
+ * `unlockWith*` function from the corresponding `@noy-db/on-*`
3891
+ * package, e.g. `unlockWithPassword(slot, password)`.
3892
+ *
3893
+ * On success, mark the active session tier as 2 — subsequent
3894
+ * `checkGate` calls see a tier-2 unlock.
3895
+ */
3896
+ unlockViaAuthenticator(vault: string, slotId: string, verify: (slot: KeyringAuthenticator) => Promise<UnlockedKeyring>): Promise<UnlockedKeyring>;
3897
+ /**
3898
+ * Set the owner-curated public envelope for a vault. Throws
3899
+ * `ValidationError` if the developer did not opt the hub into
3900
+ * `publicEnvelope` via `NoydbOptions`, or if the input violates
3901
+ * the resolved schema (oversized icon, disallowed MIME, oversized
3902
+ * string, unknown field).
3903
+ *
3904
+ * `createdAt` is set on the first write and preserved on every
3905
+ * subsequent write. `updatedAt` is refreshed on every write.
3906
+ * `version` is monotonic — increments on every successful write.
3907
+ */
3908
+ setPublicEnvelope(vault: string, input: SetPublicEnvelopeInput): Promise<PublicEnvelope>;
3909
+ /**
3910
+ * Read the public envelope for a vault. Returns `undefined` when
3911
+ * none has been written. Pass `locale` to resolve any locale-map
3912
+ * fields to plain strings; omitting `locale` returns the raw map.
3913
+ *
3914
+ * Works even when the developer didn't enable
3915
+ * `publicEnvelope` — reads are passive and never throw on a
3916
+ * missing schema (the envelope is plaintext and exists on disk
3917
+ * regardless).
3918
+ */
3919
+ getPublicEnvelope(vault: string, opts?: {
3920
+ readonly locale?: string;
3921
+ }): Promise<PublicEnvelope | undefined>;
3922
+ /** English summary of the configured auth model. */
3923
+ describeAuthConfig(vault: string): Promise<string>;
3924
+ /** Mermaid `flowchart TB` source for the auth graph. */
3925
+ diagramAuthConfig(vault: string): Promise<string>;
3926
+ /**
3927
+ * Per-user enrollment summary. Gated by `view-user-auth` (default:
3928
+ * disabled). Sanitization is allowlist-based — never renders cred
3929
+ * ids, password hashes, secrets, or any field outside the allowlist.
3930
+ */
3931
+ describeUserAuth(vault: string, userId: string, factors?: {
3932
+ factors?: ReadonlyArray<FactorProof>;
3933
+ sharedDevice?: boolean;
3934
+ }): Promise<string>;
3935
+ /** Bulk variant for owner dashboards. Gated by `view-user-auth`. */
3936
+ describeAllUsersAuth(vault: string, factors?: {
3937
+ factors?: ReadonlyArray<FactorProof>;
3938
+ sharedDevice?: boolean;
3939
+ }): Promise<Array<{
3940
+ userId: string;
3941
+ description: string;
3942
+ }>>;
3943
+ /**
3944
+ * Rotate the user's passphrase (user remembers old). Validates the
3945
+ * new phrase against the configured `passphrase` policy, runs the
3946
+ * `rotate-passphrase` gate, then re-derives + re-wraps every DEK.
3947
+ *
3948
+ * Tier-2 authenticator slots are dropped — each slot wraps the old
3949
+ * KEK and would need its derivation key to be re-presented. Re-enrol
3950
+ * via `db.enrollAuthenticator` after rotation. Tracked as a
3951
+ * v0.1.0-pre.5 limitation.
3952
+ *
3953
+ * @throws `WeakPassphraseError` on a weak new phrase.
3954
+ * @throws `PolicyDeniedError` when the gate denies (missing factor, …).
3955
+ * @throws `InvalidKeyError` when `oldPassphrase` is wrong.
3956
+ */
3957
+ rotatePassphrase(vault: string, input: RotatePassphraseInput, factors?: {
3958
+ factors?: ReadonlyArray<FactorProof>;
3959
+ sharedDevice?: boolean;
3960
+ }): Promise<void>;
3961
+ /**
3962
+ * Reset the passphrase using a recovery proof (user forgot the old).
3963
+ * v0.1.0-pre.5 supports the `'paper'` profile end-to-end; the
3964
+ * other three profiles throw {@link RecoveryProfileNotImplementedError}.
3965
+ *
3966
+ * Burns the used recovery entry on success.
3967
+ */
3968
+ recoverPassphrase(vault: string, input: RecoverPassphraseInput, factors?: {
3969
+ factors?: ReadonlyArray<FactorProof>;
3970
+ sharedDevice?: boolean;
3971
+ }): Promise<void>;
3972
+ /**
3973
+ * 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.
3978
+ *
3979
+ * ```ts
3980
+ * import { generateRecoveryCodeSet } from '@noy-db/on-recovery'
3981
+ * const { codes, entries } = await generateRecoveryCodeSet({ kek, count: 10 })
3982
+ * await db.enrollRecovery('acme', { profile: 'paper', entries })
3983
+ * showCodesToUser(codes)
3984
+ * ```
3985
+ */
3986
+ enrollRecovery(vault: string, enrollment: {
3987
+ profile: 'paper';
3988
+ entries: ReadonlyArray<PaperRecoveryEntry>;
3989
+ }): Promise<void>;
3990
+ /** Read the persisted paper-recovery entries. Used by `describeAuthConfig` (#13). */
3991
+ listRecoveryEntries(vault: string): Promise<{
3992
+ paper: ReadonlyArray<PaperRecoveryEntry>;
3993
+ }>;
3994
+ /**
3995
+ * Register a tier-3 quick-unlock state for the vault. The state is
3996
+ * an opaque blob produced by `@noy-db/on-pin/enrollPin` (or any
3997
+ * compatible primitive). It is held in memory only — never persisted
3998
+ * — and auto-clears when its `expiresAt` elapses.
3999
+ *
4000
+ * Gated by `rotate-unlock` (the same gate covers "set" and "rotate"
4001
+ * because tier-3 is a single-slot rolling secret).
4002
+ */
4003
+ enrollUnlock(vault: string, state: QuickUnlockState, presented?: {
4004
+ factors?: ReadonlyArray<FactorProof>;
4005
+ sharedDevice?: boolean;
4006
+ }): Promise<void>;
4007
+ /**
4008
+ * Resume a session via the registered tier-3 state. The verifier is
4009
+ * `@noy-db/on-pin/resumePin` (or compatible). On success, mark the
4010
+ * active session tier as 3 — every operation must re-authenticate at
4011
+ * tier 2 to elevate.
4012
+ *
4013
+ * Returns `undefined` (caller should fall back to tier 2) when no
4014
+ * tier-3 state is registered.
4015
+ */
4016
+ unlockViaPin(vault: string, resume: (state: QuickUnlockState) => Promise<UnlockedKeyring>): Promise<UnlockedKeyring | undefined>;
4017
+ /** Drop the tier-3 state for a vault — explicit logout. */
4018
+ clearQuickUnlock(vault: string): void;
3116
4019
  /** Get or load the keyring for a vault. */
3117
4020
  private getKeyring;
3118
4021
  }
@@ -3580,6 +4483,170 @@ declare function magicLinkGrantRecordId(token: string, index: number): string;
3580
4483
  */
3581
4484
  declare function isMagicLinkGrantExpired(payload: MagicLinkGrantPayload, now?: Date): boolean;
3582
4485
 
4486
+ /**
4487
+ * Public `vault.user.*` API surface.
4488
+ *
4489
+ * Three families:
4490
+ * - Write-self: `me` / `updateMe` / `setMe` — always target the writer's
4491
+ * own keyringId. **Own-only write rule** is structural — no method
4492
+ * exists to write someone else's envelope.
4493
+ * - Read-anyone: `get` / `list` — read other principals' envelopes
4494
+ * (subject to `view-team-profiles` policy gate, wired in #22).
4495
+ * - Reactive: `subscribe` / `live` — in-process event emission on local
4496
+ * writes. Cross-instance updates land via the team/sync engine and
4497
+ * surface to subscribers when the sync diff replays through this API.
4498
+ *
4499
+ * @see docs/superpowers/specs/2026-05-05-user-envelope-design.md
4500
+ *
4501
+ * @module
4502
+ */
4503
+
4504
+ /**
4505
+ * Recursive partial. Used for `updateMe(patch)` so callers can hand in
4506
+ * deeply-nested partial shapes and have them deep-merged onto the
4507
+ * current envelope.
4508
+ */
4509
+ type DeepPartial<T> = T extends object ? {
4510
+ [P in keyof T]?: DeepPartial<T[P]>;
4511
+ } : T;
4512
+ /** Cancel a previously-registered subscription. */
4513
+ type Unsubscribe = () => void;
4514
+ /**
4515
+ * Optional factor-proof bundle threaded into gated user-envelope
4516
+ * operations. Same shape as `Noydb.checkGate(vault, gate, presented)`
4517
+ * accepts elsewhere — apps that have already presented a TOTP/email-OTP
4518
+ * for this session pass it here to satisfy tightened policies.
4519
+ */
4520
+ interface UserEnvelopePresented {
4521
+ readonly factors?: readonly FactorProof[];
4522
+ readonly sharedDevice?: boolean;
4523
+ }
4524
+ /**
4525
+ * Callback used by `UserApi` to validate the active session against a
4526
+ * policy gate. Provided by the `Vault` constructor; in production this
4527
+ * delegates to `Noydb.checkGate(vault, gate, presented)`. In tests, a
4528
+ * no-op stub is fine.
4529
+ */
4530
+ type UserEnvelopeCheckGate = (gate: 'edit-own-profile' | 'view-team-profiles', presented?: UserEnvelopePresented) => Promise<void>;
4531
+ /**
4532
+ * Reactive handle returned by `live()`. `current` is the most recently
4533
+ * observed value; `subscribe(cb)` fires on subsequent local writes.
4534
+ * `stop()` releases the underlying subscription.
4535
+ */
4536
+ interface LiveUserEnvelope<T> {
4537
+ current(): UserEnvelope<T> | null;
4538
+ subscribe(cb: (env: UserEnvelope<T> | null) => void): Unsubscribe;
4539
+ stop(): void;
4540
+ }
4541
+ /**
4542
+ * Implementation behind `vault.user`. Constructed once per Vault, holds
4543
+ * the writer's keyringId in closure so `updateMe`/`setMe` cannot target
4544
+ * any other principal — the own-only rule is enforced at the type level
4545
+ * (no `set(otherKeyringId, …)` method) AND at runtime (the
4546
+ * keyringId argument simply doesn't exist on the write path).
4547
+ */
4548
+ declare class UserApi {
4549
+ private readonly adapter;
4550
+ private readonly vaultName;
4551
+ /** The writer's own keyringId. Frozen at construction time. */
4552
+ private readonly writerKeyringId;
4553
+ private readonly getDek;
4554
+ /**
4555
+ * Policy-gate validator. When omitted, gates are skipped — useful
4556
+ * for low-level tests that exercise the storage layer directly.
4557
+ * Production paths always wire the Noydb-backed implementation.
4558
+ */
4559
+ private readonly checkGate?;
4560
+ /** keyringId → set of listeners. Wildcard '*' fires on every change. */
4561
+ private readonly listeners;
4562
+ constructor(adapter: NoydbStore, vaultName: string,
4563
+ /** The writer's own keyringId. Frozen at construction time. */
4564
+ writerKeyringId: string, getDek: () => Promise<CryptoKey>,
4565
+ /**
4566
+ * Policy-gate validator. When omitted, gates are skipped — useful
4567
+ * for low-level tests that exercise the storage layer directly.
4568
+ * Production paths always wire the Noydb-backed implementation.
4569
+ */
4570
+ checkGate?: UserEnvelopeCheckGate | undefined);
4571
+ /** Read the writer's own envelope. Returns null if never written. */
4572
+ me<T = unknown>(): Promise<UserEnvelope<T> | null>;
4573
+ /**
4574
+ * Deep-merge a partial patch into the writer's own envelope. Creates
4575
+ * the envelope on first call. Optimistic-concurrency safe — a stale
4576
+ * `_v` (parallel writer on another device) throws `ConflictError`.
4577
+ *
4578
+ * Gated by the `edit-own-profile` policy gate (default `minTier: 3`).
4579
+ * Pass `presented` to satisfy tightened policies that require a
4580
+ * factor proof (e.g. STRICT_POLICY's TOTP requirement).
4581
+ */
4582
+ updateMe<T extends object = Record<string, unknown>>(patch: DeepPartial<T>, presented?: UserEnvelopePresented): Promise<UserEnvelope<T>>;
4583
+ /**
4584
+ * Replace the writer's own envelope with `payload`. Use sparingly —
4585
+ * `updateMe` is the canonical mutation. No `expectedVersion` check;
4586
+ * callers explicitly take last-write-wins semantics.
4587
+ *
4588
+ * Gated by `edit-own-profile`. See `updateMe` for `presented` usage.
4589
+ */
4590
+ setMe<T = unknown>(payload: T, presented?: UserEnvelopePresented): Promise<UserEnvelope<T>>;
4591
+ /**
4592
+ * Read another principal's envelope by their keyringId. Returns null
4593
+ * if the principal exists but has no envelope yet, or if the
4594
+ * keyringId does not exist at all.
4595
+ *
4596
+ * Gated by `view-team-profiles` (default `minTier: 2`) — but ONLY for
4597
+ * cross-principal reads. Reading your own envelope (`keyringId ===
4598
+ * self`) is never gated; that's just `me()` written long-form.
4599
+ */
4600
+ get<T = unknown>(keyringId: string, presented?: UserEnvelopePresented): Promise<UserEnvelope<T> | null>;
4601
+ /**
4602
+ * Read every persisted envelope in the vault. Order is store-defined.
4603
+ *
4604
+ * Gated by `view-team-profiles`. Default policy (`minTier: 2`) lets
4605
+ * any authenticated session read all envelopes. Two privacy-strict
4606
+ * opt-outs:
4607
+ *
4608
+ * - `view-team-profiles.enabled: false` → list() returns only the
4609
+ * caller's own envelope (silent self-fallback, no thrown error).
4610
+ * - `view-team-profiles.minTier: 1` + insufficient tier → throws
4611
+ * `PolicyDeniedError` with `reason: 'insufficient-tier'`. The
4612
+ * caller is expected to elevate, not silently degrade.
4613
+ *
4614
+ * The asymmetry is deliberate: `enabled: false` is a deliberate
4615
+ * design choice ("nobody sees teammate profiles in this app");
4616
+ * `insufficient-tier` is "you need to authenticate further". Different
4617
+ * UX prompts for different intents.
4618
+ */
4619
+ list<T = unknown>(presented?: UserEnvelopePresented): Promise<UserEnvelope<T>[]>;
4620
+ /**
4621
+ * Listen for changes to a specific keyringId's envelope. The callback
4622
+ * fires synchronously after every successful local `updateMe` /
4623
+ * `setMe` for that principal.
4624
+ *
4625
+ * Cross-instance changes (a teammate edits their profile on their
4626
+ * device, the sync engine pulls the diff onto this device) will fire
4627
+ * subscribers when the sync layer replays the write through this API.
4628
+ * In v1, subscribers do NOT fire on raw store changes — wire your sync
4629
+ * layer to call back through `vault.user.setMe` / `updateMe` if you
4630
+ * need that.
4631
+ *
4632
+ * Pass keyringId `'*'` to fire on every change in the vault.
4633
+ */
4634
+ subscribe<T = unknown>(keyringId: string, cb: (env: UserEnvelope<T> | null) => void): Unsubscribe;
4635
+ /**
4636
+ * Reactive handle that caches the current value and re-reads on every
4637
+ * change for the given keyringId. Convenient for framework bindings:
4638
+ *
4639
+ * const live = vault.user.live<UserShape>(vault.userId)
4640
+ * live.subscribe(env => render(env?.data))
4641
+ *
4642
+ * Initial value is `null` until the first `current()` call materializes
4643
+ * it via `vault.user.get()`. Call `stop()` when done to release the
4644
+ * subscription.
4645
+ */
4646
+ live<T = unknown>(keyringId: string): LiveUserEnvelope<T>;
4647
+ private fireChange;
4648
+ }
4649
+
3583
4650
  /** A vault (tenant namespace) containing collections. */
3584
4651
  declare class Vault {
3585
4652
  private readonly adapter;
@@ -3623,6 +4690,19 @@ declare class Vault {
3623
4690
  private readonly i18nStrategy;
3624
4691
  private readonly syncStrategy;
3625
4692
  private getDEK;
4693
+ /**
4694
+ * Per-principal user envelope API.
4695
+ *
4696
+ * - Write-self: `me()`, `updateMe(patch)`, `setMe(payload)` — always
4697
+ * target this vault session's keyringId. There is no method to write
4698
+ * another principal's envelope (own-only write rule, structural).
4699
+ * - Read-anyone: `get(keyringId)`, `list()` — read other principals'
4700
+ * envelopes, subject to the `view-team-profiles` policy gate (#22).
4701
+ * - Reactive: `subscribe(id, cb)`, `live(id)` — fire on local writes.
4702
+ *
4703
+ * @see docs/superpowers/specs/2026-05-05-user-envelope-design.md
4704
+ */
4705
+ readonly user: UserApi;
3626
4706
  /**
3627
4707
  * Optional callback that re-derives an UnlockedKeyring from the
3628
4708
  * adapter using the active user's passphrase. Called by `load()`
@@ -4368,6 +5448,22 @@ declare class Vault {
4368
5448
  * separate vault instances now.
4369
5449
  */
4370
5450
  getBundleHandle(): Promise<string>;
5451
+ /**
5452
+ * Read the owner-curated public envelope for this vault (or
5453
+ * `undefined` if none is persisted). The envelope lives in
5454
+ * `_meta/public-envelope` as plaintext — readable without any KEK
5455
+ * — so `getBundleHandle`-style callers can label a vault before
5456
+ * unlock.
5457
+ *
5458
+ * Mirrors `Noydb.getPublicEnvelope(vault, opts)` but scoped to a
5459
+ * single, already-opened `Vault` instance so the
5460
+ * bundle writer can snapshot it without holding a `Noydb` reference.
5461
+ *
5462
+ * @see docs/subsystems/public-envelope.md
5463
+ */
5464
+ getPublicEnvelope(opts?: {
5465
+ readonly locale?: string;
5466
+ }): Promise<PublicEnvelope | undefined>;
4371
5467
  /**
4372
5468
  * Dump vault as a verifiable encrypted JSON backup string.
4373
5469
  *
@@ -6448,6 +7544,71 @@ interface ImportCapability {
6448
7544
  readonly plaintext?: readonly ExportFormat[];
6449
7545
  readonly bundle?: boolean;
6450
7546
  }
7547
+ /**
7548
+ * Forward-declared on-disk shape for `VaultPolicy` — the actual policy
7549
+ * model lives in `policy/types.ts` (#9). Declared here as `unknown`-typed
7550
+ * map so types.ts has no dependency on the policy module while the
7551
+ * `KeyringFile.policy` field can still round-trip foreign documents.
7552
+ *
7553
+ * @internal
7554
+ */
7555
+ type VaultPolicyOnDisk = Record<string, unknown>;
7556
+ /**
7557
+ * Recovery profile enrolled at vault creation (issue #10).
7558
+ *
7559
+ * - `paper` — `on-recovery` codes (the only end-to-end profile in v0.1.0-pre.5).
7560
+ * - `shamir` / `multi-channel` / `admin-mediated` — API surface ships;
7561
+ * per-profile dispatch lands in follow-up issues. Calling
7562
+ * `db.recoverPassphrase` against these throws
7563
+ * {@link RecoveryProfileNotImplementedError}.
7564
+ */
7565
+ type RecoveryEnrollment = {
7566
+ readonly profile: 'paper';
7567
+ /** Number of single-use codes to print at enrollment. */
7568
+ readonly codes: number;
7569
+ } | {
7570
+ readonly profile: 'shamir';
7571
+ readonly k: number;
7572
+ readonly n: number;
7573
+ readonly trustees: ReadonlyArray<string>;
7574
+ } | {
7575
+ readonly profile: 'multi-channel';
7576
+ readonly email?: string;
7577
+ readonly pin?: boolean;
7578
+ readonly paperCodes?: number;
7579
+ } | {
7580
+ readonly profile: 'admin-mediated';
7581
+ readonly grantorUserId: string;
7582
+ };
7583
+ /**
7584
+ * One tier-2 authenticator slot inside a keyring file. Each slot
7585
+ * independently wraps the SAME KEK under a method-specific derived key
7586
+ * (LUKS pattern). Adding or removing a slot is a constant-time keyring
7587
+ * write — no DEK re-keying required.
7588
+ *
7589
+ * @see docs/subsystems/session-tiers.md → Tier 2 — Authenticate (multi-slot)
7590
+ */
7591
+ interface KeyringAuthenticator {
7592
+ /** Caller-chosen identifier — e.g. `'webauthn-yubikey-blue'`, `'oidc-google'`, `'password-daily'`. */
7593
+ readonly id: string;
7594
+ /** Method family — selects which `@noy-db/on-*` package handles unlock. */
7595
+ readonly method: 'webauthn' | 'oidc' | 'password';
7596
+ /** ISO-8601 timestamp at which the slot was added. */
7597
+ readonly enrolled_at: string;
7598
+ /**
7599
+ * Which session tier ENROLLED this slot. Tier 1 enrolls a fresh slot;
7600
+ * tier 2 may add a sibling slot when the active policy permits.
7601
+ */
7602
+ readonly enrolled_via_tier: 1 | 2;
7603
+ /** Base64 wrapped-KEK ciphertext under the method-derived key. */
7604
+ readonly wrapped_kek: string;
7605
+ /**
7606
+ * Method-specific metadata: WebAuthn cred id, OIDC issuer/sub, PBKDF2
7607
+ * salt for `on-password`, etc. The schema is open by design — the
7608
+ * `@noy-db/on-*` package owns the contents.
7609
+ */
7610
+ readonly meta: Record<string, unknown>;
7611
+ }
6451
7612
  interface KeyringFile {
6452
7613
  readonly _noydb_keyring: typeof NOYDB_KEYRING_VERSION;
6453
7614
  readonly user_id: string;
@@ -6458,6 +7619,23 @@ interface KeyringFile {
6458
7619
  readonly salt: string;
6459
7620
  readonly created_at: string;
6460
7621
  readonly granted_by: string;
7622
+ /**
7623
+ * Tier-2 authenticator slots (multi-slot keyring extension).
7624
+ * Optional / append-only: keyring files written before the
7625
+ * extension load with an empty list. Each slot independently wraps
7626
+ * the same KEK; any one of them unlocks.
7627
+ *
7628
+ * @see KeyringAuthenticator
7629
+ */
7630
+ readonly authenticators?: readonly KeyringAuthenticator[];
7631
+ /**
7632
+ * Per-keyring policy override (reserved). The on-disk format
7633
+ * accepts the field for forward compatibility with the Option C
7634
+ * merge engine deferred to a later release; v1.0 reads only the
7635
+ * vault-level `_meta/policy` document, so this field is parsed and
7636
+ * round-tripped but never enforced.
7637
+ */
7638
+ readonly policy?: VaultPolicyOnDisk;
6461
7639
  /**
6462
7640
  * Optional — authorization spec capability bits. Absent on keyrings written
6463
7641
  * before the RFC implementation. Loading falls back to role-based
@@ -6812,6 +7990,29 @@ interface GrantOptions {
6812
7990
  * is grantable until positively listed; bundle import is denied.
6813
7991
  */
6814
7992
  readonly importCapability?: ImportCapability;
7993
+ /**
7994
+ * Skip phrase-format strength validation (issue #7). Defaults to
7995
+ * false — `grant()` rejects phrases that don't meet the configured
7996
+ * `PassphrasePolicy`. Test fixtures and CLI scripts pass `true`.
7997
+ */
7998
+ readonly allowWeakPassphrase?: boolean;
7999
+ /**
8000
+ * Initial user-envelope payload for the new principal. Sealed under
8001
+ * the same vault DEK (the reserved `_users` collection's DEK) and
8002
+ * persisted alongside the keyring during grant.
8003
+ *
8004
+ * **Bootstrap-only.** Once the new user activates and writes their
8005
+ * own envelope, the own-only write rule kicks in — admins cannot
8006
+ * edit a teammate's envelope after activation. Use this field for
8007
+ * pre-fill at invite time (e.g. "displayName: Bob, locale: en-US")
8008
+ * and let the user take over from there.
8009
+ *
8010
+ * Hub does not introspect the payload; it is JSON-serialized and
8011
+ * encrypted opaquely. Apps own the schema.
8012
+ *
8013
+ * @see docs/superpowers/specs/2026-05-05-user-envelope-design.md → Lifecycle
8014
+ */
8015
+ readonly initialProfile?: unknown;
6815
8016
  }
6816
8017
  interface RevokeOptions {
6817
8018
  readonly userId: string;
@@ -7460,8 +8661,75 @@ interface NoydbOptions {
7460
8661
  * legacy `sessionTimeout` field.
7461
8662
  */
7462
8663
  readonly sessionPolicy?: SessionPolicy;
7463
- /** Validate passphrase strength on creation. Default: true. */
8664
+ /**
8665
+ * Validate passphrase strength against the phrase format
8666
+ * (`@noy-db/hub` issue #7) on first-time keyring creation. When
8667
+ * `true`, weak phrases throw {@link WeakPassphraseError} from
8668
+ * `createNoydb()` / `db.rotatePassphrase()`. Default: `false` for
8669
+ * back-compat in v0.1.x; planned to flip to `true` at v1.0.
8670
+ */
7464
8671
  readonly validatePassphrase?: boolean;
8672
+ /**
8673
+ * Vault-level policy gate document (issue #9). When present, the hub
8674
+ * persists the merged policy at `_meta/policy` on first-time vault
8675
+ * creation and gates sensitive operations (`db.rotatePassphrase`,
8676
+ * `db.export*`, …) against it. Omitted ⇒ the engine uses
8677
+ * {@link PERSONAL_POLICY}. Use {@link STRICT_POLICY} for regulated
8678
+ * deployments.
8679
+ *
8680
+ * The on-disk document is the source of truth — the policy field
8681
+ * is only honored at vault creation; subsequent runs read from
8682
+ * `_meta/policy`. Use `db.updatePolicy()` to change it deliberately.
8683
+ *
8684
+ * Imported from `@noy-db/hub` as a type-only reference; the runtime
8685
+ * import lives in `policy/index.ts`.
8686
+ */
8687
+ readonly policy?: VaultPolicy;
8688
+ /**
8689
+ * Mandatory recovery profile enrollment (issue #10). Vaults with
8690
+ * `recover-passphrase` enabled MUST register at least one profile
8691
+ * before being production-ready, otherwise `createNoydb()` throws
8692
+ * {@link RecoveryNotEnrolledError}. Set
8693
+ * `policy.gates['recover-passphrase'].enabled = false` to
8694
+ * deliberately opt out of recovery (passphrase loss = data loss).
8695
+ *
8696
+ * v0.1.0-pre.5 supports the `'paper'` profile end-to-end. Other
8697
+ * profiles ship the API shape and throw
8698
+ * {@link RecoveryProfileNotImplementedError} during use.
8699
+ */
8700
+ readonly recovery?: ReadonlyArray<RecoveryEnrollment>;
8701
+ /**
8702
+ * When `true`, `createNoydb` rejects vaults with no recovery
8703
+ * entries persisted (per the spec's mandatory-enrollment
8704
+ * requirement). Default `false` for v0.1.x back-compat; planned to
8705
+ * flip to `true` at v1.0. Apps in regulated environments should
8706
+ * turn this on now.
8707
+ */
8708
+ readonly requireRecovery?: boolean;
8709
+ /**
8710
+ * What to do when `openVault` finds an existing keyring in the store that
8711
+ * cannot be decrypted with the supplied credentials (`InvalidKeyError`).
8712
+ *
8713
+ * - `'error'` (default) — propagate the error. The app must prompt the user
8714
+ * to supply the correct credentials or clear both the data and auth stores.
8715
+ * - `'reset'` — delete the stale keyring and re-initialise the vault from
8716
+ * scratch using the current credentials. Use this when the data store can
8717
+ * become detached from the auth store (e.g. the user cleared the IndexedDB
8718
+ * data records but not the keyring row, or a WebAuthn credential was rotated).
8719
+ * **All previously encrypted data is unrecoverable after a reset.**
8720
+ *
8721
+ * Only applies to the passphrase (`secret`) path. When `getKeyring` is used,
8722
+ * the callback is responsible for handling stale-keyring detection itself.
8723
+ */
8724
+ readonly onInvalidKey?: 'error' | 'reset';
8725
+ /**
8726
+ * Enable the public envelope subsystem (`docs/subsystems/public-envelope.md`).
8727
+ * Pass `true` for the default schema (every standard field, 256 KB
8728
+ * icon cap, 200-char text cap), or a `PublicEnvelopeSchema` to
8729
+ * narrow what the owner can set. Off by default — vaults written
8730
+ * by hubs without this option carry no envelope, full stop.
8731
+ */
8732
+ readonly publicEnvelope?: true | PublicEnvelopeSchema;
7465
8733
  /** Audit history configuration. */
7466
8734
  readonly history?: HistoryConfig;
7467
8735
  /**
@@ -7562,4 +8830,4 @@ interface DeleteManyResult {
7562
8830
  }>;
7563
8831
  }
7564
8832
 
7565
- 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 Conflict 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, Vault as aR, type AccessibleVault as aS, BUNDLE_STORE_POLICY as aT, type BundleRecipient as aU, type CacheOptions as aV, type CacheStats as aW, type ChangeEvent as aX, Collection as aY, type CollectionChangeEvent as aZ, type CollectionConflictResolver 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 SessionPolicy as b$, type ConflictPolicy as b0, type ConflictStrategy as b1, type CrossTierAccessEvent as b2, DELEGATIONS_COLLECTION as b3, type DelegationToken as b4, type DeleteManyResult as b5, type DirtyEntry as b6, ELEVATION_AUDIT_COLLECTION as b7, ElevatedHandle as b8, type ExportCapability as b9, NOYDB_KEYRING_VERSION as bA, NOYDB_SYNC_VERSION as bB, Noydb as bC, type NoydbBundleStore as bD, type NoydbEventMap as bE, type NoydbOptions as bF, type Permission as bG, type Permissions as bH, type PlaintextTranslatorContext as bI, type PlaintextTranslatorFn as bJ, PresenceHandle as bK, type PresencePeer as bL, type PullMode as bM, type PullOptions as bN, type PullPolicy as bO, type PullResult as bP, type PushMode as bQ, type PushOptions as bR, type PushPolicy as bS, type PushResult as bT, type PutManyItemOptions as bU, type PutManyOptions as bV, type PutManyResult as bW, type QueryAcrossOptions as bX, type QueryAcrossResult as bY, type ReAuthOperation as bZ, type RevokeOptions as b_, type ExportChunk as ba, type ExportFormat as bb, type ExportStreamOptions as bc, type GhostRecord as bd, type GrantOptions as be, type HistoryConfig as bf, type HistoryEntry as bg, INDEXED_STORE_POLICY as bh, type ImportCapability as bi, type InferOutput as bj, type IssueDelegationOptions as bk, type IssueMagicLinkGrantOptions as bl, type KeyringFile as bm, type ListAccessibleVaultsOptions as bn, type ListPageResult as bo, type LocaleReadOptions as bp, Lru as bq, type LruOptions as br, type LruStats as bs, MAGIC_LINK_CONTENT_INFO_PREFIX as bt, MAGIC_LINK_GRANTS_COLLECTION as bu, MAGIC_LINK_KEK_INFO_PREFIX as bv, type MagicLinkGrantPayload as bw, type MagicLinkGrantRecord as bx, NOYDB_BACKUP_VERSION as by, NOYDB_FORMAT_VERSION as bz, DictionaryHandle as c, type StandardSchemaV1 as c0, type StandardSchemaV1Issue as c1, type StandardSchemaV1SyncResult as c2, type StoreAuth as c3, type StoreAuthKind as c4, type StoreCapabilities as c5, SyncEngine as c6, type SyncMetadata as c7, type SyncPolicy as c8, SyncScheduler as c9, revokeDelegation as cA, revokeMagicLinkGrant as cB, unwrapMagicLinkGrant as cC, validateSchemaInput as cD, validateSchemaOutput as cE, writeMagicLinkGrant as cF, type SyncSchedulerStatus as ca, type SyncStatus as cb, type SyncTarget as cc, type SyncTargetRole as cd, SyncTransaction as ce, type SyncTransactionResult as cf, type TierMode as cg, type TranslatorAuditEntry as ch, type TxOp as ci, type UserInfo as cj, type VaultBackup as ck, type VaultSnapshot as cl, buildRecipientKeyringFile as cm, createNoydb as cn, createStore as co, deriveMagicLinkContentKey as cp, evaluateExportCapability as cq, evaluateImportCapability as cr, hasExportCapability as cs, hasImportCapability as ct, isMagicLinkGrantExpired as cu, issueDelegation as cv, listMagicLinkGrants as cw, loadActiveDelegations as cx, magicLinkGrantRecordId as cy, readMagicLinkGrantRecord as cz, type DictionaryOptions as d, 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 };
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 };