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