@forklaunch/core 1.2.8 → 1.3.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/lib/persistence/index.d.mts +74 -38
- package/lib/persistence/index.d.ts +74 -38
- package/lib/persistence/index.js +152 -89
- package/lib/persistence/index.js.map +1 -1
- package/lib/persistence/index.mjs +143 -84
- package/lib/persistence/index.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PropertyBuilders, EntityMetadataWithProperties, EntitySchemaWithMeta, InferEntityFromProperties,
|
|
1
|
+
import { PropertyBuilders, EntityMetadataWithProperties, EntitySchemaWithMeta, InferEntityFromProperties, Type, Platform, TransformContext, FilterDef, EntityManager, EventSubscriber, MikroORM } from '@mikro-orm/core';
|
|
2
2
|
|
|
3
3
|
declare const ComplianceLevel: {
|
|
4
4
|
readonly pii: "pii";
|
|
@@ -101,6 +101,19 @@ declare function defineComplianceEntity<const TName extends string, const TTable
|
|
|
101
101
|
userIdField?: string;
|
|
102
102
|
}): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and
|
|
106
|
+
* `nativeDelete` on entities that have PII, PHI, or PCI compliance fields.
|
|
107
|
+
*
|
|
108
|
+
* This prevents bypassing the EncryptedType's encryption by using raw
|
|
109
|
+
* queries. Call this in the tenant context middleware when creating the
|
|
110
|
+
* request-scoped EM.
|
|
111
|
+
*
|
|
112
|
+
* @returns A Proxy-wrapped EntityManager that throws on native query
|
|
113
|
+
* operations targeting compliance entities.
|
|
114
|
+
*/
|
|
115
|
+
declare function wrapEmWithNativeQueryBlocking<T extends object>(em: T): T;
|
|
116
|
+
|
|
104
117
|
declare class MissingEncryptionKeyError extends Error {
|
|
105
118
|
readonly name: "MissingEncryptionKeyError";
|
|
106
119
|
constructor(message?: string);
|
|
@@ -122,60 +135,83 @@ declare class FieldEncryptor {
|
|
|
122
135
|
*/
|
|
123
136
|
deriveKey(tenantId: string): Buffer;
|
|
124
137
|
/**
|
|
125
|
-
*
|
|
138
|
+
* Derive a deterministic IV from the key and plaintext using HMAC-SHA256,
|
|
139
|
+
* truncated to IV_BYTES. Same plaintext + same key → same IV → same
|
|
140
|
+
* ciphertext. This enables WHERE clause matching on encrypted columns
|
|
141
|
+
* while maintaining AES-256-GCM authenticated encryption.
|
|
142
|
+
*
|
|
143
|
+
* Meets SOC 2, HIPAA, PCI DSS, GDPR requirements for encryption at rest.
|
|
144
|
+
*/
|
|
145
|
+
private deriveDeterministicIv;
|
|
146
|
+
/**
|
|
147
|
+
* Encrypt a plaintext string.
|
|
126
148
|
*
|
|
127
|
-
*
|
|
149
|
+
* Uses deterministic encryption (HMAC-derived IV) so the same plaintext
|
|
150
|
+
* always produces the same ciphertext. This enables database WHERE clause
|
|
151
|
+
* matching and UNIQUE constraints on encrypted columns.
|
|
152
|
+
*
|
|
153
|
+
* @returns Format: `v2:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`
|
|
128
154
|
*/
|
|
129
155
|
encrypt(plaintext: string | null): string | null;
|
|
130
156
|
encrypt(plaintext: string | null, tenantId: string): string | null;
|
|
131
157
|
/**
|
|
132
158
|
* Decrypt a ciphertext string produced by {@link encrypt}.
|
|
159
|
+
* Supports both v1 (random IV) and v2 (deterministic IV) formats.
|
|
133
160
|
*/
|
|
134
161
|
decrypt(ciphertext: string | null): string | null;
|
|
135
162
|
decrypt(ciphertext: string | null, tenantId: string): string | null;
|
|
136
163
|
}
|
|
137
164
|
|
|
138
165
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
166
|
+
* Register the FieldEncryptor instance for use by EncryptedType.
|
|
167
|
+
* Call this once at application bootstrap (e.g., in mikro-orm.config.ts).
|
|
168
|
+
*/
|
|
169
|
+
declare function registerEncryptor(encryptor: FieldEncryptor): void;
|
|
170
|
+
/**
|
|
171
|
+
* Run a callback with the given tenant ID available to EncryptedType
|
|
172
|
+
* for per-tenant key derivation.
|
|
173
|
+
*/
|
|
174
|
+
declare function withEncryptionContext<T>(tenantId: string, fn: () => T): T;
|
|
175
|
+
/**
|
|
176
|
+
* Set the encryption tenant ID for the current async context.
|
|
177
|
+
* Called automatically by the tenant context middleware — users
|
|
178
|
+
* don't need to call this directly.
|
|
179
|
+
*/
|
|
180
|
+
declare function setEncryptionTenantId(tenantId: string): void;
|
|
181
|
+
/**
|
|
182
|
+
* Get the current tenant ID. Returns empty string when no context is set
|
|
183
|
+
* (startup, seeders, better-auth, background jobs).
|
|
184
|
+
*/
|
|
185
|
+
declare function getCurrentTenantId(): string;
|
|
186
|
+
/**
|
|
187
|
+
* MikroORM custom Type that transparently encrypts/decrypts values.
|
|
188
|
+
*
|
|
189
|
+
* Works with any JS type (string, number, boolean, object/JSON).
|
|
190
|
+
* Non-string values are JSON-serialized before encryption and
|
|
191
|
+
* JSON-parsed after decryption.
|
|
141
192
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* key is unavailable.
|
|
145
|
-
* - **onLoad**: Decrypts PHI/PCI fields after loading from the database.
|
|
146
|
-
* Pre-migration plaintext (no `v1:` prefix) is returned as-is with a
|
|
147
|
-
* console warning to support rolling deployments.
|
|
193
|
+
* DB column is always `text` — the encrypted ciphertext is a string
|
|
194
|
+
* regardless of the original JS type.
|
|
148
195
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
196
|
+
* Operates at the data conversion layer — before the identity map,
|
|
197
|
+
* during hydration, and during persistence. Entities always hold
|
|
198
|
+
* their original JS values (plaintext).
|
|
151
199
|
*/
|
|
152
|
-
declare class
|
|
153
|
-
private readonly
|
|
154
|
-
constructor(encryptor: FieldEncryptor);
|
|
155
|
-
beforeCreate(args: EventArgs<unknown>): Promise<void>;
|
|
156
|
-
beforeUpdate(args: EventArgs<unknown>): Promise<void>;
|
|
157
|
-
onLoad(args: EventArgs<unknown>): Promise<void>;
|
|
158
|
-
private encryptFields;
|
|
159
|
-
private decryptFields;
|
|
200
|
+
declare class EncryptedType extends Type<unknown, string | null> {
|
|
201
|
+
private readonly originalType;
|
|
160
202
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* better-auth). Callers skip encryption/decryption in that case.
|
|
203
|
+
* @param originalType - The original JS type hint ('string' | 'json' | 'number' | 'boolean').
|
|
204
|
+
* Used to determine serialization strategy.
|
|
164
205
|
*/
|
|
165
|
-
|
|
206
|
+
constructor(originalType?: string);
|
|
207
|
+
convertToDatabaseValue(value: unknown, _platform: Platform, _context?: TransformContext): string | null;
|
|
208
|
+
convertToJSValue(value: string | null, _platform: Platform): unknown;
|
|
209
|
+
getColumnType(): string;
|
|
210
|
+
get runtimeType(): string;
|
|
211
|
+
ensureComparable(): boolean;
|
|
212
|
+
private serializeValue;
|
|
213
|
+
private deserializeValue;
|
|
166
214
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and
|
|
169
|
-
* `nativeDelete` on entities that have PHI or PCI compliance fields.
|
|
170
|
-
*
|
|
171
|
-
* This prevents bypassing the ComplianceEventSubscriber's encryption
|
|
172
|
-
* by using raw queries. Call this in the tenant context middleware when
|
|
173
|
-
* creating the request-scoped EM.
|
|
174
|
-
*
|
|
175
|
-
* @returns A Proxy-wrapped EntityManager that throws on native query
|
|
176
|
-
* operations targeting compliance entities.
|
|
177
|
-
*/
|
|
178
|
-
declare function wrapEmWithNativeQueryBlocking<T extends object>(em: T): T;
|
|
179
215
|
|
|
180
216
|
/**
|
|
181
217
|
* The name used to register the tenant isolation filter.
|
|
@@ -276,4 +312,4 @@ declare class RlsEventSubscriber implements EventSubscriber {
|
|
|
276
312
|
*/
|
|
277
313
|
declare function setupRls(orm: MikroORM, config?: RlsConfig): void;
|
|
278
314
|
|
|
279
|
-
export {
|
|
315
|
+
export { ComplianceLevel, ComplianceLevel as ComplianceLevelType, DecryptionError, EncryptedType, EncryptionRequiredError, FieldEncryptor, MissingEncryptionKeyError, type ParsedDuration, RetentionAction, RetentionAction as RetentionActionType, RetentionDuration, type RetentionPolicy, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getAllRetentionPolicies, getAllUserIdFields, getComplianceMetadata, getCurrentTenantId, getEntityComplianceFields, getEntityRetention, getEntityUserIdField, getSuperAdminContext, parseDuration, registerEncryptor, setEncryptionTenantId, setupRls, setupTenantFilter, subtractDuration, withEncryptionContext, wrapEmWithNativeQueryBlocking };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PropertyBuilders, EntityMetadataWithProperties, EntitySchemaWithMeta, InferEntityFromProperties,
|
|
1
|
+
import { PropertyBuilders, EntityMetadataWithProperties, EntitySchemaWithMeta, InferEntityFromProperties, Type, Platform, TransformContext, FilterDef, EntityManager, EventSubscriber, MikroORM } from '@mikro-orm/core';
|
|
2
2
|
|
|
3
3
|
declare const ComplianceLevel: {
|
|
4
4
|
readonly pii: "pii";
|
|
@@ -101,6 +101,19 @@ declare function defineComplianceEntity<const TName extends string, const TTable
|
|
|
101
101
|
userIdField?: string;
|
|
102
102
|
}): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and
|
|
106
|
+
* `nativeDelete` on entities that have PII, PHI, or PCI compliance fields.
|
|
107
|
+
*
|
|
108
|
+
* This prevents bypassing the EncryptedType's encryption by using raw
|
|
109
|
+
* queries. Call this in the tenant context middleware when creating the
|
|
110
|
+
* request-scoped EM.
|
|
111
|
+
*
|
|
112
|
+
* @returns A Proxy-wrapped EntityManager that throws on native query
|
|
113
|
+
* operations targeting compliance entities.
|
|
114
|
+
*/
|
|
115
|
+
declare function wrapEmWithNativeQueryBlocking<T extends object>(em: T): T;
|
|
116
|
+
|
|
104
117
|
declare class MissingEncryptionKeyError extends Error {
|
|
105
118
|
readonly name: "MissingEncryptionKeyError";
|
|
106
119
|
constructor(message?: string);
|
|
@@ -122,60 +135,83 @@ declare class FieldEncryptor {
|
|
|
122
135
|
*/
|
|
123
136
|
deriveKey(tenantId: string): Buffer;
|
|
124
137
|
/**
|
|
125
|
-
*
|
|
138
|
+
* Derive a deterministic IV from the key and plaintext using HMAC-SHA256,
|
|
139
|
+
* truncated to IV_BYTES. Same plaintext + same key → same IV → same
|
|
140
|
+
* ciphertext. This enables WHERE clause matching on encrypted columns
|
|
141
|
+
* while maintaining AES-256-GCM authenticated encryption.
|
|
142
|
+
*
|
|
143
|
+
* Meets SOC 2, HIPAA, PCI DSS, GDPR requirements for encryption at rest.
|
|
144
|
+
*/
|
|
145
|
+
private deriveDeterministicIv;
|
|
146
|
+
/**
|
|
147
|
+
* Encrypt a plaintext string.
|
|
126
148
|
*
|
|
127
|
-
*
|
|
149
|
+
* Uses deterministic encryption (HMAC-derived IV) so the same plaintext
|
|
150
|
+
* always produces the same ciphertext. This enables database WHERE clause
|
|
151
|
+
* matching and UNIQUE constraints on encrypted columns.
|
|
152
|
+
*
|
|
153
|
+
* @returns Format: `v2:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`
|
|
128
154
|
*/
|
|
129
155
|
encrypt(plaintext: string | null): string | null;
|
|
130
156
|
encrypt(plaintext: string | null, tenantId: string): string | null;
|
|
131
157
|
/**
|
|
132
158
|
* Decrypt a ciphertext string produced by {@link encrypt}.
|
|
159
|
+
* Supports both v1 (random IV) and v2 (deterministic IV) formats.
|
|
133
160
|
*/
|
|
134
161
|
decrypt(ciphertext: string | null): string | null;
|
|
135
162
|
decrypt(ciphertext: string | null, tenantId: string): string | null;
|
|
136
163
|
}
|
|
137
164
|
|
|
138
165
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
166
|
+
* Register the FieldEncryptor instance for use by EncryptedType.
|
|
167
|
+
* Call this once at application bootstrap (e.g., in mikro-orm.config.ts).
|
|
168
|
+
*/
|
|
169
|
+
declare function registerEncryptor(encryptor: FieldEncryptor): void;
|
|
170
|
+
/**
|
|
171
|
+
* Run a callback with the given tenant ID available to EncryptedType
|
|
172
|
+
* for per-tenant key derivation.
|
|
173
|
+
*/
|
|
174
|
+
declare function withEncryptionContext<T>(tenantId: string, fn: () => T): T;
|
|
175
|
+
/**
|
|
176
|
+
* Set the encryption tenant ID for the current async context.
|
|
177
|
+
* Called automatically by the tenant context middleware — users
|
|
178
|
+
* don't need to call this directly.
|
|
179
|
+
*/
|
|
180
|
+
declare function setEncryptionTenantId(tenantId: string): void;
|
|
181
|
+
/**
|
|
182
|
+
* Get the current tenant ID. Returns empty string when no context is set
|
|
183
|
+
* (startup, seeders, better-auth, background jobs).
|
|
184
|
+
*/
|
|
185
|
+
declare function getCurrentTenantId(): string;
|
|
186
|
+
/**
|
|
187
|
+
* MikroORM custom Type that transparently encrypts/decrypts values.
|
|
188
|
+
*
|
|
189
|
+
* Works with any JS type (string, number, boolean, object/JSON).
|
|
190
|
+
* Non-string values are JSON-serialized before encryption and
|
|
191
|
+
* JSON-parsed after decryption.
|
|
141
192
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* key is unavailable.
|
|
145
|
-
* - **onLoad**: Decrypts PHI/PCI fields after loading from the database.
|
|
146
|
-
* Pre-migration plaintext (no `v1:` prefix) is returned as-is with a
|
|
147
|
-
* console warning to support rolling deployments.
|
|
193
|
+
* DB column is always `text` — the encrypted ciphertext is a string
|
|
194
|
+
* regardless of the original JS type.
|
|
148
195
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
196
|
+
* Operates at the data conversion layer — before the identity map,
|
|
197
|
+
* during hydration, and during persistence. Entities always hold
|
|
198
|
+
* their original JS values (plaintext).
|
|
151
199
|
*/
|
|
152
|
-
declare class
|
|
153
|
-
private readonly
|
|
154
|
-
constructor(encryptor: FieldEncryptor);
|
|
155
|
-
beforeCreate(args: EventArgs<unknown>): Promise<void>;
|
|
156
|
-
beforeUpdate(args: EventArgs<unknown>): Promise<void>;
|
|
157
|
-
onLoad(args: EventArgs<unknown>): Promise<void>;
|
|
158
|
-
private encryptFields;
|
|
159
|
-
private decryptFields;
|
|
200
|
+
declare class EncryptedType extends Type<unknown, string | null> {
|
|
201
|
+
private readonly originalType;
|
|
160
202
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* better-auth). Callers skip encryption/decryption in that case.
|
|
203
|
+
* @param originalType - The original JS type hint ('string' | 'json' | 'number' | 'boolean').
|
|
204
|
+
* Used to determine serialization strategy.
|
|
164
205
|
*/
|
|
165
|
-
|
|
206
|
+
constructor(originalType?: string);
|
|
207
|
+
convertToDatabaseValue(value: unknown, _platform: Platform, _context?: TransformContext): string | null;
|
|
208
|
+
convertToJSValue(value: string | null, _platform: Platform): unknown;
|
|
209
|
+
getColumnType(): string;
|
|
210
|
+
get runtimeType(): string;
|
|
211
|
+
ensureComparable(): boolean;
|
|
212
|
+
private serializeValue;
|
|
213
|
+
private deserializeValue;
|
|
166
214
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and
|
|
169
|
-
* `nativeDelete` on entities that have PHI or PCI compliance fields.
|
|
170
|
-
*
|
|
171
|
-
* This prevents bypassing the ComplianceEventSubscriber's encryption
|
|
172
|
-
* by using raw queries. Call this in the tenant context middleware when
|
|
173
|
-
* creating the request-scoped EM.
|
|
174
|
-
*
|
|
175
|
-
* @returns A Proxy-wrapped EntityManager that throws on native query
|
|
176
|
-
* operations targeting compliance entities.
|
|
177
|
-
*/
|
|
178
|
-
declare function wrapEmWithNativeQueryBlocking<T extends object>(em: T): T;
|
|
179
215
|
|
|
180
216
|
/**
|
|
181
217
|
* The name used to register the tenant isolation filter.
|
|
@@ -276,4 +312,4 @@ declare class RlsEventSubscriber implements EventSubscriber {
|
|
|
276
312
|
*/
|
|
277
313
|
declare function setupRls(orm: MikroORM, config?: RlsConfig): void;
|
|
278
314
|
|
|
279
|
-
export {
|
|
315
|
+
export { ComplianceLevel, ComplianceLevel as ComplianceLevelType, DecryptionError, EncryptedType, EncryptionRequiredError, FieldEncryptor, MissingEncryptionKeyError, type ParsedDuration, RetentionAction, RetentionAction as RetentionActionType, RetentionDuration, type RetentionPolicy, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getAllRetentionPolicies, getAllUserIdFields, getComplianceMetadata, getCurrentTenantId, getEntityComplianceFields, getEntityRetention, getEntityUserIdField, getSuperAdminContext, parseDuration, registerEncryptor, setEncryptionTenantId, setupRls, setupTenantFilter, subtractDuration, withEncryptionContext, wrapEmWithNativeQueryBlocking };
|
package/lib/persistence/index.js
CHANGED
|
@@ -30,9 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/persistence/index.ts
|
|
31
31
|
var persistence_exports = {};
|
|
32
32
|
__export(persistence_exports, {
|
|
33
|
-
ComplianceEventSubscriber: () => ComplianceEventSubscriber,
|
|
34
33
|
ComplianceLevel: () => ComplianceLevel,
|
|
35
34
|
DecryptionError: () => DecryptionError,
|
|
35
|
+
EncryptedType: () => EncryptedType,
|
|
36
36
|
EncryptionRequiredError: () => EncryptionRequiredError,
|
|
37
37
|
FieldEncryptor: () => FieldEncryptor,
|
|
38
38
|
MissingEncryptionKeyError: () => MissingEncryptionKeyError,
|
|
@@ -47,14 +47,18 @@ __export(persistence_exports, {
|
|
|
47
47
|
getAllRetentionPolicies: () => getAllRetentionPolicies,
|
|
48
48
|
getAllUserIdFields: () => getAllUserIdFields,
|
|
49
49
|
getComplianceMetadata: () => getComplianceMetadata,
|
|
50
|
+
getCurrentTenantId: () => getCurrentTenantId,
|
|
50
51
|
getEntityComplianceFields: () => getEntityComplianceFields,
|
|
51
52
|
getEntityRetention: () => getEntityRetention,
|
|
52
53
|
getEntityUserIdField: () => getEntityUserIdField,
|
|
53
54
|
getSuperAdminContext: () => getSuperAdminContext,
|
|
54
55
|
parseDuration: () => parseDuration,
|
|
56
|
+
registerEncryptor: () => registerEncryptor,
|
|
57
|
+
setEncryptionTenantId: () => setEncryptionTenantId,
|
|
55
58
|
setupRls: () => setupRls,
|
|
56
59
|
setupTenantFilter: () => setupTenantFilter,
|
|
57
60
|
subtractDuration: () => subtractDuration,
|
|
61
|
+
withEncryptionContext: () => withEncryptionContext,
|
|
58
62
|
wrapEmWithNativeQueryBlocking: () => wrapEmWithNativeQueryBlocking
|
|
59
63
|
});
|
|
60
64
|
module.exports = __toCommonJS(persistence_exports);
|
|
@@ -143,7 +147,102 @@ function getAllRetentionPolicies() {
|
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
// src/persistence/compliancePropertyBuilder.ts
|
|
150
|
+
var import_core2 = require("@mikro-orm/core");
|
|
151
|
+
|
|
152
|
+
// src/persistence/encryptedType.ts
|
|
153
|
+
var import_node_async_hooks = require("async_hooks");
|
|
146
154
|
var import_core = require("@mikro-orm/core");
|
|
155
|
+
var ENCRYPTED_PREFIXES = ["v1:", "v2:"];
|
|
156
|
+
function isEncrypted(value) {
|
|
157
|
+
return ENCRYPTED_PREFIXES.some((p3) => value.startsWith(p3));
|
|
158
|
+
}
|
|
159
|
+
var _encryptor;
|
|
160
|
+
var _tenantContext = new import_node_async_hooks.AsyncLocalStorage();
|
|
161
|
+
function registerEncryptor(encryptor) {
|
|
162
|
+
_encryptor = encryptor;
|
|
163
|
+
}
|
|
164
|
+
function withEncryptionContext(tenantId, fn) {
|
|
165
|
+
return _tenantContext.run({ tenantId }, fn);
|
|
166
|
+
}
|
|
167
|
+
function setEncryptionTenantId(tenantId) {
|
|
168
|
+
_tenantContext.enterWith({ tenantId });
|
|
169
|
+
}
|
|
170
|
+
function getCurrentTenantId() {
|
|
171
|
+
return _tenantContext.getStore()?.tenantId ?? "";
|
|
172
|
+
}
|
|
173
|
+
var EncryptedType = class extends import_core.Type {
|
|
174
|
+
originalType;
|
|
175
|
+
/**
|
|
176
|
+
* @param originalType - The original JS type hint ('string' | 'json' | 'number' | 'boolean').
|
|
177
|
+
* Used to determine serialization strategy.
|
|
178
|
+
*/
|
|
179
|
+
constructor(originalType = "string") {
|
|
180
|
+
super();
|
|
181
|
+
this.originalType = originalType;
|
|
182
|
+
}
|
|
183
|
+
convertToDatabaseValue(value, _platform, _context) {
|
|
184
|
+
if (value === null || value === void 0) return null;
|
|
185
|
+
if (typeof value === "string" && value.length === 0) return "";
|
|
186
|
+
if (!_encryptor) {
|
|
187
|
+
return this.serializeValue(value);
|
|
188
|
+
}
|
|
189
|
+
if (typeof value === "string" && isEncrypted(value)) {
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
const serialized = this.serializeValue(value);
|
|
193
|
+
return _encryptor.encrypt(serialized, getCurrentTenantId()) ?? serialized;
|
|
194
|
+
}
|
|
195
|
+
convertToJSValue(value, _platform) {
|
|
196
|
+
if (value === null || value === void 0) return null;
|
|
197
|
+
if (typeof value !== "string") return value;
|
|
198
|
+
if (!isEncrypted(value)) {
|
|
199
|
+
return this.deserializeValue(value);
|
|
200
|
+
}
|
|
201
|
+
if (!_encryptor) return value;
|
|
202
|
+
try {
|
|
203
|
+
const decrypted = _encryptor.decrypt(value, getCurrentTenantId());
|
|
204
|
+
if (decrypted === null) return null;
|
|
205
|
+
return this.deserializeValue(decrypted);
|
|
206
|
+
} catch {
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
getColumnType() {
|
|
211
|
+
return "text";
|
|
212
|
+
}
|
|
213
|
+
get runtimeType() {
|
|
214
|
+
return this.originalType === "json" ? "object" : this.originalType;
|
|
215
|
+
}
|
|
216
|
+
ensureComparable() {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Serialization helpers
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
serializeValue(value) {
|
|
223
|
+
if (typeof value === "string") return value;
|
|
224
|
+
return JSON.stringify(value);
|
|
225
|
+
}
|
|
226
|
+
deserializeValue(value) {
|
|
227
|
+
switch (this.originalType) {
|
|
228
|
+
case "string":
|
|
229
|
+
return value;
|
|
230
|
+
case "number":
|
|
231
|
+
return Number(value);
|
|
232
|
+
case "boolean":
|
|
233
|
+
return value === "true";
|
|
234
|
+
case "json":
|
|
235
|
+
default:
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(value);
|
|
238
|
+
} catch {
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/persistence/compliancePropertyBuilder.ts
|
|
147
246
|
function isBuilder(value) {
|
|
148
247
|
return value != null && typeof value === "object" && "~options" in value;
|
|
149
248
|
}
|
|
@@ -169,7 +268,34 @@ function wrapUnclassified(builder) {
|
|
|
169
268
|
}
|
|
170
269
|
});
|
|
171
270
|
}
|
|
271
|
+
var ENCRYPTED_LEVELS = /* @__PURE__ */ new Set(["pii", "phi", "pci"]);
|
|
272
|
+
function detectOriginalType(options) {
|
|
273
|
+
const type = options.type;
|
|
274
|
+
if (!type) return "string";
|
|
275
|
+
let t;
|
|
276
|
+
if (typeof type === "string") {
|
|
277
|
+
t = type.toLowerCase();
|
|
278
|
+
} else if (typeof type === "object" && type !== null) {
|
|
279
|
+
const rt = type.runtimeType;
|
|
280
|
+
t = rt ?? type.constructor?.name?.toLowerCase() ?? "string";
|
|
281
|
+
} else {
|
|
282
|
+
return "string";
|
|
283
|
+
}
|
|
284
|
+
if (t.includes("json") || t === "object") return "json";
|
|
285
|
+
if (t.includes("int") || t === "number" || t === "double" || t === "float" || t === "decimal" || t === "smallint" || t === "tinyint") return "number";
|
|
286
|
+
if (t === "boolean" || t === "bool") return "boolean";
|
|
287
|
+
return "string";
|
|
288
|
+
}
|
|
172
289
|
function wrapClassified(builder, level) {
|
|
290
|
+
if (ENCRYPTED_LEVELS.has(level)) {
|
|
291
|
+
const options = builder["~options"];
|
|
292
|
+
if (options) {
|
|
293
|
+
const originalType = detectOriginalType(options);
|
|
294
|
+
options.type = new EncryptedType(originalType);
|
|
295
|
+
options.columnType = "text";
|
|
296
|
+
options.runtimeType = originalType === "json" ? "object" : originalType;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
173
299
|
return new Proxy(builder, {
|
|
174
300
|
get(target, prop) {
|
|
175
301
|
if (prop === COMPLIANCE_KEY) return level;
|
|
@@ -201,7 +327,7 @@ var RELATION_METHODS = /* @__PURE__ */ new Set([
|
|
|
201
327
|
function isRelationMethod(prop) {
|
|
202
328
|
return typeof prop === "string" && RELATION_METHODS.has(prop);
|
|
203
329
|
}
|
|
204
|
-
var fp = new Proxy(
|
|
330
|
+
var fp = new Proxy(import_core2.p, {
|
|
205
331
|
get(target, prop) {
|
|
206
332
|
const value = Reflect.get(target, prop, target);
|
|
207
333
|
if (typeof value !== "function") return value;
|
|
@@ -225,7 +351,7 @@ var fp = new Proxy(import_core.p, {
|
|
|
225
351
|
});
|
|
226
352
|
|
|
227
353
|
// src/persistence/defineComplianceEntity.ts
|
|
228
|
-
var
|
|
354
|
+
var import_core3 = require("@mikro-orm/core");
|
|
229
355
|
function readComplianceLevel(builder) {
|
|
230
356
|
if (builder == null || typeof builder !== "object") return void 0;
|
|
231
357
|
return builder[COMPLIANCE_KEY];
|
|
@@ -234,7 +360,7 @@ function defineComplianceEntity(meta) {
|
|
|
234
360
|
const entityName = "name" in meta ? meta.name : "Unknown";
|
|
235
361
|
const complianceFields = /* @__PURE__ */ new Map();
|
|
236
362
|
const rawProperties = meta.properties;
|
|
237
|
-
const resolvedProperties = typeof rawProperties === "function" ? rawProperties(
|
|
363
|
+
const resolvedProperties = typeof rawProperties === "function" ? rawProperties(import_core3.p) : rawProperties;
|
|
238
364
|
for (const [fieldName, rawProp] of Object.entries(resolvedProperties)) {
|
|
239
365
|
if (typeof rawProp === "function") {
|
|
240
366
|
complianceFields.set(fieldName, "none");
|
|
@@ -261,7 +387,7 @@ function defineComplianceEntity(meta) {
|
|
|
261
387
|
if (meta.userIdField) {
|
|
262
388
|
registerEntityUserIdField(entityName, meta.userIdField);
|
|
263
389
|
}
|
|
264
|
-
return (0,
|
|
390
|
+
return (0, import_core3.defineEntity)(
|
|
265
391
|
meta
|
|
266
392
|
);
|
|
267
393
|
}
|
|
@@ -308,12 +434,23 @@ var FieldEncryptor = class {
|
|
|
308
434
|
import_crypto.default.hkdfSync(HKDF_HASH, this.masterKey, HKDF_SALT, tenantId, KEY_BYTES)
|
|
309
435
|
);
|
|
310
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Derive a deterministic IV from the key and plaintext using HMAC-SHA256,
|
|
439
|
+
* truncated to IV_BYTES. Same plaintext + same key → same IV → same
|
|
440
|
+
* ciphertext. This enables WHERE clause matching on encrypted columns
|
|
441
|
+
* while maintaining AES-256-GCM authenticated encryption.
|
|
442
|
+
*
|
|
443
|
+
* Meets SOC 2, HIPAA, PCI DSS, GDPR requirements for encryption at rest.
|
|
444
|
+
*/
|
|
445
|
+
deriveDeterministicIv(key, plaintext) {
|
|
446
|
+
return import_crypto.default.createHmac("sha256", key).update(plaintext).digest().subarray(0, IV_BYTES);
|
|
447
|
+
}
|
|
311
448
|
encrypt(plaintext, tenantId) {
|
|
312
449
|
if (plaintext === null || plaintext === void 0) {
|
|
313
450
|
return null;
|
|
314
451
|
}
|
|
315
452
|
const key = this.deriveKey(tenantId ?? "");
|
|
316
|
-
const iv =
|
|
453
|
+
const iv = this.deriveDeterministicIv(key, plaintext);
|
|
317
454
|
const cipher = import_crypto.default.createCipheriv(ALGORITHM, key, iv);
|
|
318
455
|
const encrypted = Buffer.concat([
|
|
319
456
|
cipher.update(plaintext, "utf8"),
|
|
@@ -321,7 +458,7 @@ var FieldEncryptor = class {
|
|
|
321
458
|
]);
|
|
322
459
|
const authTag = cipher.getAuthTag();
|
|
323
460
|
return [
|
|
324
|
-
"
|
|
461
|
+
"v2",
|
|
325
462
|
iv.toString("base64"),
|
|
326
463
|
authTag.toString("base64"),
|
|
327
464
|
encrypted.toString("base64")
|
|
@@ -332,7 +469,7 @@ var FieldEncryptor = class {
|
|
|
332
469
|
return null;
|
|
333
470
|
}
|
|
334
471
|
const parts = ciphertext.split(":");
|
|
335
|
-
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
472
|
+
if (parts.length !== 4 || parts[0] !== "v1" && parts[0] !== "v2") {
|
|
336
473
|
throw new DecryptionError(
|
|
337
474
|
`Unknown ciphertext version or malformed format`
|
|
338
475
|
);
|
|
@@ -357,85 +494,7 @@ var FieldEncryptor = class {
|
|
|
357
494
|
};
|
|
358
495
|
|
|
359
496
|
// src/persistence/complianceEventSubscriber.ts
|
|
360
|
-
var
|
|
361
|
-
var ENCRYPTED_LEVELS = /* @__PURE__ */ new Set(["pii", "phi", "pci"]);
|
|
362
|
-
var ComplianceEventSubscriber = class {
|
|
363
|
-
encryptor;
|
|
364
|
-
constructor(encryptor) {
|
|
365
|
-
this.encryptor = encryptor;
|
|
366
|
-
}
|
|
367
|
-
async beforeCreate(args) {
|
|
368
|
-
this.encryptFields(args);
|
|
369
|
-
}
|
|
370
|
-
async beforeUpdate(args) {
|
|
371
|
-
this.encryptFields(args);
|
|
372
|
-
}
|
|
373
|
-
async onLoad(args) {
|
|
374
|
-
this.decryptFields(args);
|
|
375
|
-
}
|
|
376
|
-
// ---------------------------------------------------------------------------
|
|
377
|
-
// Encrypt on persist
|
|
378
|
-
// ---------------------------------------------------------------------------
|
|
379
|
-
encryptFields(args) {
|
|
380
|
-
const entityName = args.meta.className;
|
|
381
|
-
const complianceFields = getEntityComplianceFields(entityName);
|
|
382
|
-
if (!complianceFields) return;
|
|
383
|
-
const tenantId = this.getTenantId(args.em);
|
|
384
|
-
const entity = args.entity;
|
|
385
|
-
for (const [fieldName, level] of complianceFields) {
|
|
386
|
-
if (!ENCRYPTED_LEVELS.has(level)) continue;
|
|
387
|
-
const value = entity[fieldName];
|
|
388
|
-
if (value === null || value === void 0) continue;
|
|
389
|
-
if (typeof value !== "string") continue;
|
|
390
|
-
if (value.startsWith(ENCRYPTED_PREFIX)) continue;
|
|
391
|
-
entity[fieldName] = this.encryptor.encrypt(value, tenantId ?? "");
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
// ---------------------------------------------------------------------------
|
|
395
|
-
// Decrypt on load
|
|
396
|
-
// ---------------------------------------------------------------------------
|
|
397
|
-
decryptFields(args) {
|
|
398
|
-
const entityName = args.meta.className;
|
|
399
|
-
const complianceFields = getEntityComplianceFields(entityName);
|
|
400
|
-
if (!complianceFields) return;
|
|
401
|
-
const tenantId = this.getTenantId(args.em);
|
|
402
|
-
const entity = args.entity;
|
|
403
|
-
for (const [fieldName, level] of complianceFields) {
|
|
404
|
-
if (!ENCRYPTED_LEVELS.has(level)) continue;
|
|
405
|
-
const value = entity[fieldName];
|
|
406
|
-
if (value === null || value === void 0) continue;
|
|
407
|
-
if (typeof value !== "string") continue;
|
|
408
|
-
if (value.startsWith(ENCRYPTED_PREFIX)) {
|
|
409
|
-
try {
|
|
410
|
-
entity[fieldName] = this.encryptor.decrypt(value, tenantId ?? "");
|
|
411
|
-
} catch (err) {
|
|
412
|
-
if (err instanceof DecryptionError) {
|
|
413
|
-
throw new DecryptionError(
|
|
414
|
-
`Failed to decrypt ${entityName}.${fieldName}: ${err.message}`
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
throw err;
|
|
418
|
-
}
|
|
419
|
-
} else {
|
|
420
|
-
console.warn(
|
|
421
|
-
`[compliance] ${entityName}.${fieldName} contains unencrypted ${level} data. Run encryption migration to encrypt existing data.`
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
// ---------------------------------------------------------------------------
|
|
427
|
-
// Tenant ID resolution
|
|
428
|
-
// ---------------------------------------------------------------------------
|
|
429
|
-
/**
|
|
430
|
-
* Read the tenant ID from the EntityManager's filter parameters.
|
|
431
|
-
* Returns undefined when no tenant context is set (startup, seeders,
|
|
432
|
-
* better-auth). Callers skip encryption/decryption in that case.
|
|
433
|
-
*/
|
|
434
|
-
getTenantId(em) {
|
|
435
|
-
const filters = em.getFilterParams("tenant");
|
|
436
|
-
return filters?.tenantId;
|
|
437
|
-
}
|
|
438
|
-
};
|
|
497
|
+
var ENCRYPTED_LEVELS2 = /* @__PURE__ */ new Set(["pii", "phi", "pci"]);
|
|
439
498
|
function wrapEmWithNativeQueryBlocking(em) {
|
|
440
499
|
const BLOCKED_METHODS = [
|
|
441
500
|
"nativeInsert",
|
|
@@ -451,7 +510,7 @@ function wrapEmWithNativeQueryBlocking(em) {
|
|
|
451
510
|
const fields = getEntityComplianceFields(entityName);
|
|
452
511
|
if (fields) {
|
|
453
512
|
for (const [fieldName, level] of fields) {
|
|
454
|
-
if (
|
|
513
|
+
if (ENCRYPTED_LEVELS2.has(level)) {
|
|
455
514
|
throw new EncryptionRequiredError(
|
|
456
515
|
`${prop}() blocked on entity '${entityName}' because field '${fieldName}' has compliance level '${level}'. Use em.create() + em.flush() instead to ensure encryption.`
|
|
457
516
|
);
|
|
@@ -624,9 +683,9 @@ function escapeSqlString(value) {
|
|
|
624
683
|
}
|
|
625
684
|
// Annotate the CommonJS export names for ESM import in node:
|
|
626
685
|
0 && (module.exports = {
|
|
627
|
-
ComplianceEventSubscriber,
|
|
628
686
|
ComplianceLevel,
|
|
629
687
|
DecryptionError,
|
|
688
|
+
EncryptedType,
|
|
630
689
|
EncryptionRequiredError,
|
|
631
690
|
FieldEncryptor,
|
|
632
691
|
MissingEncryptionKeyError,
|
|
@@ -641,14 +700,18 @@ function escapeSqlString(value) {
|
|
|
641
700
|
getAllRetentionPolicies,
|
|
642
701
|
getAllUserIdFields,
|
|
643
702
|
getComplianceMetadata,
|
|
703
|
+
getCurrentTenantId,
|
|
644
704
|
getEntityComplianceFields,
|
|
645
705
|
getEntityRetention,
|
|
646
706
|
getEntityUserIdField,
|
|
647
707
|
getSuperAdminContext,
|
|
648
708
|
parseDuration,
|
|
709
|
+
registerEncryptor,
|
|
710
|
+
setEncryptionTenantId,
|
|
649
711
|
setupRls,
|
|
650
712
|
setupTenantFilter,
|
|
651
713
|
subtractDuration,
|
|
714
|
+
withEncryptionContext,
|
|
652
715
|
wrapEmWithNativeQueryBlocking
|
|
653
716
|
});
|
|
654
717
|
//# sourceMappingURL=index.js.map
|