{"version":3,"file":"EncryptedIndexedDBDriver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/EncryptedIndexedDBDriver.ts"],"sourcesContent":["import { getCacheConfig } from \"../config\";\r\nimport { CacheQuotaExceededError } from \"../errors\";\r\nimport { CacheDriverInterface, IndexedDBCacheRecord } from \"../types\";\r\nimport IndexedDBDriver from \"./IndexedDBDriver\";\r\n\r\n/**\r\n * IndexedDB driver whose payload is encrypted at rest.\r\n *\r\n * The whole `{data, expiresAt}` envelope is encrypted — the same shape\r\n * the encrypted Web Storage drivers write — so the expiry travels\r\n * *inside* the authenticated cypher and cannot be extended by anyone who\r\n * can write to the database.\r\n *\r\n * `expiresAt` is ALSO stored in the clear on the record, because the\r\n * driver has to evict expired entries without decrypting every row\r\n * (a rotated key would otherwise leave the database growing forever).\r\n * The two are checked in order, so the effective expiry is the earlier\r\n * of the two: pushing the plaintext copy further out buys an attacker\r\n * nothing, and pulling it in only evicts an entry they could have\r\n * deleted outright anyway. What the plaintext copy does leak is *when*\r\n * an entry expires — a session length, roughly. If that matters more\r\n * than unbounded growth on a stale key, set no TTL.\r\n */\r\nexport default class EncryptedIndexedDBDriver\r\n  extends IndexedDBDriver\r\n  implements CacheDriverInterface\r\n{\r\n  /**\r\n   * Set cache into storage\r\n   */\r\n  public async set(key: string, value: any, expiresAfter?: number) {\r\n    const expiresAt = this.expiryOf(expiresAfter);\r\n\r\n    const cypher = await getCacheConfig(\"encryption\")?.encrypt({\r\n      data: value,\r\n      expiresAt,\r\n    });\r\n\r\n    const record: IndexedDBCacheRecord = { value: cypher, expiresAt };\r\n\r\n    try {\r\n      await this.request(\"readwrite\", store =>\r\n        store.put(record, this.getKey(key))\r\n      );\r\n    } catch (error) {\r\n      if (this.isQuotaError(error)) {\r\n        throw new CacheQuotaExceededError(key, error);\r\n      }\r\n\r\n      throw error;\r\n    }\r\n\r\n    return this;\r\n  }\r\n\r\n  /**\r\n   * Get value from cache engine, if key does not exist return default value\r\n   *\r\n   * A cypher that fails to decrypt — tampered record, rotated key,\r\n   * truncated write — is evicted and the default value returned, so one\r\n   * poisoned row cannot make every later read throw. With AES-GCM the\r\n   * failed authentication tag lands here too, which makes eviction the\r\n   * tamper response as well.\r\n   */\r\n  public async get(key: string, defaultValue: any = null) {\r\n    const record = await this.record(key);\r\n\r\n    if (record === undefined || record === null) return defaultValue;\r\n\r\n    if (this.isExpired(record)) {\r\n      await this.remove(key);\r\n      return defaultValue;\r\n    }\r\n\r\n    try {\r\n      const decrypted = await getCacheConfig(\"encryption\")?.decrypt(\r\n        record.value\r\n      );\r\n\r\n      // Legacy / non-envelope cyphers: return whatever came out, with\r\n      // no expiry, matching the encrypted Web Storage drivers.\r\n      if (\r\n        decrypted === null ||\r\n        decrypted === undefined ||\r\n        typeof decrypted !== \"object\" ||\r\n        !(\"data\" in decrypted)\r\n      ) {\r\n        return decrypted === null || decrypted === undefined\r\n          ? defaultValue\r\n          : decrypted;\r\n      }\r\n\r\n      // The authenticated copy of the expiry wins over the plaintext\r\n      // one on the record.\r\n      if (decrypted.expiresAt && decrypted.expiresAt < new Date().getTime()) {\r\n        await this.remove(key);\r\n        return defaultValue;\r\n      }\r\n\r\n      return decrypted.data;\r\n    } catch (error) {\r\n      await this.remove(key);\r\n      return defaultValue;\r\n    }\r\n  }\r\n\r\n  /**\r\n   * Read every live entry owned by this engine\r\n   *\r\n   * Overridden because the base implementation reads records in one\r\n   * transaction and cannot `await` a decrypt inside it — the records\r\n   * are collected first, then decrypted key by key through `get()`.\r\n   */\r\n  public async getAll(): Promise<Record<string, any>> {\r\n    const entries: Record<string, any> = Object.create(null);\r\n\r\n    for (const key of await this.keys()) {\r\n      const value = await this.get(key, undefined);\r\n\r\n      if (value === undefined) continue;\r\n\r\n      Object.defineProperty(entries, key, {\r\n        value,\r\n        writable: true,\r\n        enumerable: true,\r\n        configurable: true,\r\n      });\r\n    }\r\n\r\n    return entries;\r\n  }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAqB,2BAArB,cACU,gBAEV;;;;CAIE,MAAa,IAAI,KAAa,OAAY,cAAuB;EAC/D,MAAM,YAAY,KAAK,SAAS,YAAY;EAO5C,MAAM,SAA+B;GAAE,OAAO,MALzB,eAAe,YAAY,CAAC,EAAE,QAAQ;IACzD,MAAM;IACN;GACF,CAAC;GAEqD;EAAU;EAEhE,IAAI;GACF,MAAM,KAAK,QAAQ,cAAa,UAC9B,MAAM,IAAI,QAAQ,KAAK,OAAO,GAAG,CAAC,CACpC;EACF,SAAS,OAAO;GACd,IAAI,KAAK,aAAa,KAAK,GACzB,MAAM,IAAI,wBAAwB,KAAK,KAAK;GAG9C,MAAM;EACR;EAEA,OAAO;CACT;;;;;;;;;;CAWA,MAAa,IAAI,KAAa,eAAoB,MAAM;EACtD,MAAM,SAAS,MAAM,KAAK,OAAO,GAAG;EAEpC,IAAI,WAAW,UAAa,WAAW,MAAM,OAAO;EAEpD,IAAI,KAAK,UAAU,MAAM,GAAG;GAC1B,MAAM,KAAK,OAAO,GAAG;GACrB,OAAO;EACT;EAEA,IAAI;GACF,MAAM,YAAY,MAAM,eAAe,YAAY,CAAC,EAAE,QACpD,OAAO,KACT;GAIA,IACE,cAAc,QACd,cAAc,UACd,OAAO,cAAc,YACrB,EAAE,UAAU,YAEZ,OAAO,cAAc,QAAQ,cAAc,SACvC,eACA;GAKN,IAAI,UAAU,aAAa,UAAU,6BAAY,IAAI,KAAK,EAAC,CAAC,QAAQ,GAAG;IACrE,MAAM,KAAK,OAAO,GAAG;IACrB,OAAO;GACT;GAEA,OAAO,UAAU;EACnB,SAAS,OAAO;GACd,MAAM,KAAK,OAAO,GAAG;GACrB,OAAO;EACT;CACF;;;;;;;;CASA,MAAa,SAAuC;EAClD,MAAM,UAA+B,OAAO,OAAO,IAAI;EAEvD,KAAK,MAAM,OAAO,MAAM,KAAK,KAAK,GAAG;GACnC,MAAM,QAAQ,MAAM,KAAK,IAAI,KAAK,MAAS;GAE3C,IAAI,UAAU,QAAW;GAEzB,OAAO,eAAe,SAAS,KAAK;IAClC;IACA,UAAU;IACV,YAAY;IACZ,cAAc;GAChB,CAAC;EACH;EAEA,OAAO;CACT;AACF"}