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