@majikah/majik-key 0.7.0 → 1.0.0

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 (55) hide show
  1. package/README.md +566 -175
  2. package/dist/core/backup/index.d.ts +4 -4
  3. package/dist/core/backup/index.js +3 -3
  4. package/dist/core/backup/majik-key-backup.d.ts +3 -3
  5. package/dist/core/backup/majik-key-backup.js +5 -5
  6. package/dist/core/backup/types.d.ts +1 -1
  7. package/dist/core/backup/utils.js +1 -1
  8. package/dist/core/backup/validator.d.ts +1 -1
  9. package/dist/core/backup/validator.js +1 -1
  10. package/dist/core/crypto/constants.d.ts +35 -11
  11. package/dist/core/crypto/constants.js +33 -11
  12. package/dist/core/crypto/crypto-provider.js +8 -8
  13. package/dist/core/crypto/encryption-engine.d.ts +7 -16
  14. package/dist/core/crypto/encryption-engine.js +27 -62
  15. package/dist/core/database/system/identity.d.ts +2 -2
  16. package/dist/core/database/system/identity.js +2 -2
  17. package/dist/core/keys/hkdf-recipe.d.ts +4 -0
  18. package/dist/core/keys/hkdf-recipe.js +26 -0
  19. package/dist/core/keys/key-id.d.ts +50 -0
  20. package/dist/core/keys/key-id.js +64 -0
  21. package/dist/core/keys/key-impls.d.ts +12 -0
  22. package/dist/core/keys/key-impls.js +163 -0
  23. package/dist/core/keys/key-store.d.ts +72 -0
  24. package/dist/core/keys/key-store.js +264 -0
  25. package/dist/core/keys/keypair-handle.d.ts +36 -0
  26. package/dist/core/keys/keypair-handle.js +43 -0
  27. package/dist/core/keys/registry.d.ts +16 -0
  28. package/dist/core/keys/registry.js +144 -0
  29. package/dist/core/keys/types.d.ts +47 -0
  30. package/dist/core/keys/types.js +1 -0
  31. package/dist/core/types.d.ts +12 -1
  32. package/dist/core/utils.d.ts +1 -1
  33. package/dist/core/utils.js +1 -1
  34. package/dist/core/validator.d.ts +1 -1
  35. package/dist/core/validator.js +1 -1
  36. package/dist/core/web3/bitcoin/bitcoin.d.ts +1 -1
  37. package/dist/core/web3/bitcoin/bitcoin.js +4 -4
  38. package/dist/core/web3/bitcoin/types.d.ts +1 -1
  39. package/dist/core/web3/ethereum/constants.d.ts +1 -0
  40. package/dist/core/web3/ethereum/constants.js +4 -0
  41. package/dist/core/web3/ethereum/ethereum.d.ts +21 -0
  42. package/dist/core/web3/ethereum/ethereum.js +91 -0
  43. package/dist/core/web3/ethereum/types.d.ts +32 -0
  44. package/dist/core/web3/ethereum/types.js +1 -0
  45. package/dist/core/web3/index.d.ts +10 -5
  46. package/dist/core/web3/index.js +7 -2
  47. package/dist/core/web3/solana/solana.d.ts +2 -2
  48. package/dist/core/web3/solana/solana.js +6 -6
  49. package/dist/core/web3/solana/types.d.ts +1 -1
  50. package/dist/core/web3/types.d.ts +4 -2
  51. package/dist/index.d.ts +13 -6
  52. package/dist/index.js +11 -5
  53. package/dist/majik-key.d.ts +179 -281
  54. package/dist/majik-key.js +622 -726
  55. package/package.json +20 -5
@@ -2,104 +2,70 @@
2
2
  * MajikKey.ts
3
3
  * Seed phrase account library for the Majikah ecosystem.
4
4
  *
5
+ * v0.8 — key REGISTRY. Every account stores a set of keypairs in a KeyStore,
6
+ * addressed by namespaced id (see core/keys/key-id.ts). The core four
7
+ * (classic:x25519, classic:ed25519, pq:ml-kem-768, pq:ml-dsa-87) are always
8
+ * present on new accounts; anything else is opt-in via `keys` / `addKeys()`.
9
+ *
10
+ * The pre-registry per-algorithm getters still work and are marked
11
+ * @deprecated: they are thin wrappers over the registry accessors.
12
+ *
13
+ * Derivation (all deterministic from the BIP-39 mnemonic) lives in
14
+ * core/keys/key-impls.ts. Frozen "legacy-v1" recipes are pinned by
15
+ * vectors/legacy-v1.vectors.json.
5
16
  */
6
- import { MajikContact, MajikContactData, MajikContactMeta } from "@majikah/majik-contact";
7
- import { KDF_VERSION } from "./core/crypto/constants";
8
- import type { BitcoinRawPublicKey, ED25519RawPublicKey, MajikKeyAddress, MajikKeyDangerousJSON, MajikKeyFingerprint, MajikKeyJSON, MajikKeyMetadata, MLDSA87RawPublicKey, MLKEM768RawPublicKey, MnemonicJSON, X25519RawKey } from "./core/types";
9
- import { MajikMessageIdentity } from "./core/database/system/identity";
10
- import { MajikUser } from "@thezelijah/majik-user";
11
- import { MnemonicLanguage } from "./core/crypto/wordlist";
12
- import { MajikKeyWeb3Namespace, BitcoinDerivationOptions, BitcoinKeypairMaterial, SolanaKeypairMaterial } from "./core/web3";
17
+ import { MajikContactData, MajikContactMeta } from "@majikah/majik-contact/dist/types.js";
18
+ import { MajikContact } from "@majikah/majik-contact/dist/contacts/majik-contact.js";
19
+ import { KDF_VERSION } from "./core/crypto/constants.js";
20
+ import type { BitcoinRawPublicKey, ED25519RawPublicKey, MajikKeyAddress, MajikKeyDangerousJSON, MajikKeyFingerprint, MajikKeyJSON, MajikKeyMetadata, MLDSA87RawPublicKey, MLKEM768RawPublicKey, MnemonicJSON, X25519RawKey } from "./core/types.js";
21
+ import { MajikMessageIdentity } from "./core/database/system/identity.js";
22
+ import { MajikUser } from "@thezelijah/majik-user/dist/core/majik-user.js";
23
+ import { MnemonicLanguage } from "./core/crypto/wordlist.js";
24
+ import { MajikKeyWeb3Namespace, BitcoinDerivationOptions, BitcoinKeypairMaterial, SolanaKeypairMaterial, EthereumKeypairMaterial } from "./core/web3/index.js";
25
+ import { KeyFamily, KeyId } from "./core/keys/key-id.js";
26
+ import { KeyInfo, MajikKeypair } from "./core/keys/keypair-handle.js";
27
+ export { KeyId, KeyFamily, CORE_KEYS } from "./core/keys/key-id.js";
28
+ export { MajikKeypair } from "./core/keys/keypair-handle.js";
29
+ export type { KeyInfo } from "./core/keys/keypair-handle.js";
13
30
  /**
14
- * In-memory identity bundle for an *unlocked* MajikKey — the raw CryptoKey/
15
- * Uint8Array material, not the encrypted-at-rest form. This is what
16
- * `toKeyIdentity()` returns, and what backup export starts from.
17
- *
18
- * `mlKemPublicKey`/`mlKemSecretKey` are required here because every account
19
- * (even ones mid-migration) is expected to carry ML-KEM material by the time
20
- * this shape is used. `ed*`, `mlDsa*`, and `btc*` are optional because
21
- * accounts imported before those key types existed may not have them yet —
22
- * check `hasSigningKeys` / `hasBitcoin` on the `MajikKey` instance before
23
- * relying on them.
31
+ * In-memory identity bundle for an *unlocked* MajikKey. Returned by
32
+ * `toKeyIdentity()`. Kept for backward compatibility; prefer the registry
33
+ * accessors (`getKeypair()`, `getPublicKey()`, `getPrivateKey(id)`).
24
34
  */
25
35
  export interface MajikKeyIdentity {
26
- /** Account identifier. Equal to `fingerprint` for accounts created by this library. */
27
36
  id: MajikKeyFingerprint;
28
- /** X25519 public key */
29
37
  publicKey: X25519RawKey;
30
- /** SHA-256 fingerprint of `publicKey`. */
31
38
  fingerprint: MajikKeyFingerprint;
32
- /** X25519 private key, decrypted into memory. ⚠️ Live key material — do not log or serialize directly. */
33
39
  privateKey: X25519RawKey;
34
- /** AES-256-GCM-encrypted X25519 private key (IV + ciphertext), as stored at rest. */
35
40
  encryptedPrivateKey: ArrayBuffer;
36
- /** Random salt used to derive the passphrase-based encryption key. Base64. */
37
41
  salt: string;
38
- /** KDF used to encrypt the keys on this identity: `1` = legacy PBKDF2, `2` = Argon2id. */
39
42
  kdfVersion: KDF_VERSION;
40
- /** ML-KEM-768 (FIPS-203) public key. Post-quantum key encapsulation. */
41
43
  mlKemPublicKey: Uint8Array;
42
- /** ML-KEM-768 secret key, decrypted into memory. ⚠️ Live key material. */
43
44
  mlKemSecretKey?: Uint8Array;
44
- /** Ed25519 public key. Classical signing — same keypair the X25519 identity key is converted from. */
45
45
  edPublicKey?: Uint8Array;
46
- /** Ed25519 secret key, decrypted into memory. ⚠️ Live key material. */
47
46
  edSecretKey?: Uint8Array;
48
- /** ML-DSA-87 (FIPS-204) public key. Post-quantum signing. */
49
47
  mlDsaPublicKey?: Uint8Array;
50
- /** ML-DSA-87 secret key, decrypted into memory. ⚠️ Live key material. */
51
48
  mlDsaSecretKey?: Uint8Array;
52
- /** @experimental secp256k1 Bitcoin public key. Domain-separated BIP-32/84 derivation by default. */
49
+ /** @experimental */
53
50
  btcPublicKey?: Uint8Array;
54
- /** @experimental Bitcoin private key, decrypted into memory. ⚠️ Live key material. */
51
+ /** @experimental */
55
52
  btcSecretKey?: Uint8Array;
56
53
  }
57
- /**
58
- * `MajikKeyIdentity` immediately after fresh derivation from a mnemonic —
59
- * i.e. what `create()` and `importFromMnemonicBackup()` produce internally,
60
- * before the result is wrapped into a `MajikKey` instance.
61
- *
62
- * Unlike the base `MajikKeyIdentity`, every `encrypted*` field here is
63
- * required: a fresh derivation always re-derives and re-encrypts the full
64
- * key set (ML-KEM, Ed25519, ML-DSA, and Bitcoin) in one pass, so there's no
65
- * "partially migrated" state at this point in the flow.
66
- */
54
+ /** @deprecated Pre-registry shape. No longer produced; kept so existing type imports keep compiling. */
67
55
  export type MajikKeyDerivedIdentity = MajikKeyIdentity & {
68
- /** AES-256-GCM-encrypted ML-KEM-768 secret key, freshly re-encrypted. */
69
56
  encryptedMlKemSecretKey: ArrayBuffer;
70
- /** AES-256-GCM-encrypted Ed25519 secret key, freshly re-encrypted. */
71
57
  encryptedEdSecretKey: ArrayBuffer;
72
- /** AES-256-GCM-encrypted ML-DSA-87 secret key, freshly re-encrypted. */
73
58
  encryptedMlDsaSecretKey: ArrayBuffer;
74
- /** @experimental AES-256-GCM-encrypted Bitcoin secret key, freshly re-encrypted. */
75
59
  encryptedBtcSecretKey?: ArrayBuffer;
76
60
  };
77
- /**
78
- * Minimal identity export — just enough to identify the account and, if
79
- * present, re-derive access to it. Lighter than `MajikKeyJSON`: no ML-KEM,
80
- * Ed25519, ML-DSA, or Bitcoin fields at all. Produced by
81
- * `toSerializedIdentity()` (unlocked keys only).
82
- */
83
61
  export interface SerializedIdentity {
84
62
  id: string;
85
- /** X25519 public key, base64. */
86
63
  publicKey: MajikKeyAddress;
87
64
  fingerprint: MajikKeyFingerprint;
88
- /** AES-256-GCM-encrypted X25519 private key, base64. Omitted in some contexts — check before use. */
89
65
  encryptedPrivateKey?: string;
90
- /** Base64 salt paired with `encryptedPrivateKey`. Omitted in some contexts — check before use. */
91
66
  salt?: string;
92
67
  }
93
- /**
94
- * Internal constructor payload for `MajikKey` — every static factory
95
- * (`create()`, `fromJSON()`, `fromDangerousJSON()`, `importFromMnemonicBackup()`,
96
- * etc.) builds one of these and passes it to the private constructor.
97
- *
98
- * You won't normally build this by hand; it's exported mainly for type
99
- * inference around the factory methods. Fields mirror `MajikKeyJSON` plus
100
- * the live (decrypted) counterparts where a factory is constructing an
101
- * already-unlocked instance.
102
- */
68
+ /** @deprecated Pre-registry constructor payload. The constructor now takes a KeyStore. */
103
69
  export interface MajikKeyConstructorOptions {
104
70
  id: string;
105
71
  publicKey: X25519RawKey;
@@ -108,18 +74,14 @@ export interface MajikKeyConstructorOptions {
108
74
  encryptedPrivateKey: ArrayBuffer;
109
75
  encryptedPrivateKeyBase64: string;
110
76
  salt: string;
111
- /** Encrypted mnemonic-verification blob — see `MajikKeyJSON.backup`. */
112
77
  backup: string;
113
78
  label?: string;
114
79
  timestamp?: Date;
115
- /** Defaults to legacy PBKDF2 (`KDF_VERSION.PBKDF2`) if omitted — see the private constructor. */
116
80
  kdfVersion?: KDF_VERSION;
117
81
  mlKemPublicKey: MLKEM768RawPublicKey;
118
- /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
119
82
  mlKemSecretKey?: Uint8Array;
120
83
  encryptedMlKemSecretKey?: ArrayBuffer;
121
84
  encryptedMlKemSecretKeyBase64?: string;
122
- /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
123
85
  privateKey?: X25519RawKey;
124
86
  edPublicKey?: ED25519RawPublicKey;
125
87
  encryptedEdSecretKey?: ArrayBuffer;
@@ -127,42 +89,38 @@ export interface MajikKeyConstructorOptions {
127
89
  mlDsaPublicKey?: MLDSA87RawPublicKey;
128
90
  encryptedMlDsaSecretKey?: ArrayBuffer;
129
91
  encryptedMlDsaSecretKeyBase64?: string;
130
- /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
131
92
  edSecretKey?: Uint8Array;
132
- /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
133
93
  mlDsaSecretKey?: Uint8Array;
134
- /** @experimental secp256k1 Bitcoin public key. */
135
94
  btcPublicKey?: BitcoinRawPublicKey;
136
- /** @experimental AES-256-GCM-encrypted Bitcoin private key. */
137
95
  encryptedBtcSecretKey?: ArrayBuffer;
138
- /** @experimental Base64 form of `encryptedBtcSecretKey`. */
139
96
  encryptedBtcSecretKeyBase64?: string;
140
- /** @experimental Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
141
97
  btcSecretKey?: Uint8Array;
142
98
  mnemonicLanguage?: MnemonicLanguage;
143
99
  }
100
+ /** Options for create(), fromMnemonicJSON() and importFromMnemonicBackup(). */
101
+ export interface MajikKeyCreateOptions {
102
+ mnemonicLanguage?: MnemonicLanguage;
103
+ /**
104
+ * Extra keypairs to create ON TOP of the core four (always included).
105
+ * Defaults to none. e.g. `keys: [KeyId.BTC]`.
106
+ */
107
+ keys?: KeyId[];
108
+ /** @deprecated Use `keys: [KeyId.BTC]`. `true` adds `web3:btc`; omitted/false no longer derives it. */
109
+ deriveBitcoin?: boolean;
110
+ }
111
+ export interface MajikKeyToJSONOptions {
112
+ /**
113
+ * Also write the pre-registry flat fields (`encryptedMlKemSecretKey`, …)
114
+ * so older library versions / the Rust port can read the export.
115
+ * Defaults to TRUE in this release; planned to flip to false in the next major.
116
+ */
117
+ legacy?: boolean;
118
+ }
144
119
  /**
145
120
  * MajikKey
146
121
  * ---
147
- *
148
- * Seed phrase account library for the Majikah ecosystem.
149
- *
150
- * Every account stores FIVE keypairs, all deterministically derived from a
151
- * single BIP-39 mnemonic:
152
- * 1. X25519 (Curve25519) — fingerprint, contact identity, legacy message compat
153
- * 2. ML-KEM-768 (FIPS-203) — post-quantum key encapsulation for v3 envelopes
154
- * 3. Ed25519 — classical signing
155
- * 4. ML-DSA-87 (FIPS-204) — post-quantum signing
156
- * 5. Bitcoin (secp256k1) — BIP-32/84 HD key, domain-separated by default (experimental)
157
- *
158
- * All derived from the 64-byte BIP-39 seed:
159
- * seed[0..32] → Ed25519 keypair — used directly for signing, AND converted
160
- * to X25519 via ed2curve for the encryption/identity keypair
161
- * (one Ed25519 keypair, two roles)
162
- * seed[0..64] → ml_kem768.keygen(seed) — full seed, deterministic
163
- * hash(seed || "MajikSignatureSeedDSA") → 32-byte seed → ml_dsa87.keygen()
164
- * seed[0..64] → HDKey.fromMasterSeed(seed).derive(path) — BIP-32/84 Bitcoin key
165
- *
122
+ * Registry of keypairs deterministically derived from one BIP-39 mnemonic.
123
+ * See core/keys/registry.ts for every supported algorithm and its status.
166
124
  */
167
125
  export declare class MajikKey {
168
126
  private readonly _id;
@@ -172,187 +130,162 @@ export declare class MajikKey {
172
130
  private readonly _backup;
173
131
  private readonly _timestamp;
174
132
  private readonly _mnemonicLanguage;
175
- private _encryptedPrivateKey;
176
- private _encryptedPrivateKeyBase64;
133
+ private readonly _store;
177
134
  private _salt;
178
135
  private _label;
179
136
  private _kdfVersion;
180
- private _mlKemPublicKey;
181
- private _mlKemSecretKey?;
182
- private _encryptedMlKemSecretKey?;
183
- private _encryptedMlKemSecretKeyBase64?;
184
- private _privateKey?;
185
- private _edPublicKey?;
186
- private _edSecretKey?;
187
- private _encryptedEdSecretKey?;
188
- private _encryptedEdSecretKeyBase64?;
189
- private _mlDsaPublicKey?;
190
- private _mlDsaSecretKey?;
191
- private _encryptedMlDsaSecretKey?;
192
- private _encryptedMlDsaSecretKeyBase64?;
193
- /**
194
- * @experimental
195
- */
137
+ /** @experimental derived view over classic:ed25519; cached while unlocked */
196
138
  private _solanaKeypairMaterial?;
197
- /**
198
- * @experimental
199
- */
200
- private _btcPublicKey?;
201
- /**
202
- * @experimental
203
- */
204
- private _btcSecretKey?;
205
- /**
206
- * @experimental
207
- */
208
- private _encryptedBtcSecretKey?;
209
- /**
210
- * @experimental
211
- */
212
- private _encryptedBtcSecretKeyBase64?;
213
139
  private constructor();
214
- /** Account identifier. Equal to `fingerprint` for accounts created by this library. */
215
140
  get id(): MajikKeyFingerprint;
216
- /** SHA-256 fingerprint of the X25519 public key. Stable identity anchor for the account. */
217
141
  get fingerprint(): MajikKeyFingerprint;
218
142
  /** X25519 public key. Always available, even when locked. */
219
143
  get publicKey(): X25519RawKey;
220
- /** X25519 public key, base64-encoded. Always available, even when locked. */
221
144
  get publicKeyBase64(): MajikKeyAddress;
222
- /** Human-readable, user-editable account name. Update via `updateLabel()`. */
223
145
  get label(): string;
224
- /** BIP-39 wordlist language this account's mnemonic was generated/validated against. */
225
146
  get mnemonicLanguage(): MnemonicLanguage;
226
- /**
227
- * Encrypted mnemonic-verification blob (base64 JSON). Decryptable only
228
- * with the original mnemonic — used internally to verify a supplied
229
- * mnemonic before `importFromMnemonicBackup()` re-derives the full
230
- * identity. Not a general-purpose private-key backup.
231
- */
232
147
  get backup(): string;
233
- /** Account creation time. */
234
148
  get timestamp(): Date;
235
- /** KDF currently protecting every `encrypted*` field on this account: `1` = legacy PBKDF2, `2` = Argon2id. */
236
149
  get kdfVersion(): KDF_VERSION;
237
- /** `true` if this account is on the current KDF (Argon2id). `false` means it's still on legacy PBKDF2 — see `migrate()` or `importFromMnemonicBackup()`. */
238
150
  get isArgon2id(): boolean;
239
- /** `true` if private key material is currently purged from memory (i.e. `lock()` was called, or `unlock()` hasn't been called yet). */
240
151
  get isLocked(): boolean;
241
- /** `true` if private key material is currently decrypted in memory. The inverse of `isLocked`. */
242
152
  get isUnlocked(): boolean;
243
- /** ML-KEM-768 (FIPS-203) public key. Post-quantum key encapsulation. Always available, even when locked. */
244
- get mlKemPublicKey(): MLKEM768RawPublicKey;
245
- /** ML-KEM-768 secret key. `undefined` unless the account is unlocked. ⚠️ Live key material — prefer `getMlKemSecretKey()` if you want a thrown error instead of `undefined` on locked accounts. */
246
- get mlKemSecretKey(): Uint8Array | undefined;
247
- /** `true` if this account has ML-KEM-768 keys (i.e. is post-quantum-encryption capable). `false` means it's a legacy account pending migration. */
248
- get hasMlKem(): boolean;
249
- /** `true` if this account is on Argon2id *and* has ML-KEM-768 keys — i.e. fully migrated, nothing left to upgrade. */
153
+ /** `true` if this account holds every key in CORE_KEYS. Legacy accounts may not — see `missingKeys()` / `addKeys()`. */
154
+ get isCoreComplete(): boolean;
155
+ /** `true` if this account is on Argon2id *and* has ML-KEM-768 keys. */
250
156
  get isFullyUpgraded(): boolean;
157
+ /** Is this key present on the account? Works while locked. Derived views (web3:sol) count when their source key exists. */
158
+ hasKey(id: KeyId): boolean;
159
+ hasKeys(ids: readonly KeyId[]): boolean;
160
+ /** Which of `ids` (default: the core four) are NOT on this account. */
161
+ missingKeys(ids?: readonly KeyId[]): KeyId[];
162
+ /** Namespaced ids of every key available on this account, in canonical order. */
163
+ availableKeys(options?: {
164
+ family?: KeyFamily;
165
+ }): KeyId[];
166
+ /** Metadata for every available key. No secret material. */
167
+ listKeys(): KeyInfo[];
168
+ /** Every algorithm id this library version can create/enable. */
169
+ static supportedKeys(): KeyId[];
170
+ /** Public key bytes for `id`. Works while locked (derived views need an unlocked account). */
171
+ getPublicKey(id: KeyId): Uint8Array;
251
172
  /**
252
- * @experimental secp256k1 Bitcoin public key. `undefined` if this account
253
- * has no stored Bitcoin key material (e.g. it predates Web3 support and
254
- * hasn't been re-imported via `importFromMnemonicBackup()`).
255
- */
256
- get btcPublicKey(): BitcoinRawPublicKey | undefined;
257
- /** @experimental `true` if this account has a stored Bitcoin keypair. */
258
- get hasBitcoin(): boolean;
259
- /**
260
- * Lightweight, non-secret snapshot of this account's state — no key bytes
261
- * at all, encrypted or otherwise. Useful for account pickers, dashboards,
262
- * or anywhere you want to display status without touching key material.
173
+ * With no argument: the X25519 private key wrapper.
174
+ * @deprecated The no-argument form. Use `getPrivateKey(KeyId.X25519)`.
263
175
  */
264
- get metadata(): MajikKeyMetadata;
265
- /** Ed25519 public key. Classical signing — same keypair the X25519 identity key is converted from. Always available, even when locked. */
176
+ getPrivateKey(): X25519RawKey;
177
+ /** Secret key bytes for `id`. Throws if locked or absent. ⚠️ Live key material. */
178
+ getPrivateKey(id: KeyId): Uint8Array;
179
+ /** A live handle with `.public` / `.private` / `.publicBase64`. Reads through to the account, so it never goes stale across lock(). */
180
+ getKeypair(id: KeyId): MajikKeypair;
181
+ private _requireSecret;
182
+ /** @deprecated Use `getPublicKey(KeyId.ML_KEM_768)`. */
183
+ get mlKemPublicKey(): MLKEM768RawPublicKey;
184
+ /** @deprecated Use `getPrivateKey(KeyId.ML_KEM_768)`. */
185
+ get mlKemSecretKey(): Uint8Array | undefined;
186
+ /** @deprecated Use `hasKey(KeyId.ML_KEM_768)`. */
187
+ get hasMlKem(): boolean;
188
+ /** @deprecated Use `getPublicKey(KeyId.ED25519)`. */
266
189
  get edPublicKey(): ED25519RawPublicKey | undefined;
267
- /** ML-DSA-87 (FIPS-204) public key. Post-quantum signing. Always available, even when locked. */
190
+ /** @deprecated Use `getPublicKey(KeyId.ML_DSA_87)`. */
268
191
  get mlDsaPublicKey(): MLDSA87RawPublicKey | undefined;
269
- /** `true` if this account has both Ed25519 and ML-DSA-87 signing keys. `false` means it's a legacy account pending migration. */
192
+ /** @deprecated Use `hasKeys([KeyId.ED25519, KeyId.ML_DSA_87])`. */
270
193
  get hasSigningKeys(): boolean;
194
+ /** @experimental @deprecated Use `getPublicKey(KeyId.BTC)`. */
195
+ get btcPublicKey(): BitcoinRawPublicKey | undefined;
196
+ /** @experimental @deprecated Use `hasKey(KeyId.BTC)`. */
197
+ get hasBitcoin(): boolean;
198
+ /** @deprecated Use `getPrivateKey(KeyId.ML_KEM_768)`. */
199
+ getMlKemSecretKey(): Uint8Array;
200
+ /** @deprecated Use `getPrivateKey(KeyId.ED25519)`. */
201
+ getEdSecretKey(): Uint8Array;
202
+ /** @deprecated Use `getPrivateKey(KeyId.ML_DSA_87)`. */
203
+ getMlDsaSecretKey(): Uint8Array;
204
+ /** @experimental @deprecated Use `getPrivateKey(KeyId.BTC)`. */
205
+ getBtcSecretKey(): Uint8Array;
206
+ /** Non-secret snapshot of this account's state. */
207
+ get metadata(): MajikKeyMetadata;
271
208
  /**
272
- * Creates a brand-new MajikKey account from a BIP-39 mnemonic.
209
+ * Creates a brand-new MajikKey from a BIP-39 mnemonic and returns it UNLOCKED.
210
+ *
211
+ * Always derives the core four (X25519, Ed25519, ML-KEM-768, ML-DSA-87).
212
+ * Pass `options.keys` for more, e.g. `{ keys: [KeyId.BTC] }`.
273
213
  *
274
- * Derives the full key set in one pass — X25519, ML-KEM-768, Ed25519,
275
- * ML-DSA-87, and a domain-separated Bitcoin key (see `MAJIK_BITCOIN_DOMAIN_PATH`)
276
- * — encrypts every private key with Argon2id (KDF v2), and returns an
277
- * **already-unlocked** instance (no `unlock()` call needed right after
278
- * `create()`).
214
+ * ⚠️ Behavior change vs 0.7: Bitcoin is no longer derived by default.
279
215
  *
280
- * @param mnemonic - A valid BIP-39 mnemonic phrase (12 or 24 words), matching `mnemonicLanguage`. Generate one with `MajikKey.generateMnemonic()`.
281
- * @param passphrase - Passphrase used to derive the Argon2id encryption key for every private key on this account. This is *not* the mnemonic — losing it without the mnemonic makes the account unrecoverable.
282
- * @param label - Optional human-readable account name. Defaults to an empty string. Update later via `updateLabel()`.
283
- * @param mnemonicLanguage - BIP-39 wordlist to validate `mnemonic` against. Defaults to `"en"`.
284
- * @param options.deriveBitcoin - @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true`.
285
- * @returns An unlocked `MajikKey` instance, ready for immediate use — call `.lock()` when you're done with it.
286
- * @throws {MajikKeyError} If `mnemonic` fails validation, `passphrase`/`label` fail their validators, or `mnemonic` doesn't match `mnemonicLanguage`'s wordlist.
216
+ * @throws {MajikKeyError} on invalid mnemonic/passphrase/label or unusable key ids.
217
+ */
218
+ static create(mnemonic: string, passphrase: string, label?: string, options?: MajikKeyCreateOptions): Promise<MajikKey>;
219
+ private static _resolveCreateKeys;
220
+ /**
221
+ * Parse a MajikKey from JSON. Accepts BOTH shapes:
222
+ * - registry JSON (has `keys`) → used as-is
223
+ * - legacy flat JSON (no `keys`) → auto-migrated in memory (no secrets, no
224
+ * passphrase, no mnemonic needed). Re-serialize with toJSON() to persist
225
+ * the upgraded shape.
287
226
  */
288
- static create(mnemonic: string, passphrase: string, label?: string, options?: {
289
- mnemonicLanguage?: MnemonicLanguage;
290
- /** @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true` for backward compatibility. */
291
- deriveBitcoin?: boolean;
292
- }): Promise<MajikKey>;
293
227
  static fromJSON(json: MajikKeyJSON | string): MajikKey;
228
+ private static _validateRegistryJSON;
294
229
  /**
295
230
  * Export a fully unlocked MajikKey with all raw private keys.
296
231
  * ⚠️ DANGEROUS — output contains unencrypted private key material.
297
232
  * Only use for server-side secrets injection.
298
- * Never log, store in a database, or transmit over the network.
299
233
  */
300
234
  toDangerousJSON(): MajikKeyDangerousJSON;
301
235
  /**
302
236
  * Reconstruct a fully unlocked MajikKey from a dangerous JSON export.
303
237
  * ⚠️ DANGEROUS — input contains unencrypted private key material.
304
- * Intended for server-side use only (e.g. TSA signing key loaded from Cloudflare Secrets).
305
238
  * No KDF is involved — reconstruction is instant.
306
239
  */
307
240
  static fromDangerousJSON(json: MajikKeyDangerousJSON | string): MajikKey;
308
241
  toMnemonicJSON(mnemonic: string, passphrase?: string): MnemonicJSON;
309
- static fromMnemonicJSON(mnemonicJson: MnemonicJSON | string, passphrase: string, label?: string, options?: {
310
- mnemonicLanguage?: MnemonicLanguage;
311
- /** @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true` for backward compatibility. */
312
- deriveBitcoin?: boolean;
313
- }): Promise<MajikKey>;
242
+ static fromMnemonicJSON(mnemonicJson: MnemonicJSON | string, passphrase: string, label?: string, options?: MajikKeyCreateOptions): Promise<MajikKey>;
314
243
  updateLabel(newLabel: string): this;
315
244
  updatePassphrase(currentPassphrase: string, newPassphrase: string): Promise<this>;
316
245
  /**
317
246
  * Migrate KDF from PBKDF2 to Argon2id without changing passphrase.
318
- * NOTE: Does not add ML-KEM keys — use importFromMnemonicBackup() for full upgrade.
247
+ * Does not add new key types — use addKeys() (requires the mnemonic).
319
248
  */
320
249
  migrate(passphrase: string): Promise<this>;
250
+ /**
251
+ * Add keypairs this account doesn't have yet (new algorithms, or core keys
252
+ * missing on a legacy account). Requires the original MNEMONIC: new keys are
253
+ * derived from the seed, which is never stored. Also requires the current
254
+ * passphrase (to encrypt the new keys under the account's existing salt).
255
+ *
256
+ * Safe by construction: the mnemonic must reproduce this account's X25519
257
+ * key, and the passphrase must decrypt it, before anything is added.
258
+ * Keys already present are skipped. Account must be on Argon2id — call
259
+ * `migrate(passphrase)` first if `isArgon2id` is false.
260
+ *
261
+ * @returns the ids that were added
262
+ */
263
+ addKeys(ids: readonly KeyId[], mnemonic: string, passphrase: string): Promise<KeyId[]>;
321
264
  lock(): this;
265
+ /** One KDF run decrypts every key. Atomic: a failure leaves the account fully locked. */
322
266
  unlock(passphrase: string): Promise<this>;
323
267
  verify(passphrase: string): Promise<boolean>;
324
- getPrivateKey(): X25519RawKey;
268
+ /** @deprecated Use `getPrivateKey(KeyId.X25519)`. */
325
269
  getPrivateKeyBase64(): string;
326
- getMlKemSecretKey(): Uint8Array;
327
- getEdSecretKey(): Uint8Array;
328
- getMlDsaSecretKey(): Uint8Array;
329
270
  /**
330
271
  * Executes an operation against an already-unlocked MajikKey and
331
- * automatically locks the key when the operation completes.
332
- *
333
- * The key is always locked after the operation, including when the
334
- * operation throws or rejects.
335
- *
336
- * @param key - An already-unlocked MajikKey instance.
337
- * @param operation - Synchronous or asynchronous operation to execute.
338
- * @returns The result returned by the operation.
339
- *
340
- * @throws {MajikKeyError} If the key is locked.
341
- * @throws {MajikKeyError} If `operation` is not a function.
342
- *
343
- * @example
344
- * ```ts
345
- * await key.unlock(passphrase);
346
- *
347
- * const signature = await MajikKey.withAutoLock(key, async (key) => {
348
- * return sign(key.getEdSecretKey(), message);
349
- * });
350
- *
351
- * // key.isLocked === true
352
- * ```
272
+ * automatically locks the key when the operation completes (even on throw).
353
273
  */
354
274
  static withAutoLock<T>(key: MajikKey, operation: (key: MajikKey) => T | Promise<T>): Promise<T>;
355
- toJSON(): MajikKeyJSON;
275
+ private _hasNonX25519Blobs;
276
+ /**
277
+ * Decrypt every blob under (current passphrase, current salt/KDF), re-encrypt
278
+ * under (new passphrase, fresh salt, Argon2id), then commit atomically.
279
+ * Does not need the account to be unlocked and never touches plaintext in memory.
280
+ */
281
+ private _reencryptAll;
282
+ /**
283
+ * Serialize (safe at rest: only passphrase-encrypted secrets).
284
+ * Writes the registry (`keys`) AND, by default, the pre-registry flat fields
285
+ * for compatibility. Pass `{ legacy: false }` for registry-only output.
286
+ * (JSON.stringify passes a string here; that is treated as "defaults".)
287
+ */
288
+ toJSON(options?: MajikKeyToJSONOptions | string): MajikKeyJSON;
356
289
  toString(pretty?: boolean): string;
357
290
  static generateMnemonic(strength?: 128 | 256, language?: MnemonicLanguage): Promise<string>;
358
291
  static validateMnemonic(mnemonic: string): boolean;
@@ -371,97 +304,62 @@ export declare class MajikKey {
371
304
  }): Promise<MajikMessageIdentity>;
372
305
  exportMnemonicBackup(mnemonic: string): Promise<string>;
373
306
  /**
374
- * Import a MajikKey from a mnemonic-encrypted backup.
375
- *
307
+ * Import a MajikKey from a mnemonic-encrypted backup. Re-derives the account
308
+ * from the mnemonic: the core four (plus `options.keys`) under a new passphrase.
376
309
  */
377
- static importFromMnemonicBackup(backup: string, mnemonic: string, passphrase: string, label?: string, options?: {
378
- mnemonicLanguage?: MnemonicLanguage;
379
- /** @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true` for backward compatibility. */
380
- deriveBitcoin?: boolean;
381
- }): Promise<MajikKey>;
310
+ static importFromMnemonicBackup(backup: string, mnemonic: string, passphrase: string, label?: string, options?: MajikKeyCreateOptions): Promise<MajikKey>;
382
311
  private static _getWordlist;
383
312
  /**
384
- * @param mnemonic - BIP-39 mnemonic to derive the full key set from.
385
- * @param passphrase - Passphrase used to derive the Argon2id encryption key shared by every private key produced here.
386
- * @param options.deriveBitcoin - @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true`.
313
+ * Derive `ids` from the mnemonic and seal each secret under ONE Argon2id
314
+ * key (single salt, single KDF run). Returns an UNLOCKED store.
387
315
  */
388
- private static _deriveAndEncryptFromMnemonic;
389
- private static _encryptPrivateKey;
390
- /**
391
- * Encrypt the ML-KEM secret key using the same Argon2id-derived key as X25519
392
- * (same passphrase + same salt) but a DIFFERENT random IV. One Argon2id
393
- * computation → two independently encrypted blobs.
394
- */
395
- private static _encryptMlKemSecretKey;
396
- private static _encryptSigningKey;
397
- private static _decryptPrivateKey;
398
- private static _decryptMlKemSecretKey;
399
- private static _decryptSigningKey;
316
+ private static _deriveFromMnemonic;
317
+ /** One KDF run. kdfVersion 1 = legacy PBKDF2 (X25519 blob of old accounts only). */
318
+ private static _deriveVaultKey;
400
319
  private static _verifyBackupDecryption;
401
320
  private static _exportMnemonicBackup;
402
321
  private static _deriveLegacyMnemonicKey;
403
- private static _exportKeyToBase64;
404
- /**
405
- * @experimental
406
- */
407
- getBtcSecretKey(): Uint8Array;
408
322
  /**
409
323
  * @experimental
410
324
  */
411
325
  get web3(): MajikKeyWeb3Namespace | undefined;
412
- /**
413
- * @experimental True if this MajikKey can currently produce Bitcoin
414
- * material (i.e. it's unlocked and has a Bitcoin secret key).
415
- */
326
+ /** @experimental True if this MajikKey can currently produce Bitcoin material (unlocked + has a Bitcoin key). */
416
327
  get hasBitcoinKeypair(): boolean;
417
328
  /**
418
- * @experimental Raw Bitcoin keypair material. Pass `{ standard: true }` to
419
- * get the REAL BIP-84 mainnet key (recoverable in any standard wallet from
420
- * the mnemonic alone) instead of Majik's default domain-separated key.
421
- *
422
- * NOTE: `{ standard: true }` re-derives from the raw seed on demand and is
423
- * NOT the same key as `web3.bitcoin` (which is always the stored,
424
- * domain-separated default) — it requires the mnemonic to reproduce again
425
- * outside Majik, whereas the stored default does not.
329
+ * @experimental Raw Bitcoin keypair material for the stored (domain-separated)
330
+ * key. The REAL BIP-84 key needs the mnemonic:
331
+ * use `MajikKey.deriveStandardBitcoinFromMnemonic(mnemonic)`.
426
332
  */
427
333
  getBitcoinKeypairMaterial(options?: BitcoinDerivationOptions): BitcoinKeypairMaterial;
428
- /**
429
- * @experimental Derive the REAL BIP-84 mainnet Bitcoin keypair straight
430
- * from a mnemonic — for one-off export/verification. Does not require
431
- * an unlocked MajikKey instance.
432
- */
334
+ /** @experimental Derive the REAL BIP-84 mainnet Bitcoin keypair straight from a mnemonic. */
433
335
  static deriveStandardBitcoinFromMnemonic(mnemonic: string, mnemonicLanguage?: MnemonicLanguage): Promise<BitcoinKeypairMaterial>;
434
- /**
435
- * @experimental WIF export of the default (domain-separated) Bitcoin key.
436
- */
336
+ /** @experimental WIF export of the stored (domain-separated) Bitcoin key. */
437
337
  getBitcoinWIF(options?: {
438
338
  compressed?: boolean;
439
339
  }): string;
340
+ /** @experimental True if this account has a stored Ethereum key (works while locked). */
341
+ get hasEthereum(): boolean;
440
342
  /**
441
- * @experimental True if this MajikKey can currently produce a Solana
442
- * keypair (i.e. it's unlocked and has an Ed25519 signing key).
343
+ * @experimental EIP-55 Ethereum address (standard m/44'/60'/0'/0/0 — the same
344
+ * address MetaMask shows for this mnemonic). Public-only, so it works while locked.
443
345
  */
346
+ getEthereumAddress(): string;
347
+ /** @experimental Raw Ethereum keypair material. Requires an unlocked account. */
348
+ getEthereumKeypairMaterial(): EthereumKeypairMaterial;
349
+ /** @experimental 0x-prefixed private key hex, for wallet "import private key". */
350
+ getEthereumPrivateKeyHex(): string;
351
+ /** @experimental True if this MajikKey can currently produce a Solana keypair (unlocked + has Ed25519). */
444
352
  get hasSolanaKeypair(): boolean;
445
353
  private _getOrDeriveSolanaMaterial;
446
- /**
447
- * @experimental Raw Solana keypair material (public/secret key bytes).
448
- * Pass `{ reuseMessageKey: true }` to reuse the MajikKey's message signing
449
- * Ed25519 key directly instead of the domain-separated derivation.
450
- */
354
+ /** @experimental Raw Solana keypair material. `reuseMessageKey: true` reuses the message-signing Ed25519 key. */
451
355
  getSolanaKeypairMaterial(options?: {
452
356
  reuseMessageKey?: boolean;
453
357
  }): SolanaKeypairMaterial;
454
- /**
455
- * @experimental Real @solana/kit Keypair instance. Lazily loads
456
- * @solana/kit — throws a MajikKeyError with install instructions if
457
- * it isn't present in the consuming project.
458
- */
358
+ /** @experimental Real @solana/kit Keypair instance (lazy-loads @solana/kit). */
459
359
  getSolanaKeypair(options?: {
460
360
  reuseMessageKey?: boolean;
461
361
  }): Promise<any>;
462
- /**
463
- * @experimental Base58 Solana address. Does NOT require @solana/kit.
464
- */
362
+ /** @experimental Base58 Solana address. Does NOT require @solana/kit. */
465
363
  getSolanaAddress(options?: {
466
364
  reuseMessageKey?: boolean;
467
365
  }): string;