orm-blind-indexer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oussama Berrada Ifriqi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,636 @@
1
+ # orm-blind-indexer
2
+
3
+ > A Prisma Client Extension that implements **Blind Indexing** for secure, searchable encrypted database fields.
4
+ >
5
+ > Author: **Oussama Berrada Ifriqi**
6
+
7
+ [![CI](https://github.com/oussamaberradaifriqi/orm-blind-indexer/actions/workflows/ci.yml/badge.svg)](https://github.com/oussamaberradaifriqi/orm-blind-indexer/actions/workflows/ci.yml)
8
+ [![npm version](https://badge.fury.io/js/orm-blind-indexer.svg)](https://badge.fury.io/js/orm-blind-indexer)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
11
+
12
+ ---
13
+
14
+ ## The Problem
15
+
16
+ Encrypting sensitive fields like emails breaks database lookups. If `alice@example.com` is stored as an AES-256-GCM ciphertext like `abc123...`, a `WHERE email = 'alice@example.com'` query simply fails — the database can only compare ciphertexts, and AES-GCM uses a random IV so the same plaintext produces a different ciphertext every time.
17
+
18
+ ## The Solution: Blind Indexing
19
+
20
+ Blind Indexing stores a **deterministic HMAC-SHA256 hash** of the plaintext in a separate `_bidx` column alongside the encrypted value.
21
+
22
+ ```
23
+ ┌──────────────────────────────────────────────────┬──────────────────────────┐
24
+ │ email (AES-256-GCM ciphertext, random IV) │ email_bidx (HMAC-SHA256) │
25
+ ├──────────────────────────────────────────────────┼──────────────────────────┤
26
+ │ dGVzdA==:abc123==:xyz789== │ k7a8b...== (base64) │
27
+ └──────────────────────────────────────────────────┴──────────────────────────┘
28
+ ```
29
+
30
+ - **On write**: The extension encrypts the value with AES-256-GCM and injects the HMAC hash automatically.
31
+ - **On read**: The extension intercepts the `WHERE` clause, hashes the search term, and queries `email_bidx` instead.
32
+ - **On result**: The extension decrypts the ciphertext back to plaintext and strips `_bidx` fields before returning.
33
+
34
+ The developer's code is completely transparent — they always work with plaintext.
35
+
36
+ ---
37
+
38
+ ## Security Properties
39
+
40
+ | Property | Implementation |
41
+ |-----------------------------|---------------------------------------------------------------|
42
+ | **Confidentiality** | AES-256-GCM (authenticated encryption, random 12-byte IV) |
43
+ | **Integrity / Authenticity**| 16-byte GCM Auth Tag — decryption fails if tampered |
44
+ | **Query Security** | HMAC-SHA256 keyed hash — requires secret key to compute |
45
+ | **No Double-Encryption** | Idempotency check prevents encrypting already-encrypted data |
46
+ | **Case-Insensitive Search** | Optional lowercasing before hashing (configurable per field) |
47
+
48
+ > [!CAUTION]
49
+ > Blind indexes reveal **equality relationships**: an attacker with the database can tell if two records have the same email, but cannot determine the actual email without the index key. This is an inherent trade-off of searchable encryption. For highly sensitive use cases, see [CipherSweet](https://ciphersweet.paragonie.com/) for more advanced schemes.
50
+
51
+ ---
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ npm install orm-blind-indexer
57
+ # Peer dependencies (if not already installed):
58
+ npm install prisma @prisma/client
59
+ ```
60
+
61
+ **Node.js 18+** is required (uses the native `crypto` module — zero external dependencies).
62
+
63
+ ---
64
+
65
+ ## Quick Start
66
+
67
+ ### 1. Update Your Prisma Schema
68
+
69
+ Add a `_bidx` column for every field you want to encrypt and query:
70
+
71
+ ```prisma
72
+ model User {
73
+ id String @id @default(cuid())
74
+ name String
75
+
76
+ // The encrypted email (AES-256-GCM ciphertext)
77
+ email String @unique
78
+
79
+ // The blind index (HMAC-SHA256 hash for queries)
80
+ email_bidx String @unique
81
+
82
+ createdAt DateTime @default(now())
83
+ updatedAt DateTime @updatedAt
84
+ }
85
+ ```
86
+
87
+ Run `npx prisma migrate dev` to apply the schema changes.
88
+
89
+ ### 2. Generate Secure Keys
90
+
91
+ ```bash
92
+ # Generate the AES-256 encryption key (32 bytes → 44-char base64)
93
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
94
+
95
+ # Generate the HMAC index key (separate from the encryption key!)
96
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
97
+ ```
98
+
99
+ Add them to your `.env`:
100
+
101
+ ```bash
102
+ ENCRYPTION_KEY="<your-32-byte-base64-encryption-key>"
103
+ INDEX_KEY="<your-32-byte-base64-index-key>"
104
+ ```
105
+
106
+ > [!IMPORTANT]
107
+ > Keep these keys secret and **never commit them to version control**. Store them in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) in production. Losing the encryption key means your data is permanently unrecoverable.
108
+
109
+ ### 3. Apply the Extension
110
+
111
+ ```typescript
112
+ import { PrismaClient } from '@prisma/client';
113
+ import { withBlindIndexer } from 'orm-blind-indexer';
114
+
115
+ const prisma = new PrismaClient().$extends(
116
+ withBlindIndexer({
117
+ encryptionKey: process.env.ENCRYPTION_KEY!,
118
+ indexKey: process.env.INDEX_KEY!,
119
+ fields: {
120
+ User: {
121
+ email: {
122
+ indexFieldName: 'email_bidx',
123
+ caseSensitive: false, // "Test@Test.com" matches "test@test.com"
124
+ },
125
+ },
126
+ },
127
+ })
128
+ );
129
+
130
+ export default prisma;
131
+ ```
132
+
133
+ ### 4. Use It Transparently
134
+
135
+ The extension works invisibly — write your Prisma queries as normal:
136
+
137
+ ```typescript
138
+ // CREATE: email is automatically encrypted, email_bidx is auto-injected
139
+ const user = await prisma.user.create({
140
+ data: { name: 'Alice', email: 'alice@example.com' },
141
+ });
142
+ // user.email === 'alice@example.com' (decrypted in result)
143
+ // user.email_bidx === undefined (stripped from result)
144
+
145
+ // FIND: WHERE email is auto-transformed to WHERE email_bidx = hash(...)
146
+ const found = await prisma.user.findUnique({
147
+ where: { email: 'alice@example.com' },
148
+ });
149
+ // found.email === 'alice@example.com' (decrypted from DB)
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Configuration Reference
155
+
156
+ ```typescript
157
+ interface BlindIndexConfig {
158
+ /**
159
+ * AES-256-GCM encryption key.
160
+ * Must decode to exactly 32 bytes.
161
+ * Accepts: 44-char base64 string OR 64-char hex string.
162
+ */
163
+ encryptionKey: string;
164
+
165
+ /**
166
+ * HMAC-SHA256 index key (salt).
167
+ * Must be different from encryptionKey!
168
+ * Minimum 16 bytes recommended; 32 bytes is best practice.
169
+ */
170
+ indexKey: string;
171
+
172
+ /**
173
+ * Map of model names to their encrypted field configurations.
174
+ */
175
+ fields: {
176
+ [ModelName: string]: {
177
+ [fieldName: string]: {
178
+ /** Name of the blind index column in the database. */
179
+ indexFieldName: string;
180
+ /**
181
+ * When false (default), input is lowercased before hashing.
182
+ * Enables case-insensitive equality searches.
183
+ */
184
+ caseSensitive?: boolean;
185
+ };
186
+ };
187
+ };
188
+ }
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Supported Query Patterns
194
+
195
+ ### ✅ Supported (Exact-Match)
196
+
197
+ ```typescript
198
+ // Direct equality
199
+ prisma.user.findUnique({ where: { email: 'alice@example.com' } });
200
+
201
+ // Explicit equals operator
202
+ prisma.user.findMany({ where: { email: { equals: 'alice@example.com' } } });
203
+
204
+ // IN operator (hashes each element)
205
+ prisma.user.findMany({ where: { email: { in: ['alice@example.com', 'bob@example.com'] } } });
206
+
207
+ // AND / OR / NOT combinators
208
+ prisma.user.findMany({
209
+ where: {
210
+ OR: [
211
+ { email: 'alice@example.com' },
212
+ { email: 'bob@example.com' },
213
+ ],
214
+ },
215
+ });
216
+
217
+ // Nested AND
218
+ prisma.user.findMany({
219
+ where: {
220
+ AND: [{ email: 'alice@example.com' }, { name: 'Alice' }],
221
+ },
222
+ });
223
+ ```
224
+
225
+ ### ❌ Not Supported (Partial-Match)
226
+
227
+ ```typescript
228
+ // These will throw a helpful error:
229
+ prisma.user.findMany({ where: { email: { contains: 'example.com' } } });
230
+ prisma.user.findMany({ where: { email: { startsWith: 'alice' } } });
231
+ prisma.user.findMany({ where: { email: { endsWith: '.com' } } });
232
+ ```
233
+
234
+ Partial-match (LIKE) queries are fundamentally incompatible with deterministic hashing. For substring search on encrypted fields, you need to fetch and decrypt records in your application layer.
235
+
236
+ ---
237
+
238
+ ## Multiple Encrypted Fields
239
+
240
+ ```typescript
241
+ // Schema:
242
+ model User {
243
+ id String @id @default(cuid())
244
+ email String @unique
245
+ email_bidx String @unique
246
+ phoneNumber String?
247
+ phone_bidx String? @unique
248
+ ssn String?
249
+ ssn_bidx String? @unique
250
+ }
251
+
252
+ // Extension config:
253
+ withBlindIndexer({
254
+ encryptionKey: process.env.ENCRYPTION_KEY!,
255
+ indexKey: process.env.INDEX_KEY!,
256
+ fields: {
257
+ User: {
258
+ email: { indexFieldName: 'email_bidx', caseSensitive: false },
259
+ phoneNumber: { indexFieldName: 'phone_bidx', caseSensitive: true },
260
+ ssn: { indexFieldName: 'ssn_bidx', caseSensitive: true },
261
+ },
262
+ },
263
+ })
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Multiple Models
269
+
270
+ ```typescript
271
+ withBlindIndexer({
272
+ encryptionKey: process.env.ENCRYPTION_KEY!,
273
+ indexKey: process.env.INDEX_KEY!,
274
+ fields: {
275
+ User: {
276
+ email: { indexFieldName: 'email_bidx' },
277
+ },
278
+ Patient: {
279
+ dateOfBirth: { indexFieldName: 'dob_bidx', caseSensitive: true },
280
+ ssn: { indexFieldName: 'ssn_bidx', caseSensitive: true },
281
+ },
282
+ },
283
+ })
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Advanced: Using Crypto Primitives Directly
289
+
290
+ The package exports its cryptographic primitives for advanced use cases:
291
+
292
+ ```typescript
293
+ import {
294
+ encrypt,
295
+ decrypt,
296
+ generateBlindIndex,
297
+ validateEncryptionKey,
298
+ } from 'orm-blind-indexer';
299
+
300
+ // Standalone encryption
301
+ const key = validateEncryptionKey(process.env.ENCRYPTION_KEY!);
302
+ const ciphertext = encrypt('sensitive data', key);
303
+ const plaintext = decrypt(ciphertext, key);
304
+
305
+ // Standalone blind index generation
306
+ import { validateIndexKey } from 'orm-blind-indexer';
307
+ const indexKey = validateIndexKey(process.env.INDEX_KEY!);
308
+ const hash = generateBlindIndex('alice@example.com', indexKey, false);
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Key Rotation
314
+
315
+ Rotating encryption keys requires a data migration:
316
+
317
+ 1. Decrypt all existing records with the old key.
318
+ 2. Re-encrypt with the new key.
319
+ 3. Regenerate all blind indexes with the new index key.
320
+
321
+ > [!WARNING]
322
+ > Key rotation must be atomic. Run it in a transaction and deploy the new application binary (with new keys in env vars) simultaneously with the migration to avoid downtime.
323
+
324
+ ---
325
+
326
+ ## Framework Integration Guides
327
+
328
+ `orm-blind-indexer` is built as a universal dual CommonJS + ESM package with full TypeScript definitions, compatible with any modern JavaScript/TypeScript framework running Node.js 18+.
329
+
330
+ > [!NOTE]
331
+ > Database ORMs and cryptographic operations run exclusively in **server-side runtimes** (Node.js, Bun, serverless containers). Never import Prisma or encryption keys in client-side component code.
332
+
333
+ ### 1. Next.js (App Router, Server Actions, Route Handlers)
334
+
335
+ In Next.js, hot module reloading (HMR) during local development can create multiple Prisma instances if not memoized on `globalThis`. Use the singleton pattern:
336
+
337
+ ```typescript
338
+ // lib/prisma.ts
339
+ import { PrismaClient } from '@prisma/client';
340
+ import { withBlindIndexer } from 'orm-blind-indexer';
341
+
342
+ const prismaClientSingleton = () => {
343
+ return new PrismaClient().$extends(
344
+ withBlindIndexer({
345
+ encryptionKey: process.env.ENCRYPTION_KEY!,
346
+ indexKey: process.env.INDEX_KEY!,
347
+ fields: {
348
+ User: {
349
+ email: { indexFieldName: 'email_bidx', caseSensitive: false },
350
+ },
351
+ },
352
+ })
353
+ );
354
+ };
355
+
356
+ declare global {
357
+ var prismaGlobal: undefined | ReturnType<typeof prismaClientSingleton>;
358
+ }
359
+
360
+ export const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
361
+
362
+ if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma;
363
+ ```
364
+
365
+ Then in any Server Action or Route Handler:
366
+
367
+ ```typescript
368
+ // app/actions/user.ts
369
+ 'use server';
370
+
371
+ import { prisma } from '@/lib/prisma';
372
+
373
+ export async function getUserByEmail(email: string) {
374
+ // Query operates transparently through the blind index
375
+ return await prisma.user.findUnique({
376
+ where: { email },
377
+ });
378
+ }
379
+ ```
380
+
381
+ ---
382
+
383
+ ### 2. NestJS (Angular-Style Architecture / Backend)
384
+
385
+ In NestJS, extend `PrismaClient` within a custom service and register it as an injectable provider:
386
+
387
+ ```typescript
388
+ // src/prisma/prisma.service.ts
389
+ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
390
+ import { PrismaClient } from '@prisma/client';
391
+ import { withBlindIndexer } from 'orm-blind-indexer';
392
+
393
+ function createExtendedClient() {
394
+ const baseClient = new PrismaClient();
395
+ return baseClient.$extends(
396
+ withBlindIndexer({
397
+ encryptionKey: process.env.ENCRYPTION_KEY!,
398
+ indexKey: process.env.INDEX_KEY!,
399
+ fields: {
400
+ User: {
401
+ email: { indexFieldName: 'email_bidx', caseSensitive: false },
402
+ },
403
+ },
404
+ })
405
+ );
406
+ }
407
+
408
+ export type ExtendedPrismaClient = ReturnType<typeof createExtendedClient>;
409
+
410
+ @Injectable()
411
+ export class PrismaService implements OnModuleInit, OnModuleDestroy {
412
+ public readonly client: ExtendedPrismaClient;
413
+ private readonly rawBaseClient: PrismaClient;
414
+
415
+ constructor() {
416
+ this.rawBaseClient = new PrismaClient();
417
+ this.client = this.rawBaseClient.$extends(
418
+ withBlindIndexer({
419
+ encryptionKey: process.env.ENCRYPTION_KEY!,
420
+ indexKey: process.env.INDEX_KEY!,
421
+ fields: {
422
+ User: {
423
+ email: { indexFieldName: 'email_bidx', caseSensitive: false },
424
+ },
425
+ },
426
+ })
427
+ ) as ExtendedPrismaClient;
428
+ }
429
+
430
+ async onModuleInit() {
431
+ await this.rawBaseClient.$connect();
432
+ }
433
+
434
+ async onModuleDestroy() {
435
+ await this.rawBaseClient.$disconnect();
436
+ }
437
+ }
438
+ ```
439
+
440
+ Inject and use in NestJS services:
441
+
442
+ ```typescript
443
+ // src/users/users.service.ts
444
+ import { Injectable } from '@nestjs/common';
445
+ import { PrismaService } from '../prisma/prisma.service';
446
+
447
+ @Injectable()
448
+ export class UsersService {
449
+ constructor(private prisma: PrismaService) {}
450
+
451
+ async findByEmail(email: string) {
452
+ return this.prisma.client.user.findUnique({
453
+ where: { email },
454
+ });
455
+ }
456
+ }
457
+ ```
458
+
459
+ ---
460
+
461
+ ### 3. Svelte / SvelteKit
462
+
463
+ In SvelteKit, place your Prisma client in the `$lib/server` folder so Vite guarantees it is never bundled into client-side code:
464
+
465
+ ```typescript
466
+ // src/lib/server/prisma.ts
467
+ import { PrismaClient } from '@prisma/client';
468
+ import { withBlindIndexer } from 'orm-blind-indexer';
469
+ import { ENCRYPTION_KEY, INDEX_KEY } from '$env/static/private';
470
+
471
+ export const prisma = new PrismaClient().$extends(
472
+ withBlindIndexer({
473
+ encryptionKey: ENCRYPTION_KEY,
474
+ indexKey: INDEX_KEY,
475
+ fields: {
476
+ User: {
477
+ email: { indexFieldName: 'email_bidx', caseSensitive: false },
478
+ },
479
+ },
480
+ })
481
+ );
482
+ ```
483
+
484
+ Then in any `+page.server.ts` or `+server.ts`:
485
+
486
+ ```typescript
487
+ // src/routes/users/+page.server.ts
488
+ import { prisma } from '$lib/server/prisma';
489
+ import type { PageServerLoad } from './$types';
490
+
491
+ export const load: PageServerLoad = async ({ url }) => {
492
+ const email = url.searchParams.get('email') ?? '';
493
+ const user = await prisma.user.findUnique({
494
+ where: { email },
495
+ });
496
+ return { user };
497
+ };
498
+ ```
499
+
500
+ ---
501
+
502
+ ### 4. Angular (with AnalogJS SSR or NestJS/Express API)
503
+
504
+ In modern full-stack Angular setups (e.g., [AnalogJS](https://analogjs.org/)), use server routes in `src/server/routes/`:
505
+
506
+ ```typescript
507
+ // src/server/routes/api/users.get.ts (AnalogJS / Nitro server)
508
+ import { defineEventHandler, getQuery } from 'h3';
509
+ import { PrismaClient } from '@prisma/client';
510
+ import { withBlindIndexer } from 'orm-blind-indexer';
511
+
512
+ const prisma = new PrismaClient().$extends(
513
+ withBlindIndexer({
514
+ encryptionKey: process.env.ENCRYPTION_KEY!,
515
+ indexKey: process.env.INDEX_KEY!,
516
+ fields: {
517
+ User: { email: { indexFieldName: 'email_bidx' } },
518
+ },
519
+ })
520
+ );
521
+
522
+ export default defineEventHandler(async (event) => {
523
+ const { email } = getQuery(event);
524
+ return await prisma.user.findUnique({
525
+ where: { email: String(email) },
526
+ });
527
+ });
528
+ ```
529
+
530
+ ---
531
+
532
+ ### 5. Remix & Nuxt (Vue)
533
+
534
+ **Remix** (`app/db.server.ts`):
535
+ ```typescript
536
+ import { PrismaClient } from '@prisma/client';
537
+ import { withBlindIndexer } from 'orm-blind-indexer';
538
+
539
+ export const prisma = new PrismaClient().$extends(
540
+ withBlindIndexer({
541
+ encryptionKey: process.env.ENCRYPTION_KEY!,
542
+ indexKey: process.env.INDEX_KEY!,
543
+ fields: { User: { email: { indexFieldName: 'email_bidx' } } },
544
+ })
545
+ );
546
+ ```
547
+
548
+ **Nuxt 3** (`server/utils/prisma.ts`):
549
+ ```typescript
550
+ import { PrismaClient } from '@prisma/client';
551
+ import { withBlindIndexer } from 'orm-blind-indexer';
552
+
553
+ const config = useRuntimeConfig();
554
+
555
+ export const prisma = new PrismaClient().$extends(
556
+ withBlindIndexer({
557
+ encryptionKey: config.encryptionKey,
558
+ indexKey: config.indexKey,
559
+ fields: { User: { email: { indexFieldName: 'email_bidx' } } },
560
+ })
561
+ );
562
+ ```
563
+
564
+ ---
565
+
566
+ ### 6. Express & Fastify (Node.js CommonJS or ESM)
567
+
568
+ ```typescript
569
+ import express from 'express';
570
+ import { PrismaClient } from '@prisma/client';
571
+ import { withBlindIndexer } from 'orm-blind-indexer';
572
+
573
+ const app = express();
574
+ const prisma = new PrismaClient().$extends(
575
+ withBlindIndexer({
576
+ encryptionKey: process.env.ENCRYPTION_KEY!,
577
+ indexKey: process.env.INDEX_KEY!,
578
+ fields: { User: { email: { indexFieldName: 'email_bidx' } } },
579
+ })
580
+ );
581
+
582
+ app.get('/users', async (req, res) => {
583
+ const user = await prisma.user.findUnique({
584
+ where: { email: String(req.query.email) },
585
+ });
586
+ res.json(user);
587
+ });
588
+ ```
589
+
590
+ ---
591
+
592
+ ## Development
593
+
594
+ ```bash
595
+ # Install dependencies
596
+ npm install
597
+
598
+ # Generate Prisma client for tests
599
+ npx prisma generate --schema=tests/setup/schema.prisma
600
+
601
+ # Run all tests
602
+ npm test
603
+
604
+ # Run tests with coverage
605
+ npm run test:coverage
606
+
607
+ # Build the library
608
+ npm run build
609
+
610
+ # Type check
611
+ npm run lint
612
+ ```
613
+
614
+ ---
615
+
616
+ ## How It Works — Architecture
617
+
618
+ ```
619
+ Developer Code Extension Database
620
+ ─────────────────────────────────────────────────────────────────────
621
+
622
+ prisma.user.create({ ──▶ encrypt(email) ──▶ email = "iv:tag:ct"
623
+ data: { email: "..." } inject email_bidx email_bidx = hmac(email)
624
+ })
625
+
626
+ prisma.user.findUnique({ ──▶ hash(where.email) ──▶ WHERE email_bidx = hash
627
+ where: { email: "..." } ◀── decrypt(result.email) (returns encrypted row)
628
+ }) strip _bidx fields
629
+ ```
630
+
631
+ ---
632
+
633
+ ## License
634
+
635
+ MIT © Oussama Berrada Ifriqi
636
+