mask-privacy 4.1.0 → 4.3.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
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var process2 = require('process');
4
- var path = require('path');
4
+ var path2 = require('path');
5
5
  var os = require('os');
6
6
  var cryptoNode = require('crypto');
7
7
  var fs = require('fs');
@@ -36,7 +36,7 @@ function _interopNamespace(e) {
36
36
  }
37
37
 
38
38
  var process2__namespace = /*#__PURE__*/_interopNamespace(process2);
39
- var path__namespace = /*#__PURE__*/_interopNamespace(path);
39
+ var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
40
40
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
41
41
  var cryptoNode__namespace = /*#__PURE__*/_interopNamespace(cryptoNode);
42
42
  var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
@@ -129,6 +129,11 @@ var init_config = __esm({
129
129
  get MASK_ENCRYPTION_KEY() {
130
130
  return process2__namespace.env.MASK_ENCRYPTION_KEY || null;
131
131
  },
132
+ // JSON map of keyId -> base64 key string for key rotation, e.g. {"v1":"...","v2":"..."}
133
+ // The last entry in the map is treated as the active (encryption) key.
134
+ get MASK_KEYRING() {
135
+ return process2__namespace.env.MASK_KEYRING || null;
136
+ },
132
137
  get MASK_MASTER_KEY() {
133
138
  return process2__namespace.env.MASK_MASTER_KEY || process2__namespace.env.MASK_ENCRYPTION_KEY || "";
134
139
  },
@@ -160,6 +165,9 @@ var init_config = __esm({
160
165
  get MASK_VAULT_CLEANUP_FREQUENCY() {
161
166
  return getEnvFloat("MASK_VAULT_CLEANUP_FREQUENCY", 0.01);
162
167
  },
168
+ get MASK_VAULT_MAX_MEMORY_KEYS() {
169
+ return getEnvInt("MASK_VAULT_MAX_MEMORY_KEYS", 1e5);
170
+ },
163
171
  // --- BACKEND CONNECTIONS ---
164
172
  get MASK_REDIS_URL() {
165
173
  return process2__namespace.env.MASK_REDIS_URL || "redis://localhost:6379/0";
@@ -199,7 +207,7 @@ var init_config = __esm({
199
207
  return process2__namespace.env.MASK_SCANNER_URL || "http://localhost:5001/analyze";
200
208
  },
201
209
  get MASK_MODEL_CACHE_DIR() {
202
- return process2__namespace.env.MASK_MODEL_CACHE_DIR || path__namespace.join(os__namespace.homedir(), ".cache", "mask");
210
+ return process2__namespace.env.MASK_MODEL_CACHE_DIR || path2__namespace.join(os__namespace.homedir(), ".cache", "mask");
203
211
  },
204
212
  // --- TELEMETRY & AUDIT ---
205
213
  get MASK_AUDIT_LOG_STRICT() {
@@ -224,6 +232,16 @@ var init_key_provider = __esm({
224
232
  "src/core/key_provider.ts"() {
225
233
  init_config();
226
234
  BaseKeyProvider = class {
235
+ /**
236
+ * Return a JSON keyring string (e.g. from KMS / Secrets Manager), or null
237
+ * to fall back to the MASK_KEYRING environment variable.
238
+ *
239
+ * Override in KMS-backed providers to source the full keyring from a
240
+ * secure external store, removing the need for MASK_KEYRING in env vars.
241
+ */
242
+ getKeyring() {
243
+ return null;
244
+ }
227
245
  };
228
246
  EnvKeyProvider = class extends BaseKeyProvider {
229
247
  async getEncryptionKey() {
@@ -239,6 +257,10 @@ var init_key_provider = __esm({
239
257
  let key = config.MASK_MASTER_KEY;
240
258
  return key || null;
241
259
  }
260
+ /** Return MASK_KEYRING from environment (default behaviour). */
261
+ async getKeyring() {
262
+ return config.MASK_KEYRING || null;
263
+ }
242
264
  };
243
265
  providerInstance = null;
244
266
  }
@@ -692,17 +714,34 @@ function looksLikeToken(value) {
692
714
  if (v7.includes("-") && v7.length >= 6) {
693
715
  const parts = v7.split("-");
694
716
  const tag = parts[parts.length - 1];
695
- if (tag.length === 4 && /^\d+$/.test(tag)) {
717
+ if (tag.length >= 3 && tag.length <= 10 && /^\d+$/.test(tag)) {
696
718
  return true;
697
719
  }
698
720
  }
699
721
  return false;
700
722
  }
723
+ function isUnambiguouslySafeToken(value) {
724
+ if (typeof value !== "string") return false;
725
+ const v7 = value.trim();
726
+ if (v7.startsWith("tkn-") && v7.includes("@")) {
727
+ const parts = v7.split("@");
728
+ if (parts.length === 2 && parts[0].length >= 12 && parts[1].includes(".")) {
729
+ return true;
730
+ }
731
+ }
732
+ if (/^\+[1-9]\d{0,3}-555-\d{7}$/.test(v7)) return true;
733
+ if (/^000\d{5}[A-Z]$/.test(v7)) return true;
734
+ if (/^[A-Z]{2}00[A-F0-9]{4,16}$/.test(v7)) return true;
735
+ if (/^<(PER|LOC|ORG):[^>]+>$/.test(v7)) return true;
736
+ if (v7.startsWith("[TKN-") && v7.endsWith("]")) return true;
737
+ if (/^[A-Z][a-zA-Z, ]+-[0-9]{3,10}$/.test(v7)) return true;
738
+ return false;
739
+ }
701
740
  var TOKEN_PATTERN;
702
741
  var init_fpe_utils = __esm({
703
742
  "src/core/fpe_utils.ts"() {
704
743
  TOKEN_PATTERN = new RegExp(
705
- "tkn-[a-f0-9]{8,64}@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}|\\+[1-9]\\d{0,3}-555-\\d{7}|\\d{3}-\\d{2}-\\d{4}|\\d{4}-\\d{4}-\\d{4}-\\d{4}|\\b\\d{9}\\b|\\b000\\d{5}[A-Z]\\b|[A-Z]{2}00[A-F0-9]{4,16}|<(?:PER|LOC|ORG):[^>]+>|\\b[A-Z][a-zA-Z, ]+-[0-9]{3,4}\\b|\\[TKN-[^\\]]+\\]",
744
+ "tkn-[a-f0-9]{8,64}@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}|\\+[1-9]\\d{0,3}-555-\\d{7}|\\d{3}-\\d{2}-\\d{4}|\\d{4}-\\d{4}-\\d{4}-\\d{4}|\\b\\d{9}\\b|\\b000\\d{5}[A-Z]\\b|[A-Z]{2}00[A-F0-9]{4,16}|<(?:PER|LOC|ORG):[^>]+>|\\b[A-Z][a-zA-Z, ]+-[0-9]{3,10}\\b|\\[TKN-[^\\]]+\\]",
706
745
  // Opaque
707
746
  "g"
708
747
  );
@@ -732,7 +771,7 @@ function resetMasterKey() {
732
771
  }
733
772
  async function _getAesKey() {
734
773
  const masterKey = await _getMasterKey();
735
- return cryptoNode__namespace.createHash("sha256").update(masterKey).digest();
774
+ return cryptoNode__namespace.createHmac("sha256", masterKey).update(config.MASK_TENANT_ID, "utf-8").digest();
736
775
  }
737
776
  async function _hmacHex(plaintext, n6 = 8) {
738
777
  const masterKey = await _getMasterKey();
@@ -740,61 +779,57 @@ async function _hmacHex(plaintext, n6 = 8) {
740
779
  return digest.slice(0, n6);
741
780
  }
742
781
  async function _getBijectiveTweak() {
743
- const masterKey = await _getMasterKey();
744
- let base = config.MASK_TENANT_ID;
745
782
  if (config.MASK_SALT_ROTATION !== "NONE") {
746
- const now = /* @__PURE__ */ new Date();
747
- if (config.MASK_SALT_ROTATION === "MONTHLY") {
748
- base += `-${now.getUTCFullYear()}-${now.getUTCMonth() + 1}`;
749
- } else if (config.MASK_SALT_ROTATION === "YEARLY") {
750
- base += `-${now.getUTCFullYear()}`;
751
- }
783
+ console.warn(
784
+ `[mask] MASK_SALT_ROTATION=${config.MASK_SALT_ROTATION} is deprecated and ignored. Time-based tweaks caused permanent data loss on month/year rollovers. Use MASK_KEYRING for key rotation instead.`
785
+ );
752
786
  }
753
- return cryptoNode__namespace.createHmac("sha256", masterKey).update(base, "utf-8").digest();
787
+ const masterKey = await _getMasterKey();
788
+ return cryptoNode__namespace.createHmac("sha256", masterKey).update(config.MASK_TENANT_ID, "utf-8").digest();
754
789
  }
755
790
  async function _encryptBijectiveFF1(text) {
756
791
  const canonical = text.toLowerCase().trim();
757
792
  const hash = cryptoNode__namespace.createHash("sha256").update(canonical, "utf-8").digest();
758
- const inputInt = hash.readBigUInt64BE(0);
759
- const inputStr = inputInt.toString().padStart(20, "0");
793
+ const hash128 = hash.subarray(0, 16);
794
+ const inputInt = hash128.readBigUInt64BE(0) << 64n | hash128.readBigUInt64BE(8);
795
+ const inputStr = inputInt.toString().padStart(40, "0");
760
796
  const aesKey = await _getAesKey();
761
797
  const tweak = await _getBijectiveTweak();
762
798
  const engine = new FF1(aesKey, tweak, 10);
763
799
  const cipherStr = engine.encrypt(inputStr);
764
- return BigInt(cipherStr) % 2n ** 64n;
800
+ return BigInt(cipherStr);
765
801
  }
766
802
  function _renderBijectivePerson(bits) {
767
803
  const firstIdx = Number(bits & 0x7FFn);
768
804
  const connIdx = Number(bits >> 11n & 0x3Fn);
769
805
  const rootIdx = Number(bits >> 17n & 0xFFFn);
770
806
  const suffixIdx = Number(bits >> 29n & 0x1FFn);
771
- const tag = Number(bits >> 38n & 0x3FFFn);
772
- const formatIdx = Number(bits >> 52n & 0xFn);
807
+ const tag = bits >> 38n & 0xFFFFFFFFFFn;
808
+ const formatIdx = Number(bits >> 78n & 0xFn);
773
809
  const first = FIRST_NAMES[firstIdx % FIRST_NAMES.length];
774
810
  const conn = CONNECTORS[connIdx % CONNECTORS.length];
775
811
  const root = SURNAME_ROOTS[rootIdx % SURNAME_ROOTS.length];
776
812
  const suffix = SURNAME_SUFFIXES[suffixIdx % SURNAME_SUFFIXES.length];
777
813
  const surname = `${root}${suffix}`;
778
- const numeric = tag % 1e4;
779
- const paddedNumeric = numeric.toString().padStart(4, "0");
814
+ const numeric = Number(tag % 10000000000n);
815
+ const paddedNumeric = numeric.toString().padStart(10, "0");
780
816
  if (formatIdx === 0) return `${first} ${conn} ${surname}-${paddedNumeric}`;
781
817
  if (formatIdx === 1) return `${surname}, ${first}-${paddedNumeric}`;
782
818
  if (formatIdx === 2) return `${first[0]}. ${surname}-${paddedNumeric}`;
783
- if (formatIdx === 3) return `${first} ${surname}-${paddedNumeric}`;
784
819
  return `${first} ${surname}-${paddedNumeric}`;
785
820
  }
786
821
  function _renderBijectiveLocation(bits) {
787
822
  const s1 = Number(bits & 0x3FFn);
788
823
  const s22 = Number(bits >> 10n & 0x3FFn);
789
824
  const s32 = Number(bits >> 20n & 0x3FFn);
790
- const tag = Number(bits >> 30n & 0xFFFn);
825
+ const tag = bits >> 30n & 0xFFFFFFFFFFn;
791
826
  const city = `${SYLLABLES[s1 % 1e3]}${SYLLABLES[s22 % 1e3].toLowerCase()}${SYLLABLES[s32 % 1e3].toLowerCase()}`;
792
- return `${city}-${tag.toString().padStart(3, "0")}`;
827
+ return `${city}-${Number(tag % 10000000000n).toString().padStart(10, "0")}`;
793
828
  }
794
- function _computeLuhnDigit(partialNum) {
795
- const digits = partialNum.split("").map(Number);
829
+ function _getLuhnSum(numStr) {
830
+ const digits = numStr.split("").map(Number);
796
831
  let sum = 0;
797
- let shouldDouble = true;
832
+ let shouldDouble = false;
798
833
  for (let i6 = digits.length - 1; i6 >= 0; i6--) {
799
834
  let digit = digits[i6];
800
835
  if (shouldDouble) {
@@ -804,7 +839,7 @@ function _computeLuhnDigit(partialNum) {
804
839
  sum += digit;
805
840
  shouldDouble = !shouldDouble;
806
841
  }
807
- return ((10 - sum % 10) % 10).toString();
842
+ return sum;
808
843
  }
809
844
  function _computeEsIdCheck(num) {
810
845
  return "TRWAGMYFPDXBNJZSQVHLCKE"[num % 23];
@@ -851,14 +886,15 @@ async function generateDPToken(rawText, entityType = "UNKNOWN") {
851
886
  if (type === "CREDIT_CARD" || type === "CREDIT_CARD_NUMBER") {
852
887
  const digits = _stripCcSeparators(text);
853
888
  if (digits.length === 16) {
854
- const bin6 = digits.slice(0, 6);
855
- const last4 = digits.slice(12, 16);
856
- const middle6 = digits.slice(6, 12);
889
+ const prefix6 = digits.slice(0, 6);
890
+ const suffix4 = digits.slice(12, 16);
891
+ const middle5 = digits.slice(6, 11);
857
892
  const engine = new FF1(await _getAesKey(), Buffer.from("CREDIT_CARD"), 10);
858
- const encMiddle = engine.encrypt(middle6);
859
- const base15 = bin6 + encMiddle + last4.slice(0, 3);
860
- const checkDig = _computeLuhnDigit(base15);
861
- const full = bin6 + encMiddle + last4.slice(0, 3) + checkDig;
893
+ const encMiddle5 = engine.encrypt(middle5);
894
+ const draft = prefix6 + encMiddle5 + "0" + suffix4;
895
+ const sum = _getLuhnSum(draft);
896
+ const correction = (10 - sum % 10) % 10;
897
+ const full = prefix6 + encMiddle5 + correction.toString() + suffix4;
862
898
  return `${full.slice(0, 4)}-${full.slice(4, 8)}-${full.slice(8, 12)}-${full.slice(12, 16)}`;
863
899
  } else {
864
900
  const fallbackDigits = digits.padEnd(16, "0").slice(0, 16);
@@ -3349,24 +3385,28 @@ function getCryptoEngine() {
3349
3385
  async function getCryptoEngineAsync() {
3350
3386
  return await CryptoEngine.getInstanceAsync();
3351
3387
  }
3352
- var GCM_IV_BYTES, GCM_AUTH_TAG_BYTES, GCM_ALGORITHM, AES_GCM_PREFIX, _CryptoEngine, CryptoEngine;
3388
+ var AES_KEY_BYTES, GCM_IV_BYTES, GCM_AUTH_TAG_BYTES, GCM_ALGORITHM, AES_V2_PREFIX, AES_GCM_PREFIX, AES_GCM_LEGACY_PREFIX, _CryptoEngine, CryptoEngine;
3353
3389
  var init_crypto = __esm({
3354
3390
  "src/core/crypto.ts"() {
3355
3391
  init_config();
3356
3392
  init_key_provider();
3357
3393
  init_exceptions();
3394
+ AES_KEY_BYTES = 32;
3358
3395
  GCM_IV_BYTES = 12;
3359
3396
  GCM_AUTH_TAG_BYTES = 16;
3360
3397
  GCM_ALGORITHM = "aes-256-gcm";
3361
- AES_GCM_PREFIX = "aes:";
3398
+ AES_V2_PREFIX = "aes:v2:";
3399
+ AES_GCM_PREFIX = "aes:v1:";
3400
+ AES_GCM_LEGACY_PREFIX = "aes:";
3362
3401
  _CryptoEngine = class _CryptoEngine {
3363
3402
  constructor() {
3364
- this._aesKey = null;
3403
+ this._keyring = /* @__PURE__ */ new Map();
3404
+ this._activeKeyId = "default";
3365
3405
  this._indexSecret = null;
3366
3406
  }
3367
- /**
3407
+ /**
3368
3408
  * Return the singleton instance, initialising it if necessary.
3369
- * This is asynchronous because key providers (KMS, etc.) might be async.
3409
+ * Async because Argon2id key derivation is async.
3370
3410
  */
3371
3411
  static async getInstanceAsync() {
3372
3412
  if (this._instance === null) {
@@ -3382,11 +3422,11 @@ var init_crypto = __esm({
3382
3422
  }
3383
3423
  return this._instance;
3384
3424
  }
3385
- /** Clear the singleton instance to force re-initialization (useful for key rotation). */
3425
+ /** Clear the singleton (useful for key rotation / tests). */
3386
3426
  static reset() {
3387
3427
  this._instance = null;
3388
3428
  }
3389
- async _init() {
3429
+ async _deriveAesKey(rawKey, keyId) {
3390
3430
  let argon2;
3391
3431
  try {
3392
3432
  argon2 = __require("argon2");
@@ -3395,38 +3435,72 @@ var init_crypto = __esm({
3395
3435
  "The 'argon2' package is required for Mask SDK cryptographic operations. Install with: npm install argon2"
3396
3436
  );
3397
3437
  }
3398
- const provider = getKeyProvider();
3399
- const keyFromProvider = await provider.getEncryptionKey();
3400
- let key;
3401
- if (!keyFromProvider) {
3402
- if (config.MASK_DEV_MODE) {
3403
- key = cryptoNode__namespace.randomBytes(32).toString("base64");
3404
- process.env.MASK_ENCRYPTION_KEY = key;
3405
- console.warn(
3406
- "MASK_DEV_MODE is enabled. Using a generated throwaway key. DO NOT USE THIS IN PRODUCTION \u2014 tokens will be lost on restart."
3407
- );
3408
- } else {
3409
- throw new Error(
3410
- "MASK_ENCRYPTION_KEY is not set. Set MASK_ENCRYPTION_KEY to a valid encryption key, or set MASK_DEV_MODE=true to use an ephemeral throwaway key."
3411
- );
3412
- }
3413
- } else {
3414
- key = keyFromProvider;
3415
- }
3416
- const kdfSaltStr = config.MASK_KDF_SALT + "-" + config.MASK_TENANT_ID;
3438
+ const kdfSaltStr = config.MASK_KDF_SALT + "-" + config.MASK_TENANT_ID + "-" + keyId;
3417
3439
  const kdfSaltBytes = cryptoNode__namespace.createHash("sha256").update(kdfSaltStr).digest().subarray(0, 16);
3418
- this._aesKey = await argon2.hash(key, {
3440
+ return await argon2.hash(rawKey, {
3419
3441
  type: argon2.argon2id,
3420
3442
  memoryCost: 19456,
3421
- // 19 MiB
3422
3443
  timeCost: 2,
3423
3444
  parallelism: 1,
3424
- hashLength: 32,
3445
+ hashLength: AES_KEY_BYTES,
3425
3446
  salt: kdfSaltBytes,
3426
3447
  raw: true
3427
- // return a Buffer, not a hash string
3428
3448
  });
3429
- const masterKey = await provider.getMasterKey() || key;
3449
+ }
3450
+ async _init() {
3451
+ let argon2;
3452
+ try {
3453
+ argon2 = __require("argon2");
3454
+ } catch (e6) {
3455
+ throw new Error("The 'argon2' package is required. Install with: npm install argon2");
3456
+ }
3457
+ const provider = getKeyProvider();
3458
+ const rawKeys = /* @__PURE__ */ new Map();
3459
+ let activeKeyId = "default";
3460
+ const keyringJson = await provider.getKeyring();
3461
+ if (keyringJson) {
3462
+ let parsed;
3463
+ try {
3464
+ parsed = JSON.parse(keyringJson);
3465
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3466
+ throw new Error("MASK_KEYRING must be a non-empty JSON object.");
3467
+ }
3468
+ } catch (e6) {
3469
+ throw new Error(`Invalid MASK_KEYRING format: ${e6}`);
3470
+ }
3471
+ const entries = Object.entries(parsed);
3472
+ if (entries.length === 0) throw new Error("MASK_KEYRING must contain at least one key.");
3473
+ for (const [kid, k6] of entries) rawKeys.set(kid, k6);
3474
+ activeKeyId = entries[entries.length - 1][0];
3475
+ } else {
3476
+ const keyFromProvider = await provider.getEncryptionKey();
3477
+ let key;
3478
+ if (!keyFromProvider) {
3479
+ if (config.MASK_DEV_MODE) {
3480
+ key = cryptoNode__namespace.randomBytes(32).toString("base64");
3481
+ process.env.MASK_ENCRYPTION_KEY = key;
3482
+ console.warn(
3483
+ "MASK_DEV_MODE is enabled. Using a generated throwaway key. DO NOT USE THIS IN PRODUCTION \u2014 tokens will be lost on restart."
3484
+ );
3485
+ } else {
3486
+ throw new Error(
3487
+ "MASK_ENCRYPTION_KEY or MASK_KEYRING is not set. Set one of these, or set MASK_DEV_MODE=true for ephemeral use."
3488
+ );
3489
+ }
3490
+ } else {
3491
+ key = keyFromProvider;
3492
+ }
3493
+ rawKeys.set("default", key);
3494
+ activeKeyId = "default";
3495
+ }
3496
+ this._keyring = /* @__PURE__ */ new Map();
3497
+ for (const [kid, rawKey] of rawKeys) {
3498
+ this._keyring.set(kid, await this._deriveAesKey(rawKey, kid));
3499
+ }
3500
+ this._activeKeyId = activeKeyId;
3501
+ const rawKeysArr = Array.from(rawKeys.values());
3502
+ const lastRawKey = rawKeysArr[rawKeysArr.length - 1];
3503
+ const masterKey = await provider.getMasterKey() || lastRawKey;
3430
3504
  const indexSaltStr = config.MASK_BLIND_INDEX_SALT + "-" + config.MASK_TENANT_ID;
3431
3505
  const indexSaltBytes = cryptoNode__namespace.createHash("sha256").update(indexSaltStr).digest().subarray(0, 16);
3432
3506
  this._indexSecret = await argon2.hash(masterKey, {
@@ -3434,7 +3508,7 @@ var init_crypto = __esm({
3434
3508
  memoryCost: 19456,
3435
3509
  timeCost: 2,
3436
3510
  parallelism: 1,
3437
- hashLength: 32,
3511
+ hashLength: AES_KEY_BYTES,
3438
3512
  salt: indexSaltBytes,
3439
3513
  raw: true
3440
3514
  });
@@ -3446,36 +3520,56 @@ var init_crypto = __esm({
3446
3520
  }
3447
3521
  return this._indexSecret;
3448
3522
  }
3523
+ /** Encrypt plaintext using the active keyring key.
3524
+ * Envelope format: aes:v2:{keyId}:{base64(iv+authTag+ciphertext)}
3525
+ */
3449
3526
  encrypt(plaintext) {
3450
- if (!this._aesKey) {
3451
- throw new Error("CryptoEngine not initialised. AES key missing.");
3527
+ const aesKey = this._keyring.get(this._activeKeyId);
3528
+ if (!aesKey) {
3529
+ throw new Error(`CryptoEngine: active key ID '${this._activeKeyId}' not found in keyring.`);
3452
3530
  }
3453
3531
  const iv = cryptoNode__namespace.randomBytes(GCM_IV_BYTES);
3454
- const cipher = cryptoNode__namespace.createCipheriv(GCM_ALGORITHM, this._aesKey, iv);
3455
- const encrypted = Buffer.concat([
3456
- cipher.update(plaintext, "utf8"),
3457
- cipher.final()
3458
- ]);
3532
+ const cipher = cryptoNode__namespace.createCipheriv(GCM_ALGORITHM, aesKey, iv);
3533
+ const plaintextBuf = Buffer.from(plaintext, "utf8");
3534
+ const encrypted = Buffer.concat([cipher.update(plaintextBuf), cipher.final()]);
3459
3535
  const authTag = cipher.getAuthTag();
3536
+ plaintextBuf.fill(0);
3460
3537
  const combined = Buffer.concat([iv, authTag, encrypted]);
3461
- return AES_GCM_PREFIX + combined.toString("base64");
3538
+ return `${AES_V2_PREFIX}${this._activeKeyId}:${combined.toString("base64")}`;
3462
3539
  }
3540
+ /** Decrypt ciphertext. Supports all historical envelope formats. */
3463
3541
  decrypt(ciphertext) {
3464
- if (!this._aesKey) {
3465
- throw new Error("CryptoEngine not initialised. AES key missing.");
3542
+ if (this._keyring.size === 0) {
3543
+ throw new Error("CryptoEngine not initialised.");
3466
3544
  }
3467
3545
  try {
3546
+ if (ciphertext.startsWith(AES_V2_PREFIX)) {
3547
+ const rest = ciphertext.slice(AES_V2_PREFIX.length);
3548
+ const sep3 = rest.indexOf(":");
3549
+ if (sep3 === -1) throw new Error("Malformed aes:v2 envelope: missing key ID separator.");
3550
+ const keyId = rest.slice(0, sep3);
3551
+ const b64 = rest.slice(sep3 + 1);
3552
+ return this._decryptAesGcm(keyId, b64);
3553
+ }
3468
3554
  if (ciphertext.startsWith(AES_GCM_PREFIX)) {
3469
- return this._decryptAesGcm(ciphertext.slice(AES_GCM_PREFIX.length));
3555
+ return this._decryptAesGcm("default", ciphertext.slice(AES_GCM_PREFIX.length));
3556
+ }
3557
+ if (ciphertext.startsWith(AES_GCM_LEGACY_PREFIX)) {
3558
+ return this._decryptAesGcm("default", ciphertext.slice(AES_GCM_LEGACY_PREFIX.length));
3470
3559
  }
3471
3560
  return this._decryptLegacyFernet(ciphertext);
3472
3561
  } catch (e6) {
3473
- console.error("Failed to decrypt vault payload. Check your MASK_ENCRYPTION_KEY. Inner error:", e6);
3562
+ console.error("Failed to decrypt vault payload. Check your MASK_ENCRYPTION_KEY / MASK_KEYRING. Inner error:", e6);
3474
3563
  throw new exports.MaskDecryptionError("Decryption failed");
3475
3564
  }
3476
3565
  }
3477
- /** Decrypt an AES-256-GCM token (base64 encoded). */
3478
- _decryptAesGcm(b64) {
3566
+ _decryptAesGcm(keyId, b64) {
3567
+ const aesKey = this._keyring.get(keyId);
3568
+ if (!aesKey) {
3569
+ throw new exports.MaskDecryptionError(
3570
+ `No key found for key ID '${keyId}'. Ensure the key is present in MASK_KEYRING.`
3571
+ );
3572
+ }
3479
3573
  const combined = Buffer.from(b64, "base64");
3480
3574
  if (combined.length < GCM_IV_BYTES + GCM_AUTH_TAG_BYTES) {
3481
3575
  throw new Error("Ciphertext too short for AES-GCM");
@@ -3483,22 +3577,11 @@ var init_crypto = __esm({
3483
3577
  const iv = combined.subarray(0, GCM_IV_BYTES);
3484
3578
  const authTag = combined.subarray(GCM_IV_BYTES, GCM_IV_BYTES + GCM_AUTH_TAG_BYTES);
3485
3579
  const encrypted = combined.subarray(GCM_IV_BYTES + GCM_AUTH_TAG_BYTES);
3486
- const decipher = cryptoNode__namespace.createDecipheriv(GCM_ALGORITHM, this._aesKey, iv);
3580
+ const decipher = cryptoNode__namespace.createDecipheriv(GCM_ALGORITHM, aesKey, iv);
3487
3581
  decipher.setAuthTag(authTag);
3488
- const decrypted = Buffer.concat([
3489
- decipher.update(encrypted),
3490
- decipher.final()
3491
- ]);
3582
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
3492
3583
  return decrypted.toString("utf8");
3493
3584
  }
3494
- /**
3495
- * Attempt to decrypt a legacy Fernet-format token.
3496
- *
3497
- * Fernet format: Version (1) || Timestamp (8) || IV (16) || Ciphertext (var) || HMAC (32)
3498
- * All base64url-encoded.
3499
- *
3500
- * We try to use the `fernet` npm package if available, otherwise throw.
3501
- */
3502
3585
  _decryptLegacyFernet(ciphertext) {
3503
3586
  let fernet;
3504
3587
  try {
@@ -3559,16 +3642,15 @@ function getLogger(name) {
3559
3642
  error: (...args) => _log("error", ...args)
3560
3643
  };
3561
3644
  }
3562
- function _makeEvent(action, token, dataType, agent = "", tool = "", extra = null) {
3645
+ function _makeEvent(action, token, dataType, agent = "", tool = "", extra = null, instanceId = "") {
3563
3646
  const event = {
3564
3647
  ts: Date.now() / 1e3,
3565
3648
  action,
3566
- // "encode" | "decode" | "expired" | "error"
3567
3649
  token,
3568
3650
  data_type: dataType,
3569
- // "email" | "phone" | "ssn" | "opaque"
3570
3651
  agent,
3571
- tool
3652
+ tool,
3653
+ instance_id: instanceId
3572
3654
  };
3573
3655
  if (extra) {
3574
3656
  Object.assign(event, _deepMask(extra));
@@ -3578,7 +3660,7 @@ function _makeEvent(action, token, dataType, agent = "", tool = "", extra = null
3578
3660
  function _deepMask(obj) {
3579
3661
  if (obj === null || obj === void 0) return obj;
3580
3662
  if (typeof obj === "string") {
3581
- return looksLikeToken(obj) ? obj : "[REDACTED]";
3663
+ return isUnambiguouslySafeToken(obj) ? obj : "[REDACTED]";
3582
3664
  }
3583
3665
  if (typeof obj !== "object") return obj;
3584
3666
  if (Array.isArray(obj)) {
@@ -3614,7 +3696,8 @@ var init_audit_logger = __esm({
3614
3696
  this._strictMode = config.MASK_AUDIT_LOG_STRICT;
3615
3697
  const rawKey = process.env.MASK_MASTER_KEY || process.env.MASK_ENCRYPTION_KEY || "";
3616
3698
  this._signingKey = cryptoNode__namespace.createHash("sha256").update(rawKey).digest();
3617
- this._prevSig = "0".repeat(64);
3699
+ this._instanceId = cryptoNode__namespace.randomUUID();
3700
+ this._prevSig = cryptoNode__namespace.createHmac("sha256", this._signingKey).update(this._instanceId, "utf-8").digest("hex");
3618
3701
  }
3619
3702
  static getInstance() {
3620
3703
  if (this._instance === null) {
@@ -3622,16 +3705,46 @@ var init_audit_logger = __esm({
3622
3705
  }
3623
3706
  return this._instance;
3624
3707
  }
3708
+ _getOverflowPath() {
3709
+ const d6 = process.env.MASK_SECURE_AUDIT_LOG_DIR || __require("os").tmpdir();
3710
+ return path2__namespace.join(d6, `mask_audit_overflow_${this._instanceId}.ndjson`);
3711
+ }
3712
+ _writeOverflow(event) {
3713
+ try {
3714
+ fs__namespace.appendFileSync(this._getOverflowPath(), JSON.stringify(event) + "\n", "utf-8");
3715
+ } catch {
3716
+ }
3717
+ }
3718
+ _consumeOverflow(events) {
3719
+ const overflowPath = this._getOverflowPath();
3720
+ if (!fs__namespace.existsSync(overflowPath)) return;
3721
+ const processingPath = overflowPath + ".processing";
3722
+ try {
3723
+ fs__namespace.renameSync(overflowPath, processingPath);
3724
+ } catch {
3725
+ return;
3726
+ }
3727
+ try {
3728
+ const content = fs__namespace.readFileSync(processingPath, "utf-8");
3729
+ for (const line of content.split("\n")) {
3730
+ if (line.trim()) events.push(JSON.parse(line));
3731
+ }
3732
+ fs__namespace.unlinkSync(processingPath);
3733
+ } catch (e6) {
3734
+ _logger.error(`Failed to consume overflow: ${e6}`);
3735
+ }
3736
+ }
3625
3737
  log(action, token, dataType = "opaque", agent = "", tool = "", extra = {}) {
3626
- const event = _makeEvent(action, token, dataType, agent, tool, extra);
3738
+ const event = _makeEvent(action, token, dataType, agent, tool, extra, this._instanceId);
3627
3739
  if (this._buffer.length >= this._maxBufferSize) {
3628
3740
  if (!this._bufferFullWarned) {
3629
3741
  _logger.warn(
3630
- `AuditLogger buffer full (max=${this._maxBufferSize}). Performing emergency sync-flush to prevent data loss.`
3742
+ `AuditLogger buffer full (max=${this._maxBufferSize}). Spooling to disk overflow to prevent OOM.`
3631
3743
  );
3632
3744
  this._bufferFullWarned = true;
3633
3745
  }
3634
- this._flushSync();
3746
+ this._writeOverflow(event);
3747
+ return;
3635
3748
  }
3636
3749
  this._buffer.push(event);
3637
3750
  }
@@ -3661,18 +3774,20 @@ var init_audit_logger = __esm({
3661
3774
  await this._flush();
3662
3775
  }
3663
3776
  async _flush() {
3664
- if (this._isFlushing || this._buffer.length === 0) return;
3777
+ if (this._isFlushing) return;
3665
3778
  this._isFlushing = true;
3666
3779
  try {
3667
3780
  const events = [...this._buffer];
3668
3781
  this._buffer = [];
3669
3782
  this._bufferFullWarned = false;
3783
+ this._consumeOverflow(events);
3784
+ if (events.length === 0) return;
3670
3785
  const secureLogDir = process.env.MASK_SECURE_AUDIT_LOG_DIR || "";
3671
3786
  let secureStream = null;
3672
3787
  if (secureLogDir) {
3673
3788
  fs__namespace.mkdirSync(secureLogDir, { recursive: true });
3674
3789
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3675
- const filePath = path__namespace.join(secureLogDir, `mask-audit-${dateStr}.ndjson`);
3790
+ const filePath = path2__namespace.join(secureLogDir, `mask-audit-${dateStr}.ndjson`);
3676
3791
  try {
3677
3792
  secureStream = fs__namespace.createWriteStream(filePath, { flags: "a" });
3678
3793
  } catch {
@@ -3700,13 +3815,43 @@ var init_audit_logger = __esm({
3700
3815
  this._isFlushing = false;
3701
3816
  }
3702
3817
  }
3703
- /** Synchronous flush for use in signal handlers where async is unreliable. */
3818
+ /** Synchronous flush for use in signal handlers where async is unreliable.
3819
+ *
3820
+ * Computes HMAC signatures to maintain chain integrity and writes to the
3821
+ * secure ndjson audit file (MASK_SECURE_AUDIT_LOG_DIR) if configured,
3822
+ * ensuring SOC 2 tamper-evidence guarantees hold through process shutdown.
3823
+ */
3704
3824
  _flushSync() {
3705
3825
  if (this._buffer.length === 0) return;
3706
3826
  const events = [...this._buffer];
3707
3827
  this._buffer = [];
3828
+ const secureLogDir = process.env.MASK_SECURE_AUDIT_LOG_DIR || "";
3829
+ let secureFilePath = null;
3830
+ if (secureLogDir) {
3831
+ try {
3832
+ fs__namespace.mkdirSync(secureLogDir, { recursive: true });
3833
+ const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3834
+ secureFilePath = path2__namespace.join(secureLogDir, `mask-audit-${dateStr}.ndjson`);
3835
+ } catch {
3836
+ }
3837
+ }
3708
3838
  for (const evt of events) {
3709
- process.stdout.write(JSON.stringify(evt) + "\n");
3839
+ const body = JSON.stringify(evt, (_, v7) => typeof v7 === "bigint" ? v7.toString() : v7);
3840
+ const sigInput = Buffer.from(this._prevSig + body, "utf-8");
3841
+ const sig = cryptoNode__namespace.createHmac("sha256", this._signingKey).update(sigInput).digest("hex");
3842
+ const signedLine = JSON.stringify({
3843
+ ...evt,
3844
+ prev_sig: this._prevSig,
3845
+ sig
3846
+ }, (_, v7) => typeof v7 === "bigint" ? v7.toString() : v7);
3847
+ this._prevSig = sig;
3848
+ process.stdout.write(signedLine + "\n");
3849
+ if (secureFilePath) {
3850
+ try {
3851
+ fs__namespace.appendFileSync(secureFilePath, signedLine + "\n", { encoding: "utf-8" });
3852
+ } catch {
3853
+ }
3854
+ }
3710
3855
  }
3711
3856
  }
3712
3857
  };
@@ -18299,10 +18444,10 @@ var init_date_utils = __esm({
18299
18444
  };
18300
18445
  }
18301
18446
  });
18302
- var randomUUID;
18447
+ var randomUUID2;
18303
18448
  var init_randomUUID = __esm({
18304
18449
  "node_modules/@smithy/uuid/dist-es/randomUUID.js"() {
18305
- randomUUID = cryptoNode__namespace.default.randomUUID.bind(cryptoNode__namespace.default);
18450
+ randomUUID2 = cryptoNode__namespace.default.randomUUID.bind(cryptoNode__namespace.default);
18306
18451
  }
18307
18452
  });
18308
18453
 
@@ -18313,8 +18458,8 @@ var init_v4 = __esm({
18313
18458
  init_randomUUID();
18314
18459
  decimalToHex = Array.from({ length: 256 }, (_, i6) => i6.toString(16).padStart(2, "0"));
18315
18460
  v4 = () => {
18316
- if (randomUUID) {
18317
- return randomUUID();
18461
+ if (randomUUID2) {
18462
+ return randomUUID2();
18318
18463
  }
18319
18464
  const rnds = new Uint8Array(16);
18320
18465
  crypto.getRandomValues(rnds);
@@ -27993,7 +28138,7 @@ var init_getHomeDir = __esm({
27993
28138
  return "DEFAULT";
27994
28139
  };
27995
28140
  getHomeDir = () => {
27996
- const { HOME, USERPROFILE, HOMEPATH, HOMEDRIVE = `C:${path.sep}` } = process.env;
28141
+ const { HOME, USERPROFILE, HOMEPATH, HOMEDRIVE = `C:${path2.sep}` } = process.env;
27997
28142
  if (HOME)
27998
28143
  return HOME;
27999
28144
  if (USERPROFILE)
@@ -28024,7 +28169,7 @@ var init_getSSOTokenFilepath = __esm({
28024
28169
  getSSOTokenFilepath = (id) => {
28025
28170
  const hasher = cryptoNode.createHash("sha1");
28026
28171
  const cacheName = hasher.update(id).digest("hex");
28027
- return path.join(getHomeDir(), ".aws", "sso", "cache", `${cacheName}.json`);
28172
+ return path2.join(getHomeDir(), ".aws", "sso", "cache", `${cacheName}.json`);
28028
28173
  };
28029
28174
  }
28030
28175
  });
@@ -28079,7 +28224,7 @@ var init_getConfigFilepath = __esm({
28079
28224
  "node_modules/@smithy/shared-ini-file-loader/dist-es/getConfigFilepath.js"() {
28080
28225
  init_getHomeDir();
28081
28226
  ENV_CONFIG_PATH = "AWS_CONFIG_FILE";
28082
- getConfigFilepath = () => process.env[ENV_CONFIG_PATH] || path.join(getHomeDir(), ".aws", "config");
28227
+ getConfigFilepath = () => process.env[ENV_CONFIG_PATH] || path2.join(getHomeDir(), ".aws", "config");
28083
28228
  }
28084
28229
  });
28085
28230
  var ENV_CREDENTIALS_PATH, getCredentialsFilepath;
@@ -28087,7 +28232,7 @@ var init_getCredentialsFilepath = __esm({
28087
28232
  "node_modules/@smithy/shared-ini-file-loader/dist-es/getCredentialsFilepath.js"() {
28088
28233
  init_getHomeDir();
28089
28234
  ENV_CREDENTIALS_PATH = "AWS_SHARED_CREDENTIALS_FILE";
28090
- getCredentialsFilepath = () => process.env[ENV_CREDENTIALS_PATH] || path.join(getHomeDir(), ".aws", "credentials");
28235
+ getCredentialsFilepath = () => process.env[ENV_CREDENTIALS_PATH] || path2.join(getHomeDir(), ".aws", "credentials");
28091
28236
  }
28092
28237
  });
28093
28238
 
@@ -28179,11 +28324,11 @@ var init_loadSharedConfigFiles = __esm({
28179
28324
  const relativeHomeDirPrefix = "~/";
28180
28325
  let resolvedFilepath = filepath;
28181
28326
  if (filepath.startsWith(relativeHomeDirPrefix)) {
28182
- resolvedFilepath = path.join(homeDir, filepath.slice(2));
28327
+ resolvedFilepath = path2.join(homeDir, filepath.slice(2));
28183
28328
  }
28184
28329
  let resolvedConfigFilepath = configFilepath;
28185
28330
  if (configFilepath.startsWith(relativeHomeDirPrefix)) {
28186
- resolvedConfigFilepath = path.join(homeDir, configFilepath.slice(2));
28331
+ resolvedConfigFilepath = path2.join(homeDir, configFilepath.slice(2));
28187
28332
  }
28188
28333
  const parsedFiles = await Promise.all([
28189
28334
  readFile2(resolvedConfigFilepath, {
@@ -34448,10 +34593,10 @@ var init_getNodeModulesParentDirs = __esm({
34448
34593
  if (!dirname2) {
34449
34594
  return [cwd];
34450
34595
  }
34451
- const normalizedPath = path.normalize(dirname2);
34452
- const parts = normalizedPath.split(path.sep);
34596
+ const normalizedPath = path2.normalize(dirname2);
34597
+ const parts = normalizedPath.split(path2.sep);
34453
34598
  const nodeModulesIndex = parts.indexOf("node_modules");
34454
- const parentDir = nodeModulesIndex !== -1 ? parts.slice(0, nodeModulesIndex).join(path.sep) : normalizedPath;
34599
+ const parentDir = nodeModulesIndex !== -1 ? parts.slice(0, nodeModulesIndex).join(path2.sep) : normalizedPath;
34455
34600
  if (cwd === parentDir) {
34456
34601
  return [cwd];
34457
34602
  }
@@ -34503,7 +34648,7 @@ var init_getTypeScriptUserAgentPair = __esm({
34503
34648
  init_getNodeModulesParentDirs();
34504
34649
  init_getSanitizedDevTypeScriptVersion();
34505
34650
  init_getSanitizedTypeScriptVersion();
34506
- TS_PACKAGE_JSON = path.join("node_modules", "typescript", "package.json");
34651
+ TS_PACKAGE_JSON = path2.join("node_modules", "typescript", "package.json");
34507
34652
  getTypeScriptUserAgentPair = async () => {
34508
34653
  if (tscVersion === null) {
34509
34654
  return void 0;
@@ -34524,7 +34669,7 @@ var init_getTypeScriptUserAgentPair = __esm({
34524
34669
  let versionFromApp;
34525
34670
  for (const nodeModulesParentDir of nodeModulesParentDirs) {
34526
34671
  try {
34527
- const appPackageJsonPath = path.join(nodeModulesParentDir, "package.json");
34672
+ const appPackageJsonPath = path2.join(nodeModulesParentDir, "package.json");
34528
34673
  const packageJson = await fs2.readFile(appPackageJsonPath, "utf-8");
34529
34674
  const { dependencies, devDependencies } = JSON.parse(packageJson);
34530
34675
  const version = devDependencies?.typescript ?? dependencies?.typescript;
@@ -34543,7 +34688,7 @@ var init_getTypeScriptUserAgentPair = __esm({
34543
34688
  let versionFromNodeModules;
34544
34689
  for (const nodeModulesParentDir of nodeModulesParentDirs) {
34545
34690
  try {
34546
- const tsPackageJsonPath = path.join(nodeModulesParentDir, TS_PACKAGE_JSON);
34691
+ const tsPackageJsonPath = path2.join(nodeModulesParentDir, TS_PACKAGE_JSON);
34547
34692
  const packageJson = await fs2.readFile(tsPackageJsonPath, "utf-8");
34548
34693
  const { version } = JSON.parse(packageJson);
34549
34694
  const sanitizedVersion2 = getSanitizedTypeScriptVersion(version);
@@ -38912,7 +39057,7 @@ var init_LoginCredentialsFetcher = __esm({
38912
39057
  }
38913
39058
  async saveToken(token) {
38914
39059
  const tokenFilePath = this.getTokenFilePath();
38915
- const directory = path.dirname(tokenFilePath);
39060
+ const directory = path2.dirname(tokenFilePath);
38916
39061
  try {
38917
39062
  await fs.promises.mkdir(directory, { recursive: true });
38918
39063
  } catch (error) {
@@ -38920,10 +39065,10 @@ var init_LoginCredentialsFetcher = __esm({
38920
39065
  await fs.promises.writeFile(tokenFilePath, JSON.stringify(token, null, 2), "utf8");
38921
39066
  }
38922
39067
  getTokenFilePath() {
38923
- const directory = process.env.AWS_LOGIN_CACHE_DIRECTORY ?? path.join(os.homedir(), ".aws", "login", "cache");
39068
+ const directory = process.env.AWS_LOGIN_CACHE_DIRECTORY ?? path2.join(os.homedir(), ".aws", "login", "cache");
38924
39069
  const loginSessionBytes = Buffer.from(this.loginSession, "utf8");
38925
39070
  const loginSessionSha256 = cryptoNode.createHash("sha256").update(loginSessionBytes).digest("hex");
38926
- return path.join(directory, `${loginSessionSha256}.json`);
39071
+ return path2.join(directory, `${loginSessionSha256}.json`);
38927
39072
  }
38928
39073
  derToRawSignature(derSignature) {
38929
39074
  let offset = 2;
@@ -43542,6 +43687,25 @@ function _hashPlaintext(plaintext, secret) {
43542
43687
  }
43543
43688
  return cryptoNode__namespace.createHash("sha256").update(trimmed, "utf-8").digest("hex");
43544
43689
  }
43690
+ function _vaultKey(token) {
43691
+ return `mask:${config.MASK_TENANT_ID}:${token}`;
43692
+ }
43693
+ function _vaultRevKey(ptHash) {
43694
+ return `mask-rev:${config.MASK_TENANT_ID}:${ptHash}`;
43695
+ }
43696
+ function _vaultHashKey(token) {
43697
+ return `mask-hash:${config.MASK_TENANT_ID}:${token}`;
43698
+ }
43699
+ function _unwrapPayload(raw) {
43700
+ if (raw && raw.startsWith("{")) {
43701
+ try {
43702
+ const obj = JSON.parse(raw);
43703
+ if (obj.ct) return obj.ct;
43704
+ } catch {
43705
+ }
43706
+ }
43707
+ return raw;
43708
+ }
43545
43709
  function getVault() {
43546
43710
  if (_vaultInstance === null) {
43547
43711
  const vaultType = config.MASK_VAULT_TYPE;
@@ -43666,14 +43830,15 @@ var init_vault = __esm({
43666
43830
  MemoryVault = class extends BaseVault {
43667
43831
  constructor() {
43668
43832
  super();
43833
+ this._cleanupTimer = null;
43669
43834
  this._store = /* @__PURE__ */ new Map();
43670
43835
  this._reverseStore = /* @__PURE__ */ new Map();
43836
+ this._cleanupTimer = setInterval(() => this._cleanup(), 6e4);
43837
+ if (this._cleanupTimer && typeof this._cleanupTimer.unref === "function") {
43838
+ this._cleanupTimer.unref();
43839
+ }
43671
43840
  }
43672
43841
  _cleanup() {
43673
- const cleanupFreq = config.MASK_VAULT_CLEANUP_FREQUENCY;
43674
- if (Math.random() > cleanupFreq) {
43675
- return;
43676
- }
43677
43842
  const now = Date.now() / 1e3;
43678
43843
  for (const [token, entry] of this._store.entries()) {
43679
43844
  if (now > entry.expiry) {
@@ -43685,8 +43850,18 @@ var init_vault = __esm({
43685
43850
  }
43686
43851
  }
43687
43852
  async store(token, ciphertext, ttlSeconds, ptHash = null, metadata = null) {
43688
- this._cleanup();
43853
+ if (!this._store.has(token) && this._store.size >= config.MASK_VAULT_MAX_MEMORY_KEYS) {
43854
+ const firstKey = this._store.keys().next().value;
43855
+ if (firstKey !== void 0) {
43856
+ const oldEntry = this._store.get(firstKey);
43857
+ this._store.delete(firstKey);
43858
+ if (oldEntry?.ptHash && this._reverseStore.get(oldEntry.ptHash) === firstKey) {
43859
+ this._reverseStore.delete(oldEntry.ptHash);
43860
+ }
43861
+ }
43862
+ }
43689
43863
  const existing = this._store.get(token);
43864
+ if (existing) this._store.delete(token);
43690
43865
  this._store.set(token, {
43691
43866
  plaintext: ciphertext,
43692
43867
  expiry: Date.now() / 1e3 + ttlSeconds,
@@ -43698,7 +43873,6 @@ var init_vault = __esm({
43698
43873
  }
43699
43874
  }
43700
43875
  async getTokenByPlaintextHash(ptHash) {
43701
- this._cleanup();
43702
43876
  const token = this._reverseStore.get(ptHash);
43703
43877
  if (token && this._store.has(token)) {
43704
43878
  return token;
@@ -43710,7 +43884,6 @@ var init_vault = __esm({
43710
43884
  return entry?.ptHash ?? null;
43711
43885
  }
43712
43886
  async retrieve(token) {
43713
- this._cleanup();
43714
43887
  const entry = this._store.get(token);
43715
43888
  if (!entry) {
43716
43889
  return null;
@@ -43770,10 +43943,10 @@ var init_vault = __esm({
43770
43943
  try {
43771
43944
  const pipeline = this._client.pipeline();
43772
43945
  const payload = metadata ? JSON.stringify({ ct: ciphertext, meta: metadata }) : ciphertext;
43773
- pipeline.set(`mask:${token}`, payload, "EX", ttlSeconds);
43946
+ pipeline.set(_vaultKey(token), payload, "EX", ttlSeconds);
43774
43947
  if (ptHash) {
43775
- pipeline.set(`mask-rev:${ptHash}`, token, "EX", ttlSeconds);
43776
- pipeline.set(`mask-hash:${token}`, ptHash, "EX", ttlSeconds);
43948
+ pipeline.set(_vaultRevKey(ptHash), token, "EX", ttlSeconds);
43949
+ pipeline.set(_vaultHashKey(token), ptHash, "EX", ttlSeconds);
43777
43950
  }
43778
43951
  const results = await pipeline.exec();
43779
43952
  if (results) {
@@ -43787,19 +43960,19 @@ var init_vault = __esm({
43787
43960
  }
43788
43961
  async getPtHashForToken(token) {
43789
43962
  try {
43790
- return await this._client.get(`mask-hash:${token}`);
43963
+ return await this._client.get(_vaultHashKey(token));
43791
43964
  } catch {
43792
43965
  return null;
43793
43966
  }
43794
43967
  }
43795
43968
  async getTokenByPlaintextHash(ptHash) {
43796
43969
  try {
43797
- const token = await this._client.get(`mask-rev:${ptHash}`);
43970
+ const token = await this._client.get(_vaultRevKey(ptHash));
43798
43971
  if (token) {
43799
- if (await this._client.exists(`mask:${token}`)) {
43972
+ if (await this._client.exists(_vaultKey(token))) {
43800
43973
  return token;
43801
43974
  } else {
43802
- await this._client.del(`mask-rev:${ptHash}`);
43975
+ await this._client.del(_vaultRevKey(ptHash));
43803
43976
  }
43804
43977
  }
43805
43978
  return null;
@@ -43812,7 +43985,8 @@ var init_vault = __esm({
43812
43985
  }
43813
43986
  async retrieve(token) {
43814
43987
  try {
43815
- return await this._client.get(`mask:${token}`);
43988
+ const raw = await this._client.get(_vaultKey(token));
43989
+ return raw ? _unwrapPayload(raw) : null;
43816
43990
  } catch (e6) {
43817
43991
  if (_getFailStrategy() === "closed") {
43818
43992
  throw new exports.MaskVaultConnectionError(`Redis read failed: ${e6}`);
@@ -43822,12 +43996,12 @@ var init_vault = __esm({
43822
43996
  }
43823
43997
  async delete(token) {
43824
43998
  try {
43825
- const ptHash = await this._client.get(`mask-hash:${token}`);
43999
+ const ptHash = await this._client.get(_vaultHashKey(token));
43826
44000
  const pipeline = this._client.pipeline();
43827
- pipeline.del(`mask:${token}`);
43828
- pipeline.del(`mask-hash:${token}`);
44001
+ pipeline.del(_vaultKey(token));
44002
+ pipeline.del(_vaultHashKey(token));
43829
44003
  if (ptHash) {
43830
- pipeline.del(`mask-rev:${ptHash}`);
44004
+ pipeline.del(_vaultRevKey(ptHash));
43831
44005
  }
43832
44006
  await pipeline.exec();
43833
44007
  } catch (e6) {
@@ -43866,39 +44040,26 @@ var init_vault = __esm({
43866
44040
  this._client = DynamoDBDocument.from(baseClient);
43867
44041
  console.info(`DynamoDBVault connected to table ${this._tableName} in ${this._region}`);
43868
44042
  }
43869
- async store(token, ciphertext, ttlSeconds, ptHash = null) {
44043
+ async store(token, ciphertext, ttlSeconds, ptHash = null, metadata = null) {
43870
44044
  const { TransactWriteCommand, PutCommand } = __require("@aws-sdk/lib-dynamodb");
43871
44045
  const now = Math.floor(Date.now() / 1e3);
43872
44046
  const ttlVal = now + ttlSeconds;
43873
- const item = {
43874
- token: `mask:${token}`,
44047
+ const primaryItem = {
44048
+ token: _vaultKey(token),
43875
44049
  ciphertext,
43876
- ttl: ttlVal,
43877
- ptr_hash: ptHash || void 0
44050
+ ttl: ttlVal
43878
44051
  };
44052
+ if (ptHash) primaryItem.ptr_hash = ptHash;
44053
+ if (metadata) primaryItem.meta_json = JSON.stringify(metadata);
43879
44054
  if (ptHash) {
43880
44055
  try {
43881
44056
  await this._client.send(new TransactWriteCommand({
43882
44057
  TransactItems: [
44058
+ { Put: { TableName: this._tableName, Item: primaryItem } },
43883
44059
  {
43884
44060
  Put: {
43885
44061
  TableName: this._tableName,
43886
- Item: {
43887
- token: `mask:${token}`,
43888
- ciphertext,
43889
- ttl: ttlVal,
43890
- ptr_hash: ptHash
43891
- }
43892
- }
43893
- },
43894
- {
43895
- Put: {
43896
- TableName: this._tableName,
43897
- Item: {
43898
- token: `mask-rev:${ptHash}`,
43899
- ciphertext: token,
43900
- ttl: ttlVal
43901
- }
44062
+ Item: { token: _vaultRevKey(ptHash), ciphertext: token, ttl: ttlVal }
43902
44063
  }
43903
44064
  }
43904
44065
  ]
@@ -43909,7 +44070,7 @@ var init_vault = __esm({
43909
44070
  }
43910
44071
  } else {
43911
44072
  try {
43912
- await this._client.send(new PutCommand({ TableName: this._tableName, Item: item }));
44073
+ await this._client.send(new PutCommand({ TableName: this._tableName, Item: primaryItem }));
43913
44074
  } catch (e6) {
43914
44075
  throw new exports.MaskVaultConnectionError(`DynamoDB individual write failed: ${e6}`);
43915
44076
  }
@@ -43921,12 +44082,12 @@ var init_vault = __esm({
43921
44082
  const now = Math.floor(Date.now() / 1e3);
43922
44083
  const resp = await this._client.send(new GetCommand({
43923
44084
  TableName: this._tableName,
43924
- Key: { token: `mask-rev:${ptHash}` }
44085
+ Key: { token: _vaultRevKey(ptHash) }
43925
44086
  }));
43926
44087
  const item = resp.Item;
43927
44088
  if (!item) return null;
43928
44089
  if (now > (item.ttl || 0)) {
43929
- await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: `mask-rev:${ptHash}` } }));
44090
+ await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: _vaultRevKey(ptHash) } }));
43930
44091
  return null;
43931
44092
  }
43932
44093
  const token = item.ciphertext;
@@ -43942,16 +44103,16 @@ var init_vault = __esm({
43942
44103
  const now = Math.floor(Date.now() / 1e3);
43943
44104
  const resp = await this._client.send(new GetCommand({
43944
44105
  TableName: this._tableName,
43945
- Key: { token: `mask:${token}` }
44106
+ Key: { token: _vaultKey(token) }
43946
44107
  }));
43947
44108
  const item = resp.Item;
43948
44109
  if (!item) return null;
43949
44110
  if (now > (item.ttl || 0)) {
43950
44111
  const ptHash = item.ptr_hash;
43951
44112
  if (ptHash) {
43952
- await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: `mask-rev:${ptHash}` } }));
44113
+ await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: _vaultRevKey(ptHash) } }));
43953
44114
  }
43954
- await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: `mask:${token}` } }));
44115
+ await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: _vaultKey(token) } }));
43955
44116
  return null;
43956
44117
  }
43957
44118
  return item.ciphertext;
@@ -43965,7 +44126,7 @@ var init_vault = __esm({
43965
44126
  const { GetCommand } = __require("@aws-sdk/lib-dynamodb");
43966
44127
  const resp = await this._client.send(new GetCommand({
43967
44128
  TableName: this._tableName,
43968
- Key: { token: `mask:${token}` }
44129
+ Key: { token: _vaultKey(token) }
43969
44130
  }));
43970
44131
  return resp.Item?.ptr_hash ?? null;
43971
44132
  } catch {
@@ -43977,13 +44138,13 @@ var init_vault = __esm({
43977
44138
  const { GetCommand, DeleteCommand } = __require("@aws-sdk/lib-dynamodb");
43978
44139
  const resp = await this._client.send(new GetCommand({
43979
44140
  TableName: this._tableName,
43980
- Key: { token: `mask:${token}` }
44141
+ Key: { token: _vaultKey(token) }
43981
44142
  }));
43982
44143
  const item = resp.Item;
43983
44144
  if (item && item.ptr_hash) {
43984
- await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: `mask-rev:${item.ptr_hash}` } }));
44145
+ await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: _vaultRevKey(item.ptr_hash) } }));
43985
44146
  }
43986
- await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: `mask:${token}` } }));
44147
+ await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: _vaultKey(token) } }));
43987
44148
  } catch (e6) {
43988
44149
  if (_getFailStrategy() === "closed") throw new exports.MaskVaultConnectionError(`DynamoDB delete failed: ${e6}`);
43989
44150
  }
@@ -44005,10 +44166,10 @@ var init_vault = __esm({
44005
44166
  async store(token, ciphertext, ttlSeconds, ptHash = null, metadata = null) {
44006
44167
  try {
44007
44168
  const payload = metadata ? JSON.stringify({ ct: ciphertext, meta: metadata }) : ciphertext;
44008
- await this._client.set(`mask:${token}`, Buffer.from(payload), { expires: ttlSeconds });
44169
+ await this._client.set(_vaultKey(token), Buffer.from(payload), { expires: ttlSeconds });
44009
44170
  if (ptHash) {
44010
- await this._client.set(`mask-rev:${ptHash}`, Buffer.from(token), { expires: ttlSeconds });
44011
- await this._client.set(`mask-hash:${token}`, Buffer.from(ptHash), { expires: ttlSeconds });
44171
+ await this._client.set(_vaultRevKey(ptHash), Buffer.from(token), { expires: ttlSeconds });
44172
+ await this._client.set(_vaultHashKey(token), Buffer.from(ptHash), { expires: ttlSeconds });
44012
44173
  }
44013
44174
  } catch (e6) {
44014
44175
  throw new exports.MaskVaultConnectionError(`Memcached error: ${e6}`);
@@ -44016,7 +44177,7 @@ var init_vault = __esm({
44016
44177
  }
44017
44178
  async getPtHashForToken(token) {
44018
44179
  try {
44019
- const { value } = await this._client.get(`mask-hash:${token}`);
44180
+ const { value } = await this._client.get(_vaultHashKey(token));
44020
44181
  return value ? value.toString() : null;
44021
44182
  } catch {
44022
44183
  return null;
@@ -44024,7 +44185,7 @@ var init_vault = __esm({
44024
44185
  }
44025
44186
  async getTokenByPlaintextHash(ptHash) {
44026
44187
  try {
44027
- const { value } = await this._client.get(`mask-rev:${ptHash}`);
44188
+ const { value } = await this._client.get(_vaultRevKey(ptHash));
44028
44189
  if (!value) return null;
44029
44190
  const token = value.toString();
44030
44191
  return await this.retrieve(token) !== null ? token : null;
@@ -44035,8 +44196,9 @@ var init_vault = __esm({
44035
44196
  }
44036
44197
  async retrieve(token) {
44037
44198
  try {
44038
- const { value } = await this._client.get(`mask:${token}`);
44039
- return value ? value.toString() : null;
44199
+ const { value } = await this._client.get(_vaultKey(token));
44200
+ if (!value) return null;
44201
+ return _unwrapPayload(value.toString());
44040
44202
  } catch (e6) {
44041
44203
  if (_getFailStrategy() === "closed") throw new exports.MaskVaultConnectionError(`Memcached read failed: ${e6}`);
44042
44204
  return null;
@@ -44044,12 +44206,12 @@ var init_vault = __esm({
44044
44206
  }
44045
44207
  async delete(token) {
44046
44208
  try {
44047
- const { value } = await this._client.get(`mask-hash:${token}`);
44209
+ const { value } = await this._client.get(_vaultHashKey(token));
44048
44210
  const ptHash = value ? value.toString() : null;
44049
- await this._client.delete(`mask:${token}`);
44050
- await this._client.delete(`mask-hash:${token}`);
44211
+ await this._client.delete(_vaultKey(token));
44212
+ await this._client.delete(_vaultHashKey(token));
44051
44213
  if (ptHash) {
44052
- await this._client.delete(`mask-rev:${ptHash}`);
44214
+ await this._client.delete(_vaultRevKey(ptHash));
44053
44215
  }
44054
44216
  } catch (e6) {
44055
44217
  if (_getFailStrategy() === "closed") throw new exports.MaskVaultConnectionError(`Memcached delete failed: ${e6}`);
@@ -59534,7 +59696,7 @@ var init_transformers_scanner = __esm({
59534
59696
  );
59535
59697
  }
59536
59698
  if (!this._pool) {
59537
- const workerPath = path__namespace.resolve(__dirname, "nlp_worker.js");
59699
+ const workerPath = path2__namespace.resolve(__dirname, "nlp_worker.js");
59538
59700
  const maxThreads = Math.max(1, Math.min(os__namespace.cpus().length - 1, 4));
59539
59701
  this._pool = new Piscina({
59540
59702
  filename: workerPath,