zenstack-encryption 0.1.1 → 0.1.3
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/README.md +29 -12
- package/dist/index.d.mts +15 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +68 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +62 -59
package/README.md
CHANGED
|
@@ -77,15 +77,18 @@ npx zenstack generate
|
|
|
77
77
|
|
|
78
78
|
```typescript
|
|
79
79
|
import { ZenStackClient } from '@zenstackhq/orm';
|
|
80
|
-
import { createEncryptionPlugin
|
|
80
|
+
import { createEncryptionPlugin } from 'zenstack-encryption';
|
|
81
81
|
import schema from './schema.js';
|
|
82
82
|
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
);
|
|
83
|
+
// Pass a string secret — it will be derived to a 32-byte key via SHA-256
|
|
84
|
+
const encryptionPlugin = createEncryptionPlugin({
|
|
85
|
+
encryptionKey: process.env.ENCRYPTION_SECRET!,
|
|
86
|
+
});
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// Or pass a raw 32-byte Uint8Array if you already have one
|
|
89
|
+
// const encryptionPlugin = createEncryptionPlugin({
|
|
90
|
+
// encryptionKey: new Uint8Array(Buffer.from(process.env.ENCRYPTION_KEY!, 'base64')),
|
|
91
|
+
// });
|
|
89
92
|
|
|
90
93
|
const client = new ZenStackClient(schema, {
|
|
91
94
|
plugins: [encryptionPlugin],
|
|
@@ -104,12 +107,12 @@ console.log(user.secretToken); // → "super-secret-value" (decrypted)
|
|
|
104
107
|
|
|
105
108
|
## Key Rotation
|
|
106
109
|
|
|
107
|
-
When you need to rotate encryption keys, pass old keys via `decryptionKeys`. The plugin will use the primary `encryptionKey` for new writes, but try all keys (primary + decryption) when decrypting:
|
|
110
|
+
When you need to rotate encryption keys, pass old keys via `decryptionKeys`. The plugin will use the primary `encryptionKey` for new writes, but try all keys (primary + decryption) when decrypting. Both strings and `Uint8Array` keys can be mixed:
|
|
108
111
|
|
|
109
112
|
```typescript
|
|
110
113
|
const plugin = createEncryptionPlugin({
|
|
111
|
-
encryptionKey:
|
|
112
|
-
decryptionKeys: [
|
|
114
|
+
encryptionKey: 'new-secret', // used for all new encryptions
|
|
115
|
+
decryptionKeys: ['old-secret'], // tried during decryption alongside encryptionKey
|
|
113
116
|
});
|
|
114
117
|
```
|
|
115
118
|
|
|
@@ -150,12 +153,26 @@ const baseClient = new ZenStackClient(schema);
|
|
|
150
153
|
const client = baseClient.$use(createEncryptionPlugin({ encryptionKey }));
|
|
151
154
|
```
|
|
152
155
|
|
|
156
|
+
## Security Notes
|
|
157
|
+
|
|
158
|
+
When passing a string as `encryptionKey`, the plugin derives a 32-byte key using SHA-256. This is **not** a password-based key derivation function — it does not use salting or iterations. Your string secret should be **high-entropy** (e.g. a random 32+ character token from a secrets manager, not a human-chosen password).
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Good: generate a random secret
|
|
162
|
+
openssl rand -base64 32
|
|
163
|
+
|
|
164
|
+
# Bad: weak password
|
|
165
|
+
ENCRYPTION_SECRET="password123"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If you need to derive keys from low-entropy passwords, use a proper KDF (PBKDF2, Argon2) yourself and pass the resulting `Uint8Array` directly.
|
|
169
|
+
|
|
153
170
|
## Limitations
|
|
154
171
|
|
|
155
172
|
- **ORM only** — only applies to ORM CRUD operations, not direct Kysely query builder calls via `client.$qb`
|
|
156
|
-
- **String fields only** — `@encrypted` can only be applied to `String` fields
|
|
157
|
-
- **No encrypted filtering** — encrypted fields cannot be used in `where` clauses
|
|
158
|
-
- **Storage overhead** — encrypted values are
|
|
173
|
+
- **String fields only** — `@encrypted` can only be applied to `String` fields. Applying `@encrypted` to non-String fields will log a warning at runtime and be ignored.
|
|
174
|
+
- **No encrypted filtering** — encrypted fields **cannot** be used in `where` clauses, `orderBy`, or unique constraints. Since encryption is non-deterministic (each encryption produces different ciphertext due to random IVs), queries like `where: { secretField: 'value' }` will never match. If you need to search by a field, don't encrypt it — or store a separate non-encrypted hash for lookups.
|
|
175
|
+
- **Storage overhead** — encrypted values are larger than the original plaintext. Expect roughly **80 bytes of overhead** per field (IV + GCM tag + metadata + base64 encoding), plus ~37% expansion of the plaintext itself. A 100-character plaintext becomes ~215 characters. Ensure your database columns use `TEXT` or a sufficiently large `VARCHAR`.
|
|
159
176
|
|
|
160
177
|
## License
|
|
161
178
|
|
package/dist/index.d.mts
CHANGED
|
@@ -8,7 +8,9 @@ import { FieldDef, SchemaDef } from "@zenstackhq/orm/schema";
|
|
|
8
8
|
declare class Decrypter {
|
|
9
9
|
private readonly decryptionKeys;
|
|
10
10
|
private keys;
|
|
11
|
+
private initPromise;
|
|
11
12
|
constructor(decryptionKeys: Uint8Array[]);
|
|
13
|
+
private ensureKeys;
|
|
12
14
|
/**
|
|
13
15
|
* Decrypts the given data
|
|
14
16
|
*/
|
|
@@ -36,14 +38,16 @@ declare class Encrypter {
|
|
|
36
38
|
*/
|
|
37
39
|
type SimpleEncryption = {
|
|
38
40
|
/**
|
|
39
|
-
* The encryption key
|
|
41
|
+
* The encryption key. Pass a Uint8Array of exactly 32 bytes, or a string
|
|
42
|
+
* which will be derived to a 32-byte key via SHA-256.
|
|
40
43
|
*/
|
|
41
|
-
encryptionKey: Uint8Array;
|
|
44
|
+
encryptionKey: string | Uint8Array;
|
|
42
45
|
/**
|
|
43
46
|
* Additional decryption keys for key rotation support.
|
|
44
47
|
* When decrypting, all keys (encryptionKey + decryptionKeys) are tried.
|
|
48
|
+
* Each key can be a Uint8Array (32 bytes) or a string (derived via SHA-256).
|
|
45
49
|
*/
|
|
46
|
-
decryptionKeys?: Uint8Array[];
|
|
50
|
+
decryptionKeys?: (string | Uint8Array)[];
|
|
47
51
|
};
|
|
48
52
|
/**
|
|
49
53
|
* Custom encryption configuration for user-provided encryption handlers
|
|
@@ -82,10 +86,16 @@ declare function isCustomEncryption(config: EncryptionConfig): config is CustomE
|
|
|
82
86
|
* @param config Encryption configuration (simple or custom)
|
|
83
87
|
* @returns A runtime plugin that handles field encryption/decryption
|
|
84
88
|
*/
|
|
85
|
-
declare function createEncryptionPlugin<Schema extends SchemaDef>(config: EncryptionConfig): _zenstackhq_orm0.RuntimePlugin<any,
|
|
89
|
+
declare function createEncryptionPlugin<Schema extends SchemaDef>(config: EncryptionConfig): _zenstackhq_orm0.RuntimePlugin<any, Record<string, never>, Record<string, never>>;
|
|
86
90
|
//#endregion
|
|
87
91
|
//#region src/utils.d.ts
|
|
88
92
|
declare const ENCRYPTION_KEY_BYTES = 32;
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a key input to a Uint8Array. If the input is a string, it is
|
|
95
|
+
* derived to 32 bytes via SHA-256. If it is already a Uint8Array, its
|
|
96
|
+
* length is validated.
|
|
97
|
+
*/
|
|
98
|
+
declare function deriveKey(input: string | Uint8Array): Promise<Uint8Array>;
|
|
89
99
|
//#endregion
|
|
90
|
-
export { type CustomEncryption, Decrypter, ENCRYPTION_KEY_BYTES, Encrypter, type EncryptionConfig, type SimpleEncryption, createEncryptionPlugin, isCustomEncryption };
|
|
100
|
+
export { type CustomEncryption, Decrypter, ENCRYPTION_KEY_BYTES, Encrypter, type EncryptionConfig, type SimpleEncryption, createEncryptionPlugin, deriveKey, isCustomEncryption };
|
|
91
101
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/decrypter.ts","../src/encrypter.ts","../src/types.ts","../src/plugin.ts","../src/utils.ts"],"mappings":";;;;;;;cAKa,SAAA;EAAA,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/decrypter.ts","../src/encrypter.ts","../src/types.ts","../src/plugin.ts","../src/utils.ts"],"mappings":";;;;;;;cAKa,SAAA;EAAA,iBAIoB,cAAA;EAAA,QAHrB,IAAA;EAAA,QACA,WAAA;cAEqB,cAAA,EAAgB,UAAA;EAAA,QAY/B,UAAA;EAfN;;;EAkCF,OAAA,CAAQ,IAAA,WAAe,OAAA;AAAA;;;;;;cCnCpB,SAAA;EAAA,iBAIoB,aAAA;EAAA,QAHrB,GAAA;EAAA,QACA,SAAA;cAEqB,aAAA,EAAe,UAAA;EDAf;;;ECSvB,OAAA,CAAQ,IAAA,WAAe,OAAA;AAAA;;;;;;KCbrB,gBAAA;EFAU;;;;EEKlB,aAAA,WAAwB,UAAA;EFHhB;;;;;EEUR,cAAA,aAA2B,UAAA;AAAA;;;;KAMnB,gBAAA;;;ADlBZ;;;;;EC0BI,OAAA,GAAU,KAAA,UAAe,KAAA,EAAO,QAAA,EAAU,KAAA,aAAkB,OAAA;EDxBpD;;;;;;;ECiCR,OAAA,GAAU,KAAA,UAAe,KAAA,EAAO,QAAA,EAAU,MAAA,aAAmB,OAAA;AAAA;;;;KAMrD,gBAAA,GAAmB,gBAAA,GAAmB,gBAAA;;;;iBAKlC,kBAAA,CAAmB,MAAA,EAAQ,gBAAA,GAAmB,MAAA,IAAU,gBAAA;;;;;AF9CxE;;;;iBGyCgB,sBAAA,gBAAsC,SAAA,CAAA,CAAW,MAAA,EAAQ,gBAAA,GAAgB,gBAAA,CAAA,aAAA,MAAA,MAAA,iBAAA,MAAA;;;cC3C5E,oBAAA;;;;;;iBAsBS,SAAA,CAAU,KAAA,WAAgB,UAAA,GAAa,OAAA,CAAQ,UAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -15,6 +15,19 @@ const encryptionMetaSchema = z.object({
|
|
|
15
15
|
k: z.string()
|
|
16
16
|
});
|
|
17
17
|
/**
|
|
18
|
+
* Resolve a key input to a Uint8Array. If the input is a string, it is
|
|
19
|
+
* derived to 32 bytes via SHA-256. If it is already a Uint8Array, its
|
|
20
|
+
* length is validated.
|
|
21
|
+
*/
|
|
22
|
+
async function deriveKey(input) {
|
|
23
|
+
if (typeof input === "string") {
|
|
24
|
+
const encoded = new TextEncoder().encode(input);
|
|
25
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", encoded));
|
|
26
|
+
}
|
|
27
|
+
if (input.length !== ENCRYPTION_KEY_BYTES) throw new Error(`Encryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);
|
|
28
|
+
return input;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
18
31
|
* Load a raw encryption key into a CryptoKey object
|
|
19
32
|
*/
|
|
20
33
|
async function loadKey(key, keyUsages) {
|
|
@@ -78,7 +91,7 @@ async function _decrypt(data, findKey) {
|
|
|
78
91
|
}
|
|
79
92
|
return decoder.decode(decrypted);
|
|
80
93
|
}
|
|
81
|
-
throw lastError;
|
|
94
|
+
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Decryption failed with all available keys");
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
//#endregion
|
|
@@ -93,14 +106,22 @@ var Decrypter = class {
|
|
|
93
106
|
if (decryptionKeys.length === 0) throw new Error("At least one decryption key must be provided");
|
|
94
107
|
for (const key of decryptionKeys) if (key.length !== ENCRYPTION_KEY_BYTES) throw new Error(`Decryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);
|
|
95
108
|
}
|
|
109
|
+
async ensureKeys() {
|
|
110
|
+
if (this.keys.length > 0) return;
|
|
111
|
+
if (this.initPromise) return this.initPromise;
|
|
112
|
+
this.initPromise = (async () => {
|
|
113
|
+
this.keys = await Promise.all(this.decryptionKeys.map(async (key) => ({
|
|
114
|
+
key: await loadKey(key, ["decrypt"]),
|
|
115
|
+
digest: await getKeyDigest(key)
|
|
116
|
+
})));
|
|
117
|
+
})();
|
|
118
|
+
return this.initPromise;
|
|
119
|
+
}
|
|
96
120
|
/**
|
|
97
121
|
* Decrypts the given data
|
|
98
122
|
*/
|
|
99
123
|
async decrypt(data) {
|
|
100
|
-
|
|
101
|
-
key: await loadKey(key, ["decrypt"]),
|
|
102
|
-
digest: await getKeyDigest(key)
|
|
103
|
-
})));
|
|
124
|
+
await this.ensureKeys();
|
|
104
125
|
return _decrypt(data, async (digest) => this.keys.filter((entry) => entry.digest === digest).map((entry) => entry.key));
|
|
105
126
|
}
|
|
106
127
|
};
|
|
@@ -137,6 +158,7 @@ function isCustomEncryption(config) {
|
|
|
137
158
|
//#endregion
|
|
138
159
|
//#region src/plugin.ts
|
|
139
160
|
const ENCRYPTED_ATTRIBUTE = "@encrypted";
|
|
161
|
+
const warnedNonStringFields = /* @__PURE__ */ new Set();
|
|
140
162
|
/**
|
|
141
163
|
* Check if a field has the @encrypted attribute
|
|
142
164
|
*/
|
|
@@ -144,6 +166,18 @@ function isEncryptedField(field) {
|
|
|
144
166
|
return field.attributes?.some((attr) => attr.name === ENCRYPTED_ATTRIBUTE) ?? false;
|
|
145
167
|
}
|
|
146
168
|
/**
|
|
169
|
+
* Warn once if @encrypted is applied to a non-String field
|
|
170
|
+
*/
|
|
171
|
+
function warnIfNonStringEncrypted(modelName, fieldName, field) {
|
|
172
|
+
if (isEncryptedField(field) && field.type !== "String") {
|
|
173
|
+
const key = `${modelName}.${fieldName}`;
|
|
174
|
+
if (!warnedNonStringFields.has(key)) {
|
|
175
|
+
warnedNonStringFields.add(key);
|
|
176
|
+
console.warn(`@encrypted is only supported on String fields. ${key} (type: ${field.type}) will be ignored.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
147
181
|
* Check if a model has any encrypted fields
|
|
148
182
|
*/
|
|
149
183
|
function hasEncryptedFields(model) {
|
|
@@ -159,11 +193,23 @@ function createEncryptionPlugin(config) {
|
|
|
159
193
|
let encrypter;
|
|
160
194
|
let decrypter;
|
|
161
195
|
let customEncryption;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
196
|
+
let initialized = false;
|
|
197
|
+
let initPromise;
|
|
198
|
+
async function ensureInitialized() {
|
|
199
|
+
if (initialized) return;
|
|
200
|
+
if (initPromise) return initPromise;
|
|
201
|
+
initPromise = (async () => {
|
|
202
|
+
if (isCustomEncryption(config)) customEncryption = config;
|
|
203
|
+
else {
|
|
204
|
+
const simpleConfig = config;
|
|
205
|
+
const encryptionKey = await deriveKey(simpleConfig.encryptionKey);
|
|
206
|
+
const decryptionKeys = await Promise.all((simpleConfig.decryptionKeys ?? []).map(deriveKey));
|
|
207
|
+
encrypter = new Encrypter(encryptionKey);
|
|
208
|
+
decrypter = new Decrypter([encryptionKey, ...decryptionKeys]);
|
|
209
|
+
}
|
|
210
|
+
initialized = true;
|
|
211
|
+
})();
|
|
212
|
+
return initPromise;
|
|
167
213
|
}
|
|
168
214
|
async function encryptValue(model, field, value) {
|
|
169
215
|
if (customEncryption) return customEncryption.encrypt(model, field, value);
|
|
@@ -180,9 +226,10 @@ function createEncryptionPlugin(config) {
|
|
|
180
226
|
const model = schema.models[modelName];
|
|
181
227
|
if (!model) return;
|
|
182
228
|
for (const [fieldName, value] of Object.entries(data)) {
|
|
183
|
-
if (value === null || value === void 0
|
|
229
|
+
if (value === null || value === void 0) continue;
|
|
184
230
|
const field = model.fields[fieldName];
|
|
185
231
|
if (!field) continue;
|
|
232
|
+
warnIfNonStringEncrypted(modelName, fieldName, field);
|
|
186
233
|
if (isEncryptedField(field) && typeof value === "string") {
|
|
187
234
|
data[fieldName] = await encryptValue(modelName, field, value);
|
|
188
235
|
continue;
|
|
@@ -211,10 +258,8 @@ function createEncryptionPlugin(config) {
|
|
|
211
258
|
if (itemData) await encryptWriteData(schema, modelName, itemData);
|
|
212
259
|
}
|
|
213
260
|
else {
|
|
214
|
-
const
|
|
215
|
-
const nestedData = updateObj["data"];
|
|
261
|
+
const nestedData = updateData["data"];
|
|
216
262
|
if (nestedData) await encryptWriteData(schema, modelName, nestedData);
|
|
217
|
-
else await encryptWriteData(schema, modelName, updateObj);
|
|
218
263
|
}
|
|
219
264
|
const updateManyData = data["updateMany"];
|
|
220
265
|
if (updateManyData) if (Array.isArray(updateManyData)) for (const item of updateManyData) {
|
|
@@ -257,14 +302,14 @@ function createEncryptionPlugin(config) {
|
|
|
257
302
|
const model = schema.models[modelName];
|
|
258
303
|
if (!model) return;
|
|
259
304
|
for (const [fieldName, value] of Object.entries(data)) {
|
|
260
|
-
if (value === null || value === void 0
|
|
305
|
+
if (value === null || value === void 0) continue;
|
|
261
306
|
const field = model.fields[fieldName];
|
|
262
307
|
if (!field) continue;
|
|
263
308
|
if (isEncryptedField(field) && typeof value === "string") {
|
|
264
309
|
try {
|
|
265
310
|
data[fieldName] = await decryptValue(modelName, field, value);
|
|
266
|
-
} catch
|
|
267
|
-
console.warn(
|
|
311
|
+
} catch {
|
|
312
|
+
console.warn("Failed to decrypt an encrypted field");
|
|
268
313
|
}
|
|
269
314
|
continue;
|
|
270
315
|
}
|
|
@@ -281,12 +326,15 @@ function createEncryptionPlugin(config) {
|
|
|
281
326
|
name: "Encryption Plugin",
|
|
282
327
|
description: "Automatically encrypts and decrypts fields marked with @encrypted",
|
|
283
328
|
onQuery: async (ctx) => {
|
|
329
|
+
await ensureInitialized();
|
|
284
330
|
const { model, operation, args, proceed, client } = ctx;
|
|
285
331
|
const schema = client.schema;
|
|
286
332
|
const modelDef = schema.models[model];
|
|
287
333
|
if (!modelDef || !hasEncryptedFields(modelDef)) return proceed(args);
|
|
288
|
-
const
|
|
289
|
-
|
|
334
|
+
const isWrite = operation === "create" || operation === "update" || operation === "upsert" || operation === "createMany" || operation === "updateMany" || operation === "createManyAndReturn";
|
|
335
|
+
let processedArgs = args;
|
|
336
|
+
if (isWrite) {
|
|
337
|
+
processedArgs = args ? JSON.parse(JSON.stringify(args)) : void 0;
|
|
290
338
|
if (processedArgs?.data) if (Array.isArray(processedArgs.data)) for (const item of processedArgs.data) await encryptWriteData(schema, model, item);
|
|
291
339
|
else await encryptWriteData(schema, model, processedArgs.data);
|
|
292
340
|
if (operation === "upsert") {
|
|
@@ -306,5 +354,5 @@ function createEncryptionPlugin(config) {
|
|
|
306
354
|
}
|
|
307
355
|
|
|
308
356
|
//#endregion
|
|
309
|
-
export { Decrypter, ENCRYPTION_KEY_BYTES, Encrypter, createEncryptionPlugin, isCustomEncryption };
|
|
357
|
+
export { Decrypter, ENCRYPTION_KEY_BYTES, Encrypter, createEncryptionPlugin, deriveKey, isCustomEncryption };
|
|
310
358
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/utils.ts","../src/decrypter.ts","../src/encrypter.ts","../src/types.ts","../src/plugin.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const ENCRYPTER_VERSION = 1;\nexport const ENCRYPTION_KEY_BYTES = 32;\nexport const IV_BYTES = 12;\nexport const ALGORITHM = 'AES-GCM';\nexport const KEY_DIGEST_BYTES = 8;\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nconst encryptionMetaSchema = z.object({\n // version\n v: z.number(),\n // algorithm\n a: z.string(),\n // key digest\n k: z.string(),\n});\n\n/**\n * Load a raw encryption key into a CryptoKey object\n */\nexport async function loadKey(key: Uint8Array, keyUsages: KeyUsage[]): Promise<CryptoKey> {\n // Convert to ArrayBuffer for crypto.subtle compatibility\n const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength) as ArrayBuffer;\n return crypto.subtle.importKey('raw', keyBuffer, ALGORITHM, false, keyUsages);\n}\n\n/**\n * Get a digest of the encryption key for identification\n */\nexport async function getKeyDigest(key: Uint8Array): Promise<string> {\n // Convert to ArrayBuffer for crypto.subtle compatibility\n const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength) as ArrayBuffer;\n const rawDigest = await crypto.subtle.digest('SHA-256', keyBuffer);\n return new Uint8Array(rawDigest.slice(0, KEY_DIGEST_BYTES)).reduce(\n (acc, byte) => acc + byte.toString(16).padStart(2, '0'),\n '',\n );\n}\n\n/**\n * Encrypt data using AES-GCM\n */\nexport async function _encrypt(data: string, key: CryptoKey, keyDigest: string): Promise<string> {\n const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));\n const encrypted = await crypto.subtle.encrypt(\n {\n name: ALGORITHM,\n iv,\n },\n key,\n encoder.encode(data),\n );\n\n // combine IV and encrypted data into a single array of bytes\n const cipherBytes = [...iv, ...new Uint8Array(encrypted)];\n\n // encryption metadata\n const meta = { v: ENCRYPTER_VERSION, a: ALGORITHM, k: keyDigest };\n\n // convert concatenated result to base64 string\n return `${btoa(JSON.stringify(meta))}.${btoa(String.fromCharCode(...cipherBytes))}`;\n}\n\n/**\n * Decrypt data using AES-GCM\n */\nexport async function _decrypt(data: string, findKey: (digest: string) => Promise<CryptoKey[]>): Promise<string> {\n const [metaText, cipherText] = data.split('.');\n if (!metaText || !cipherText) {\n throw new Error('Malformed encrypted data');\n }\n\n let metaObj: unknown;\n try {\n metaObj = JSON.parse(atob(metaText));\n } catch {\n throw new Error('Malformed metadata');\n }\n\n // parse meta\n const { a: algorithm, k: keyDigest } = encryptionMetaSchema.parse(metaObj);\n\n // find a matching decryption key\n const keys = await findKey(keyDigest);\n if (keys.length === 0) {\n throw new Error('No matching decryption key found');\n }\n\n // convert base64 back to bytes\n const bytes = Uint8Array.from(atob(cipherText), (c) => c.charCodeAt(0));\n\n // extract IV from the head\n const iv = bytes.slice(0, IV_BYTES);\n const cipher = bytes.slice(IV_BYTES);\n let lastError: unknown;\n\n for (const key of keys) {\n let decrypted: ArrayBuffer;\n try {\n decrypted = await crypto.subtle.decrypt({ name: algorithm, iv }, key, cipher);\n } catch (err) {\n lastError = err;\n continue;\n }\n return decoder.decode(decrypted);\n }\n\n throw lastError;\n}\n","import { _decrypt, ENCRYPTION_KEY_BYTES, getKeyDigest, loadKey } from './utils.js';\n\n/**\n * Default decrypter with support for key rotation\n */\nexport class Decrypter {\n private keys: Array<{ key: CryptoKey; digest: string }> = [];\n\n constructor(private readonly decryptionKeys: Uint8Array[]) {\n if (decryptionKeys.length === 0) {\n throw new Error('At least one decryption key must be provided');\n }\n\n for (const key of decryptionKeys) {\n if (key.length !== ENCRYPTION_KEY_BYTES) {\n throw new Error(`Decryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);\n }\n }\n }\n\n /**\n * Decrypts the given data\n */\n async decrypt(data: string): Promise<string> {\n if (this.keys.length === 0) {\n this.keys = await Promise.all(\n this.decryptionKeys.map(async (key) => ({\n key: await loadKey(key, ['decrypt']),\n digest: await getKeyDigest(key),\n })),\n );\n }\n\n return _decrypt(data, async (digest) =>\n this.keys.filter((entry) => entry.digest === digest).map((entry) => entry.key),\n );\n }\n}\n","import { _encrypt, ENCRYPTION_KEY_BYTES, getKeyDigest, loadKey } from './utils.js';\n\n/**\n * Default encrypter using AES-256-GCM\n */\nexport class Encrypter {\n private key: CryptoKey | undefined;\n private keyDigest: string | undefined;\n\n constructor(private readonly encryptionKey: Uint8Array) {\n if (encryptionKey.length !== ENCRYPTION_KEY_BYTES) {\n throw new Error(`Encryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);\n }\n }\n\n /**\n * Encrypts the given data\n */\n async encrypt(data: string): Promise<string> {\n if (!this.key) {\n this.key = await loadKey(this.encryptionKey, ['encrypt']);\n }\n\n if (!this.keyDigest) {\n this.keyDigest = await getKeyDigest(this.encryptionKey);\n }\n\n return _encrypt(data, this.key, this.keyDigest);\n }\n}\n","import type { FieldDef } from '@zenstackhq/orm/schema';\n\n/**\n * Simple encryption configuration using built-in AES-256-GCM encryption\n */\nexport type SimpleEncryption = {\n /**\n * The encryption key (must be 32 bytes / 256 bits)\n */\n encryptionKey: Uint8Array;\n\n /**\n * Additional decryption keys for key rotation support.\n * When decrypting, all keys (encryptionKey + decryptionKeys) are tried.\n */\n decryptionKeys?: Uint8Array[];\n};\n\n/**\n * Custom encryption configuration for user-provided encryption handlers\n */\nexport type CustomEncryption = {\n /**\n * Custom encryption function\n * @param model The model name\n * @param field The field definition\n * @param plain The plaintext value to encrypt\n * @returns The encrypted value\n */\n encrypt: (model: string, field: FieldDef, plain: string) => Promise<string>;\n\n /**\n * Custom decryption function\n * @param model The model name\n * @param field The field definition\n * @param cipher The encrypted value to decrypt\n * @returns The decrypted value\n */\n decrypt: (model: string, field: FieldDef, cipher: string) => Promise<string>;\n};\n\n/**\n * Encryption configuration - either simple (built-in) or custom\n */\nexport type EncryptionConfig = SimpleEncryption | CustomEncryption;\n\n/**\n * Type guard to check if encryption config is custom\n */\nexport function isCustomEncryption(config: EncryptionConfig): config is CustomEncryption {\n return 'encrypt' in config && 'decrypt' in config;\n}\n","import { definePlugin } from '@zenstackhq/orm';\nimport type { FieldDef, ModelDef, SchemaDef } from '@zenstackhq/orm/schema';\nimport { Decrypter } from './decrypter.js';\nimport { Encrypter } from './encrypter.js';\nimport type { CustomEncryption, EncryptionConfig, SimpleEncryption } from './types.js';\nimport { isCustomEncryption } from './types.js';\n\nconst ENCRYPTED_ATTRIBUTE = '@encrypted';\n\n/**\n * Check if a field has the @encrypted attribute\n */\nfunction isEncryptedField(field: FieldDef): boolean {\n return field.attributes?.some((attr) => attr.name === ENCRYPTED_ATTRIBUTE) ?? false;\n}\n\n/**\n * Check if a model has any encrypted fields\n */\nfunction hasEncryptedFields(model: ModelDef): boolean {\n return Object.values(model.fields).some(isEncryptedField);\n}\n\n/**\n * Creates an encryption plugin for ZenStack ORM\n *\n * @param config Encryption configuration (simple or custom)\n * @returns A runtime plugin that handles field encryption/decryption\n */\nexport function createEncryptionPlugin<Schema extends SchemaDef>(config: EncryptionConfig) {\n let encrypter: Encrypter | undefined;\n let decrypter: Decrypter | undefined;\n let customEncryption: CustomEncryption | undefined;\n\n if (isCustomEncryption(config)) {\n customEncryption = config;\n } else {\n const simpleConfig = config as SimpleEncryption;\n encrypter = new Encrypter(simpleConfig.encryptionKey);\n const allDecryptionKeys = [simpleConfig.encryptionKey, ...(simpleConfig.decryptionKeys ?? [])];\n decrypter = new Decrypter(allDecryptionKeys);\n }\n\n async function encryptValue(model: string, field: FieldDef, value: string): Promise<string> {\n if (customEncryption) {\n return customEncryption.encrypt(model, field, value);\n }\n return encrypter!.encrypt(value);\n }\n\n async function decryptValue(model: string, field: FieldDef, value: string): Promise<string> {\n if (customEncryption) {\n return customEncryption.decrypt(model, field, value);\n }\n return decrypter!.decrypt(value);\n }\n\n /**\n * Recursively encrypt fields in write data\n */\n async function encryptWriteData(\n schema: SchemaDef,\n modelName: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n const model = schema.models[modelName];\n if (!model) return;\n\n for (const [fieldName, value] of Object.entries(data)) {\n if (value === null || value === undefined || value === '') {\n continue;\n }\n\n const field = model.fields[fieldName];\n if (!field) continue;\n\n // Handle encrypted string fields\n if (isEncryptedField(field) && typeof value === 'string') {\n data[fieldName] = await encryptValue(modelName, field, value);\n continue;\n }\n\n // Handle relation fields (nested writes)\n if (field.relation && typeof value === 'object') {\n const relatedModel = field.type;\n await encryptNestedWrites(schema, relatedModel, value as Record<string, unknown>);\n }\n }\n }\n\n /**\n * Handle nested write operations (create, update, connect, etc.)\n */\n async function encryptNestedWrites(\n schema: SchemaDef,\n modelName: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n // Handle create\n const createData = data['create'];\n if (createData) {\n if (Array.isArray(createData)) {\n for (const item of createData) {\n await encryptWriteData(schema, modelName, item as Record<string, unknown>);\n }\n } else {\n await encryptWriteData(schema, modelName, createData as Record<string, unknown>);\n }\n }\n\n // Handle createMany\n const createManyData = data['createMany'];\n if (createManyData && typeof createManyData === 'object') {\n const createManyItems = (createManyData as Record<string, unknown>)['data'];\n if (Array.isArray(createManyItems)) {\n for (const item of createManyItems) {\n await encryptWriteData(schema, modelName, item as Record<string, unknown>);\n }\n }\n }\n\n // Handle update\n const updateData = data['update'];\n if (updateData) {\n if (Array.isArray(updateData)) {\n for (const item of updateData) {\n const updateItem = item as Record<string, unknown>;\n const itemData = updateItem['data'];\n if (itemData) {\n await encryptWriteData(schema, modelName, itemData as Record<string, unknown>);\n }\n }\n } else {\n const updateObj = updateData as Record<string, unknown>;\n const nestedData = updateObj['data'];\n if (nestedData) {\n await encryptWriteData(schema, modelName, nestedData as Record<string, unknown>);\n } else {\n await encryptWriteData(schema, modelName, updateObj);\n }\n }\n }\n\n // Handle updateMany\n const updateManyData = data['updateMany'];\n if (updateManyData) {\n if (Array.isArray(updateManyData)) {\n for (const item of updateManyData) {\n const updateItem = item as Record<string, unknown>;\n const itemData = updateItem['data'];\n if (itemData) {\n await encryptWriteData(schema, modelName, itemData as Record<string, unknown>);\n }\n }\n } else {\n const updateObj = updateManyData as Record<string, unknown>;\n const nestedData = updateObj['data'];\n if (nestedData) {\n await encryptWriteData(schema, modelName, nestedData as Record<string, unknown>);\n }\n }\n }\n\n // Handle upsert\n const upsertData = data['upsert'];\n if (upsertData) {\n if (Array.isArray(upsertData)) {\n for (const item of upsertData) {\n const upsertItem = item as Record<string, unknown>;\n const createPart = upsertItem['create'];\n const updatePart = upsertItem['update'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n if (updatePart) {\n await encryptWriteData(schema, modelName, updatePart as Record<string, unknown>);\n }\n }\n } else {\n const upsertObj = upsertData as Record<string, unknown>;\n const createPart = upsertObj['create'];\n const updatePart = upsertObj['update'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n if (updatePart) {\n await encryptWriteData(schema, modelName, updatePart as Record<string, unknown>);\n }\n }\n }\n\n // Handle connectOrCreate\n const connectOrCreateData = data['connectOrCreate'];\n if (connectOrCreateData) {\n if (Array.isArray(connectOrCreateData)) {\n for (const item of connectOrCreateData) {\n const cocItem = item as Record<string, unknown>;\n const createPart = cocItem['create'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n }\n } else {\n const cocObj = connectOrCreateData as Record<string, unknown>;\n const createPart = cocObj['create'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n }\n }\n }\n\n /**\n * Recursively decrypt fields in result data\n */\n async function decryptResultData(\n schema: SchemaDef,\n modelName: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n const model = schema.models[modelName];\n if (!model) return;\n\n for (const [fieldName, value] of Object.entries(data)) {\n if (value === null || value === undefined || value === '') {\n continue;\n }\n\n const field = model.fields[fieldName];\n if (!field) continue;\n\n // Handle encrypted string fields\n if (isEncryptedField(field) && typeof value === 'string') {\n try {\n data[fieldName] = await decryptValue(modelName, field, value);\n } catch (error) {\n // If decryption fails, log warning and keep original value\n console.warn(`Failed to decrypt field ${modelName}.${fieldName}:`, error);\n }\n continue;\n }\n\n // Handle relation fields (nested data)\n if (field.relation && value !== null) {\n const relatedModel = field.type;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (typeof item === 'object' && item !== null) {\n await decryptResultData(schema, relatedModel, item as Record<string, unknown>);\n }\n }\n } else if (typeof value === 'object') {\n await decryptResultData(schema, relatedModel, value as Record<string, unknown>);\n }\n }\n }\n }\n\n return definePlugin<Schema, {}, {}>({\n id: 'encryption',\n name: 'Encryption Plugin',\n description: 'Automatically encrypts and decrypts fields marked with @encrypted',\n\n onQuery: async (ctx) => {\n const { model, operation, args, proceed, client } = ctx;\n const schema = (client as any).schema as SchemaDef;\n const modelDef = schema.models[model];\n\n // Check if this model has any encrypted fields\n if (!modelDef || !hasEncryptedFields(modelDef)) {\n return proceed(args);\n }\n\n // Clone args to avoid mutating original\n const processedArgs = args ? JSON.parse(JSON.stringify(args)) : undefined;\n\n // Handle write operations - encrypt data before writing\n if (\n operation === 'create' ||\n operation === 'update' ||\n operation === 'upsert' ||\n operation === 'createMany' ||\n operation === 'updateMany' ||\n operation === 'createManyAndReturn'\n ) {\n if (processedArgs?.data) {\n if (Array.isArray(processedArgs.data)) {\n for (const item of processedArgs.data) {\n await encryptWriteData(schema, model, item);\n }\n } else {\n await encryptWriteData(schema, model, processedArgs.data);\n }\n }\n\n // Handle upsert create/update\n if (operation === 'upsert') {\n if (processedArgs?.create) {\n await encryptWriteData(schema, model, processedArgs.create);\n }\n if (processedArgs?.update) {\n await encryptWriteData(schema, model, processedArgs.update);\n }\n }\n }\n\n // Execute the query\n const result = await proceed(processedArgs);\n\n // Handle read operations - decrypt data after reading\n if (result !== null && result !== undefined) {\n if (Array.isArray(result)) {\n for (const item of result) {\n if (typeof item === 'object' && item !== null) {\n await decryptResultData(schema, model, item as Record<string, unknown>);\n }\n }\n } else if (typeof result === 'object') {\n await decryptResultData(schema, model, result as Record<string, unknown>);\n }\n }\n\n return result;\n },\n });\n}\n"],"mappings":";;;;AAEA,MAAa,oBAAoB;AACjC,MAAa,uBAAuB;AACpC,MAAa,WAAW;AACxB,MAAa,YAAY;AACzB,MAAa,mBAAmB;AAEhC,MAAM,UAAU,IAAI,aAAa;AACjC,MAAM,UAAU,IAAI,aAAa;AAEjC,MAAM,uBAAuB,EAAE,OAAO;CAElC,GAAG,EAAE,QAAQ;CAEb,GAAG,EAAE,QAAQ;CAEb,GAAG,EAAE,QAAQ;CAChB,CAAC;;;;AAKF,eAAsB,QAAQ,KAAiB,WAA2C;CAEtF,MAAM,YAAY,IAAI,OAAO,MAAM,IAAI,YAAY,IAAI,aAAa,IAAI,WAAW;AACnF,QAAO,OAAO,OAAO,UAAU,OAAO,WAAW,WAAW,OAAO,UAAU;;;;;AAMjF,eAAsB,aAAa,KAAkC;CAEjE,MAAM,YAAY,IAAI,OAAO,MAAM,IAAI,YAAY,IAAI,aAAa,IAAI,WAAW;CACnF,MAAM,YAAY,MAAM,OAAO,OAAO,OAAO,WAAW,UAAU;AAClE,QAAO,IAAI,WAAW,UAAU,MAAM,GAAG,iBAAiB,CAAC,CAAC,QACvD,KAAK,SAAS,MAAM,KAAK,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,EACvD,GACH;;;;;AAML,eAAsB,SAAS,MAAc,KAAgB,WAAoC;CAC7F,MAAM,KAAK,OAAO,gBAAgB,IAAI,WAAW,SAAS,CAAC;CAC3D,MAAM,YAAY,MAAM,OAAO,OAAO,QAClC;EACI,MAAM;EACN;EACH,EACD,KACA,QAAQ,OAAO,KAAK,CACvB;CAGD,MAAM,cAAc,CAAC,GAAG,IAAI,GAAG,IAAI,WAAW,UAAU,CAAC;CAGzD,MAAM,OAAO;EAAE,GAAG;EAAmB,GAAG;EAAW,GAAG;EAAW;AAGjE,QAAO,GAAG,KAAK,KAAK,UAAU,KAAK,CAAC,CAAC,GAAG,KAAK,OAAO,aAAa,GAAG,YAAY,CAAC;;;;;AAMrF,eAAsB,SAAS,MAAc,SAAoE;CAC7G,MAAM,CAAC,UAAU,cAAc,KAAK,MAAM,IAAI;AAC9C,KAAI,CAAC,YAAY,CAAC,WACd,OAAM,IAAI,MAAM,2BAA2B;CAG/C,IAAI;AACJ,KAAI;AACA,YAAU,KAAK,MAAM,KAAK,SAAS,CAAC;SAChC;AACJ,QAAM,IAAI,MAAM,qBAAqB;;CAIzC,MAAM,EAAE,GAAG,WAAW,GAAG,cAAc,qBAAqB,MAAM,QAAQ;CAG1E,MAAM,OAAO,MAAM,QAAQ,UAAU;AACrC,KAAI,KAAK,WAAW,EAChB,OAAM,IAAI,MAAM,mCAAmC;CAIvD,MAAM,QAAQ,WAAW,KAAK,KAAK,WAAW,GAAG,MAAM,EAAE,WAAW,EAAE,CAAC;CAGvE,MAAM,KAAK,MAAM,MAAM,GAAG,SAAS;CACnC,MAAM,SAAS,MAAM,MAAM,SAAS;CACpC,IAAI;AAEJ,MAAK,MAAM,OAAO,MAAM;EACpB,IAAI;AACJ,MAAI;AACA,eAAY,MAAM,OAAO,OAAO,QAAQ;IAAE,MAAM;IAAW;IAAI,EAAE,KAAK,OAAO;WACxE,KAAK;AACV,eAAY;AACZ;;AAEJ,SAAO,QAAQ,OAAO,UAAU;;AAGpC,OAAM;;;;;;;;ACzGV,IAAa,YAAb,MAAuB;CAGnB,YAAY,AAAiB,gBAA8B;EAA9B;cAF6B,EAAE;AAGxD,MAAI,eAAe,WAAW,EAC1B,OAAM,IAAI,MAAM,+CAA+C;AAGnE,OAAK,MAAM,OAAO,eACd,KAAI,IAAI,WAAW,qBACf,OAAM,IAAI,MAAM,0BAA0B,qBAAqB,QAAQ;;;;;CAQnF,MAAM,QAAQ,MAA+B;AACzC,MAAI,KAAK,KAAK,WAAW,EACrB,MAAK,OAAO,MAAM,QAAQ,IACtB,KAAK,eAAe,IAAI,OAAO,SAAS;GACpC,KAAK,MAAM,QAAQ,KAAK,CAAC,UAAU,CAAC;GACpC,QAAQ,MAAM,aAAa,IAAI;GAClC,EAAE,CACN;AAGL,SAAO,SAAS,MAAM,OAAO,WACzB,KAAK,KAAK,QAAQ,UAAU,MAAM,WAAW,OAAO,CAAC,KAAK,UAAU,MAAM,IAAI,CACjF;;;;;;;;;AC9BT,IAAa,YAAb,MAAuB;CAInB,YAAY,AAAiB,eAA2B;EAA3B;AACzB,MAAI,cAAc,WAAW,qBACzB,OAAM,IAAI,MAAM,0BAA0B,qBAAqB,QAAQ;;;;;CAO/E,MAAM,QAAQ,MAA+B;AACzC,MAAI,CAAC,KAAK,IACN,MAAK,MAAM,MAAM,QAAQ,KAAK,eAAe,CAAC,UAAU,CAAC;AAG7D,MAAI,CAAC,KAAK,UACN,MAAK,YAAY,MAAM,aAAa,KAAK,cAAc;AAG3D,SAAO,SAAS,MAAM,KAAK,KAAK,KAAK,UAAU;;;;;;;;;ACsBvD,SAAgB,mBAAmB,QAAsD;AACrF,QAAO,aAAa,UAAU,aAAa;;;;;AC3C/C,MAAM,sBAAsB;;;;AAK5B,SAAS,iBAAiB,OAA0B;AAChD,QAAO,MAAM,YAAY,MAAM,SAAS,KAAK,SAAS,oBAAoB,IAAI;;;;;AAMlF,SAAS,mBAAmB,OAA0B;AAClD,QAAO,OAAO,OAAO,MAAM,OAAO,CAAC,KAAK,iBAAiB;;;;;;;;AAS7D,SAAgB,uBAAiD,QAA0B;CACvF,IAAI;CACJ,IAAI;CACJ,IAAI;AAEJ,KAAI,mBAAmB,OAAO,CAC1B,oBAAmB;MAChB;EACH,MAAM,eAAe;AACrB,cAAY,IAAI,UAAU,aAAa,cAAc;AAErD,cAAY,IAAI,UADU,CAAC,aAAa,eAAe,GAAI,aAAa,kBAAkB,EAAE,CAAE,CAClD;;CAGhD,eAAe,aAAa,OAAe,OAAiB,OAAgC;AACxF,MAAI,iBACA,QAAO,iBAAiB,QAAQ,OAAO,OAAO,MAAM;AAExD,SAAO,UAAW,QAAQ,MAAM;;CAGpC,eAAe,aAAa,OAAe,OAAiB,OAAgC;AACxF,MAAI,iBACA,QAAO,iBAAiB,QAAQ,OAAO,OAAO,MAAM;AAExD,SAAO,UAAW,QAAQ,MAAM;;;;;CAMpC,eAAe,iBACX,QACA,WACA,MACa;EACb,MAAM,QAAQ,OAAO,OAAO;AAC5B,MAAI,CAAC,MAAO;AAEZ,OAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,KAAK,EAAE;AACnD,OAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,GACnD;GAGJ,MAAM,QAAQ,MAAM,OAAO;AAC3B,OAAI,CAAC,MAAO;AAGZ,OAAI,iBAAiB,MAAM,IAAI,OAAO,UAAU,UAAU;AACtD,SAAK,aAAa,MAAM,aAAa,WAAW,OAAO,MAAM;AAC7D;;AAIJ,OAAI,MAAM,YAAY,OAAO,UAAU,UAAU;IAC7C,MAAM,eAAe,MAAM;AAC3B,UAAM,oBAAoB,QAAQ,cAAc,MAAiC;;;;;;;CAQ7F,eAAe,oBACX,QACA,WACA,MACa;EAEb,MAAM,aAAa,KAAK;AACxB,MAAI,WACA,KAAI,MAAM,QAAQ,WAAW,CACzB,MAAK,MAAM,QAAQ,WACf,OAAM,iBAAiB,QAAQ,WAAW,KAAgC;MAG9E,OAAM,iBAAiB,QAAQ,WAAW,WAAsC;EAKxF,MAAM,iBAAiB,KAAK;AAC5B,MAAI,kBAAkB,OAAO,mBAAmB,UAAU;GACtD,MAAM,kBAAmB,eAA2C;AACpE,OAAI,MAAM,QAAQ,gBAAgB,CAC9B,MAAK,MAAM,QAAQ,gBACf,OAAM,iBAAiB,QAAQ,WAAW,KAAgC;;EAMtF,MAAM,aAAa,KAAK;AACxB,MAAI,WACA,KAAI,MAAM,QAAQ,WAAW,CACzB,MAAK,MAAM,QAAQ,YAAY;GAE3B,MAAM,WADa,KACS;AAC5B,OAAI,SACA,OAAM,iBAAiB,QAAQ,WAAW,SAAoC;;OAGnF;GACH,MAAM,YAAY;GAClB,MAAM,aAAa,UAAU;AAC7B,OAAI,WACA,OAAM,iBAAiB,QAAQ,WAAW,WAAsC;OAEhF,OAAM,iBAAiB,QAAQ,WAAW,UAAU;;EAMhE,MAAM,iBAAiB,KAAK;AAC5B,MAAI,eACA,KAAI,MAAM,QAAQ,eAAe,CAC7B,MAAK,MAAM,QAAQ,gBAAgB;GAE/B,MAAM,WADa,KACS;AAC5B,OAAI,SACA,OAAM,iBAAiB,QAAQ,WAAW,SAAoC;;OAGnF;GAEH,MAAM,aADY,eACW;AAC7B,OAAI,WACA,OAAM,iBAAiB,QAAQ,WAAW,WAAsC;;EAM5F,MAAM,aAAa,KAAK;AACxB,MAAI,WACA,KAAI,MAAM,QAAQ,WAAW,CACzB,MAAK,MAAM,QAAQ,YAAY;GAC3B,MAAM,aAAa;GACnB,MAAM,aAAa,WAAW;GAC9B,MAAM,aAAa,WAAW;AAC9B,OAAI,WACA,OAAM,iBAAiB,QAAQ,WAAW,WAAsC;AAEpF,OAAI,WACA,OAAM,iBAAiB,QAAQ,WAAW,WAAsC;;OAGrF;GACH,MAAM,YAAY;GAClB,MAAM,aAAa,UAAU;GAC7B,MAAM,aAAa,UAAU;AAC7B,OAAI,WACA,OAAM,iBAAiB,QAAQ,WAAW,WAAsC;AAEpF,OAAI,WACA,OAAM,iBAAiB,QAAQ,WAAW,WAAsC;;EAM5F,MAAM,sBAAsB,KAAK;AACjC,MAAI,oBACA,KAAI,MAAM,QAAQ,oBAAoB,CAClC,MAAK,MAAM,QAAQ,qBAAqB;GAEpC,MAAM,aADU,KACW;AAC3B,OAAI,WACA,OAAM,iBAAiB,QAAQ,WAAW,WAAsC;;OAGrF;GAEH,MAAM,aADS,oBACW;AAC1B,OAAI,WACA,OAAM,iBAAiB,QAAQ,WAAW,WAAsC;;;;;;CAShG,eAAe,kBACX,QACA,WACA,MACa;EACb,MAAM,QAAQ,OAAO,OAAO;AAC5B,MAAI,CAAC,MAAO;AAEZ,OAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,KAAK,EAAE;AACnD,OAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,GACnD;GAGJ,MAAM,QAAQ,MAAM,OAAO;AAC3B,OAAI,CAAC,MAAO;AAGZ,OAAI,iBAAiB,MAAM,IAAI,OAAO,UAAU,UAAU;AACtD,QAAI;AACA,UAAK,aAAa,MAAM,aAAa,WAAW,OAAO,MAAM;aACxD,OAAO;AAEZ,aAAQ,KAAK,2BAA2B,UAAU,GAAG,UAAU,IAAI,MAAM;;AAE7E;;AAIJ,OAAI,MAAM,YAAY,UAAU,MAAM;IAClC,MAAM,eAAe,MAAM;AAC3B,QAAI,MAAM,QAAQ,MAAM,EACpB;UAAK,MAAM,QAAQ,MACf,KAAI,OAAO,SAAS,YAAY,SAAS,KACrC,OAAM,kBAAkB,QAAQ,cAAc,KAAgC;eAG/E,OAAO,UAAU,SACxB,OAAM,kBAAkB,QAAQ,cAAc,MAAiC;;;;AAM/F,QAAO,aAA6B;EAChC,IAAI;EACJ,MAAM;EACN,aAAa;EAEb,SAAS,OAAO,QAAQ;GACpB,MAAM,EAAE,OAAO,WAAW,MAAM,SAAS,WAAW;GACpD,MAAM,SAAU,OAAe;GAC/B,MAAM,WAAW,OAAO,OAAO;AAG/B,OAAI,CAAC,YAAY,CAAC,mBAAmB,SAAS,CAC1C,QAAO,QAAQ,KAAK;GAIxB,MAAM,gBAAgB,OAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC,GAAG;AAGhE,OACI,cAAc,YACd,cAAc,YACd,cAAc,YACd,cAAc,gBACd,cAAc,gBACd,cAAc,uBAChB;AACE,QAAI,eAAe,KACf,KAAI,MAAM,QAAQ,cAAc,KAAK,CACjC,MAAK,MAAM,QAAQ,cAAc,KAC7B,OAAM,iBAAiB,QAAQ,OAAO,KAAK;QAG/C,OAAM,iBAAiB,QAAQ,OAAO,cAAc,KAAK;AAKjE,QAAI,cAAc,UAAU;AACxB,SAAI,eAAe,OACf,OAAM,iBAAiB,QAAQ,OAAO,cAAc,OAAO;AAE/D,SAAI,eAAe,OACf,OAAM,iBAAiB,QAAQ,OAAO,cAAc,OAAO;;;GAMvE,MAAM,SAAS,MAAM,QAAQ,cAAc;AAG3C,OAAI,WAAW,QAAQ,WAAW,QAC9B;QAAI,MAAM,QAAQ,OAAO,EACrB;UAAK,MAAM,QAAQ,OACf,KAAI,OAAO,SAAS,YAAY,SAAS,KACrC,OAAM,kBAAkB,QAAQ,OAAO,KAAgC;eAGxE,OAAO,WAAW,SACzB,OAAM,kBAAkB,QAAQ,OAAO,OAAkC;;AAIjF,UAAO;;EAEd,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/utils.ts","../src/decrypter.ts","../src/encrypter.ts","../src/types.ts","../src/plugin.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const ENCRYPTER_VERSION = 1;\nexport const ENCRYPTION_KEY_BYTES = 32;\nexport const IV_BYTES = 12;\nexport const ALGORITHM = 'AES-GCM';\nexport const KEY_DIGEST_BYTES = 8;\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\nconst encryptionMetaSchema = z.object({\n // version\n v: z.number(),\n // algorithm\n a: z.string(),\n // key digest\n k: z.string(),\n});\n\n/**\n * Resolve a key input to a Uint8Array. If the input is a string, it is\n * derived to 32 bytes via SHA-256. If it is already a Uint8Array, its\n * length is validated.\n */\nexport async function deriveKey(input: string | Uint8Array): Promise<Uint8Array> {\n if (typeof input === 'string') {\n const encoded = new TextEncoder().encode(input);\n return new Uint8Array(await crypto.subtle.digest('SHA-256', encoded));\n }\n if (input.length !== ENCRYPTION_KEY_BYTES) {\n throw new Error(`Encryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);\n }\n return input;\n}\n\n/**\n * Load a raw encryption key into a CryptoKey object\n */\nexport async function loadKey(key: Uint8Array, keyUsages: KeyUsage[]): Promise<CryptoKey> {\n // Convert to ArrayBuffer for crypto.subtle compatibility\n const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength) as ArrayBuffer;\n return crypto.subtle.importKey('raw', keyBuffer, ALGORITHM, false, keyUsages);\n}\n\n/**\n * Get a digest of the encryption key for identification\n */\nexport async function getKeyDigest(key: Uint8Array): Promise<string> {\n // Convert to ArrayBuffer for crypto.subtle compatibility\n const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength) as ArrayBuffer;\n const rawDigest = await crypto.subtle.digest('SHA-256', keyBuffer);\n return new Uint8Array(rawDigest.slice(0, KEY_DIGEST_BYTES)).reduce(\n (acc, byte) => acc + byte.toString(16).padStart(2, '0'),\n '',\n );\n}\n\n/**\n * Encrypt data using AES-GCM\n */\nexport async function _encrypt(data: string, key: CryptoKey, keyDigest: string): Promise<string> {\n const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));\n const encrypted = await crypto.subtle.encrypt(\n {\n name: ALGORITHM,\n iv,\n },\n key,\n encoder.encode(data),\n );\n\n // combine IV and encrypted data into a single array of bytes\n const cipherBytes = [...iv, ...new Uint8Array(encrypted)];\n\n // encryption metadata\n const meta = { v: ENCRYPTER_VERSION, a: ALGORITHM, k: keyDigest };\n\n // convert concatenated result to base64 string\n return `${btoa(JSON.stringify(meta))}.${btoa(String.fromCharCode(...cipherBytes))}`;\n}\n\n/**\n * Decrypt data using AES-GCM\n */\nexport async function _decrypt(data: string, findKey: (digest: string) => Promise<CryptoKey[]>): Promise<string> {\n const [metaText, cipherText] = data.split('.');\n if (!metaText || !cipherText) {\n throw new Error('Malformed encrypted data');\n }\n\n let metaObj: unknown;\n try {\n metaObj = JSON.parse(atob(metaText));\n } catch {\n throw new Error('Malformed metadata');\n }\n\n // parse meta\n const { a: algorithm, k: keyDigest } = encryptionMetaSchema.parse(metaObj);\n\n // find a matching decryption key\n const keys = await findKey(keyDigest);\n if (keys.length === 0) {\n throw new Error('No matching decryption key found');\n }\n\n // convert base64 back to bytes\n const bytes = Uint8Array.from(atob(cipherText), (c) => c.charCodeAt(0));\n\n // extract IV from the head\n const iv = bytes.slice(0, IV_BYTES);\n const cipher = bytes.slice(IV_BYTES);\n let lastError: unknown;\n\n for (const key of keys) {\n let decrypted: ArrayBuffer;\n try {\n decrypted = await crypto.subtle.decrypt({ name: algorithm, iv }, key, cipher);\n } catch (err) {\n lastError = err;\n continue;\n }\n return decoder.decode(decrypted);\n }\n\n throw lastError instanceof Error\n ? lastError\n : new Error('Decryption failed with all available keys');\n}\n","import { _decrypt, ENCRYPTION_KEY_BYTES, getKeyDigest, loadKey } from './utils.js';\n\n/**\n * Default decrypter with support for key rotation\n */\nexport class Decrypter {\n private keys: Array<{ key: CryptoKey; digest: string }> = [];\n private initPromise: Promise<void> | undefined;\n\n constructor(private readonly decryptionKeys: Uint8Array[]) {\n if (decryptionKeys.length === 0) {\n throw new Error('At least one decryption key must be provided');\n }\n\n for (const key of decryptionKeys) {\n if (key.length !== ENCRYPTION_KEY_BYTES) {\n throw new Error(`Decryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);\n }\n }\n }\n\n private async ensureKeys(): Promise<void> {\n if (this.keys.length > 0) return;\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = (async () => {\n this.keys = await Promise.all(\n this.decryptionKeys.map(async (key) => ({\n key: await loadKey(key, ['decrypt']),\n digest: await getKeyDigest(key),\n })),\n );\n })();\n\n return this.initPromise;\n }\n\n /**\n * Decrypts the given data\n */\n async decrypt(data: string): Promise<string> {\n await this.ensureKeys();\n\n return _decrypt(data, async (digest) =>\n this.keys.filter((entry) => entry.digest === digest).map((entry) => entry.key),\n );\n }\n}\n","import { _encrypt, ENCRYPTION_KEY_BYTES, getKeyDigest, loadKey } from './utils.js';\n\n/**\n * Default encrypter using AES-256-GCM\n */\nexport class Encrypter {\n private key: CryptoKey | undefined;\n private keyDigest: string | undefined;\n\n constructor(private readonly encryptionKey: Uint8Array) {\n if (encryptionKey.length !== ENCRYPTION_KEY_BYTES) {\n throw new Error(`Encryption key must be ${ENCRYPTION_KEY_BYTES} bytes`);\n }\n }\n\n /**\n * Encrypts the given data\n */\n async encrypt(data: string): Promise<string> {\n if (!this.key) {\n this.key = await loadKey(this.encryptionKey, ['encrypt']);\n }\n\n if (!this.keyDigest) {\n this.keyDigest = await getKeyDigest(this.encryptionKey);\n }\n\n return _encrypt(data, this.key, this.keyDigest);\n }\n}\n","import type { FieldDef } from '@zenstackhq/orm/schema';\n\n/**\n * Simple encryption configuration using built-in AES-256-GCM encryption\n */\nexport type SimpleEncryption = {\n /**\n * The encryption key. Pass a Uint8Array of exactly 32 bytes, or a string\n * which will be derived to a 32-byte key via SHA-256.\n */\n encryptionKey: string | Uint8Array;\n\n /**\n * Additional decryption keys for key rotation support.\n * When decrypting, all keys (encryptionKey + decryptionKeys) are tried.\n * Each key can be a Uint8Array (32 bytes) or a string (derived via SHA-256).\n */\n decryptionKeys?: (string | Uint8Array)[];\n};\n\n/**\n * Custom encryption configuration for user-provided encryption handlers\n */\nexport type CustomEncryption = {\n /**\n * Custom encryption function\n * @param model The model name\n * @param field The field definition\n * @param plain The plaintext value to encrypt\n * @returns The encrypted value\n */\n encrypt: (model: string, field: FieldDef, plain: string) => Promise<string>;\n\n /**\n * Custom decryption function\n * @param model The model name\n * @param field The field definition\n * @param cipher The encrypted value to decrypt\n * @returns The decrypted value\n */\n decrypt: (model: string, field: FieldDef, cipher: string) => Promise<string>;\n};\n\n/**\n * Encryption configuration - either simple (built-in) or custom\n */\nexport type EncryptionConfig = SimpleEncryption | CustomEncryption;\n\n/**\n * Type guard to check if encryption config is custom\n */\nexport function isCustomEncryption(config: EncryptionConfig): config is CustomEncryption {\n return 'encrypt' in config && 'decrypt' in config;\n}\n","import { definePlugin } from '@zenstackhq/orm';\nimport type { FieldDef, ModelDef, SchemaDef } from '@zenstackhq/orm/schema';\nimport { Decrypter } from './decrypter.js';\nimport { Encrypter } from './encrypter.js';\nimport type { CustomEncryption, EncryptionConfig, SimpleEncryption } from './types.js';\nimport { isCustomEncryption } from './types.js';\nimport { deriveKey } from './utils.js';\n\nconst ENCRYPTED_ATTRIBUTE = '@encrypted';\nconst warnedNonStringFields = new Set<string>();\n\n/**\n * Check if a field has the @encrypted attribute\n */\nfunction isEncryptedField(field: FieldDef): boolean {\n return field.attributes?.some((attr) => attr.name === ENCRYPTED_ATTRIBUTE) ?? false;\n}\n\n/**\n * Warn once if @encrypted is applied to a non-String field\n */\nfunction warnIfNonStringEncrypted(modelName: string, fieldName: string, field: FieldDef): void {\n if (isEncryptedField(field) && field.type !== 'String') {\n const key = `${modelName}.${fieldName}`;\n if (!warnedNonStringFields.has(key)) {\n warnedNonStringFields.add(key);\n console.warn(\n `@encrypted is only supported on String fields. ${key} (type: ${field.type}) will be ignored.`,\n );\n }\n }\n}\n\n/**\n * Check if a model has any encrypted fields\n */\nfunction hasEncryptedFields(model: ModelDef): boolean {\n return Object.values(model.fields).some(isEncryptedField);\n}\n\n/**\n * Creates an encryption plugin for ZenStack ORM\n *\n * @param config Encryption configuration (simple or custom)\n * @returns A runtime plugin that handles field encryption/decryption\n */\nexport function createEncryptionPlugin<Schema extends SchemaDef>(config: EncryptionConfig) {\n let encrypter: Encrypter | undefined;\n let decrypter: Decrypter | undefined;\n let customEncryption: CustomEncryption | undefined;\n let initialized = false;\n let initPromise: Promise<void> | undefined;\n\n async function ensureInitialized() {\n if (initialized) return;\n if (initPromise) return initPromise;\n\n initPromise = (async () => {\n if (isCustomEncryption(config)) {\n customEncryption = config;\n } else {\n const simpleConfig = config as SimpleEncryption;\n const encryptionKey = await deriveKey(simpleConfig.encryptionKey);\n const decryptionKeys = await Promise.all(\n (simpleConfig.decryptionKeys ?? []).map(deriveKey),\n );\n encrypter = new Encrypter(encryptionKey);\n decrypter = new Decrypter([encryptionKey, ...decryptionKeys]);\n }\n initialized = true;\n })();\n\n return initPromise;\n }\n\n async function encryptValue(model: string, field: FieldDef, value: string): Promise<string> {\n if (customEncryption) {\n return customEncryption.encrypt(model, field, value);\n }\n return encrypter!.encrypt(value);\n }\n\n async function decryptValue(model: string, field: FieldDef, value: string): Promise<string> {\n if (customEncryption) {\n return customEncryption.decrypt(model, field, value);\n }\n return decrypter!.decrypt(value);\n }\n\n /**\n * Recursively encrypt fields in write data\n */\n async function encryptWriteData(\n schema: SchemaDef,\n modelName: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n const model = schema.models[modelName];\n if (!model) return;\n\n for (const [fieldName, value] of Object.entries(data)) {\n if (value === null || value === undefined) {\n continue;\n }\n\n const field = model.fields[fieldName];\n if (!field) continue;\n\n warnIfNonStringEncrypted(modelName, fieldName, field);\n\n // Handle encrypted string fields\n if (isEncryptedField(field) && typeof value === 'string') {\n data[fieldName] = await encryptValue(modelName, field, value);\n continue;\n }\n\n // Handle relation fields (nested writes)\n if (field.relation && typeof value === 'object') {\n const relatedModel = field.type;\n await encryptNestedWrites(schema, relatedModel, value as Record<string, unknown>);\n }\n }\n }\n\n /**\n * Handle nested write operations (create, update, connect, etc.)\n */\n async function encryptNestedWrites(\n schema: SchemaDef,\n modelName: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n // Handle create\n const createData = data['create'];\n if (createData) {\n if (Array.isArray(createData)) {\n for (const item of createData) {\n await encryptWriteData(schema, modelName, item as Record<string, unknown>);\n }\n } else {\n await encryptWriteData(schema, modelName, createData as Record<string, unknown>);\n }\n }\n\n // Handle createMany\n const createManyData = data['createMany'];\n if (createManyData && typeof createManyData === 'object') {\n const createManyItems = (createManyData as Record<string, unknown>)['data'];\n if (Array.isArray(createManyItems)) {\n for (const item of createManyItems) {\n await encryptWriteData(schema, modelName, item as Record<string, unknown>);\n }\n }\n }\n\n // Handle update\n const updateData = data['update'];\n if (updateData) {\n if (Array.isArray(updateData)) {\n for (const item of updateData) {\n const updateItem = item as Record<string, unknown>;\n const itemData = updateItem['data'];\n if (itemData) {\n await encryptWriteData(schema, modelName, itemData as Record<string, unknown>);\n }\n }\n } else {\n const updateObj = updateData as Record<string, unknown>;\n const nestedData = updateObj['data'];\n if (nestedData) {\n await encryptWriteData(schema, modelName, nestedData as Record<string, unknown>);\n }\n }\n }\n\n // Handle updateMany\n const updateManyData = data['updateMany'];\n if (updateManyData) {\n if (Array.isArray(updateManyData)) {\n for (const item of updateManyData) {\n const updateItem = item as Record<string, unknown>;\n const itemData = updateItem['data'];\n if (itemData) {\n await encryptWriteData(schema, modelName, itemData as Record<string, unknown>);\n }\n }\n } else {\n const updateObj = updateManyData as Record<string, unknown>;\n const nestedData = updateObj['data'];\n if (nestedData) {\n await encryptWriteData(schema, modelName, nestedData as Record<string, unknown>);\n }\n }\n }\n\n // Handle upsert\n const upsertData = data['upsert'];\n if (upsertData) {\n if (Array.isArray(upsertData)) {\n for (const item of upsertData) {\n const upsertItem = item as Record<string, unknown>;\n const createPart = upsertItem['create'];\n const updatePart = upsertItem['update'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n if (updatePart) {\n await encryptWriteData(schema, modelName, updatePart as Record<string, unknown>);\n }\n }\n } else {\n const upsertObj = upsertData as Record<string, unknown>;\n const createPart = upsertObj['create'];\n const updatePart = upsertObj['update'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n if (updatePart) {\n await encryptWriteData(schema, modelName, updatePart as Record<string, unknown>);\n }\n }\n }\n\n // Handle connectOrCreate\n const connectOrCreateData = data['connectOrCreate'];\n if (connectOrCreateData) {\n if (Array.isArray(connectOrCreateData)) {\n for (const item of connectOrCreateData) {\n const cocItem = item as Record<string, unknown>;\n const createPart = cocItem['create'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n }\n } else {\n const cocObj = connectOrCreateData as Record<string, unknown>;\n const createPart = cocObj['create'];\n if (createPart) {\n await encryptWriteData(schema, modelName, createPart as Record<string, unknown>);\n }\n }\n }\n }\n\n /**\n * Recursively decrypt fields in result data\n */\n async function decryptResultData(\n schema: SchemaDef,\n modelName: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n const model = schema.models[modelName];\n if (!model) return;\n\n for (const [fieldName, value] of Object.entries(data)) {\n if (value === null || value === undefined) {\n continue;\n }\n\n const field = model.fields[fieldName];\n if (!field) continue;\n\n // Handle encrypted string fields\n if (isEncryptedField(field) && typeof value === 'string') {\n try {\n data[fieldName] = await decryptValue(modelName, field, value);\n } catch {\n // If decryption fails, keep original value\n console.warn('Failed to decrypt an encrypted field');\n }\n continue;\n }\n\n // Handle relation fields (nested data)\n if (field.relation && value !== null) {\n const relatedModel = field.type;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (typeof item === 'object' && item !== null) {\n await decryptResultData(schema, relatedModel, item as Record<string, unknown>);\n }\n }\n } else if (typeof value === 'object') {\n await decryptResultData(schema, relatedModel, value as Record<string, unknown>);\n }\n }\n }\n }\n\n return definePlugin<Schema, Record<string, never>, Record<string, never>>({\n id: 'encryption',\n name: 'Encryption Plugin',\n description: 'Automatically encrypts and decrypts fields marked with @encrypted',\n\n onQuery: async (ctx) => {\n await ensureInitialized();\n const { model, operation, args, proceed, client } = ctx;\n const schema = (client as unknown as { schema: SchemaDef }).schema;\n const modelDef = schema.models[model];\n\n // Check if this model has any encrypted fields\n if (!modelDef || !hasEncryptedFields(modelDef)) {\n return proceed(args);\n }\n\n // Handle write operations - encrypt data before writing\n const isWrite =\n operation === 'create' ||\n operation === 'update' ||\n operation === 'upsert' ||\n operation === 'createMany' ||\n operation === 'updateMany' ||\n operation === 'createManyAndReturn';\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let processedArgs = args as Record<string, any> | undefined;\n\n if (isWrite) {\n // Clone args to avoid mutating original\n processedArgs = args ? JSON.parse(JSON.stringify(args)) : undefined;\n\n if (processedArgs?.data) {\n if (Array.isArray(processedArgs.data)) {\n for (const item of processedArgs.data) {\n await encryptWriteData(schema, model, item);\n }\n } else {\n await encryptWriteData(schema, model, processedArgs.data);\n }\n }\n\n // Handle upsert create/update\n if (operation === 'upsert') {\n if (processedArgs?.create) {\n await encryptWriteData(schema, model, processedArgs.create);\n }\n if (processedArgs?.update) {\n await encryptWriteData(schema, model, processedArgs.update);\n }\n }\n }\n\n // Execute the query\n const result = await proceed(processedArgs);\n\n // Handle read operations - decrypt data after reading\n if (result !== null && result !== undefined) {\n if (Array.isArray(result)) {\n for (const item of result) {\n if (typeof item === 'object' && item !== null) {\n await decryptResultData(schema, model, item as Record<string, unknown>);\n }\n }\n } else if (typeof result === 'object') {\n await decryptResultData(schema, model, result as Record<string, unknown>);\n }\n }\n\n return result;\n },\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,60 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
2
|
+
"name": "zenstack-encryption",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "ZenStack Encryption Plugin - Automatic field encryption/decryption for @encrypted fields",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"zenstack",
|
|
7
|
+
"encryption",
|
|
8
|
+
"aes",
|
|
9
|
+
"crypto",
|
|
10
|
+
"plugin"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/genu/zenstack-encryption#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/genu/zenstack-encryption/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/genu/zenstack-encryption.git"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"plugin.zmodel",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./plugin.zmodel": "./plugin.zmodel",
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"watch": "tsdown --watch",
|
|
43
|
+
"lint": "eslint .",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"prepublishOnly": "pnpm run build",
|
|
46
|
+
"prepack": "pnpm run build"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@zenstackhq/orm": ">=3.3.0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"zod": "4.3.6"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@zenstackhq/orm": "3.3.3",
|
|
56
|
+
"eslint": "10.0.0",
|
|
57
|
+
"tsdown": "0.20.3",
|
|
58
|
+
"typescript": "5.9.3",
|
|
59
|
+
"typescript-eslint": "8.54.0",
|
|
60
|
+
"vitest": "4.0.18"
|
|
61
|
+
},
|
|
62
|
+
"packageManager": "pnpm@10.29.1"
|
|
63
|
+
}
|