@forklaunch/core 1.0.1 → 1.0.2
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _mikro_orm_core from '@mikro-orm/core';
|
|
2
|
-
import { PropertyChain, PropertyBuilders, UniversalPropertyOptionsBuilder, EventSubscriber, EventArgs, EntityManager, FilterDef, MikroORM, TransactionEventArgs } from '@mikro-orm/core';
|
|
2
|
+
import { PropertyChain, PropertyBuilders, UniversalPropertyOptionsBuilder, EntityMetadataWithProperties, EventSubscriber, EventArgs, EntityManager, FilterDef, MikroORM, TransactionEventArgs } from '@mikro-orm/core';
|
|
3
3
|
export { InferEntity } from '@mikro-orm/core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -19,15 +19,21 @@ type ComplianceLevel = (typeof ComplianceLevel)[keyof typeof ComplianceLevel];
|
|
|
19
19
|
*/
|
|
20
20
|
declare const CLASSIFIED: unique symbol;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* At runtime this is a Proxy wrapping a MikroORM PropertyBuilder.
|
|
26
|
-
* The brand exists only at the type level for compile-time enforcement.
|
|
22
|
+
* Brand-only type used by `AssertAllClassified` to check that every
|
|
23
|
+
* property has been classified. Both `ClassifiedScalarProperty` and
|
|
24
|
+
* `ClassifiedRelationChain` extend this.
|
|
27
25
|
*/
|
|
28
26
|
interface ClassifiedProperty {
|
|
29
27
|
readonly [CLASSIFIED]: true;
|
|
30
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* A scalar property classified via `.compliance()`.
|
|
31
|
+
* Extends `PropertyChain<Value, Options>` so that `defineEntity` (and
|
|
32
|
+
* therefore `InferEntity`) can still read the value/options types for
|
|
33
|
+
* entity type inference. The `[CLASSIFIED]` brand prevents unclassified
|
|
34
|
+
* properties from being accepted by `defineComplianceEntity`.
|
|
35
|
+
*/
|
|
36
|
+
type ClassifiedScalarProperty<Value, Options> = PropertyChain<Value, Options> & ClassifiedProperty;
|
|
31
37
|
/**
|
|
32
38
|
* Look up the compliance level for a single field on an entity.
|
|
33
39
|
* Returns `'none'` if the entity or field is not registered.
|
|
@@ -52,9 +58,10 @@ interface ForklaunchPropertyChain<Value, Options> extends RemapReturns<Value, Op
|
|
|
52
58
|
/**
|
|
53
59
|
* Classify this field's compliance level. Must be called on every scalar
|
|
54
60
|
* field passed to `defineComplianceEntity`.
|
|
55
|
-
* Returns
|
|
61
|
+
* Returns a `ClassifiedScalarProperty` that preserves the PropertyChain
|
|
62
|
+
* type info for `InferEntity` to work.
|
|
56
63
|
*/
|
|
57
|
-
compliance(level: ComplianceLevel):
|
|
64
|
+
compliance(level: ComplianceLevel): ClassifiedScalarProperty<Value, Options>;
|
|
58
65
|
}
|
|
59
66
|
type RemapReturns<Value, Options> = {
|
|
60
67
|
[K in keyof PropertyChain<Value, Options>]: PropertyChain<Value, Options>[K] extends (...args: infer A) => PropertyChain<infer V2, infer O2> ? (...args: A) => ForklaunchPropertyChain<V2, O2> : PropertyChain<Value, Options>[K];
|
|
@@ -109,45 +116,12 @@ type ClassifiedRelationChain<Value, Options> = {
|
|
|
109
116
|
*/
|
|
110
117
|
declare const fp: ForklaunchPropertyBuilders;
|
|
111
118
|
|
|
112
|
-
/**
|
|
113
|
-
* Maps each property to `never` if it doesn't extend ClassifiedProperty.
|
|
114
|
-
* Used in an intersection with TProperties to produce a type error
|
|
115
|
-
* when any property is not classified.
|
|
116
|
-
*/
|
|
117
|
-
type AssertAllClassified<T extends Record<string, unknown>> = {
|
|
118
|
-
[K in keyof T]: T[K] extends ClassifiedProperty ? T[K] : T[K] extends () => ClassifiedProperty ? T[K] : ClassifiedProperty;
|
|
119
|
-
};
|
|
120
|
-
/**
|
|
121
|
-
* Metadata descriptor for `defineComplianceEntity`.
|
|
122
|
-
*/
|
|
123
|
-
interface ComplianceEntityMetadata<TProperties extends Record<string, unknown>> {
|
|
124
|
-
name: string;
|
|
125
|
-
tableName?: string;
|
|
126
|
-
properties: TProperties & AssertAllClassified<TProperties>;
|
|
127
|
-
extends?: unknown;
|
|
128
|
-
primaryKeys?: string[];
|
|
129
|
-
hooks?: Record<string, unknown>;
|
|
130
|
-
repository?: () => unknown;
|
|
131
|
-
forceObject?: boolean;
|
|
132
|
-
inheritance?: 'tpt';
|
|
133
|
-
orderBy?: Record<string, unknown> | Record<string, unknown>[];
|
|
134
|
-
discriminatorColumn?: string;
|
|
135
|
-
versionProperty?: string;
|
|
136
|
-
concurrencyCheckKeys?: Set<string>;
|
|
137
|
-
serializedPrimaryKey?: string;
|
|
138
|
-
indexes?: unknown[];
|
|
139
|
-
uniques?: unknown[];
|
|
140
|
-
}
|
|
141
119
|
/**
|
|
142
120
|
* Wrapper around MikroORM's `defineEntity` that enforces compliance
|
|
143
|
-
* classification on every field.
|
|
121
|
+
* classification on every field at both compile-time and runtime.
|
|
144
122
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* - Relation fields: auto-classified as `'none'` by the `fp` builder.
|
|
148
|
-
*
|
|
149
|
-
* Compliance metadata is stored in a module-level registry, accessible via
|
|
150
|
-
* `getComplianceMetadata(entityName, fieldName)`.
|
|
123
|
+
* The return type is inferred directly from `defineEntity` — `InferEntity`
|
|
124
|
+
* works because `ClassifiedScalarProperty<V,O>` extends `PropertyChain<V,O>`.
|
|
151
125
|
*
|
|
152
126
|
* @example
|
|
153
127
|
* ```typescript
|
|
@@ -156,14 +130,13 @@ interface ComplianceEntityMetadata<TProperties extends Record<string, unknown>>
|
|
|
156
130
|
* properties: {
|
|
157
131
|
* id: fp.uuid().primary().compliance('none'),
|
|
158
132
|
* email: fp.string().unique().compliance('pii'),
|
|
159
|
-
* medicalRecord: fp.string().nullable().compliance('phi'),
|
|
160
133
|
* organization: () => fp.manyToOne(Organization).nullable(),
|
|
161
134
|
* }
|
|
162
135
|
* });
|
|
163
136
|
* export type User = InferEntity<typeof User>;
|
|
164
137
|
* ```
|
|
165
138
|
*/
|
|
166
|
-
declare function defineComplianceEntity<TProperties extends Record<string,
|
|
139
|
+
declare function defineComplianceEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, ClassifiedProperty>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject>): _mikro_orm_core.EntitySchemaWithMeta<TName, TTableName, _mikro_orm_core.InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties, _mikro_orm_core.EntityCtor<_mikro_orm_core.InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>>>;
|
|
167
140
|
|
|
168
141
|
declare class FieldEncryptor {
|
|
169
142
|
private readonly masterKey;
|
|
@@ -296,4 +269,4 @@ declare class RlsEventSubscriber implements EventSubscriber {
|
|
|
296
269
|
*/
|
|
297
270
|
declare function setupRls(orm: MikroORM, config?: RlsConfig): void;
|
|
298
271
|
|
|
299
|
-
export { type ClassifiedProperty, type ClassifiedRelationChain, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ForklaunchPropertyBuilders, type ForklaunchPropertyChain, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getComplianceMetadata, getEntityComplianceFields, getSuperAdminContext, setupRls, setupTenantFilter, wrapEmWithNativeQueryBlocking };
|
|
272
|
+
export { type ClassifiedProperty, type ClassifiedRelationChain, type ClassifiedScalarProperty, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ForklaunchPropertyBuilders, type ForklaunchPropertyChain, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getComplianceMetadata, getEntityComplianceFields, getSuperAdminContext, setupRls, setupTenantFilter, wrapEmWithNativeQueryBlocking };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _mikro_orm_core from '@mikro-orm/core';
|
|
2
|
-
import { PropertyChain, PropertyBuilders, UniversalPropertyOptionsBuilder, EventSubscriber, EventArgs, EntityManager, FilterDef, MikroORM, TransactionEventArgs } from '@mikro-orm/core';
|
|
2
|
+
import { PropertyChain, PropertyBuilders, UniversalPropertyOptionsBuilder, EntityMetadataWithProperties, EventSubscriber, EventArgs, EntityManager, FilterDef, MikroORM, TransactionEventArgs } from '@mikro-orm/core';
|
|
3
3
|
export { InferEntity } from '@mikro-orm/core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -19,15 +19,21 @@ type ComplianceLevel = (typeof ComplianceLevel)[keyof typeof ComplianceLevel];
|
|
|
19
19
|
*/
|
|
20
20
|
declare const CLASSIFIED: unique symbol;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* At runtime this is a Proxy wrapping a MikroORM PropertyBuilder.
|
|
26
|
-
* The brand exists only at the type level for compile-time enforcement.
|
|
22
|
+
* Brand-only type used by `AssertAllClassified` to check that every
|
|
23
|
+
* property has been classified. Both `ClassifiedScalarProperty` and
|
|
24
|
+
* `ClassifiedRelationChain` extend this.
|
|
27
25
|
*/
|
|
28
26
|
interface ClassifiedProperty {
|
|
29
27
|
readonly [CLASSIFIED]: true;
|
|
30
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* A scalar property classified via `.compliance()`.
|
|
31
|
+
* Extends `PropertyChain<Value, Options>` so that `defineEntity` (and
|
|
32
|
+
* therefore `InferEntity`) can still read the value/options types for
|
|
33
|
+
* entity type inference. The `[CLASSIFIED]` brand prevents unclassified
|
|
34
|
+
* properties from being accepted by `defineComplianceEntity`.
|
|
35
|
+
*/
|
|
36
|
+
type ClassifiedScalarProperty<Value, Options> = PropertyChain<Value, Options> & ClassifiedProperty;
|
|
31
37
|
/**
|
|
32
38
|
* Look up the compliance level for a single field on an entity.
|
|
33
39
|
* Returns `'none'` if the entity or field is not registered.
|
|
@@ -52,9 +58,10 @@ interface ForklaunchPropertyChain<Value, Options> extends RemapReturns<Value, Op
|
|
|
52
58
|
/**
|
|
53
59
|
* Classify this field's compliance level. Must be called on every scalar
|
|
54
60
|
* field passed to `defineComplianceEntity`.
|
|
55
|
-
* Returns
|
|
61
|
+
* Returns a `ClassifiedScalarProperty` that preserves the PropertyChain
|
|
62
|
+
* type info for `InferEntity` to work.
|
|
56
63
|
*/
|
|
57
|
-
compliance(level: ComplianceLevel):
|
|
64
|
+
compliance(level: ComplianceLevel): ClassifiedScalarProperty<Value, Options>;
|
|
58
65
|
}
|
|
59
66
|
type RemapReturns<Value, Options> = {
|
|
60
67
|
[K in keyof PropertyChain<Value, Options>]: PropertyChain<Value, Options>[K] extends (...args: infer A) => PropertyChain<infer V2, infer O2> ? (...args: A) => ForklaunchPropertyChain<V2, O2> : PropertyChain<Value, Options>[K];
|
|
@@ -109,45 +116,12 @@ type ClassifiedRelationChain<Value, Options> = {
|
|
|
109
116
|
*/
|
|
110
117
|
declare const fp: ForklaunchPropertyBuilders;
|
|
111
118
|
|
|
112
|
-
/**
|
|
113
|
-
* Maps each property to `never` if it doesn't extend ClassifiedProperty.
|
|
114
|
-
* Used in an intersection with TProperties to produce a type error
|
|
115
|
-
* when any property is not classified.
|
|
116
|
-
*/
|
|
117
|
-
type AssertAllClassified<T extends Record<string, unknown>> = {
|
|
118
|
-
[K in keyof T]: T[K] extends ClassifiedProperty ? T[K] : T[K] extends () => ClassifiedProperty ? T[K] : ClassifiedProperty;
|
|
119
|
-
};
|
|
120
|
-
/**
|
|
121
|
-
* Metadata descriptor for `defineComplianceEntity`.
|
|
122
|
-
*/
|
|
123
|
-
interface ComplianceEntityMetadata<TProperties extends Record<string, unknown>> {
|
|
124
|
-
name: string;
|
|
125
|
-
tableName?: string;
|
|
126
|
-
properties: TProperties & AssertAllClassified<TProperties>;
|
|
127
|
-
extends?: unknown;
|
|
128
|
-
primaryKeys?: string[];
|
|
129
|
-
hooks?: Record<string, unknown>;
|
|
130
|
-
repository?: () => unknown;
|
|
131
|
-
forceObject?: boolean;
|
|
132
|
-
inheritance?: 'tpt';
|
|
133
|
-
orderBy?: Record<string, unknown> | Record<string, unknown>[];
|
|
134
|
-
discriminatorColumn?: string;
|
|
135
|
-
versionProperty?: string;
|
|
136
|
-
concurrencyCheckKeys?: Set<string>;
|
|
137
|
-
serializedPrimaryKey?: string;
|
|
138
|
-
indexes?: unknown[];
|
|
139
|
-
uniques?: unknown[];
|
|
140
|
-
}
|
|
141
119
|
/**
|
|
142
120
|
* Wrapper around MikroORM's `defineEntity` that enforces compliance
|
|
143
|
-
* classification on every field.
|
|
121
|
+
* classification on every field at both compile-time and runtime.
|
|
144
122
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* - Relation fields: auto-classified as `'none'` by the `fp` builder.
|
|
148
|
-
*
|
|
149
|
-
* Compliance metadata is stored in a module-level registry, accessible via
|
|
150
|
-
* `getComplianceMetadata(entityName, fieldName)`.
|
|
123
|
+
* The return type is inferred directly from `defineEntity` — `InferEntity`
|
|
124
|
+
* works because `ClassifiedScalarProperty<V,O>` extends `PropertyChain<V,O>`.
|
|
151
125
|
*
|
|
152
126
|
* @example
|
|
153
127
|
* ```typescript
|
|
@@ -156,14 +130,13 @@ interface ComplianceEntityMetadata<TProperties extends Record<string, unknown>>
|
|
|
156
130
|
* properties: {
|
|
157
131
|
* id: fp.uuid().primary().compliance('none'),
|
|
158
132
|
* email: fp.string().unique().compliance('pii'),
|
|
159
|
-
* medicalRecord: fp.string().nullable().compliance('phi'),
|
|
160
133
|
* organization: () => fp.manyToOne(Organization).nullable(),
|
|
161
134
|
* }
|
|
162
135
|
* });
|
|
163
136
|
* export type User = InferEntity<typeof User>;
|
|
164
137
|
* ```
|
|
165
138
|
*/
|
|
166
|
-
declare function defineComplianceEntity<TProperties extends Record<string,
|
|
139
|
+
declare function defineComplianceEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, ClassifiedProperty>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject>): _mikro_orm_core.EntitySchemaWithMeta<TName, TTableName, _mikro_orm_core.InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties, _mikro_orm_core.EntityCtor<_mikro_orm_core.InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>>>;
|
|
167
140
|
|
|
168
141
|
declare class FieldEncryptor {
|
|
169
142
|
private readonly masterKey;
|
|
@@ -296,4 +269,4 @@ declare class RlsEventSubscriber implements EventSubscriber {
|
|
|
296
269
|
*/
|
|
297
270
|
declare function setupRls(orm: MikroORM, config?: RlsConfig): void;
|
|
298
271
|
|
|
299
|
-
export { type ClassifiedProperty, type ClassifiedRelationChain, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ForklaunchPropertyBuilders, type ForklaunchPropertyChain, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getComplianceMetadata, getEntityComplianceFields, getSuperAdminContext, setupRls, setupTenantFilter, wrapEmWithNativeQueryBlocking };
|
|
272
|
+
export { type ClassifiedProperty, type ClassifiedRelationChain, type ClassifiedScalarProperty, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ForklaunchPropertyBuilders, type ForklaunchPropertyChain, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getComplianceMetadata, getEntityComplianceFields, getSuperAdminContext, setupRls, setupTenantFilter, wrapEmWithNativeQueryBlocking };
|
package/lib/persistence/index.js
CHANGED
|
@@ -153,8 +153,10 @@ function readComplianceLevel(builder) {
|
|
|
153
153
|
return builder[COMPLIANCE_KEY];
|
|
154
154
|
}
|
|
155
155
|
function defineComplianceEntity(meta) {
|
|
156
|
-
const
|
|
156
|
+
const entityName = meta.name;
|
|
157
157
|
const complianceFields = /* @__PURE__ */ new Map();
|
|
158
|
+
const rawProperties = meta.properties;
|
|
159
|
+
const properties = typeof rawProperties === "function" ? rawProperties(import_core2.p) : rawProperties;
|
|
158
160
|
for (const [fieldName, rawProp] of Object.entries(properties)) {
|
|
159
161
|
const prop = typeof rawProp === "function" ? rawProp() : rawProp;
|
|
160
162
|
const level = readComplianceLevel(prop);
|
|
@@ -166,11 +168,7 @@ function defineComplianceEntity(meta) {
|
|
|
166
168
|
complianceFields.set(fieldName, level);
|
|
167
169
|
}
|
|
168
170
|
registerEntityCompliance(entityName, complianceFields);
|
|
169
|
-
return (0, import_core2.defineEntity)(
|
|
170
|
-
name: entityName,
|
|
171
|
-
properties,
|
|
172
|
-
...rest
|
|
173
|
-
});
|
|
171
|
+
return (0, import_core2.defineEntity)(meta);
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
// src/encryption/fieldEncryptor.ts
|
|
@@ -421,7 +419,7 @@ async function validateRlsPolicies(orm) {
|
|
|
421
419
|
);
|
|
422
420
|
const policies = Array.isArray(result) ? result : [];
|
|
423
421
|
const hasTenantPolicy = policies.some(
|
|
424
|
-
(
|
|
422
|
+
(p3) => p3.policyname.includes("tenant")
|
|
425
423
|
);
|
|
426
424
|
if (!hasTenantPolicy) {
|
|
427
425
|
console.warn(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/persistence/index.ts","../../src/persistence/complianceTypes.ts","../../src/persistence/compliancePropertyBuilder.ts","../../src/persistence/defineComplianceEntity.ts","../../src/encryption/fieldEncryptor.ts","../../src/persistence/complianceEventSubscriber.ts","../../src/persistence/tenantFilter.ts","../../src/persistence/rls.ts"],"sourcesContent":["// Compliance types and registry\nexport {\n ComplianceLevel,\n type ComplianceLevel as ComplianceLevelType,\n type ClassifiedProperty,\n type ClassifiedRelationChain,\n type ForklaunchPropertyBuilders,\n type ForklaunchPropertyChain,\n getComplianceMetadata,\n getEntityComplianceFields,\n entityHasEncryptedFields\n} from './complianceTypes';\n\n// Compliance-aware property builder (drop-in replacement for MikroORM's p)\nexport { fp } from './compliancePropertyBuilder';\n\n// Compliance-aware entity definition (drop-in replacement for MikroORM's defineEntity)\nexport { defineComplianceEntity } from './defineComplianceEntity';\n\n// Compliance EventSubscriber (encrypt on persist, decrypt on load)\nexport {\n ComplianceEventSubscriber,\n wrapEmWithNativeQueryBlocking\n} from './complianceEventSubscriber';\n\n// Re-export InferEntity from MikroORM for convenience\nexport type { InferEntity } from '@mikro-orm/core';\n\n// Tenant isolation filter\nexport {\n setupTenantFilter,\n getSuperAdminContext,\n createTenantFilterDef,\n TENANT_FILTER_NAME\n} from './tenantFilter';\n\n// PostgreSQL Row-Level Security\nexport { setupRls, RlsEventSubscriber, type RlsConfig } from './rls';\n","import type {\n PropertyBuilders,\n PropertyChain,\n UniversalPropertyOptionsBuilder\n} from '@mikro-orm/core';\n\n/**\n * Classification levels for entity field compliance.\n * Drives encryption (phi/pci), audit log redaction (all non-none), and compliance reporting.\n */\nexport const ComplianceLevel = {\n pii: 'pii',\n phi: 'phi',\n pci: 'pci',\n none: 'none'\n} as const;\nexport type ComplianceLevel =\n (typeof ComplianceLevel)[keyof typeof ComplianceLevel];\n\n/**\n * Brand symbol — makes ClassifiedProperty structurally distinct from\n * plain PropertyChain at the TypeScript level.\n */\ndeclare const CLASSIFIED: unique symbol;\n\n/**\n * A property that has been classified via `.compliance()`.\n * Only ClassifiedProperty values are accepted by `defineComplianceEntity`.\n *\n * At runtime this is a Proxy wrapping a MikroORM PropertyBuilder.\n * The brand exists only at the type level for compile-time enforcement.\n */\nexport interface ClassifiedProperty {\n readonly [CLASSIFIED]: true;\n}\n\n/**\n * Internal key used by the runtime Proxy to store compliance level\n * on the builder instance. Not part of the public API.\n */\nexport const COMPLIANCE_KEY = '~compliance' as const;\n\n// ---------------------------------------------------------------------------\n// Compliance metadata registry\n// ---------------------------------------------------------------------------\n\n/** entityName → (fieldName → ComplianceLevel) */\nconst complianceRegistry = new Map<string, Map<string, ComplianceLevel>>();\n\n/**\n * Register compliance metadata for an entity's fields.\n * Called by `defineComplianceEntity` during entity definition.\n */\nexport function registerEntityCompliance(\n entityName: string,\n fields: Map<string, ComplianceLevel>\n): void {\n complianceRegistry.set(entityName, fields);\n}\n\n/**\n * Look up the compliance level for a single field on an entity.\n * Returns `'none'` if the entity or field is not registered.\n */\nexport function getComplianceMetadata(\n entityName: string,\n fieldName: string\n): ComplianceLevel {\n return complianceRegistry.get(entityName)?.get(fieldName) ?? 'none';\n}\n\n/**\n * Get all compliance fields for an entity.\n * Returns undefined if the entity is not registered.\n */\nexport function getEntityComplianceFields(\n entityName: string\n): Map<string, ComplianceLevel> | undefined {\n return complianceRegistry.get(entityName);\n}\n\n/**\n * Check whether an entity has any fields requiring encryption (phi or pci).\n */\nexport function entityHasEncryptedFields(entityName: string): boolean {\n const fields = complianceRegistry.get(entityName);\n if (!fields) return false;\n for (const level of fields.values()) {\n if (level === 'phi' || level === 'pci') return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyChain — remapped PropertyChain that preserves\n// `.compliance()` through method chaining.\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively remaps every method on PropertyChain<V,O> so those returning\n * PropertyChain<V2,O2> instead return ForklaunchPropertyChain<V2,O2>.\n * This preserves the `.compliance()` method through chained calls like\n * `.nullable().unique()`.\n */\n\nexport interface ForklaunchPropertyChain<Value, Options> extends RemapReturns<\n Value,\n Options\n> {\n /**\n * Classify this field's compliance level. Must be called on every scalar\n * field passed to `defineComplianceEntity`.\n * Returns an opaque `ClassifiedProperty`.\n */\n compliance(level: ComplianceLevel): ClassifiedProperty;\n}\n\ntype RemapReturns<Value, Options> = {\n [K in keyof PropertyChain<Value, Options>]: PropertyChain<\n Value,\n Options\n >[K] extends (...args: infer A) => PropertyChain<infer V2, infer O2>\n ? (...args: A) => ForklaunchPropertyChain<V2, O2>\n : PropertyChain<Value, Options>[K];\n};\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyBuilders — the type of `fp`\n// ---------------------------------------------------------------------------\n\n/**\n * Keys on PropertyBuilders that return relation builders.\n * These are auto-classified as 'none' — the fp proxy wraps them\n * to return ClassifiedProperty directly.\n */\ntype RelationBuilderKeys =\n | 'manyToOne'\n | 'oneToMany'\n | 'manyToMany'\n | 'oneToOne'\n | 'embedded';\n\n/**\n * The type of `fp` — mirrors `PropertyBuilders` but:\n * - Scalar methods return `ForklaunchPropertyChain` (must call `.compliance()`)\n * - Relation methods return `ClassifiedProperty` directly (auto 'none')\n */\nexport type ForklaunchPropertyBuilders = {\n [K in Exclude<\n keyof PropertyBuilders,\n RelationBuilderKeys\n >]: PropertyBuilders[K] extends (\n ...args: infer A\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n ) => UniversalPropertyOptionsBuilder<infer V, infer O, infer _IK>\n ? (...args: A) => ForklaunchPropertyChain<V, O>\n : PropertyBuilders[K] extends (\n ...args: infer A\n ) => PropertyChain<infer V, infer O>\n ? (...args: A) => ForklaunchPropertyChain<V, O>\n : PropertyBuilders[K];\n} & {\n [K in RelationBuilderKeys]: PropertyBuilders[K] extends (\n ...args: infer A\n ) => PropertyChain<infer V, infer O>\n ? (...args: A) => ClassifiedRelationChain<V, O>\n : PropertyBuilders[K];\n};\n\n/**\n * A relation builder that is already classified (as 'none') but still\n * supports chaining relation-specific methods like `.mappedBy()`, `.nullable()`.\n * All chain methods return ClassifiedRelationChain (preserving the brand).\n */\nexport type ClassifiedRelationChain<Value, Options> = {\n [K in keyof PropertyChain<Value, Options>]: PropertyChain<\n Value,\n Options\n >[K] extends (...args: infer A) => PropertyChain<infer V2, infer O2>\n ? (...args: A) => ClassifiedRelationChain<V2, O2>\n : PropertyChain<Value, Options>[K];\n} & ClassifiedProperty;\n","import { p } from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n type ComplianceLevel,\n type ForklaunchPropertyBuilders\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Runtime Proxy implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a value is a MikroORM property builder (has ~options).\n */\nfunction isBuilder(value: unknown): value is object {\n return (\n value != null &&\n typeof value === 'object' &&\n '~options' in (value as Record<string, unknown>)\n );\n}\n\n/**\n * Wraps a MikroORM scalar PropertyBuilder in a Proxy that:\n * 1. Adds a `.compliance(level)` method\n * 2. Forwards all other method calls to the underlying builder\n * 3. Re-wraps returned builders so `.compliance()` persists through chains\n */\nfunction wrapUnclassified(builder: unknown): unknown {\n return new Proxy(builder as object, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === 'compliance') {\n return (level: ComplianceLevel) => wrapClassified(target, level);\n }\n if (prop === '~options') return Reflect.get(target, prop, target);\n if (prop === COMPLIANCE_KEY) return undefined;\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a builder that has been classified via `.compliance()`.\n * Stores the compliance level under `~compliance` for `defineComplianceEntity`.\n * Chaining after `.compliance()` propagates the level through subsequent builders.\n */\nfunction wrapClassified(builder: object, level: ComplianceLevel): unknown {\n return new Proxy(builder, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === COMPLIANCE_KEY) return level;\n if (prop === '~options') return Reflect.get(target, prop, target);\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapClassified(result, level) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a relation PropertyBuilder (manyToOne, oneToMany, etc.).\n * Auto-classified as 'none' — no `.compliance()` call needed.\n * All chained methods preserve the auto-classification.\n */\nfunction wrapRelation(builder: object): unknown {\n return wrapClassified(builder, 'none');\n}\n\n// ---------------------------------------------------------------------------\n// Relation method detection\n// ---------------------------------------------------------------------------\n\nconst RELATION_METHODS = new Set([\n 'manyToOne',\n 'oneToMany',\n 'manyToMany',\n 'oneToOne',\n 'embedded'\n]);\n\nfunction isRelationMethod(prop: string | symbol): boolean {\n return typeof prop === 'string' && RELATION_METHODS.has(prop);\n}\n\n// ---------------------------------------------------------------------------\n// fp — the ForkLaunch property builder\n// ---------------------------------------------------------------------------\n\n/**\n * ForkLaunch property builder. Drop-in replacement for MikroORM's `p`\n * that adds `.compliance(level)` to every scalar property builder\n * and auto-classifies relation builders as 'none'.\n *\n * - Scalar fields: `fp.string().compliance('pii')` — must call `.compliance()`\n * - Relation fields: `fp.manyToOne(Target)` — auto-classified, no `.compliance()` needed\n *\n * @example\n * ```typescript\n * import { defineComplianceEntity, fp } from '@forklaunch/core/persistence';\n *\n * const User = defineComplianceEntity({\n * name: 'User',\n * properties: {\n * id: fp.uuid().primary().compliance('none'),\n * email: fp.string().unique().compliance('pii'),\n * medicalRecord: fp.string().nullable().compliance('phi'),\n * organization: () => fp.manyToOne(Organization).nullable(),\n * }\n * });\n * ```\n */\nexport const fp: ForklaunchPropertyBuilders = new Proxy(p, {\n get(target: Record<string | symbol, unknown>, prop) {\n const value = Reflect.get(target, prop, target);\n if (typeof value !== 'function') return value;\n\n if (isRelationMethod(prop)) {\n // Relation methods: call the original, wrap result as auto-classified 'none'\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapRelation(result) : result;\n };\n }\n\n // Scalar methods: call the original, wrap result with .compliance()\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n}) as ForklaunchPropertyBuilders;\n","import { defineEntity } from '@mikro-orm/core';\nimport type { InferEntity } from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n type ClassifiedProperty,\n type ComplianceLevel,\n registerEntityCompliance\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Maps each property to `never` if it doesn't extend ClassifiedProperty.\n * Used in an intersection with TProperties to produce a type error\n * when any property is not classified.\n */\ntype AssertAllClassified<T extends Record<string, unknown>> = {\n [K in keyof T]: T[K] extends ClassifiedProperty\n ? T[K]\n : T[K] extends () => ClassifiedProperty\n ? T[K]\n : ClassifiedProperty; // Force error: value not assignable to ClassifiedProperty\n};\n\n/**\n * Metadata descriptor for `defineComplianceEntity`.\n */\ninterface ComplianceEntityMetadata<\n TProperties extends Record<string, unknown>\n> {\n name: string;\n tableName?: string;\n properties: TProperties & AssertAllClassified<TProperties>;\n extends?: unknown;\n primaryKeys?: string[];\n hooks?: Record<string, unknown>;\n repository?: () => unknown;\n forceObject?: boolean;\n inheritance?: 'tpt';\n orderBy?: Record<string, unknown> | Record<string, unknown>[];\n discriminatorColumn?: string;\n versionProperty?: string;\n concurrencyCheckKeys?: Set<string>;\n serializedPrimaryKey?: string;\n indexes?: unknown[];\n uniques?: unknown[];\n}\n\n// ---------------------------------------------------------------------------\n// Runtime helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Read the compliance level from a proxy-wrapped builder.\n * Returns the level if the proxy has `~compliance`, undefined otherwise.\n */\nfunction readComplianceLevel(builder: unknown): ComplianceLevel | undefined {\n if (builder == null || typeof builder !== 'object') return undefined;\n return (builder as Record<string, unknown>)[COMPLIANCE_KEY] as\n | ComplianceLevel\n | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// defineComplianceEntity\n// ---------------------------------------------------------------------------\n\n/**\n * Wrapper around MikroORM's `defineEntity` that enforces compliance\n * classification on every field.\n *\n * - Scalar fields: must call `.compliance(level)` — forgetting it is a\n * compile-time error (TypeScript rejects it) AND a runtime error.\n * - Relation fields: auto-classified as `'none'` by the `fp` builder.\n *\n * Compliance metadata is stored in a module-level registry, accessible via\n * `getComplianceMetadata(entityName, fieldName)`.\n *\n * @example\n * ```typescript\n * const User = defineComplianceEntity({\n * name: 'User',\n * properties: {\n * id: fp.uuid().primary().compliance('none'),\n * email: fp.string().unique().compliance('pii'),\n * medicalRecord: fp.string().nullable().compliance('phi'),\n * organization: () => fp.manyToOne(Organization).nullable(),\n * }\n * });\n * export type User = InferEntity<typeof User>;\n * ```\n */\nexport function defineComplianceEntity<\n TProperties extends Record<string, unknown>\n>(meta: ComplianceEntityMetadata<TProperties>) {\n const { name: entityName, properties, ...rest } = meta;\n const complianceFields = new Map<string, ComplianceLevel>();\n\n // Validate and extract compliance from each property\n for (const [fieldName, rawProp] of Object.entries(properties)) {\n const prop = typeof rawProp === 'function' ? rawProp() : rawProp;\n const level = readComplianceLevel(prop);\n\n if (level == null) {\n throw new Error(\n `Field '${entityName}.${fieldName}' is missing compliance classification. ` +\n `Call .compliance('pii' | 'phi' | 'pci' | 'none') on this property, ` +\n `or use a relation method (fp.manyToOne, etc.) which is auto-classified.`\n );\n }\n complianceFields.set(fieldName, level);\n }\n\n // Store compliance metadata in the global registry\n registerEntityCompliance(entityName, complianceFields);\n\n // Delegate to MikroORM's defineEntity.\n // The Proxy-wrapped builders forward ~options correctly.\n return defineEntity({\n name: entityName,\n properties: properties as Record<string, unknown>,\n ...rest\n } as Parameters<typeof defineEntity>[0]);\n}\n\n// Re-export InferEntity for convenience\nexport type { InferEntity };\n","import crypto from 'crypto';\n\n// ---------------------------------------------------------------------------\n// Error types\n// ---------------------------------------------------------------------------\n\nexport class MissingEncryptionKeyError extends Error {\n readonly name = 'MissingEncryptionKeyError' as const;\n constructor(message = 'Master encryption key must be provided') {\n super(message);\n }\n}\n\nexport class DecryptionError extends Error {\n readonly name = 'DecryptionError' as const;\n constructor(\n message = 'Decryption failed: ciphertext is corrupted or the wrong key was used'\n ) {\n super(message);\n }\n}\n\nexport class EncryptionRequiredError extends Error {\n readonly name = 'EncryptionRequiredError' as const;\n constructor(\n message = 'Encryption is required before persisting this compliance field'\n ) {\n super(message);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst ALGORITHM = 'aes-256-gcm' as const;\nconst IV_BYTES = 12;\nconst KEY_BYTES = 32;\nconst HKDF_HASH = 'sha256' as const;\nconst HKDF_SALT = Buffer.alloc(0); // empty salt – key material is already high-entropy\n\n// ---------------------------------------------------------------------------\n// FieldEncryptor\n// ---------------------------------------------------------------------------\n\nexport class FieldEncryptor {\n private readonly masterKey: string;\n\n constructor(masterKey: string) {\n if (!masterKey) {\n throw new MissingEncryptionKeyError();\n }\n this.masterKey = masterKey;\n }\n\n /**\n * Derive a per-tenant 32-byte key using HKDF-SHA256.\n * The master key is used as input key material and the tenantId as info context.\n */\n deriveKey(tenantId: string): Buffer {\n return Buffer.from(\n crypto.hkdfSync(HKDF_HASH, this.masterKey, HKDF_SALT, tenantId, KEY_BYTES)\n );\n }\n\n /**\n * Encrypt a plaintext string for a specific tenant.\n *\n * @returns Format: `v1:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`\n */\n encrypt(plaintext: string | null): string | null;\n encrypt(plaintext: string | null, tenantId: string): string | null;\n encrypt(plaintext: string | null, tenantId?: string): string | null {\n if (plaintext === null || plaintext === undefined) {\n return null;\n }\n\n const key = this.deriveKey(tenantId ?? '');\n const iv = crypto.randomBytes(IV_BYTES);\n\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final()\n ]);\n const authTag = cipher.getAuthTag();\n\n return [\n 'v1',\n iv.toString('base64'),\n authTag.toString('base64'),\n encrypted.toString('base64')\n ].join(':');\n }\n\n /**\n * Decrypt a ciphertext string produced by {@link encrypt}.\n */\n decrypt(ciphertext: string | null): string | null;\n decrypt(ciphertext: string | null, tenantId: string): string | null;\n decrypt(ciphertext: string | null, tenantId?: string): string | null {\n if (ciphertext === null || ciphertext === undefined) {\n return null;\n }\n\n const parts = ciphertext.split(':');\n if (parts.length !== 4 || parts[0] !== 'v1') {\n throw new DecryptionError(\n `Unknown ciphertext version or malformed format`\n );\n }\n\n const [, ivB64, authTagB64, encryptedB64] = parts;\n const iv = Buffer.from(ivB64, 'base64');\n const authTag = Buffer.from(authTagB64, 'base64');\n const encrypted = Buffer.from(encryptedB64, 'base64');\n const key = this.deriveKey(tenantId ?? '');\n\n try {\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final()\n ]);\n return decrypted.toString('utf8');\n } catch {\n throw new DecryptionError();\n }\n }\n}\n","import type {\n EntityManager,\n EventArgs,\n EventSubscriber\n} from '@mikro-orm/core';\nimport {\n DecryptionError,\n EncryptionRequiredError,\n FieldEncryptor\n} from '../encryption/fieldEncryptor';\nimport {\n type ComplianceLevel,\n getEntityComplianceFields\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst ENCRYPTED_PREFIX = 'v1:';\n\n/**\n * Compliance levels that require field-level encryption.\n * PII is NOT encrypted (RDS encryption + TLS sufficient).\n */\nconst ENCRYPTED_LEVELS: ReadonlySet<ComplianceLevel> = new Set(['phi', 'pci']);\n\n// ---------------------------------------------------------------------------\n// ComplianceEventSubscriber\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM EventSubscriber that enforces field-level encryption for\n * compliance-classified fields (PHI and PCI).\n *\n * - **onBeforeCreate / onBeforeUpdate**: Encrypts PHI/PCI fields before\n * database persistence. Throws `EncryptionRequiredError` if the encryption\n * key is unavailable.\n * - **onLoad**: Decrypts PHI/PCI fields after loading from the database.\n * Pre-migration plaintext (no `v1:` prefix) is returned as-is with a\n * console warning to support rolling deployments.\n *\n * The tenant ID for key derivation is read from the EntityManager's filter\n * parameters (set by the tenant context middleware).\n */\nexport class ComplianceEventSubscriber implements EventSubscriber {\n private readonly encryptor: FieldEncryptor;\n\n constructor(encryptor: FieldEncryptor) {\n this.encryptor = encryptor;\n }\n\n async beforeCreate(args: EventArgs<unknown>): Promise<void> {\n this.encryptFields(args);\n }\n\n async beforeUpdate(args: EventArgs<unknown>): Promise<void> {\n this.encryptFields(args);\n }\n\n async onLoad(args: EventArgs<unknown>): Promise<void> {\n this.decryptFields(args);\n }\n\n // ---------------------------------------------------------------------------\n // Encrypt on persist\n // ---------------------------------------------------------------------------\n\n private encryptFields(args: EventArgs<unknown>): void {\n const entityName = args.meta.className;\n const complianceFields = getEntityComplianceFields(entityName);\n if (!complianceFields) return;\n\n const tenantId = this.getTenantId(args.em);\n const entity = args.entity as Record<string, unknown>;\n\n for (const [fieldName, level] of complianceFields) {\n if (!ENCRYPTED_LEVELS.has(level)) continue;\n\n const value = entity[fieldName];\n if (value === null || value === undefined) continue;\n if (typeof value !== 'string') continue;\n\n // Don't double-encrypt\n if (value.startsWith(ENCRYPTED_PREFIX)) continue;\n\n entity[fieldName] = this.encryptor.encrypt(value, tenantId);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Decrypt on load\n // ---------------------------------------------------------------------------\n\n private decryptFields(args: EventArgs<unknown>): void {\n const entityName = args.meta.className;\n const complianceFields = getEntityComplianceFields(entityName);\n if (!complianceFields) return;\n\n const tenantId = this.getTenantId(args.em);\n const entity = args.entity as Record<string, unknown>;\n\n for (const [fieldName, level] of complianceFields) {\n if (!ENCRYPTED_LEVELS.has(level)) continue;\n\n const value = entity[fieldName];\n if (value === null || value === undefined) continue;\n if (typeof value !== 'string') continue;\n\n if (value.startsWith(ENCRYPTED_PREFIX)) {\n // Encrypted — decrypt it\n try {\n entity[fieldName] = this.encryptor.decrypt(value, tenantId);\n } catch (err) {\n if (err instanceof DecryptionError) {\n throw new DecryptionError(\n `Failed to decrypt ${entityName}.${fieldName}: ${err.message}`\n );\n }\n throw err;\n }\n } else {\n // Pre-migration plaintext — return as-is, log warning\n console.warn(\n `[compliance] ${entityName}.${fieldName} contains unencrypted ${level} data. ` +\n `Run encryption migration to encrypt existing data.`\n );\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Tenant ID resolution\n // ---------------------------------------------------------------------------\n\n /**\n * Read the tenant ID from the EntityManager's filter parameters.\n * The tenant context middleware sets this when forking the EM per request.\n */\n private getTenantId(em: EntityManager): string {\n const filters = em.getFilterParams('tenant') as\n | { tenantId?: string }\n | undefined;\n const tenantId = filters?.tenantId;\n if (!tenantId) {\n throw new EncryptionRequiredError(\n 'Cannot encrypt/decrypt without tenant context. ' +\n 'Ensure the tenant filter is set on the EntityManager.'\n );\n }\n return tenantId;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Native query blocking\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and\n * `nativeDelete` on entities that have PHI or PCI compliance fields.\n *\n * This prevents bypassing the ComplianceEventSubscriber's encryption\n * by using raw queries. Call this in the tenant context middleware when\n * creating the request-scoped EM.\n *\n * @returns A Proxy-wrapped EntityManager that throws on native query\n * operations targeting compliance entities.\n */\nexport function wrapEmWithNativeQueryBlocking<T extends EntityManager>(\n em: T\n): T {\n const BLOCKED_METHODS = [\n 'nativeInsert',\n 'nativeUpdate',\n 'nativeDelete'\n ] as const;\n\n return new Proxy(em, {\n get(target, prop, receiver) {\n if (\n typeof prop === 'string' &&\n BLOCKED_METHODS.includes(prop as (typeof BLOCKED_METHODS)[number])\n ) {\n return (entityNameOrEntity: unknown, ...rest: unknown[]) => {\n const entityName = resolveEntityName(entityNameOrEntity);\n if (entityName) {\n const fields = getEntityComplianceFields(entityName);\n if (fields) {\n for (const [fieldName, level] of fields) {\n if (ENCRYPTED_LEVELS.has(level)) {\n throw new EncryptionRequiredError(\n `${prop}() blocked on entity '${entityName}' because field ` +\n `'${fieldName}' has compliance level '${level}'. ` +\n `Use em.create() + em.flush() instead to ensure encryption.`\n );\n }\n }\n }\n }\n // No compliance fields requiring encryption — allow the native query\n const method = Reflect.get(target, prop, receiver);\n return (method as (...args: unknown[]) => unknown).call(\n target,\n entityNameOrEntity,\n ...rest\n );\n };\n }\n return Reflect.get(target, prop, receiver);\n }\n });\n}\n\n/**\n * Resolve an entity name from the first argument to nativeInsert/Update/Delete.\n * MikroORM accepts entity name strings, entity class references, or entity instances.\n */\nfunction resolveEntityName(entityNameOrEntity: unknown): string | undefined {\n if (typeof entityNameOrEntity === 'string') {\n return entityNameOrEntity;\n }\n if (typeof entityNameOrEntity === 'function') {\n return (entityNameOrEntity as { name?: string }).name;\n }\n if (\n entityNameOrEntity != null &&\n typeof entityNameOrEntity === 'object' &&\n 'constructor' in entityNameOrEntity\n ) {\n return (entityNameOrEntity.constructor as { name?: string }).name;\n }\n return undefined;\n}\n","import type {\n Dictionary,\n EntityManager,\n FilterDef,\n MikroORM\n} from '@mikro-orm/core';\n\n/**\n * The name used to register the tenant isolation filter.\n */\nexport const TENANT_FILTER_NAME = 'tenant';\n\n/**\n * Creates the tenant filter definition.\n *\n * The filter adds `WHERE organizationId = :tenantId` to all queries\n * on entities that have an `organizationId` or `organization` property.\n * Entities without either property are unaffected (empty condition).\n */\nexport function createTenantFilterDef(): FilterDef {\n return {\n name: TENANT_FILTER_NAME,\n cond(\n args: Dictionary,\n _type: 'read' | 'update' | 'delete',\n em: EntityManager,\n _options?: unknown,\n entityName?: string\n ) {\n if (!entityName) {\n return {};\n }\n\n try {\n const metadata = em.getMetadata().getByClassName(entityName, false);\n if (!metadata) {\n return {};\n }\n\n const hasOrganizationId = metadata.properties['organizationId'] != null;\n const hasOrganization = metadata.properties['organization'] != null;\n\n if (hasOrganizationId || hasOrganization) {\n return { organizationId: args.tenantId };\n }\n } catch {\n // Entity not found in metadata — skip filtering\n }\n\n return {};\n },\n default: true,\n args: true\n };\n}\n\n/**\n * Registers the global tenant isolation filter on the ORM's entity manager.\n * Call this once at application bootstrap after `MikroORM.init()`.\n *\n * After calling this, every fork of the EM will inherit the filter.\n * Set the tenant ID per-request via:\n *\n * ```ts\n * em.setFilterParams('tenant', { tenantId: 'org-123' });\n * ```\n */\nexport function setupTenantFilter(orm: MikroORM): void {\n orm.em.addFilter(createTenantFilterDef());\n}\n\n/**\n * Returns a forked EntityManager with the tenant filter disabled.\n *\n * Use this only from code paths that have verified super-admin permissions.\n * Queries executed through the returned EM will return cross-tenant data.\n */\nexport function getSuperAdminContext(em: EntityManager): EntityManager {\n const forked = em.fork();\n forked.setFilterParams(TENANT_FILTER_NAME, { tenantId: undefined });\n // Disable the filter by passing false for the filter in each query isn't\n // sufficient globally; instead we add the filter with enabled = false.\n // The cleanest way is to re-add the filter as disabled on this fork.\n forked.addFilter({\n name: TENANT_FILTER_NAME,\n cond: {},\n default: false\n });\n return forked;\n}\n","import type {\n EntityManager,\n EventSubscriber,\n MikroORM,\n TransactionEventArgs\n} from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface RlsConfig {\n /**\n * Whether to enable PostgreSQL Row-Level Security.\n * Defaults to `true` when the driver is PostgreSQL, `false` otherwise.\n * Set to `false` to opt out even on PostgreSQL.\n */\n enabled?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// RLS EventSubscriber\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM EventSubscriber that executes `SET LOCAL app.tenant_id = :tenantId`\n * at the start of every transaction when PostgreSQL RLS is enabled.\n *\n * This ensures that even if the MikroORM global filter is somehow bypassed,\n * the database-level RLS policy enforces tenant isolation.\n *\n * The tenant ID is read from the EntityManager's filter parameters\n * (set by the tenant context middleware).\n */\nexport class RlsEventSubscriber implements EventSubscriber {\n async afterTransactionStart(args: TransactionEventArgs): Promise<void> {\n const tenantId = this.getTenantId(args.em);\n if (!tenantId) {\n // No tenant context (e.g., super-admin or public route) — skip SET LOCAL\n return;\n }\n\n const connection = args.em.getConnection();\n // Execute SET LOCAL within the transaction context\n // SET LOCAL only persists for the current transaction — no connection leakage\n await connection.execute(\n `SET LOCAL app.tenant_id = '${escapeSqlString(tenantId)}'`,\n [],\n 'run',\n args.transaction\n );\n }\n\n private getTenantId(em: EntityManager): string | undefined {\n const params = em.getFilterParams(TENANT_FILTER_NAME) as\n | { tenantId?: string }\n | undefined;\n return params?.tenantId;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Setup\n// ---------------------------------------------------------------------------\n\n/**\n * Sets up PostgreSQL Row-Level Security integration.\n *\n * 1. Registers the `RlsEventSubscriber` to run `SET LOCAL app.tenant_id`\n * at the start of every transaction.\n * 2. Validates that RLS policies exist on tenant-scoped tables (warns if missing).\n *\n * Call this at application bootstrap after `MikroORM.init()` and `setupTenantFilter()`.\n *\n * @param orm - The initialized MikroORM instance\n * @param config - RLS configuration (enabled defaults to auto-detect PostgreSQL)\n */\nexport function setupRls(orm: MikroORM, config: RlsConfig = {}): void {\n const isPostgres = isPostgresDriver(orm);\n const enabled = config.enabled ?? isPostgres;\n\n if (!enabled) {\n if (!isPostgres) {\n // Non-PostgreSQL — RLS not available, ORM filter is the sole enforcement\n return;\n }\n // PostgreSQL but explicitly disabled\n console.info(\n '[compliance] PostgreSQL RLS disabled by configuration. ORM filter is the sole tenant enforcement layer.'\n );\n return;\n }\n\n if (!isPostgres) {\n console.warn(\n '[compliance] RLS enabled but database driver is not PostgreSQL. ' +\n 'RLS is only supported on PostgreSQL. Falling back to ORM filter only.'\n );\n return;\n }\n\n // Register the RLS transaction subscriber\n orm.em.getEventManager().registerSubscriber(new RlsEventSubscriber());\n\n // Validate RLS policies exist\n validateRlsPolicies(orm).catch((err) => {\n console.warn('[compliance] Failed to validate RLS policies:', err);\n });\n}\n\n// ---------------------------------------------------------------------------\n// RLS policy validation\n// ---------------------------------------------------------------------------\n\n/**\n * Checks that tenant-scoped entities have RLS policies on their tables.\n * Logs warnings with the SQL needed to create missing policies.\n */\nasync function validateRlsPolicies(orm: MikroORM): Promise<void> {\n const metadata = orm.em.getMetadata().getAll();\n\n for (const meta of Object.values(metadata)) {\n const hasOrgId = meta.properties['organizationId'] != null;\n const hasOrg = meta.properties['organization'] != null;\n\n if (!hasOrgId && !hasOrg) continue;\n\n const tableName = meta.tableName;\n try {\n const connection = orm.em.getConnection();\n const result = await connection.execute<{ policyname: string }[]>(\n `SELECT policyname FROM pg_policies WHERE tablename = '${escapeSqlString(tableName)}'`,\n [],\n 'all'\n );\n\n const policies = Array.isArray(result) ? result : [];\n const hasTenantPolicy = policies.some((p: { policyname: string }) =>\n p.policyname.includes('tenant')\n );\n\n if (!hasTenantPolicy) {\n console.warn(\n `[compliance] No tenant RLS policy found on table '${tableName}'. ` +\n `Create one with:\\n` +\n ` ALTER TABLE \"${tableName}\" ENABLE ROW LEVEL SECURITY;\\n` +\n ` CREATE POLICY tenant_isolation ON \"${tableName}\"\\n` +\n ` USING (organization_id = current_setting('app.tenant_id'));`\n );\n }\n } catch {\n // Query failed — likely not connected yet or table doesn't exist\n // Skip validation for this table\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the ORM is using a PostgreSQL driver.\n * Checks the platform constructor name which is 'PostgreSqlPlatform' for PG.\n */\nfunction isPostgresDriver(orm: MikroORM): boolean {\n try {\n const platform = orm.em.getPlatform();\n const name = platform.constructor.name.toLowerCase();\n return name.includes('postgre');\n } catch {\n return false;\n }\n}\n\n/**\n * Escape a string for safe inclusion in SQL. Prevents SQL injection in\n * the SET LOCAL statement.\n */\nfunction escapeSqlString(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n"],"mappings":"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","names":["import_core","p"]}
|
|
1
|
+
{"version":3,"sources":["../../src/persistence/index.ts","../../src/persistence/complianceTypes.ts","../../src/persistence/compliancePropertyBuilder.ts","../../src/persistence/defineComplianceEntity.ts","../../src/encryption/fieldEncryptor.ts","../../src/persistence/complianceEventSubscriber.ts","../../src/persistence/tenantFilter.ts","../../src/persistence/rls.ts"],"sourcesContent":["// Compliance types and registry\nexport {\n ComplianceLevel,\n type ComplianceLevel as ComplianceLevelType,\n type ClassifiedProperty,\n type ClassifiedScalarProperty,\n type ClassifiedRelationChain,\n type ForklaunchPropertyBuilders,\n type ForklaunchPropertyChain,\n getComplianceMetadata,\n getEntityComplianceFields,\n entityHasEncryptedFields\n} from './complianceTypes';\n\n// Compliance-aware property builder (drop-in replacement for MikroORM's p)\nexport { fp } from './compliancePropertyBuilder';\n\n// Compliance-aware entity definition (drop-in replacement for MikroORM's defineEntity)\nexport { defineComplianceEntity } from './defineComplianceEntity';\n\n// Compliance EventSubscriber (encrypt on persist, decrypt on load)\nexport {\n ComplianceEventSubscriber,\n wrapEmWithNativeQueryBlocking\n} from './complianceEventSubscriber';\n\n// Re-export InferEntity from MikroORM for convenience\nexport type { InferEntity } from '@mikro-orm/core';\n\n// Tenant isolation filter\nexport {\n setupTenantFilter,\n getSuperAdminContext,\n createTenantFilterDef,\n TENANT_FILTER_NAME\n} from './tenantFilter';\n\n// PostgreSQL Row-Level Security\nexport { setupRls, RlsEventSubscriber, type RlsConfig } from './rls';\n","import type {\n PropertyBuilders,\n PropertyChain,\n UniversalPropertyOptionsBuilder\n} from '@mikro-orm/core';\n\n/**\n * Classification levels for entity field compliance.\n * Drives encryption (phi/pci), audit log redaction (all non-none), and compliance reporting.\n */\nexport const ComplianceLevel = {\n pii: 'pii',\n phi: 'phi',\n pci: 'pci',\n none: 'none'\n} as const;\nexport type ComplianceLevel =\n (typeof ComplianceLevel)[keyof typeof ComplianceLevel];\n\n/**\n * Brand symbol — makes ClassifiedProperty structurally distinct from\n * plain PropertyChain at the TypeScript level.\n */\ndeclare const CLASSIFIED: unique symbol;\n\n/**\n * Brand-only type used by `AssertAllClassified` to check that every\n * property has been classified. Both `ClassifiedScalarProperty` and\n * `ClassifiedRelationChain` extend this.\n */\nexport interface ClassifiedProperty {\n readonly [CLASSIFIED]: true;\n}\n\n/**\n * A scalar property classified via `.compliance()`.\n * Extends `PropertyChain<Value, Options>` so that `defineEntity` (and\n * therefore `InferEntity`) can still read the value/options types for\n * entity type inference. The `[CLASSIFIED]` brand prevents unclassified\n * properties from being accepted by `defineComplianceEntity`.\n */\nexport type ClassifiedScalarProperty<Value, Options> = PropertyChain<\n Value,\n Options\n> &\n ClassifiedProperty;\n\n/**\n * Internal key used by the runtime Proxy to store compliance level\n * on the builder instance. Not part of the public API.\n */\nexport const COMPLIANCE_KEY = '~compliance' as const;\n\n// ---------------------------------------------------------------------------\n// Compliance metadata registry\n// ---------------------------------------------------------------------------\n\n/** entityName → (fieldName → ComplianceLevel) */\nconst complianceRegistry = new Map<string, Map<string, ComplianceLevel>>();\n\n/**\n * Register compliance metadata for an entity's fields.\n * Called by `defineComplianceEntity` during entity definition.\n */\nexport function registerEntityCompliance(\n entityName: string,\n fields: Map<string, ComplianceLevel>\n): void {\n complianceRegistry.set(entityName, fields);\n}\n\n/**\n * Look up the compliance level for a single field on an entity.\n * Returns `'none'` if the entity or field is not registered.\n */\nexport function getComplianceMetadata(\n entityName: string,\n fieldName: string\n): ComplianceLevel {\n return complianceRegistry.get(entityName)?.get(fieldName) ?? 'none';\n}\n\n/**\n * Get all compliance fields for an entity.\n * Returns undefined if the entity is not registered.\n */\nexport function getEntityComplianceFields(\n entityName: string\n): Map<string, ComplianceLevel> | undefined {\n return complianceRegistry.get(entityName);\n}\n\n/**\n * Check whether an entity has any fields requiring encryption (phi or pci).\n */\nexport function entityHasEncryptedFields(entityName: string): boolean {\n const fields = complianceRegistry.get(entityName);\n if (!fields) return false;\n for (const level of fields.values()) {\n if (level === 'phi' || level === 'pci') return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyChain — remapped PropertyChain that preserves\n// `.compliance()` through method chaining.\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively remaps every method on PropertyChain<V,O> so those returning\n * PropertyChain<V2,O2> instead return ForklaunchPropertyChain<V2,O2>.\n * This preserves the `.compliance()` method through chained calls like\n * `.nullable().unique()`.\n */\n\nexport interface ForklaunchPropertyChain<Value, Options> extends RemapReturns<\n Value,\n Options\n> {\n /**\n * Classify this field's compliance level. Must be called on every scalar\n * field passed to `defineComplianceEntity`.\n * Returns a `ClassifiedScalarProperty` that preserves the PropertyChain\n * type info for `InferEntity` to work.\n */\n compliance(level: ComplianceLevel): ClassifiedScalarProperty<Value, Options>;\n}\n\ntype RemapReturns<Value, Options> = {\n [K in keyof PropertyChain<Value, Options>]: PropertyChain<\n Value,\n Options\n >[K] extends (...args: infer A) => PropertyChain<infer V2, infer O2>\n ? (...args: A) => ForklaunchPropertyChain<V2, O2>\n : PropertyChain<Value, Options>[K];\n};\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyBuilders — the type of `fp`\n// ---------------------------------------------------------------------------\n\n/**\n * Keys on PropertyBuilders that return relation builders.\n * These are auto-classified as 'none' — the fp proxy wraps them\n * to return ClassifiedProperty directly.\n */\ntype RelationBuilderKeys =\n | 'manyToOne'\n | 'oneToMany'\n | 'manyToMany'\n | 'oneToOne'\n | 'embedded';\n\n/**\n * The type of `fp` — mirrors `PropertyBuilders` but:\n * - Scalar methods return `ForklaunchPropertyChain` (must call `.compliance()`)\n * - Relation methods return `ClassifiedProperty` directly (auto 'none')\n */\nexport type ForklaunchPropertyBuilders = {\n [K in Exclude<\n keyof PropertyBuilders,\n RelationBuilderKeys\n >]: PropertyBuilders[K] extends (\n ...args: infer A\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n ) => UniversalPropertyOptionsBuilder<infer V, infer O, infer _IK>\n ? (...args: A) => ForklaunchPropertyChain<V, O>\n : PropertyBuilders[K] extends (\n ...args: infer A\n ) => PropertyChain<infer V, infer O>\n ? (...args: A) => ForklaunchPropertyChain<V, O>\n : PropertyBuilders[K];\n} & {\n [K in RelationBuilderKeys]: PropertyBuilders[K] extends (\n ...args: infer A\n ) => PropertyChain<infer V, infer O>\n ? (...args: A) => ClassifiedRelationChain<V, O>\n : PropertyBuilders[K];\n};\n\n/**\n * A relation builder that is already classified (as 'none') but still\n * supports chaining relation-specific methods like `.mappedBy()`, `.nullable()`.\n * All chain methods return ClassifiedRelationChain (preserving the brand).\n */\nexport type ClassifiedRelationChain<Value, Options> = {\n [K in keyof PropertyChain<Value, Options>]: PropertyChain<\n Value,\n Options\n >[K] extends (...args: infer A) => PropertyChain<infer V2, infer O2>\n ? (...args: A) => ClassifiedRelationChain<V2, O2>\n : PropertyChain<Value, Options>[K];\n} & ClassifiedProperty;\n","import { p } from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n type ComplianceLevel,\n type ForklaunchPropertyBuilders\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Runtime Proxy implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a value is a MikroORM property builder (has ~options).\n */\nfunction isBuilder(value: unknown): value is object {\n return (\n value != null &&\n typeof value === 'object' &&\n '~options' in (value as Record<string, unknown>)\n );\n}\n\n/**\n * Wraps a MikroORM scalar PropertyBuilder in a Proxy that:\n * 1. Adds a `.compliance(level)` method\n * 2. Forwards all other method calls to the underlying builder\n * 3. Re-wraps returned builders so `.compliance()` persists through chains\n */\nfunction wrapUnclassified(builder: unknown): unknown {\n return new Proxy(builder as object, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === 'compliance') {\n return (level: ComplianceLevel) => wrapClassified(target, level);\n }\n if (prop === '~options') return Reflect.get(target, prop, target);\n if (prop === COMPLIANCE_KEY) return undefined;\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a builder that has been classified via `.compliance()`.\n * Stores the compliance level under `~compliance` for `defineComplianceEntity`.\n * Chaining after `.compliance()` propagates the level through subsequent builders.\n */\nfunction wrapClassified(builder: object, level: ComplianceLevel): unknown {\n return new Proxy(builder, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === COMPLIANCE_KEY) return level;\n if (prop === '~options') return Reflect.get(target, prop, target);\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapClassified(result, level) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a relation PropertyBuilder (manyToOne, oneToMany, etc.).\n * Auto-classified as 'none' — no `.compliance()` call needed.\n * All chained methods preserve the auto-classification.\n */\nfunction wrapRelation(builder: object): unknown {\n return wrapClassified(builder, 'none');\n}\n\n// ---------------------------------------------------------------------------\n// Relation method detection\n// ---------------------------------------------------------------------------\n\nconst RELATION_METHODS = new Set([\n 'manyToOne',\n 'oneToMany',\n 'manyToMany',\n 'oneToOne',\n 'embedded'\n]);\n\nfunction isRelationMethod(prop: string | symbol): boolean {\n return typeof prop === 'string' && RELATION_METHODS.has(prop);\n}\n\n// ---------------------------------------------------------------------------\n// fp — the ForkLaunch property builder\n// ---------------------------------------------------------------------------\n\n/**\n * ForkLaunch property builder. Drop-in replacement for MikroORM's `p`\n * that adds `.compliance(level)` to every scalar property builder\n * and auto-classifies relation builders as 'none'.\n *\n * - Scalar fields: `fp.string().compliance('pii')` — must call `.compliance()`\n * - Relation fields: `fp.manyToOne(Target)` — auto-classified, no `.compliance()` needed\n *\n * @example\n * ```typescript\n * import { defineComplianceEntity, fp } from '@forklaunch/core/persistence';\n *\n * const User = defineComplianceEntity({\n * name: 'User',\n * properties: {\n * id: fp.uuid().primary().compliance('none'),\n * email: fp.string().unique().compliance('pii'),\n * medicalRecord: fp.string().nullable().compliance('phi'),\n * organization: () => fp.manyToOne(Organization).nullable(),\n * }\n * });\n * ```\n */\nexport const fp: ForklaunchPropertyBuilders = new Proxy(p, {\n get(target: Record<string | symbol, unknown>, prop) {\n const value = Reflect.get(target, prop, target);\n if (typeof value !== 'function') return value;\n\n if (isRelationMethod(prop)) {\n // Relation methods: call the original, wrap result as auto-classified 'none'\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapRelation(result) : result;\n };\n }\n\n // Scalar methods: call the original, wrap result with .compliance()\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n}) as ForklaunchPropertyBuilders;\n","import {\n defineEntity,\n p,\n type EntityMetadataWithProperties\n} from '@mikro-orm/core';\nimport type { InferEntity } from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n type ClassifiedProperty,\n type ComplianceLevel,\n registerEntityCompliance\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Checks each property in TProperties extends ClassifiedProperty.\n * Properties that don't have the [CLASSIFIED] brand are mapped to\n * ClassifiedProperty, causing a type error at the call site.\n */\ntype AssertAllClassified<T extends Record<string, unknown>> = {\n [K in keyof T]: T[K] extends ClassifiedProperty\n ? T[K]\n : T[K] extends () => ClassifiedProperty\n ? T[K]\n : ClassifiedProperty;\n};\n\n// ---------------------------------------------------------------------------\n// Runtime helpers\n// ---------------------------------------------------------------------------\n\nfunction readComplianceLevel(builder: unknown): ComplianceLevel | undefined {\n if (builder == null || typeof builder !== 'object') return undefined;\n return (builder as Record<string, unknown>)[COMPLIANCE_KEY] as\n | ComplianceLevel\n | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// defineComplianceEntity\n// ---------------------------------------------------------------------------\n\n/**\n * Wrapper around MikroORM's `defineEntity` that enforces compliance\n * classification on every field at both compile-time and runtime.\n *\n * The return type is inferred directly from `defineEntity` — `InferEntity`\n * works because `ClassifiedScalarProperty<V,O>` extends `PropertyChain<V,O>`.\n *\n * @example\n * ```typescript\n * const User = defineComplianceEntity({\n * name: 'User',\n * properties: {\n * id: fp.uuid().primary().compliance('none'),\n * email: fp.string().unique().compliance('pii'),\n * organization: () => fp.manyToOne(Organization).nullable(),\n * }\n * });\n * export type User = InferEntity<typeof User>;\n * ```\n */\nexport function defineComplianceEntity<\n const TName extends string,\n const TTableName extends string,\n const TProperties extends Record<string, ClassifiedProperty>,\n const TPK extends (keyof TProperties)[] | undefined = undefined,\n const TBase = never,\n const TRepository = never,\n const TForceObject extends boolean = false\n>(\n meta: EntityMetadataWithProperties<\n TName,\n TTableName,\n TProperties,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >\n) {\n const entityName = meta.name;\n const complianceFields = new Map<string, ComplianceLevel>();\n\n // Resolve properties — meta.properties can be an object or factory function\n const rawProperties = meta.properties;\n const properties: Record<string, unknown> =\n typeof rawProperties === 'function' ? rawProperties(p) : rawProperties;\n\n // Validate and extract compliance from each property\n for (const [fieldName, rawProp] of Object.entries(properties)) {\n const prop = typeof rawProp === 'function' ? rawProp() : rawProp;\n const level = readComplianceLevel(prop);\n\n if (level == null) {\n throw new Error(\n `Field '${entityName}.${fieldName}' is missing compliance classification. ` +\n `Call .compliance('pii' | 'phi' | 'pci' | 'none') on this property, ` +\n `or use a relation method (fp.manyToOne, etc.) which is auto-classified.`\n );\n }\n complianceFields.set(fieldName, level);\n }\n\n // Store compliance metadata in the global registry\n registerEntityCompliance(entityName, complianceFields);\n\n // Pass through to defineEntity — ClassifiedScalarProperty<V,O> extends\n // PropertyChain<V,O> so MikroORM's type inference works correctly.\n return defineEntity(meta);\n}\n\n// Re-export InferEntity for convenience\nexport type { InferEntity };\n","import crypto from 'crypto';\n\n// ---------------------------------------------------------------------------\n// Error types\n// ---------------------------------------------------------------------------\n\nexport class MissingEncryptionKeyError extends Error {\n readonly name = 'MissingEncryptionKeyError' as const;\n constructor(message = 'Master encryption key must be provided') {\n super(message);\n }\n}\n\nexport class DecryptionError extends Error {\n readonly name = 'DecryptionError' as const;\n constructor(\n message = 'Decryption failed: ciphertext is corrupted or the wrong key was used'\n ) {\n super(message);\n }\n}\n\nexport class EncryptionRequiredError extends Error {\n readonly name = 'EncryptionRequiredError' as const;\n constructor(\n message = 'Encryption is required before persisting this compliance field'\n ) {\n super(message);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst ALGORITHM = 'aes-256-gcm' as const;\nconst IV_BYTES = 12;\nconst KEY_BYTES = 32;\nconst HKDF_HASH = 'sha256' as const;\nconst HKDF_SALT = Buffer.alloc(0); // empty salt – key material is already high-entropy\n\n// ---------------------------------------------------------------------------\n// FieldEncryptor\n// ---------------------------------------------------------------------------\n\nexport class FieldEncryptor {\n private readonly masterKey: string;\n\n constructor(masterKey: string) {\n if (!masterKey) {\n throw new MissingEncryptionKeyError();\n }\n this.masterKey = masterKey;\n }\n\n /**\n * Derive a per-tenant 32-byte key using HKDF-SHA256.\n * The master key is used as input key material and the tenantId as info context.\n */\n deriveKey(tenantId: string): Buffer {\n return Buffer.from(\n crypto.hkdfSync(HKDF_HASH, this.masterKey, HKDF_SALT, tenantId, KEY_BYTES)\n );\n }\n\n /**\n * Encrypt a plaintext string for a specific tenant.\n *\n * @returns Format: `v1:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`\n */\n encrypt(plaintext: string | null): string | null;\n encrypt(plaintext: string | null, tenantId: string): string | null;\n encrypt(plaintext: string | null, tenantId?: string): string | null {\n if (plaintext === null || plaintext === undefined) {\n return null;\n }\n\n const key = this.deriveKey(tenantId ?? '');\n const iv = crypto.randomBytes(IV_BYTES);\n\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final()\n ]);\n const authTag = cipher.getAuthTag();\n\n return [\n 'v1',\n iv.toString('base64'),\n authTag.toString('base64'),\n encrypted.toString('base64')\n ].join(':');\n }\n\n /**\n * Decrypt a ciphertext string produced by {@link encrypt}.\n */\n decrypt(ciphertext: string | null): string | null;\n decrypt(ciphertext: string | null, tenantId: string): string | null;\n decrypt(ciphertext: string | null, tenantId?: string): string | null {\n if (ciphertext === null || ciphertext === undefined) {\n return null;\n }\n\n const parts = ciphertext.split(':');\n if (parts.length !== 4 || parts[0] !== 'v1') {\n throw new DecryptionError(\n `Unknown ciphertext version or malformed format`\n );\n }\n\n const [, ivB64, authTagB64, encryptedB64] = parts;\n const iv = Buffer.from(ivB64, 'base64');\n const authTag = Buffer.from(authTagB64, 'base64');\n const encrypted = Buffer.from(encryptedB64, 'base64');\n const key = this.deriveKey(tenantId ?? '');\n\n try {\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final()\n ]);\n return decrypted.toString('utf8');\n } catch {\n throw new DecryptionError();\n }\n }\n}\n","import type {\n EntityManager,\n EventArgs,\n EventSubscriber\n} from '@mikro-orm/core';\nimport {\n DecryptionError,\n EncryptionRequiredError,\n FieldEncryptor\n} from '../encryption/fieldEncryptor';\nimport {\n type ComplianceLevel,\n getEntityComplianceFields\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst ENCRYPTED_PREFIX = 'v1:';\n\n/**\n * Compliance levels that require field-level encryption.\n * PII is NOT encrypted (RDS encryption + TLS sufficient).\n */\nconst ENCRYPTED_LEVELS: ReadonlySet<ComplianceLevel> = new Set(['phi', 'pci']);\n\n// ---------------------------------------------------------------------------\n// ComplianceEventSubscriber\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM EventSubscriber that enforces field-level encryption for\n * compliance-classified fields (PHI and PCI).\n *\n * - **onBeforeCreate / onBeforeUpdate**: Encrypts PHI/PCI fields before\n * database persistence. Throws `EncryptionRequiredError` if the encryption\n * key is unavailable.\n * - **onLoad**: Decrypts PHI/PCI fields after loading from the database.\n * Pre-migration plaintext (no `v1:` prefix) is returned as-is with a\n * console warning to support rolling deployments.\n *\n * The tenant ID for key derivation is read from the EntityManager's filter\n * parameters (set by the tenant context middleware).\n */\nexport class ComplianceEventSubscriber implements EventSubscriber {\n private readonly encryptor: FieldEncryptor;\n\n constructor(encryptor: FieldEncryptor) {\n this.encryptor = encryptor;\n }\n\n async beforeCreate(args: EventArgs<unknown>): Promise<void> {\n this.encryptFields(args);\n }\n\n async beforeUpdate(args: EventArgs<unknown>): Promise<void> {\n this.encryptFields(args);\n }\n\n async onLoad(args: EventArgs<unknown>): Promise<void> {\n this.decryptFields(args);\n }\n\n // ---------------------------------------------------------------------------\n // Encrypt on persist\n // ---------------------------------------------------------------------------\n\n private encryptFields(args: EventArgs<unknown>): void {\n const entityName = args.meta.className;\n const complianceFields = getEntityComplianceFields(entityName);\n if (!complianceFields) return;\n\n const tenantId = this.getTenantId(args.em);\n const entity = args.entity as Record<string, unknown>;\n\n for (const [fieldName, level] of complianceFields) {\n if (!ENCRYPTED_LEVELS.has(level)) continue;\n\n const value = entity[fieldName];\n if (value === null || value === undefined) continue;\n if (typeof value !== 'string') continue;\n\n // Don't double-encrypt\n if (value.startsWith(ENCRYPTED_PREFIX)) continue;\n\n entity[fieldName] = this.encryptor.encrypt(value, tenantId);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Decrypt on load\n // ---------------------------------------------------------------------------\n\n private decryptFields(args: EventArgs<unknown>): void {\n const entityName = args.meta.className;\n const complianceFields = getEntityComplianceFields(entityName);\n if (!complianceFields) return;\n\n const tenantId = this.getTenantId(args.em);\n const entity = args.entity as Record<string, unknown>;\n\n for (const [fieldName, level] of complianceFields) {\n if (!ENCRYPTED_LEVELS.has(level)) continue;\n\n const value = entity[fieldName];\n if (value === null || value === undefined) continue;\n if (typeof value !== 'string') continue;\n\n if (value.startsWith(ENCRYPTED_PREFIX)) {\n // Encrypted — decrypt it\n try {\n entity[fieldName] = this.encryptor.decrypt(value, tenantId);\n } catch (err) {\n if (err instanceof DecryptionError) {\n throw new DecryptionError(\n `Failed to decrypt ${entityName}.${fieldName}: ${err.message}`\n );\n }\n throw err;\n }\n } else {\n // Pre-migration plaintext — return as-is, log warning\n console.warn(\n `[compliance] ${entityName}.${fieldName} contains unencrypted ${level} data. ` +\n `Run encryption migration to encrypt existing data.`\n );\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Tenant ID resolution\n // ---------------------------------------------------------------------------\n\n /**\n * Read the tenant ID from the EntityManager's filter parameters.\n * The tenant context middleware sets this when forking the EM per request.\n */\n private getTenantId(em: EntityManager): string {\n const filters = em.getFilterParams('tenant') as\n | { tenantId?: string }\n | undefined;\n const tenantId = filters?.tenantId;\n if (!tenantId) {\n throw new EncryptionRequiredError(\n 'Cannot encrypt/decrypt without tenant context. ' +\n 'Ensure the tenant filter is set on the EntityManager.'\n );\n }\n return tenantId;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Native query blocking\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and\n * `nativeDelete` on entities that have PHI or PCI compliance fields.\n *\n * This prevents bypassing the ComplianceEventSubscriber's encryption\n * by using raw queries. Call this in the tenant context middleware when\n * creating the request-scoped EM.\n *\n * @returns A Proxy-wrapped EntityManager that throws on native query\n * operations targeting compliance entities.\n */\nexport function wrapEmWithNativeQueryBlocking<T extends EntityManager>(\n em: T\n): T {\n const BLOCKED_METHODS = [\n 'nativeInsert',\n 'nativeUpdate',\n 'nativeDelete'\n ] as const;\n\n return new Proxy(em, {\n get(target, prop, receiver) {\n if (\n typeof prop === 'string' &&\n BLOCKED_METHODS.includes(prop as (typeof BLOCKED_METHODS)[number])\n ) {\n return (entityNameOrEntity: unknown, ...rest: unknown[]) => {\n const entityName = resolveEntityName(entityNameOrEntity);\n if (entityName) {\n const fields = getEntityComplianceFields(entityName);\n if (fields) {\n for (const [fieldName, level] of fields) {\n if (ENCRYPTED_LEVELS.has(level)) {\n throw new EncryptionRequiredError(\n `${prop}() blocked on entity '${entityName}' because field ` +\n `'${fieldName}' has compliance level '${level}'. ` +\n `Use em.create() + em.flush() instead to ensure encryption.`\n );\n }\n }\n }\n }\n // No compliance fields requiring encryption — allow the native query\n const method = Reflect.get(target, prop, receiver);\n return (method as (...args: unknown[]) => unknown).call(\n target,\n entityNameOrEntity,\n ...rest\n );\n };\n }\n return Reflect.get(target, prop, receiver);\n }\n });\n}\n\n/**\n * Resolve an entity name from the first argument to nativeInsert/Update/Delete.\n * MikroORM accepts entity name strings, entity class references, or entity instances.\n */\nfunction resolveEntityName(entityNameOrEntity: unknown): string | undefined {\n if (typeof entityNameOrEntity === 'string') {\n return entityNameOrEntity;\n }\n if (typeof entityNameOrEntity === 'function') {\n return (entityNameOrEntity as { name?: string }).name;\n }\n if (\n entityNameOrEntity != null &&\n typeof entityNameOrEntity === 'object' &&\n 'constructor' in entityNameOrEntity\n ) {\n return (entityNameOrEntity.constructor as { name?: string }).name;\n }\n return undefined;\n}\n","import type {\n Dictionary,\n EntityManager,\n FilterDef,\n MikroORM\n} from '@mikro-orm/core';\n\n/**\n * The name used to register the tenant isolation filter.\n */\nexport const TENANT_FILTER_NAME = 'tenant';\n\n/**\n * Creates the tenant filter definition.\n *\n * The filter adds `WHERE organizationId = :tenantId` to all queries\n * on entities that have an `organizationId` or `organization` property.\n * Entities without either property are unaffected (empty condition).\n */\nexport function createTenantFilterDef(): FilterDef {\n return {\n name: TENANT_FILTER_NAME,\n cond(\n args: Dictionary,\n _type: 'read' | 'update' | 'delete',\n em: EntityManager,\n _options?: unknown,\n entityName?: string\n ) {\n if (!entityName) {\n return {};\n }\n\n try {\n const metadata = em.getMetadata().getByClassName(entityName, false);\n if (!metadata) {\n return {};\n }\n\n const hasOrganizationId = metadata.properties['organizationId'] != null;\n const hasOrganization = metadata.properties['organization'] != null;\n\n if (hasOrganizationId || hasOrganization) {\n return { organizationId: args.tenantId };\n }\n } catch {\n // Entity not found in metadata — skip filtering\n }\n\n return {};\n },\n default: true,\n args: true\n };\n}\n\n/**\n * Registers the global tenant isolation filter on the ORM's entity manager.\n * Call this once at application bootstrap after `MikroORM.init()`.\n *\n * After calling this, every fork of the EM will inherit the filter.\n * Set the tenant ID per-request via:\n *\n * ```ts\n * em.setFilterParams('tenant', { tenantId: 'org-123' });\n * ```\n */\nexport function setupTenantFilter(orm: MikroORM): void {\n orm.em.addFilter(createTenantFilterDef());\n}\n\n/**\n * Returns a forked EntityManager with the tenant filter disabled.\n *\n * Use this only from code paths that have verified super-admin permissions.\n * Queries executed through the returned EM will return cross-tenant data.\n */\nexport function getSuperAdminContext(em: EntityManager): EntityManager {\n const forked = em.fork();\n forked.setFilterParams(TENANT_FILTER_NAME, { tenantId: undefined });\n // Disable the filter by passing false for the filter in each query isn't\n // sufficient globally; instead we add the filter with enabled = false.\n // The cleanest way is to re-add the filter as disabled on this fork.\n forked.addFilter({\n name: TENANT_FILTER_NAME,\n cond: {},\n default: false\n });\n return forked;\n}\n","import type {\n EntityManager,\n EventSubscriber,\n MikroORM,\n TransactionEventArgs\n} from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface RlsConfig {\n /**\n * Whether to enable PostgreSQL Row-Level Security.\n * Defaults to `true` when the driver is PostgreSQL, `false` otherwise.\n * Set to `false` to opt out even on PostgreSQL.\n */\n enabled?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// RLS EventSubscriber\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM EventSubscriber that executes `SET LOCAL app.tenant_id = :tenantId`\n * at the start of every transaction when PostgreSQL RLS is enabled.\n *\n * This ensures that even if the MikroORM global filter is somehow bypassed,\n * the database-level RLS policy enforces tenant isolation.\n *\n * The tenant ID is read from the EntityManager's filter parameters\n * (set by the tenant context middleware).\n */\nexport class RlsEventSubscriber implements EventSubscriber {\n async afterTransactionStart(args: TransactionEventArgs): Promise<void> {\n const tenantId = this.getTenantId(args.em);\n if (!tenantId) {\n // No tenant context (e.g., super-admin or public route) — skip SET LOCAL\n return;\n }\n\n const connection = args.em.getConnection();\n // Execute SET LOCAL within the transaction context\n // SET LOCAL only persists for the current transaction — no connection leakage\n await connection.execute(\n `SET LOCAL app.tenant_id = '${escapeSqlString(tenantId)}'`,\n [],\n 'run',\n args.transaction\n );\n }\n\n private getTenantId(em: EntityManager): string | undefined {\n const params = em.getFilterParams(TENANT_FILTER_NAME) as\n | { tenantId?: string }\n | undefined;\n return params?.tenantId;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Setup\n// ---------------------------------------------------------------------------\n\n/**\n * Sets up PostgreSQL Row-Level Security integration.\n *\n * 1. Registers the `RlsEventSubscriber` to run `SET LOCAL app.tenant_id`\n * at the start of every transaction.\n * 2. Validates that RLS policies exist on tenant-scoped tables (warns if missing).\n *\n * Call this at application bootstrap after `MikroORM.init()` and `setupTenantFilter()`.\n *\n * @param orm - The initialized MikroORM instance\n * @param config - RLS configuration (enabled defaults to auto-detect PostgreSQL)\n */\nexport function setupRls(orm: MikroORM, config: RlsConfig = {}): void {\n const isPostgres = isPostgresDriver(orm);\n const enabled = config.enabled ?? isPostgres;\n\n if (!enabled) {\n if (!isPostgres) {\n // Non-PostgreSQL — RLS not available, ORM filter is the sole enforcement\n return;\n }\n // PostgreSQL but explicitly disabled\n console.info(\n '[compliance] PostgreSQL RLS disabled by configuration. ORM filter is the sole tenant enforcement layer.'\n );\n return;\n }\n\n if (!isPostgres) {\n console.warn(\n '[compliance] RLS enabled but database driver is not PostgreSQL. ' +\n 'RLS is only supported on PostgreSQL. Falling back to ORM filter only.'\n );\n return;\n }\n\n // Register the RLS transaction subscriber\n orm.em.getEventManager().registerSubscriber(new RlsEventSubscriber());\n\n // Validate RLS policies exist\n validateRlsPolicies(orm).catch((err) => {\n console.warn('[compliance] Failed to validate RLS policies:', err);\n });\n}\n\n// ---------------------------------------------------------------------------\n// RLS policy validation\n// ---------------------------------------------------------------------------\n\n/**\n * Checks that tenant-scoped entities have RLS policies on their tables.\n * Logs warnings with the SQL needed to create missing policies.\n */\nasync function validateRlsPolicies(orm: MikroORM): Promise<void> {\n const metadata = orm.em.getMetadata().getAll();\n\n for (const meta of Object.values(metadata)) {\n const hasOrgId = meta.properties['organizationId'] != null;\n const hasOrg = meta.properties['organization'] != null;\n\n if (!hasOrgId && !hasOrg) continue;\n\n const tableName = meta.tableName;\n try {\n const connection = orm.em.getConnection();\n const result = await connection.execute<{ policyname: string }[]>(\n `SELECT policyname FROM pg_policies WHERE tablename = '${escapeSqlString(tableName)}'`,\n [],\n 'all'\n );\n\n const policies = Array.isArray(result) ? result : [];\n const hasTenantPolicy = policies.some((p: { policyname: string }) =>\n p.policyname.includes('tenant')\n );\n\n if (!hasTenantPolicy) {\n console.warn(\n `[compliance] No tenant RLS policy found on table '${tableName}'. ` +\n `Create one with:\\n` +\n ` ALTER TABLE \"${tableName}\" ENABLE ROW LEVEL SECURITY;\\n` +\n ` CREATE POLICY tenant_isolation ON \"${tableName}\"\\n` +\n ` USING (organization_id = current_setting('app.tenant_id'));`\n );\n }\n } catch {\n // Query failed — likely not connected yet or table doesn't exist\n // Skip validation for this table\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the ORM is using a PostgreSQL driver.\n * Checks the platform constructor name which is 'PostgreSqlPlatform' for PG.\n */\nfunction isPostgresDriver(orm: MikroORM): boolean {\n try {\n const platform = orm.em.getPlatform();\n const name = platform.constructor.name.toLowerCase();\n return name.includes('postgre');\n } catch {\n return false;\n }\n}\n\n/**\n * Escape a string for safe inclusion in SQL. Prevents SQL injection in\n * the SET LOCAL statement.\n */\nfunction escapeSqlString(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n"],"mappings":"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","names":["import_core","p"]}
|
|
@@ -108,14 +108,19 @@ var fp = new Proxy(p, {
|
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
// src/persistence/defineComplianceEntity.ts
|
|
111
|
-
import {
|
|
111
|
+
import {
|
|
112
|
+
defineEntity,
|
|
113
|
+
p as p2
|
|
114
|
+
} from "@mikro-orm/core";
|
|
112
115
|
function readComplianceLevel(builder) {
|
|
113
116
|
if (builder == null || typeof builder !== "object") return void 0;
|
|
114
117
|
return builder[COMPLIANCE_KEY];
|
|
115
118
|
}
|
|
116
119
|
function defineComplianceEntity(meta) {
|
|
117
|
-
const
|
|
120
|
+
const entityName = meta.name;
|
|
118
121
|
const complianceFields = /* @__PURE__ */ new Map();
|
|
122
|
+
const rawProperties = meta.properties;
|
|
123
|
+
const properties = typeof rawProperties === "function" ? rawProperties(p2) : rawProperties;
|
|
119
124
|
for (const [fieldName, rawProp] of Object.entries(properties)) {
|
|
120
125
|
const prop = typeof rawProp === "function" ? rawProp() : rawProp;
|
|
121
126
|
const level = readComplianceLevel(prop);
|
|
@@ -127,11 +132,7 @@ function defineComplianceEntity(meta) {
|
|
|
127
132
|
complianceFields.set(fieldName, level);
|
|
128
133
|
}
|
|
129
134
|
registerEntityCompliance(entityName, complianceFields);
|
|
130
|
-
return defineEntity(
|
|
131
|
-
name: entityName,
|
|
132
|
-
properties,
|
|
133
|
-
...rest
|
|
134
|
-
});
|
|
135
|
+
return defineEntity(meta);
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
// src/encryption/fieldEncryptor.ts
|
|
@@ -382,7 +383,7 @@ async function validateRlsPolicies(orm) {
|
|
|
382
383
|
);
|
|
383
384
|
const policies = Array.isArray(result) ? result : [];
|
|
384
385
|
const hasTenantPolicy = policies.some(
|
|
385
|
-
(
|
|
386
|
+
(p3) => p3.policyname.includes("tenant")
|
|
386
387
|
);
|
|
387
388
|
if (!hasTenantPolicy) {
|
|
388
389
|
console.warn(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/persistence/complianceTypes.ts","../../src/persistence/compliancePropertyBuilder.ts","../../src/persistence/defineComplianceEntity.ts","../../src/encryption/fieldEncryptor.ts","../../src/persistence/complianceEventSubscriber.ts","../../src/persistence/tenantFilter.ts","../../src/persistence/rls.ts"],"sourcesContent":["import type {\n PropertyBuilders,\n PropertyChain,\n UniversalPropertyOptionsBuilder\n} from '@mikro-orm/core';\n\n/**\n * Classification levels for entity field compliance.\n * Drives encryption (phi/pci), audit log redaction (all non-none), and compliance reporting.\n */\nexport const ComplianceLevel = {\n pii: 'pii',\n phi: 'phi',\n pci: 'pci',\n none: 'none'\n} as const;\nexport type ComplianceLevel =\n (typeof ComplianceLevel)[keyof typeof ComplianceLevel];\n\n/**\n * Brand symbol — makes ClassifiedProperty structurally distinct from\n * plain PropertyChain at the TypeScript level.\n */\ndeclare const CLASSIFIED: unique symbol;\n\n/**\n * A property that has been classified via `.compliance()`.\n * Only ClassifiedProperty values are accepted by `defineComplianceEntity`.\n *\n * At runtime this is a Proxy wrapping a MikroORM PropertyBuilder.\n * The brand exists only at the type level for compile-time enforcement.\n */\nexport interface ClassifiedProperty {\n readonly [CLASSIFIED]: true;\n}\n\n/**\n * Internal key used by the runtime Proxy to store compliance level\n * on the builder instance. Not part of the public API.\n */\nexport const COMPLIANCE_KEY = '~compliance' as const;\n\n// ---------------------------------------------------------------------------\n// Compliance metadata registry\n// ---------------------------------------------------------------------------\n\n/** entityName → (fieldName → ComplianceLevel) */\nconst complianceRegistry = new Map<string, Map<string, ComplianceLevel>>();\n\n/**\n * Register compliance metadata for an entity's fields.\n * Called by `defineComplianceEntity` during entity definition.\n */\nexport function registerEntityCompliance(\n entityName: string,\n fields: Map<string, ComplianceLevel>\n): void {\n complianceRegistry.set(entityName, fields);\n}\n\n/**\n * Look up the compliance level for a single field on an entity.\n * Returns `'none'` if the entity or field is not registered.\n */\nexport function getComplianceMetadata(\n entityName: string,\n fieldName: string\n): ComplianceLevel {\n return complianceRegistry.get(entityName)?.get(fieldName) ?? 'none';\n}\n\n/**\n * Get all compliance fields for an entity.\n * Returns undefined if the entity is not registered.\n */\nexport function getEntityComplianceFields(\n entityName: string\n): Map<string, ComplianceLevel> | undefined {\n return complianceRegistry.get(entityName);\n}\n\n/**\n * Check whether an entity has any fields requiring encryption (phi or pci).\n */\nexport function entityHasEncryptedFields(entityName: string): boolean {\n const fields = complianceRegistry.get(entityName);\n if (!fields) return false;\n for (const level of fields.values()) {\n if (level === 'phi' || level === 'pci') return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyChain — remapped PropertyChain that preserves\n// `.compliance()` through method chaining.\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively remaps every method on PropertyChain<V,O> so those returning\n * PropertyChain<V2,O2> instead return ForklaunchPropertyChain<V2,O2>.\n * This preserves the `.compliance()` method through chained calls like\n * `.nullable().unique()`.\n */\n\nexport interface ForklaunchPropertyChain<Value, Options> extends RemapReturns<\n Value,\n Options\n> {\n /**\n * Classify this field's compliance level. Must be called on every scalar\n * field passed to `defineComplianceEntity`.\n * Returns an opaque `ClassifiedProperty`.\n */\n compliance(level: ComplianceLevel): ClassifiedProperty;\n}\n\ntype RemapReturns<Value, Options> = {\n [K in keyof PropertyChain<Value, Options>]: PropertyChain<\n Value,\n Options\n >[K] extends (...args: infer A) => PropertyChain<infer V2, infer O2>\n ? (...args: A) => ForklaunchPropertyChain<V2, O2>\n : PropertyChain<Value, Options>[K];\n};\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyBuilders — the type of `fp`\n// ---------------------------------------------------------------------------\n\n/**\n * Keys on PropertyBuilders that return relation builders.\n * These are auto-classified as 'none' — the fp proxy wraps them\n * to return ClassifiedProperty directly.\n */\ntype RelationBuilderKeys =\n | 'manyToOne'\n | 'oneToMany'\n | 'manyToMany'\n | 'oneToOne'\n | 'embedded';\n\n/**\n * The type of `fp` — mirrors `PropertyBuilders` but:\n * - Scalar methods return `ForklaunchPropertyChain` (must call `.compliance()`)\n * - Relation methods return `ClassifiedProperty` directly (auto 'none')\n */\nexport type ForklaunchPropertyBuilders = {\n [K in Exclude<\n keyof PropertyBuilders,\n RelationBuilderKeys\n >]: PropertyBuilders[K] extends (\n ...args: infer A\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n ) => UniversalPropertyOptionsBuilder<infer V, infer O, infer _IK>\n ? (...args: A) => ForklaunchPropertyChain<V, O>\n : PropertyBuilders[K] extends (\n ...args: infer A\n ) => PropertyChain<infer V, infer O>\n ? (...args: A) => ForklaunchPropertyChain<V, O>\n : PropertyBuilders[K];\n} & {\n [K in RelationBuilderKeys]: PropertyBuilders[K] extends (\n ...args: infer A\n ) => PropertyChain<infer V, infer O>\n ? (...args: A) => ClassifiedRelationChain<V, O>\n : PropertyBuilders[K];\n};\n\n/**\n * A relation builder that is already classified (as 'none') but still\n * supports chaining relation-specific methods like `.mappedBy()`, `.nullable()`.\n * All chain methods return ClassifiedRelationChain (preserving the brand).\n */\nexport type ClassifiedRelationChain<Value, Options> = {\n [K in keyof PropertyChain<Value, Options>]: PropertyChain<\n Value,\n Options\n >[K] extends (...args: infer A) => PropertyChain<infer V2, infer O2>\n ? (...args: A) => ClassifiedRelationChain<V2, O2>\n : PropertyChain<Value, Options>[K];\n} & ClassifiedProperty;\n","import { p } from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n type ComplianceLevel,\n type ForklaunchPropertyBuilders\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Runtime Proxy implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a value is a MikroORM property builder (has ~options).\n */\nfunction isBuilder(value: unknown): value is object {\n return (\n value != null &&\n typeof value === 'object' &&\n '~options' in (value as Record<string, unknown>)\n );\n}\n\n/**\n * Wraps a MikroORM scalar PropertyBuilder in a Proxy that:\n * 1. Adds a `.compliance(level)` method\n * 2. Forwards all other method calls to the underlying builder\n * 3. Re-wraps returned builders so `.compliance()` persists through chains\n */\nfunction wrapUnclassified(builder: unknown): unknown {\n return new Proxy(builder as object, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === 'compliance') {\n return (level: ComplianceLevel) => wrapClassified(target, level);\n }\n if (prop === '~options') return Reflect.get(target, prop, target);\n if (prop === COMPLIANCE_KEY) return undefined;\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a builder that has been classified via `.compliance()`.\n * Stores the compliance level under `~compliance` for `defineComplianceEntity`.\n * Chaining after `.compliance()` propagates the level through subsequent builders.\n */\nfunction wrapClassified(builder: object, level: ComplianceLevel): unknown {\n return new Proxy(builder, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === COMPLIANCE_KEY) return level;\n if (prop === '~options') return Reflect.get(target, prop, target);\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapClassified(result, level) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a relation PropertyBuilder (manyToOne, oneToMany, etc.).\n * Auto-classified as 'none' — no `.compliance()` call needed.\n * All chained methods preserve the auto-classification.\n */\nfunction wrapRelation(builder: object): unknown {\n return wrapClassified(builder, 'none');\n}\n\n// ---------------------------------------------------------------------------\n// Relation method detection\n// ---------------------------------------------------------------------------\n\nconst RELATION_METHODS = new Set([\n 'manyToOne',\n 'oneToMany',\n 'manyToMany',\n 'oneToOne',\n 'embedded'\n]);\n\nfunction isRelationMethod(prop: string | symbol): boolean {\n return typeof prop === 'string' && RELATION_METHODS.has(prop);\n}\n\n// ---------------------------------------------------------------------------\n// fp — the ForkLaunch property builder\n// ---------------------------------------------------------------------------\n\n/**\n * ForkLaunch property builder. Drop-in replacement for MikroORM's `p`\n * that adds `.compliance(level)` to every scalar property builder\n * and auto-classifies relation builders as 'none'.\n *\n * - Scalar fields: `fp.string().compliance('pii')` — must call `.compliance()`\n * - Relation fields: `fp.manyToOne(Target)` — auto-classified, no `.compliance()` needed\n *\n * @example\n * ```typescript\n * import { defineComplianceEntity, fp } from '@forklaunch/core/persistence';\n *\n * const User = defineComplianceEntity({\n * name: 'User',\n * properties: {\n * id: fp.uuid().primary().compliance('none'),\n * email: fp.string().unique().compliance('pii'),\n * medicalRecord: fp.string().nullable().compliance('phi'),\n * organization: () => fp.manyToOne(Organization).nullable(),\n * }\n * });\n * ```\n */\nexport const fp: ForklaunchPropertyBuilders = new Proxy(p, {\n get(target: Record<string | symbol, unknown>, prop) {\n const value = Reflect.get(target, prop, target);\n if (typeof value !== 'function') return value;\n\n if (isRelationMethod(prop)) {\n // Relation methods: call the original, wrap result as auto-classified 'none'\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapRelation(result) : result;\n };\n }\n\n // Scalar methods: call the original, wrap result with .compliance()\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n}) as ForklaunchPropertyBuilders;\n","import { defineEntity } from '@mikro-orm/core';\nimport type { InferEntity } from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n type ClassifiedProperty,\n type ComplianceLevel,\n registerEntityCompliance\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Maps each property to `never` if it doesn't extend ClassifiedProperty.\n * Used in an intersection with TProperties to produce a type error\n * when any property is not classified.\n */\ntype AssertAllClassified<T extends Record<string, unknown>> = {\n [K in keyof T]: T[K] extends ClassifiedProperty\n ? T[K]\n : T[K] extends () => ClassifiedProperty\n ? T[K]\n : ClassifiedProperty; // Force error: value not assignable to ClassifiedProperty\n};\n\n/**\n * Metadata descriptor for `defineComplianceEntity`.\n */\ninterface ComplianceEntityMetadata<\n TProperties extends Record<string, unknown>\n> {\n name: string;\n tableName?: string;\n properties: TProperties & AssertAllClassified<TProperties>;\n extends?: unknown;\n primaryKeys?: string[];\n hooks?: Record<string, unknown>;\n repository?: () => unknown;\n forceObject?: boolean;\n inheritance?: 'tpt';\n orderBy?: Record<string, unknown> | Record<string, unknown>[];\n discriminatorColumn?: string;\n versionProperty?: string;\n concurrencyCheckKeys?: Set<string>;\n serializedPrimaryKey?: string;\n indexes?: unknown[];\n uniques?: unknown[];\n}\n\n// ---------------------------------------------------------------------------\n// Runtime helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Read the compliance level from a proxy-wrapped builder.\n * Returns the level if the proxy has `~compliance`, undefined otherwise.\n */\nfunction readComplianceLevel(builder: unknown): ComplianceLevel | undefined {\n if (builder == null || typeof builder !== 'object') return undefined;\n return (builder as Record<string, unknown>)[COMPLIANCE_KEY] as\n | ComplianceLevel\n | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// defineComplianceEntity\n// ---------------------------------------------------------------------------\n\n/**\n * Wrapper around MikroORM's `defineEntity` that enforces compliance\n * classification on every field.\n *\n * - Scalar fields: must call `.compliance(level)` — forgetting it is a\n * compile-time error (TypeScript rejects it) AND a runtime error.\n * - Relation fields: auto-classified as `'none'` by the `fp` builder.\n *\n * Compliance metadata is stored in a module-level registry, accessible via\n * `getComplianceMetadata(entityName, fieldName)`.\n *\n * @example\n * ```typescript\n * const User = defineComplianceEntity({\n * name: 'User',\n * properties: {\n * id: fp.uuid().primary().compliance('none'),\n * email: fp.string().unique().compliance('pii'),\n * medicalRecord: fp.string().nullable().compliance('phi'),\n * organization: () => fp.manyToOne(Organization).nullable(),\n * }\n * });\n * export type User = InferEntity<typeof User>;\n * ```\n */\nexport function defineComplianceEntity<\n TProperties extends Record<string, unknown>\n>(meta: ComplianceEntityMetadata<TProperties>) {\n const { name: entityName, properties, ...rest } = meta;\n const complianceFields = new Map<string, ComplianceLevel>();\n\n // Validate and extract compliance from each property\n for (const [fieldName, rawProp] of Object.entries(properties)) {\n const prop = typeof rawProp === 'function' ? rawProp() : rawProp;\n const level = readComplianceLevel(prop);\n\n if (level == null) {\n throw new Error(\n `Field '${entityName}.${fieldName}' is missing compliance classification. ` +\n `Call .compliance('pii' | 'phi' | 'pci' | 'none') on this property, ` +\n `or use a relation method (fp.manyToOne, etc.) which is auto-classified.`\n );\n }\n complianceFields.set(fieldName, level);\n }\n\n // Store compliance metadata in the global registry\n registerEntityCompliance(entityName, complianceFields);\n\n // Delegate to MikroORM's defineEntity.\n // The Proxy-wrapped builders forward ~options correctly.\n return defineEntity({\n name: entityName,\n properties: properties as Record<string, unknown>,\n ...rest\n } as Parameters<typeof defineEntity>[0]);\n}\n\n// Re-export InferEntity for convenience\nexport type { InferEntity };\n","import crypto from 'crypto';\n\n// ---------------------------------------------------------------------------\n// Error types\n// ---------------------------------------------------------------------------\n\nexport class MissingEncryptionKeyError extends Error {\n readonly name = 'MissingEncryptionKeyError' as const;\n constructor(message = 'Master encryption key must be provided') {\n super(message);\n }\n}\n\nexport class DecryptionError extends Error {\n readonly name = 'DecryptionError' as const;\n constructor(\n message = 'Decryption failed: ciphertext is corrupted or the wrong key was used'\n ) {\n super(message);\n }\n}\n\nexport class EncryptionRequiredError extends Error {\n readonly name = 'EncryptionRequiredError' as const;\n constructor(\n message = 'Encryption is required before persisting this compliance field'\n ) {\n super(message);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst ALGORITHM = 'aes-256-gcm' as const;\nconst IV_BYTES = 12;\nconst KEY_BYTES = 32;\nconst HKDF_HASH = 'sha256' as const;\nconst HKDF_SALT = Buffer.alloc(0); // empty salt – key material is already high-entropy\n\n// ---------------------------------------------------------------------------\n// FieldEncryptor\n// ---------------------------------------------------------------------------\n\nexport class FieldEncryptor {\n private readonly masterKey: string;\n\n constructor(masterKey: string) {\n if (!masterKey) {\n throw new MissingEncryptionKeyError();\n }\n this.masterKey = masterKey;\n }\n\n /**\n * Derive a per-tenant 32-byte key using HKDF-SHA256.\n * The master key is used as input key material and the tenantId as info context.\n */\n deriveKey(tenantId: string): Buffer {\n return Buffer.from(\n crypto.hkdfSync(HKDF_HASH, this.masterKey, HKDF_SALT, tenantId, KEY_BYTES)\n );\n }\n\n /**\n * Encrypt a plaintext string for a specific tenant.\n *\n * @returns Format: `v1:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`\n */\n encrypt(plaintext: string | null): string | null;\n encrypt(plaintext: string | null, tenantId: string): string | null;\n encrypt(plaintext: string | null, tenantId?: string): string | null {\n if (plaintext === null || plaintext === undefined) {\n return null;\n }\n\n const key = this.deriveKey(tenantId ?? '');\n const iv = crypto.randomBytes(IV_BYTES);\n\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final()\n ]);\n const authTag = cipher.getAuthTag();\n\n return [\n 'v1',\n iv.toString('base64'),\n authTag.toString('base64'),\n encrypted.toString('base64')\n ].join(':');\n }\n\n /**\n * Decrypt a ciphertext string produced by {@link encrypt}.\n */\n decrypt(ciphertext: string | null): string | null;\n decrypt(ciphertext: string | null, tenantId: string): string | null;\n decrypt(ciphertext: string | null, tenantId?: string): string | null {\n if (ciphertext === null || ciphertext === undefined) {\n return null;\n }\n\n const parts = ciphertext.split(':');\n if (parts.length !== 4 || parts[0] !== 'v1') {\n throw new DecryptionError(\n `Unknown ciphertext version or malformed format`\n );\n }\n\n const [, ivB64, authTagB64, encryptedB64] = parts;\n const iv = Buffer.from(ivB64, 'base64');\n const authTag = Buffer.from(authTagB64, 'base64');\n const encrypted = Buffer.from(encryptedB64, 'base64');\n const key = this.deriveKey(tenantId ?? '');\n\n try {\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final()\n ]);\n return decrypted.toString('utf8');\n } catch {\n throw new DecryptionError();\n }\n }\n}\n","import type {\n EntityManager,\n EventArgs,\n EventSubscriber\n} from '@mikro-orm/core';\nimport {\n DecryptionError,\n EncryptionRequiredError,\n FieldEncryptor\n} from '../encryption/fieldEncryptor';\nimport {\n type ComplianceLevel,\n getEntityComplianceFields\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst ENCRYPTED_PREFIX = 'v1:';\n\n/**\n * Compliance levels that require field-level encryption.\n * PII is NOT encrypted (RDS encryption + TLS sufficient).\n */\nconst ENCRYPTED_LEVELS: ReadonlySet<ComplianceLevel> = new Set(['phi', 'pci']);\n\n// ---------------------------------------------------------------------------\n// ComplianceEventSubscriber\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM EventSubscriber that enforces field-level encryption for\n * compliance-classified fields (PHI and PCI).\n *\n * - **onBeforeCreate / onBeforeUpdate**: Encrypts PHI/PCI fields before\n * database persistence. Throws `EncryptionRequiredError` if the encryption\n * key is unavailable.\n * - **onLoad**: Decrypts PHI/PCI fields after loading from the database.\n * Pre-migration plaintext (no `v1:` prefix) is returned as-is with a\n * console warning to support rolling deployments.\n *\n * The tenant ID for key derivation is read from the EntityManager's filter\n * parameters (set by the tenant context middleware).\n */\nexport class ComplianceEventSubscriber implements EventSubscriber {\n private readonly encryptor: FieldEncryptor;\n\n constructor(encryptor: FieldEncryptor) {\n this.encryptor = encryptor;\n }\n\n async beforeCreate(args: EventArgs<unknown>): Promise<void> {\n this.encryptFields(args);\n }\n\n async beforeUpdate(args: EventArgs<unknown>): Promise<void> {\n this.encryptFields(args);\n }\n\n async onLoad(args: EventArgs<unknown>): Promise<void> {\n this.decryptFields(args);\n }\n\n // ---------------------------------------------------------------------------\n // Encrypt on persist\n // ---------------------------------------------------------------------------\n\n private encryptFields(args: EventArgs<unknown>): void {\n const entityName = args.meta.className;\n const complianceFields = getEntityComplianceFields(entityName);\n if (!complianceFields) return;\n\n const tenantId = this.getTenantId(args.em);\n const entity = args.entity as Record<string, unknown>;\n\n for (const [fieldName, level] of complianceFields) {\n if (!ENCRYPTED_LEVELS.has(level)) continue;\n\n const value = entity[fieldName];\n if (value === null || value === undefined) continue;\n if (typeof value !== 'string') continue;\n\n // Don't double-encrypt\n if (value.startsWith(ENCRYPTED_PREFIX)) continue;\n\n entity[fieldName] = this.encryptor.encrypt(value, tenantId);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Decrypt on load\n // ---------------------------------------------------------------------------\n\n private decryptFields(args: EventArgs<unknown>): void {\n const entityName = args.meta.className;\n const complianceFields = getEntityComplianceFields(entityName);\n if (!complianceFields) return;\n\n const tenantId = this.getTenantId(args.em);\n const entity = args.entity as Record<string, unknown>;\n\n for (const [fieldName, level] of complianceFields) {\n if (!ENCRYPTED_LEVELS.has(level)) continue;\n\n const value = entity[fieldName];\n if (value === null || value === undefined) continue;\n if (typeof value !== 'string') continue;\n\n if (value.startsWith(ENCRYPTED_PREFIX)) {\n // Encrypted — decrypt it\n try {\n entity[fieldName] = this.encryptor.decrypt(value, tenantId);\n } catch (err) {\n if (err instanceof DecryptionError) {\n throw new DecryptionError(\n `Failed to decrypt ${entityName}.${fieldName}: ${err.message}`\n );\n }\n throw err;\n }\n } else {\n // Pre-migration plaintext — return as-is, log warning\n console.warn(\n `[compliance] ${entityName}.${fieldName} contains unencrypted ${level} data. ` +\n `Run encryption migration to encrypt existing data.`\n );\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Tenant ID resolution\n // ---------------------------------------------------------------------------\n\n /**\n * Read the tenant ID from the EntityManager's filter parameters.\n * The tenant context middleware sets this when forking the EM per request.\n */\n private getTenantId(em: EntityManager): string {\n const filters = em.getFilterParams('tenant') as\n | { tenantId?: string }\n | undefined;\n const tenantId = filters?.tenantId;\n if (!tenantId) {\n throw new EncryptionRequiredError(\n 'Cannot encrypt/decrypt without tenant context. ' +\n 'Ensure the tenant filter is set on the EntityManager.'\n );\n }\n return tenantId;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Native query blocking\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and\n * `nativeDelete` on entities that have PHI or PCI compliance fields.\n *\n * This prevents bypassing the ComplianceEventSubscriber's encryption\n * by using raw queries. Call this in the tenant context middleware when\n * creating the request-scoped EM.\n *\n * @returns A Proxy-wrapped EntityManager that throws on native query\n * operations targeting compliance entities.\n */\nexport function wrapEmWithNativeQueryBlocking<T extends EntityManager>(\n em: T\n): T {\n const BLOCKED_METHODS = [\n 'nativeInsert',\n 'nativeUpdate',\n 'nativeDelete'\n ] as const;\n\n return new Proxy(em, {\n get(target, prop, receiver) {\n if (\n typeof prop === 'string' &&\n BLOCKED_METHODS.includes(prop as (typeof BLOCKED_METHODS)[number])\n ) {\n return (entityNameOrEntity: unknown, ...rest: unknown[]) => {\n const entityName = resolveEntityName(entityNameOrEntity);\n if (entityName) {\n const fields = getEntityComplianceFields(entityName);\n if (fields) {\n for (const [fieldName, level] of fields) {\n if (ENCRYPTED_LEVELS.has(level)) {\n throw new EncryptionRequiredError(\n `${prop}() blocked on entity '${entityName}' because field ` +\n `'${fieldName}' has compliance level '${level}'. ` +\n `Use em.create() + em.flush() instead to ensure encryption.`\n );\n }\n }\n }\n }\n // No compliance fields requiring encryption — allow the native query\n const method = Reflect.get(target, prop, receiver);\n return (method as (...args: unknown[]) => unknown).call(\n target,\n entityNameOrEntity,\n ...rest\n );\n };\n }\n return Reflect.get(target, prop, receiver);\n }\n });\n}\n\n/**\n * Resolve an entity name from the first argument to nativeInsert/Update/Delete.\n * MikroORM accepts entity name strings, entity class references, or entity instances.\n */\nfunction resolveEntityName(entityNameOrEntity: unknown): string | undefined {\n if (typeof entityNameOrEntity === 'string') {\n return entityNameOrEntity;\n }\n if (typeof entityNameOrEntity === 'function') {\n return (entityNameOrEntity as { name?: string }).name;\n }\n if (\n entityNameOrEntity != null &&\n typeof entityNameOrEntity === 'object' &&\n 'constructor' in entityNameOrEntity\n ) {\n return (entityNameOrEntity.constructor as { name?: string }).name;\n }\n return undefined;\n}\n","import type {\n Dictionary,\n EntityManager,\n FilterDef,\n MikroORM\n} from '@mikro-orm/core';\n\n/**\n * The name used to register the tenant isolation filter.\n */\nexport const TENANT_FILTER_NAME = 'tenant';\n\n/**\n * Creates the tenant filter definition.\n *\n * The filter adds `WHERE organizationId = :tenantId` to all queries\n * on entities that have an `organizationId` or `organization` property.\n * Entities without either property are unaffected (empty condition).\n */\nexport function createTenantFilterDef(): FilterDef {\n return {\n name: TENANT_FILTER_NAME,\n cond(\n args: Dictionary,\n _type: 'read' | 'update' | 'delete',\n em: EntityManager,\n _options?: unknown,\n entityName?: string\n ) {\n if (!entityName) {\n return {};\n }\n\n try {\n const metadata = em.getMetadata().getByClassName(entityName, false);\n if (!metadata) {\n return {};\n }\n\n const hasOrganizationId = metadata.properties['organizationId'] != null;\n const hasOrganization = metadata.properties['organization'] != null;\n\n if (hasOrganizationId || hasOrganization) {\n return { organizationId: args.tenantId };\n }\n } catch {\n // Entity not found in metadata — skip filtering\n }\n\n return {};\n },\n default: true,\n args: true\n };\n}\n\n/**\n * Registers the global tenant isolation filter on the ORM's entity manager.\n * Call this once at application bootstrap after `MikroORM.init()`.\n *\n * After calling this, every fork of the EM will inherit the filter.\n * Set the tenant ID per-request via:\n *\n * ```ts\n * em.setFilterParams('tenant', { tenantId: 'org-123' });\n * ```\n */\nexport function setupTenantFilter(orm: MikroORM): void {\n orm.em.addFilter(createTenantFilterDef());\n}\n\n/**\n * Returns a forked EntityManager with the tenant filter disabled.\n *\n * Use this only from code paths that have verified super-admin permissions.\n * Queries executed through the returned EM will return cross-tenant data.\n */\nexport function getSuperAdminContext(em: EntityManager): EntityManager {\n const forked = em.fork();\n forked.setFilterParams(TENANT_FILTER_NAME, { tenantId: undefined });\n // Disable the filter by passing false for the filter in each query isn't\n // sufficient globally; instead we add the filter with enabled = false.\n // The cleanest way is to re-add the filter as disabled on this fork.\n forked.addFilter({\n name: TENANT_FILTER_NAME,\n cond: {},\n default: false\n });\n return forked;\n}\n","import type {\n EntityManager,\n EventSubscriber,\n MikroORM,\n TransactionEventArgs\n} from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface RlsConfig {\n /**\n * Whether to enable PostgreSQL Row-Level Security.\n * Defaults to `true` when the driver is PostgreSQL, `false` otherwise.\n * Set to `false` to opt out even on PostgreSQL.\n */\n enabled?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// RLS EventSubscriber\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM EventSubscriber that executes `SET LOCAL app.tenant_id = :tenantId`\n * at the start of every transaction when PostgreSQL RLS is enabled.\n *\n * This ensures that even if the MikroORM global filter is somehow bypassed,\n * the database-level RLS policy enforces tenant isolation.\n *\n * The tenant ID is read from the EntityManager's filter parameters\n * (set by the tenant context middleware).\n */\nexport class RlsEventSubscriber implements EventSubscriber {\n async afterTransactionStart(args: TransactionEventArgs): Promise<void> {\n const tenantId = this.getTenantId(args.em);\n if (!tenantId) {\n // No tenant context (e.g., super-admin or public route) — skip SET LOCAL\n return;\n }\n\n const connection = args.em.getConnection();\n // Execute SET LOCAL within the transaction context\n // SET LOCAL only persists for the current transaction — no connection leakage\n await connection.execute(\n `SET LOCAL app.tenant_id = '${escapeSqlString(tenantId)}'`,\n [],\n 'run',\n args.transaction\n );\n }\n\n private getTenantId(em: EntityManager): string | undefined {\n const params = em.getFilterParams(TENANT_FILTER_NAME) as\n | { tenantId?: string }\n | undefined;\n return params?.tenantId;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Setup\n// ---------------------------------------------------------------------------\n\n/**\n * Sets up PostgreSQL Row-Level Security integration.\n *\n * 1. Registers the `RlsEventSubscriber` to run `SET LOCAL app.tenant_id`\n * at the start of every transaction.\n * 2. Validates that RLS policies exist on tenant-scoped tables (warns if missing).\n *\n * Call this at application bootstrap after `MikroORM.init()` and `setupTenantFilter()`.\n *\n * @param orm - The initialized MikroORM instance\n * @param config - RLS configuration (enabled defaults to auto-detect PostgreSQL)\n */\nexport function setupRls(orm: MikroORM, config: RlsConfig = {}): void {\n const isPostgres = isPostgresDriver(orm);\n const enabled = config.enabled ?? isPostgres;\n\n if (!enabled) {\n if (!isPostgres) {\n // Non-PostgreSQL — RLS not available, ORM filter is the sole enforcement\n return;\n }\n // PostgreSQL but explicitly disabled\n console.info(\n '[compliance] PostgreSQL RLS disabled by configuration. ORM filter is the sole tenant enforcement layer.'\n );\n return;\n }\n\n if (!isPostgres) {\n console.warn(\n '[compliance] RLS enabled but database driver is not PostgreSQL. ' +\n 'RLS is only supported on PostgreSQL. Falling back to ORM filter only.'\n );\n return;\n }\n\n // Register the RLS transaction subscriber\n orm.em.getEventManager().registerSubscriber(new RlsEventSubscriber());\n\n // Validate RLS policies exist\n validateRlsPolicies(orm).catch((err) => {\n console.warn('[compliance] Failed to validate RLS policies:', err);\n });\n}\n\n// ---------------------------------------------------------------------------\n// RLS policy validation\n// ---------------------------------------------------------------------------\n\n/**\n * Checks that tenant-scoped entities have RLS policies on their tables.\n * Logs warnings with the SQL needed to create missing policies.\n */\nasync function validateRlsPolicies(orm: MikroORM): Promise<void> {\n const metadata = orm.em.getMetadata().getAll();\n\n for (const meta of Object.values(metadata)) {\n const hasOrgId = meta.properties['organizationId'] != null;\n const hasOrg = meta.properties['organization'] != null;\n\n if (!hasOrgId && !hasOrg) continue;\n\n const tableName = meta.tableName;\n try {\n const connection = orm.em.getConnection();\n const result = await connection.execute<{ policyname: string }[]>(\n `SELECT policyname FROM pg_policies WHERE tablename = '${escapeSqlString(tableName)}'`,\n [],\n 'all'\n );\n\n const policies = Array.isArray(result) ? result : [];\n const hasTenantPolicy = policies.some((p: { policyname: string }) =>\n p.policyname.includes('tenant')\n );\n\n if (!hasTenantPolicy) {\n console.warn(\n `[compliance] No tenant RLS policy found on table '${tableName}'. ` +\n `Create one with:\\n` +\n ` ALTER TABLE \"${tableName}\" ENABLE ROW LEVEL SECURITY;\\n` +\n ` CREATE POLICY tenant_isolation ON \"${tableName}\"\\n` +\n ` USING (organization_id = current_setting('app.tenant_id'));`\n );\n }\n } catch {\n // Query failed — likely not connected yet or table doesn't exist\n // Skip validation for this table\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the ORM is using a PostgreSQL driver.\n * Checks the platform constructor name which is 'PostgreSqlPlatform' for PG.\n */\nfunction isPostgresDriver(orm: MikroORM): boolean {\n try {\n const platform = orm.em.getPlatform();\n const name = platform.constructor.name.toLowerCase();\n return name.includes('postgre');\n } catch {\n return false;\n }\n}\n\n/**\n * Escape a string for safe inclusion in SQL. Prevents SQL injection in\n * the SET LOCAL statement.\n */\nfunction escapeSqlString(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n"],"mappings":"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","names":["p"]}
|
|
1
|
+
{"version":3,"sources":["../../src/persistence/complianceTypes.ts","../../src/persistence/compliancePropertyBuilder.ts","../../src/persistence/defineComplianceEntity.ts","../../src/encryption/fieldEncryptor.ts","../../src/persistence/complianceEventSubscriber.ts","../../src/persistence/tenantFilter.ts","../../src/persistence/rls.ts"],"sourcesContent":["import type {\n PropertyBuilders,\n PropertyChain,\n UniversalPropertyOptionsBuilder\n} from '@mikro-orm/core';\n\n/**\n * Classification levels for entity field compliance.\n * Drives encryption (phi/pci), audit log redaction (all non-none), and compliance reporting.\n */\nexport const ComplianceLevel = {\n pii: 'pii',\n phi: 'phi',\n pci: 'pci',\n none: 'none'\n} as const;\nexport type ComplianceLevel =\n (typeof ComplianceLevel)[keyof typeof ComplianceLevel];\n\n/**\n * Brand symbol — makes ClassifiedProperty structurally distinct from\n * plain PropertyChain at the TypeScript level.\n */\ndeclare const CLASSIFIED: unique symbol;\n\n/**\n * Brand-only type used by `AssertAllClassified` to check that every\n * property has been classified. Both `ClassifiedScalarProperty` and\n * `ClassifiedRelationChain` extend this.\n */\nexport interface ClassifiedProperty {\n readonly [CLASSIFIED]: true;\n}\n\n/**\n * A scalar property classified via `.compliance()`.\n * Extends `PropertyChain<Value, Options>` so that `defineEntity` (and\n * therefore `InferEntity`) can still read the value/options types for\n * entity type inference. The `[CLASSIFIED]` brand prevents unclassified\n * properties from being accepted by `defineComplianceEntity`.\n */\nexport type ClassifiedScalarProperty<Value, Options> = PropertyChain<\n Value,\n Options\n> &\n ClassifiedProperty;\n\n/**\n * Internal key used by the runtime Proxy to store compliance level\n * on the builder instance. Not part of the public API.\n */\nexport const COMPLIANCE_KEY = '~compliance' as const;\n\n// ---------------------------------------------------------------------------\n// Compliance metadata registry\n// ---------------------------------------------------------------------------\n\n/** entityName → (fieldName → ComplianceLevel) */\nconst complianceRegistry = new Map<string, Map<string, ComplianceLevel>>();\n\n/**\n * Register compliance metadata for an entity's fields.\n * Called by `defineComplianceEntity` during entity definition.\n */\nexport function registerEntityCompliance(\n entityName: string,\n fields: Map<string, ComplianceLevel>\n): void {\n complianceRegistry.set(entityName, fields);\n}\n\n/**\n * Look up the compliance level for a single field on an entity.\n * Returns `'none'` if the entity or field is not registered.\n */\nexport function getComplianceMetadata(\n entityName: string,\n fieldName: string\n): ComplianceLevel {\n return complianceRegistry.get(entityName)?.get(fieldName) ?? 'none';\n}\n\n/**\n * Get all compliance fields for an entity.\n * Returns undefined if the entity is not registered.\n */\nexport function getEntityComplianceFields(\n entityName: string\n): Map<string, ComplianceLevel> | undefined {\n return complianceRegistry.get(entityName);\n}\n\n/**\n * Check whether an entity has any fields requiring encryption (phi or pci).\n */\nexport function entityHasEncryptedFields(entityName: string): boolean {\n const fields = complianceRegistry.get(entityName);\n if (!fields) return false;\n for (const level of fields.values()) {\n if (level === 'phi' || level === 'pci') return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyChain — remapped PropertyChain that preserves\n// `.compliance()` through method chaining.\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively remaps every method on PropertyChain<V,O> so those returning\n * PropertyChain<V2,O2> instead return ForklaunchPropertyChain<V2,O2>.\n * This preserves the `.compliance()` method through chained calls like\n * `.nullable().unique()`.\n */\n\nexport interface ForklaunchPropertyChain<Value, Options> extends RemapReturns<\n Value,\n Options\n> {\n /**\n * Classify this field's compliance level. Must be called on every scalar\n * field passed to `defineComplianceEntity`.\n * Returns a `ClassifiedScalarProperty` that preserves the PropertyChain\n * type info for `InferEntity` to work.\n */\n compliance(level: ComplianceLevel): ClassifiedScalarProperty<Value, Options>;\n}\n\ntype RemapReturns<Value, Options> = {\n [K in keyof PropertyChain<Value, Options>]: PropertyChain<\n Value,\n Options\n >[K] extends (...args: infer A) => PropertyChain<infer V2, infer O2>\n ? (...args: A) => ForklaunchPropertyChain<V2, O2>\n : PropertyChain<Value, Options>[K];\n};\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyBuilders — the type of `fp`\n// ---------------------------------------------------------------------------\n\n/**\n * Keys on PropertyBuilders that return relation builders.\n * These are auto-classified as 'none' — the fp proxy wraps them\n * to return ClassifiedProperty directly.\n */\ntype RelationBuilderKeys =\n | 'manyToOne'\n | 'oneToMany'\n | 'manyToMany'\n | 'oneToOne'\n | 'embedded';\n\n/**\n * The type of `fp` — mirrors `PropertyBuilders` but:\n * - Scalar methods return `ForklaunchPropertyChain` (must call `.compliance()`)\n * - Relation methods return `ClassifiedProperty` directly (auto 'none')\n */\nexport type ForklaunchPropertyBuilders = {\n [K in Exclude<\n keyof PropertyBuilders,\n RelationBuilderKeys\n >]: PropertyBuilders[K] extends (\n ...args: infer A\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n ) => UniversalPropertyOptionsBuilder<infer V, infer O, infer _IK>\n ? (...args: A) => ForklaunchPropertyChain<V, O>\n : PropertyBuilders[K] extends (\n ...args: infer A\n ) => PropertyChain<infer V, infer O>\n ? (...args: A) => ForklaunchPropertyChain<V, O>\n : PropertyBuilders[K];\n} & {\n [K in RelationBuilderKeys]: PropertyBuilders[K] extends (\n ...args: infer A\n ) => PropertyChain<infer V, infer O>\n ? (...args: A) => ClassifiedRelationChain<V, O>\n : PropertyBuilders[K];\n};\n\n/**\n * A relation builder that is already classified (as 'none') but still\n * supports chaining relation-specific methods like `.mappedBy()`, `.nullable()`.\n * All chain methods return ClassifiedRelationChain (preserving the brand).\n */\nexport type ClassifiedRelationChain<Value, Options> = {\n [K in keyof PropertyChain<Value, Options>]: PropertyChain<\n Value,\n Options\n >[K] extends (...args: infer A) => PropertyChain<infer V2, infer O2>\n ? (...args: A) => ClassifiedRelationChain<V2, O2>\n : PropertyChain<Value, Options>[K];\n} & ClassifiedProperty;\n","import { p } from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n type ComplianceLevel,\n type ForklaunchPropertyBuilders\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Runtime Proxy implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a value is a MikroORM property builder (has ~options).\n */\nfunction isBuilder(value: unknown): value is object {\n return (\n value != null &&\n typeof value === 'object' &&\n '~options' in (value as Record<string, unknown>)\n );\n}\n\n/**\n * Wraps a MikroORM scalar PropertyBuilder in a Proxy that:\n * 1. Adds a `.compliance(level)` method\n * 2. Forwards all other method calls to the underlying builder\n * 3. Re-wraps returned builders so `.compliance()` persists through chains\n */\nfunction wrapUnclassified(builder: unknown): unknown {\n return new Proxy(builder as object, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === 'compliance') {\n return (level: ComplianceLevel) => wrapClassified(target, level);\n }\n if (prop === '~options') return Reflect.get(target, prop, target);\n if (prop === COMPLIANCE_KEY) return undefined;\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a builder that has been classified via `.compliance()`.\n * Stores the compliance level under `~compliance` for `defineComplianceEntity`.\n * Chaining after `.compliance()` propagates the level through subsequent builders.\n */\nfunction wrapClassified(builder: object, level: ComplianceLevel): unknown {\n return new Proxy(builder, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === COMPLIANCE_KEY) return level;\n if (prop === '~options') return Reflect.get(target, prop, target);\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapClassified(result, level) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a relation PropertyBuilder (manyToOne, oneToMany, etc.).\n * Auto-classified as 'none' — no `.compliance()` call needed.\n * All chained methods preserve the auto-classification.\n */\nfunction wrapRelation(builder: object): unknown {\n return wrapClassified(builder, 'none');\n}\n\n// ---------------------------------------------------------------------------\n// Relation method detection\n// ---------------------------------------------------------------------------\n\nconst RELATION_METHODS = new Set([\n 'manyToOne',\n 'oneToMany',\n 'manyToMany',\n 'oneToOne',\n 'embedded'\n]);\n\nfunction isRelationMethod(prop: string | symbol): boolean {\n return typeof prop === 'string' && RELATION_METHODS.has(prop);\n}\n\n// ---------------------------------------------------------------------------\n// fp — the ForkLaunch property builder\n// ---------------------------------------------------------------------------\n\n/**\n * ForkLaunch property builder. Drop-in replacement for MikroORM's `p`\n * that adds `.compliance(level)` to every scalar property builder\n * and auto-classifies relation builders as 'none'.\n *\n * - Scalar fields: `fp.string().compliance('pii')` — must call `.compliance()`\n * - Relation fields: `fp.manyToOne(Target)` — auto-classified, no `.compliance()` needed\n *\n * @example\n * ```typescript\n * import { defineComplianceEntity, fp } from '@forklaunch/core/persistence';\n *\n * const User = defineComplianceEntity({\n * name: 'User',\n * properties: {\n * id: fp.uuid().primary().compliance('none'),\n * email: fp.string().unique().compliance('pii'),\n * medicalRecord: fp.string().nullable().compliance('phi'),\n * organization: () => fp.manyToOne(Organization).nullable(),\n * }\n * });\n * ```\n */\nexport const fp: ForklaunchPropertyBuilders = new Proxy(p, {\n get(target: Record<string | symbol, unknown>, prop) {\n const value = Reflect.get(target, prop, target);\n if (typeof value !== 'function') return value;\n\n if (isRelationMethod(prop)) {\n // Relation methods: call the original, wrap result as auto-classified 'none'\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapRelation(result) : result;\n };\n }\n\n // Scalar methods: call the original, wrap result with .compliance()\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n}) as ForklaunchPropertyBuilders;\n","import {\n defineEntity,\n p,\n type EntityMetadataWithProperties\n} from '@mikro-orm/core';\nimport type { InferEntity } from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n type ClassifiedProperty,\n type ComplianceLevel,\n registerEntityCompliance\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Checks each property in TProperties extends ClassifiedProperty.\n * Properties that don't have the [CLASSIFIED] brand are mapped to\n * ClassifiedProperty, causing a type error at the call site.\n */\ntype AssertAllClassified<T extends Record<string, unknown>> = {\n [K in keyof T]: T[K] extends ClassifiedProperty\n ? T[K]\n : T[K] extends () => ClassifiedProperty\n ? T[K]\n : ClassifiedProperty;\n};\n\n// ---------------------------------------------------------------------------\n// Runtime helpers\n// ---------------------------------------------------------------------------\n\nfunction readComplianceLevel(builder: unknown): ComplianceLevel | undefined {\n if (builder == null || typeof builder !== 'object') return undefined;\n return (builder as Record<string, unknown>)[COMPLIANCE_KEY] as\n | ComplianceLevel\n | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// defineComplianceEntity\n// ---------------------------------------------------------------------------\n\n/**\n * Wrapper around MikroORM's `defineEntity` that enforces compliance\n * classification on every field at both compile-time and runtime.\n *\n * The return type is inferred directly from `defineEntity` — `InferEntity`\n * works because `ClassifiedScalarProperty<V,O>` extends `PropertyChain<V,O>`.\n *\n * @example\n * ```typescript\n * const User = defineComplianceEntity({\n * name: 'User',\n * properties: {\n * id: fp.uuid().primary().compliance('none'),\n * email: fp.string().unique().compliance('pii'),\n * organization: () => fp.manyToOne(Organization).nullable(),\n * }\n * });\n * export type User = InferEntity<typeof User>;\n * ```\n */\nexport function defineComplianceEntity<\n const TName extends string,\n const TTableName extends string,\n const TProperties extends Record<string, ClassifiedProperty>,\n const TPK extends (keyof TProperties)[] | undefined = undefined,\n const TBase = never,\n const TRepository = never,\n const TForceObject extends boolean = false\n>(\n meta: EntityMetadataWithProperties<\n TName,\n TTableName,\n TProperties,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >\n) {\n const entityName = meta.name;\n const complianceFields = new Map<string, ComplianceLevel>();\n\n // Resolve properties — meta.properties can be an object or factory function\n const rawProperties = meta.properties;\n const properties: Record<string, unknown> =\n typeof rawProperties === 'function' ? rawProperties(p) : rawProperties;\n\n // Validate and extract compliance from each property\n for (const [fieldName, rawProp] of Object.entries(properties)) {\n const prop = typeof rawProp === 'function' ? rawProp() : rawProp;\n const level = readComplianceLevel(prop);\n\n if (level == null) {\n throw new Error(\n `Field '${entityName}.${fieldName}' is missing compliance classification. ` +\n `Call .compliance('pii' | 'phi' | 'pci' | 'none') on this property, ` +\n `or use a relation method (fp.manyToOne, etc.) which is auto-classified.`\n );\n }\n complianceFields.set(fieldName, level);\n }\n\n // Store compliance metadata in the global registry\n registerEntityCompliance(entityName, complianceFields);\n\n // Pass through to defineEntity — ClassifiedScalarProperty<V,O> extends\n // PropertyChain<V,O> so MikroORM's type inference works correctly.\n return defineEntity(meta);\n}\n\n// Re-export InferEntity for convenience\nexport type { InferEntity };\n","import crypto from 'crypto';\n\n// ---------------------------------------------------------------------------\n// Error types\n// ---------------------------------------------------------------------------\n\nexport class MissingEncryptionKeyError extends Error {\n readonly name = 'MissingEncryptionKeyError' as const;\n constructor(message = 'Master encryption key must be provided') {\n super(message);\n }\n}\n\nexport class DecryptionError extends Error {\n readonly name = 'DecryptionError' as const;\n constructor(\n message = 'Decryption failed: ciphertext is corrupted or the wrong key was used'\n ) {\n super(message);\n }\n}\n\nexport class EncryptionRequiredError extends Error {\n readonly name = 'EncryptionRequiredError' as const;\n constructor(\n message = 'Encryption is required before persisting this compliance field'\n ) {\n super(message);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst ALGORITHM = 'aes-256-gcm' as const;\nconst IV_BYTES = 12;\nconst KEY_BYTES = 32;\nconst HKDF_HASH = 'sha256' as const;\nconst HKDF_SALT = Buffer.alloc(0); // empty salt – key material is already high-entropy\n\n// ---------------------------------------------------------------------------\n// FieldEncryptor\n// ---------------------------------------------------------------------------\n\nexport class FieldEncryptor {\n private readonly masterKey: string;\n\n constructor(masterKey: string) {\n if (!masterKey) {\n throw new MissingEncryptionKeyError();\n }\n this.masterKey = masterKey;\n }\n\n /**\n * Derive a per-tenant 32-byte key using HKDF-SHA256.\n * The master key is used as input key material and the tenantId as info context.\n */\n deriveKey(tenantId: string): Buffer {\n return Buffer.from(\n crypto.hkdfSync(HKDF_HASH, this.masterKey, HKDF_SALT, tenantId, KEY_BYTES)\n );\n }\n\n /**\n * Encrypt a plaintext string for a specific tenant.\n *\n * @returns Format: `v1:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`\n */\n encrypt(plaintext: string | null): string | null;\n encrypt(plaintext: string | null, tenantId: string): string | null;\n encrypt(plaintext: string | null, tenantId?: string): string | null {\n if (plaintext === null || plaintext === undefined) {\n return null;\n }\n\n const key = this.deriveKey(tenantId ?? '');\n const iv = crypto.randomBytes(IV_BYTES);\n\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv);\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf8'),\n cipher.final()\n ]);\n const authTag = cipher.getAuthTag();\n\n return [\n 'v1',\n iv.toString('base64'),\n authTag.toString('base64'),\n encrypted.toString('base64')\n ].join(':');\n }\n\n /**\n * Decrypt a ciphertext string produced by {@link encrypt}.\n */\n decrypt(ciphertext: string | null): string | null;\n decrypt(ciphertext: string | null, tenantId: string): string | null;\n decrypt(ciphertext: string | null, tenantId?: string): string | null {\n if (ciphertext === null || ciphertext === undefined) {\n return null;\n }\n\n const parts = ciphertext.split(':');\n if (parts.length !== 4 || parts[0] !== 'v1') {\n throw new DecryptionError(\n `Unknown ciphertext version or malformed format`\n );\n }\n\n const [, ivB64, authTagB64, encryptedB64] = parts;\n const iv = Buffer.from(ivB64, 'base64');\n const authTag = Buffer.from(authTagB64, 'base64');\n const encrypted = Buffer.from(encryptedB64, 'base64');\n const key = this.deriveKey(tenantId ?? '');\n\n try {\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final()\n ]);\n return decrypted.toString('utf8');\n } catch {\n throw new DecryptionError();\n }\n }\n}\n","import type {\n EntityManager,\n EventArgs,\n EventSubscriber\n} from '@mikro-orm/core';\nimport {\n DecryptionError,\n EncryptionRequiredError,\n FieldEncryptor\n} from '../encryption/fieldEncryptor';\nimport {\n type ComplianceLevel,\n getEntityComplianceFields\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst ENCRYPTED_PREFIX = 'v1:';\n\n/**\n * Compliance levels that require field-level encryption.\n * PII is NOT encrypted (RDS encryption + TLS sufficient).\n */\nconst ENCRYPTED_LEVELS: ReadonlySet<ComplianceLevel> = new Set(['phi', 'pci']);\n\n// ---------------------------------------------------------------------------\n// ComplianceEventSubscriber\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM EventSubscriber that enforces field-level encryption for\n * compliance-classified fields (PHI and PCI).\n *\n * - **onBeforeCreate / onBeforeUpdate**: Encrypts PHI/PCI fields before\n * database persistence. Throws `EncryptionRequiredError` if the encryption\n * key is unavailable.\n * - **onLoad**: Decrypts PHI/PCI fields after loading from the database.\n * Pre-migration plaintext (no `v1:` prefix) is returned as-is with a\n * console warning to support rolling deployments.\n *\n * The tenant ID for key derivation is read from the EntityManager's filter\n * parameters (set by the tenant context middleware).\n */\nexport class ComplianceEventSubscriber implements EventSubscriber {\n private readonly encryptor: FieldEncryptor;\n\n constructor(encryptor: FieldEncryptor) {\n this.encryptor = encryptor;\n }\n\n async beforeCreate(args: EventArgs<unknown>): Promise<void> {\n this.encryptFields(args);\n }\n\n async beforeUpdate(args: EventArgs<unknown>): Promise<void> {\n this.encryptFields(args);\n }\n\n async onLoad(args: EventArgs<unknown>): Promise<void> {\n this.decryptFields(args);\n }\n\n // ---------------------------------------------------------------------------\n // Encrypt on persist\n // ---------------------------------------------------------------------------\n\n private encryptFields(args: EventArgs<unknown>): void {\n const entityName = args.meta.className;\n const complianceFields = getEntityComplianceFields(entityName);\n if (!complianceFields) return;\n\n const tenantId = this.getTenantId(args.em);\n const entity = args.entity as Record<string, unknown>;\n\n for (const [fieldName, level] of complianceFields) {\n if (!ENCRYPTED_LEVELS.has(level)) continue;\n\n const value = entity[fieldName];\n if (value === null || value === undefined) continue;\n if (typeof value !== 'string') continue;\n\n // Don't double-encrypt\n if (value.startsWith(ENCRYPTED_PREFIX)) continue;\n\n entity[fieldName] = this.encryptor.encrypt(value, tenantId);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Decrypt on load\n // ---------------------------------------------------------------------------\n\n private decryptFields(args: EventArgs<unknown>): void {\n const entityName = args.meta.className;\n const complianceFields = getEntityComplianceFields(entityName);\n if (!complianceFields) return;\n\n const tenantId = this.getTenantId(args.em);\n const entity = args.entity as Record<string, unknown>;\n\n for (const [fieldName, level] of complianceFields) {\n if (!ENCRYPTED_LEVELS.has(level)) continue;\n\n const value = entity[fieldName];\n if (value === null || value === undefined) continue;\n if (typeof value !== 'string') continue;\n\n if (value.startsWith(ENCRYPTED_PREFIX)) {\n // Encrypted — decrypt it\n try {\n entity[fieldName] = this.encryptor.decrypt(value, tenantId);\n } catch (err) {\n if (err instanceof DecryptionError) {\n throw new DecryptionError(\n `Failed to decrypt ${entityName}.${fieldName}: ${err.message}`\n );\n }\n throw err;\n }\n } else {\n // Pre-migration plaintext — return as-is, log warning\n console.warn(\n `[compliance] ${entityName}.${fieldName} contains unencrypted ${level} data. ` +\n `Run encryption migration to encrypt existing data.`\n );\n }\n }\n }\n\n // ---------------------------------------------------------------------------\n // Tenant ID resolution\n // ---------------------------------------------------------------------------\n\n /**\n * Read the tenant ID from the EntityManager's filter parameters.\n * The tenant context middleware sets this when forking the EM per request.\n */\n private getTenantId(em: EntityManager): string {\n const filters = em.getFilterParams('tenant') as\n | { tenantId?: string }\n | undefined;\n const tenantId = filters?.tenantId;\n if (!tenantId) {\n throw new EncryptionRequiredError(\n 'Cannot encrypt/decrypt without tenant context. ' +\n 'Ensure the tenant filter is set on the EntityManager.'\n );\n }\n return tenantId;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Native query blocking\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and\n * `nativeDelete` on entities that have PHI or PCI compliance fields.\n *\n * This prevents bypassing the ComplianceEventSubscriber's encryption\n * by using raw queries. Call this in the tenant context middleware when\n * creating the request-scoped EM.\n *\n * @returns A Proxy-wrapped EntityManager that throws on native query\n * operations targeting compliance entities.\n */\nexport function wrapEmWithNativeQueryBlocking<T extends EntityManager>(\n em: T\n): T {\n const BLOCKED_METHODS = [\n 'nativeInsert',\n 'nativeUpdate',\n 'nativeDelete'\n ] as const;\n\n return new Proxy(em, {\n get(target, prop, receiver) {\n if (\n typeof prop === 'string' &&\n BLOCKED_METHODS.includes(prop as (typeof BLOCKED_METHODS)[number])\n ) {\n return (entityNameOrEntity: unknown, ...rest: unknown[]) => {\n const entityName = resolveEntityName(entityNameOrEntity);\n if (entityName) {\n const fields = getEntityComplianceFields(entityName);\n if (fields) {\n for (const [fieldName, level] of fields) {\n if (ENCRYPTED_LEVELS.has(level)) {\n throw new EncryptionRequiredError(\n `${prop}() blocked on entity '${entityName}' because field ` +\n `'${fieldName}' has compliance level '${level}'. ` +\n `Use em.create() + em.flush() instead to ensure encryption.`\n );\n }\n }\n }\n }\n // No compliance fields requiring encryption — allow the native query\n const method = Reflect.get(target, prop, receiver);\n return (method as (...args: unknown[]) => unknown).call(\n target,\n entityNameOrEntity,\n ...rest\n );\n };\n }\n return Reflect.get(target, prop, receiver);\n }\n });\n}\n\n/**\n * Resolve an entity name from the first argument to nativeInsert/Update/Delete.\n * MikroORM accepts entity name strings, entity class references, or entity instances.\n */\nfunction resolveEntityName(entityNameOrEntity: unknown): string | undefined {\n if (typeof entityNameOrEntity === 'string') {\n return entityNameOrEntity;\n }\n if (typeof entityNameOrEntity === 'function') {\n return (entityNameOrEntity as { name?: string }).name;\n }\n if (\n entityNameOrEntity != null &&\n typeof entityNameOrEntity === 'object' &&\n 'constructor' in entityNameOrEntity\n ) {\n return (entityNameOrEntity.constructor as { name?: string }).name;\n }\n return undefined;\n}\n","import type {\n Dictionary,\n EntityManager,\n FilterDef,\n MikroORM\n} from '@mikro-orm/core';\n\n/**\n * The name used to register the tenant isolation filter.\n */\nexport const TENANT_FILTER_NAME = 'tenant';\n\n/**\n * Creates the tenant filter definition.\n *\n * The filter adds `WHERE organizationId = :tenantId` to all queries\n * on entities that have an `organizationId` or `organization` property.\n * Entities without either property are unaffected (empty condition).\n */\nexport function createTenantFilterDef(): FilterDef {\n return {\n name: TENANT_FILTER_NAME,\n cond(\n args: Dictionary,\n _type: 'read' | 'update' | 'delete',\n em: EntityManager,\n _options?: unknown,\n entityName?: string\n ) {\n if (!entityName) {\n return {};\n }\n\n try {\n const metadata = em.getMetadata().getByClassName(entityName, false);\n if (!metadata) {\n return {};\n }\n\n const hasOrganizationId = metadata.properties['organizationId'] != null;\n const hasOrganization = metadata.properties['organization'] != null;\n\n if (hasOrganizationId || hasOrganization) {\n return { organizationId: args.tenantId };\n }\n } catch {\n // Entity not found in metadata — skip filtering\n }\n\n return {};\n },\n default: true,\n args: true\n };\n}\n\n/**\n * Registers the global tenant isolation filter on the ORM's entity manager.\n * Call this once at application bootstrap after `MikroORM.init()`.\n *\n * After calling this, every fork of the EM will inherit the filter.\n * Set the tenant ID per-request via:\n *\n * ```ts\n * em.setFilterParams('tenant', { tenantId: 'org-123' });\n * ```\n */\nexport function setupTenantFilter(orm: MikroORM): void {\n orm.em.addFilter(createTenantFilterDef());\n}\n\n/**\n * Returns a forked EntityManager with the tenant filter disabled.\n *\n * Use this only from code paths that have verified super-admin permissions.\n * Queries executed through the returned EM will return cross-tenant data.\n */\nexport function getSuperAdminContext(em: EntityManager): EntityManager {\n const forked = em.fork();\n forked.setFilterParams(TENANT_FILTER_NAME, { tenantId: undefined });\n // Disable the filter by passing false for the filter in each query isn't\n // sufficient globally; instead we add the filter with enabled = false.\n // The cleanest way is to re-add the filter as disabled on this fork.\n forked.addFilter({\n name: TENANT_FILTER_NAME,\n cond: {},\n default: false\n });\n return forked;\n}\n","import type {\n EntityManager,\n EventSubscriber,\n MikroORM,\n TransactionEventArgs\n} from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface RlsConfig {\n /**\n * Whether to enable PostgreSQL Row-Level Security.\n * Defaults to `true` when the driver is PostgreSQL, `false` otherwise.\n * Set to `false` to opt out even on PostgreSQL.\n */\n enabled?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// RLS EventSubscriber\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM EventSubscriber that executes `SET LOCAL app.tenant_id = :tenantId`\n * at the start of every transaction when PostgreSQL RLS is enabled.\n *\n * This ensures that even if the MikroORM global filter is somehow bypassed,\n * the database-level RLS policy enforces tenant isolation.\n *\n * The tenant ID is read from the EntityManager's filter parameters\n * (set by the tenant context middleware).\n */\nexport class RlsEventSubscriber implements EventSubscriber {\n async afterTransactionStart(args: TransactionEventArgs): Promise<void> {\n const tenantId = this.getTenantId(args.em);\n if (!tenantId) {\n // No tenant context (e.g., super-admin or public route) — skip SET LOCAL\n return;\n }\n\n const connection = args.em.getConnection();\n // Execute SET LOCAL within the transaction context\n // SET LOCAL only persists for the current transaction — no connection leakage\n await connection.execute(\n `SET LOCAL app.tenant_id = '${escapeSqlString(tenantId)}'`,\n [],\n 'run',\n args.transaction\n );\n }\n\n private getTenantId(em: EntityManager): string | undefined {\n const params = em.getFilterParams(TENANT_FILTER_NAME) as\n | { tenantId?: string }\n | undefined;\n return params?.tenantId;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Setup\n// ---------------------------------------------------------------------------\n\n/**\n * Sets up PostgreSQL Row-Level Security integration.\n *\n * 1. Registers the `RlsEventSubscriber` to run `SET LOCAL app.tenant_id`\n * at the start of every transaction.\n * 2. Validates that RLS policies exist on tenant-scoped tables (warns if missing).\n *\n * Call this at application bootstrap after `MikroORM.init()` and `setupTenantFilter()`.\n *\n * @param orm - The initialized MikroORM instance\n * @param config - RLS configuration (enabled defaults to auto-detect PostgreSQL)\n */\nexport function setupRls(orm: MikroORM, config: RlsConfig = {}): void {\n const isPostgres = isPostgresDriver(orm);\n const enabled = config.enabled ?? isPostgres;\n\n if (!enabled) {\n if (!isPostgres) {\n // Non-PostgreSQL — RLS not available, ORM filter is the sole enforcement\n return;\n }\n // PostgreSQL but explicitly disabled\n console.info(\n '[compliance] PostgreSQL RLS disabled by configuration. ORM filter is the sole tenant enforcement layer.'\n );\n return;\n }\n\n if (!isPostgres) {\n console.warn(\n '[compliance] RLS enabled but database driver is not PostgreSQL. ' +\n 'RLS is only supported on PostgreSQL. Falling back to ORM filter only.'\n );\n return;\n }\n\n // Register the RLS transaction subscriber\n orm.em.getEventManager().registerSubscriber(new RlsEventSubscriber());\n\n // Validate RLS policies exist\n validateRlsPolicies(orm).catch((err) => {\n console.warn('[compliance] Failed to validate RLS policies:', err);\n });\n}\n\n// ---------------------------------------------------------------------------\n// RLS policy validation\n// ---------------------------------------------------------------------------\n\n/**\n * Checks that tenant-scoped entities have RLS policies on their tables.\n * Logs warnings with the SQL needed to create missing policies.\n */\nasync function validateRlsPolicies(orm: MikroORM): Promise<void> {\n const metadata = orm.em.getMetadata().getAll();\n\n for (const meta of Object.values(metadata)) {\n const hasOrgId = meta.properties['organizationId'] != null;\n const hasOrg = meta.properties['organization'] != null;\n\n if (!hasOrgId && !hasOrg) continue;\n\n const tableName = meta.tableName;\n try {\n const connection = orm.em.getConnection();\n const result = await connection.execute<{ policyname: string }[]>(\n `SELECT policyname FROM pg_policies WHERE tablename = '${escapeSqlString(tableName)}'`,\n [],\n 'all'\n );\n\n const policies = Array.isArray(result) ? result : [];\n const hasTenantPolicy = policies.some((p: { policyname: string }) =>\n p.policyname.includes('tenant')\n );\n\n if (!hasTenantPolicy) {\n console.warn(\n `[compliance] No tenant RLS policy found on table '${tableName}'. ` +\n `Create one with:\\n` +\n ` ALTER TABLE \"${tableName}\" ENABLE ROW LEVEL SECURITY;\\n` +\n ` CREATE POLICY tenant_isolation ON \"${tableName}\"\\n` +\n ` USING (organization_id = current_setting('app.tenant_id'));`\n );\n }\n } catch {\n // Query failed — likely not connected yet or table doesn't exist\n // Skip validation for this table\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the ORM is using a PostgreSQL driver.\n * Checks the platform constructor name which is 'PostgreSqlPlatform' for PG.\n */\nfunction isPostgresDriver(orm: MikroORM): boolean {\n try {\n const platform = orm.em.getPlatform();\n const name = platform.constructor.name.toLowerCase();\n return name.includes('postgre');\n } catch {\n return false;\n }\n}\n\n/**\n * Escape a string for safe inclusion in SQL. Prevents SQL injection in\n * the SET LOCAL statement.\n */\nfunction escapeSqlString(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n"],"mappings":"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","names":["p","p","p"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forklaunch/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "forklaunch-js core package. Contains useful building blocks.",
|
|
5
5
|
"homepage": "https://github.com/forklaunch/forklaunch-js#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -105,8 +105,8 @@
|
|
|
105
105
|
"redis": "^5.11.0",
|
|
106
106
|
"uuid": "^13.0.0",
|
|
107
107
|
"zod": "^4.3.6",
|
|
108
|
-
"@forklaunch/
|
|
109
|
-
"@forklaunch/
|
|
108
|
+
"@forklaunch/validator": "1.0.2",
|
|
109
|
+
"@forklaunch/common": "1.0.2"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
112
|
"@eslint/js": "^10.0.1",
|