@wishknish/knishio-client-ts 1.0.0 → 1.1.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.cjs CHANGED
@@ -572,6 +572,14 @@ var init_SecretStorageException = __esm({
572
572
  }
573
573
  );
574
574
  }
575
+ /**
576
+ * Validation error for storage options or parameters
577
+ */
578
+ static validationError(message) {
579
+ return new _SecretStorageException(message, {
580
+ code: "VALIDATION_ERROR"
581
+ });
582
+ }
575
583
  };
576
584
  }
577
585
  });
@@ -10697,10 +10705,121 @@ function constantTimeCompare(a, b) {
10697
10705
  return result === 0;
10698
10706
  }
10699
10707
 
10708
+ // src/storage/secretEnvelope.ts
10709
+ init_SecretStorageException();
10710
+ var ENVELOPE_ALGORITHM = "AES-GCM";
10711
+ var DEFAULT_ITERATIONS = 1e5;
10712
+ var SECRET_KEY_PREFIX = "knishio:secret:";
10713
+ var RECOVERY_KEY_PREFIX = "knishio:recovery:";
10714
+ var GCM_IV_LENGTH = 12;
10715
+ var SALT_LENGTH = 16;
10716
+ var textEncoder2 = new TextEncoder();
10717
+ function uint8ArrayToBase64(bytes) {
10718
+ let binary = "";
10719
+ const len = bytes.byteLength;
10720
+ for (let i = 0; i < len; i++) {
10721
+ const byte = bytes[i];
10722
+ if (byte !== void 0) {
10723
+ binary += String.fromCharCode(byte);
10724
+ }
10725
+ }
10726
+ return btoa(binary);
10727
+ }
10728
+ function base64ToUint8Array(base64) {
10729
+ const binary = atob(base64);
10730
+ const len = binary.length;
10731
+ const bytes = new Uint8Array(len);
10732
+ for (let i = 0; i < len; i++) {
10733
+ bytes[i] = binary.charCodeAt(i);
10734
+ }
10735
+ return bytes;
10736
+ }
10737
+ async function deriveEnvelopeKey(passphrase, salt, iterations = DEFAULT_ITERATIONS) {
10738
+ if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
10739
+ throw new exports.SecretStorageException("WebCrypto API is not available");
10740
+ }
10741
+ const passphraseBytes = textEncoder2.encode(passphrase);
10742
+ try {
10743
+ const baseKey = await globalThis.crypto.subtle.importKey(
10744
+ "raw",
10745
+ passphraseBytes,
10746
+ "PBKDF2",
10747
+ false,
10748
+ ["deriveKey"]
10749
+ );
10750
+ return await globalThis.crypto.subtle.deriveKey(
10751
+ {
10752
+ name: "PBKDF2",
10753
+ salt,
10754
+ iterations,
10755
+ hash: "SHA-256"
10756
+ },
10757
+ baseKey,
10758
+ { name: "AES-GCM", length: 256 },
10759
+ false,
10760
+ ["encrypt", "decrypt"]
10761
+ );
10762
+ } finally {
10763
+ zeroizeBytes(passphraseBytes);
10764
+ }
10765
+ }
10766
+ async function sealEnvelope(secret, passphrase, metadata) {
10767
+ if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
10768
+ throw new exports.SecretStorageException("WebCrypto API is not available");
10769
+ }
10770
+ const salt = new Uint8Array(SALT_LENGTH);
10771
+ const iv = new Uint8Array(GCM_IV_LENGTH);
10772
+ globalThis.crypto.getRandomValues(salt);
10773
+ globalThis.crypto.getRandomValues(iv);
10774
+ const key = await deriveEnvelopeKey(passphrase, salt, DEFAULT_ITERATIONS);
10775
+ const secretBytes = textEncoder2.encode(secret);
10776
+ try {
10777
+ const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
10778
+ {
10779
+ name: ENVELOPE_ALGORITHM,
10780
+ iv
10781
+ },
10782
+ key,
10783
+ secretBytes
10784
+ );
10785
+ const ciphertext = uint8ArrayToBase64(new Uint8Array(encryptedBuffer));
10786
+ return {
10787
+ version: 1,
10788
+ ciphertext,
10789
+ iv: uint8ArrayToBase64(iv),
10790
+ salt: uint8ArrayToBase64(salt),
10791
+ algorithm: ENVELOPE_ALGORITHM,
10792
+ iterations: DEFAULT_ITERATIONS,
10793
+ metadata
10794
+ };
10795
+ } finally {
10796
+ zeroizeBytes(secretBytes);
10797
+ }
10798
+ }
10799
+ async function openEnvelope(payload, passphrase) {
10800
+ if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
10801
+ throw new exports.SecretStorageException("WebCrypto API is not available");
10802
+ }
10803
+ const salt = base64ToUint8Array(payload.salt);
10804
+ const iv = base64ToUint8Array(payload.iv);
10805
+ const ciphertext = base64ToUint8Array(payload.ciphertext);
10806
+ const key = await deriveEnvelopeKey(passphrase, salt, payload.iterations ?? DEFAULT_ITERATIONS);
10807
+ const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
10808
+ {
10809
+ name: ENVELOPE_ALGORITHM,
10810
+ iv
10811
+ },
10812
+ key,
10813
+ ciphertext
10814
+ );
10815
+ return new Uint8Array(decryptedBuffer);
10816
+ }
10817
+
10700
10818
  // src/storage/MemorySecretStorageProvider.ts
10701
10819
  var MemorySecretStorageProvider = class {
10702
10820
  providerType = "memory";
10703
10821
  secrets = /* @__PURE__ */ new Map();
10822
+ recoverySecrets = /* @__PURE__ */ new Map();
10704
10823
  /**
10705
10824
  * Memory storage is not hardware backed
10706
10825
  */
@@ -10731,6 +10850,17 @@ var MemorySecretStorageProvider = class {
10731
10850
  providerType: this.providerType
10732
10851
  };
10733
10852
  this.secrets.set(bundleHash, { secret, metadata });
10853
+ if (options?.recoveryPassphrase) {
10854
+ const recoveryMetadata = {
10855
+ bundleHash,
10856
+ label: options?.label,
10857
+ createdAt: Date.now(),
10858
+ hardwareBacked: false,
10859
+ providerType: "webcrypto-aes-gcm"
10860
+ };
10861
+ const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
10862
+ this.recoverySecrets.set(bundleHash, JSON.stringify(recoveryPayload));
10863
+ }
10734
10864
  }
10735
10865
  /**
10736
10866
  * Retrieve a secret from memory
@@ -10743,6 +10873,7 @@ var MemorySecretStorageProvider = class {
10743
10873
  * Delete a stored secret
10744
10874
  */
10745
10875
  async deleteSecret(bundleHash) {
10876
+ this.recoverySecrets.delete(bundleHash);
10746
10877
  return this.secrets.delete(bundleHash);
10747
10878
  }
10748
10879
  /**
@@ -10772,6 +10903,48 @@ var MemorySecretStorageProvider = class {
10772
10903
  */
10773
10904
  clear() {
10774
10905
  this.secrets.clear();
10906
+ this.recoverySecrets.clear();
10907
+ }
10908
+ /**
10909
+ * Recover a secret using its recovery envelope and restore it
10910
+ */
10911
+ async recoverSecret(bundleHash, recoveryPassphrase, options) {
10912
+ if (!bundleHash) {
10913
+ throw new exports.SecretStorageException("Bundle hash cannot be empty");
10914
+ }
10915
+ if (!recoveryPassphrase) {
10916
+ throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
10917
+ }
10918
+ const raw = this.recoverySecrets.get(bundleHash);
10919
+ if (!raw) {
10920
+ throw exports.SecretStorageException.notFound(bundleHash);
10921
+ }
10922
+ let payload;
10923
+ try {
10924
+ payload = JSON.parse(raw);
10925
+ } catch {
10926
+ throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
10927
+ }
10928
+ let decryptedBytes;
10929
+ try {
10930
+ decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
10931
+ } catch (err) {
10932
+ if (err instanceof exports.SecretStorageException) {
10933
+ throw err;
10934
+ }
10935
+ const msg = err instanceof Error ? err.message : String(err);
10936
+ throw exports.SecretStorageException.decryptionFailed(msg);
10937
+ }
10938
+ let secretStr;
10939
+ try {
10940
+ secretStr = new TextDecoder().decode(decryptedBytes);
10941
+ } finally {
10942
+ zeroizeBytes(decryptedBytes);
10943
+ }
10944
+ await this.storeSecret(bundleHash, secretStr, {
10945
+ ...options,
10946
+ recoveryPassphrase
10947
+ });
10775
10948
  }
10776
10949
  };
10777
10950
 
@@ -12593,45 +12766,25 @@ var MemoryStorageBackend = class {
12593
12766
  return Array.from(this.store.keys());
12594
12767
  }
12595
12768
  };
12596
- function uint8ArrayToBase64(bytes) {
12597
- let binary = "";
12598
- const len = bytes.byteLength;
12599
- for (let i = 0; i < len; i++) {
12600
- const byte = bytes[i];
12601
- if (byte !== void 0) {
12602
- binary += String.fromCharCode(byte);
12603
- }
12604
- }
12605
- return btoa(binary);
12606
- }
12607
- function base64ToUint8Array(base64) {
12608
- const binary = atob(base64);
12609
- const len = binary.length;
12610
- const bytes = new Uint8Array(len);
12611
- for (let i = 0; i < len; i++) {
12612
- bytes[i] = binary.charCodeAt(i);
12613
- }
12614
- return bytes;
12615
- }
12616
- var textEncoder2 = new TextEncoder();
12617
12769
  var textDecoder = new TextDecoder();
12618
- var KEY_PREFIX = "knishio:secret:";
12619
- var DEFAULT_ITERATIONS = 1e5;
12770
+ var KEY_PREFIX = SECRET_KEY_PREFIX;
12620
12771
  var WebCryptoSecretStorageProvider = class {
12621
12772
  providerType = "webcrypto-aes-gcm";
12622
12773
  backend;
12623
12774
  defaultPassphrase;
12624
- hardwareBacked;
12625
12775
  constructor(options = {}) {
12626
12776
  this.backend = options.backend ?? new MemoryStorageBackend();
12627
12777
  this.defaultPassphrase = options.defaultPassphrase;
12628
- this.hardwareBacked = options.hardwareBacked ?? false;
12629
12778
  }
12630
12779
  /**
12631
- * Whether this provider is backed by hardware (e.g. WebAuthn PRF wrapping)
12780
+ * True only when this provider holds a non-exportable key inside platform-secure
12781
+ * hardware (Android TEE/StrongBox, Secure Enclave, TPM) and learned that from the
12782
+ * platform itself — never from a caller argument. Software envelope providers
12783
+ * return false. The value is persisted as `metadata.hardwareBacked` in every
12784
+ * envelope this provider writes.
12632
12785
  */
12633
12786
  isHardwareBacked() {
12634
- return this.hardwareBacked;
12787
+ return false;
12635
12788
  }
12636
12789
  /**
12637
12790
  * Check if WebCrypto subtle API is available
@@ -12639,38 +12792,6 @@ var WebCryptoSecretStorageProvider = class {
12639
12792
  async isAvailable() {
12640
12793
  return typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
12641
12794
  }
12642
- /**
12643
- * Derive an AES-GCM CryptoKey from a passphrase and salt using PBKDF2
12644
- */
12645
- async deriveKey(passphrase, salt, iterations = DEFAULT_ITERATIONS) {
12646
- if (!await this.isAvailable()) {
12647
- throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
12648
- }
12649
- const passphraseBytes = textEncoder2.encode(passphrase);
12650
- try {
12651
- const baseKey = await globalThis.crypto.subtle.importKey(
12652
- "raw",
12653
- passphraseBytes,
12654
- "PBKDF2",
12655
- false,
12656
- ["deriveKey"]
12657
- );
12658
- return await globalThis.crypto.subtle.deriveKey(
12659
- {
12660
- name: "PBKDF2",
12661
- salt,
12662
- iterations,
12663
- hash: "SHA-256"
12664
- },
12665
- baseKey,
12666
- { name: "AES-GCM", length: 256 },
12667
- false,
12668
- ["encrypt", "decrypt"]
12669
- );
12670
- } finally {
12671
- zeroizeBytes(passphraseBytes);
12672
- }
12673
- }
12674
12795
  /**
12675
12796
  * Store and encrypt a master secret
12676
12797
  */
@@ -12685,44 +12806,36 @@ var WebCryptoSecretStorageProvider = class {
12685
12806
  if (!passphrase) {
12686
12807
  throw new exports.SecretStorageException("Passphrase required for envelope encryption");
12687
12808
  }
12688
- const salt = new Uint8Array(16);
12689
- const iv = new Uint8Array(12);
12690
- globalThis.crypto.getRandomValues(salt);
12691
- globalThis.crypto.getRandomValues(iv);
12692
- const key = await this.deriveKey(passphrase, salt, DEFAULT_ITERATIONS);
12693
- const secretBytes = textEncoder2.encode(secret);
12809
+ if (!await this.isAvailable()) {
12810
+ throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
12811
+ }
12694
12812
  try {
12695
- const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
12696
- {
12697
- name: "AES-GCM",
12698
- iv
12699
- },
12700
- key,
12701
- secretBytes
12702
- );
12703
- const ciphertext = uint8ArrayToBase64(new Uint8Array(encryptedBuffer));
12704
12813
  const metadata = {
12705
12814
  bundleHash,
12706
12815
  label: options?.label,
12707
12816
  createdAt: Date.now(),
12708
- hardwareBacked: this.hardwareBacked,
12817
+ hardwareBacked: false,
12709
12818
  providerType: this.providerType
12710
12819
  };
12711
- const payload = {
12712
- version: 1,
12713
- ciphertext,
12714
- iv: uint8ArrayToBase64(iv),
12715
- salt: uint8ArrayToBase64(salt),
12716
- algorithm: "AES-GCM",
12717
- iterations: DEFAULT_ITERATIONS,
12718
- metadata
12719
- };
12820
+ const payload = await sealEnvelope(secret, passphrase, metadata);
12720
12821
  await this.backend.setItem(`${KEY_PREFIX}${bundleHash}`, JSON.stringify(payload));
12822
+ if (options?.recoveryPassphrase) {
12823
+ const recoveryMetadata = {
12824
+ bundleHash,
12825
+ label: options?.label,
12826
+ createdAt: Date.now(),
12827
+ hardwareBacked: false,
12828
+ providerType: "webcrypto-aes-gcm"
12829
+ };
12830
+ const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
12831
+ await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
12832
+ }
12721
12833
  } catch (err) {
12834
+ if (err instanceof exports.SecretStorageException) {
12835
+ throw err;
12836
+ }
12722
12837
  const msg = err instanceof Error ? err.message : String(err);
12723
12838
  throw new exports.SecretStorageException(`Encryption failed: ${msg}`);
12724
- } finally {
12725
- zeroizeBytes(secretBytes);
12726
12839
  }
12727
12840
  }
12728
12841
  /**
@@ -12743,26 +12856,20 @@ var WebCryptoSecretStorageProvider = class {
12743
12856
  if (!passphrase) {
12744
12857
  throw new exports.SecretStorageException("Passphrase required for secret decryption");
12745
12858
  }
12746
- const salt = base64ToUint8Array(payload.salt);
12747
- const iv = base64ToUint8Array(payload.iv);
12748
- const ciphertext = base64ToUint8Array(payload.ciphertext);
12859
+ if (!await this.isAvailable()) {
12860
+ throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
12861
+ }
12749
12862
  try {
12750
- const key = await this.deriveKey(passphrase, salt, payload.iterations ?? DEFAULT_ITERATIONS);
12751
- const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
12752
- {
12753
- name: "AES-GCM",
12754
- iv
12755
- },
12756
- key,
12757
- ciphertext
12758
- );
12759
- const decryptedBytes = new Uint8Array(decryptedBuffer);
12863
+ const decryptedBytes = await openEnvelope(payload, passphrase);
12760
12864
  try {
12761
12865
  return textDecoder.decode(decryptedBytes);
12762
12866
  } finally {
12763
12867
  zeroizeBytes(decryptedBytes);
12764
12868
  }
12765
12869
  } catch (err) {
12870
+ if (err instanceof exports.SecretStorageException) {
12871
+ throw err;
12872
+ }
12766
12873
  const msg = err instanceof Error ? err.message : String(err);
12767
12874
  throw exports.SecretStorageException.decryptionFailed(msg);
12768
12875
  }
@@ -12772,7 +12879,9 @@ var WebCryptoSecretStorageProvider = class {
12772
12879
  */
12773
12880
  async deleteSecret(bundleHash) {
12774
12881
  const key = `${KEY_PREFIX}${bundleHash}`;
12882
+ const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
12775
12883
  const result = await this.backend.removeItem(key);
12884
+ await this.backend.removeItem(recoveryKey);
12776
12885
  return result !== false;
12777
12886
  }
12778
12887
  /**
@@ -12787,7 +12896,7 @@ var WebCryptoSecretStorageProvider = class {
12787
12896
  */
12788
12897
  async listSecrets() {
12789
12898
  const keys = await this.backend.keys();
12790
- const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX));
12899
+ const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX) && !k.startsWith(RECOVERY_KEY_PREFIX));
12791
12900
  const results = [];
12792
12901
  for (const key of matchingKeys) {
12793
12902
  const raw = await this.backend.getItem(key);
@@ -12821,20 +12930,11 @@ var WebCryptoSecretStorageProvider = class {
12821
12930
  if (!passphrase) {
12822
12931
  throw new exports.SecretStorageException("Passphrase required for secret decryption");
12823
12932
  }
12824
- const salt = base64ToUint8Array(payload.salt);
12825
- const iv = base64ToUint8Array(payload.iv);
12826
- const ciphertext = base64ToUint8Array(payload.ciphertext);
12933
+ if (!await this.isAvailable()) {
12934
+ throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
12935
+ }
12827
12936
  try {
12828
- const key = await this.deriveKey(passphrase, salt, payload.iterations ?? DEFAULT_ITERATIONS);
12829
- const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
12830
- {
12831
- name: "AES-GCM",
12832
- iv
12833
- },
12834
- key,
12835
- ciphertext
12836
- );
12837
- const decryptedBytes = new Uint8Array(decryptedBuffer);
12937
+ const decryptedBytes = await openEnvelope(payload, passphrase);
12838
12938
  return await withSecureBytes(decryptedBytes, async (bytes) => {
12839
12939
  const secretString = textDecoder.decode(bytes);
12840
12940
  return await fn(secretString);
@@ -12847,6 +12947,1071 @@ var WebCryptoSecretStorageProvider = class {
12847
12947
  throw exports.SecretStorageException.decryptionFailed(msg);
12848
12948
  }
12849
12949
  }
12950
+ /**
12951
+ * Recover a secret using its recovery envelope and re-enroll it
12952
+ */
12953
+ async recoverSecret(bundleHash, recoveryPassphrase, options) {
12954
+ if (!bundleHash) {
12955
+ throw new exports.SecretStorageException("Bundle hash cannot be empty");
12956
+ }
12957
+ if (!recoveryPassphrase) {
12958
+ throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
12959
+ }
12960
+ const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
12961
+ if (!raw) {
12962
+ throw exports.SecretStorageException.notFound(bundleHash);
12963
+ }
12964
+ let payload;
12965
+ try {
12966
+ payload = JSON.parse(raw);
12967
+ } catch {
12968
+ throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
12969
+ }
12970
+ let decryptedBytes;
12971
+ try {
12972
+ decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
12973
+ } catch (err) {
12974
+ if (err instanceof exports.SecretStorageException) {
12975
+ throw err;
12976
+ }
12977
+ const msg = err instanceof Error ? err.message : String(err);
12978
+ throw exports.SecretStorageException.decryptionFailed(msg);
12979
+ }
12980
+ let secretStr;
12981
+ try {
12982
+ secretStr = textDecoder.decode(decryptedBytes);
12983
+ } finally {
12984
+ zeroizeBytes(decryptedBytes);
12985
+ }
12986
+ const storePassphrase = options?.passphrase ?? this.defaultPassphrase ?? recoveryPassphrase;
12987
+ await this.storeSecret(bundleHash, secretStr, {
12988
+ ...options,
12989
+ passphrase: storePassphrase,
12990
+ recoveryPassphrase
12991
+ });
12992
+ }
12993
+ };
12994
+
12995
+ // src/storage/FileStorageBackend.ts
12996
+ init_SecretStorageException();
12997
+ var FileStorageBackend = class {
12998
+ filePath;
12999
+ store = /* @__PURE__ */ new Map();
13000
+ loaded = false;
13001
+ constructor(filePath) {
13002
+ if (!filePath) {
13003
+ throw new exports.SecretStorageException("Storage file path cannot be empty");
13004
+ }
13005
+ this.filePath = filePath;
13006
+ }
13007
+ async getFs() {
13008
+ try {
13009
+ const fs = await import('fs/promises');
13010
+ const path = await import('path');
13011
+ return { fs, path };
13012
+ } catch {
13013
+ throw exports.SecretStorageException.unavailable(
13014
+ "file-storage",
13015
+ "FileStorageBackend is only supported in Node.js environments with node:fs access"
13016
+ );
13017
+ }
13018
+ }
13019
+ async ensureLoaded() {
13020
+ if (this.loaded) {
13021
+ return this.store;
13022
+ }
13023
+ const { fs } = await this.getFs();
13024
+ try {
13025
+ const content = await fs.readFile(this.filePath, "utf8");
13026
+ let parsed;
13027
+ try {
13028
+ parsed = JSON.parse(content);
13029
+ } catch {
13030
+ throw exports.SecretStorageException.decryptionFailed("Corrupted storage file format");
13031
+ }
13032
+ if (parsed && typeof parsed === "object") {
13033
+ this.store = new Map(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
13034
+ }
13035
+ } catch (err) {
13036
+ if (err instanceof exports.SecretStorageException) {
13037
+ throw err;
13038
+ }
13039
+ const nodeErr = err;
13040
+ if (nodeErr?.code !== "ENOENT") {
13041
+ const msg = err instanceof Error ? err.message : String(err);
13042
+ throw new exports.SecretStorageException(`Failed to read storage file: ${msg}`);
13043
+ }
13044
+ this.store = /* @__PURE__ */ new Map();
13045
+ }
13046
+ this.loaded = true;
13047
+ return this.store;
13048
+ }
13049
+ async persist() {
13050
+ const { fs, path } = await this.getFs();
13051
+ const dir = path.dirname(this.filePath);
13052
+ if (dir && dir !== ".") {
13053
+ await fs.mkdir(dir, { recursive: true });
13054
+ }
13055
+ const tmpPath = `${this.filePath}.tmp.${Date.now()}_${Math.random().toString(36).slice(2)}`;
13056
+ const data = JSON.stringify(Object.fromEntries(this.store), null, 2);
13057
+ try {
13058
+ await fs.writeFile(tmpPath, data, { mode: 384, encoding: "utf8" });
13059
+ if (typeof process !== "undefined" && process.platform !== "win32") {
13060
+ try {
13061
+ await fs.chmod(tmpPath, 384);
13062
+ } catch {
13063
+ }
13064
+ }
13065
+ await fs.rename(tmpPath, this.filePath);
13066
+ } catch (err) {
13067
+ try {
13068
+ await fs.unlink(tmpPath);
13069
+ } catch {
13070
+ }
13071
+ const msg = err instanceof Error ? err.message : String(err);
13072
+ throw new exports.SecretStorageException(`Failed to persist storage file: ${msg}`);
13073
+ }
13074
+ }
13075
+ async getItem(key) {
13076
+ await this.ensureLoaded();
13077
+ return this.store.get(key) ?? null;
13078
+ }
13079
+ async setItem(key, value) {
13080
+ await this.ensureLoaded();
13081
+ this.store.set(key, value);
13082
+ await this.persist();
13083
+ }
13084
+ async removeItem(key) {
13085
+ await this.ensureLoaded();
13086
+ const existed = this.store.delete(key);
13087
+ if (existed) {
13088
+ await this.persist();
13089
+ }
13090
+ return existed;
13091
+ }
13092
+ async keys() {
13093
+ await this.ensureLoaded();
13094
+ return Array.from(this.store.keys());
13095
+ }
13096
+ };
13097
+
13098
+ // src/storage/WebStorageBackend.ts
13099
+ init_SecretStorageException();
13100
+ var WebStorageBackend = class {
13101
+ storage;
13102
+ prefix;
13103
+ constructor(storage, prefix = "knishio:") {
13104
+ if (storage) {
13105
+ this.storage = storage;
13106
+ } else if (typeof globalThis !== "undefined" && globalThis.localStorage) {
13107
+ this.storage = globalThis.localStorage;
13108
+ } else {
13109
+ throw exports.SecretStorageException.unavailable(
13110
+ "web-storage",
13111
+ "WebStorageBackend requires a Storage object or global localStorage"
13112
+ );
13113
+ }
13114
+ this.prefix = prefix;
13115
+ }
13116
+ getItem(key) {
13117
+ return this.storage.getItem(key);
13118
+ }
13119
+ setItem(key, value) {
13120
+ this.storage.setItem(key, value);
13121
+ }
13122
+ removeItem(key) {
13123
+ const existed = this.storage.getItem(key) !== null;
13124
+ this.storage.removeItem(key);
13125
+ return existed;
13126
+ }
13127
+ keys() {
13128
+ const result = [];
13129
+ const len = this.storage.length;
13130
+ for (let i = 0; i < len; i++) {
13131
+ const k = this.storage.key(i);
13132
+ if (k !== null) {
13133
+ if (!this.prefix || k.startsWith(this.prefix)) {
13134
+ result.push(k);
13135
+ }
13136
+ }
13137
+ }
13138
+ return result;
13139
+ }
13140
+ };
13141
+
13142
+ // src/storage/WebAuthnPrfSecretStorageProvider.ts
13143
+ init_SecretStorageException();
13144
+ var PRF_SALT_LABEL = "knishio:secret-storage:webauthn-prf:v1";
13145
+ var KEK_INFO = "knishio:secret-storage:kek:v1";
13146
+ var KEY_PREFIX2 = SECRET_KEY_PREFIX;
13147
+ var GCM_IV_LENGTH2 = 12;
13148
+ var textEncoder3 = new TextEncoder();
13149
+ var textDecoder2 = new TextDecoder();
13150
+ function base64UrlEncode(bytes) {
13151
+ return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
13152
+ }
13153
+ function base64UrlDecode(str) {
13154
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
13155
+ while (base64.length % 4 !== 0) {
13156
+ base64 += "=";
13157
+ }
13158
+ return base64ToUint8Array(base64);
13159
+ }
13160
+ function toUint8Array(buf) {
13161
+ if (buf instanceof Uint8Array) {
13162
+ return buf;
13163
+ }
13164
+ if (ArrayBuffer.isView(buf)) {
13165
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
13166
+ }
13167
+ return new Uint8Array(buf);
13168
+ }
13169
+ async function computePrfSalt() {
13170
+ const hash = await globalThis.crypto.subtle.digest("SHA-256", textEncoder3.encode(PRF_SALT_LABEL));
13171
+ return new Uint8Array(hash);
13172
+ }
13173
+ async function deriveKekFromPrf(prfOutput, prfSalt) {
13174
+ const hkdfKey = await globalThis.crypto.subtle.importKey(
13175
+ "raw",
13176
+ prfOutput,
13177
+ "HKDF",
13178
+ false,
13179
+ ["deriveKey"]
13180
+ );
13181
+ return await globalThis.crypto.subtle.deriveKey(
13182
+ {
13183
+ name: "HKDF",
13184
+ hash: "SHA-256",
13185
+ salt: prfSalt,
13186
+ info: textEncoder3.encode(KEK_INFO)
13187
+ },
13188
+ hkdfKey,
13189
+ { name: "AES-GCM", length: 256 },
13190
+ false,
13191
+ ["encrypt", "decrypt"]
13192
+ );
13193
+ }
13194
+ var WebAuthnPrfSecretStorageProvider = class {
13195
+ providerType = "webauthn-prf";
13196
+ backend;
13197
+ rp;
13198
+ user;
13199
+ credentialsContainer;
13200
+ alias;
13201
+ cachedPassphrase;
13202
+ constructor(options) {
13203
+ this.backend = options.backend;
13204
+ this.rp = options.rp;
13205
+ this.user = options.user;
13206
+ this.credentialsContainer = options.credentials;
13207
+ this.alias = options.alias ?? "default";
13208
+ }
13209
+ get credentials() {
13210
+ if (this.credentialsContainer) {
13211
+ return this.credentialsContainer;
13212
+ }
13213
+ if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.credentials) {
13214
+ return globalThis.navigator.credentials;
13215
+ }
13216
+ throw exports.SecretStorageException.unavailable(
13217
+ this.providerType,
13218
+ "WebAuthn credentials container is not available"
13219
+ );
13220
+ }
13221
+ get recordKey() {
13222
+ return `knishio:webauthn-prf:${this.alias}`;
13223
+ }
13224
+ isHardwareBacked() {
13225
+ return false;
13226
+ }
13227
+ async isAvailable() {
13228
+ const hasWebCrypto = typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
13229
+ const hasCredentials = Boolean(
13230
+ this.credentialsContainer || typeof globalThis.navigator !== "undefined" && globalThis.navigator.credentials && typeof globalThis.PublicKeyCredential !== "undefined"
13231
+ );
13232
+ return hasWebCrypto && hasCredentials;
13233
+ }
13234
+ /**
13235
+ * Enroll a new passkey credential with PRF support and wrap a random device passphrase
13236
+ */
13237
+ async enroll() {
13238
+ const existing = await this.backend.getItem(this.recordKey);
13239
+ if (existing) {
13240
+ return;
13241
+ }
13242
+ if (!await this.isAvailable()) {
13243
+ throw exports.SecretStorageException.unavailable(this.providerType, "WebAuthn PRF is not available");
13244
+ }
13245
+ const challenge = new Uint8Array(32);
13246
+ globalThis.crypto.getRandomValues(challenge);
13247
+ const credential = await this.credentials.create({
13248
+ publicKey: {
13249
+ rp: this.rp,
13250
+ user: {
13251
+ id: this.user.id,
13252
+ name: this.user.name,
13253
+ displayName: this.user.displayName
13254
+ },
13255
+ challenge,
13256
+ pubKeyCredParams: [
13257
+ { type: "public-key", alg: -7 },
13258
+ { type: "public-key", alg: -257 }
13259
+ ],
13260
+ authenticatorSelection: {
13261
+ residentKey: "required",
13262
+ userVerification: "required"
13263
+ },
13264
+ extensions: {
13265
+ prf: {}
13266
+ }
13267
+ }
13268
+ });
13269
+ if (!credential) {
13270
+ throw exports.SecretStorageException.unavailable(this.providerType, "Authenticator creation returned null");
13271
+ }
13272
+ const extResults = credential.getClientExtensionResults?.();
13273
+ if (extResults?.prf?.enabled !== true) {
13274
+ throw exports.SecretStorageException.unavailable(
13275
+ this.providerType,
13276
+ "authenticator does not support the PRF extension"
13277
+ );
13278
+ }
13279
+ const credentialIdBytes = new Uint8Array(credential.rawId);
13280
+ const prfSalt = await computePrfSalt();
13281
+ const getChallenge = new Uint8Array(32);
13282
+ globalThis.crypto.getRandomValues(getChallenge);
13283
+ let assertion;
13284
+ try {
13285
+ assertion = await this.credentials.get({
13286
+ publicKey: {
13287
+ challenge: getChallenge,
13288
+ rpId: this.rp.id,
13289
+ allowCredentials: [
13290
+ {
13291
+ type: "public-key",
13292
+ id: credentialIdBytes
13293
+ }
13294
+ ],
13295
+ userVerification: "required",
13296
+ extensions: {
13297
+ prf: {
13298
+ eval: {
13299
+ first: prfSalt
13300
+ }
13301
+ }
13302
+ }
13303
+ }
13304
+ });
13305
+ } catch (err) {
13306
+ const isNotAllowed = err instanceof Error && err.name === "NotAllowedError" || err?.name === "NotAllowedError";
13307
+ if (isNotAllowed) {
13308
+ throw exports.SecretStorageException.unavailable(
13309
+ this.providerType,
13310
+ "authenticator refused or credential missing"
13311
+ );
13312
+ }
13313
+ throw err;
13314
+ }
13315
+ if (!assertion) {
13316
+ throw exports.SecretStorageException.unavailable(
13317
+ this.providerType,
13318
+ "authenticator refused or credential missing"
13319
+ );
13320
+ }
13321
+ const getExtResults = assertion?.getClientExtensionResults?.();
13322
+ const firstOutput = getExtResults?.prf?.results?.first;
13323
+ if (!firstOutput) {
13324
+ throw exports.SecretStorageException.unavailable(
13325
+ this.providerType,
13326
+ "authenticator returned no PRF result"
13327
+ );
13328
+ }
13329
+ const prfBytes = toUint8Array(firstOutput);
13330
+ const kek = await deriveKekFromPrf(prfBytes, prfSalt);
13331
+ const devicePassphraseBytes = new Uint8Array(32);
13332
+ globalThis.crypto.getRandomValues(devicePassphraseBytes);
13333
+ const devicePassphrase = uint8ArrayToBase64(devicePassphraseBytes);
13334
+ const iv = new Uint8Array(GCM_IV_LENGTH2);
13335
+ globalThis.crypto.getRandomValues(iv);
13336
+ const passphraseBytes = textEncoder3.encode(devicePassphrase);
13337
+ try {
13338
+ const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
13339
+ {
13340
+ name: "AES-GCM",
13341
+ iv
13342
+ },
13343
+ kek,
13344
+ passphraseBytes
13345
+ );
13346
+ const record = {
13347
+ version: 1,
13348
+ credentialId: base64UrlEncode(credentialIdBytes),
13349
+ iv: uint8ArrayToBase64(iv),
13350
+ ciphertext: uint8ArrayToBase64(new Uint8Array(encryptedBuffer))
13351
+ };
13352
+ await this.backend.setItem(this.recordKey, JSON.stringify(record));
13353
+ this.cachedPassphrase = devicePassphrase;
13354
+ } finally {
13355
+ zeroizeBytes(passphraseBytes);
13356
+ zeroizeBytes(devicePassphraseBytes);
13357
+ }
13358
+ }
13359
+ /**
13360
+ * Unlock the device passphrase using the enrolled WebAuthn PRF credential
13361
+ */
13362
+ async unlock() {
13363
+ if (this.cachedPassphrase) {
13364
+ return this.cachedPassphrase;
13365
+ }
13366
+ const rawRecord = await this.backend.getItem(this.recordKey);
13367
+ if (!rawRecord) {
13368
+ throw exports.SecretStorageException.unavailable(
13369
+ this.providerType,
13370
+ "no enrolled credential; call enroll() first"
13371
+ );
13372
+ }
13373
+ let record;
13374
+ try {
13375
+ record = JSON.parse(rawRecord);
13376
+ } catch {
13377
+ throw exports.SecretStorageException.decryptionFailed("Corrupted PRF record format");
13378
+ }
13379
+ const credentialIdBytes = base64UrlDecode(record.credentialId);
13380
+ const prfSalt = await computePrfSalt();
13381
+ const challenge = new Uint8Array(32);
13382
+ globalThis.crypto.getRandomValues(challenge);
13383
+ let assertion;
13384
+ try {
13385
+ assertion = await this.credentials.get({
13386
+ publicKey: {
13387
+ challenge,
13388
+ rpId: this.rp.id,
13389
+ allowCredentials: [
13390
+ {
13391
+ type: "public-key",
13392
+ id: credentialIdBytes
13393
+ }
13394
+ ],
13395
+ userVerification: "required",
13396
+ extensions: {
13397
+ prf: {
13398
+ eval: {
13399
+ first: prfSalt
13400
+ }
13401
+ }
13402
+ }
13403
+ }
13404
+ });
13405
+ } catch (err) {
13406
+ const isNotAllowed = err instanceof Error && err.name === "NotAllowedError" || err?.name === "NotAllowedError";
13407
+ if (isNotAllowed) {
13408
+ throw exports.SecretStorageException.unavailable(
13409
+ this.providerType,
13410
+ "authenticator refused or credential missing"
13411
+ );
13412
+ }
13413
+ throw err;
13414
+ }
13415
+ if (!assertion) {
13416
+ throw exports.SecretStorageException.unavailable(
13417
+ this.providerType,
13418
+ "authenticator refused or credential missing"
13419
+ );
13420
+ }
13421
+ const extResults = assertion?.getClientExtensionResults?.();
13422
+ const firstOutput = extResults?.prf?.results?.first;
13423
+ if (!firstOutput) {
13424
+ throw exports.SecretStorageException.unavailable(
13425
+ this.providerType,
13426
+ "authenticator returned no PRF result"
13427
+ );
13428
+ }
13429
+ const prfBytes = toUint8Array(firstOutput);
13430
+ const kek = await deriveKekFromPrf(prfBytes, prfSalt);
13431
+ const iv = base64ToUint8Array(record.iv);
13432
+ const ciphertext = base64ToUint8Array(record.ciphertext);
13433
+ try {
13434
+ const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
13435
+ {
13436
+ name: "AES-GCM",
13437
+ iv
13438
+ },
13439
+ kek,
13440
+ ciphertext
13441
+ );
13442
+ const decryptedBytes = new Uint8Array(decryptedBuffer);
13443
+ try {
13444
+ this.cachedPassphrase = textDecoder2.decode(decryptedBytes);
13445
+ return this.cachedPassphrase;
13446
+ } finally {
13447
+ zeroizeBytes(decryptedBytes);
13448
+ }
13449
+ } catch {
13450
+ throw exports.SecretStorageException.decryptionFailed(
13451
+ "wrapped device passphrase failed authentication under the enrolled credential"
13452
+ );
13453
+ }
13454
+ }
13455
+ /**
13456
+ * Lock the provider by clearing cached passphrase material
13457
+ */
13458
+ lock() {
13459
+ this.cachedPassphrase = void 0;
13460
+ }
13461
+ /**
13462
+ * Unenroll the current credential, removing the stored PRF record and clearing cached passphrase
13463
+ */
13464
+ async unenroll() {
13465
+ this.lock();
13466
+ await this.backend.removeItem(this.recordKey);
13467
+ }
13468
+ async storeSecret(bundleHash, secret, options) {
13469
+ if (!bundleHash) {
13470
+ throw new exports.SecretStorageException("Bundle hash cannot be empty");
13471
+ }
13472
+ if (!secret) {
13473
+ throw new exports.SecretStorageException("Secret cannot be empty");
13474
+ }
13475
+ if (options?.passphrase) {
13476
+ throw new exports.SecretStorageException(
13477
+ "WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
13478
+ );
13479
+ }
13480
+ if (!options?.recoveryPassphrase && !options?.allowUnrecoverable) {
13481
+ throw exports.SecretStorageException.validationError(
13482
+ "Recovery passphrase required for non-exportable hardware key unless allowUnrecoverable is true"
13483
+ );
13484
+ }
13485
+ const passphrase = await this.unlock();
13486
+ const metadata = {
13487
+ bundleHash,
13488
+ label: options?.label,
13489
+ createdAt: Date.now(),
13490
+ hardwareBacked: false,
13491
+ providerType: this.providerType
13492
+ };
13493
+ const payload = await sealEnvelope(secret, passphrase, metadata);
13494
+ await this.backend.setItem(`${KEY_PREFIX2}${bundleHash}`, JSON.stringify(payload));
13495
+ if (options?.recoveryPassphrase) {
13496
+ const recoveryMetadata = {
13497
+ bundleHash,
13498
+ label: options?.label,
13499
+ createdAt: Date.now(),
13500
+ hardwareBacked: false,
13501
+ providerType: "webcrypto-aes-gcm"
13502
+ };
13503
+ const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
13504
+ await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
13505
+ }
13506
+ }
13507
+ async retrieveSecret(bundleHash, options) {
13508
+ if (options?.passphrase) {
13509
+ throw new exports.SecretStorageException(
13510
+ "WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
13511
+ );
13512
+ }
13513
+ const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
13514
+ if (!raw) {
13515
+ return null;
13516
+ }
13517
+ let payload;
13518
+ try {
13519
+ payload = JSON.parse(raw);
13520
+ } catch {
13521
+ throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
13522
+ }
13523
+ const passphrase = await this.unlock();
13524
+ try {
13525
+ const decryptedBytes = await openEnvelope(payload, passphrase);
13526
+ try {
13527
+ return textDecoder2.decode(decryptedBytes);
13528
+ } finally {
13529
+ zeroizeBytes(decryptedBytes);
13530
+ }
13531
+ } catch (err) {
13532
+ if (err instanceof exports.SecretStorageException) {
13533
+ throw err;
13534
+ }
13535
+ const msg = err instanceof Error ? err.message : String(err);
13536
+ throw exports.SecretStorageException.decryptionFailed(msg);
13537
+ }
13538
+ }
13539
+ async withSecret(bundleHash, fn, options) {
13540
+ if (options?.passphrase) {
13541
+ throw new exports.SecretStorageException(
13542
+ "WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
13543
+ );
13544
+ }
13545
+ const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
13546
+ if (!raw) {
13547
+ throw exports.SecretStorageException.notFound(bundleHash);
13548
+ }
13549
+ let payload;
13550
+ try {
13551
+ payload = JSON.parse(raw);
13552
+ } catch {
13553
+ throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
13554
+ }
13555
+ const passphrase = await this.unlock();
13556
+ try {
13557
+ const decryptedBytes = await openEnvelope(payload, passphrase);
13558
+ return await withSecureBytes(decryptedBytes, async (bytes) => {
13559
+ const secretString = textDecoder2.decode(bytes);
13560
+ return await fn(secretString);
13561
+ });
13562
+ } catch (err) {
13563
+ if (err instanceof exports.SecretStorageException) {
13564
+ throw err;
13565
+ }
13566
+ const msg = err instanceof Error ? err.message : String(err);
13567
+ throw exports.SecretStorageException.decryptionFailed(msg);
13568
+ }
13569
+ }
13570
+ async deleteSecret(bundleHash) {
13571
+ const key = `${KEY_PREFIX2}${bundleHash}`;
13572
+ const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
13573
+ const result = await this.backend.removeItem(key);
13574
+ await this.backend.removeItem(recoveryKey);
13575
+ return result !== false;
13576
+ }
13577
+ async hasSecret(bundleHash) {
13578
+ const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
13579
+ return raw !== null;
13580
+ }
13581
+ async listSecrets() {
13582
+ const keys = await this.backend.keys();
13583
+ const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX2) && !k.startsWith(RECOVERY_KEY_PREFIX));
13584
+ const results = [];
13585
+ for (const key of matchingKeys) {
13586
+ const raw = await this.backend.getItem(key);
13587
+ if (raw) {
13588
+ try {
13589
+ const payload = JSON.parse(raw);
13590
+ if (payload.metadata) {
13591
+ results.push(payload.metadata);
13592
+ }
13593
+ } catch {
13594
+ }
13595
+ }
13596
+ }
13597
+ return results;
13598
+ }
13599
+ /**
13600
+ * Recover a secret using its recovery envelope and re-enroll it under a fresh WebAuthn PRF credential
13601
+ */
13602
+ async recoverSecret(bundleHash, recoveryPassphrase, options) {
13603
+ if (!bundleHash) {
13604
+ throw new exports.SecretStorageException("Bundle hash cannot be empty");
13605
+ }
13606
+ if (!recoveryPassphrase) {
13607
+ throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
13608
+ }
13609
+ const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
13610
+ if (!raw) {
13611
+ throw exports.SecretStorageException.notFound(bundleHash);
13612
+ }
13613
+ let payload;
13614
+ try {
13615
+ payload = JSON.parse(raw);
13616
+ } catch {
13617
+ throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
13618
+ }
13619
+ let decryptedBytes;
13620
+ try {
13621
+ decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
13622
+ } catch (err) {
13623
+ if (err instanceof exports.SecretStorageException) {
13624
+ throw err;
13625
+ }
13626
+ const msg = err instanceof Error ? err.message : String(err);
13627
+ throw exports.SecretStorageException.decryptionFailed(msg);
13628
+ }
13629
+ let secretStr;
13630
+ try {
13631
+ secretStr = textDecoder2.decode(decryptedBytes);
13632
+ } finally {
13633
+ zeroizeBytes(decryptedBytes);
13634
+ }
13635
+ await this.storeSecret(bundleHash, secretStr, {
13636
+ ...options,
13637
+ recoveryPassphrase
13638
+ });
13639
+ }
13640
+ };
13641
+
13642
+ // src/storage/NonExtractableKeySecretStorageProvider.ts
13643
+ init_SecretStorageException();
13644
+ var KEY_PREFIX3 = SECRET_KEY_PREFIX;
13645
+ var GCM_IV_LENGTH3 = 12;
13646
+ var textEncoder4 = new TextEncoder();
13647
+ var textDecoder3 = new TextDecoder();
13648
+ var MemoryKeyStore = class {
13649
+ keys = /* @__PURE__ */ new Map();
13650
+ async get(name) {
13651
+ return this.keys.get(name);
13652
+ }
13653
+ async put(name, key) {
13654
+ this.keys.set(name, key);
13655
+ }
13656
+ async delete(name) {
13657
+ return this.keys.delete(name);
13658
+ }
13659
+ };
13660
+ var IndexedDbKeyStore = class {
13661
+ dbName;
13662
+ storeName = "keys";
13663
+ constructor(dbName = "knishio-secret-storage") {
13664
+ this.dbName = dbName;
13665
+ }
13666
+ // Executor form intentionally retained for browser runtime compatibility with ES2022 / browsers without Promise.withResolvers polyfill
13667
+ async getDb() {
13668
+ if (typeof globalThis.indexedDB === "undefined") {
13669
+ throw exports.SecretStorageException.unavailable(
13670
+ "webcrypto-nonextractable",
13671
+ "IndexedDB is not available"
13672
+ );
13673
+ }
13674
+ return new Promise((resolve, reject) => {
13675
+ const request = globalThis.indexedDB.open(this.dbName, 1);
13676
+ request.onupgradeneeded = () => {
13677
+ const db = request.result;
13678
+ if (!db.objectStoreNames.contains(this.storeName)) {
13679
+ db.createObjectStore(this.storeName);
13680
+ }
13681
+ };
13682
+ request.onsuccess = () => resolve(request.result);
13683
+ request.onerror = () => reject(request.error);
13684
+ });
13685
+ }
13686
+ async get(name) {
13687
+ const db = await this.getDb();
13688
+ return new Promise((resolve, reject) => {
13689
+ const tx = db.transaction(this.storeName, "readonly");
13690
+ const store = tx.objectStore(this.storeName);
13691
+ const request = store.get(name);
13692
+ request.onsuccess = () => resolve(request.result);
13693
+ request.onerror = () => reject(request.error);
13694
+ });
13695
+ }
13696
+ async put(name, key) {
13697
+ const db = await this.getDb();
13698
+ return new Promise((resolve, reject) => {
13699
+ const tx = db.transaction(this.storeName, "readwrite");
13700
+ const store = tx.objectStore(this.storeName);
13701
+ const request = store.put(key, name);
13702
+ request.onsuccess = () => resolve();
13703
+ request.onerror = () => reject(request.error);
13704
+ });
13705
+ }
13706
+ async delete(name) {
13707
+ const db = await this.getDb();
13708
+ return new Promise((resolve, reject) => {
13709
+ const tx = db.transaction(this.storeName, "readwrite");
13710
+ const store = tx.objectStore(this.storeName);
13711
+ const request = store.delete(name);
13712
+ request.onsuccess = () => resolve(true);
13713
+ request.onerror = () => reject(request.error);
13714
+ });
13715
+ }
13716
+ };
13717
+ var NonExtractableKeySecretStorageProvider = class {
13718
+ providerType = "webcrypto-nonextractable";
13719
+ backend;
13720
+ keyStore;
13721
+ alias;
13722
+ cachedPassphrase;
13723
+ constructor(options) {
13724
+ this.backend = options.backend;
13725
+ this.keyStore = options.keyStore ?? new IndexedDbKeyStore();
13726
+ this.alias = options.alias ?? "default";
13727
+ }
13728
+ get recordKey() {
13729
+ return `knishio:kek:webcrypto-nonextractable:${this.alias}`;
13730
+ }
13731
+ get kekStoreKey() {
13732
+ return `knishio:kek:${this.alias}`;
13733
+ }
13734
+ isHardwareBacked() {
13735
+ return false;
13736
+ }
13737
+ async isAvailable() {
13738
+ return typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
13739
+ }
13740
+ /**
13741
+ * Unlock or initialize the device passphrase using the non-extractable KEK
13742
+ */
13743
+ async unlock() {
13744
+ if (this.cachedPassphrase) {
13745
+ return this.cachedPassphrase;
13746
+ }
13747
+ if (!await this.isAvailable()) {
13748
+ throw exports.SecretStorageException.unavailable(
13749
+ this.providerType,
13750
+ "WebCrypto API is not available"
13751
+ );
13752
+ }
13753
+ const rawRecord = await this.backend.getItem(this.recordKey);
13754
+ if (!rawRecord) {
13755
+ let kek2 = await this.keyStore.get(this.kekStoreKey);
13756
+ if (!kek2) {
13757
+ kek2 = await globalThis.crypto.subtle.generateKey(
13758
+ { name: "AES-GCM", length: 256 },
13759
+ false,
13760
+ ["encrypt", "decrypt"]
13761
+ );
13762
+ await this.keyStore.put(this.kekStoreKey, kek2);
13763
+ }
13764
+ const devicePassphraseBytes = new Uint8Array(32);
13765
+ globalThis.crypto.getRandomValues(devicePassphraseBytes);
13766
+ const devicePassphrase = uint8ArrayToBase64(devicePassphraseBytes);
13767
+ const iv2 = new Uint8Array(GCM_IV_LENGTH3);
13768
+ globalThis.crypto.getRandomValues(iv2);
13769
+ const passphraseBytes = textEncoder4.encode(devicePassphrase);
13770
+ try {
13771
+ const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
13772
+ {
13773
+ name: "AES-GCM",
13774
+ iv: iv2
13775
+ },
13776
+ kek2,
13777
+ passphraseBytes
13778
+ );
13779
+ const record2 = {
13780
+ version: 1,
13781
+ iv: uint8ArrayToBase64(iv2),
13782
+ ciphertext: uint8ArrayToBase64(new Uint8Array(encryptedBuffer))
13783
+ };
13784
+ await this.backend.setItem(this.recordKey, JSON.stringify(record2));
13785
+ this.cachedPassphrase = devicePassphrase;
13786
+ return devicePassphrase;
13787
+ } finally {
13788
+ zeroizeBytes(passphraseBytes);
13789
+ zeroizeBytes(devicePassphraseBytes);
13790
+ }
13791
+ }
13792
+ let record;
13793
+ try {
13794
+ record = JSON.parse(rawRecord);
13795
+ } catch {
13796
+ throw exports.SecretStorageException.decryptionFailed("Corrupted key record format");
13797
+ }
13798
+ const kek = await this.keyStore.get(this.kekStoreKey);
13799
+ if (!kek) {
13800
+ throw exports.SecretStorageException.unavailable(
13801
+ this.providerType,
13802
+ `no non-extractable key found for alias '${this.alias}'`
13803
+ );
13804
+ }
13805
+ const iv = base64ToUint8Array(record.iv);
13806
+ const ciphertext = base64ToUint8Array(record.ciphertext);
13807
+ try {
13808
+ const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
13809
+ {
13810
+ name: "AES-GCM",
13811
+ iv
13812
+ },
13813
+ kek,
13814
+ ciphertext
13815
+ );
13816
+ const decryptedBytes = new Uint8Array(decryptedBuffer);
13817
+ try {
13818
+ this.cachedPassphrase = textDecoder3.decode(decryptedBytes);
13819
+ return this.cachedPassphrase;
13820
+ } finally {
13821
+ zeroizeBytes(decryptedBytes);
13822
+ }
13823
+ } catch {
13824
+ throw exports.SecretStorageException.decryptionFailed(
13825
+ "wrapped device passphrase failed authentication under non-extractable key"
13826
+ );
13827
+ }
13828
+ }
13829
+ /**
13830
+ * Lock the provider by clearing cached passphrase material
13831
+ */
13832
+ lock() {
13833
+ this.cachedPassphrase = void 0;
13834
+ }
13835
+ /**
13836
+ * Unenroll the non-extractable key, removing the stored wrapped record and KEK
13837
+ */
13838
+ async unenroll() {
13839
+ this.lock();
13840
+ await this.backend.removeItem(this.recordKey);
13841
+ await this.keyStore.delete(this.kekStoreKey);
13842
+ }
13843
+ async storeSecret(bundleHash, secret, options) {
13844
+ if (!bundleHash) {
13845
+ throw new exports.SecretStorageException("Bundle hash cannot be empty");
13846
+ }
13847
+ if (!secret) {
13848
+ throw new exports.SecretStorageException("Secret cannot be empty");
13849
+ }
13850
+ if (options?.passphrase) {
13851
+ throw new exports.SecretStorageException(
13852
+ "NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
13853
+ );
13854
+ }
13855
+ if (!options?.recoveryPassphrase && !options?.allowUnrecoverable) {
13856
+ throw exports.SecretStorageException.validationError(
13857
+ "Recovery passphrase required for non-exportable hardware key unless allowUnrecoverable is true"
13858
+ );
13859
+ }
13860
+ const passphrase = await this.unlock();
13861
+ const metadata = {
13862
+ bundleHash,
13863
+ label: options?.label,
13864
+ createdAt: Date.now(),
13865
+ hardwareBacked: false,
13866
+ providerType: this.providerType
13867
+ };
13868
+ const payload = await sealEnvelope(secret, passphrase, metadata);
13869
+ await this.backend.setItem(`${KEY_PREFIX3}${bundleHash}`, JSON.stringify(payload));
13870
+ if (options?.recoveryPassphrase) {
13871
+ const recoveryMetadata = {
13872
+ bundleHash,
13873
+ label: options?.label,
13874
+ createdAt: Date.now(),
13875
+ hardwareBacked: false,
13876
+ providerType: "webcrypto-aes-gcm"
13877
+ };
13878
+ const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
13879
+ await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
13880
+ }
13881
+ }
13882
+ async retrieveSecret(bundleHash, options) {
13883
+ if (options?.passphrase) {
13884
+ throw new exports.SecretStorageException(
13885
+ "NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
13886
+ );
13887
+ }
13888
+ const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
13889
+ if (!raw) {
13890
+ return null;
13891
+ }
13892
+ let payload;
13893
+ try {
13894
+ payload = JSON.parse(raw);
13895
+ } catch {
13896
+ throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
13897
+ }
13898
+ const passphrase = await this.unlock();
13899
+ try {
13900
+ const decryptedBytes = await openEnvelope(payload, passphrase);
13901
+ try {
13902
+ return textDecoder3.decode(decryptedBytes);
13903
+ } finally {
13904
+ zeroizeBytes(decryptedBytes);
13905
+ }
13906
+ } catch (err) {
13907
+ if (err instanceof exports.SecretStorageException) {
13908
+ throw err;
13909
+ }
13910
+ const msg = err instanceof Error ? err.message : String(err);
13911
+ throw exports.SecretStorageException.decryptionFailed(msg);
13912
+ }
13913
+ }
13914
+ async withSecret(bundleHash, fn, options) {
13915
+ if (options?.passphrase) {
13916
+ throw new exports.SecretStorageException(
13917
+ "NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
13918
+ );
13919
+ }
13920
+ const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
13921
+ if (!raw) {
13922
+ throw exports.SecretStorageException.notFound(bundleHash);
13923
+ }
13924
+ let payload;
13925
+ try {
13926
+ payload = JSON.parse(raw);
13927
+ } catch {
13928
+ throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
13929
+ }
13930
+ const passphrase = await this.unlock();
13931
+ try {
13932
+ const decryptedBytes = await openEnvelope(payload, passphrase);
13933
+ return await withSecureBytes(decryptedBytes, async (bytes) => {
13934
+ const secretString = textDecoder3.decode(bytes);
13935
+ return await fn(secretString);
13936
+ });
13937
+ } catch (err) {
13938
+ if (err instanceof exports.SecretStorageException) {
13939
+ throw err;
13940
+ }
13941
+ const msg = err instanceof Error ? err.message : String(err);
13942
+ throw exports.SecretStorageException.decryptionFailed(msg);
13943
+ }
13944
+ }
13945
+ async deleteSecret(bundleHash) {
13946
+ const key = `${KEY_PREFIX3}${bundleHash}`;
13947
+ const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
13948
+ const result = await this.backend.removeItem(key);
13949
+ await this.backend.removeItem(recoveryKey);
13950
+ return result !== false;
13951
+ }
13952
+ async hasSecret(bundleHash) {
13953
+ const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
13954
+ return raw !== null;
13955
+ }
13956
+ async listSecrets() {
13957
+ const keys = await this.backend.keys();
13958
+ const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX3) && !k.startsWith(RECOVERY_KEY_PREFIX));
13959
+ const results = [];
13960
+ for (const key of matchingKeys) {
13961
+ const raw = await this.backend.getItem(key);
13962
+ if (raw) {
13963
+ try {
13964
+ const payload = JSON.parse(raw);
13965
+ if (payload.metadata) {
13966
+ results.push(payload.metadata);
13967
+ }
13968
+ } catch {
13969
+ }
13970
+ }
13971
+ }
13972
+ return results;
13973
+ }
13974
+ /**
13975
+ * Recover a secret using its recovery envelope and re-enroll it under a fresh non-extractable KEK
13976
+ */
13977
+ async recoverSecret(bundleHash, recoveryPassphrase, options) {
13978
+ if (!bundleHash) {
13979
+ throw new exports.SecretStorageException("Bundle hash cannot be empty");
13980
+ }
13981
+ if (!recoveryPassphrase) {
13982
+ throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
13983
+ }
13984
+ const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
13985
+ if (!raw) {
13986
+ throw exports.SecretStorageException.notFound(bundleHash);
13987
+ }
13988
+ let payload;
13989
+ try {
13990
+ payload = JSON.parse(raw);
13991
+ } catch {
13992
+ throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
13993
+ }
13994
+ let decryptedBytes;
13995
+ try {
13996
+ decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
13997
+ } catch (err) {
13998
+ if (err instanceof exports.SecretStorageException) {
13999
+ throw err;
14000
+ }
14001
+ const msg = err instanceof Error ? err.message : String(err);
14002
+ throw exports.SecretStorageException.decryptionFailed(msg);
14003
+ }
14004
+ let secretStr;
14005
+ try {
14006
+ secretStr = textDecoder3.decode(decryptedBytes);
14007
+ } finally {
14008
+ zeroizeBytes(decryptedBytes);
14009
+ }
14010
+ await this.storeSecret(bundleHash, secretStr, {
14011
+ ...options,
14012
+ recoveryPassphrase
14013
+ });
14014
+ }
12850
14015
  };
12851
14016
 
12852
14017
  // src/storage/index.ts
@@ -12857,15 +14022,14 @@ function createDefaultSecretStorage(options = {}) {
12857
14022
  if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined") {
12858
14023
  return new WebCryptoSecretStorageProvider({
12859
14024
  backend: options.backend,
12860
- defaultPassphrase: options.defaultPassphrase,
12861
- hardwareBacked: options.hardwareBacked
14025
+ defaultPassphrase: options.defaultPassphrase
12862
14026
  });
12863
14027
  }
12864
14028
  return new MemorySecretStorageProvider();
12865
14029
  }
12866
14030
 
12867
14031
  // src/index.ts
12868
- var SDK_VERSION = "1.0.0";
14032
+ var SDK_VERSION = "1.1.0";
12869
14033
  var SDK_NAME = "KnishIO-Client-TS";
12870
14034
  var COMPATIBLE_SERVER_VERSIONS = [4, 5];
12871
14035
  var SDK_INFO = {
@@ -12963,9 +14127,12 @@ exports.CRYPTO_CONSTANTS = CRYPTO_CONSTANTS;
12963
14127
  exports.CheckMolecule = CheckMolecule;
12964
14128
  exports.DevUtils = DevUtils;
12965
14129
  exports.EXTENDED_COMPATIBILITY_TEST_VECTORS = EXTENDED_COMPATIBILITY_TEST_VECTORS;
14130
+ exports.FileStorageBackend = FileStorageBackend;
12966
14131
  exports.GraphQLClient = GraphQLClient;
14132
+ exports.IndexedDbKeyStore = IndexedDbKeyStore;
12967
14133
  exports.KnishIO = KnishIO;
12968
14134
  exports.KnishIOClient = KnishIOClient;
14135
+ exports.MemoryKeyStore = MemoryKeyStore;
12969
14136
  exports.MemorySecretStorageProvider = MemorySecretStorageProvider;
12970
14137
  exports.MemoryStorageBackend = MemoryStorageBackend;
12971
14138
  exports.Meta = Meta;
@@ -12980,6 +14147,7 @@ exports.MutationProposeMolecule = MutationProposeMolecule;
12980
14147
  exports.MutationRequestAuthorization = MutationRequestAuthorization;
12981
14148
  exports.MutationRequestTokens = MutationRequestTokens;
12982
14149
  exports.MutationTransferTokens = MutationTransferTokens;
14150
+ exports.NonExtractableKeySecretStorageProvider = NonExtractableKeySecretStorageProvider;
12983
14151
  exports.PolicyMeta = PolicyMeta;
12984
14152
  exports.Query = Query;
12985
14153
  exports.QueryAtom = QueryAtom;
@@ -12991,6 +14159,7 @@ exports.QueryMetaType = QueryMetaType;
12991
14159
  exports.QueryMetaTypeViaAtom = QueryMetaTypeViaAtom;
12992
14160
  exports.QueryWalletBundle = QueryWalletBundle;
12993
14161
  exports.QueryWalletList = QueryWalletList;
14162
+ exports.RECOVERY_KEY_PREFIX = RECOVERY_KEY_PREFIX;
12994
14163
  exports.ResponseAppendRequest = ResponseAppendRequest;
12995
14164
  exports.ResponseAtom = ResponseAtom;
12996
14165
  exports.ResponseBalance = ResponseBalance;
@@ -13011,9 +14180,12 @@ exports.ResponseWalletList = ResponseWalletList;
13011
14180
  exports.SDK_INFO = SDK_INFO;
13012
14181
  exports.SDK_NAME = SDK_NAME;
13013
14182
  exports.SDK_VERSION = SDK_VERSION;
14183
+ exports.SECRET_KEY_PREFIX = SECRET_KEY_PREFIX;
13014
14184
  exports.TokenUnit = TokenUnit;
13015
14185
  exports.Wallet = Wallet;
14186
+ exports.WebAuthnPrfSecretStorageProvider = WebAuthnPrfSecretStorageProvider;
13016
14187
  exports.WebCryptoSecretStorageProvider = WebCryptoSecretStorageProvider;
14188
+ exports.WebStorageBackend = WebStorageBackend;
13017
14189
  exports.base64ToHex = base64ToHex;
13018
14190
  exports.bufferToHexString = bufferToHexString;
13019
14191
  exports.capitalize = capitalize;
@@ -13052,9 +14224,11 @@ exports.isNumeric = isNumeric;
13052
14224
  exports.isPosition = isPosition2;
13053
14225
  exports.isWalletAddress = isWalletAddress2;
13054
14226
  exports.normalizeMolecularHash = normalizeMolecularHash;
14227
+ exports.openEnvelope = openEnvelope;
13055
14228
  exports.randomString = randomString;
13056
14229
  exports.runCompatibilityTests = runCompatibilityTests;
13057
14230
  exports.runExtendedCompatibilityTests = runExtendedCompatibilityTests;
14231
+ exports.sealEnvelope = sealEnvelope;
13058
14232
  exports.shake256 = shake256;
13059
14233
  exports.toCamelCase = toCamelCase;
13060
14234
  exports.toSnakeCase = toSnakeCase;