@vaultkeepr/core 0.1.3 → 0.2.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.
package/dist/index.mjs CHANGED
@@ -33,6 +33,78 @@ function getEntryGroupLabel(folder) {
33
33
  if (!folder) return ENTRY_GROUP_LABELS.identifiants;
34
34
  return ENTRY_GROUP_LABELS[folder] ?? folder;
35
35
  }
36
+ var JUNK_SUBDOMAIN_PREFIXES = /* @__PURE__ */ new Set([
37
+ "www",
38
+ "app",
39
+ "mobile",
40
+ "accounts",
41
+ "dashboard",
42
+ "kyc",
43
+ "mail",
44
+ "web",
45
+ "my",
46
+ "login",
47
+ "portal",
48
+ "id",
49
+ "news"
50
+ ]);
51
+ function brandFrom(url) {
52
+ if (!url?.trim()) return null;
53
+ try {
54
+ const raw = url.includes("://") ? url : `https://${url}`;
55
+ const host = new URL(raw).hostname.replace(/^www\./i, "");
56
+ if (/^\d+(\.\d+){3}$/.test(host)) return null;
57
+ const parts = host.split(".").filter(Boolean);
58
+ if (parts.length < 2) return null;
59
+ let label = parts[0];
60
+ if (JUNK_SUBDOMAIN_PREFIXES.has(label.toLowerCase()) && parts.length > 2) label = parts[1];
61
+ if (!label) return null;
62
+ return { brand: label.charAt(0).toUpperCase() + label.slice(1).toLowerCase(), host: host.toLowerCase() };
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ function cleanEntryTitle(rawTitle, url) {
68
+ const raw = rawTitle.trim();
69
+ let candidate = raw;
70
+ {
71
+ const lower = raw.toLowerCase();
72
+ let pos = lower.indexOf("passkey");
73
+ while (pos > 0) {
74
+ let i = pos;
75
+ while (i > 0 && /\s/.test(raw.charAt(i - 1))) i--;
76
+ const prefix = raw.slice(0, i);
77
+ if (i < pos && !prefix.includes("\n")) {
78
+ let j = pos + 7;
79
+ while (j < raw.length && /\s/.test(raw.charAt(j))) j++;
80
+ const tail = raw.slice(j);
81
+ if (j > pos + 7 && tail.length > 0 && !tail.includes("\n")) {
82
+ if (prefix.trim() !== "" && /\d/.test(tail)) candidate = prefix.trim();
83
+ break;
84
+ }
85
+ }
86
+ pos = lower.indexOf("passkey", pos + 1);
87
+ }
88
+ }
89
+ const brandInfo = brandFrom(url);
90
+ if (brandInfo) {
91
+ if (!candidate) return brandInfo.brand;
92
+ if (/^[\d\s()+.-]+$/.test(candidate)) return brandInfo.brand;
93
+ if (candidate.toLowerCase() === brandInfo.brand.toLowerCase()) return brandInfo.brand;
94
+ const schemeStripped = candidate.replace(/^https?:\/\//i, "");
95
+ const nl = schemeStripped.lastIndexOf("\n");
96
+ const slash = schemeStripped.indexOf("/", nl + 1);
97
+ const hostCandidate = slash === -1 ? schemeStripped : schemeStripped.slice(0, slash);
98
+ if (hostCandidate.toLowerCase() === brandInfo.host) {
99
+ return brandInfo.brand;
100
+ }
101
+ }
102
+ return candidate || brandInfo?.brand || raw;
103
+ }
104
+ function getEntryDisplayName(entry, primaryUrl) {
105
+ const url = primaryUrl ?? entry.urls?.[0]?.uri ?? entry.url ?? void 0;
106
+ return cleanEntryTitle(entry.title ?? entry.username ?? "", url);
107
+ }
36
108
  function resolveFolderName(folderId, folderTree) {
37
109
  if (!folderId) return ENTRY_GROUP_LABELS.identifiants;
38
110
  if (folderId in ENTRY_GROUP_LABELS) return ENTRY_GROUP_LABELS[folderId];
@@ -531,9 +603,12 @@ function normalizeUrl(raw) {
531
603
  let u = (raw ?? "").trim().toLowerCase();
532
604
  u = u.replace(/^https?:\/\//, "");
533
605
  u = u.replace(/^www\./, "");
534
- u = u.replace(/#.*$/, "");
535
- u = u.replace(/\/+$/, "");
536
- return u;
606
+ const nl = u.lastIndexOf("\n");
607
+ const hash = u.indexOf("#", nl + 1);
608
+ if (hash !== -1) u = u.slice(0, hash);
609
+ let end = u.length;
610
+ while (end > 0 && u.charAt(end - 1) === "/") end--;
611
+ return u.slice(0, end);
537
612
  }
538
613
  function entryFingerprint(entry) {
539
614
  const url = normalizeUrl(entry.url ?? "");
@@ -702,14 +777,14 @@ function isEncryptedExport(jsonStr) {
702
777
  return false;
703
778
  }
704
779
  }
705
- async function importEncryptedVault(encryptedJson, password, argon2Opts) {
780
+ async function importEncryptedVault(encryptedJson, password, argon2Opts, precomputed) {
706
781
  const payload = JSON.parse(encryptedJson.trim());
707
782
  if (payload?.format !== ENCRYPTED_EXPORT_FORMAT || !payload.salt || !payload.nonce || !payload.ciphertext) {
708
783
  throw new Error("Format d'export chiffr\xE9 invalide.");
709
784
  }
710
785
  const salt = hexToBytes3(payload.salt);
711
786
  const effectiveOpts = payload.kdf ? { t: payload.kdf.t, m: payload.kdf.m, p: payload.kdf.p, dkLen: payload.kdf.dkLen } : argon2Opts ?? {};
712
- const key = await deriveKeyFromPasswordArgon2(password, salt, effectiveOpts);
787
+ const key = precomputed?.key ?? await deriveKeyFromPasswordArgon2(password, precomputed?.salt ?? salt, effectiveOpts);
713
788
  const plaintext = decryptVault(
714
789
  {
715
790
  ciphertext: payload.ciphertext,
@@ -1421,7 +1496,6 @@ function isEnpassCsv(text) {
1421
1496
  const firstLine = text.split(/\r?\n/)[0];
1422
1497
  if (!firstLine) return false;
1423
1498
  const rawHeaders = firstLine.split(",");
1424
- const headers = rawHeaders.map((h) => h.replace(/^["']|["']$/g, "").trim());
1425
1499
  return rawHeaders.some((h) => {
1426
1500
  const clean = h.replace(/^["']|["']$/g, "").trim();
1427
1501
  return clean === "Title";
@@ -1959,22 +2033,28 @@ function parseCsvNoteExtras(rawNote) {
1959
2033
  let totpSecret;
1960
2034
  const customFields = [];
1961
2035
  for (const line of lines) {
1962
- const to = line.match(/^TOTP:\s+(.+)$/);
1963
- if (to) {
1964
- totpSecret = to[1].trim();
1965
- continue;
2036
+ if (line.startsWith("TOTP:")) {
2037
+ let j = 5;
2038
+ while (j < line.length && /\s/.test(line.charAt(j))) j++;
2039
+ if (j > 5 && j < line.length) {
2040
+ totpSecret = line.slice(j).trim();
2041
+ continue;
2042
+ }
1966
2043
  }
1967
- const fieldsMatch = line.match(/^Fields:\s+(.+)$/);
1968
- if (fieldsMatch) {
1969
- const entries = fieldsMatch[1].split("|");
1970
- for (const entry of entries) {
1971
- const colonIdx = entry.indexOf(":");
1972
- if (colonIdx > 0) {
1973
- customFields.push({
1974
- name: entry.slice(0, colonIdx).trim(),
1975
- value: entry.slice(colonIdx + 1).trim(),
1976
- type: "text"
1977
- });
2044
+ if (line.startsWith("Fields:")) {
2045
+ let j = 7;
2046
+ while (j < line.length && /\s/.test(line.charAt(j))) j++;
2047
+ if (j > 7 && j < line.length) {
2048
+ const entries = line.slice(j).split("|");
2049
+ for (const entry of entries) {
2050
+ const colonIdx = entry.indexOf(":");
2051
+ if (colonIdx > 0) {
2052
+ customFields.push({
2053
+ name: entry.slice(0, colonIdx).trim(),
2054
+ value: entry.slice(colonIdx + 1).trim(),
2055
+ type: "text"
2056
+ });
2057
+ }
1978
2058
  }
1979
2059
  }
1980
2060
  }
@@ -2475,7 +2555,6 @@ function exportCsv(vault, opts = {}) {
2475
2555
  continue;
2476
2556
  }
2477
2557
  if (e.folder === "cartes") {
2478
- const m = parseCardMeta(e.notes);
2479
2558
  rows.push(
2480
2559
  [
2481
2560
  csvEscape("cartes"),
@@ -2831,7 +2910,10 @@ import { sha512 } from "@noble/hashes/sha2.js";
2831
2910
  var TOTP_STEP_SEC = 30;
2832
2911
  function base32Decode(input) {
2833
2912
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
2834
- const clean = input.replace(/\s/g, "").toUpperCase().replace(/=+$/, "");
2913
+ const upper = input.replace(/\s/g, "").toUpperCase();
2914
+ let end = upper.length;
2915
+ while (end > 0 && upper.charAt(end - 1) === "=") end--;
2916
+ const clean = upper.slice(0, end);
2835
2917
  const bits = [];
2836
2918
  for (let i = 0; i < clean.length; i++) {
2837
2919
  const idx = alphabet.indexOf(clean[i]);
@@ -2902,9 +2984,9 @@ function parseTOTPUri(uri) {
2902
2984
  import { sha1 as sha12 } from "@noble/hashes/legacy.js";
2903
2985
  import { utf8ToBytes as utf8ToBytes4, bytesToHex as bytesToHex5 } from "@noble/hashes/utils.js";
2904
2986
  var RANGE_BASE = "https://api.pwnedpasswords.com/range/";
2905
- async function getPwnedPasswordCount(password, options) {
2906
- if (!password.length) return 0;
2907
- const hash = bytesToHex5(sha12(utf8ToBytes4(password))).toUpperCase();
2987
+ async function getPwnedCountForHash(sha1Hex, options) {
2988
+ const hash = sha1Hex.trim().toUpperCase();
2989
+ if (hash.length !== 40) throw new Error(`Invalid SHA-1 length: ${hash.length}`);
2908
2990
  const prefix = hash.slice(0, 5);
2909
2991
  const suffix = hash.slice(5);
2910
2992
  const fetchFn = options?.fetchFn ?? fetch;
@@ -2920,7 +3002,6 @@ async function getPwnedPasswordCount(password, options) {
2920
3002
  throw new Error(`PwnedPasswords API: ${res.status}`);
2921
3003
  }
2922
3004
  const text = await res.text();
2923
- const suffixUpper = suffix.toUpperCase();
2924
3005
  for (const line of text.split("\n")) {
2925
3006
  const trimmed = line.trim();
2926
3007
  if (!trimmed) continue;
@@ -2928,13 +3009,18 @@ async function getPwnedPasswordCount(password, options) {
2928
3009
  if (colon < 0) continue;
2929
3010
  const h = trimmed.slice(0, colon).toUpperCase();
2930
3011
  const countStr = trimmed.slice(colon + 1);
2931
- if (h === suffixUpper) {
3012
+ if (h === suffix) {
2932
3013
  const n = parseInt(countStr, 10);
2933
3014
  return Number.isFinite(n) ? n : 0;
2934
3015
  }
2935
3016
  }
2936
3017
  return 0;
2937
3018
  }
3019
+ async function getPwnedPasswordCount(password, options) {
3020
+ if (!password.length) return 0;
3021
+ const hash = bytesToHex5(sha12(utf8ToBytes4(password))).toUpperCase();
3022
+ return getPwnedCountForHash(hash, options);
3023
+ }
2938
3024
 
2939
3025
  // src/wordlist.ts
2940
3026
  var EFF_WORDLIST = [
@@ -11512,7 +11598,10 @@ function toBase64Url2(bytes) {
11512
11598
  for (let i = 0; i < bytes.length; i++) {
11513
11599
  binary += String.fromCharCode(bytes[i]);
11514
11600
  }
11515
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
11601
+ const b64 = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_");
11602
+ let end = b64.length;
11603
+ while (end > 0 && b64.charAt(end - 1) === "=") end -= 1;
11604
+ return b64.slice(0, end);
11516
11605
  }
11517
11606
  function fromBase64Url2(str) {
11518
11607
  const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
@@ -11575,6 +11664,26 @@ async function getHiddenWalletFromPassword(masterPassword, secretKey) {
11575
11664
  _hwCache.set(cacheKey, account);
11576
11665
  return account;
11577
11666
  }
11667
+ async function getHiddenWalletFromSecretKey(secretKey) {
11668
+ const cacheKey = keccak256(toHex(`${secretKey}:sk-only:v1`));
11669
+ const cached = _hwCache.get(cacheKey);
11670
+ if (cached) return cached;
11671
+ const privateKey = await getHiddenWalletPrivateKeyFromSecretKey(secretKey);
11672
+ const account = privateKeyToAccount(privateKey);
11673
+ _hwCache.set(cacheKey, account);
11674
+ return account;
11675
+ }
11676
+ async function getHiddenWalletPrivateKeyFromSecretKey(secretKey) {
11677
+ const salt = deriveHwSalt(secretKey);
11678
+ const inputBytes = new TextEncoder().encode(secretKey);
11679
+ let derived;
11680
+ if (_argon2Provider) {
11681
+ derived = await _argon2Provider(inputBytes, salt, HW_ARGON2_OPTS);
11682
+ } else {
11683
+ derived = argon2id2(inputBytes, salt, HW_ARGON2_OPTS);
11684
+ }
11685
+ return bytesToHex8(derived);
11686
+ }
11578
11687
  function clearHiddenWalletCache() {
11579
11688
  _hwCache.clear();
11580
11689
  }
@@ -11737,7 +11846,9 @@ function calculateMrzChecksum(str) {
11737
11846
  return sum % 10;
11738
11847
  }
11739
11848
  function cleanMrzField(str) {
11740
- return str.replace(/<+$/g, "").replace(/</g, " ").trim();
11849
+ let end = str.length;
11850
+ while (end > 0 && str.charAt(end - 1) === "<") end--;
11851
+ return str.slice(0, end).replace(/</g, " ").trim();
11741
11852
  }
11742
11853
  function parseMrzDate(str) {
11743
11854
  if (str.length !== 6) return "";
@@ -12006,6 +12117,7 @@ import { secp256k1 as secp256k12 } from "@noble/curves/secp256k1.js";
12006
12117
  import { xchacha20poly1305 as xchacha20poly13057 } from "@noble/ciphers/chacha.js";
12007
12118
  import { randomBytes as randomBytes8 } from "@noble/ciphers/utils.js";
12008
12119
  import { hkdf as hkdf5 } from "@noble/hashes/hkdf.js";
12120
+ import { hmac as hmac4 } from "@noble/hashes/hmac.js";
12009
12121
  import { sha256 as sha2568 } from "@noble/hashes/sha2.js";
12010
12122
  import { bytesToHex as bytesToHex10, hexToBytes as hexToBytes8, utf8ToBytes as utf8ToBytes8 } from "@noble/hashes/utils.js";
12011
12123
  var NONCE_LENGTH5 = 24;
@@ -12020,8 +12132,10 @@ function generateSharePin() {
12020
12132
  const num = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
12021
12133
  return String(num % 1e6).padStart(PIN_LENGTH, "0");
12022
12134
  }
12023
- function computeApiPinHash(pin) {
12024
- return bytesToHex10(sha2568(utf8ToBytes8(pin)));
12135
+ var PIN_PROOF_INFO = utf8ToBytes8("vaultkeepr-share-pin-proof-v1");
12136
+ function computeApiPinHash(pin, keyHex) {
12137
+ const proofKey = hmac4(sha2568, hexToBytes8(keyHex), PIN_PROOF_INFO);
12138
+ return bytesToHex10(hmac4(sha2568, proofKey, utf8ToBytes8(pin)));
12025
12139
  }
12026
12140
  function generateShareId() {
12027
12141
  const ts = Date.now().toString(36);
@@ -12039,7 +12153,6 @@ function deriveShareKeyV1(sharedSecret, pin) {
12039
12153
  function createSecureShare(payload, pin) {
12040
12154
  const sharePin = pin ?? generateSharePin();
12041
12155
  const shareId = generateShareId();
12042
- const apiPinHash = computeApiPinHash(sharePin);
12043
12156
  const fullPayload = {
12044
12157
  ...payload,
12045
12158
  version: SHARE_VERSION,
@@ -12052,6 +12165,7 @@ function createSecureShare(payload, pin) {
12052
12165
  }
12053
12166
  const ephemeralPrivate = secp256k12.utils.randomSecretKey();
12054
12167
  const ephemeralPublic = secp256k12.getPublicKey(ephemeralPrivate, false);
12168
+ const apiPinHash = computeApiPinHash(sharePin, bytesToHex10(ephemeralPrivate));
12055
12169
  const shareKey = deriveShareKeyV2(ephemeralPrivate, sharePin);
12056
12170
  const nonce = randomBytes8(NONCE_LENGTH5);
12057
12171
  const chacha = xchacha20poly13057(shareKey, nonce);
@@ -12187,7 +12301,7 @@ function calculatePasswordStrength(password) {
12187
12301
  } else {
12188
12302
  feedback.push("passwordFeedback.addSpecialChars");
12189
12303
  }
12190
- if (!/(.)\\1{2,}/.test(password)) {
12304
+ if (!/(.)\1{2,}/.test(password)) {
12191
12305
  score += 10;
12192
12306
  } else {
12193
12307
  feedback.push("passwordFeedback.avoidRepeated");
@@ -12394,6 +12508,59 @@ function mergePrfCredentials(local, remote) {
12394
12508
  for (const cred of local) byId.set(cred.credentialId, cred);
12395
12509
  return Array.from(byId.values());
12396
12510
  }
12511
+ function legacyContactKey(c) {
12512
+ return JSON.stringify([c.label, c.email, c.telegram, c.address, c.publicKey]);
12513
+ }
12514
+ function mergeLegacyContacts(base, local, remote) {
12515
+ const byKey = /* @__PURE__ */ new Map();
12516
+ for (const c of [...local ?? [], ...remote ?? []]) {
12517
+ const key = legacyContactKey(c);
12518
+ const existing = byKey.get(key);
12519
+ if (!existing) {
12520
+ byKey.set(key, c);
12521
+ continue;
12522
+ }
12523
+ if (c.addedAt !== existing.addedAt) {
12524
+ byKey.set(key, c.addedAt > existing.addedAt ? c : existing);
12525
+ } else {
12526
+ byKey.set(key, deterministicPick(existing, c));
12527
+ }
12528
+ }
12529
+ if (byKey.size === 0) return void 0;
12530
+ const ordered = [];
12531
+ const placed = /* @__PURE__ */ new Set();
12532
+ for (const c of base ?? []) {
12533
+ const key = legacyContactKey(c);
12534
+ const kept = byKey.get(key);
12535
+ if (kept && !placed.has(key)) {
12536
+ placed.add(key);
12537
+ ordered.push(kept);
12538
+ }
12539
+ }
12540
+ const additions = Array.from(byKey.entries()).filter(([key]) => !placed.has(key));
12541
+ additions.sort((a, b) => {
12542
+ if (a[1].addedAt !== b[1].addedAt) return a[1].addedAt < b[1].addedAt ? -1 : 1;
12543
+ return JSON.stringify(a[1]) <= JSON.stringify(b[1]) ? -1 : 1;
12544
+ });
12545
+ for (const [, c] of additions) ordered.push(c);
12546
+ return ordered;
12547
+ }
12548
+ function mergeMonitoredEmails(base, local, remote) {
12549
+ const present = /* @__PURE__ */ new Set();
12550
+ for (const v of [...local ?? [], ...remote ?? []]) present.add(v);
12551
+ if (present.size === 0) return void 0;
12552
+ const ordered = [];
12553
+ for (const v of base ?? []) {
12554
+ if (present.has(v) && !ordered.includes(v)) ordered.push(v);
12555
+ }
12556
+ const additions = [];
12557
+ for (const v of present) {
12558
+ if (!ordered.includes(v)) additions.push(v);
12559
+ }
12560
+ additions.sort((a, b) => a.localeCompare(b));
12561
+ for (const v of additions) ordered.push(v);
12562
+ return ordered;
12563
+ }
12397
12564
  function threeWayMerge(base, local, remote) {
12398
12565
  const effectiveBase = base ?? {
12399
12566
  version: local.version,
@@ -12457,6 +12624,16 @@ function threeWayMerge(base, local, remote) {
12457
12624
  (sum, f) => sum + (f.originalSize || 0),
12458
12625
  0
12459
12626
  );
12627
+ const mergedLegacyContacts = mergeLegacyContacts(
12628
+ base ? base.legacyContacts : void 0,
12629
+ local.legacyContacts,
12630
+ remote.legacyContacts
12631
+ );
12632
+ const mergedMonitoredEmails = mergeMonitoredEmails(
12633
+ base ? base.monitoredEmails : void 0,
12634
+ local.monitoredEmails,
12635
+ remote.monitoredEmails
12636
+ );
12460
12637
  const out = {
12461
12638
  version: Math.max(local.version, remote.version),
12462
12639
  createdAt: local.createdAt,
@@ -12481,6 +12658,15 @@ function threeWayMerge(base, local, remote) {
12481
12658
  if (Object.keys(mergedEntryTombstones).length > 0) {
12482
12659
  out.entryTombstones = mergedEntryTombstones;
12483
12660
  }
12661
+ if (mergedLegacyContacts) {
12662
+ out.legacyContacts = mergedLegacyContacts;
12663
+ }
12664
+ if (mergedMonitoredEmails) {
12665
+ out.monitoredEmails = mergedMonitoredEmails;
12666
+ }
12667
+ if (local.secretKey !== void 0 || remote.secretKey !== void 0) {
12668
+ out.secretKey = local.secretKey !== void 0 && remote.secretKey !== void 0 ? deterministicPick(local.secretKey, remote.secretKey) : local.secretKey ?? remote.secretKey;
12669
+ }
12484
12670
  return out;
12485
12671
  }
12486
12672
  function mergeFolders(base, local, remote, baseTombstones, localTombstones, remoteTombstones, now) {
@@ -14722,7 +14908,7 @@ var BIP39_WORDLIST = [
14722
14908
  "zoo"
14723
14909
  ];
14724
14910
  function suggestBip39Words(prefix, limit = 5) {
14725
- const p = prefix.toLowerCase().trim();
14911
+ const p = (prefix ?? "").toLowerCase().trim();
14726
14912
  if (!p) return [];
14727
14913
  const results = [];
14728
14914
  for (const word of BIP39_WORDLIST) {
@@ -15000,6 +15186,146 @@ var VaultKeepR_SLM = class {
15000
15186
  return { severity, ...data };
15001
15187
  }
15002
15188
  };
15189
+
15190
+ // src/account-state.ts
15191
+ function accountKeyState(input) {
15192
+ if (input.hasSecretKey) return { kind: "ready" };
15193
+ if (input.remote?.moved) return { kind: "read_only_pair" };
15194
+ if (input.remote?.exists || input.hasLocalVault) return { kind: "passage" };
15195
+ return { kind: "choose" };
15196
+ }
15197
+ function pointerWalletFor(input) {
15198
+ return input.hasSecretKey && input.remoteMoved ? input.smartAccount : input.eoa;
15199
+ }
15200
+ function passageNext(step, event) {
15201
+ switch (`${step}:${event}`) {
15202
+ case "idle:key_generated":
15203
+ return { step: "key_pending", discardPendingKey: false };
15204
+ case "key_pending:copy_ok":
15205
+ return { step: "copied", discardPendingKey: false };
15206
+ case "key_pending:copy_failed":
15207
+ return { step: "idle", discardPendingKey: true };
15208
+ case "copied:move_ok":
15209
+ return { step: "moved", discardPendingKey: false };
15210
+ case "copied:move_failed":
15211
+ return { step: "copied", discardPendingKey: false };
15212
+ case "copied:move_conflict":
15213
+ return { step: "idle", discardPendingKey: true };
15214
+ case "moved:promote_ok":
15215
+ return { step: "done", discardPendingKey: false };
15216
+ default:
15217
+ return { error: `invalid passage transition: ${step} + ${event}` };
15218
+ }
15219
+ }
15220
+ function passageIdentity(step) {
15221
+ return step === "done" ? "smart_account" : "legacy";
15222
+ }
15223
+
15224
+ // src/recovery-code.ts
15225
+ import { argon2id as argon2id3 } from "@noble/hashes/argon2.js";
15226
+ import { bytesToHex as bytesToHex12, hexToBytes as hexToBytes10 } from "@noble/hashes/utils.js";
15227
+ import { randomBytes as randomBytes10 } from "@noble/ciphers/utils.js";
15228
+ var RECOVERY_CODE_LENGTH = 20;
15229
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
15230
+ var ID_SALT = new TextEncoder().encode("vaultkeepr-recovery-id-v1");
15231
+ var SALT_LENGTH2 = 16;
15232
+ var ARGON2_OPTS2 = { t: 3, m: 65536, p: 4, dkLen: 32 };
15233
+ var SECRET_KEY_HEX = /^[0-9a-fA-F]{64}$/;
15234
+ function isAccountKeyText(value) {
15235
+ if (SECRET_KEY_HEX.test(value)) return true;
15236
+ if (value.length < 32 || value.length > 4096 || value.includes("BEGIN PGP")) return false;
15237
+ return !isEnvelopeText(value);
15238
+ }
15239
+ function isEnvelopeText(value) {
15240
+ if (!value.startsWith("{")) return false;
15241
+ try {
15242
+ const o = JSON.parse(value);
15243
+ return !!o && typeof o === "object" && typeof o.v === "number" && typeof o.iv === "string" && typeof o.ct === "string";
15244
+ } catch {
15245
+ return false;
15246
+ }
15247
+ }
15248
+ var argon2RecoveryKdf = (codeBytes2, salt) => argon2id3(codeBytes2, salt, ARGON2_OPTS2);
15249
+ function generateRecoveryCode() {
15250
+ const bytes = randomBytes10(RECOVERY_CODE_LENGTH);
15251
+ let code = "";
15252
+ for (const b of bytes) code += ALPHABET[b % 32];
15253
+ return code;
15254
+ }
15255
+ function normalizeRecoveryCode(input) {
15256
+ return input.toUpperCase().replace(/[\s-]/g, "").replace(/O/g, "0").replace(/[IL]/g, "1");
15257
+ }
15258
+ function isValidRecoveryCode(input) {
15259
+ const code = normalizeRecoveryCode(input);
15260
+ return code.length === RECOVERY_CODE_LENGTH && [...code].every((c) => ALPHABET.includes(c));
15261
+ }
15262
+ function formatRecoveryCode(code) {
15263
+ const canonical = normalizeRecoveryCode(code);
15264
+ return canonical.match(/.{1,5}/g)?.join("-") ?? canonical;
15265
+ }
15266
+ function codeBytes(code) {
15267
+ const canonical = normalizeRecoveryCode(code);
15268
+ if (!isValidRecoveryCode(canonical)) throw new Error("Invalid recovery code");
15269
+ return new TextEncoder().encode(canonical);
15270
+ }
15271
+ async function deriveRecoveryId(code, kdf = argon2RecoveryKdf) {
15272
+ return bytesToHex12(await kdf(codeBytes(code), ID_SALT));
15273
+ }
15274
+ async function createRecoveryBlob(secretKeyHex, code, kdf = argon2RecoveryKdf) {
15275
+ if (!isAccountKeyText(secretKeyHex)) throw new Error("Invalid secret key");
15276
+ const salt = randomBytes10(SALT_LENGTH2);
15277
+ const key = await kdf(codeBytes(code), salt);
15278
+ const encrypted = encryptVault(secretKeyHex, key);
15279
+ return { version: 1, salt: bytesToHex12(salt), encrypted };
15280
+ }
15281
+ async function openRecoveryBlob(blob, code, kdf = argon2RecoveryKdf) {
15282
+ if (blob.version !== 1) throw new Error("Unsupported recovery blob version");
15283
+ let plain;
15284
+ try {
15285
+ const key = await kdf(codeBytes(code), hexToBytes10(blob.salt));
15286
+ plain = decryptVault(blob.encrypted, key);
15287
+ } catch {
15288
+ throw new Error("Recovery failed");
15289
+ }
15290
+ const secretKey = typeof plain === "string" ? plain : new TextDecoder().decode(plain);
15291
+ if (!isAccountKeyText(secretKey)) throw new Error("Recovery failed");
15292
+ return secretKey;
15293
+ }
15294
+
15295
+ // src/key-join.ts
15296
+ var KeyJoinError = class extends Error {
15297
+ constructor(code) {
15298
+ super(code);
15299
+ this.code = code;
15300
+ this.name = "KeyJoinError";
15301
+ }
15302
+ code;
15303
+ };
15304
+ async function prepareKeyJoin(secretKeyHex, post, kdf) {
15305
+ const code = generateRecoveryCode();
15306
+ const blob = await createRecoveryBlob(secretKeyHex, code, kdf);
15307
+ const id = await deriveRecoveryId(code, kdf);
15308
+ await post(id, blob);
15309
+ return { code: formatRecoveryCode(code) };
15310
+ }
15311
+ async function joinWithKeyCode(input, fetchBlob, kdf) {
15312
+ const code = normalizeRecoveryCode(input);
15313
+ if (!isValidRecoveryCode(code)) throw new KeyJoinError("invalid_code");
15314
+ const id = await deriveRecoveryId(code, kdf);
15315
+ const blob = await fetchBlob(id);
15316
+ if (!blob) throw new KeyJoinError("not_found");
15317
+ return openRecoveryBlob(blob, code, kdf);
15318
+ }
15319
+
15320
+ // src/import-key-policy.ts
15321
+ function decideEncryptedImport(input) {
15322
+ const device = input.deviceKey ? input.deviceKey.toLowerCase() : null;
15323
+ const file = input.fileKey ? input.fileKey.toLowerCase() : null;
15324
+ if (!device && !file) return { action: "import_keyless" };
15325
+ if (!device && file) return { action: "join_with_file_key" };
15326
+ if (device && !file) return { action: "refuse", reason: "keyless_file_on_keyed_device" };
15327
+ return device === file ? { action: "keep_device_key" } : { action: "refuse", reason: "other_account" };
15328
+ }
15003
15329
  export {
15004
15330
  BIP39_WORDLIST,
15005
15331
  BUILTIN_FOLDER_IDS,
@@ -15014,6 +15340,7 @@ export {
15014
15340
  EXTENSION_MIN_PAYLOAD_VERSION,
15015
15341
  FRAGMENTED_DEFAULT_THRESHOLD,
15016
15342
  FRAGMENTED_DEFAULT_TOTAL,
15343
+ KeyJoinError,
15017
15344
  MOBILE_MAX_PAYLOAD_VERSION,
15018
15345
  MOBILE_MIN_PAYLOAD_VERSION,
15019
15346
  P256_SPKI_HEADER,
@@ -15024,11 +15351,14 @@ export {
15024
15351
  PAYLOAD_VERSION_V2,
15025
15352
  PAYLOAD_VERSION_V3,
15026
15353
  PAYLOAD_VERSION_V4,
15354
+ RECOVERY_CODE_LENGTH,
15027
15355
  TAG_COLORS,
15028
15356
  VALID_TAGS,
15029
15357
  VaultKeepR_SLM,
15358
+ accountKeyState,
15030
15359
  addEntry,
15031
15360
  analyzeVaultHealth,
15361
+ argon2RecoveryKdf,
15032
15362
  buildAttestationObject,
15033
15363
  buildAuthDataForCreate,
15034
15364
  buildAuthDataForGet,
@@ -15043,6 +15373,7 @@ export {
15043
15373
  captureError,
15044
15374
  checkEmailBreaches,
15045
15375
  checkVaultBreaches,
15376
+ cleanEntryTitle,
15046
15377
  cleanMrzField,
15047
15378
  clearErrorBuffer,
15048
15379
  clearHiddenWalletCache,
@@ -15053,8 +15384,10 @@ export {
15053
15384
  createExportPayload,
15054
15385
  createFolder,
15055
15386
  createKeyEnvelope,
15387
+ createRecoveryBlob,
15056
15388
  createSecureShare,
15057
15389
  createVaultPayload,
15390
+ decideEncryptedImport,
15058
15391
  decompressVault,
15059
15392
  decryptFromPair,
15060
15393
  decryptKeyEnvelope,
@@ -15076,6 +15409,7 @@ export {
15076
15409
  deriveMrzKseed,
15077
15410
  deriveNfcKey,
15078
15411
  derivePairSecret,
15412
+ deriveRecoveryId,
15079
15413
  digitsOnly,
15080
15414
  encryptAndFragmentDocument,
15081
15415
  encryptForPair,
@@ -15093,6 +15427,7 @@ export {
15093
15427
  formatCardDisplayMasked,
15094
15428
  formatCardNumber,
15095
15429
  formatErrorReport,
15430
+ formatRecoveryCode,
15096
15431
  fromBase64Url,
15097
15432
  generateBlurredThumbnailCanvas,
15098
15433
  generateCredentialId,
@@ -15104,17 +15439,22 @@ export {
15104
15439
  generatePassphrase,
15105
15440
  generatePassword,
15106
15441
  generatePrfSalt,
15442
+ generateRecoveryCode,
15107
15443
  generateSaltArgon2,
15108
15444
  generateSharePin,
15109
15445
  getCardBrand,
15110
15446
  getChecksumAddress,
15447
+ getEntryDisplayName,
15111
15448
  getEntryGroupLabel,
15112
15449
  getErrorBuffer,
15113
15450
  getHiddenWalletFromPassword,
15451
+ getHiddenWalletFromSecretKey,
15114
15452
  getHiddenWalletLegacy,
15115
15453
  getHiddenWalletPrivateKey,
15454
+ getHiddenWalletPrivateKeyFromSecretKey,
15116
15455
  getLast4,
15117
15456
  getMostUsedEntries,
15457
+ getPwnedCountForHash,
15118
15458
  getPwnedPasswordCount,
15119
15459
  getTOTPCode,
15120
15460
  getTOTPRemainingSeconds,
@@ -15128,6 +15468,7 @@ export {
15128
15468
  importProtonPassJson,
15129
15469
  importVaultText,
15130
15470
  isAcceptablePayloadVersion,
15471
+ isAccountKeyText,
15131
15472
  isCurrentPayloadVersion,
15132
15473
  isEncryptedExport,
15133
15474
  isNfcPayloadV1,
@@ -15139,15 +15480,19 @@ export {
15139
15480
  isPasskeyPrivateKeyEncrypted,
15140
15481
  isPrfSupported,
15141
15482
  isProtonPassExport,
15483
+ isValidRecoveryCode,
15142
15484
  isWebAuthnSupported,
15485
+ joinWithKeyCode,
15143
15486
  maskCardNumber,
15144
15487
  mergeFolderTrees,
15145
15488
  mergeFolders,
15146
15489
  mergeFragments,
15147
15490
  mergeImportedEntries,
15148
15491
  moveFolder,
15492
+ normalizeRecoveryCode,
15149
15493
  normalizeSignatureForKdf,
15150
15494
  normalizeUrl,
15495
+ openRecoveryBlob,
15151
15496
  parseMRZ,
15152
15497
  parseMrzDate,
15153
15498
  parsePairPayload,
@@ -15157,6 +15502,10 @@ export {
15157
15502
  parseTOTPUri,
15158
15503
  parseVault,
15159
15504
  parseVaultPayload,
15505
+ passageIdentity,
15506
+ passageNext,
15507
+ pointerWalletFor,
15508
+ prepareKeyJoin,
15160
15509
  publicKeyToCose,
15161
15510
  publicKeyToSpki,
15162
15511
  reassembleAndDecryptDocument,