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 +21 -0
- package/README.md +636 -0
- package/dist/index.d.mts +364 -0
- package/dist/index.d.ts +364 -0
- package/dist/index.js +510 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +475 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +66 -0
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
|
+
[](https://github.com/oussamaberradaifriqi/orm-blind-indexer/actions/workflows/ci.yml)
|
|
8
|
+
[](https://badge.fury.io/js/orm-blind-indexer)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](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
|
+
|