orm-blind-indexer 1.0.0 → 1.0.1

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.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import * as _prisma_client from '@prisma/client';
1
+ import * as _prisma_client_extension from '@prisma/client/extension';
2
2
  import * as _prisma_client_runtime_library from '@prisma/client/runtime/library';
3
3
 
4
4
  /**
@@ -151,7 +151,7 @@ interface EncryptionResult {
151
151
  * @returns A Prisma Client Extension object to pass to `.$extends()`.
152
152
  * @throws {Error} At initialization time if the keys are invalid.
153
153
  */
154
- declare function withBlindIndexer(config: BlindIndexConfig): (client: any) => _prisma_client.PrismaClientExtends<_prisma_client_runtime_library.InternalArgs<{}, {}, {}, {}>>;
154
+ declare function withBlindIndexer(config: BlindIndexConfig): (client: any) => _prisma_client_extension.PrismaClientExtends<_prisma_client_runtime_library.InternalArgs<{}, {}, {}, {}>>;
155
155
 
156
156
  /**
157
157
  * @file crypto/encryptor.ts
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as _prisma_client from '@prisma/client';
1
+ import * as _prisma_client_extension from '@prisma/client/extension';
2
2
  import * as _prisma_client_runtime_library from '@prisma/client/runtime/library';
3
3
 
4
4
  /**
@@ -151,7 +151,7 @@ interface EncryptionResult {
151
151
  * @returns A Prisma Client Extension object to pass to `.$extends()`.
152
152
  * @throws {Error} At initialization time if the keys are invalid.
153
153
  */
154
- declare function withBlindIndexer(config: BlindIndexConfig): (client: any) => _prisma_client.PrismaClientExtends<_prisma_client_runtime_library.InternalArgs<{}, {}, {}, {}>>;
154
+ declare function withBlindIndexer(config: BlindIndexConfig): (client: any) => _prisma_client_extension.PrismaClientExtends<_prisma_client_runtime_library.InternalArgs<{}, {}, {}, {}>>;
155
155
 
156
156
  /**
157
157
  * @file crypto/encryptor.ts
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ __export(index_exports, {
33
33
  module.exports = __toCommonJS(index_exports);
34
34
 
35
35
  // src/extension/prisma.ts
36
- var import_client = require("@prisma/client");
36
+ var import_extension = require("@prisma/client/extension");
37
37
 
38
38
  // src/crypto/encryptor.ts
39
39
  var import_crypto = require("crypto");
@@ -424,7 +424,7 @@ function processResult(result, modelConfig, encryptionKey) {
424
424
  function withBlindIndexer(config) {
425
425
  const encryptionKey = validateEncryptionKey(config.encryptionKey);
426
426
  const indexKey = validateIndexKey(config.indexKey);
427
- return import_client.Prisma.defineExtension({
427
+ return import_extension.Prisma.defineExtension({
428
428
  name: "orm-blind-indexer",
429
429
  query: {
430
430
  $allModels: {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/extension/prisma.ts","../src/crypto/encryptor.ts","../src/crypto/hasher.ts","../src/crypto/utils.ts","../src/extension/interceptors.ts"],"sourcesContent":["/**\n * @file index.ts\n * @description Public API surface for the orm-blind-indexer package.\n *\n * Only symbols explicitly exported from this file are part of the public API.\n * Internal implementation details remain private.\n */\n\n// --- Primary Extension Factory ---\nexport { withBlindIndexer } from './extension/prisma.js';\n\n// --- Cryptographic Primitives (for advanced use cases) ---\nexport { encrypt, decrypt, isEncryptedFormat } from './crypto/encryptor.js';\nexport { generateBlindIndex, generateBlindIndexBatch } from './crypto/hasher.js';\nexport { validateEncryptionKey, validateIndexKey, parseAndValidateKey } from './crypto/utils.js';\n\n// --- TypeScript Types ---\nexport type {\n BlindIndexConfig,\n FieldConfig,\n ModelFieldConfig,\n FieldsConfig,\n ParsedCiphertext,\n EncryptionResult,\n} from './types/index.js';\n\n","/**\n * @file extension/prisma.ts\n * @description The core Prisma Client Extension implementing the blind indexing middleware.\n *\n * ## How Prisma Client Extensions Work\n *\n * Prisma Client Extensions (introduced in Prisma 4.7) allow augmenting the Prisma\n * client with custom behavior. We use the `query` extension component, which\n * intercepts all database operations before and after they execute.\n *\n * The `query.$allModels.$allOperations` hook receives:\n * - `model`: The Prisma model name (e.g., \"User\")\n * - `operation`: The operation name (e.g., \"findUnique\", \"create\")\n * - `args`: The operation arguments (where clause, data, etc.) — MUTATED before passing to `next()`\n * - `query`: The `next` function to call to execute the actual query\n *\n * ## Lifecycle of a Write Operation (create/update/upsert)\n *\n * 1. Developer calls: `prisma.user.create({ data: { email: \"user@example.com\" } })`\n * 2. Extension intercepts `args.data`\n * 3. For each configured encrypted field found in `data`:\n * a. Encrypt the plaintext with AES-256-GCM → `email` becomes the ciphertext\n * b. Generate HMAC-SHA256 blind index → inject as `email_bidx`\n * 4. The modified `args` are passed to `next()` → Prisma writes both columns\n * 5. The result is post-processed: `email` is decrypted back, `email_bidx` is stripped\n *\n * ## Lifecycle of a Read Operation (findUnique/findMany/findFirst)\n *\n * 1. Developer calls: `prisma.user.findUnique({ where: { email: \"user@example.com\" } })`\n * 2. Extension intercepts `args.where`\n * 3. `transformWhereClause()` replaces `email` with `email_bidx: hash(\"user@example.com\")`\n * 4. The modified `args` are passed to `next()` → Prisma queries the blind index column\n * 5. The result is post-processed: `email` is decrypted, `email_bidx` is stripped\n *\n * ## Write Operations Supported\n *\n * | Operation | Data Location | Supported |\n * |--------------------|--------------------|-----------|\n * | create | args.data | ✅ |\n * | update | args.data | ✅ |\n * | upsert | args.create + args.update | ✅ |\n * | createMany | args.data (array) | ✅ |\n * | updateMany | args.data | ✅ |\n *\n * ## Read Operations Supported\n *\n * | Operation | Where Location | Supported |\n * |--------------------|-------------------|-----------|\n * | findUnique | args.where | ✅ |\n * | findUniqueOrThrow | args.where | ✅ |\n * | findFirst | args.where | ✅ |\n * | findFirstOrThrow | args.where | ✅ |\n * | findMany | args.where | ✅ |\n * | count | args.where | ✅ |\n * | aggregate | args.where | ✅ |\n * | delete | args.where | ✅ |\n * | deleteMany | args.where | ✅ |\n */\n\nimport { Prisma } from '@prisma/client';\nimport { encrypt, decrypt, isEncryptedFormat } from '../crypto/encryptor.js';\nimport { generateBlindIndex } from '../crypto/hasher.js';\nimport { validateEncryptionKey, validateIndexKey } from '../crypto/utils.js';\nimport { transformWhereClause, stripBlindIndexFields } from './interceptors.js';\nimport type { BlindIndexConfig, ModelFieldConfig } from '../types/index.js';\n\n/** Operations that write data and require encrypt + blind-index injection */\nconst WRITE_OPERATIONS = new Set([\n 'create',\n 'update',\n 'upsert',\n 'createMany',\n 'updateMany',\n]);\n\n/** Operations that read data and require where-clause transformation */\nconst READ_OPERATIONS = new Set([\n 'findUnique',\n 'findUniqueOrThrow',\n 'findFirst',\n 'findFirstOrThrow',\n 'findMany',\n 'count',\n 'aggregate',\n 'groupBy',\n 'delete',\n 'deleteMany',\n]);\n\n/**\n * Processes a single data object (from create/update `data`), encrypting\n * configured plaintext fields and injecting their blind index counterparts.\n *\n * @param data - The raw data object from the Prisma args.\n * @param modelConfig - Field configuration for the current model.\n * @param encryptionKey - AES-256-GCM key Buffer.\n * @param indexKey - HMAC-SHA256 key Buffer.\n * @returns A new data object with encrypted values and blind index fields injected.\n */\nfunction processWriteData(\n data: Record<string, unknown>,\n modelConfig: ModelFieldConfig,\n encryptionKey: Buffer,\n indexKey: Buffer\n): Record<string, unknown> {\n const result = { ...data };\n\n for (const [fieldName, fieldConfig] of Object.entries(modelConfig)) {\n const value = result[fieldName];\n\n if (value === undefined || value === null) {\n // Field not present in this write, or explicitly null — skip\n continue;\n }\n\n if (typeof value !== 'string') {\n // Non-string values (e.g. Prisma `set` wrappers) — skip for safety\n continue;\n }\n\n // Guard against double-encryption if the value is already in encrypted format\n if (isEncryptedFormat(value)) {\n continue;\n }\n\n // 1. Encrypt the plaintext value\n result[fieldName] = encrypt(value, encryptionKey);\n\n // 2. Generate and inject the blind index\n result[fieldConfig.indexFieldName] = generateBlindIndex(\n value,\n indexKey,\n fieldConfig.caseSensitive ?? false\n );\n }\n\n return result;\n}\n\n/**\n * Post-processes a Prisma result, decrypting encrypted fields and removing\n * blind index fields from the output.\n *\n * @param result - The raw result from Prisma.\n * @param modelConfig - Field configuration for the current model.\n * @param encryptionKey - AES-256-GCM key Buffer for decryption.\n * @returns The cleaned result with decrypted plaintext and no `_bidx` fields.\n */\nfunction processResult(\n result: unknown,\n modelConfig: ModelFieldConfig,\n encryptionKey: Buffer\n): unknown {\n if (result === null || result === undefined) {\n return result;\n }\n\n if (Array.isArray(result)) {\n return result.map((item) => processResult(item, modelConfig, encryptionKey));\n }\n\n if (typeof result === 'object') {\n const obj = result as Record<string, unknown>;\n // Decrypt each configured field\n for (const [fieldName] of Object.entries(modelConfig)) {\n const value = obj[fieldName];\n if (typeof value === 'string' && isEncryptedFormat(value)) {\n try {\n obj[fieldName] = decrypt(value, encryptionKey);\n } catch {\n // If decryption fails, leave the value as-is (may not be encrypted)\n }\n }\n }\n // Strip blind index fields from the result\n return stripBlindIndexFields(obj, modelConfig);\n }\n\n return result;\n}\n\n/**\n * Creates a Prisma Client Extension that transparently implements blind indexing\n * for the configured fields.\n *\n * **Usage:**\n * ```ts\n * import { PrismaClient } from '@prisma/client';\n * import { withBlindIndexer } from 'orm-blind-indexer';\n *\n * const prisma = new PrismaClient().$extends(\n * withBlindIndexer({\n * encryptionKey: process.env.ENCRYPTION_KEY!,\n * indexKey: process.env.INDEX_KEY!,\n * fields: {\n * User: {\n * email: {\n * indexFieldName: 'email_bidx',\n * caseSensitive: false,\n * },\n * },\n * },\n * })\n * );\n * ```\n *\n * @param config - The blind indexer configuration object.\n * @returns A Prisma Client Extension object to pass to `.$extends()`.\n * @throws {Error} At initialization time if the keys are invalid.\n */\nexport function withBlindIndexer(config: BlindIndexConfig) {\n // Validate keys eagerly at initialization time for fast failure\n const encryptionKey = validateEncryptionKey(config.encryptionKey);\n const indexKey = validateIndexKey(config.indexKey);\n\n return Prisma.defineExtension({\n name: 'orm-blind-indexer',\n\n query: {\n $allModels: {\n async $allOperations({ model, operation, args, query }) {\n // Get the field config for the current model (if any)\n const modelConfig: ModelFieldConfig | undefined =\n model !== undefined ? config.fields[model] : undefined;\n\n // If no config for this model, pass through unchanged\n if (modelConfig === undefined || Object.keys(modelConfig).length === 0) {\n return query(args);\n }\n\n const typedArgs = args as Record<string, unknown>;\n\n // ----------------------------------------------------------------\n // WRITE OPERATIONS: encrypt data + inject blind indexes\n // ----------------------------------------------------------------\n if (WRITE_OPERATIONS.has(operation)) {\n if (operation === 'upsert') {\n // upsert has both `create` and `update` data objects\n if (typedArgs['create'] && typeof typedArgs['create'] === 'object') {\n typedArgs['create'] = processWriteData(\n typedArgs['create'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n if (typedArgs['update'] && typeof typedArgs['update'] === 'object') {\n typedArgs['update'] = processWriteData(\n typedArgs['update'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n } else if (operation === 'createMany') {\n // createMany has an array of data objects\n if (Array.isArray(typedArgs['data'])) {\n typedArgs['data'] = (typedArgs['data'] as Record<string, unknown>[]).map(\n (item) => processWriteData(item, modelConfig, encryptionKey, indexKey)\n );\n }\n } else {\n // create, update, updateMany\n if (typedArgs['data'] && typeof typedArgs['data'] === 'object') {\n typedArgs['data'] = processWriteData(\n typedArgs['data'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n }\n\n // Also transform where clause for update/delete operations\n if (typedArgs['where'] && typeof typedArgs['where'] === 'object') {\n typedArgs['where'] = transformWhereClause(\n typedArgs['where'] as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n }\n\n // ----------------------------------------------------------------\n // READ OPERATIONS: transform where clause\n // ----------------------------------------------------------------\n if (READ_OPERATIONS.has(operation)) {\n if (typedArgs['where'] && typeof typedArgs['where'] === 'object') {\n typedArgs['where'] = transformWhereClause(\n typedArgs['where'] as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n }\n\n // Execute the (possibly mutated) query\n const result = await query(args);\n\n // ----------------------------------------------------------------\n // POST-PROCESS: decrypt results + strip blind index fields\n // ----------------------------------------------------------------\n // Don't post-process count/aggregate (they return numbers, not records)\n if (operation === 'count' || operation === 'aggregate' || operation === 'groupBy') {\n return result;\n }\n\n return processResult(result, modelConfig, encryptionKey);\n },\n },\n },\n });\n}\n\n","/**\n * @file crypto/encryptor.ts\n * @description AES-256-GCM authenticated encryption and decryption.\n *\n * ## Why AES-256-GCM?\n *\n * AES-256-GCM (Galois/Counter Mode) is an authenticated encryption with\n * associated data (AEAD) cipher. It provides:\n *\n * 1. **Confidentiality**: The ciphertext reveals nothing about the plaintext.\n * 2. **Integrity & Authenticity**: The 16-byte authentication tag (Auth Tag)\n * cryptographically proves the ciphertext has not been tampered with.\n * Decryption fails with an error if the tag doesn't verify.\n * 3. **No padding oracle**: GCM is a stream-cipher mode — no PKCS#7 padding\n * means no padding oracle attacks.\n *\n * ## On-disk format\n *\n * We serialize the IV, Auth Tag, and ciphertext as three colon-separated\n * base64 segments: `base64(iv):base64(authTag):base64(ciphertext)`\n *\n * This is a self-contained format: everything needed to decrypt the value\n * lives in the value itself. The key is stored separately (environment variable).\n *\n * ## IV Randomness\n *\n * A fresh 12-byte random IV is generated for **every** encryption call using\n * `crypto.randomBytes()`. This is critical: reusing an IV with the same key\n * in GCM mode completely breaks confidentiality (two-time pad attack).\n * With a 12-byte IV, the probability of a collision is negligible for\n * reasonable database sizes (birthday bound: ~2^48 operations for 2^-32 risk).\n *\n * ## Auth Tag Length\n *\n * We use the full 16-byte (128-bit) authentication tag, which is the\n * maximum and recommended length. Shorter tags reduce forgery resistance.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes } from 'crypto';\nimport type { ParsedCiphertext } from '../types/index.js';\n\n/** AES-256-GCM algorithm identifier used by Node.js crypto. */\nconst ALGORITHM = 'aes-256-gcm' as const;\n\n/** Length of the random Initialization Vector in bytes. NIST recommends 96 bits (12 bytes) for GCM. */\nconst IV_LENGTH_BYTES = 12;\n\n/** Length of the GCM authentication tag in bytes. 128 bits (16 bytes) is the maximum and recommended. */\nconst AUTH_TAG_LENGTH_BYTES = 16;\n\n/** Delimiter used to separate the IV, Auth Tag, and ciphertext in the serialized format. */\nconst DELIMITER = ':';\n\n/**\n * Encrypts a UTF-8 plaintext string using AES-256-GCM.\n *\n * @param plaintext - The plaintext string to encrypt (e.g., an email address).\n * @param key - A 32-byte Buffer containing the AES-256 encryption key.\n * @returns A serialized ciphertext string: `base64(iv):base64(authTag):base64(ciphertext)`.\n * @throws {Error} If encryption fails (e.g., invalid key length).\n *\n * @example\n * ```ts\n * const encrypted = encrypt('user@example.com', encryptionKeyBuffer);\n * // => \"ABC123...:XYZ789...:cipher...\"\n * ```\n */\nexport function encrypt(plaintext: string, key: Buffer): string {\n // Generate a fresh random 12-byte IV for every encryption\n const iv = randomBytes(IV_LENGTH_BYTES);\n\n // Create the cipher. Node.js validates the key length here and throws\n // if it doesn't match the algorithm's requirement (32 bytes for AES-256).\n const cipher = createCipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH_BYTES,\n });\n\n // Encrypt the plaintext. The update() call does the actual encryption;\n // final() flushes any remaining bytes (empty for a stream cipher like GCM).\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n\n // Retrieve the GCM authentication tag AFTER calling final()\n const authTag = cipher.getAuthTag();\n\n // Serialize: base64(iv) : base64(authTag) : base64(ciphertext)\n return [\n iv.toString('base64'),\n authTag.toString('base64'),\n encrypted.toString('base64'),\n ].join(DELIMITER);\n}\n\n/**\n * Parses a serialized AES-256-GCM ciphertext string into its components.\n *\n * @param cipherText - The serialized string in `base64(iv):base64(authTag):base64(ct)` format.\n * @returns A `ParsedCiphertext` object with Buffer components.\n * @throws {Error} If the string format is invalid or any component has an unexpected length.\n */\nfunction parseCiphertext(cipherText: string): ParsedCiphertext {\n const parts = cipherText.split(DELIMITER);\n if (parts.length !== 3) {\n throw new Error(\n `[orm-blind-indexer] Invalid ciphertext format. ` +\n `Expected \"base64(iv):base64(authTag):base64(ciphertext)\" with exactly 2 colons, ` +\n `but got ${parts.length - 1} colons. ` +\n `The value may not be encrypted or may be corrupted.`\n );\n }\n\n const [ivB64, authTagB64, ciphertextB64] = parts as [string, string, string];\n\n const iv = Buffer.from(ivB64, 'base64');\n const authTag = Buffer.from(authTagB64, 'base64');\n const ciphertext = Buffer.from(ciphertextB64, 'base64');\n\n if (iv.byteLength !== IV_LENGTH_BYTES) {\n throw new Error(\n `[orm-blind-indexer] Invalid IV length in ciphertext. ` +\n `Expected ${IV_LENGTH_BYTES} bytes, got ${iv.byteLength} bytes.`\n );\n }\n\n if (authTag.byteLength !== AUTH_TAG_LENGTH_BYTES) {\n throw new Error(\n `[orm-blind-indexer] Invalid Auth Tag length in ciphertext. ` +\n `Expected ${AUTH_TAG_LENGTH_BYTES} bytes, got ${authTag.byteLength} bytes.`\n );\n }\n\n return { iv, authTag, ciphertext };\n}\n\n/**\n * Decrypts a serialized AES-256-GCM ciphertext string back to UTF-8 plaintext.\n *\n * The decryption process automatically verifies the GCM authentication tag.\n * If the ciphertext or key has been tampered with, decryption will throw an error\n * (\"Unsupported state or unable to authenticate data\") before any plaintext is returned.\n *\n * @param cipherText - The serialized ciphertext string from `encrypt()`.\n * @param key - The same 32-byte Buffer used during encryption.\n * @returns The original UTF-8 plaintext string.\n * @throws {Error} If the ciphertext format is invalid, the key is wrong,\n * or the authentication tag verification fails (indicating tampering).\n *\n * @example\n * ```ts\n * const plaintext = decrypt(encryptedValue, encryptionKeyBuffer);\n * // => \"user@example.com\"\n * ```\n */\nexport function decrypt(cipherText: string, key: Buffer): string {\n const { iv, authTag, ciphertext } = parseCiphertext(cipherText);\n\n const decipher = createDecipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH_BYTES,\n });\n\n // Set the authentication tag BEFORE decrypting.\n // The decipher will verify this tag during final(), throwing if it doesn't match.\n decipher.setAuthTag(authTag);\n\n // Decrypt the ciphertext. final() triggers the auth tag verification.\n const decrypted = Buffer.concat([\n decipher.update(ciphertext),\n decipher.final(), // Throws if auth tag verification fails\n ]);\n\n return decrypted.toString('utf8');\n}\n\n/**\n * Checks if a string appears to be in the encrypted format produced by `encrypt()`.\n * Used to avoid double-encrypting values that are already encrypted.\n *\n * This is a heuristic check based on format, not cryptographic verification.\n *\n * @param value - The string to check.\n * @returns `true` if the string matches the `base64:base64:base64` pattern.\n */\nexport function isEncryptedFormat(value: string): boolean {\n const parts = value.split(DELIMITER);\n if (parts.length !== 3) return false;\n\n // Check that each part is non-empty and looks like base64\n const base64Pattern = /^[A-Za-z0-9+/]+=*$/;\n return parts.every(\n (part): boolean => typeof part === 'string' && part.length > 0 && base64Pattern.test(part)\n );\n}\n\n","/**\n * @file crypto/hasher.ts\n * @description HMAC-SHA256 blind index generation.\n *\n * ## What is a Blind Index?\n *\n * A blind index solves the problem of querying encrypted data. Because AES-256-GCM\n * uses a random IV on every encryption, the same plaintext produces a different\n * ciphertext each time. This makes exact-match database queries impossible on\n * the encrypted column.\n *\n * A blind index is a **deterministic** hash of the plaintext that:\n * - Always produces the same output for the same input (queryable)\n * - Reveals no information about the plaintext without the key (private)\n * - Is stored in a separate `_bidx` column alongside the encrypted data\n *\n * ## Why HMAC-SHA256 (not plain SHA256)?\n *\n * Plain SHA256 is a one-way function, but it has no secret. An attacker who\n * gains read access to your database could mount a **dictionary attack**:\n * they hash every common email address and look for matches.\n *\n * HMAC-SHA256 incorporates a **secret key** (the `indexKey`). Without knowing\n * the key, an attacker cannot compute the hash, so the blind index leaks no\n * information about the plaintext even if the database is fully compromised.\n *\n * ## Case Sensitivity\n *\n * Email addresses and usernames are often compared case-insensitively.\n * When `caseSensitive: false` (the default), we lowercase the input before\n * hashing. This ensures that \"Test@Test.com\" and \"test@test.com\" produce the\n * same blind index, enabling case-insensitive equality searches.\n *\n * **Important**: The case normalization must be applied consistently on both\n * write (when storing) and read (when querying). The extension handles this.\n */\n\nimport { createHmac } from 'crypto';\n\n/** HMAC digest algorithm — SHA-256 produces a 32-byte (256-bit) output. */\nconst HMAC_ALGORITHM = 'sha256' as const;\n\n/**\n * Generates a deterministic HMAC-SHA256 blind index for a given plaintext.\n *\n * The output is a base64-encoded 32-byte HMAC digest. Base64 is used (rather\n * than hex) for compactness: 44 characters vs 64 characters for the same value.\n *\n * @param text - The plaintext value to hash (e.g., an email address).\n * @param indexKey - A secret Buffer used as the HMAC key. Must be kept private.\n * @param caseSensitive - When `false`, `text` is lowercased before hashing.\n * Defaults to `false` for case-insensitive equality searches.\n * @param truncateBytes - Optional byte length (8 to 32) to truncate the HMAC\n * digest before base64 encoding. Useful for frequency-analysis defense.\n * @returns A base64-encoded HMAC-SHA256 digest string.\n *\n * @example\n * ```ts\n * const indexKey = Buffer.from(process.env.INDEX_KEY!, 'base64');\n *\n * // Case-insensitive (default): both produce the same hash\n * generateBlindIndex('Test@Test.com', indexKey, false);\n * generateBlindIndex('test@test.com', indexKey, false);\n *\n * // Case-sensitive: these produce different hashes\n * generateBlindIndex('Test@Test.com', indexKey, true);\n * generateBlindIndex('test@test.com', indexKey, true);\n *\n * // Truncated blind index (e.g., 16 bytes for frequency defense)\n * generateBlindIndex('user@example.com', indexKey, false, 16);\n * ```\n */\nexport function generateBlindIndex(\n text: string,\n indexKey: Buffer,\n caseSensitive: boolean = false,\n truncateBytes?: number\n): string {\n // Normalize to lowercase for case-insensitive lookups\n const normalized = caseSensitive ? text : text.toLowerCase();\n\n const digest = createHmac(HMAC_ALGORITHM, indexKey)\n .update(normalized, 'utf8')\n .digest();\n\n if (truncateBytes !== undefined) {\n if (truncateBytes < 8 || truncateBytes > 32) {\n throw new Error(\n `[orm-blind-indexer] \"truncateBytes\" must be between 8 and 32 bytes, got ${truncateBytes}.`\n );\n }\n return digest.subarray(0, truncateBytes).toString('base64');\n }\n\n return digest.toString('base64');\n}\n\n/**\n * Generates blind indexes for an array of plaintext values.\n * Useful for `{ in: [...] }` and `{ notIn: [...] }` Prisma query operators.\n *\n * @param values - An array of plaintext strings to hash.\n * @param indexKey - The secret HMAC key Buffer.\n * @param caseSensitive - When `false`, each value is lowercased before hashing.\n * @param truncateBytes - Optional byte length (8 to 32) to truncate each digest.\n * @returns An array of base64-encoded HMAC-SHA256 digests, in the same order as `values`.\n */\nexport function generateBlindIndexBatch(\n values: string[],\n indexKey: Buffer,\n caseSensitive: boolean = false,\n truncateBytes?: number\n): string[] {\n return values.map((value) => generateBlindIndex(value, indexKey, caseSensitive, truncateBytes));\n}\n\n","/**\n * @file crypto/utils.ts\n * @description Key validation and parsing utilities for cryptographic operations.\n *\n * Security Design Notes:\n * - AES-256 requires a 256-bit (32-byte) key. Any deviation is a hard error.\n * - Keys may be provided as base64 or hex strings for safe environment variable storage.\n * - We validate at extension initialization time, not at query time, to fail fast.\n */\n\n/**\n * Attempts to parse a string as a base64-encoded Buffer.\n * Returns `null` if the string is not valid base64.\n */\nfunction tryParseBase64(value: string): Buffer | null {\n try {\n const buf = Buffer.from(value, 'base64');\n // Verify round-trip: if re-encoding doesn't match, the original wasn't valid base64\n if (buf.toString('base64') === value || buf.toString('base64url') === value) {\n return buf;\n }\n // Allow base64 with padding stripped or standard base64\n // A valid base64 string of length L decodes to floor(L*3/4) bytes\n // We just trust the Buffer decode here for practical use\n return buf.byteLength > 0 ? buf : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Attempts to parse a string as a hex-encoded Buffer.\n * Returns `null` if the string is not a valid even-length hex string.\n */\nfunction tryParseHex(value: string): Buffer | null {\n if (!/^[0-9a-fA-F]+$/.test(value) || value.length % 2 !== 0) {\n return null;\n }\n return Buffer.from(value, 'hex');\n}\n\n/**\n * Parses a key string (base64 or hex) into a Buffer and validates its byte length.\n *\n * The function tries hex first (unambiguous format), then base64.\n *\n * @param keyString - The raw key string from environment variables or config.\n * @param expectedBytes - The required byte length of the decoded key.\n * @param keyName - A descriptive name for the key, used in error messages.\n * @returns A `Buffer` of exactly `expectedBytes` bytes.\n * @throws {Error} If the key cannot be parsed or has an incorrect length.\n *\n * @example\n * ```ts\n * const key = parseAndValidateKey(process.env.ENCRYPTION_KEY!, 32, 'encryptionKey');\n * ```\n */\nexport function parseAndValidateKey(\n keyString: string,\n expectedBytes: number,\n keyName: string\n): Buffer {\n if (!keyString || typeof keyString !== 'string') {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" must be a non-empty string. ` +\n `Generate one with: node -e \"console.log(require('crypto').randomBytes(${expectedBytes}).toString('base64'))\"`\n );\n }\n\n // Try hex first (unambiguous — only hex chars, even length)\n const hexBuf = tryParseHex(keyString);\n if (hexBuf !== null) {\n if (hexBuf.byteLength !== expectedBytes) {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" decoded to ${hexBuf.byteLength} bytes from hex, ` +\n `but ${expectedBytes} bytes are required. ` +\n `A ${expectedBytes}-byte hex key must be ${expectedBytes * 2} hex characters.`\n );\n }\n return hexBuf;\n }\n\n // Try base64\n const b64Buf = tryParseBase64(keyString);\n if (b64Buf !== null) {\n if (b64Buf.byteLength !== expectedBytes) {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" decoded to ${b64Buf.byteLength} bytes from base64, ` +\n `but ${expectedBytes} bytes are required. ` +\n `Generate a valid key with: node -e \"console.log(require('crypto').randomBytes(${expectedBytes}).toString('base64'))\"`\n );\n }\n return b64Buf;\n }\n\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" could not be parsed as hex or base64. ` +\n `Provide a ${expectedBytes * 2}-char hex string or a ${Math.ceil(expectedBytes * 4 / 3)}-char base64 string.`\n );\n}\n\n/**\n * Validates an encryption key for AES-256 (requires exactly 32 bytes / 256 bits).\n *\n * @param keyString - The raw encryption key string from config.\n * @returns A `Buffer` of exactly 32 bytes suitable for AES-256-GCM.\n * @throws {Error} If the key is invalid or not exactly 32 bytes.\n */\nexport function validateEncryptionKey(keyString: string): Buffer {\n return parseAndValidateKey(keyString, 32, 'encryptionKey');\n}\n\n/**\n * Parses the index key (HMAC salt) from a string.\n * Unlike the encryption key, the HMAC key has no strict length requirement in the\n * cryptographic spec (SHA-256 accepts any length), but we enforce a minimum of 16 bytes\n * for security best-practice guidance.\n *\n * @param keyString - The raw index key string from config.\n * @returns A `Buffer` usable as an HMAC key.\n * @throws {Error} If the key cannot be parsed or is shorter than 16 bytes.\n */\nexport function validateIndexKey(keyString: string): Buffer {\n if (!keyString || typeof keyString !== 'string') {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" must be a non-empty string. ` +\n `Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"`\n );\n }\n\n // Try hex\n const hexBuf = tryParseHex(keyString);\n if (hexBuf !== null) {\n if (hexBuf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${hexBuf.byteLength} bytes). ` +\n `Minimum recommended length is 16 bytes (32 hex chars).`\n );\n }\n return hexBuf;\n }\n\n // Try base64\n const b64Buf = tryParseBase64(keyString);\n if (b64Buf !== null) {\n if (b64Buf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${b64Buf.byteLength} bytes). ` +\n `Minimum recommended length is 16 bytes.`\n );\n }\n return b64Buf;\n }\n\n // Fall back to treating it as a raw UTF-8 string key (not recommended but functional)\n const rawBuf = Buffer.from(keyString, 'utf8');\n if (rawBuf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${rawBuf.byteLength} bytes as UTF-8). ` +\n `Minimum recommended length is 16 bytes.`\n );\n }\n return rawBuf;\n}\n\n","/**\n * @file extension/interceptors.ts\n * @description Recursive WHERE clause transformation for blind index substitution.\n *\n * ## The Problem\n *\n * Prisma's `where` clause is a deeply nested object that can contain:\n * - Direct equality: `{ email: \"user@example.com\" }`\n * - Operator objects: `{ email: { in: [...], equals: \"...\" } }`\n * - Logical combinators: `{ AND: [...], OR: [...], NOT: { ... } }`\n * - Nested relations: `{ posts: { some: { author: { email: \"...\" } } } }`\n *\n * We need to intercept any occurrence of an encrypted field name and replace it\n * with a blind index lookup, regardless of how deeply nested it is.\n *\n * ## Strategy\n *\n * We use a **recursive depth-first traversal** of the `where` object:\n *\n * 1. For each key in the current object, check if it's a logical combinator\n * (`AND`, `OR`, `NOT`). If so, recurse into its children.\n * 2. If the key matches a configured encrypted field, transform the value:\n * - Remove the original field key.\n * - Add the `indexFieldName` key with the hashed value.\n * 3. Handle the `in` operator specially: hash each array element.\n *\n * ## Supported Prisma Filter Operators (on encrypted fields)\n *\n * | Prisma Operator | Supported | Notes |\n * |----------------|-----------|------------------------------------------|\n * | `equals` | ✅ | Direct value or `{ equals: value }` |\n * | `in` | ✅ | Hashes each element of the array |\n * | (direct) | ✅ | `{ email: \"value\" }` shorthand |\n * | `contains` | ❌ | LIKE queries incompatible with hashing |\n * | `startsWith` | ❌ | LIKE queries incompatible with hashing |\n * | `endsWith` | ❌ | LIKE queries incompatible with hashing |\n * | `lt/gt/lte/gte`| ❌ | Range queries incompatible with hashing |\n *\n * Attempting to use unsupported operators on encrypted fields will throw an error\n * guiding the developer to the correct approach.\n */\n\nimport { generateBlindIndex, generateBlindIndexBatch } from '../crypto/hasher.js';\nimport type { ModelFieldConfig } from '../types/index.js';\n\n/** Prisma logical combinator keys that contain nested where clause arrays or objects. */\nconst LOGICAL_COMBINATORS = new Set(['AND', 'OR', 'NOT']);\n\n/** Prisma filter operators that are incompatible with blind indexing. */\nconst UNSUPPORTED_OPERATORS = new Set([\n 'contains',\n 'startsWith',\n 'endsWith',\n 'lt',\n 'lte',\n 'gt',\n 'gte',\n 'search',\n]);\n\n/**\n * Transforms a Prisma `where` clause, replacing encrypted field lookups with\n * their corresponding blind index field lookups.\n *\n * This function is **pure** (it returns a new object, not mutating the original)\n * and **recursive** (it handles arbitrarily nested `AND`/`OR`/`NOT` clauses).\n *\n * @param where - The Prisma where clause object to transform.\n * @param modelConfig - The field configuration for the current model.\n * @param indexKey - The HMAC key Buffer for generating blind indexes.\n * @returns A new where clause object with encrypted fields replaced by blind index fields.\n * @throws {Error} If an unsupported operator is used on an encrypted field.\n */\nexport function transformWhereClause(\n where: Record<string, unknown>,\n modelConfig: ModelFieldConfig,\n indexKey: Buffer\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(where)) {\n // --- Handle logical combinators: AND, OR, NOT ---\n if (LOGICAL_COMBINATORS.has(key)) {\n if (Array.isArray(value)) {\n // AND: [...], OR: [...]\n result[key] = value.map((item) => {\n if (item !== null && typeof item === 'object' && !Array.isArray(item)) {\n return transformWhereClause(\n item as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n return item;\n });\n } else if (value !== null && typeof value === 'object') {\n // NOT: { ... }\n result[key] = transformWhereClause(\n value as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n } else {\n result[key] = value;\n }\n continue;\n }\n\n // --- Handle encrypted fields ---\n const fieldConfig = modelConfig[key];\n if (fieldConfig !== undefined) {\n const { indexFieldName, caseSensitive = false, truncateBytes } = fieldConfig;\n\n // Value is a plain string: `{ email: \"user@example.com\" }` shorthand\n if (typeof value === 'string') {\n result[indexFieldName] = generateBlindIndex(value, indexKey, caseSensitive, truncateBytes);\n continue;\n }\n\n // Value is null: pass through (querying for null encrypted fields)\n if (value === null) {\n result[indexFieldName] = null;\n continue;\n }\n\n // Value is an operator object: `{ email: { in: [...], equals: \"...\" } }`\n if (typeof value === 'object' && !Array.isArray(value)) {\n const operatorObj = value as Record<string, unknown>;\n const transformedOperator: Record<string, unknown> = {};\n\n for (const [op, opValue] of Object.entries(operatorObj)) {\n // Check for unsupported operators\n if (UNSUPPORTED_OPERATORS.has(op)) {\n throw new Error(\n `[orm-blind-indexer] The \"${op}\" filter operator is not supported on encrypted ` +\n `field \"${key}\". Blind indexes only support exact-match lookups (equals, in, notIn). ` +\n `Partial string searches on encrypted fields require application-level decryption.`\n );\n }\n\n // Handle Prisma's mode: \"insensitive\" / \"default\"\n if (op === 'mode') {\n if (opValue === 'insensitive' && caseSensitive) {\n throw new Error(\n `[orm-blind-indexer] \"mode: 'insensitive'\" cannot be used on encrypted field \"${key}\" ` +\n `configured with \"caseSensitive: true\". Change the field configuration to \"caseSensitive: false\" ` +\n `for case-insensitive blind index lookups.`\n );\n }\n // If caseSensitive is false, case insensitivity is inherently handled\n // during HMAC generation. We omit `mode` from the transformed query.\n continue;\n }\n\n if (op === 'equals') {\n if (typeof opValue === 'string') {\n transformedOperator['equals'] = generateBlindIndex(opValue, indexKey, caseSensitive, truncateBytes);\n } else if (opValue === null) {\n transformedOperator['equals'] = null;\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"equals\" operator on encrypted field \"${key}\" ` +\n `expects a string value, got ${typeof opValue}.`\n );\n }\n } else if (op === 'in') {\n if (Array.isArray(opValue)) {\n const strings = opValue.filter((v): v is string => typeof v === 'string');\n if (strings.length !== opValue.length) {\n throw new Error(\n `[orm-blind-indexer] The \"in\" operator on encrypted field \"${key}\" ` +\n `requires all values to be strings.`\n );\n }\n transformedOperator['in'] = generateBlindIndexBatch(strings, indexKey, caseSensitive, truncateBytes);\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"in\" operator on encrypted field \"${key}\" ` +\n `expects an array, got ${typeof opValue}.`\n );\n }\n } else if (op === 'notIn') {\n if (Array.isArray(opValue)) {\n const strings = opValue.filter((v): v is string => typeof v === 'string');\n if (strings.length !== opValue.length) {\n throw new Error(\n `[orm-blind-indexer] The \"notIn\" operator on encrypted field \"${key}\" ` +\n `requires all values to be strings.`\n );\n }\n transformedOperator['notIn'] = generateBlindIndexBatch(strings, indexKey, caseSensitive, truncateBytes);\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"notIn\" operator on encrypted field \"${key}\" ` +\n `expects an array, got ${typeof opValue}.`\n );\n }\n } else if (op === 'not') {\n // `NOT` as a filter operator (different from `NOT` combinator)\n if (typeof opValue === 'string') {\n transformedOperator['not'] = generateBlindIndex(opValue, indexKey, caseSensitive, truncateBytes);\n } else if (opValue === null) {\n transformedOperator['not'] = null;\n } else {\n transformedOperator['not'] = opValue;\n }\n } else {\n // Pass through unknown operators (future Prisma operators)\n transformedOperator[op] = opValue;\n }\n }\n\n result[indexFieldName] = transformedOperator;\n continue;\n }\n\n // Fallback: pass through as-is (handles edge cases)\n result[key] = value;\n continue;\n }\n\n // --- Non-encrypted field: pass through unchanged ---\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Strips blind index fields (`_bidx` columns) from a query result object\n * so the caller only sees the logical schema (without implementation details).\n *\n * This function also handles arrays of results (for `findMany`) and\n * recursively cleans nested relation objects.\n *\n * @param result - The raw result from Prisma (single object or array).\n * @param modelConfig - The field configuration for the current model.\n * @returns The cleaned result with `_bidx` fields removed.\n */\nexport function stripBlindIndexFields(\n result: unknown,\n modelConfig: ModelFieldConfig\n): unknown {\n if (result === null || result === undefined) {\n return result;\n }\n\n if (Array.isArray(result)) {\n return result.map((item) => stripBlindIndexFields(item, modelConfig));\n }\n\n if (typeof result === 'object') {\n const obj = result as Record<string, unknown>;\n const cleaned: Record<string, unknown> = {};\n\n // Collect all bidx field names to exclude\n const bidxFieldNames = new Set(\n Object.values(modelConfig).map((cfg) => cfg.indexFieldName)\n );\n\n for (const [key, value] of Object.entries(obj)) {\n if (!bidxFieldNames.has(key)) {\n // Recursively clean nested objects (for relations)\n if (value !== null && typeof value === 'object') {\n cleaned[key] = stripBlindIndexFields(value, modelConfig);\n } else {\n cleaned[key] = value;\n }\n }\n // Skip bidx fields — don't include them in output\n }\n\n return cleaned;\n }\n\n return result;\n}\n\n"],"mappings":"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","names":["import_crypto"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/extension/prisma.ts","../src/crypto/encryptor.ts","../src/crypto/hasher.ts","../src/crypto/utils.ts","../src/extension/interceptors.ts"],"sourcesContent":["/**\n * @file index.ts\n * @description Public API surface for the orm-blind-indexer package.\n *\n * Only symbols explicitly exported from this file are part of the public API.\n * Internal implementation details remain private.\n */\n\n// --- Primary Extension Factory ---\nexport { withBlindIndexer } from './extension/prisma.js';\n\n// --- Cryptographic Primitives (for advanced use cases) ---\nexport { encrypt, decrypt, isEncryptedFormat } from './crypto/encryptor.js';\nexport { generateBlindIndex, generateBlindIndexBatch } from './crypto/hasher.js';\nexport { validateEncryptionKey, validateIndexKey, parseAndValidateKey } from './crypto/utils.js';\n\n// --- TypeScript Types ---\nexport type {\n BlindIndexConfig,\n FieldConfig,\n ModelFieldConfig,\n FieldsConfig,\n ParsedCiphertext,\n EncryptionResult,\n} from './types/index.js';\n\n","/**\n * @file extension/prisma.ts\n * @description The core Prisma Client Extension implementing the blind indexing middleware.\n *\n * ## How Prisma Client Extensions Work\n *\n * Prisma Client Extensions (introduced in Prisma 4.7) allow augmenting the Prisma\n * client with custom behavior. We use the `query` extension component, which\n * intercepts all database operations before and after they execute.\n *\n * The `query.$allModels.$allOperations` hook receives:\n * - `model`: The Prisma model name (e.g., \"User\")\n * - `operation`: The operation name (e.g., \"findUnique\", \"create\")\n * - `args`: The operation arguments (where clause, data, etc.) — MUTATED before passing to `next()`\n * - `query`: The `next` function to call to execute the actual query\n *\n * ## Lifecycle of a Write Operation (create/update/upsert)\n *\n * 1. Developer calls: `prisma.user.create({ data: { email: \"user@example.com\" } })`\n * 2. Extension intercepts `args.data`\n * 3. For each configured encrypted field found in `data`:\n * a. Encrypt the plaintext with AES-256-GCM → `email` becomes the ciphertext\n * b. Generate HMAC-SHA256 blind index → inject as `email_bidx`\n * 4. The modified `args` are passed to `next()` → Prisma writes both columns\n * 5. The result is post-processed: `email` is decrypted back, `email_bidx` is stripped\n *\n * ## Lifecycle of a Read Operation (findUnique/findMany/findFirst)\n *\n * 1. Developer calls: `prisma.user.findUnique({ where: { email: \"user@example.com\" } })`\n * 2. Extension intercepts `args.where`\n * 3. `transformWhereClause()` replaces `email` with `email_bidx: hash(\"user@example.com\")`\n * 4. The modified `args` are passed to `next()` → Prisma queries the blind index column\n * 5. The result is post-processed: `email` is decrypted, `email_bidx` is stripped\n *\n * ## Write Operations Supported\n *\n * | Operation | Data Location | Supported |\n * |--------------------|--------------------|-----------|\n * | create | args.data | ✅ |\n * | update | args.data | ✅ |\n * | upsert | args.create + args.update | ✅ |\n * | createMany | args.data (array) | ✅ |\n * | updateMany | args.data | ✅ |\n *\n * ## Read Operations Supported\n *\n * | Operation | Where Location | Supported |\n * |--------------------|-------------------|-----------|\n * | findUnique | args.where | ✅ |\n * | findUniqueOrThrow | args.where | ✅ |\n * | findFirst | args.where | ✅ |\n * | findFirstOrThrow | args.where | ✅ |\n * | findMany | args.where | ✅ |\n * | count | args.where | ✅ |\n * | aggregate | args.where | ✅ |\n * | delete | args.where | ✅ |\n * | deleteMany | args.where | ✅ |\n */\n\nimport { Prisma } from '@prisma/client/extension';\nimport { encrypt, decrypt, isEncryptedFormat } from '../crypto/encryptor.js';\nimport { generateBlindIndex } from '../crypto/hasher.js';\nimport { validateEncryptionKey, validateIndexKey } from '../crypto/utils.js';\nimport { transformWhereClause, stripBlindIndexFields } from './interceptors.js';\nimport type { BlindIndexConfig, ModelFieldConfig } from '../types/index.js';\n\n/** Operations that write data and require encrypt + blind-index injection */\nconst WRITE_OPERATIONS = new Set([\n 'create',\n 'update',\n 'upsert',\n 'createMany',\n 'updateMany',\n]);\n\n/** Operations that read data and require where-clause transformation */\nconst READ_OPERATIONS = new Set([\n 'findUnique',\n 'findUniqueOrThrow',\n 'findFirst',\n 'findFirstOrThrow',\n 'findMany',\n 'count',\n 'aggregate',\n 'groupBy',\n 'delete',\n 'deleteMany',\n]);\n\n/**\n * Processes a single data object (from create/update `data`), encrypting\n * configured plaintext fields and injecting their blind index counterparts.\n *\n * @param data - The raw data object from the Prisma args.\n * @param modelConfig - Field configuration for the current model.\n * @param encryptionKey - AES-256-GCM key Buffer.\n * @param indexKey - HMAC-SHA256 key Buffer.\n * @returns A new data object with encrypted values and blind index fields injected.\n */\nfunction processWriteData(\n data: Record<string, unknown>,\n modelConfig: ModelFieldConfig,\n encryptionKey: Buffer,\n indexKey: Buffer\n): Record<string, unknown> {\n const result = { ...data };\n\n for (const [fieldName, fieldConfig] of Object.entries(modelConfig)) {\n const value = result[fieldName];\n\n if (value === undefined || value === null) {\n // Field not present in this write, or explicitly null — skip\n continue;\n }\n\n if (typeof value !== 'string') {\n // Non-string values (e.g. Prisma `set` wrappers) — skip for safety\n continue;\n }\n\n // Guard against double-encryption if the value is already in encrypted format\n if (isEncryptedFormat(value)) {\n continue;\n }\n\n // 1. Encrypt the plaintext value\n result[fieldName] = encrypt(value, encryptionKey);\n\n // 2. Generate and inject the blind index\n result[fieldConfig.indexFieldName] = generateBlindIndex(\n value,\n indexKey,\n fieldConfig.caseSensitive ?? false\n );\n }\n\n return result;\n}\n\n/**\n * Post-processes a Prisma result, decrypting encrypted fields and removing\n * blind index fields from the output.\n *\n * @param result - The raw result from Prisma.\n * @param modelConfig - Field configuration for the current model.\n * @param encryptionKey - AES-256-GCM key Buffer for decryption.\n * @returns The cleaned result with decrypted plaintext and no `_bidx` fields.\n */\nfunction processResult(\n result: unknown,\n modelConfig: ModelFieldConfig,\n encryptionKey: Buffer\n): unknown {\n if (result === null || result === undefined) {\n return result;\n }\n\n if (Array.isArray(result)) {\n return result.map((item) => processResult(item, modelConfig, encryptionKey));\n }\n\n if (typeof result === 'object') {\n const obj = result as Record<string, unknown>;\n // Decrypt each configured field\n for (const [fieldName] of Object.entries(modelConfig)) {\n const value = obj[fieldName];\n if (typeof value === 'string' && isEncryptedFormat(value)) {\n try {\n obj[fieldName] = decrypt(value, encryptionKey);\n } catch {\n // If decryption fails, leave the value as-is (may not be encrypted)\n }\n }\n }\n // Strip blind index fields from the result\n return stripBlindIndexFields(obj, modelConfig);\n }\n\n return result;\n}\n\n/**\n * Creates a Prisma Client Extension that transparently implements blind indexing\n * for the configured fields.\n *\n * **Usage:**\n * ```ts\n * import { PrismaClient } from '@prisma/client';\n * import { withBlindIndexer } from 'orm-blind-indexer';\n *\n * const prisma = new PrismaClient().$extends(\n * withBlindIndexer({\n * encryptionKey: process.env.ENCRYPTION_KEY!,\n * indexKey: process.env.INDEX_KEY!,\n * fields: {\n * User: {\n * email: {\n * indexFieldName: 'email_bidx',\n * caseSensitive: false,\n * },\n * },\n * },\n * })\n * );\n * ```\n *\n * @param config - The blind indexer configuration object.\n * @returns A Prisma Client Extension object to pass to `.$extends()`.\n * @throws {Error} At initialization time if the keys are invalid.\n */\nexport function withBlindIndexer(config: BlindIndexConfig) {\n // Validate keys eagerly at initialization time for fast failure\n const encryptionKey = validateEncryptionKey(config.encryptionKey);\n const indexKey = validateIndexKey(config.indexKey);\n\n return Prisma.defineExtension({\n name: 'orm-blind-indexer',\n\n query: {\n $allModels: {\n async $allOperations({ model, operation, args, query }) {\n // Get the field config for the current model (if any)\n const modelConfig: ModelFieldConfig | undefined =\n model !== undefined ? config.fields[model] : undefined;\n\n // If no config for this model, pass through unchanged\n if (modelConfig === undefined || Object.keys(modelConfig).length === 0) {\n return query(args);\n }\n\n const typedArgs = args as Record<string, unknown>;\n\n // ----------------------------------------------------------------\n // WRITE OPERATIONS: encrypt data + inject blind indexes\n // ----------------------------------------------------------------\n if (WRITE_OPERATIONS.has(operation)) {\n if (operation === 'upsert') {\n // upsert has both `create` and `update` data objects\n if (typedArgs['create'] && typeof typedArgs['create'] === 'object') {\n typedArgs['create'] = processWriteData(\n typedArgs['create'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n if (typedArgs['update'] && typeof typedArgs['update'] === 'object') {\n typedArgs['update'] = processWriteData(\n typedArgs['update'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n } else if (operation === 'createMany') {\n // createMany has an array of data objects\n if (Array.isArray(typedArgs['data'])) {\n typedArgs['data'] = (typedArgs['data'] as Record<string, unknown>[]).map(\n (item) => processWriteData(item, modelConfig, encryptionKey, indexKey)\n );\n }\n } else {\n // create, update, updateMany\n if (typedArgs['data'] && typeof typedArgs['data'] === 'object') {\n typedArgs['data'] = processWriteData(\n typedArgs['data'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n }\n\n // Also transform where clause for update/delete operations\n if (typedArgs['where'] && typeof typedArgs['where'] === 'object') {\n typedArgs['where'] = transformWhereClause(\n typedArgs['where'] as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n }\n\n // ----------------------------------------------------------------\n // READ OPERATIONS: transform where clause\n // ----------------------------------------------------------------\n if (READ_OPERATIONS.has(operation)) {\n if (typedArgs['where'] && typeof typedArgs['where'] === 'object') {\n typedArgs['where'] = transformWhereClause(\n typedArgs['where'] as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n }\n\n // Execute the (possibly mutated) query\n const result = await query(args);\n\n // ----------------------------------------------------------------\n // POST-PROCESS: decrypt results + strip blind index fields\n // ----------------------------------------------------------------\n // Don't post-process count/aggregate (they return numbers, not records)\n if (operation === 'count' || operation === 'aggregate' || operation === 'groupBy') {\n return result;\n }\n\n return processResult(result, modelConfig, encryptionKey);\n },\n },\n },\n });\n}\n\n","/**\n * @file crypto/encryptor.ts\n * @description AES-256-GCM authenticated encryption and decryption.\n *\n * ## Why AES-256-GCM?\n *\n * AES-256-GCM (Galois/Counter Mode) is an authenticated encryption with\n * associated data (AEAD) cipher. It provides:\n *\n * 1. **Confidentiality**: The ciphertext reveals nothing about the plaintext.\n * 2. **Integrity & Authenticity**: The 16-byte authentication tag (Auth Tag)\n * cryptographically proves the ciphertext has not been tampered with.\n * Decryption fails with an error if the tag doesn't verify.\n * 3. **No padding oracle**: GCM is a stream-cipher mode — no PKCS#7 padding\n * means no padding oracle attacks.\n *\n * ## On-disk format\n *\n * We serialize the IV, Auth Tag, and ciphertext as three colon-separated\n * base64 segments: `base64(iv):base64(authTag):base64(ciphertext)`\n *\n * This is a self-contained format: everything needed to decrypt the value\n * lives in the value itself. The key is stored separately (environment variable).\n *\n * ## IV Randomness\n *\n * A fresh 12-byte random IV is generated for **every** encryption call using\n * `crypto.randomBytes()`. This is critical: reusing an IV with the same key\n * in GCM mode completely breaks confidentiality (two-time pad attack).\n * With a 12-byte IV, the probability of a collision is negligible for\n * reasonable database sizes (birthday bound: ~2^48 operations for 2^-32 risk).\n *\n * ## Auth Tag Length\n *\n * We use the full 16-byte (128-bit) authentication tag, which is the\n * maximum and recommended length. Shorter tags reduce forgery resistance.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes } from 'crypto';\nimport type { ParsedCiphertext } from '../types/index.js';\n\n/** AES-256-GCM algorithm identifier used by Node.js crypto. */\nconst ALGORITHM = 'aes-256-gcm' as const;\n\n/** Length of the random Initialization Vector in bytes. NIST recommends 96 bits (12 bytes) for GCM. */\nconst IV_LENGTH_BYTES = 12;\n\n/** Length of the GCM authentication tag in bytes. 128 bits (16 bytes) is the maximum and recommended. */\nconst AUTH_TAG_LENGTH_BYTES = 16;\n\n/** Delimiter used to separate the IV, Auth Tag, and ciphertext in the serialized format. */\nconst DELIMITER = ':';\n\n/**\n * Encrypts a UTF-8 plaintext string using AES-256-GCM.\n *\n * @param plaintext - The plaintext string to encrypt (e.g., an email address).\n * @param key - A 32-byte Buffer containing the AES-256 encryption key.\n * @returns A serialized ciphertext string: `base64(iv):base64(authTag):base64(ciphertext)`.\n * @throws {Error} If encryption fails (e.g., invalid key length).\n *\n * @example\n * ```ts\n * const encrypted = encrypt('user@example.com', encryptionKeyBuffer);\n * // => \"ABC123...:XYZ789...:cipher...\"\n * ```\n */\nexport function encrypt(plaintext: string, key: Buffer): string {\n // Generate a fresh random 12-byte IV for every encryption\n const iv = randomBytes(IV_LENGTH_BYTES);\n\n // Create the cipher. Node.js validates the key length here and throws\n // if it doesn't match the algorithm's requirement (32 bytes for AES-256).\n const cipher = createCipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH_BYTES,\n });\n\n // Encrypt the plaintext. The update() call does the actual encryption;\n // final() flushes any remaining bytes (empty for a stream cipher like GCM).\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n\n // Retrieve the GCM authentication tag AFTER calling final()\n const authTag = cipher.getAuthTag();\n\n // Serialize: base64(iv) : base64(authTag) : base64(ciphertext)\n return [\n iv.toString('base64'),\n authTag.toString('base64'),\n encrypted.toString('base64'),\n ].join(DELIMITER);\n}\n\n/**\n * Parses a serialized AES-256-GCM ciphertext string into its components.\n *\n * @param cipherText - The serialized string in `base64(iv):base64(authTag):base64(ct)` format.\n * @returns A `ParsedCiphertext` object with Buffer components.\n * @throws {Error} If the string format is invalid or any component has an unexpected length.\n */\nfunction parseCiphertext(cipherText: string): ParsedCiphertext {\n const parts = cipherText.split(DELIMITER);\n if (parts.length !== 3) {\n throw new Error(\n `[orm-blind-indexer] Invalid ciphertext format. ` +\n `Expected \"base64(iv):base64(authTag):base64(ciphertext)\" with exactly 2 colons, ` +\n `but got ${parts.length - 1} colons. ` +\n `The value may not be encrypted or may be corrupted.`\n );\n }\n\n const [ivB64, authTagB64, ciphertextB64] = parts as [string, string, string];\n\n const iv = Buffer.from(ivB64, 'base64');\n const authTag = Buffer.from(authTagB64, 'base64');\n const ciphertext = Buffer.from(ciphertextB64, 'base64');\n\n if (iv.byteLength !== IV_LENGTH_BYTES) {\n throw new Error(\n `[orm-blind-indexer] Invalid IV length in ciphertext. ` +\n `Expected ${IV_LENGTH_BYTES} bytes, got ${iv.byteLength} bytes.`\n );\n }\n\n if (authTag.byteLength !== AUTH_TAG_LENGTH_BYTES) {\n throw new Error(\n `[orm-blind-indexer] Invalid Auth Tag length in ciphertext. ` +\n `Expected ${AUTH_TAG_LENGTH_BYTES} bytes, got ${authTag.byteLength} bytes.`\n );\n }\n\n return { iv, authTag, ciphertext };\n}\n\n/**\n * Decrypts a serialized AES-256-GCM ciphertext string back to UTF-8 plaintext.\n *\n * The decryption process automatically verifies the GCM authentication tag.\n * If the ciphertext or key has been tampered with, decryption will throw an error\n * (\"Unsupported state or unable to authenticate data\") before any plaintext is returned.\n *\n * @param cipherText - The serialized ciphertext string from `encrypt()`.\n * @param key - The same 32-byte Buffer used during encryption.\n * @returns The original UTF-8 plaintext string.\n * @throws {Error} If the ciphertext format is invalid, the key is wrong,\n * or the authentication tag verification fails (indicating tampering).\n *\n * @example\n * ```ts\n * const plaintext = decrypt(encryptedValue, encryptionKeyBuffer);\n * // => \"user@example.com\"\n * ```\n */\nexport function decrypt(cipherText: string, key: Buffer): string {\n const { iv, authTag, ciphertext } = parseCiphertext(cipherText);\n\n const decipher = createDecipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH_BYTES,\n });\n\n // Set the authentication tag BEFORE decrypting.\n // The decipher will verify this tag during final(), throwing if it doesn't match.\n decipher.setAuthTag(authTag);\n\n // Decrypt the ciphertext. final() triggers the auth tag verification.\n const decrypted = Buffer.concat([\n decipher.update(ciphertext),\n decipher.final(), // Throws if auth tag verification fails\n ]);\n\n return decrypted.toString('utf8');\n}\n\n/**\n * Checks if a string appears to be in the encrypted format produced by `encrypt()`.\n * Used to avoid double-encrypting values that are already encrypted.\n *\n * This is a heuristic check based on format, not cryptographic verification.\n *\n * @param value - The string to check.\n * @returns `true` if the string matches the `base64:base64:base64` pattern.\n */\nexport function isEncryptedFormat(value: string): boolean {\n const parts = value.split(DELIMITER);\n if (parts.length !== 3) return false;\n\n // Check that each part is non-empty and looks like base64\n const base64Pattern = /^[A-Za-z0-9+/]+=*$/;\n return parts.every(\n (part): boolean => typeof part === 'string' && part.length > 0 && base64Pattern.test(part)\n );\n}\n\n","/**\n * @file crypto/hasher.ts\n * @description HMAC-SHA256 blind index generation.\n *\n * ## What is a Blind Index?\n *\n * A blind index solves the problem of querying encrypted data. Because AES-256-GCM\n * uses a random IV on every encryption, the same plaintext produces a different\n * ciphertext each time. This makes exact-match database queries impossible on\n * the encrypted column.\n *\n * A blind index is a **deterministic** hash of the plaintext that:\n * - Always produces the same output for the same input (queryable)\n * - Reveals no information about the plaintext without the key (private)\n * - Is stored in a separate `_bidx` column alongside the encrypted data\n *\n * ## Why HMAC-SHA256 (not plain SHA256)?\n *\n * Plain SHA256 is a one-way function, but it has no secret. An attacker who\n * gains read access to your database could mount a **dictionary attack**:\n * they hash every common email address and look for matches.\n *\n * HMAC-SHA256 incorporates a **secret key** (the `indexKey`). Without knowing\n * the key, an attacker cannot compute the hash, so the blind index leaks no\n * information about the plaintext even if the database is fully compromised.\n *\n * ## Case Sensitivity\n *\n * Email addresses and usernames are often compared case-insensitively.\n * When `caseSensitive: false` (the default), we lowercase the input before\n * hashing. This ensures that \"Test@Test.com\" and \"test@test.com\" produce the\n * same blind index, enabling case-insensitive equality searches.\n *\n * **Important**: The case normalization must be applied consistently on both\n * write (when storing) and read (when querying). The extension handles this.\n */\n\nimport { createHmac } from 'crypto';\n\n/** HMAC digest algorithm — SHA-256 produces a 32-byte (256-bit) output. */\nconst HMAC_ALGORITHM = 'sha256' as const;\n\n/**\n * Generates a deterministic HMAC-SHA256 blind index for a given plaintext.\n *\n * The output is a base64-encoded 32-byte HMAC digest. Base64 is used (rather\n * than hex) for compactness: 44 characters vs 64 characters for the same value.\n *\n * @param text - The plaintext value to hash (e.g., an email address).\n * @param indexKey - A secret Buffer used as the HMAC key. Must be kept private.\n * @param caseSensitive - When `false`, `text` is lowercased before hashing.\n * Defaults to `false` for case-insensitive equality searches.\n * @param truncateBytes - Optional byte length (8 to 32) to truncate the HMAC\n * digest before base64 encoding. Useful for frequency-analysis defense.\n * @returns A base64-encoded HMAC-SHA256 digest string.\n *\n * @example\n * ```ts\n * const indexKey = Buffer.from(process.env.INDEX_KEY!, 'base64');\n *\n * // Case-insensitive (default): both produce the same hash\n * generateBlindIndex('Test@Test.com', indexKey, false);\n * generateBlindIndex('test@test.com', indexKey, false);\n *\n * // Case-sensitive: these produce different hashes\n * generateBlindIndex('Test@Test.com', indexKey, true);\n * generateBlindIndex('test@test.com', indexKey, true);\n *\n * // Truncated blind index (e.g., 16 bytes for frequency defense)\n * generateBlindIndex('user@example.com', indexKey, false, 16);\n * ```\n */\nexport function generateBlindIndex(\n text: string,\n indexKey: Buffer,\n caseSensitive: boolean = false,\n truncateBytes?: number\n): string {\n // Normalize to lowercase for case-insensitive lookups\n const normalized = caseSensitive ? text : text.toLowerCase();\n\n const digest = createHmac(HMAC_ALGORITHM, indexKey)\n .update(normalized, 'utf8')\n .digest();\n\n if (truncateBytes !== undefined) {\n if (truncateBytes < 8 || truncateBytes > 32) {\n throw new Error(\n `[orm-blind-indexer] \"truncateBytes\" must be between 8 and 32 bytes, got ${truncateBytes}.`\n );\n }\n return digest.subarray(0, truncateBytes).toString('base64');\n }\n\n return digest.toString('base64');\n}\n\n/**\n * Generates blind indexes for an array of plaintext values.\n * Useful for `{ in: [...] }` and `{ notIn: [...] }` Prisma query operators.\n *\n * @param values - An array of plaintext strings to hash.\n * @param indexKey - The secret HMAC key Buffer.\n * @param caseSensitive - When `false`, each value is lowercased before hashing.\n * @param truncateBytes - Optional byte length (8 to 32) to truncate each digest.\n * @returns An array of base64-encoded HMAC-SHA256 digests, in the same order as `values`.\n */\nexport function generateBlindIndexBatch(\n values: string[],\n indexKey: Buffer,\n caseSensitive: boolean = false,\n truncateBytes?: number\n): string[] {\n return values.map((value) => generateBlindIndex(value, indexKey, caseSensitive, truncateBytes));\n}\n\n","/**\n * @file crypto/utils.ts\n * @description Key validation and parsing utilities for cryptographic operations.\n *\n * Security Design Notes:\n * - AES-256 requires a 256-bit (32-byte) key. Any deviation is a hard error.\n * - Keys may be provided as base64 or hex strings for safe environment variable storage.\n * - We validate at extension initialization time, not at query time, to fail fast.\n */\n\n/**\n * Attempts to parse a string as a base64-encoded Buffer.\n * Returns `null` if the string is not valid base64.\n */\nfunction tryParseBase64(value: string): Buffer | null {\n try {\n const buf = Buffer.from(value, 'base64');\n // Verify round-trip: if re-encoding doesn't match, the original wasn't valid base64\n if (buf.toString('base64') === value || buf.toString('base64url') === value) {\n return buf;\n }\n // Allow base64 with padding stripped or standard base64\n // A valid base64 string of length L decodes to floor(L*3/4) bytes\n // We just trust the Buffer decode here for practical use\n return buf.byteLength > 0 ? buf : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Attempts to parse a string as a hex-encoded Buffer.\n * Returns `null` if the string is not a valid even-length hex string.\n */\nfunction tryParseHex(value: string): Buffer | null {\n if (!/^[0-9a-fA-F]+$/.test(value) || value.length % 2 !== 0) {\n return null;\n }\n return Buffer.from(value, 'hex');\n}\n\n/**\n * Parses a key string (base64 or hex) into a Buffer and validates its byte length.\n *\n * The function tries hex first (unambiguous format), then base64.\n *\n * @param keyString - The raw key string from environment variables or config.\n * @param expectedBytes - The required byte length of the decoded key.\n * @param keyName - A descriptive name for the key, used in error messages.\n * @returns A `Buffer` of exactly `expectedBytes` bytes.\n * @throws {Error} If the key cannot be parsed or has an incorrect length.\n *\n * @example\n * ```ts\n * const key = parseAndValidateKey(process.env.ENCRYPTION_KEY!, 32, 'encryptionKey');\n * ```\n */\nexport function parseAndValidateKey(\n keyString: string,\n expectedBytes: number,\n keyName: string\n): Buffer {\n if (!keyString || typeof keyString !== 'string') {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" must be a non-empty string. ` +\n `Generate one with: node -e \"console.log(require('crypto').randomBytes(${expectedBytes}).toString('base64'))\"`\n );\n }\n\n // Try hex first (unambiguous — only hex chars, even length)\n const hexBuf = tryParseHex(keyString);\n if (hexBuf !== null) {\n if (hexBuf.byteLength !== expectedBytes) {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" decoded to ${hexBuf.byteLength} bytes from hex, ` +\n `but ${expectedBytes} bytes are required. ` +\n `A ${expectedBytes}-byte hex key must be ${expectedBytes * 2} hex characters.`\n );\n }\n return hexBuf;\n }\n\n // Try base64\n const b64Buf = tryParseBase64(keyString);\n if (b64Buf !== null) {\n if (b64Buf.byteLength !== expectedBytes) {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" decoded to ${b64Buf.byteLength} bytes from base64, ` +\n `but ${expectedBytes} bytes are required. ` +\n `Generate a valid key with: node -e \"console.log(require('crypto').randomBytes(${expectedBytes}).toString('base64'))\"`\n );\n }\n return b64Buf;\n }\n\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" could not be parsed as hex or base64. ` +\n `Provide a ${expectedBytes * 2}-char hex string or a ${Math.ceil(expectedBytes * 4 / 3)}-char base64 string.`\n );\n}\n\n/**\n * Validates an encryption key for AES-256 (requires exactly 32 bytes / 256 bits).\n *\n * @param keyString - The raw encryption key string from config.\n * @returns A `Buffer` of exactly 32 bytes suitable for AES-256-GCM.\n * @throws {Error} If the key is invalid or not exactly 32 bytes.\n */\nexport function validateEncryptionKey(keyString: string): Buffer {\n return parseAndValidateKey(keyString, 32, 'encryptionKey');\n}\n\n/**\n * Parses the index key (HMAC salt) from a string.\n * Unlike the encryption key, the HMAC key has no strict length requirement in the\n * cryptographic spec (SHA-256 accepts any length), but we enforce a minimum of 16 bytes\n * for security best-practice guidance.\n *\n * @param keyString - The raw index key string from config.\n * @returns A `Buffer` usable as an HMAC key.\n * @throws {Error} If the key cannot be parsed or is shorter than 16 bytes.\n */\nexport function validateIndexKey(keyString: string): Buffer {\n if (!keyString || typeof keyString !== 'string') {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" must be a non-empty string. ` +\n `Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"`\n );\n }\n\n // Try hex\n const hexBuf = tryParseHex(keyString);\n if (hexBuf !== null) {\n if (hexBuf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${hexBuf.byteLength} bytes). ` +\n `Minimum recommended length is 16 bytes (32 hex chars).`\n );\n }\n return hexBuf;\n }\n\n // Try base64\n const b64Buf = tryParseBase64(keyString);\n if (b64Buf !== null) {\n if (b64Buf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${b64Buf.byteLength} bytes). ` +\n `Minimum recommended length is 16 bytes.`\n );\n }\n return b64Buf;\n }\n\n // Fall back to treating it as a raw UTF-8 string key (not recommended but functional)\n const rawBuf = Buffer.from(keyString, 'utf8');\n if (rawBuf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${rawBuf.byteLength} bytes as UTF-8). ` +\n `Minimum recommended length is 16 bytes.`\n );\n }\n return rawBuf;\n}\n\n","/**\n * @file extension/interceptors.ts\n * @description Recursive WHERE clause transformation for blind index substitution.\n *\n * ## The Problem\n *\n * Prisma's `where` clause is a deeply nested object that can contain:\n * - Direct equality: `{ email: \"user@example.com\" }`\n * - Operator objects: `{ email: { in: [...], equals: \"...\" } }`\n * - Logical combinators: `{ AND: [...], OR: [...], NOT: { ... } }`\n * - Nested relations: `{ posts: { some: { author: { email: \"...\" } } } }`\n *\n * We need to intercept any occurrence of an encrypted field name and replace it\n * with a blind index lookup, regardless of how deeply nested it is.\n *\n * ## Strategy\n *\n * We use a **recursive depth-first traversal** of the `where` object:\n *\n * 1. For each key in the current object, check if it's a logical combinator\n * (`AND`, `OR`, `NOT`). If so, recurse into its children.\n * 2. If the key matches a configured encrypted field, transform the value:\n * - Remove the original field key.\n * - Add the `indexFieldName` key with the hashed value.\n * 3. Handle the `in` operator specially: hash each array element.\n *\n * ## Supported Prisma Filter Operators (on encrypted fields)\n *\n * | Prisma Operator | Supported | Notes |\n * |----------------|-----------|------------------------------------------|\n * | `equals` | ✅ | Direct value or `{ equals: value }` |\n * | `in` | ✅ | Hashes each element of the array |\n * | (direct) | ✅ | `{ email: \"value\" }` shorthand |\n * | `contains` | ❌ | LIKE queries incompatible with hashing |\n * | `startsWith` | ❌ | LIKE queries incompatible with hashing |\n * | `endsWith` | ❌ | LIKE queries incompatible with hashing |\n * | `lt/gt/lte/gte`| ❌ | Range queries incompatible with hashing |\n *\n * Attempting to use unsupported operators on encrypted fields will throw an error\n * guiding the developer to the correct approach.\n */\n\nimport { generateBlindIndex, generateBlindIndexBatch } from '../crypto/hasher.js';\nimport type { ModelFieldConfig } from '../types/index.js';\n\n/** Prisma logical combinator keys that contain nested where clause arrays or objects. */\nconst LOGICAL_COMBINATORS = new Set(['AND', 'OR', 'NOT']);\n\n/** Prisma filter operators that are incompatible with blind indexing. */\nconst UNSUPPORTED_OPERATORS = new Set([\n 'contains',\n 'startsWith',\n 'endsWith',\n 'lt',\n 'lte',\n 'gt',\n 'gte',\n 'search',\n]);\n\n/**\n * Transforms a Prisma `where` clause, replacing encrypted field lookups with\n * their corresponding blind index field lookups.\n *\n * This function is **pure** (it returns a new object, not mutating the original)\n * and **recursive** (it handles arbitrarily nested `AND`/`OR`/`NOT` clauses).\n *\n * @param where - The Prisma where clause object to transform.\n * @param modelConfig - The field configuration for the current model.\n * @param indexKey - The HMAC key Buffer for generating blind indexes.\n * @returns A new where clause object with encrypted fields replaced by blind index fields.\n * @throws {Error} If an unsupported operator is used on an encrypted field.\n */\nexport function transformWhereClause(\n where: Record<string, unknown>,\n modelConfig: ModelFieldConfig,\n indexKey: Buffer\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(where)) {\n // --- Handle logical combinators: AND, OR, NOT ---\n if (LOGICAL_COMBINATORS.has(key)) {\n if (Array.isArray(value)) {\n // AND: [...], OR: [...]\n result[key] = value.map((item) => {\n if (item !== null && typeof item === 'object' && !Array.isArray(item)) {\n return transformWhereClause(\n item as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n return item;\n });\n } else if (value !== null && typeof value === 'object') {\n // NOT: { ... }\n result[key] = transformWhereClause(\n value as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n } else {\n result[key] = value;\n }\n continue;\n }\n\n // --- Handle encrypted fields ---\n const fieldConfig = modelConfig[key];\n if (fieldConfig !== undefined) {\n const { indexFieldName, caseSensitive = false, truncateBytes } = fieldConfig;\n\n // Value is a plain string: `{ email: \"user@example.com\" }` shorthand\n if (typeof value === 'string') {\n result[indexFieldName] = generateBlindIndex(value, indexKey, caseSensitive, truncateBytes);\n continue;\n }\n\n // Value is null: pass through (querying for null encrypted fields)\n if (value === null) {\n result[indexFieldName] = null;\n continue;\n }\n\n // Value is an operator object: `{ email: { in: [...], equals: \"...\" } }`\n if (typeof value === 'object' && !Array.isArray(value)) {\n const operatorObj = value as Record<string, unknown>;\n const transformedOperator: Record<string, unknown> = {};\n\n for (const [op, opValue] of Object.entries(operatorObj)) {\n // Check for unsupported operators\n if (UNSUPPORTED_OPERATORS.has(op)) {\n throw new Error(\n `[orm-blind-indexer] The \"${op}\" filter operator is not supported on encrypted ` +\n `field \"${key}\". Blind indexes only support exact-match lookups (equals, in, notIn). ` +\n `Partial string searches on encrypted fields require application-level decryption.`\n );\n }\n\n // Handle Prisma's mode: \"insensitive\" / \"default\"\n if (op === 'mode') {\n if (opValue === 'insensitive' && caseSensitive) {\n throw new Error(\n `[orm-blind-indexer] \"mode: 'insensitive'\" cannot be used on encrypted field \"${key}\" ` +\n `configured with \"caseSensitive: true\". Change the field configuration to \"caseSensitive: false\" ` +\n `for case-insensitive blind index lookups.`\n );\n }\n // If caseSensitive is false, case insensitivity is inherently handled\n // during HMAC generation. We omit `mode` from the transformed query.\n continue;\n }\n\n if (op === 'equals') {\n if (typeof opValue === 'string') {\n transformedOperator['equals'] = generateBlindIndex(opValue, indexKey, caseSensitive, truncateBytes);\n } else if (opValue === null) {\n transformedOperator['equals'] = null;\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"equals\" operator on encrypted field \"${key}\" ` +\n `expects a string value, got ${typeof opValue}.`\n );\n }\n } else if (op === 'in') {\n if (Array.isArray(opValue)) {\n const strings = opValue.filter((v): v is string => typeof v === 'string');\n if (strings.length !== opValue.length) {\n throw new Error(\n `[orm-blind-indexer] The \"in\" operator on encrypted field \"${key}\" ` +\n `requires all values to be strings.`\n );\n }\n transformedOperator['in'] = generateBlindIndexBatch(strings, indexKey, caseSensitive, truncateBytes);\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"in\" operator on encrypted field \"${key}\" ` +\n `expects an array, got ${typeof opValue}.`\n );\n }\n } else if (op === 'notIn') {\n if (Array.isArray(opValue)) {\n const strings = opValue.filter((v): v is string => typeof v === 'string');\n if (strings.length !== opValue.length) {\n throw new Error(\n `[orm-blind-indexer] The \"notIn\" operator on encrypted field \"${key}\" ` +\n `requires all values to be strings.`\n );\n }\n transformedOperator['notIn'] = generateBlindIndexBatch(strings, indexKey, caseSensitive, truncateBytes);\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"notIn\" operator on encrypted field \"${key}\" ` +\n `expects an array, got ${typeof opValue}.`\n );\n }\n } else if (op === 'not') {\n // `NOT` as a filter operator (different from `NOT` combinator)\n if (typeof opValue === 'string') {\n transformedOperator['not'] = generateBlindIndex(opValue, indexKey, caseSensitive, truncateBytes);\n } else if (opValue === null) {\n transformedOperator['not'] = null;\n } else {\n transformedOperator['not'] = opValue;\n }\n } else {\n // Pass through unknown operators (future Prisma operators)\n transformedOperator[op] = opValue;\n }\n }\n\n result[indexFieldName] = transformedOperator;\n continue;\n }\n\n // Fallback: pass through as-is (handles edge cases)\n result[key] = value;\n continue;\n }\n\n // --- Non-encrypted field: pass through unchanged ---\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Strips blind index fields (`_bidx` columns) from a query result object\n * so the caller only sees the logical schema (without implementation details).\n *\n * This function also handles arrays of results (for `findMany`) and\n * recursively cleans nested relation objects.\n *\n * @param result - The raw result from Prisma (single object or array).\n * @param modelConfig - The field configuration for the current model.\n * @returns The cleaned result with `_bidx` fields removed.\n */\nexport function stripBlindIndexFields(\n result: unknown,\n modelConfig: ModelFieldConfig\n): unknown {\n if (result === null || result === undefined) {\n return result;\n }\n\n if (Array.isArray(result)) {\n return result.map((item) => stripBlindIndexFields(item, modelConfig));\n }\n\n if (typeof result === 'object') {\n const obj = result as Record<string, unknown>;\n const cleaned: Record<string, unknown> = {};\n\n // Collect all bidx field names to exclude\n const bidxFieldNames = new Set(\n Object.values(modelConfig).map((cfg) => cfg.indexFieldName)\n );\n\n for (const [key, value] of Object.entries(obj)) {\n if (!bidxFieldNames.has(key)) {\n // Recursively clean nested objects (for relations)\n if (value !== null && typeof value === 'object') {\n cleaned[key] = stripBlindIndexFields(value, modelConfig);\n } else {\n cleaned[key] = value;\n }\n }\n // Skip bidx fields — don't include them in output\n }\n\n return cleaned;\n }\n\n return result;\n}\n\n"],"mappings":"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","names":["import_crypto"]}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/extension/prisma.ts
2
- import { Prisma } from "@prisma/client";
2
+ import { Prisma } from "@prisma/client/extension";
3
3
 
4
4
  // src/crypto/encryptor.ts
5
5
  import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/extension/prisma.ts","../src/crypto/encryptor.ts","../src/crypto/hasher.ts","../src/crypto/utils.ts","../src/extension/interceptors.ts"],"sourcesContent":["/**\n * @file extension/prisma.ts\n * @description The core Prisma Client Extension implementing the blind indexing middleware.\n *\n * ## How Prisma Client Extensions Work\n *\n * Prisma Client Extensions (introduced in Prisma 4.7) allow augmenting the Prisma\n * client with custom behavior. We use the `query` extension component, which\n * intercepts all database operations before and after they execute.\n *\n * The `query.$allModels.$allOperations` hook receives:\n * - `model`: The Prisma model name (e.g., \"User\")\n * - `operation`: The operation name (e.g., \"findUnique\", \"create\")\n * - `args`: The operation arguments (where clause, data, etc.) — MUTATED before passing to `next()`\n * - `query`: The `next` function to call to execute the actual query\n *\n * ## Lifecycle of a Write Operation (create/update/upsert)\n *\n * 1. Developer calls: `prisma.user.create({ data: { email: \"user@example.com\" } })`\n * 2. Extension intercepts `args.data`\n * 3. For each configured encrypted field found in `data`:\n * a. Encrypt the plaintext with AES-256-GCM → `email` becomes the ciphertext\n * b. Generate HMAC-SHA256 blind index → inject as `email_bidx`\n * 4. The modified `args` are passed to `next()` → Prisma writes both columns\n * 5. The result is post-processed: `email` is decrypted back, `email_bidx` is stripped\n *\n * ## Lifecycle of a Read Operation (findUnique/findMany/findFirst)\n *\n * 1. Developer calls: `prisma.user.findUnique({ where: { email: \"user@example.com\" } })`\n * 2. Extension intercepts `args.where`\n * 3. `transformWhereClause()` replaces `email` with `email_bidx: hash(\"user@example.com\")`\n * 4. The modified `args` are passed to `next()` → Prisma queries the blind index column\n * 5. The result is post-processed: `email` is decrypted, `email_bidx` is stripped\n *\n * ## Write Operations Supported\n *\n * | Operation | Data Location | Supported |\n * |--------------------|--------------------|-----------|\n * | create | args.data | ✅ |\n * | update | args.data | ✅ |\n * | upsert | args.create + args.update | ✅ |\n * | createMany | args.data (array) | ✅ |\n * | updateMany | args.data | ✅ |\n *\n * ## Read Operations Supported\n *\n * | Operation | Where Location | Supported |\n * |--------------------|-------------------|-----------|\n * | findUnique | args.where | ✅ |\n * | findUniqueOrThrow | args.where | ✅ |\n * | findFirst | args.where | ✅ |\n * | findFirstOrThrow | args.where | ✅ |\n * | findMany | args.where | ✅ |\n * | count | args.where | ✅ |\n * | aggregate | args.where | ✅ |\n * | delete | args.where | ✅ |\n * | deleteMany | args.where | ✅ |\n */\n\nimport { Prisma } from '@prisma/client';\nimport { encrypt, decrypt, isEncryptedFormat } from '../crypto/encryptor.js';\nimport { generateBlindIndex } from '../crypto/hasher.js';\nimport { validateEncryptionKey, validateIndexKey } from '../crypto/utils.js';\nimport { transformWhereClause, stripBlindIndexFields } from './interceptors.js';\nimport type { BlindIndexConfig, ModelFieldConfig } from '../types/index.js';\n\n/** Operations that write data and require encrypt + blind-index injection */\nconst WRITE_OPERATIONS = new Set([\n 'create',\n 'update',\n 'upsert',\n 'createMany',\n 'updateMany',\n]);\n\n/** Operations that read data and require where-clause transformation */\nconst READ_OPERATIONS = new Set([\n 'findUnique',\n 'findUniqueOrThrow',\n 'findFirst',\n 'findFirstOrThrow',\n 'findMany',\n 'count',\n 'aggregate',\n 'groupBy',\n 'delete',\n 'deleteMany',\n]);\n\n/**\n * Processes a single data object (from create/update `data`), encrypting\n * configured plaintext fields and injecting their blind index counterparts.\n *\n * @param data - The raw data object from the Prisma args.\n * @param modelConfig - Field configuration for the current model.\n * @param encryptionKey - AES-256-GCM key Buffer.\n * @param indexKey - HMAC-SHA256 key Buffer.\n * @returns A new data object with encrypted values and blind index fields injected.\n */\nfunction processWriteData(\n data: Record<string, unknown>,\n modelConfig: ModelFieldConfig,\n encryptionKey: Buffer,\n indexKey: Buffer\n): Record<string, unknown> {\n const result = { ...data };\n\n for (const [fieldName, fieldConfig] of Object.entries(modelConfig)) {\n const value = result[fieldName];\n\n if (value === undefined || value === null) {\n // Field not present in this write, or explicitly null — skip\n continue;\n }\n\n if (typeof value !== 'string') {\n // Non-string values (e.g. Prisma `set` wrappers) — skip for safety\n continue;\n }\n\n // Guard against double-encryption if the value is already in encrypted format\n if (isEncryptedFormat(value)) {\n continue;\n }\n\n // 1. Encrypt the plaintext value\n result[fieldName] = encrypt(value, encryptionKey);\n\n // 2. Generate and inject the blind index\n result[fieldConfig.indexFieldName] = generateBlindIndex(\n value,\n indexKey,\n fieldConfig.caseSensitive ?? false\n );\n }\n\n return result;\n}\n\n/**\n * Post-processes a Prisma result, decrypting encrypted fields and removing\n * blind index fields from the output.\n *\n * @param result - The raw result from Prisma.\n * @param modelConfig - Field configuration for the current model.\n * @param encryptionKey - AES-256-GCM key Buffer for decryption.\n * @returns The cleaned result with decrypted plaintext and no `_bidx` fields.\n */\nfunction processResult(\n result: unknown,\n modelConfig: ModelFieldConfig,\n encryptionKey: Buffer\n): unknown {\n if (result === null || result === undefined) {\n return result;\n }\n\n if (Array.isArray(result)) {\n return result.map((item) => processResult(item, modelConfig, encryptionKey));\n }\n\n if (typeof result === 'object') {\n const obj = result as Record<string, unknown>;\n // Decrypt each configured field\n for (const [fieldName] of Object.entries(modelConfig)) {\n const value = obj[fieldName];\n if (typeof value === 'string' && isEncryptedFormat(value)) {\n try {\n obj[fieldName] = decrypt(value, encryptionKey);\n } catch {\n // If decryption fails, leave the value as-is (may not be encrypted)\n }\n }\n }\n // Strip blind index fields from the result\n return stripBlindIndexFields(obj, modelConfig);\n }\n\n return result;\n}\n\n/**\n * Creates a Prisma Client Extension that transparently implements blind indexing\n * for the configured fields.\n *\n * **Usage:**\n * ```ts\n * import { PrismaClient } from '@prisma/client';\n * import { withBlindIndexer } from 'orm-blind-indexer';\n *\n * const prisma = new PrismaClient().$extends(\n * withBlindIndexer({\n * encryptionKey: process.env.ENCRYPTION_KEY!,\n * indexKey: process.env.INDEX_KEY!,\n * fields: {\n * User: {\n * email: {\n * indexFieldName: 'email_bidx',\n * caseSensitive: false,\n * },\n * },\n * },\n * })\n * );\n * ```\n *\n * @param config - The blind indexer configuration object.\n * @returns A Prisma Client Extension object to pass to `.$extends()`.\n * @throws {Error} At initialization time if the keys are invalid.\n */\nexport function withBlindIndexer(config: BlindIndexConfig) {\n // Validate keys eagerly at initialization time for fast failure\n const encryptionKey = validateEncryptionKey(config.encryptionKey);\n const indexKey = validateIndexKey(config.indexKey);\n\n return Prisma.defineExtension({\n name: 'orm-blind-indexer',\n\n query: {\n $allModels: {\n async $allOperations({ model, operation, args, query }) {\n // Get the field config for the current model (if any)\n const modelConfig: ModelFieldConfig | undefined =\n model !== undefined ? config.fields[model] : undefined;\n\n // If no config for this model, pass through unchanged\n if (modelConfig === undefined || Object.keys(modelConfig).length === 0) {\n return query(args);\n }\n\n const typedArgs = args as Record<string, unknown>;\n\n // ----------------------------------------------------------------\n // WRITE OPERATIONS: encrypt data + inject blind indexes\n // ----------------------------------------------------------------\n if (WRITE_OPERATIONS.has(operation)) {\n if (operation === 'upsert') {\n // upsert has both `create` and `update` data objects\n if (typedArgs['create'] && typeof typedArgs['create'] === 'object') {\n typedArgs['create'] = processWriteData(\n typedArgs['create'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n if (typedArgs['update'] && typeof typedArgs['update'] === 'object') {\n typedArgs['update'] = processWriteData(\n typedArgs['update'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n } else if (operation === 'createMany') {\n // createMany has an array of data objects\n if (Array.isArray(typedArgs['data'])) {\n typedArgs['data'] = (typedArgs['data'] as Record<string, unknown>[]).map(\n (item) => processWriteData(item, modelConfig, encryptionKey, indexKey)\n );\n }\n } else {\n // create, update, updateMany\n if (typedArgs['data'] && typeof typedArgs['data'] === 'object') {\n typedArgs['data'] = processWriteData(\n typedArgs['data'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n }\n\n // Also transform where clause for update/delete operations\n if (typedArgs['where'] && typeof typedArgs['where'] === 'object') {\n typedArgs['where'] = transformWhereClause(\n typedArgs['where'] as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n }\n\n // ----------------------------------------------------------------\n // READ OPERATIONS: transform where clause\n // ----------------------------------------------------------------\n if (READ_OPERATIONS.has(operation)) {\n if (typedArgs['where'] && typeof typedArgs['where'] === 'object') {\n typedArgs['where'] = transformWhereClause(\n typedArgs['where'] as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n }\n\n // Execute the (possibly mutated) query\n const result = await query(args);\n\n // ----------------------------------------------------------------\n // POST-PROCESS: decrypt results + strip blind index fields\n // ----------------------------------------------------------------\n // Don't post-process count/aggregate (they return numbers, not records)\n if (operation === 'count' || operation === 'aggregate' || operation === 'groupBy') {\n return result;\n }\n\n return processResult(result, modelConfig, encryptionKey);\n },\n },\n },\n });\n}\n\n","/**\n * @file crypto/encryptor.ts\n * @description AES-256-GCM authenticated encryption and decryption.\n *\n * ## Why AES-256-GCM?\n *\n * AES-256-GCM (Galois/Counter Mode) is an authenticated encryption with\n * associated data (AEAD) cipher. It provides:\n *\n * 1. **Confidentiality**: The ciphertext reveals nothing about the plaintext.\n * 2. **Integrity & Authenticity**: The 16-byte authentication tag (Auth Tag)\n * cryptographically proves the ciphertext has not been tampered with.\n * Decryption fails with an error if the tag doesn't verify.\n * 3. **No padding oracle**: GCM is a stream-cipher mode — no PKCS#7 padding\n * means no padding oracle attacks.\n *\n * ## On-disk format\n *\n * We serialize the IV, Auth Tag, and ciphertext as three colon-separated\n * base64 segments: `base64(iv):base64(authTag):base64(ciphertext)`\n *\n * This is a self-contained format: everything needed to decrypt the value\n * lives in the value itself. The key is stored separately (environment variable).\n *\n * ## IV Randomness\n *\n * A fresh 12-byte random IV is generated for **every** encryption call using\n * `crypto.randomBytes()`. This is critical: reusing an IV with the same key\n * in GCM mode completely breaks confidentiality (two-time pad attack).\n * With a 12-byte IV, the probability of a collision is negligible for\n * reasonable database sizes (birthday bound: ~2^48 operations for 2^-32 risk).\n *\n * ## Auth Tag Length\n *\n * We use the full 16-byte (128-bit) authentication tag, which is the\n * maximum and recommended length. Shorter tags reduce forgery resistance.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes } from 'crypto';\nimport type { ParsedCiphertext } from '../types/index.js';\n\n/** AES-256-GCM algorithm identifier used by Node.js crypto. */\nconst ALGORITHM = 'aes-256-gcm' as const;\n\n/** Length of the random Initialization Vector in bytes. NIST recommends 96 bits (12 bytes) for GCM. */\nconst IV_LENGTH_BYTES = 12;\n\n/** Length of the GCM authentication tag in bytes. 128 bits (16 bytes) is the maximum and recommended. */\nconst AUTH_TAG_LENGTH_BYTES = 16;\n\n/** Delimiter used to separate the IV, Auth Tag, and ciphertext in the serialized format. */\nconst DELIMITER = ':';\n\n/**\n * Encrypts a UTF-8 plaintext string using AES-256-GCM.\n *\n * @param plaintext - The plaintext string to encrypt (e.g., an email address).\n * @param key - A 32-byte Buffer containing the AES-256 encryption key.\n * @returns A serialized ciphertext string: `base64(iv):base64(authTag):base64(ciphertext)`.\n * @throws {Error} If encryption fails (e.g., invalid key length).\n *\n * @example\n * ```ts\n * const encrypted = encrypt('user@example.com', encryptionKeyBuffer);\n * // => \"ABC123...:XYZ789...:cipher...\"\n * ```\n */\nexport function encrypt(plaintext: string, key: Buffer): string {\n // Generate a fresh random 12-byte IV for every encryption\n const iv = randomBytes(IV_LENGTH_BYTES);\n\n // Create the cipher. Node.js validates the key length here and throws\n // if it doesn't match the algorithm's requirement (32 bytes for AES-256).\n const cipher = createCipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH_BYTES,\n });\n\n // Encrypt the plaintext. The update() call does the actual encryption;\n // final() flushes any remaining bytes (empty for a stream cipher like GCM).\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n\n // Retrieve the GCM authentication tag AFTER calling final()\n const authTag = cipher.getAuthTag();\n\n // Serialize: base64(iv) : base64(authTag) : base64(ciphertext)\n return [\n iv.toString('base64'),\n authTag.toString('base64'),\n encrypted.toString('base64'),\n ].join(DELIMITER);\n}\n\n/**\n * Parses a serialized AES-256-GCM ciphertext string into its components.\n *\n * @param cipherText - The serialized string in `base64(iv):base64(authTag):base64(ct)` format.\n * @returns A `ParsedCiphertext` object with Buffer components.\n * @throws {Error} If the string format is invalid or any component has an unexpected length.\n */\nfunction parseCiphertext(cipherText: string): ParsedCiphertext {\n const parts = cipherText.split(DELIMITER);\n if (parts.length !== 3) {\n throw new Error(\n `[orm-blind-indexer] Invalid ciphertext format. ` +\n `Expected \"base64(iv):base64(authTag):base64(ciphertext)\" with exactly 2 colons, ` +\n `but got ${parts.length - 1} colons. ` +\n `The value may not be encrypted or may be corrupted.`\n );\n }\n\n const [ivB64, authTagB64, ciphertextB64] = parts as [string, string, string];\n\n const iv = Buffer.from(ivB64, 'base64');\n const authTag = Buffer.from(authTagB64, 'base64');\n const ciphertext = Buffer.from(ciphertextB64, 'base64');\n\n if (iv.byteLength !== IV_LENGTH_BYTES) {\n throw new Error(\n `[orm-blind-indexer] Invalid IV length in ciphertext. ` +\n `Expected ${IV_LENGTH_BYTES} bytes, got ${iv.byteLength} bytes.`\n );\n }\n\n if (authTag.byteLength !== AUTH_TAG_LENGTH_BYTES) {\n throw new Error(\n `[orm-blind-indexer] Invalid Auth Tag length in ciphertext. ` +\n `Expected ${AUTH_TAG_LENGTH_BYTES} bytes, got ${authTag.byteLength} bytes.`\n );\n }\n\n return { iv, authTag, ciphertext };\n}\n\n/**\n * Decrypts a serialized AES-256-GCM ciphertext string back to UTF-8 plaintext.\n *\n * The decryption process automatically verifies the GCM authentication tag.\n * If the ciphertext or key has been tampered with, decryption will throw an error\n * (\"Unsupported state or unable to authenticate data\") before any plaintext is returned.\n *\n * @param cipherText - The serialized ciphertext string from `encrypt()`.\n * @param key - The same 32-byte Buffer used during encryption.\n * @returns The original UTF-8 plaintext string.\n * @throws {Error} If the ciphertext format is invalid, the key is wrong,\n * or the authentication tag verification fails (indicating tampering).\n *\n * @example\n * ```ts\n * const plaintext = decrypt(encryptedValue, encryptionKeyBuffer);\n * // => \"user@example.com\"\n * ```\n */\nexport function decrypt(cipherText: string, key: Buffer): string {\n const { iv, authTag, ciphertext } = parseCiphertext(cipherText);\n\n const decipher = createDecipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH_BYTES,\n });\n\n // Set the authentication tag BEFORE decrypting.\n // The decipher will verify this tag during final(), throwing if it doesn't match.\n decipher.setAuthTag(authTag);\n\n // Decrypt the ciphertext. final() triggers the auth tag verification.\n const decrypted = Buffer.concat([\n decipher.update(ciphertext),\n decipher.final(), // Throws if auth tag verification fails\n ]);\n\n return decrypted.toString('utf8');\n}\n\n/**\n * Checks if a string appears to be in the encrypted format produced by `encrypt()`.\n * Used to avoid double-encrypting values that are already encrypted.\n *\n * This is a heuristic check based on format, not cryptographic verification.\n *\n * @param value - The string to check.\n * @returns `true` if the string matches the `base64:base64:base64` pattern.\n */\nexport function isEncryptedFormat(value: string): boolean {\n const parts = value.split(DELIMITER);\n if (parts.length !== 3) return false;\n\n // Check that each part is non-empty and looks like base64\n const base64Pattern = /^[A-Za-z0-9+/]+=*$/;\n return parts.every(\n (part): boolean => typeof part === 'string' && part.length > 0 && base64Pattern.test(part)\n );\n}\n\n","/**\n * @file crypto/hasher.ts\n * @description HMAC-SHA256 blind index generation.\n *\n * ## What is a Blind Index?\n *\n * A blind index solves the problem of querying encrypted data. Because AES-256-GCM\n * uses a random IV on every encryption, the same plaintext produces a different\n * ciphertext each time. This makes exact-match database queries impossible on\n * the encrypted column.\n *\n * A blind index is a **deterministic** hash of the plaintext that:\n * - Always produces the same output for the same input (queryable)\n * - Reveals no information about the plaintext without the key (private)\n * - Is stored in a separate `_bidx` column alongside the encrypted data\n *\n * ## Why HMAC-SHA256 (not plain SHA256)?\n *\n * Plain SHA256 is a one-way function, but it has no secret. An attacker who\n * gains read access to your database could mount a **dictionary attack**:\n * they hash every common email address and look for matches.\n *\n * HMAC-SHA256 incorporates a **secret key** (the `indexKey`). Without knowing\n * the key, an attacker cannot compute the hash, so the blind index leaks no\n * information about the plaintext even if the database is fully compromised.\n *\n * ## Case Sensitivity\n *\n * Email addresses and usernames are often compared case-insensitively.\n * When `caseSensitive: false` (the default), we lowercase the input before\n * hashing. This ensures that \"Test@Test.com\" and \"test@test.com\" produce the\n * same blind index, enabling case-insensitive equality searches.\n *\n * **Important**: The case normalization must be applied consistently on both\n * write (when storing) and read (when querying). The extension handles this.\n */\n\nimport { createHmac } from 'crypto';\n\n/** HMAC digest algorithm — SHA-256 produces a 32-byte (256-bit) output. */\nconst HMAC_ALGORITHM = 'sha256' as const;\n\n/**\n * Generates a deterministic HMAC-SHA256 blind index for a given plaintext.\n *\n * The output is a base64-encoded 32-byte HMAC digest. Base64 is used (rather\n * than hex) for compactness: 44 characters vs 64 characters for the same value.\n *\n * @param text - The plaintext value to hash (e.g., an email address).\n * @param indexKey - A secret Buffer used as the HMAC key. Must be kept private.\n * @param caseSensitive - When `false`, `text` is lowercased before hashing.\n * Defaults to `false` for case-insensitive equality searches.\n * @param truncateBytes - Optional byte length (8 to 32) to truncate the HMAC\n * digest before base64 encoding. Useful for frequency-analysis defense.\n * @returns A base64-encoded HMAC-SHA256 digest string.\n *\n * @example\n * ```ts\n * const indexKey = Buffer.from(process.env.INDEX_KEY!, 'base64');\n *\n * // Case-insensitive (default): both produce the same hash\n * generateBlindIndex('Test@Test.com', indexKey, false);\n * generateBlindIndex('test@test.com', indexKey, false);\n *\n * // Case-sensitive: these produce different hashes\n * generateBlindIndex('Test@Test.com', indexKey, true);\n * generateBlindIndex('test@test.com', indexKey, true);\n *\n * // Truncated blind index (e.g., 16 bytes for frequency defense)\n * generateBlindIndex('user@example.com', indexKey, false, 16);\n * ```\n */\nexport function generateBlindIndex(\n text: string,\n indexKey: Buffer,\n caseSensitive: boolean = false,\n truncateBytes?: number\n): string {\n // Normalize to lowercase for case-insensitive lookups\n const normalized = caseSensitive ? text : text.toLowerCase();\n\n const digest = createHmac(HMAC_ALGORITHM, indexKey)\n .update(normalized, 'utf8')\n .digest();\n\n if (truncateBytes !== undefined) {\n if (truncateBytes < 8 || truncateBytes > 32) {\n throw new Error(\n `[orm-blind-indexer] \"truncateBytes\" must be between 8 and 32 bytes, got ${truncateBytes}.`\n );\n }\n return digest.subarray(0, truncateBytes).toString('base64');\n }\n\n return digest.toString('base64');\n}\n\n/**\n * Generates blind indexes for an array of plaintext values.\n * Useful for `{ in: [...] }` and `{ notIn: [...] }` Prisma query operators.\n *\n * @param values - An array of plaintext strings to hash.\n * @param indexKey - The secret HMAC key Buffer.\n * @param caseSensitive - When `false`, each value is lowercased before hashing.\n * @param truncateBytes - Optional byte length (8 to 32) to truncate each digest.\n * @returns An array of base64-encoded HMAC-SHA256 digests, in the same order as `values`.\n */\nexport function generateBlindIndexBatch(\n values: string[],\n indexKey: Buffer,\n caseSensitive: boolean = false,\n truncateBytes?: number\n): string[] {\n return values.map((value) => generateBlindIndex(value, indexKey, caseSensitive, truncateBytes));\n}\n\n","/**\n * @file crypto/utils.ts\n * @description Key validation and parsing utilities for cryptographic operations.\n *\n * Security Design Notes:\n * - AES-256 requires a 256-bit (32-byte) key. Any deviation is a hard error.\n * - Keys may be provided as base64 or hex strings for safe environment variable storage.\n * - We validate at extension initialization time, not at query time, to fail fast.\n */\n\n/**\n * Attempts to parse a string as a base64-encoded Buffer.\n * Returns `null` if the string is not valid base64.\n */\nfunction tryParseBase64(value: string): Buffer | null {\n try {\n const buf = Buffer.from(value, 'base64');\n // Verify round-trip: if re-encoding doesn't match, the original wasn't valid base64\n if (buf.toString('base64') === value || buf.toString('base64url') === value) {\n return buf;\n }\n // Allow base64 with padding stripped or standard base64\n // A valid base64 string of length L decodes to floor(L*3/4) bytes\n // We just trust the Buffer decode here for practical use\n return buf.byteLength > 0 ? buf : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Attempts to parse a string as a hex-encoded Buffer.\n * Returns `null` if the string is not a valid even-length hex string.\n */\nfunction tryParseHex(value: string): Buffer | null {\n if (!/^[0-9a-fA-F]+$/.test(value) || value.length % 2 !== 0) {\n return null;\n }\n return Buffer.from(value, 'hex');\n}\n\n/**\n * Parses a key string (base64 or hex) into a Buffer and validates its byte length.\n *\n * The function tries hex first (unambiguous format), then base64.\n *\n * @param keyString - The raw key string from environment variables or config.\n * @param expectedBytes - The required byte length of the decoded key.\n * @param keyName - A descriptive name for the key, used in error messages.\n * @returns A `Buffer` of exactly `expectedBytes` bytes.\n * @throws {Error} If the key cannot be parsed or has an incorrect length.\n *\n * @example\n * ```ts\n * const key = parseAndValidateKey(process.env.ENCRYPTION_KEY!, 32, 'encryptionKey');\n * ```\n */\nexport function parseAndValidateKey(\n keyString: string,\n expectedBytes: number,\n keyName: string\n): Buffer {\n if (!keyString || typeof keyString !== 'string') {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" must be a non-empty string. ` +\n `Generate one with: node -e \"console.log(require('crypto').randomBytes(${expectedBytes}).toString('base64'))\"`\n );\n }\n\n // Try hex first (unambiguous — only hex chars, even length)\n const hexBuf = tryParseHex(keyString);\n if (hexBuf !== null) {\n if (hexBuf.byteLength !== expectedBytes) {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" decoded to ${hexBuf.byteLength} bytes from hex, ` +\n `but ${expectedBytes} bytes are required. ` +\n `A ${expectedBytes}-byte hex key must be ${expectedBytes * 2} hex characters.`\n );\n }\n return hexBuf;\n }\n\n // Try base64\n const b64Buf = tryParseBase64(keyString);\n if (b64Buf !== null) {\n if (b64Buf.byteLength !== expectedBytes) {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" decoded to ${b64Buf.byteLength} bytes from base64, ` +\n `but ${expectedBytes} bytes are required. ` +\n `Generate a valid key with: node -e \"console.log(require('crypto').randomBytes(${expectedBytes}).toString('base64'))\"`\n );\n }\n return b64Buf;\n }\n\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" could not be parsed as hex or base64. ` +\n `Provide a ${expectedBytes * 2}-char hex string or a ${Math.ceil(expectedBytes * 4 / 3)}-char base64 string.`\n );\n}\n\n/**\n * Validates an encryption key for AES-256 (requires exactly 32 bytes / 256 bits).\n *\n * @param keyString - The raw encryption key string from config.\n * @returns A `Buffer` of exactly 32 bytes suitable for AES-256-GCM.\n * @throws {Error} If the key is invalid or not exactly 32 bytes.\n */\nexport function validateEncryptionKey(keyString: string): Buffer {\n return parseAndValidateKey(keyString, 32, 'encryptionKey');\n}\n\n/**\n * Parses the index key (HMAC salt) from a string.\n * Unlike the encryption key, the HMAC key has no strict length requirement in the\n * cryptographic spec (SHA-256 accepts any length), but we enforce a minimum of 16 bytes\n * for security best-practice guidance.\n *\n * @param keyString - The raw index key string from config.\n * @returns A `Buffer` usable as an HMAC key.\n * @throws {Error} If the key cannot be parsed or is shorter than 16 bytes.\n */\nexport function validateIndexKey(keyString: string): Buffer {\n if (!keyString || typeof keyString !== 'string') {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" must be a non-empty string. ` +\n `Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"`\n );\n }\n\n // Try hex\n const hexBuf = tryParseHex(keyString);\n if (hexBuf !== null) {\n if (hexBuf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${hexBuf.byteLength} bytes). ` +\n `Minimum recommended length is 16 bytes (32 hex chars).`\n );\n }\n return hexBuf;\n }\n\n // Try base64\n const b64Buf = tryParseBase64(keyString);\n if (b64Buf !== null) {\n if (b64Buf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${b64Buf.byteLength} bytes). ` +\n `Minimum recommended length is 16 bytes.`\n );\n }\n return b64Buf;\n }\n\n // Fall back to treating it as a raw UTF-8 string key (not recommended but functional)\n const rawBuf = Buffer.from(keyString, 'utf8');\n if (rawBuf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${rawBuf.byteLength} bytes as UTF-8). ` +\n `Minimum recommended length is 16 bytes.`\n );\n }\n return rawBuf;\n}\n\n","/**\n * @file extension/interceptors.ts\n * @description Recursive WHERE clause transformation for blind index substitution.\n *\n * ## The Problem\n *\n * Prisma's `where` clause is a deeply nested object that can contain:\n * - Direct equality: `{ email: \"user@example.com\" }`\n * - Operator objects: `{ email: { in: [...], equals: \"...\" } }`\n * - Logical combinators: `{ AND: [...], OR: [...], NOT: { ... } }`\n * - Nested relations: `{ posts: { some: { author: { email: \"...\" } } } }`\n *\n * We need to intercept any occurrence of an encrypted field name and replace it\n * with a blind index lookup, regardless of how deeply nested it is.\n *\n * ## Strategy\n *\n * We use a **recursive depth-first traversal** of the `where` object:\n *\n * 1. For each key in the current object, check if it's a logical combinator\n * (`AND`, `OR`, `NOT`). If so, recurse into its children.\n * 2. If the key matches a configured encrypted field, transform the value:\n * - Remove the original field key.\n * - Add the `indexFieldName` key with the hashed value.\n * 3. Handle the `in` operator specially: hash each array element.\n *\n * ## Supported Prisma Filter Operators (on encrypted fields)\n *\n * | Prisma Operator | Supported | Notes |\n * |----------------|-----------|------------------------------------------|\n * | `equals` | ✅ | Direct value or `{ equals: value }` |\n * | `in` | ✅ | Hashes each element of the array |\n * | (direct) | ✅ | `{ email: \"value\" }` shorthand |\n * | `contains` | ❌ | LIKE queries incompatible with hashing |\n * | `startsWith` | ❌ | LIKE queries incompatible with hashing |\n * | `endsWith` | ❌ | LIKE queries incompatible with hashing |\n * | `lt/gt/lte/gte`| ❌ | Range queries incompatible with hashing |\n *\n * Attempting to use unsupported operators on encrypted fields will throw an error\n * guiding the developer to the correct approach.\n */\n\nimport { generateBlindIndex, generateBlindIndexBatch } from '../crypto/hasher.js';\nimport type { ModelFieldConfig } from '../types/index.js';\n\n/** Prisma logical combinator keys that contain nested where clause arrays or objects. */\nconst LOGICAL_COMBINATORS = new Set(['AND', 'OR', 'NOT']);\n\n/** Prisma filter operators that are incompatible with blind indexing. */\nconst UNSUPPORTED_OPERATORS = new Set([\n 'contains',\n 'startsWith',\n 'endsWith',\n 'lt',\n 'lte',\n 'gt',\n 'gte',\n 'search',\n]);\n\n/**\n * Transforms a Prisma `where` clause, replacing encrypted field lookups with\n * their corresponding blind index field lookups.\n *\n * This function is **pure** (it returns a new object, not mutating the original)\n * and **recursive** (it handles arbitrarily nested `AND`/`OR`/`NOT` clauses).\n *\n * @param where - The Prisma where clause object to transform.\n * @param modelConfig - The field configuration for the current model.\n * @param indexKey - The HMAC key Buffer for generating blind indexes.\n * @returns A new where clause object with encrypted fields replaced by blind index fields.\n * @throws {Error} If an unsupported operator is used on an encrypted field.\n */\nexport function transformWhereClause(\n where: Record<string, unknown>,\n modelConfig: ModelFieldConfig,\n indexKey: Buffer\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(where)) {\n // --- Handle logical combinators: AND, OR, NOT ---\n if (LOGICAL_COMBINATORS.has(key)) {\n if (Array.isArray(value)) {\n // AND: [...], OR: [...]\n result[key] = value.map((item) => {\n if (item !== null && typeof item === 'object' && !Array.isArray(item)) {\n return transformWhereClause(\n item as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n return item;\n });\n } else if (value !== null && typeof value === 'object') {\n // NOT: { ... }\n result[key] = transformWhereClause(\n value as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n } else {\n result[key] = value;\n }\n continue;\n }\n\n // --- Handle encrypted fields ---\n const fieldConfig = modelConfig[key];\n if (fieldConfig !== undefined) {\n const { indexFieldName, caseSensitive = false, truncateBytes } = fieldConfig;\n\n // Value is a plain string: `{ email: \"user@example.com\" }` shorthand\n if (typeof value === 'string') {\n result[indexFieldName] = generateBlindIndex(value, indexKey, caseSensitive, truncateBytes);\n continue;\n }\n\n // Value is null: pass through (querying for null encrypted fields)\n if (value === null) {\n result[indexFieldName] = null;\n continue;\n }\n\n // Value is an operator object: `{ email: { in: [...], equals: \"...\" } }`\n if (typeof value === 'object' && !Array.isArray(value)) {\n const operatorObj = value as Record<string, unknown>;\n const transformedOperator: Record<string, unknown> = {};\n\n for (const [op, opValue] of Object.entries(operatorObj)) {\n // Check for unsupported operators\n if (UNSUPPORTED_OPERATORS.has(op)) {\n throw new Error(\n `[orm-blind-indexer] The \"${op}\" filter operator is not supported on encrypted ` +\n `field \"${key}\". Blind indexes only support exact-match lookups (equals, in, notIn). ` +\n `Partial string searches on encrypted fields require application-level decryption.`\n );\n }\n\n // Handle Prisma's mode: \"insensitive\" / \"default\"\n if (op === 'mode') {\n if (opValue === 'insensitive' && caseSensitive) {\n throw new Error(\n `[orm-blind-indexer] \"mode: 'insensitive'\" cannot be used on encrypted field \"${key}\" ` +\n `configured with \"caseSensitive: true\". Change the field configuration to \"caseSensitive: false\" ` +\n `for case-insensitive blind index lookups.`\n );\n }\n // If caseSensitive is false, case insensitivity is inherently handled\n // during HMAC generation. We omit `mode` from the transformed query.\n continue;\n }\n\n if (op === 'equals') {\n if (typeof opValue === 'string') {\n transformedOperator['equals'] = generateBlindIndex(opValue, indexKey, caseSensitive, truncateBytes);\n } else if (opValue === null) {\n transformedOperator['equals'] = null;\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"equals\" operator on encrypted field \"${key}\" ` +\n `expects a string value, got ${typeof opValue}.`\n );\n }\n } else if (op === 'in') {\n if (Array.isArray(opValue)) {\n const strings = opValue.filter((v): v is string => typeof v === 'string');\n if (strings.length !== opValue.length) {\n throw new Error(\n `[orm-blind-indexer] The \"in\" operator on encrypted field \"${key}\" ` +\n `requires all values to be strings.`\n );\n }\n transformedOperator['in'] = generateBlindIndexBatch(strings, indexKey, caseSensitive, truncateBytes);\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"in\" operator on encrypted field \"${key}\" ` +\n `expects an array, got ${typeof opValue}.`\n );\n }\n } else if (op === 'notIn') {\n if (Array.isArray(opValue)) {\n const strings = opValue.filter((v): v is string => typeof v === 'string');\n if (strings.length !== opValue.length) {\n throw new Error(\n `[orm-blind-indexer] The \"notIn\" operator on encrypted field \"${key}\" ` +\n `requires all values to be strings.`\n );\n }\n transformedOperator['notIn'] = generateBlindIndexBatch(strings, indexKey, caseSensitive, truncateBytes);\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"notIn\" operator on encrypted field \"${key}\" ` +\n `expects an array, got ${typeof opValue}.`\n );\n }\n } else if (op === 'not') {\n // `NOT` as a filter operator (different from `NOT` combinator)\n if (typeof opValue === 'string') {\n transformedOperator['not'] = generateBlindIndex(opValue, indexKey, caseSensitive, truncateBytes);\n } else if (opValue === null) {\n transformedOperator['not'] = null;\n } else {\n transformedOperator['not'] = opValue;\n }\n } else {\n // Pass through unknown operators (future Prisma operators)\n transformedOperator[op] = opValue;\n }\n }\n\n result[indexFieldName] = transformedOperator;\n continue;\n }\n\n // Fallback: pass through as-is (handles edge cases)\n result[key] = value;\n continue;\n }\n\n // --- Non-encrypted field: pass through unchanged ---\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Strips blind index fields (`_bidx` columns) from a query result object\n * so the caller only sees the logical schema (without implementation details).\n *\n * This function also handles arrays of results (for `findMany`) and\n * recursively cleans nested relation objects.\n *\n * @param result - The raw result from Prisma (single object or array).\n * @param modelConfig - The field configuration for the current model.\n * @returns The cleaned result with `_bidx` fields removed.\n */\nexport function stripBlindIndexFields(\n result: unknown,\n modelConfig: ModelFieldConfig\n): unknown {\n if (result === null || result === undefined) {\n return result;\n }\n\n if (Array.isArray(result)) {\n return result.map((item) => stripBlindIndexFields(item, modelConfig));\n }\n\n if (typeof result === 'object') {\n const obj = result as Record<string, unknown>;\n const cleaned: Record<string, unknown> = {};\n\n // Collect all bidx field names to exclude\n const bidxFieldNames = new Set(\n Object.values(modelConfig).map((cfg) => cfg.indexFieldName)\n );\n\n for (const [key, value] of Object.entries(obj)) {\n if (!bidxFieldNames.has(key)) {\n // Recursively clean nested objects (for relations)\n if (value !== null && typeof value === 'object') {\n cleaned[key] = stripBlindIndexFields(value, modelConfig);\n } else {\n cleaned[key] = value;\n }\n }\n // Skip bidx fields — don't include them in output\n }\n\n return cleaned;\n }\n\n return result;\n}\n\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/extension/prisma.ts","../src/crypto/encryptor.ts","../src/crypto/hasher.ts","../src/crypto/utils.ts","../src/extension/interceptors.ts"],"sourcesContent":["/**\n * @file extension/prisma.ts\n * @description The core Prisma Client Extension implementing the blind indexing middleware.\n *\n * ## How Prisma Client Extensions Work\n *\n * Prisma Client Extensions (introduced in Prisma 4.7) allow augmenting the Prisma\n * client with custom behavior. We use the `query` extension component, which\n * intercepts all database operations before and after they execute.\n *\n * The `query.$allModels.$allOperations` hook receives:\n * - `model`: The Prisma model name (e.g., \"User\")\n * - `operation`: The operation name (e.g., \"findUnique\", \"create\")\n * - `args`: The operation arguments (where clause, data, etc.) — MUTATED before passing to `next()`\n * - `query`: The `next` function to call to execute the actual query\n *\n * ## Lifecycle of a Write Operation (create/update/upsert)\n *\n * 1. Developer calls: `prisma.user.create({ data: { email: \"user@example.com\" } })`\n * 2. Extension intercepts `args.data`\n * 3. For each configured encrypted field found in `data`:\n * a. Encrypt the plaintext with AES-256-GCM → `email` becomes the ciphertext\n * b. Generate HMAC-SHA256 blind index → inject as `email_bidx`\n * 4. The modified `args` are passed to `next()` → Prisma writes both columns\n * 5. The result is post-processed: `email` is decrypted back, `email_bidx` is stripped\n *\n * ## Lifecycle of a Read Operation (findUnique/findMany/findFirst)\n *\n * 1. Developer calls: `prisma.user.findUnique({ where: { email: \"user@example.com\" } })`\n * 2. Extension intercepts `args.where`\n * 3. `transformWhereClause()` replaces `email` with `email_bidx: hash(\"user@example.com\")`\n * 4. The modified `args` are passed to `next()` → Prisma queries the blind index column\n * 5. The result is post-processed: `email` is decrypted, `email_bidx` is stripped\n *\n * ## Write Operations Supported\n *\n * | Operation | Data Location | Supported |\n * |--------------------|--------------------|-----------|\n * | create | args.data | ✅ |\n * | update | args.data | ✅ |\n * | upsert | args.create + args.update | ✅ |\n * | createMany | args.data (array) | ✅ |\n * | updateMany | args.data | ✅ |\n *\n * ## Read Operations Supported\n *\n * | Operation | Where Location | Supported |\n * |--------------------|-------------------|-----------|\n * | findUnique | args.where | ✅ |\n * | findUniqueOrThrow | args.where | ✅ |\n * | findFirst | args.where | ✅ |\n * | findFirstOrThrow | args.where | ✅ |\n * | findMany | args.where | ✅ |\n * | count | args.where | ✅ |\n * | aggregate | args.where | ✅ |\n * | delete | args.where | ✅ |\n * | deleteMany | args.where | ✅ |\n */\n\nimport { Prisma } from '@prisma/client/extension';\nimport { encrypt, decrypt, isEncryptedFormat } from '../crypto/encryptor.js';\nimport { generateBlindIndex } from '../crypto/hasher.js';\nimport { validateEncryptionKey, validateIndexKey } from '../crypto/utils.js';\nimport { transformWhereClause, stripBlindIndexFields } from './interceptors.js';\nimport type { BlindIndexConfig, ModelFieldConfig } from '../types/index.js';\n\n/** Operations that write data and require encrypt + blind-index injection */\nconst WRITE_OPERATIONS = new Set([\n 'create',\n 'update',\n 'upsert',\n 'createMany',\n 'updateMany',\n]);\n\n/** Operations that read data and require where-clause transformation */\nconst READ_OPERATIONS = new Set([\n 'findUnique',\n 'findUniqueOrThrow',\n 'findFirst',\n 'findFirstOrThrow',\n 'findMany',\n 'count',\n 'aggregate',\n 'groupBy',\n 'delete',\n 'deleteMany',\n]);\n\n/**\n * Processes a single data object (from create/update `data`), encrypting\n * configured plaintext fields and injecting their blind index counterparts.\n *\n * @param data - The raw data object from the Prisma args.\n * @param modelConfig - Field configuration for the current model.\n * @param encryptionKey - AES-256-GCM key Buffer.\n * @param indexKey - HMAC-SHA256 key Buffer.\n * @returns A new data object with encrypted values and blind index fields injected.\n */\nfunction processWriteData(\n data: Record<string, unknown>,\n modelConfig: ModelFieldConfig,\n encryptionKey: Buffer,\n indexKey: Buffer\n): Record<string, unknown> {\n const result = { ...data };\n\n for (const [fieldName, fieldConfig] of Object.entries(modelConfig)) {\n const value = result[fieldName];\n\n if (value === undefined || value === null) {\n // Field not present in this write, or explicitly null — skip\n continue;\n }\n\n if (typeof value !== 'string') {\n // Non-string values (e.g. Prisma `set` wrappers) — skip for safety\n continue;\n }\n\n // Guard against double-encryption if the value is already in encrypted format\n if (isEncryptedFormat(value)) {\n continue;\n }\n\n // 1. Encrypt the plaintext value\n result[fieldName] = encrypt(value, encryptionKey);\n\n // 2. Generate and inject the blind index\n result[fieldConfig.indexFieldName] = generateBlindIndex(\n value,\n indexKey,\n fieldConfig.caseSensitive ?? false\n );\n }\n\n return result;\n}\n\n/**\n * Post-processes a Prisma result, decrypting encrypted fields and removing\n * blind index fields from the output.\n *\n * @param result - The raw result from Prisma.\n * @param modelConfig - Field configuration for the current model.\n * @param encryptionKey - AES-256-GCM key Buffer for decryption.\n * @returns The cleaned result with decrypted plaintext and no `_bidx` fields.\n */\nfunction processResult(\n result: unknown,\n modelConfig: ModelFieldConfig,\n encryptionKey: Buffer\n): unknown {\n if (result === null || result === undefined) {\n return result;\n }\n\n if (Array.isArray(result)) {\n return result.map((item) => processResult(item, modelConfig, encryptionKey));\n }\n\n if (typeof result === 'object') {\n const obj = result as Record<string, unknown>;\n // Decrypt each configured field\n for (const [fieldName] of Object.entries(modelConfig)) {\n const value = obj[fieldName];\n if (typeof value === 'string' && isEncryptedFormat(value)) {\n try {\n obj[fieldName] = decrypt(value, encryptionKey);\n } catch {\n // If decryption fails, leave the value as-is (may not be encrypted)\n }\n }\n }\n // Strip blind index fields from the result\n return stripBlindIndexFields(obj, modelConfig);\n }\n\n return result;\n}\n\n/**\n * Creates a Prisma Client Extension that transparently implements blind indexing\n * for the configured fields.\n *\n * **Usage:**\n * ```ts\n * import { PrismaClient } from '@prisma/client';\n * import { withBlindIndexer } from 'orm-blind-indexer';\n *\n * const prisma = new PrismaClient().$extends(\n * withBlindIndexer({\n * encryptionKey: process.env.ENCRYPTION_KEY!,\n * indexKey: process.env.INDEX_KEY!,\n * fields: {\n * User: {\n * email: {\n * indexFieldName: 'email_bidx',\n * caseSensitive: false,\n * },\n * },\n * },\n * })\n * );\n * ```\n *\n * @param config - The blind indexer configuration object.\n * @returns A Prisma Client Extension object to pass to `.$extends()`.\n * @throws {Error} At initialization time if the keys are invalid.\n */\nexport function withBlindIndexer(config: BlindIndexConfig) {\n // Validate keys eagerly at initialization time for fast failure\n const encryptionKey = validateEncryptionKey(config.encryptionKey);\n const indexKey = validateIndexKey(config.indexKey);\n\n return Prisma.defineExtension({\n name: 'orm-blind-indexer',\n\n query: {\n $allModels: {\n async $allOperations({ model, operation, args, query }) {\n // Get the field config for the current model (if any)\n const modelConfig: ModelFieldConfig | undefined =\n model !== undefined ? config.fields[model] : undefined;\n\n // If no config for this model, pass through unchanged\n if (modelConfig === undefined || Object.keys(modelConfig).length === 0) {\n return query(args);\n }\n\n const typedArgs = args as Record<string, unknown>;\n\n // ----------------------------------------------------------------\n // WRITE OPERATIONS: encrypt data + inject blind indexes\n // ----------------------------------------------------------------\n if (WRITE_OPERATIONS.has(operation)) {\n if (operation === 'upsert') {\n // upsert has both `create` and `update` data objects\n if (typedArgs['create'] && typeof typedArgs['create'] === 'object') {\n typedArgs['create'] = processWriteData(\n typedArgs['create'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n if (typedArgs['update'] && typeof typedArgs['update'] === 'object') {\n typedArgs['update'] = processWriteData(\n typedArgs['update'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n } else if (operation === 'createMany') {\n // createMany has an array of data objects\n if (Array.isArray(typedArgs['data'])) {\n typedArgs['data'] = (typedArgs['data'] as Record<string, unknown>[]).map(\n (item) => processWriteData(item, modelConfig, encryptionKey, indexKey)\n );\n }\n } else {\n // create, update, updateMany\n if (typedArgs['data'] && typeof typedArgs['data'] === 'object') {\n typedArgs['data'] = processWriteData(\n typedArgs['data'] as Record<string, unknown>,\n modelConfig,\n encryptionKey,\n indexKey\n );\n }\n }\n\n // Also transform where clause for update/delete operations\n if (typedArgs['where'] && typeof typedArgs['where'] === 'object') {\n typedArgs['where'] = transformWhereClause(\n typedArgs['where'] as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n }\n\n // ----------------------------------------------------------------\n // READ OPERATIONS: transform where clause\n // ----------------------------------------------------------------\n if (READ_OPERATIONS.has(operation)) {\n if (typedArgs['where'] && typeof typedArgs['where'] === 'object') {\n typedArgs['where'] = transformWhereClause(\n typedArgs['where'] as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n }\n\n // Execute the (possibly mutated) query\n const result = await query(args);\n\n // ----------------------------------------------------------------\n // POST-PROCESS: decrypt results + strip blind index fields\n // ----------------------------------------------------------------\n // Don't post-process count/aggregate (they return numbers, not records)\n if (operation === 'count' || operation === 'aggregate' || operation === 'groupBy') {\n return result;\n }\n\n return processResult(result, modelConfig, encryptionKey);\n },\n },\n },\n });\n}\n\n","/**\n * @file crypto/encryptor.ts\n * @description AES-256-GCM authenticated encryption and decryption.\n *\n * ## Why AES-256-GCM?\n *\n * AES-256-GCM (Galois/Counter Mode) is an authenticated encryption with\n * associated data (AEAD) cipher. It provides:\n *\n * 1. **Confidentiality**: The ciphertext reveals nothing about the plaintext.\n * 2. **Integrity & Authenticity**: The 16-byte authentication tag (Auth Tag)\n * cryptographically proves the ciphertext has not been tampered with.\n * Decryption fails with an error if the tag doesn't verify.\n * 3. **No padding oracle**: GCM is a stream-cipher mode — no PKCS#7 padding\n * means no padding oracle attacks.\n *\n * ## On-disk format\n *\n * We serialize the IV, Auth Tag, and ciphertext as three colon-separated\n * base64 segments: `base64(iv):base64(authTag):base64(ciphertext)`\n *\n * This is a self-contained format: everything needed to decrypt the value\n * lives in the value itself. The key is stored separately (environment variable).\n *\n * ## IV Randomness\n *\n * A fresh 12-byte random IV is generated for **every** encryption call using\n * `crypto.randomBytes()`. This is critical: reusing an IV with the same key\n * in GCM mode completely breaks confidentiality (two-time pad attack).\n * With a 12-byte IV, the probability of a collision is negligible for\n * reasonable database sizes (birthday bound: ~2^48 operations for 2^-32 risk).\n *\n * ## Auth Tag Length\n *\n * We use the full 16-byte (128-bit) authentication tag, which is the\n * maximum and recommended length. Shorter tags reduce forgery resistance.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes } from 'crypto';\nimport type { ParsedCiphertext } from '../types/index.js';\n\n/** AES-256-GCM algorithm identifier used by Node.js crypto. */\nconst ALGORITHM = 'aes-256-gcm' as const;\n\n/** Length of the random Initialization Vector in bytes. NIST recommends 96 bits (12 bytes) for GCM. */\nconst IV_LENGTH_BYTES = 12;\n\n/** Length of the GCM authentication tag in bytes. 128 bits (16 bytes) is the maximum and recommended. */\nconst AUTH_TAG_LENGTH_BYTES = 16;\n\n/** Delimiter used to separate the IV, Auth Tag, and ciphertext in the serialized format. */\nconst DELIMITER = ':';\n\n/**\n * Encrypts a UTF-8 plaintext string using AES-256-GCM.\n *\n * @param plaintext - The plaintext string to encrypt (e.g., an email address).\n * @param key - A 32-byte Buffer containing the AES-256 encryption key.\n * @returns A serialized ciphertext string: `base64(iv):base64(authTag):base64(ciphertext)`.\n * @throws {Error} If encryption fails (e.g., invalid key length).\n *\n * @example\n * ```ts\n * const encrypted = encrypt('user@example.com', encryptionKeyBuffer);\n * // => \"ABC123...:XYZ789...:cipher...\"\n * ```\n */\nexport function encrypt(plaintext: string, key: Buffer): string {\n // Generate a fresh random 12-byte IV for every encryption\n const iv = randomBytes(IV_LENGTH_BYTES);\n\n // Create the cipher. Node.js validates the key length here and throws\n // if it doesn't match the algorithm's requirement (32 bytes for AES-256).\n const cipher = createCipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH_BYTES,\n });\n\n // Encrypt the plaintext. The update() call does the actual encryption;\n // final() flushes any remaining bytes (empty for a stream cipher like GCM).\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final(),\n ]);\n\n // Retrieve the GCM authentication tag AFTER calling final()\n const authTag = cipher.getAuthTag();\n\n // Serialize: base64(iv) : base64(authTag) : base64(ciphertext)\n return [\n iv.toString('base64'),\n authTag.toString('base64'),\n encrypted.toString('base64'),\n ].join(DELIMITER);\n}\n\n/**\n * Parses a serialized AES-256-GCM ciphertext string into its components.\n *\n * @param cipherText - The serialized string in `base64(iv):base64(authTag):base64(ct)` format.\n * @returns A `ParsedCiphertext` object with Buffer components.\n * @throws {Error} If the string format is invalid or any component has an unexpected length.\n */\nfunction parseCiphertext(cipherText: string): ParsedCiphertext {\n const parts = cipherText.split(DELIMITER);\n if (parts.length !== 3) {\n throw new Error(\n `[orm-blind-indexer] Invalid ciphertext format. ` +\n `Expected \"base64(iv):base64(authTag):base64(ciphertext)\" with exactly 2 colons, ` +\n `but got ${parts.length - 1} colons. ` +\n `The value may not be encrypted or may be corrupted.`\n );\n }\n\n const [ivB64, authTagB64, ciphertextB64] = parts as [string, string, string];\n\n const iv = Buffer.from(ivB64, 'base64');\n const authTag = Buffer.from(authTagB64, 'base64');\n const ciphertext = Buffer.from(ciphertextB64, 'base64');\n\n if (iv.byteLength !== IV_LENGTH_BYTES) {\n throw new Error(\n `[orm-blind-indexer] Invalid IV length in ciphertext. ` +\n `Expected ${IV_LENGTH_BYTES} bytes, got ${iv.byteLength} bytes.`\n );\n }\n\n if (authTag.byteLength !== AUTH_TAG_LENGTH_BYTES) {\n throw new Error(\n `[orm-blind-indexer] Invalid Auth Tag length in ciphertext. ` +\n `Expected ${AUTH_TAG_LENGTH_BYTES} bytes, got ${authTag.byteLength} bytes.`\n );\n }\n\n return { iv, authTag, ciphertext };\n}\n\n/**\n * Decrypts a serialized AES-256-GCM ciphertext string back to UTF-8 plaintext.\n *\n * The decryption process automatically verifies the GCM authentication tag.\n * If the ciphertext or key has been tampered with, decryption will throw an error\n * (\"Unsupported state or unable to authenticate data\") before any plaintext is returned.\n *\n * @param cipherText - The serialized ciphertext string from `encrypt()`.\n * @param key - The same 32-byte Buffer used during encryption.\n * @returns The original UTF-8 plaintext string.\n * @throws {Error} If the ciphertext format is invalid, the key is wrong,\n * or the authentication tag verification fails (indicating tampering).\n *\n * @example\n * ```ts\n * const plaintext = decrypt(encryptedValue, encryptionKeyBuffer);\n * // => \"user@example.com\"\n * ```\n */\nexport function decrypt(cipherText: string, key: Buffer): string {\n const { iv, authTag, ciphertext } = parseCiphertext(cipherText);\n\n const decipher = createDecipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH_BYTES,\n });\n\n // Set the authentication tag BEFORE decrypting.\n // The decipher will verify this tag during final(), throwing if it doesn't match.\n decipher.setAuthTag(authTag);\n\n // Decrypt the ciphertext. final() triggers the auth tag verification.\n const decrypted = Buffer.concat([\n decipher.update(ciphertext),\n decipher.final(), // Throws if auth tag verification fails\n ]);\n\n return decrypted.toString('utf8');\n}\n\n/**\n * Checks if a string appears to be in the encrypted format produced by `encrypt()`.\n * Used to avoid double-encrypting values that are already encrypted.\n *\n * This is a heuristic check based on format, not cryptographic verification.\n *\n * @param value - The string to check.\n * @returns `true` if the string matches the `base64:base64:base64` pattern.\n */\nexport function isEncryptedFormat(value: string): boolean {\n const parts = value.split(DELIMITER);\n if (parts.length !== 3) return false;\n\n // Check that each part is non-empty and looks like base64\n const base64Pattern = /^[A-Za-z0-9+/]+=*$/;\n return parts.every(\n (part): boolean => typeof part === 'string' && part.length > 0 && base64Pattern.test(part)\n );\n}\n\n","/**\n * @file crypto/hasher.ts\n * @description HMAC-SHA256 blind index generation.\n *\n * ## What is a Blind Index?\n *\n * A blind index solves the problem of querying encrypted data. Because AES-256-GCM\n * uses a random IV on every encryption, the same plaintext produces a different\n * ciphertext each time. This makes exact-match database queries impossible on\n * the encrypted column.\n *\n * A blind index is a **deterministic** hash of the plaintext that:\n * - Always produces the same output for the same input (queryable)\n * - Reveals no information about the plaintext without the key (private)\n * - Is stored in a separate `_bidx` column alongside the encrypted data\n *\n * ## Why HMAC-SHA256 (not plain SHA256)?\n *\n * Plain SHA256 is a one-way function, but it has no secret. An attacker who\n * gains read access to your database could mount a **dictionary attack**:\n * they hash every common email address and look for matches.\n *\n * HMAC-SHA256 incorporates a **secret key** (the `indexKey`). Without knowing\n * the key, an attacker cannot compute the hash, so the blind index leaks no\n * information about the plaintext even if the database is fully compromised.\n *\n * ## Case Sensitivity\n *\n * Email addresses and usernames are often compared case-insensitively.\n * When `caseSensitive: false` (the default), we lowercase the input before\n * hashing. This ensures that \"Test@Test.com\" and \"test@test.com\" produce the\n * same blind index, enabling case-insensitive equality searches.\n *\n * **Important**: The case normalization must be applied consistently on both\n * write (when storing) and read (when querying). The extension handles this.\n */\n\nimport { createHmac } from 'crypto';\n\n/** HMAC digest algorithm — SHA-256 produces a 32-byte (256-bit) output. */\nconst HMAC_ALGORITHM = 'sha256' as const;\n\n/**\n * Generates a deterministic HMAC-SHA256 blind index for a given plaintext.\n *\n * The output is a base64-encoded 32-byte HMAC digest. Base64 is used (rather\n * than hex) for compactness: 44 characters vs 64 characters for the same value.\n *\n * @param text - The plaintext value to hash (e.g., an email address).\n * @param indexKey - A secret Buffer used as the HMAC key. Must be kept private.\n * @param caseSensitive - When `false`, `text` is lowercased before hashing.\n * Defaults to `false` for case-insensitive equality searches.\n * @param truncateBytes - Optional byte length (8 to 32) to truncate the HMAC\n * digest before base64 encoding. Useful for frequency-analysis defense.\n * @returns A base64-encoded HMAC-SHA256 digest string.\n *\n * @example\n * ```ts\n * const indexKey = Buffer.from(process.env.INDEX_KEY!, 'base64');\n *\n * // Case-insensitive (default): both produce the same hash\n * generateBlindIndex('Test@Test.com', indexKey, false);\n * generateBlindIndex('test@test.com', indexKey, false);\n *\n * // Case-sensitive: these produce different hashes\n * generateBlindIndex('Test@Test.com', indexKey, true);\n * generateBlindIndex('test@test.com', indexKey, true);\n *\n * // Truncated blind index (e.g., 16 bytes for frequency defense)\n * generateBlindIndex('user@example.com', indexKey, false, 16);\n * ```\n */\nexport function generateBlindIndex(\n text: string,\n indexKey: Buffer,\n caseSensitive: boolean = false,\n truncateBytes?: number\n): string {\n // Normalize to lowercase for case-insensitive lookups\n const normalized = caseSensitive ? text : text.toLowerCase();\n\n const digest = createHmac(HMAC_ALGORITHM, indexKey)\n .update(normalized, 'utf8')\n .digest();\n\n if (truncateBytes !== undefined) {\n if (truncateBytes < 8 || truncateBytes > 32) {\n throw new Error(\n `[orm-blind-indexer] \"truncateBytes\" must be between 8 and 32 bytes, got ${truncateBytes}.`\n );\n }\n return digest.subarray(0, truncateBytes).toString('base64');\n }\n\n return digest.toString('base64');\n}\n\n/**\n * Generates blind indexes for an array of plaintext values.\n * Useful for `{ in: [...] }` and `{ notIn: [...] }` Prisma query operators.\n *\n * @param values - An array of plaintext strings to hash.\n * @param indexKey - The secret HMAC key Buffer.\n * @param caseSensitive - When `false`, each value is lowercased before hashing.\n * @param truncateBytes - Optional byte length (8 to 32) to truncate each digest.\n * @returns An array of base64-encoded HMAC-SHA256 digests, in the same order as `values`.\n */\nexport function generateBlindIndexBatch(\n values: string[],\n indexKey: Buffer,\n caseSensitive: boolean = false,\n truncateBytes?: number\n): string[] {\n return values.map((value) => generateBlindIndex(value, indexKey, caseSensitive, truncateBytes));\n}\n\n","/**\n * @file crypto/utils.ts\n * @description Key validation and parsing utilities for cryptographic operations.\n *\n * Security Design Notes:\n * - AES-256 requires a 256-bit (32-byte) key. Any deviation is a hard error.\n * - Keys may be provided as base64 or hex strings for safe environment variable storage.\n * - We validate at extension initialization time, not at query time, to fail fast.\n */\n\n/**\n * Attempts to parse a string as a base64-encoded Buffer.\n * Returns `null` if the string is not valid base64.\n */\nfunction tryParseBase64(value: string): Buffer | null {\n try {\n const buf = Buffer.from(value, 'base64');\n // Verify round-trip: if re-encoding doesn't match, the original wasn't valid base64\n if (buf.toString('base64') === value || buf.toString('base64url') === value) {\n return buf;\n }\n // Allow base64 with padding stripped or standard base64\n // A valid base64 string of length L decodes to floor(L*3/4) bytes\n // We just trust the Buffer decode here for practical use\n return buf.byteLength > 0 ? buf : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Attempts to parse a string as a hex-encoded Buffer.\n * Returns `null` if the string is not a valid even-length hex string.\n */\nfunction tryParseHex(value: string): Buffer | null {\n if (!/^[0-9a-fA-F]+$/.test(value) || value.length % 2 !== 0) {\n return null;\n }\n return Buffer.from(value, 'hex');\n}\n\n/**\n * Parses a key string (base64 or hex) into a Buffer and validates its byte length.\n *\n * The function tries hex first (unambiguous format), then base64.\n *\n * @param keyString - The raw key string from environment variables or config.\n * @param expectedBytes - The required byte length of the decoded key.\n * @param keyName - A descriptive name for the key, used in error messages.\n * @returns A `Buffer` of exactly `expectedBytes` bytes.\n * @throws {Error} If the key cannot be parsed or has an incorrect length.\n *\n * @example\n * ```ts\n * const key = parseAndValidateKey(process.env.ENCRYPTION_KEY!, 32, 'encryptionKey');\n * ```\n */\nexport function parseAndValidateKey(\n keyString: string,\n expectedBytes: number,\n keyName: string\n): Buffer {\n if (!keyString || typeof keyString !== 'string') {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" must be a non-empty string. ` +\n `Generate one with: node -e \"console.log(require('crypto').randomBytes(${expectedBytes}).toString('base64'))\"`\n );\n }\n\n // Try hex first (unambiguous — only hex chars, even length)\n const hexBuf = tryParseHex(keyString);\n if (hexBuf !== null) {\n if (hexBuf.byteLength !== expectedBytes) {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" decoded to ${hexBuf.byteLength} bytes from hex, ` +\n `but ${expectedBytes} bytes are required. ` +\n `A ${expectedBytes}-byte hex key must be ${expectedBytes * 2} hex characters.`\n );\n }\n return hexBuf;\n }\n\n // Try base64\n const b64Buf = tryParseBase64(keyString);\n if (b64Buf !== null) {\n if (b64Buf.byteLength !== expectedBytes) {\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" decoded to ${b64Buf.byteLength} bytes from base64, ` +\n `but ${expectedBytes} bytes are required. ` +\n `Generate a valid key with: node -e \"console.log(require('crypto').randomBytes(${expectedBytes}).toString('base64'))\"`\n );\n }\n return b64Buf;\n }\n\n throw new Error(\n `[orm-blind-indexer] \"${keyName}\" could not be parsed as hex or base64. ` +\n `Provide a ${expectedBytes * 2}-char hex string or a ${Math.ceil(expectedBytes * 4 / 3)}-char base64 string.`\n );\n}\n\n/**\n * Validates an encryption key for AES-256 (requires exactly 32 bytes / 256 bits).\n *\n * @param keyString - The raw encryption key string from config.\n * @returns A `Buffer` of exactly 32 bytes suitable for AES-256-GCM.\n * @throws {Error} If the key is invalid or not exactly 32 bytes.\n */\nexport function validateEncryptionKey(keyString: string): Buffer {\n return parseAndValidateKey(keyString, 32, 'encryptionKey');\n}\n\n/**\n * Parses the index key (HMAC salt) from a string.\n * Unlike the encryption key, the HMAC key has no strict length requirement in the\n * cryptographic spec (SHA-256 accepts any length), but we enforce a minimum of 16 bytes\n * for security best-practice guidance.\n *\n * @param keyString - The raw index key string from config.\n * @returns A `Buffer` usable as an HMAC key.\n * @throws {Error} If the key cannot be parsed or is shorter than 16 bytes.\n */\nexport function validateIndexKey(keyString: string): Buffer {\n if (!keyString || typeof keyString !== 'string') {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" must be a non-empty string. ` +\n `Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"`\n );\n }\n\n // Try hex\n const hexBuf = tryParseHex(keyString);\n if (hexBuf !== null) {\n if (hexBuf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${hexBuf.byteLength} bytes). ` +\n `Minimum recommended length is 16 bytes (32 hex chars).`\n );\n }\n return hexBuf;\n }\n\n // Try base64\n const b64Buf = tryParseBase64(keyString);\n if (b64Buf !== null) {\n if (b64Buf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${b64Buf.byteLength} bytes). ` +\n `Minimum recommended length is 16 bytes.`\n );\n }\n return b64Buf;\n }\n\n // Fall back to treating it as a raw UTF-8 string key (not recommended but functional)\n const rawBuf = Buffer.from(keyString, 'utf8');\n if (rawBuf.byteLength < 16) {\n throw new Error(\n `[orm-blind-indexer] \"indexKey\" is too short (${rawBuf.byteLength} bytes as UTF-8). ` +\n `Minimum recommended length is 16 bytes.`\n );\n }\n return rawBuf;\n}\n\n","/**\n * @file extension/interceptors.ts\n * @description Recursive WHERE clause transformation for blind index substitution.\n *\n * ## The Problem\n *\n * Prisma's `where` clause is a deeply nested object that can contain:\n * - Direct equality: `{ email: \"user@example.com\" }`\n * - Operator objects: `{ email: { in: [...], equals: \"...\" } }`\n * - Logical combinators: `{ AND: [...], OR: [...], NOT: { ... } }`\n * - Nested relations: `{ posts: { some: { author: { email: \"...\" } } } }`\n *\n * We need to intercept any occurrence of an encrypted field name and replace it\n * with a blind index lookup, regardless of how deeply nested it is.\n *\n * ## Strategy\n *\n * We use a **recursive depth-first traversal** of the `where` object:\n *\n * 1. For each key in the current object, check if it's a logical combinator\n * (`AND`, `OR`, `NOT`). If so, recurse into its children.\n * 2. If the key matches a configured encrypted field, transform the value:\n * - Remove the original field key.\n * - Add the `indexFieldName` key with the hashed value.\n * 3. Handle the `in` operator specially: hash each array element.\n *\n * ## Supported Prisma Filter Operators (on encrypted fields)\n *\n * | Prisma Operator | Supported | Notes |\n * |----------------|-----------|------------------------------------------|\n * | `equals` | ✅ | Direct value or `{ equals: value }` |\n * | `in` | ✅ | Hashes each element of the array |\n * | (direct) | ✅ | `{ email: \"value\" }` shorthand |\n * | `contains` | ❌ | LIKE queries incompatible with hashing |\n * | `startsWith` | ❌ | LIKE queries incompatible with hashing |\n * | `endsWith` | ❌ | LIKE queries incompatible with hashing |\n * | `lt/gt/lte/gte`| ❌ | Range queries incompatible with hashing |\n *\n * Attempting to use unsupported operators on encrypted fields will throw an error\n * guiding the developer to the correct approach.\n */\n\nimport { generateBlindIndex, generateBlindIndexBatch } from '../crypto/hasher.js';\nimport type { ModelFieldConfig } from '../types/index.js';\n\n/** Prisma logical combinator keys that contain nested where clause arrays or objects. */\nconst LOGICAL_COMBINATORS = new Set(['AND', 'OR', 'NOT']);\n\n/** Prisma filter operators that are incompatible with blind indexing. */\nconst UNSUPPORTED_OPERATORS = new Set([\n 'contains',\n 'startsWith',\n 'endsWith',\n 'lt',\n 'lte',\n 'gt',\n 'gte',\n 'search',\n]);\n\n/**\n * Transforms a Prisma `where` clause, replacing encrypted field lookups with\n * their corresponding blind index field lookups.\n *\n * This function is **pure** (it returns a new object, not mutating the original)\n * and **recursive** (it handles arbitrarily nested `AND`/`OR`/`NOT` clauses).\n *\n * @param where - The Prisma where clause object to transform.\n * @param modelConfig - The field configuration for the current model.\n * @param indexKey - The HMAC key Buffer for generating blind indexes.\n * @returns A new where clause object with encrypted fields replaced by blind index fields.\n * @throws {Error} If an unsupported operator is used on an encrypted field.\n */\nexport function transformWhereClause(\n where: Record<string, unknown>,\n modelConfig: ModelFieldConfig,\n indexKey: Buffer\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(where)) {\n // --- Handle logical combinators: AND, OR, NOT ---\n if (LOGICAL_COMBINATORS.has(key)) {\n if (Array.isArray(value)) {\n // AND: [...], OR: [...]\n result[key] = value.map((item) => {\n if (item !== null && typeof item === 'object' && !Array.isArray(item)) {\n return transformWhereClause(\n item as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n }\n return item;\n });\n } else if (value !== null && typeof value === 'object') {\n // NOT: { ... }\n result[key] = transformWhereClause(\n value as Record<string, unknown>,\n modelConfig,\n indexKey\n );\n } else {\n result[key] = value;\n }\n continue;\n }\n\n // --- Handle encrypted fields ---\n const fieldConfig = modelConfig[key];\n if (fieldConfig !== undefined) {\n const { indexFieldName, caseSensitive = false, truncateBytes } = fieldConfig;\n\n // Value is a plain string: `{ email: \"user@example.com\" }` shorthand\n if (typeof value === 'string') {\n result[indexFieldName] = generateBlindIndex(value, indexKey, caseSensitive, truncateBytes);\n continue;\n }\n\n // Value is null: pass through (querying for null encrypted fields)\n if (value === null) {\n result[indexFieldName] = null;\n continue;\n }\n\n // Value is an operator object: `{ email: { in: [...], equals: \"...\" } }`\n if (typeof value === 'object' && !Array.isArray(value)) {\n const operatorObj = value as Record<string, unknown>;\n const transformedOperator: Record<string, unknown> = {};\n\n for (const [op, opValue] of Object.entries(operatorObj)) {\n // Check for unsupported operators\n if (UNSUPPORTED_OPERATORS.has(op)) {\n throw new Error(\n `[orm-blind-indexer] The \"${op}\" filter operator is not supported on encrypted ` +\n `field \"${key}\". Blind indexes only support exact-match lookups (equals, in, notIn). ` +\n `Partial string searches on encrypted fields require application-level decryption.`\n );\n }\n\n // Handle Prisma's mode: \"insensitive\" / \"default\"\n if (op === 'mode') {\n if (opValue === 'insensitive' && caseSensitive) {\n throw new Error(\n `[orm-blind-indexer] \"mode: 'insensitive'\" cannot be used on encrypted field \"${key}\" ` +\n `configured with \"caseSensitive: true\". Change the field configuration to \"caseSensitive: false\" ` +\n `for case-insensitive blind index lookups.`\n );\n }\n // If caseSensitive is false, case insensitivity is inherently handled\n // during HMAC generation. We omit `mode` from the transformed query.\n continue;\n }\n\n if (op === 'equals') {\n if (typeof opValue === 'string') {\n transformedOperator['equals'] = generateBlindIndex(opValue, indexKey, caseSensitive, truncateBytes);\n } else if (opValue === null) {\n transformedOperator['equals'] = null;\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"equals\" operator on encrypted field \"${key}\" ` +\n `expects a string value, got ${typeof opValue}.`\n );\n }\n } else if (op === 'in') {\n if (Array.isArray(opValue)) {\n const strings = opValue.filter((v): v is string => typeof v === 'string');\n if (strings.length !== opValue.length) {\n throw new Error(\n `[orm-blind-indexer] The \"in\" operator on encrypted field \"${key}\" ` +\n `requires all values to be strings.`\n );\n }\n transformedOperator['in'] = generateBlindIndexBatch(strings, indexKey, caseSensitive, truncateBytes);\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"in\" operator on encrypted field \"${key}\" ` +\n `expects an array, got ${typeof opValue}.`\n );\n }\n } else if (op === 'notIn') {\n if (Array.isArray(opValue)) {\n const strings = opValue.filter((v): v is string => typeof v === 'string');\n if (strings.length !== opValue.length) {\n throw new Error(\n `[orm-blind-indexer] The \"notIn\" operator on encrypted field \"${key}\" ` +\n `requires all values to be strings.`\n );\n }\n transformedOperator['notIn'] = generateBlindIndexBatch(strings, indexKey, caseSensitive, truncateBytes);\n } else {\n throw new Error(\n `[orm-blind-indexer] The \"notIn\" operator on encrypted field \"${key}\" ` +\n `expects an array, got ${typeof opValue}.`\n );\n }\n } else if (op === 'not') {\n // `NOT` as a filter operator (different from `NOT` combinator)\n if (typeof opValue === 'string') {\n transformedOperator['not'] = generateBlindIndex(opValue, indexKey, caseSensitive, truncateBytes);\n } else if (opValue === null) {\n transformedOperator['not'] = null;\n } else {\n transformedOperator['not'] = opValue;\n }\n } else {\n // Pass through unknown operators (future Prisma operators)\n transformedOperator[op] = opValue;\n }\n }\n\n result[indexFieldName] = transformedOperator;\n continue;\n }\n\n // Fallback: pass through as-is (handles edge cases)\n result[key] = value;\n continue;\n }\n\n // --- Non-encrypted field: pass through unchanged ---\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Strips blind index fields (`_bidx` columns) from a query result object\n * so the caller only sees the logical schema (without implementation details).\n *\n * This function also handles arrays of results (for `findMany`) and\n * recursively cleans nested relation objects.\n *\n * @param result - The raw result from Prisma (single object or array).\n * @param modelConfig - The field configuration for the current model.\n * @returns The cleaned result with `_bidx` fields removed.\n */\nexport function stripBlindIndexFields(\n result: unknown,\n modelConfig: ModelFieldConfig\n): unknown {\n if (result === null || result === undefined) {\n return result;\n }\n\n if (Array.isArray(result)) {\n return result.map((item) => stripBlindIndexFields(item, modelConfig));\n }\n\n if (typeof result === 'object') {\n const obj = result as Record<string, unknown>;\n const cleaned: Record<string, unknown> = {};\n\n // Collect all bidx field names to exclude\n const bidxFieldNames = new Set(\n Object.values(modelConfig).map((cfg) => cfg.indexFieldName)\n );\n\n for (const [key, value] of Object.entries(obj)) {\n if (!bidxFieldNames.has(key)) {\n // Recursively clean nested objects (for relations)\n if (value !== null && typeof value === 'object') {\n cleaned[key] = stripBlindIndexFields(value, modelConfig);\n } else {\n cleaned[key] = value;\n }\n }\n // Skip bidx fields — don't include them in output\n }\n\n return cleaned;\n }\n\n return result;\n}\n\n"],"mappings":";AA2DA,SAAS,cAAc;;;ACrBvB,SAAS,gBAAgB,kBAAkB,mBAAmB;AAI9D,IAAM,YAAY;AAGlB,IAAM,kBAAkB;AAGxB,IAAM,wBAAwB;AAG9B,IAAM,YAAY;AAgBX,SAAS,QAAQ,WAAmB,KAAqB;AAE9D,QAAM,KAAK,YAAY,eAAe;AAItC,QAAM,SAAS,eAAe,WAAW,KAAK,IAAI;AAAA,IAChD,eAAe;AAAA,EACjB,CAAC;AAID,QAAM,YAAY,OAAO,OAAO;AAAA,IAC9B,OAAO,OAAO,WAAW,MAAM;AAAA,IAC/B,OAAO,MAAM;AAAA,EACf,CAAC;AAGD,QAAM,UAAU,OAAO,WAAW;AAGlC,SAAO;AAAA,IACL,GAAG,SAAS,QAAQ;AAAA,IACpB,QAAQ,SAAS,QAAQ;AAAA,IACzB,UAAU,SAAS,QAAQ;AAAA,EAC7B,EAAE,KAAK,SAAS;AAClB;AASA,SAAS,gBAAgB,YAAsC;AAC7D,QAAM,QAAQ,WAAW,MAAM,SAAS;AACxC,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI;AAAA,MACR,0IAEW,MAAM,SAAS,CAAC;AAAA,IAE7B;AAAA,EACF;AAEA,QAAM,CAAC,OAAO,YAAY,aAAa,IAAI;AAE3C,QAAM,KAAK,OAAO,KAAK,OAAO,QAAQ;AACtC,QAAM,UAAU,OAAO,KAAK,YAAY,QAAQ;AAChD,QAAM,aAAa,OAAO,KAAK,eAAe,QAAQ;AAEtD,MAAI,GAAG,eAAe,iBAAiB;AACrC,UAAM,IAAI;AAAA,MACR,iEACY,eAAe,eAAe,GAAG,UAAU;AAAA,IACzD;AAAA,EACF;AAEA,MAAI,QAAQ,eAAe,uBAAuB;AAChD,UAAM,IAAI;AAAA,MACR,uEACY,qBAAqB,eAAe,QAAQ,UAAU;AAAA,IACpE;AAAA,EACF;AAEA,SAAO,EAAE,IAAI,SAAS,WAAW;AACnC;AAqBO,SAAS,QAAQ,YAAoB,KAAqB;AAC/D,QAAM,EAAE,IAAI,SAAS,WAAW,IAAI,gBAAgB,UAAU;AAE9D,QAAM,WAAW,iBAAiB,WAAW,KAAK,IAAI;AAAA,IACpD,eAAe;AAAA,EACjB,CAAC;AAID,WAAS,WAAW,OAAO;AAG3B,QAAM,YAAY,OAAO,OAAO;AAAA,IAC9B,SAAS,OAAO,UAAU;AAAA,IAC1B,SAAS,MAAM;AAAA;AAAA,EACjB,CAAC;AAED,SAAO,UAAU,SAAS,MAAM;AAClC;AAWO,SAAS,kBAAkB,OAAwB;AACxD,QAAM,QAAQ,MAAM,MAAM,SAAS;AACnC,MAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,QAAM,gBAAgB;AACtB,SAAO,MAAM;AAAA,IACX,CAAC,SAAkB,OAAO,SAAS,YAAY,KAAK,SAAS,KAAK,cAAc,KAAK,IAAI;AAAA,EAC3F;AACF;;;AC5JA,SAAS,kBAAkB;AAG3B,IAAM,iBAAiB;AAgChB,SAAS,mBACd,MACA,UACA,gBAAyB,OACzB,eACQ;AAER,QAAM,aAAa,gBAAgB,OAAO,KAAK,YAAY;AAE3D,QAAM,SAAS,WAAW,gBAAgB,QAAQ,EAC/C,OAAO,YAAY,MAAM,EACzB,OAAO;AAEV,MAAI,kBAAkB,QAAW;AAC/B,QAAI,gBAAgB,KAAK,gBAAgB,IAAI;AAC3C,YAAM,IAAI;AAAA,QACR,2EAA2E,aAAa;AAAA,MAC1F;AAAA,IACF;AACA,WAAO,OAAO,SAAS,GAAG,aAAa,EAAE,SAAS,QAAQ;AAAA,EAC5D;AAEA,SAAO,OAAO,SAAS,QAAQ;AACjC;AAYO,SAAS,wBACd,QACA,UACA,gBAAyB,OACzB,eACU;AACV,SAAO,OAAO,IAAI,CAAC,UAAU,mBAAmB,OAAO,UAAU,eAAe,aAAa,CAAC;AAChG;;;ACpGA,SAAS,eAAe,OAA8B;AACpD,MAAI;AACF,UAAM,MAAM,OAAO,KAAK,OAAO,QAAQ;AAEvC,QAAI,IAAI,SAAS,QAAQ,MAAM,SAAS,IAAI,SAAS,WAAW,MAAM,OAAO;AAC3E,aAAO;AAAA,IACT;AAIA,WAAO,IAAI,aAAa,IAAI,MAAM;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,SAAS,YAAY,OAA8B;AACjD,MAAI,CAAC,iBAAiB,KAAK,KAAK,KAAK,MAAM,SAAS,MAAM,GAAG;AAC3D,WAAO;AAAA,EACT;AACA,SAAO,OAAO,KAAK,OAAO,KAAK;AACjC;AAkBO,SAAS,oBACd,WACA,eACA,SACQ;AACR,MAAI,CAAC,aAAa,OAAO,cAAc,UAAU;AAC/C,UAAM,IAAI;AAAA,MACR,wBAAwB,OAAO,uGAC0C,aAAa;AAAA,IACxF;AAAA,EACF;AAGA,QAAM,SAAS,YAAY,SAAS;AACpC,MAAI,WAAW,MAAM;AACnB,QAAI,OAAO,eAAe,eAAe;AACvC,YAAM,IAAI;AAAA,QACR,wBAAwB,OAAO,gBAAgB,OAAO,UAAU,wBACzD,aAAa,0BACf,aAAa,yBAAyB,gBAAgB,CAAC;AAAA,MAC9D;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,eAAe,SAAS;AACvC,MAAI,WAAW,MAAM;AACnB,QAAI,OAAO,eAAe,eAAe;AACvC,YAAM,IAAI;AAAA,QACR,wBAAwB,OAAO,gBAAgB,OAAO,UAAU,2BACzD,aAAa,sGAC6D,aAAa;AAAA,MAChG;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,IAAI;AAAA,IACR,wBAAwB,OAAO,qDAClB,gBAAgB,CAAC,yBAAyB,KAAK,KAAK,gBAAgB,IAAI,CAAC,CAAC;AAAA,EACzF;AACF;AASO,SAAS,sBAAsB,WAA2B;AAC/D,SAAO,oBAAoB,WAAW,IAAI,eAAe;AAC3D;AAYO,SAAS,iBAAiB,WAA2B;AAC1D,MAAI,CAAC,aAAa,OAAO,cAAc,UAAU;AAC/C,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAGA,QAAM,SAAS,YAAY,SAAS;AACpC,MAAI,WAAW,MAAM;AACnB,QAAI,OAAO,aAAa,IAAI;AAC1B,YAAM,IAAI;AAAA,QACR,gDAAgD,OAAO,UAAU;AAAA,MAEnE;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,eAAe,SAAS;AACvC,MAAI,WAAW,MAAM;AACnB,QAAI,OAAO,aAAa,IAAI;AAC1B,YAAM,IAAI;AAAA,QACR,gDAAgD,OAAO,UAAU;AAAA,MAEnE;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,OAAO,KAAK,WAAW,MAAM;AAC5C,MAAI,OAAO,aAAa,IAAI;AAC1B,UAAM,IAAI;AAAA,MACR,gDAAgD,OAAO,UAAU;AAAA,IAEnE;AAAA,EACF;AACA,SAAO;AACT;;;ACrHA,IAAM,sBAAsB,oBAAI,IAAI,CAAC,OAAO,MAAM,KAAK,CAAC;AAGxD,IAAM,wBAAwB,oBAAI,IAAI;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAeM,SAAS,qBACd,OACA,aACA,UACyB;AACzB,QAAM,SAAkC,CAAC;AAEzC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAEhD,QAAI,oBAAoB,IAAI,GAAG,GAAG;AAChC,UAAI,MAAM,QAAQ,KAAK,GAAG;AAExB,eAAO,GAAG,IAAI,MAAM,IAAI,CAAC,SAAS;AAChC,cAAI,SAAS,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,GAAG;AACrE,mBAAO;AAAA,cACL;AAAA,cACA;AAAA,cACA;AAAA,YACF;AAAA,UACF;AACA,iBAAO;AAAA,QACT,CAAC;AAAA,MACH,WAAW,UAAU,QAAQ,OAAO,UAAU,UAAU;AAEtD,eAAO,GAAG,IAAI;AAAA,UACZ;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF,OAAO;AACL,eAAO,GAAG,IAAI;AAAA,MAChB;AACA;AAAA,IACF;AAGA,UAAM,cAAc,YAAY,GAAG;AACnC,QAAI,gBAAgB,QAAW;AAC7B,YAAM,EAAE,gBAAgB,gBAAgB,OAAO,cAAc,IAAI;AAGjE,UAAI,OAAO,UAAU,UAAU;AAC7B,eAAO,cAAc,IAAI,mBAAmB,OAAO,UAAU,eAAe,aAAa;AACzF;AAAA,MACF;AAGA,UAAI,UAAU,MAAM;AAClB,eAAO,cAAc,IAAI;AACzB;AAAA,MACF;AAGA,UAAI,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AACtD,cAAM,cAAc;AACpB,cAAM,sBAA+C,CAAC;AAEtD,mBAAW,CAAC,IAAI,OAAO,KAAK,OAAO,QAAQ,WAAW,GAAG;AAEvD,cAAI,sBAAsB,IAAI,EAAE,GAAG;AACjC,kBAAM,IAAI;AAAA,cACR,4BAA4B,EAAE,0DACpB,GAAG;AAAA,YAEf;AAAA,UACF;AAGA,cAAI,OAAO,QAAQ;AACjB,gBAAI,YAAY,iBAAiB,eAAe;AAC9C,oBAAM,IAAI;AAAA,gBACR,gFAAgF,GAAG;AAAA,cAGrF;AAAA,YACF;AAGA;AAAA,UACF;AAEA,cAAI,OAAO,UAAU;AACnB,gBAAI,OAAO,YAAY,UAAU;AAC/B,kCAAoB,QAAQ,IAAI,mBAAmB,SAAS,UAAU,eAAe,aAAa;AAAA,YACpG,WAAW,YAAY,MAAM;AAC3B,kCAAoB,QAAQ,IAAI;AAAA,YAClC,OAAO;AACL,oBAAM,IAAI;AAAA,gBACR,iEAAiE,GAAG,iCACrC,OAAO,OAAO;AAAA,cAC/C;AAAA,YACF;AAAA,UACF,WAAW,OAAO,MAAM;AACtB,gBAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,oBAAM,UAAU,QAAQ,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;AACxE,kBAAI,QAAQ,WAAW,QAAQ,QAAQ;AACrC,sBAAM,IAAI;AAAA,kBACR,6DAA6D,GAAG;AAAA,gBAElE;AAAA,cACF;AACA,kCAAoB,IAAI,IAAI,wBAAwB,SAAS,UAAU,eAAe,aAAa;AAAA,YACrG,OAAO;AACL,oBAAM,IAAI;AAAA,gBACR,6DAA6D,GAAG,2BACvC,OAAO,OAAO;AAAA,cACzC;AAAA,YACF;AAAA,UACF,WAAW,OAAO,SAAS;AACzB,gBAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,oBAAM,UAAU,QAAQ,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;AACxE,kBAAI,QAAQ,WAAW,QAAQ,QAAQ;AACrC,sBAAM,IAAI;AAAA,kBACR,gEAAgE,GAAG;AAAA,gBAErE;AAAA,cACF;AACA,kCAAoB,OAAO,IAAI,wBAAwB,SAAS,UAAU,eAAe,aAAa;AAAA,YACxG,OAAO;AACL,oBAAM,IAAI;AAAA,gBACR,gEAAgE,GAAG,2BAC1C,OAAO,OAAO;AAAA,cACzC;AAAA,YACF;AAAA,UACF,WAAW,OAAO,OAAO;AAEvB,gBAAI,OAAO,YAAY,UAAU;AAC/B,kCAAoB,KAAK,IAAI,mBAAmB,SAAS,UAAU,eAAe,aAAa;AAAA,YACjG,WAAW,YAAY,MAAM;AAC3B,kCAAoB,KAAK,IAAI;AAAA,YAC/B,OAAO;AACL,kCAAoB,KAAK,IAAI;AAAA,YAC/B;AAAA,UACF,OAAO;AAEL,gCAAoB,EAAE,IAAI;AAAA,UAC5B;AAAA,QACF;AAEA,eAAO,cAAc,IAAI;AACzB;AAAA,MACF;AAGA,aAAO,GAAG,IAAI;AACd;AAAA,IACF;AAGA,WAAO,GAAG,IAAI;AAAA,EAChB;AAEA,SAAO;AACT;AAaO,SAAS,sBACd,QACA,aACS;AACT,MAAI,WAAW,QAAQ,WAAW,QAAW;AAC3C,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO,OAAO,IAAI,CAAC,SAAS,sBAAsB,MAAM,WAAW,CAAC;AAAA,EACtE;AAEA,MAAI,OAAO,WAAW,UAAU;AAC9B,UAAM,MAAM;AACZ,UAAM,UAAmC,CAAC;AAG1C,UAAM,iBAAiB,IAAI;AAAA,MACzB,OAAO,OAAO,WAAW,EAAE,IAAI,CAAC,QAAQ,IAAI,cAAc;AAAA,IAC5D;AAEA,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,UAAI,CAAC,eAAe,IAAI,GAAG,GAAG;AAE5B,YAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,kBAAQ,GAAG,IAAI,sBAAsB,OAAO,WAAW;AAAA,QACzD,OAAO;AACL,kBAAQ,GAAG,IAAI;AAAA,QACjB;AAAA,MACF;AAAA,IAEF;AAEA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;;;AJjNA,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,IAAM,kBAAkB,oBAAI,IAAI;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAYD,SAAS,iBACP,MACA,aACA,eACA,UACyB;AACzB,QAAM,SAAS,EAAE,GAAG,KAAK;AAEzB,aAAW,CAAC,WAAW,WAAW,KAAK,OAAO,QAAQ,WAAW,GAAG;AAClE,UAAM,QAAQ,OAAO,SAAS;AAE9B,QAAI,UAAU,UAAa,UAAU,MAAM;AAEzC;AAAA,IACF;AAEA,QAAI,OAAO,UAAU,UAAU;AAE7B;AAAA,IACF;AAGA,QAAI,kBAAkB,KAAK,GAAG;AAC5B;AAAA,IACF;AAGA,WAAO,SAAS,IAAI,QAAQ,OAAO,aAAa;AAGhD,WAAO,YAAY,cAAc,IAAI;AAAA,MACnC;AAAA,MACA;AAAA,MACA,YAAY,iBAAiB;AAAA,IAC/B;AAAA,EACF;AAEA,SAAO;AACT;AAWA,SAAS,cACP,QACA,aACA,eACS;AACT,MAAI,WAAW,QAAQ,WAAW,QAAW;AAC3C,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO,OAAO,IAAI,CAAC,SAAS,cAAc,MAAM,aAAa,aAAa,CAAC;AAAA,EAC7E;AAEA,MAAI,OAAO,WAAW,UAAU;AAC9B,UAAM,MAAM;AAEZ,eAAW,CAAC,SAAS,KAAK,OAAO,QAAQ,WAAW,GAAG;AACrD,YAAM,QAAQ,IAAI,SAAS;AAC3B,UAAI,OAAO,UAAU,YAAY,kBAAkB,KAAK,GAAG;AACzD,YAAI;AACF,cAAI,SAAS,IAAI,QAAQ,OAAO,aAAa;AAAA,QAC/C,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,WAAO,sBAAsB,KAAK,WAAW;AAAA,EAC/C;AAEA,SAAO;AACT;AA+BO,SAAS,iBAAiB,QAA0B;AAEzD,QAAM,gBAAgB,sBAAsB,OAAO,aAAa;AAChE,QAAM,WAAW,iBAAiB,OAAO,QAAQ;AAEjD,SAAO,OAAO,gBAAgB;AAAA,IAC5B,MAAM;AAAA,IAEN,OAAO;AAAA,MACL,YAAY;AAAA,QACV,MAAM,eAAe,EAAE,OAAO,WAAW,MAAM,MAAM,GAAG;AAEtD,gBAAM,cACJ,UAAU,SAAY,OAAO,OAAO,KAAK,IAAI;AAG/C,cAAI,gBAAgB,UAAa,OAAO,KAAK,WAAW,EAAE,WAAW,GAAG;AACtE,mBAAO,MAAM,IAAI;AAAA,UACnB;AAEA,gBAAM,YAAY;AAKlB,cAAI,iBAAiB,IAAI,SAAS,GAAG;AACnC,gBAAI,cAAc,UAAU;AAE1B,kBAAI,UAAU,QAAQ,KAAK,OAAO,UAAU,QAAQ,MAAM,UAAU;AAClE,0BAAU,QAAQ,IAAI;AAAA,kBACpB,UAAU,QAAQ;AAAA,kBAClB;AAAA,kBACA;AAAA,kBACA;AAAA,gBACF;AAAA,cACF;AACA,kBAAI,UAAU,QAAQ,KAAK,OAAO,UAAU,QAAQ,MAAM,UAAU;AAClE,0BAAU,QAAQ,IAAI;AAAA,kBACpB,UAAU,QAAQ;AAAA,kBAClB;AAAA,kBACA;AAAA,kBACA;AAAA,gBACF;AAAA,cACF;AAAA,YACF,WAAW,cAAc,cAAc;AAErC,kBAAI,MAAM,QAAQ,UAAU,MAAM,CAAC,GAAG;AACpC,0BAAU,MAAM,IAAK,UAAU,MAAM,EAAgC;AAAA,kBACnE,CAAC,SAAS,iBAAiB,MAAM,aAAa,eAAe,QAAQ;AAAA,gBACvE;AAAA,cACF;AAAA,YACF,OAAO;AAEL,kBAAI,UAAU,MAAM,KAAK,OAAO,UAAU,MAAM,MAAM,UAAU;AAC9D,0BAAU,MAAM,IAAI;AAAA,kBAClB,UAAU,MAAM;AAAA,kBAChB;AAAA,kBACA;AAAA,kBACA;AAAA,gBACF;AAAA,cACF;AAAA,YACF;AAGA,gBAAI,UAAU,OAAO,KAAK,OAAO,UAAU,OAAO,MAAM,UAAU;AAChE,wBAAU,OAAO,IAAI;AAAA,gBACnB,UAAU,OAAO;AAAA,gBACjB;AAAA,gBACA;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAKA,cAAI,gBAAgB,IAAI,SAAS,GAAG;AAClC,gBAAI,UAAU,OAAO,KAAK,OAAO,UAAU,OAAO,MAAM,UAAU;AAChE,wBAAU,OAAO,IAAI;AAAA,gBACnB,UAAU,OAAO;AAAA,gBACjB;AAAA,gBACA;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAGA,gBAAM,SAAS,MAAM,MAAM,IAAI;AAM/B,cAAI,cAAc,WAAW,cAAc,eAAe,cAAc,WAAW;AACjF,mBAAO;AAAA,UACT;AAEA,iBAAO,cAAc,QAAQ,aAAa,aAAa;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orm-blind-indexer",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A Prisma Client Extension implementing Blind Indexing for secure, searchable AES-256-GCM encrypted fields using HMAC-SHA256 blind indexes.",
5
5
  "keywords": [
6
6
  "prisma",