@wishknish/knishio-client-ts 1.0.0 → 1.2.0

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