@forklaunch/core 1.0.7 → 1.0.9
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,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { PropertyBuilders, UniversalPropertyOptionsBuilder, PropertyChain, EmptyOptions, EntityMetadataWithProperties, EventSubscriber, EventArgs, FilterDef, EntityManager, MikroORM } from '@mikro-orm/core';
|
|
1
|
+
import { PropertyBuilders, UniversalPropertyOptionsBuilder, PropertyChain, EmptyOptions, EntityMetadataWithProperties, EntitySchemaWithMeta, InferEntityFromProperties, EventSubscriber, EventArgs, FilterDef, EntityManager, MikroORM } from '@mikro-orm/core';
|
|
3
2
|
export { InferEntity } from '@mikro-orm/core';
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -109,41 +108,53 @@ type ForklaunchPropertyBuilders = {
|
|
|
109
108
|
declare const fp: ForklaunchPropertyBuilders;
|
|
110
109
|
|
|
111
110
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* (avoids circular type resolution for self-referencing entities)
|
|
115
|
-
* - NeedsClassification (fp.string() without .compliance()) → never (rejected)
|
|
116
|
-
* - ClassifiedProperty branded (fp.string().compliance()) → accepted
|
|
117
|
-
* - Anything else (raw p.string()) → never (rejected)
|
|
111
|
+
* Rejects properties with the NeedsClassification brand
|
|
112
|
+
* (fp.string() without .compliance()). All other properties pass.
|
|
118
113
|
*/
|
|
119
|
-
type
|
|
120
|
-
[K in keyof T]: T[K] extends
|
|
114
|
+
type RejectUnclassified$1<T> = {
|
|
115
|
+
[K in keyof T]: T[K] extends NeedsClassification ? never : T[K];
|
|
121
116
|
};
|
|
122
117
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
118
|
+
* Call signature: enforces ClassifiedProperty brand on input properties.
|
|
119
|
+
* Return type: uses StripClassified to remove the brand so InferEntity
|
|
120
|
+
* sees pure PropertyChain types across package boundaries.
|
|
121
|
+
*/
|
|
122
|
+
declare function defineComplianceEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, unknown>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties & RejectUnclassified$1<TProperties>, TPK, TBase, TRepository, TForceObject>): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Rejects properties with NeedsClassification brand (same check
|
|
126
|
+
* as defineComplianceEntity).
|
|
127
|
+
*/
|
|
128
|
+
type RejectUnclassified<T> = {
|
|
129
|
+
[K in keyof T]: T[K] extends NeedsClassification ? never : T[K];
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Strips the ClassifiedProperty brand from a property type,
|
|
133
|
+
* returning the underlying PropertyChain.
|
|
134
|
+
*/
|
|
135
|
+
type StripBrand<T> = T extends PropertyChain<infer V, infer O> & ClassifiedProperty ? PropertyChain<V, O> : T;
|
|
136
|
+
/**
|
|
137
|
+
* Wraps shared base properties to:
|
|
138
|
+
* 1. Validate all properties have `.compliance()` called (compile check)
|
|
139
|
+
* 2. Strip the ClassifiedProperty brand from the output types
|
|
125
140
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* compile-time error via `RejectUnclassified`.
|
|
130
|
-
* - Relation fields: raw `PropertyChain` from `fp.manyToOne()` etc. —
|
|
131
|
-
* no brand, no `.compliance()` needed.
|
|
141
|
+
* Without this wrapper, spreading base properties into entities
|
|
142
|
+
* leaks the ClassifiedProperty brand across package boundaries,
|
|
143
|
+
* causing type identity mismatches.
|
|
132
144
|
*
|
|
133
145
|
* @example
|
|
134
146
|
* ```typescript
|
|
135
|
-
* const
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* organization: () => fp.manyToOne(Organization).nullable(),
|
|
141
|
-
* }
|
|
147
|
+
* export const sqlBaseProperties = defineBaseProperties({
|
|
148
|
+
* id: fp.uuid().primary().onCreate(() => v4()).compliance('none'),
|
|
149
|
+
* createdAt: fp.datetime().onCreate(() => new Date()).compliance('none'),
|
|
150
|
+
* updatedAt: fp.datetime().onCreate(() => new Date())
|
|
151
|
+
* .onUpdate(() => new Date()).compliance('none')
|
|
142
152
|
* });
|
|
143
|
-
* export type User = InferEntity<typeof User>;
|
|
144
153
|
* ```
|
|
145
154
|
*/
|
|
146
|
-
declare function
|
|
155
|
+
declare function defineBaseProperties<T extends Record<string, unknown>>(properties: T & RejectUnclassified<T>): {
|
|
156
|
+
[K in keyof T]: StripBrand<T[K]>;
|
|
157
|
+
};
|
|
147
158
|
|
|
148
159
|
declare class FieldEncryptor {
|
|
149
160
|
private readonly masterKey;
|
|
@@ -292,4 +303,4 @@ declare class RlsEventSubscriber implements EventSubscriber {
|
|
|
292
303
|
*/
|
|
293
304
|
declare function setupRls(orm: MikroORM, config?: RlsConfig): void;
|
|
294
305
|
|
|
295
|
-
export { type ClassifiedProperty, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ForklaunchPropertyBuilders, type ForklaunchPropertyChain, type NeedsClassification, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getComplianceMetadata, getEntityComplianceFields, getSuperAdminContext, setupRls, setupTenantFilter, wrapEmWithNativeQueryBlocking };
|
|
306
|
+
export { type ClassifiedProperty, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ForklaunchPropertyBuilders, type ForklaunchPropertyChain, type NeedsClassification, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineBaseProperties, defineComplianceEntity, entityHasEncryptedFields, fp, getComplianceMetadata, getEntityComplianceFields, getSuperAdminContext, setupRls, setupTenantFilter, wrapEmWithNativeQueryBlocking };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { PropertyBuilders, UniversalPropertyOptionsBuilder, PropertyChain, EmptyOptions, EntityMetadataWithProperties, EventSubscriber, EventArgs, FilterDef, EntityManager, MikroORM } from '@mikro-orm/core';
|
|
1
|
+
import { PropertyBuilders, UniversalPropertyOptionsBuilder, PropertyChain, EmptyOptions, EntityMetadataWithProperties, EntitySchemaWithMeta, InferEntityFromProperties, EventSubscriber, EventArgs, FilterDef, EntityManager, MikroORM } from '@mikro-orm/core';
|
|
3
2
|
export { InferEntity } from '@mikro-orm/core';
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -109,41 +108,53 @@ type ForklaunchPropertyBuilders = {
|
|
|
109
108
|
declare const fp: ForklaunchPropertyBuilders;
|
|
110
109
|
|
|
111
110
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* (avoids circular type resolution for self-referencing entities)
|
|
115
|
-
* - NeedsClassification (fp.string() without .compliance()) → never (rejected)
|
|
116
|
-
* - ClassifiedProperty branded (fp.string().compliance()) → accepted
|
|
117
|
-
* - Anything else (raw p.string()) → never (rejected)
|
|
111
|
+
* Rejects properties with the NeedsClassification brand
|
|
112
|
+
* (fp.string() without .compliance()). All other properties pass.
|
|
118
113
|
*/
|
|
119
|
-
type
|
|
120
|
-
[K in keyof T]: T[K] extends
|
|
114
|
+
type RejectUnclassified$1<T> = {
|
|
115
|
+
[K in keyof T]: T[K] extends NeedsClassification ? never : T[K];
|
|
121
116
|
};
|
|
122
117
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
118
|
+
* Call signature: enforces ClassifiedProperty brand on input properties.
|
|
119
|
+
* Return type: uses StripClassified to remove the brand so InferEntity
|
|
120
|
+
* sees pure PropertyChain types across package boundaries.
|
|
121
|
+
*/
|
|
122
|
+
declare function defineComplianceEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, unknown>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties & RejectUnclassified$1<TProperties>, TPK, TBase, TRepository, TForceObject>): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Rejects properties with NeedsClassification brand (same check
|
|
126
|
+
* as defineComplianceEntity).
|
|
127
|
+
*/
|
|
128
|
+
type RejectUnclassified<T> = {
|
|
129
|
+
[K in keyof T]: T[K] extends NeedsClassification ? never : T[K];
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Strips the ClassifiedProperty brand from a property type,
|
|
133
|
+
* returning the underlying PropertyChain.
|
|
134
|
+
*/
|
|
135
|
+
type StripBrand<T> = T extends PropertyChain<infer V, infer O> & ClassifiedProperty ? PropertyChain<V, O> : T;
|
|
136
|
+
/**
|
|
137
|
+
* Wraps shared base properties to:
|
|
138
|
+
* 1. Validate all properties have `.compliance()` called (compile check)
|
|
139
|
+
* 2. Strip the ClassifiedProperty brand from the output types
|
|
125
140
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* compile-time error via `RejectUnclassified`.
|
|
130
|
-
* - Relation fields: raw `PropertyChain` from `fp.manyToOne()` etc. —
|
|
131
|
-
* no brand, no `.compliance()` needed.
|
|
141
|
+
* Without this wrapper, spreading base properties into entities
|
|
142
|
+
* leaks the ClassifiedProperty brand across package boundaries,
|
|
143
|
+
* causing type identity mismatches.
|
|
132
144
|
*
|
|
133
145
|
* @example
|
|
134
146
|
* ```typescript
|
|
135
|
-
* const
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* organization: () => fp.manyToOne(Organization).nullable(),
|
|
141
|
-
* }
|
|
147
|
+
* export const sqlBaseProperties = defineBaseProperties({
|
|
148
|
+
* id: fp.uuid().primary().onCreate(() => v4()).compliance('none'),
|
|
149
|
+
* createdAt: fp.datetime().onCreate(() => new Date()).compliance('none'),
|
|
150
|
+
* updatedAt: fp.datetime().onCreate(() => new Date())
|
|
151
|
+
* .onUpdate(() => new Date()).compliance('none')
|
|
142
152
|
* });
|
|
143
|
-
* export type User = InferEntity<typeof User>;
|
|
144
153
|
* ```
|
|
145
154
|
*/
|
|
146
|
-
declare function
|
|
155
|
+
declare function defineBaseProperties<T extends Record<string, unknown>>(properties: T & RejectUnclassified<T>): {
|
|
156
|
+
[K in keyof T]: StripBrand<T[K]>;
|
|
157
|
+
};
|
|
147
158
|
|
|
148
159
|
declare class FieldEncryptor {
|
|
149
160
|
private readonly masterKey;
|
|
@@ -292,4 +303,4 @@ declare class RlsEventSubscriber implements EventSubscriber {
|
|
|
292
303
|
*/
|
|
293
304
|
declare function setupRls(orm: MikroORM, config?: RlsConfig): void;
|
|
294
305
|
|
|
295
|
-
export { type ClassifiedProperty, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ForklaunchPropertyBuilders, type ForklaunchPropertyChain, type NeedsClassification, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getComplianceMetadata, getEntityComplianceFields, getSuperAdminContext, setupRls, setupTenantFilter, wrapEmWithNativeQueryBlocking };
|
|
306
|
+
export { type ClassifiedProperty, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ForklaunchPropertyBuilders, type ForklaunchPropertyChain, type NeedsClassification, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineBaseProperties, defineComplianceEntity, entityHasEncryptedFields, fp, getComplianceMetadata, getEntityComplianceFields, getSuperAdminContext, setupRls, setupTenantFilter, wrapEmWithNativeQueryBlocking };
|
package/lib/persistence/index.js
CHANGED
|
@@ -25,6 +25,7 @@ __export(persistence_exports, {
|
|
|
25
25
|
RlsEventSubscriber: () => RlsEventSubscriber,
|
|
26
26
|
TENANT_FILTER_NAME: () => TENANT_FILTER_NAME,
|
|
27
27
|
createTenantFilterDef: () => createTenantFilterDef,
|
|
28
|
+
defineBaseProperties: () => defineBaseProperties,
|
|
28
29
|
defineComplianceEntity: () => defineComplianceEntity,
|
|
29
30
|
entityHasEncryptedFields: () => entityHasEncryptedFields,
|
|
30
31
|
fp: () => fp,
|
|
@@ -153,6 +154,7 @@ function readComplianceLevel(builder) {
|
|
|
153
154
|
return builder[COMPLIANCE_KEY];
|
|
154
155
|
}
|
|
155
156
|
function defineComplianceEntity(meta) {
|
|
157
|
+
const entityName = "name" in meta ? meta.name : "Unknown";
|
|
156
158
|
const complianceFields = /* @__PURE__ */ new Map();
|
|
157
159
|
const rawProperties = meta.properties;
|
|
158
160
|
const resolvedProperties = typeof rawProperties === "function" ? rawProperties(import_core2.p) : rawProperties;
|
|
@@ -161,15 +163,20 @@ function defineComplianceEntity(meta) {
|
|
|
161
163
|
const level = readComplianceLevel(prop);
|
|
162
164
|
if (level == null) {
|
|
163
165
|
throw new Error(
|
|
164
|
-
`Field '${
|
|
166
|
+
`Field '${entityName}.${fieldName}' is missing compliance classification. Call .compliance('pii' | 'phi' | 'pci' | 'none') on this property, or use a relation method (fp.manyToOne, etc.) which is auto-classified.`
|
|
165
167
|
);
|
|
166
168
|
}
|
|
167
169
|
complianceFields.set(fieldName, level);
|
|
168
170
|
}
|
|
169
|
-
registerEntityCompliance(
|
|
171
|
+
registerEntityCompliance(entityName, complianceFields);
|
|
170
172
|
return (0, import_core2.defineEntity)(meta);
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
// src/persistence/defineBaseProperties.ts
|
|
176
|
+
function defineBaseProperties(properties) {
|
|
177
|
+
return properties;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
// src/encryption/fieldEncryptor.ts
|
|
174
181
|
var DecryptionError = class extends Error {
|
|
175
182
|
name = "DecryptionError";
|
|
@@ -451,6 +458,7 @@ function escapeSqlString(value) {
|
|
|
451
458
|
RlsEventSubscriber,
|
|
452
459
|
TENANT_FILTER_NAME,
|
|
453
460
|
createTenantFilterDef,
|
|
461
|
+
defineBaseProperties,
|
|
454
462
|
defineComplianceEntity,
|
|
455
463
|
entityHasEncryptedFields,
|
|
456
464
|
fp,
|
|
@@ -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 NeedsClassification,\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 EmptyOptions,\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 * 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\nexport function registerEntityCompliance(\n entityName: string,\n fields: Map<string, ComplianceLevel>\n): void {\n complianceRegistry.set(entityName, fields);\n}\n\nexport function getComplianceMetadata(\n entityName: string,\n fieldName: string\n): ComplianceLevel {\n return complianceRegistry.get(entityName)?.get(fieldName) ?? 'none';\n}\n\nexport function getEntityComplianceFields(\n entityName: string\n): Map<string, ComplianceLevel> | undefined {\n return complianceRegistry.get(entityName);\n}\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// \"Needs classification\" brand\n// ---------------------------------------------------------------------------\n\n/**\n * Brand symbol on unclassified scalar properties. Properties with this brand\n * are rejected by `defineComplianceEntity`. Calling `.compliance()` strips\n * the brand and returns raw `PropertyChain<V, O>`.\n */\ndeclare const NEEDS_CLASSIFICATION: unique symbol;\n\nexport interface NeedsClassification {\n readonly [NEEDS_CLASSIFICATION]: true;\n}\n\n/**\n * Brand on classified scalar properties. Added by `.compliance()`.\n * Uses a regular string property (not unique symbol) so it survives\n * .d.ts boundaries across packages without identity divergence.\n * MikroORM's InferBuilderValue only checks ~type and ~options — it\n * ignores this property.\n */\nexport interface ClassifiedProperty {\n readonly __forklaunch_classified: true;\n}\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyChain\n// ---------------------------------------------------------------------------\n\n/**\n * An unclassified scalar property builder. Has all `PropertyChain` methods\n * (remapped to preserve `.compliance()` through chaining) plus the\n * `.compliance()` method itself.\n *\n * Carries the `NEEDS_CLASSIFICATION` brand — `defineComplianceEntity`\n * rejects properties with this brand.\n *\n * `.compliance(level)` returns the raw `PropertyChain<Value, Options>`\n * from MikroORM (no brand) — which `defineEntity` and `InferEntity`\n * understand natively.\n */\nexport type ForklaunchPropertyChain<Value, Options> = RemapReturns<\n Value,\n Options\n> &\n NeedsClassification & {\n /**\n * Classify this field's compliance level. Returns the raw MikroORM\n * `PropertyChain` so `InferEntity` works without interference.\n */\n compliance(\n level: ComplianceLevel\n ): PropertyChain<Value, Options> & 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\ntype RelationBuilderKeys =\n | 'manyToOne'\n | 'oneToMany'\n | 'manyToMany'\n | 'oneToOne'\n | 'embedded';\n\n/**\n * Generic methods on PropertyBuilders that need explicit signatures\n * because mapped types collapse their type parameters.\n */\ntype GenericBuilderKeys =\n | 'json'\n | 'formula'\n | 'type'\n | 'bigint'\n | 'array'\n | 'decimal'\n | 'enum';\n\n/**\n * The type of `fp`:\n * - Scalar methods → `ForklaunchPropertyChain` (must call `.compliance()`)\n * - Relation methods → raw `PropertyChain` (auto-classified at runtime, no brand)\n * - Generic methods → explicit signatures preserving type parameters\n */\nexport type ForklaunchPropertyBuilders = {\n [K in Exclude<\n keyof PropertyBuilders,\n RelationBuilderKeys | GenericBuilderKeys\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 // Relations return raw PropertyChain — no brand, no .compliance() needed\n [K in RelationBuilderKeys]: PropertyBuilders[K];\n} & {\n // Generic methods: preserve type parameters\n json: <T>() => ForklaunchPropertyChain<T, EmptyOptions>;\n formula: <T>(\n formula: string | ((...args: never[]) => string)\n ) => ForklaunchPropertyChain<T, EmptyOptions>;\n type: <T>(type: T) => ForklaunchPropertyChain<T, EmptyOptions>;\n bigint: <Mode extends 'bigint' | 'number' | 'string' = 'bigint'>(\n mode?: Mode\n ) => ForklaunchPropertyChain<\n Mode extends 'bigint' ? bigint : Mode extends 'number' ? number : string,\n EmptyOptions\n >;\n array: <T = string>(\n toJsValue?: (i: string) => T,\n toDbValue?: (i: T) => string\n ) => ForklaunchPropertyChain<T[], EmptyOptions>;\n decimal: <Mode extends 'number' | 'string' = 'string'>(\n mode?: Mode\n ) => ForklaunchPropertyChain<\n Mode extends 'number' ? number : string,\n EmptyOptions\n >;\n enum: <\n const T extends (number | string)[] | (() => Record<string, unknown>)\n >(\n items?: T\n ) => ForklaunchPropertyChain<\n T extends () => Record<string, unknown>\n ? T extends () => infer R\n ? R[keyof R]\n : never\n : T extends (infer Value)[]\n ? Value\n : T,\n EmptyOptions\n >;\n};\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 type { InferEntity } from '@mikro-orm/core';\nimport {\n defineEntity,\n p,\n type EntityMetadataWithProperties\n} from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n registerEntityCompliance,\n type ClassifiedProperty,\n type ComplianceLevel,\n type NeedsClassification\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Type-level enforcement\n// ---------------------------------------------------------------------------\n\n/**\n * Validates each property in the entity definition:\n * - Functions (lazy relations) → pass through WITHOUT evaluating return type\n * (avoids circular type resolution for self-referencing entities)\n * - NeedsClassification (fp.string() without .compliance()) → never (rejected)\n * - ClassifiedProperty branded (fp.string().compliance()) → accepted\n * - Anything else (raw p.string()) → never (rejected)\n */\ntype ValidateProperties<T> = {\n [K in keyof T]: T[K] extends (...args: never[]) => unknown\n ? T[K]\n : T[K] extends NeedsClassification\n ? never\n : T[K] extends ClassifiedProperty\n ? T[K]\n : never;\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 scalar field.\n *\n * - Scalar fields: `.compliance()` returns raw `PropertyChain<V,O>` —\n * `InferEntity` works natively because it's MikroORM's own type.\n * - Without `.compliance()`: the `NeedsClassification` brand causes a\n * compile-time error via `RejectUnclassified`.\n * - Relation fields: raw `PropertyChain` from `fp.manyToOne()` etc. —\n * no brand, no `.compliance()` needed.\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, unknown>,\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 & ValidateProperties<TProperties>,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >\n) {\n const complianceFields = new Map<string, ComplianceLevel>();\n\n // Resolve properties for compliance extraction (side effect only)\n const rawProperties = meta.properties;\n const resolvedProperties: Record<string, unknown> =\n typeof rawProperties === 'function' ? rawProperties(p) : rawProperties;\n\n for (const [fieldName, rawProp] of Object.entries(resolvedProperties)) {\n const prop = typeof rawProp === 'function' ? rawProp() : rawProp;\n const level = readComplianceLevel(prop);\n\n if (level == null) {\n throw new Error(\n `Field '${meta.name}.${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 registerEntityCompliance(meta.name, complianceFields);\n\n // Pass through to defineEntity — properties are raw PropertyChain\n // (from .compliance()) or raw relation builders. MikroORM's own types.\n return defineEntity(meta);\n}\n\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 EntityMetadata,\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 object>(em: T): 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: {\n em: Pick<EntityManager, 'addFilter'>;\n}): 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(\n em: Pick<EntityManager, 'fork'>\n): ReturnType<EntityManager['fork']> {\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 Transaction,\n TransactionEventArgs\n} from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n/**\n * Structural subset of {@link TransactionEventArgs} used by\n * `RlsEventSubscriber` so that callers (and tests) only need to provide\n * the properties the subscriber actually reads.\n */\nexport interface RlsTransactionEventArgs {\n em: {\n getFilterParams: EntityManager['getFilterParams'];\n getConnection(): {\n execute(\n query: string,\n params?: unknown[],\n method?: 'all' | 'get' | 'run',\n ctx?: unknown\n ): Promise<unknown>;\n };\n };\n transaction?: unknown;\n}\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: RlsTransactionEventArgs): 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(\n em: Pick<EntityManager, 'getFilterParams'>\n ): 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/persistence/defineBaseProperties.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 NeedsClassification,\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// Base property wrapper (strips brand for cross-package spreads)\nexport { defineBaseProperties } from './defineBaseProperties';\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 EmptyOptions,\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 * 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\nexport function registerEntityCompliance(\n entityName: string,\n fields: Map<string, ComplianceLevel>\n): void {\n complianceRegistry.set(entityName, fields);\n}\n\nexport function getComplianceMetadata(\n entityName: string,\n fieldName: string\n): ComplianceLevel {\n return complianceRegistry.get(entityName)?.get(fieldName) ?? 'none';\n}\n\nexport function getEntityComplianceFields(\n entityName: string\n): Map<string, ComplianceLevel> | undefined {\n return complianceRegistry.get(entityName);\n}\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// \"Needs classification\" brand\n// ---------------------------------------------------------------------------\n\n/**\n * Brand symbol on unclassified scalar properties. Properties with this brand\n * are rejected by `defineComplianceEntity`. Calling `.compliance()` strips\n * the brand and returns raw `PropertyChain<V, O>`.\n */\ndeclare const NEEDS_CLASSIFICATION: unique symbol;\n\nexport interface NeedsClassification {\n readonly [NEEDS_CLASSIFICATION]: true;\n}\n\n/**\n * Brand on classified scalar properties. Added by `.compliance()`.\n * Uses a regular string property (not unique symbol) so it survives\n * .d.ts boundaries across packages without identity divergence.\n * MikroORM's InferBuilderValue only checks ~type and ~options — it\n * ignores this property.\n */\nexport interface ClassifiedProperty {\n readonly __forklaunch_classified: true;\n}\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyChain\n// ---------------------------------------------------------------------------\n\n/**\n * An unclassified scalar property builder. Has all `PropertyChain` methods\n * (remapped to preserve `.compliance()` through chaining) plus the\n * `.compliance()` method itself.\n *\n * Carries the `NEEDS_CLASSIFICATION` brand — `defineComplianceEntity`\n * rejects properties with this brand.\n *\n * `.compliance(level)` returns the raw `PropertyChain<Value, Options>`\n * from MikroORM (no brand) — which `defineEntity` and `InferEntity`\n * understand natively.\n */\nexport type ForklaunchPropertyChain<Value, Options> = RemapReturns<\n Value,\n Options\n> &\n NeedsClassification & {\n /**\n * Classify this field's compliance level. Returns the raw MikroORM\n * `PropertyChain` so `InferEntity` works without interference.\n */\n compliance(\n level: ComplianceLevel\n ): PropertyChain<Value, Options> & 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\ntype RelationBuilderKeys =\n | 'manyToOne'\n | 'oneToMany'\n | 'manyToMany'\n | 'oneToOne'\n | 'embedded';\n\n/**\n * Generic methods on PropertyBuilders that need explicit signatures\n * because mapped types collapse their type parameters.\n */\ntype GenericBuilderKeys =\n | 'json'\n | 'formula'\n | 'type'\n | 'bigint'\n | 'array'\n | 'decimal'\n | 'enum';\n\n/**\n * The type of `fp`:\n * - Scalar methods → `ForklaunchPropertyChain` (must call `.compliance()`)\n * - Relation methods → raw `PropertyChain` (auto-classified at runtime, no brand)\n * - Generic methods → explicit signatures preserving type parameters\n */\nexport type ForklaunchPropertyBuilders = {\n [K in Exclude<\n keyof PropertyBuilders,\n RelationBuilderKeys | GenericBuilderKeys\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 // Relations return raw PropertyChain — no brand, no .compliance() needed\n [K in RelationBuilderKeys]: PropertyBuilders[K];\n} & {\n // Generic methods: preserve type parameters\n json: <T>() => ForklaunchPropertyChain<T, EmptyOptions>;\n formula: <T>(\n formula: string | ((...args: never[]) => string)\n ) => ForklaunchPropertyChain<T, EmptyOptions>;\n type: <T>(type: T) => ForklaunchPropertyChain<T, EmptyOptions>;\n bigint: <Mode extends 'bigint' | 'number' | 'string' = 'bigint'>(\n mode?: Mode\n ) => ForklaunchPropertyChain<\n Mode extends 'bigint' ? bigint : Mode extends 'number' ? number : string,\n EmptyOptions\n >;\n array: <T = string>(\n toJsValue?: (i: string) => T,\n toDbValue?: (i: T) => string\n ) => ForklaunchPropertyChain<T[], EmptyOptions>;\n decimal: <Mode extends 'number' | 'string' = 'string'>(\n mode?: Mode\n ) => ForklaunchPropertyChain<\n Mode extends 'number' ? number : string,\n EmptyOptions\n >;\n enum: <\n const T extends (number | string)[] | (() => Record<string, unknown>)\n >(\n items?: T\n ) => ForklaunchPropertyChain<\n T extends () => Record<string, unknown>\n ? T extends () => infer R\n ? R[keyof R]\n : never\n : T extends (infer Value)[]\n ? Value\n : T,\n EmptyOptions\n >;\n};\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 type { InferEntity } from '@mikro-orm/core';\nimport {\n defineEntity,\n p,\n type EntityMetadataWithProperties,\n type EntitySchemaWithMeta,\n type InferEntityFromProperties\n} from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n registerEntityCompliance,\n type ClassifiedProperty,\n type ComplianceLevel,\n type NeedsClassification\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Type-level enforcement (input) + brand stripping (output)\n// ---------------------------------------------------------------------------\n\n/**\n * Rejects properties with the NeedsClassification brand\n * (fp.string() without .compliance()). All other properties pass.\n */\ntype RejectUnclassified<T> = {\n [K in keyof T]: T[K] extends NeedsClassification ? never : T[K];\n};\n\n/**\n * Strips the ClassifiedProperty brand from inline properties.\n * Base properties are already clean (from defineBaseProperties).\n * Functions (lazy relations) pass through unchanged.\n */\ntype StripClassified<T> = {\n [K in keyof T]: T[K] extends ClassifiedProperty\n ? Omit<T[K], '__forklaunch_classified'>\n : T[K];\n};\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 * Call signature: enforces ClassifiedProperty brand on input properties.\n * Return type: uses StripClassified to remove the brand so InferEntity\n * sees pure PropertyChain types across package boundaries.\n */\nexport function defineComplianceEntity<\n const TName extends string,\n const TTableName extends string,\n const TProperties extends Record<string, unknown>,\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 & RejectUnclassified<TProperties>,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >\n): EntitySchemaWithMeta<\n TName,\n TTableName,\n InferEntityFromProperties<\n TProperties,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >,\n TBase,\n TProperties\n>;\n\n/**\n * Implementation: extracts compliance metadata, then delegates to defineEntity.\n */\nexport function defineComplianceEntity<\n const TName extends string,\n const TTableName extends string,\n const TProperties extends Record<string, unknown>,\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 =\n 'name' in meta ? (meta.name as string) : 'Unknown';\n const complianceFields = new Map<string, ComplianceLevel>();\n\n const rawProperties = meta.properties;\n const resolvedProperties: Record<string, unknown> =\n typeof rawProperties === 'function' ? rawProperties(p) : rawProperties;\n\n for (const [fieldName, rawProp] of Object.entries(resolvedProperties)) {\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 registerEntityCompliance(entityName, complianceFields);\n\n return defineEntity(meta);\n}\n\nexport type { InferEntity };\n","import type { PropertyChain } from '@mikro-orm/core';\nimport type { ClassifiedProperty, NeedsClassification } from './complianceTypes';\n\n/**\n * Rejects properties with NeedsClassification brand (same check\n * as defineComplianceEntity).\n */\ntype RejectUnclassified<T> = {\n [K in keyof T]: T[K] extends NeedsClassification ? never : T[K];\n};\n\n/**\n * Strips the ClassifiedProperty brand from a property type,\n * returning the underlying PropertyChain.\n */\ntype StripBrand<T> = T extends PropertyChain<infer V, infer O> &\n ClassifiedProperty\n ? PropertyChain<V, O>\n : T;\n\n/**\n * Wraps shared base properties to:\n * 1. Validate all properties have `.compliance()` called (compile check)\n * 2. Strip the ClassifiedProperty brand from the output types\n *\n * Without this wrapper, spreading base properties into entities\n * leaks the ClassifiedProperty brand across package boundaries,\n * causing type identity mismatches.\n *\n * @example\n * ```typescript\n * export const sqlBaseProperties = defineBaseProperties({\n * id: fp.uuid().primary().onCreate(() => v4()).compliance('none'),\n * createdAt: fp.datetime().onCreate(() => new Date()).compliance('none'),\n * updatedAt: fp.datetime().onCreate(() => new Date())\n * .onUpdate(() => new Date()).compliance('none')\n * });\n * ```\n */\nexport function defineBaseProperties<\n T extends Record<string, unknown>\n>(properties: T & RejectUnclassified<T>): { [K in keyof T]: StripBrand<T[K]> } {\n return properties as { [K in keyof T]: StripBrand<T[K]> };\n}\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 EntityMetadata,\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 object>(em: T): 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: {\n em: Pick<EntityManager, 'addFilter'>;\n}): 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(\n em: Pick<EntityManager, 'fork'>\n): ReturnType<EntityManager['fork']> {\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 Transaction,\n TransactionEventArgs\n} from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n/**\n * Structural subset of {@link TransactionEventArgs} used by\n * `RlsEventSubscriber` so that callers (and tests) only need to provide\n * the properties the subscriber actually reads.\n */\nexport interface RlsTransactionEventArgs {\n em: {\n getFilterParams: EntityManager['getFilterParams'];\n getConnection(): {\n execute(\n query: string,\n params?: unknown[],\n method?: 'all' | 'get' | 'run',\n ctx?: unknown\n ): Promise<unknown>;\n };\n };\n transaction?: unknown;\n}\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: RlsTransactionEventArgs): 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(\n em: Pick<EntityManager, 'getFilterParams'>\n ): 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"]}
|
|
@@ -117,6 +117,7 @@ function readComplianceLevel(builder) {
|
|
|
117
117
|
return builder[COMPLIANCE_KEY];
|
|
118
118
|
}
|
|
119
119
|
function defineComplianceEntity(meta) {
|
|
120
|
+
const entityName = "name" in meta ? meta.name : "Unknown";
|
|
120
121
|
const complianceFields = /* @__PURE__ */ new Map();
|
|
121
122
|
const rawProperties = meta.properties;
|
|
122
123
|
const resolvedProperties = typeof rawProperties === "function" ? rawProperties(p2) : rawProperties;
|
|
@@ -125,15 +126,20 @@ function defineComplianceEntity(meta) {
|
|
|
125
126
|
const level = readComplianceLevel(prop);
|
|
126
127
|
if (level == null) {
|
|
127
128
|
throw new Error(
|
|
128
|
-
`Field '${
|
|
129
|
+
`Field '${entityName}.${fieldName}' is missing compliance classification. Call .compliance('pii' | 'phi' | 'pci' | 'none') on this property, or use a relation method (fp.manyToOne, etc.) which is auto-classified.`
|
|
129
130
|
);
|
|
130
131
|
}
|
|
131
132
|
complianceFields.set(fieldName, level);
|
|
132
133
|
}
|
|
133
|
-
registerEntityCompliance(
|
|
134
|
+
registerEntityCompliance(entityName, complianceFields);
|
|
134
135
|
return defineEntity(meta);
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
// src/persistence/defineBaseProperties.ts
|
|
139
|
+
function defineBaseProperties(properties) {
|
|
140
|
+
return properties;
|
|
141
|
+
}
|
|
142
|
+
|
|
137
143
|
// src/encryption/fieldEncryptor.ts
|
|
138
144
|
var DecryptionError = class extends Error {
|
|
139
145
|
name = "DecryptionError";
|
|
@@ -414,6 +420,7 @@ export {
|
|
|
414
420
|
RlsEventSubscriber,
|
|
415
421
|
TENANT_FILTER_NAME,
|
|
416
422
|
createTenantFilterDef,
|
|
423
|
+
defineBaseProperties,
|
|
417
424
|
defineComplianceEntity,
|
|
418
425
|
entityHasEncryptedFields,
|
|
419
426
|
fp,
|
|
@@ -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 EmptyOptions,\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 * 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\nexport function registerEntityCompliance(\n entityName: string,\n fields: Map<string, ComplianceLevel>\n): void {\n complianceRegistry.set(entityName, fields);\n}\n\nexport function getComplianceMetadata(\n entityName: string,\n fieldName: string\n): ComplianceLevel {\n return complianceRegistry.get(entityName)?.get(fieldName) ?? 'none';\n}\n\nexport function getEntityComplianceFields(\n entityName: string\n): Map<string, ComplianceLevel> | undefined {\n return complianceRegistry.get(entityName);\n}\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// \"Needs classification\" brand\n// ---------------------------------------------------------------------------\n\n/**\n * Brand symbol on unclassified scalar properties. Properties with this brand\n * are rejected by `defineComplianceEntity`. Calling `.compliance()` strips\n * the brand and returns raw `PropertyChain<V, O>`.\n */\ndeclare const NEEDS_CLASSIFICATION: unique symbol;\n\nexport interface NeedsClassification {\n readonly [NEEDS_CLASSIFICATION]: true;\n}\n\n/**\n * Brand on classified scalar properties. Added by `.compliance()`.\n * Uses a regular string property (not unique symbol) so it survives\n * .d.ts boundaries across packages without identity divergence.\n * MikroORM's InferBuilderValue only checks ~type and ~options — it\n * ignores this property.\n */\nexport interface ClassifiedProperty {\n readonly __forklaunch_classified: true;\n}\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyChain\n// ---------------------------------------------------------------------------\n\n/**\n * An unclassified scalar property builder. Has all `PropertyChain` methods\n * (remapped to preserve `.compliance()` through chaining) plus the\n * `.compliance()` method itself.\n *\n * Carries the `NEEDS_CLASSIFICATION` brand — `defineComplianceEntity`\n * rejects properties with this brand.\n *\n * `.compliance(level)` returns the raw `PropertyChain<Value, Options>`\n * from MikroORM (no brand) — which `defineEntity` and `InferEntity`\n * understand natively.\n */\nexport type ForklaunchPropertyChain<Value, Options> = RemapReturns<\n Value,\n Options\n> &\n NeedsClassification & {\n /**\n * Classify this field's compliance level. Returns the raw MikroORM\n * `PropertyChain` so `InferEntity` works without interference.\n */\n compliance(\n level: ComplianceLevel\n ): PropertyChain<Value, Options> & 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\ntype RelationBuilderKeys =\n | 'manyToOne'\n | 'oneToMany'\n | 'manyToMany'\n | 'oneToOne'\n | 'embedded';\n\n/**\n * Generic methods on PropertyBuilders that need explicit signatures\n * because mapped types collapse their type parameters.\n */\ntype GenericBuilderKeys =\n | 'json'\n | 'formula'\n | 'type'\n | 'bigint'\n | 'array'\n | 'decimal'\n | 'enum';\n\n/**\n * The type of `fp`:\n * - Scalar methods → `ForklaunchPropertyChain` (must call `.compliance()`)\n * - Relation methods → raw `PropertyChain` (auto-classified at runtime, no brand)\n * - Generic methods → explicit signatures preserving type parameters\n */\nexport type ForklaunchPropertyBuilders = {\n [K in Exclude<\n keyof PropertyBuilders,\n RelationBuilderKeys | GenericBuilderKeys\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 // Relations return raw PropertyChain — no brand, no .compliance() needed\n [K in RelationBuilderKeys]: PropertyBuilders[K];\n} & {\n // Generic methods: preserve type parameters\n json: <T>() => ForklaunchPropertyChain<T, EmptyOptions>;\n formula: <T>(\n formula: string | ((...args: never[]) => string)\n ) => ForklaunchPropertyChain<T, EmptyOptions>;\n type: <T>(type: T) => ForklaunchPropertyChain<T, EmptyOptions>;\n bigint: <Mode extends 'bigint' | 'number' | 'string' = 'bigint'>(\n mode?: Mode\n ) => ForklaunchPropertyChain<\n Mode extends 'bigint' ? bigint : Mode extends 'number' ? number : string,\n EmptyOptions\n >;\n array: <T = string>(\n toJsValue?: (i: string) => T,\n toDbValue?: (i: T) => string\n ) => ForklaunchPropertyChain<T[], EmptyOptions>;\n decimal: <Mode extends 'number' | 'string' = 'string'>(\n mode?: Mode\n ) => ForklaunchPropertyChain<\n Mode extends 'number' ? number : string,\n EmptyOptions\n >;\n enum: <\n const T extends (number | string)[] | (() => Record<string, unknown>)\n >(\n items?: T\n ) => ForklaunchPropertyChain<\n T extends () => Record<string, unknown>\n ? T extends () => infer R\n ? R[keyof R]\n : never\n : T extends (infer Value)[]\n ? Value\n : T,\n EmptyOptions\n >;\n};\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 type { InferEntity } from '@mikro-orm/core';\nimport {\n defineEntity,\n p,\n type EntityMetadataWithProperties\n} from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n registerEntityCompliance,\n type ClassifiedProperty,\n type ComplianceLevel,\n type NeedsClassification\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Type-level enforcement\n// ---------------------------------------------------------------------------\n\n/**\n * Validates each property in the entity definition:\n * - Functions (lazy relations) → pass through WITHOUT evaluating return type\n * (avoids circular type resolution for self-referencing entities)\n * - NeedsClassification (fp.string() without .compliance()) → never (rejected)\n * - ClassifiedProperty branded (fp.string().compliance()) → accepted\n * - Anything else (raw p.string()) → never (rejected)\n */\ntype ValidateProperties<T> = {\n [K in keyof T]: T[K] extends (...args: never[]) => unknown\n ? T[K]\n : T[K] extends NeedsClassification\n ? never\n : T[K] extends ClassifiedProperty\n ? T[K]\n : never;\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 scalar field.\n *\n * - Scalar fields: `.compliance()` returns raw `PropertyChain<V,O>` —\n * `InferEntity` works natively because it's MikroORM's own type.\n * - Without `.compliance()`: the `NeedsClassification` brand causes a\n * compile-time error via `RejectUnclassified`.\n * - Relation fields: raw `PropertyChain` from `fp.manyToOne()` etc. —\n * no brand, no `.compliance()` needed.\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, unknown>,\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 & ValidateProperties<TProperties>,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >\n) {\n const complianceFields = new Map<string, ComplianceLevel>();\n\n // Resolve properties for compliance extraction (side effect only)\n const rawProperties = meta.properties;\n const resolvedProperties: Record<string, unknown> =\n typeof rawProperties === 'function' ? rawProperties(p) : rawProperties;\n\n for (const [fieldName, rawProp] of Object.entries(resolvedProperties)) {\n const prop = typeof rawProp === 'function' ? rawProp() : rawProp;\n const level = readComplianceLevel(prop);\n\n if (level == null) {\n throw new Error(\n `Field '${meta.name}.${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 registerEntityCompliance(meta.name, complianceFields);\n\n // Pass through to defineEntity — properties are raw PropertyChain\n // (from .compliance()) or raw relation builders. MikroORM's own types.\n return defineEntity(meta);\n}\n\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 EntityMetadata,\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 object>(em: T): 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: {\n em: Pick<EntityManager, 'addFilter'>;\n}): 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(\n em: Pick<EntityManager, 'fork'>\n): ReturnType<EntityManager['fork']> {\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 Transaction,\n TransactionEventArgs\n} from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n/**\n * Structural subset of {@link TransactionEventArgs} used by\n * `RlsEventSubscriber` so that callers (and tests) only need to provide\n * the properties the subscriber actually reads.\n */\nexport interface RlsTransactionEventArgs {\n em: {\n getFilterParams: EntityManager['getFilterParams'];\n getConnection(): {\n execute(\n query: string,\n params?: unknown[],\n method?: 'all' | 'get' | 'run',\n ctx?: unknown\n ): Promise<unknown>;\n };\n };\n transaction?: unknown;\n}\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: RlsTransactionEventArgs): 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(\n em: Pick<EntityManager, 'getFilterParams'>\n ): 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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/persistence/complianceTypes.ts","../../src/persistence/compliancePropertyBuilder.ts","../../src/persistence/defineComplianceEntity.ts","../../src/persistence/defineBaseProperties.ts","../../src/encryption/fieldEncryptor.ts","../../src/persistence/complianceEventSubscriber.ts","../../src/persistence/tenantFilter.ts","../../src/persistence/rls.ts"],"sourcesContent":["import type {\n EmptyOptions,\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 * 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\nexport function registerEntityCompliance(\n entityName: string,\n fields: Map<string, ComplianceLevel>\n): void {\n complianceRegistry.set(entityName, fields);\n}\n\nexport function getComplianceMetadata(\n entityName: string,\n fieldName: string\n): ComplianceLevel {\n return complianceRegistry.get(entityName)?.get(fieldName) ?? 'none';\n}\n\nexport function getEntityComplianceFields(\n entityName: string\n): Map<string, ComplianceLevel> | undefined {\n return complianceRegistry.get(entityName);\n}\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// \"Needs classification\" brand\n// ---------------------------------------------------------------------------\n\n/**\n * Brand symbol on unclassified scalar properties. Properties with this brand\n * are rejected by `defineComplianceEntity`. Calling `.compliance()` strips\n * the brand and returns raw `PropertyChain<V, O>`.\n */\ndeclare const NEEDS_CLASSIFICATION: unique symbol;\n\nexport interface NeedsClassification {\n readonly [NEEDS_CLASSIFICATION]: true;\n}\n\n/**\n * Brand on classified scalar properties. Added by `.compliance()`.\n * Uses a regular string property (not unique symbol) so it survives\n * .d.ts boundaries across packages without identity divergence.\n * MikroORM's InferBuilderValue only checks ~type and ~options — it\n * ignores this property.\n */\nexport interface ClassifiedProperty {\n readonly __forklaunch_classified: true;\n}\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyChain\n// ---------------------------------------------------------------------------\n\n/**\n * An unclassified scalar property builder. Has all `PropertyChain` methods\n * (remapped to preserve `.compliance()` through chaining) plus the\n * `.compliance()` method itself.\n *\n * Carries the `NEEDS_CLASSIFICATION` brand — `defineComplianceEntity`\n * rejects properties with this brand.\n *\n * `.compliance(level)` returns the raw `PropertyChain<Value, Options>`\n * from MikroORM (no brand) — which `defineEntity` and `InferEntity`\n * understand natively.\n */\nexport type ForklaunchPropertyChain<Value, Options> = RemapReturns<\n Value,\n Options\n> &\n NeedsClassification & {\n /**\n * Classify this field's compliance level. Returns the raw MikroORM\n * `PropertyChain` so `InferEntity` works without interference.\n */\n compliance(\n level: ComplianceLevel\n ): PropertyChain<Value, Options> & 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\ntype RelationBuilderKeys =\n | 'manyToOne'\n | 'oneToMany'\n | 'manyToMany'\n | 'oneToOne'\n | 'embedded';\n\n/**\n * Generic methods on PropertyBuilders that need explicit signatures\n * because mapped types collapse their type parameters.\n */\ntype GenericBuilderKeys =\n | 'json'\n | 'formula'\n | 'type'\n | 'bigint'\n | 'array'\n | 'decimal'\n | 'enum';\n\n/**\n * The type of `fp`:\n * - Scalar methods → `ForklaunchPropertyChain` (must call `.compliance()`)\n * - Relation methods → raw `PropertyChain` (auto-classified at runtime, no brand)\n * - Generic methods → explicit signatures preserving type parameters\n */\nexport type ForklaunchPropertyBuilders = {\n [K in Exclude<\n keyof PropertyBuilders,\n RelationBuilderKeys | GenericBuilderKeys\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 // Relations return raw PropertyChain — no brand, no .compliance() needed\n [K in RelationBuilderKeys]: PropertyBuilders[K];\n} & {\n // Generic methods: preserve type parameters\n json: <T>() => ForklaunchPropertyChain<T, EmptyOptions>;\n formula: <T>(\n formula: string | ((...args: never[]) => string)\n ) => ForklaunchPropertyChain<T, EmptyOptions>;\n type: <T>(type: T) => ForklaunchPropertyChain<T, EmptyOptions>;\n bigint: <Mode extends 'bigint' | 'number' | 'string' = 'bigint'>(\n mode?: Mode\n ) => ForklaunchPropertyChain<\n Mode extends 'bigint' ? bigint : Mode extends 'number' ? number : string,\n EmptyOptions\n >;\n array: <T = string>(\n toJsValue?: (i: string) => T,\n toDbValue?: (i: T) => string\n ) => ForklaunchPropertyChain<T[], EmptyOptions>;\n decimal: <Mode extends 'number' | 'string' = 'string'>(\n mode?: Mode\n ) => ForklaunchPropertyChain<\n Mode extends 'number' ? number : string,\n EmptyOptions\n >;\n enum: <\n const T extends (number | string)[] | (() => Record<string, unknown>)\n >(\n items?: T\n ) => ForklaunchPropertyChain<\n T extends () => Record<string, unknown>\n ? T extends () => infer R\n ? R[keyof R]\n : never\n : T extends (infer Value)[]\n ? Value\n : T,\n EmptyOptions\n >;\n};\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 type { InferEntity } from '@mikro-orm/core';\nimport {\n defineEntity,\n p,\n type EntityMetadataWithProperties,\n type EntitySchemaWithMeta,\n type InferEntityFromProperties\n} from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n registerEntityCompliance,\n type ClassifiedProperty,\n type ComplianceLevel,\n type NeedsClassification\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Type-level enforcement (input) + brand stripping (output)\n// ---------------------------------------------------------------------------\n\n/**\n * Rejects properties with the NeedsClassification brand\n * (fp.string() without .compliance()). All other properties pass.\n */\ntype RejectUnclassified<T> = {\n [K in keyof T]: T[K] extends NeedsClassification ? never : T[K];\n};\n\n/**\n * Strips the ClassifiedProperty brand from inline properties.\n * Base properties are already clean (from defineBaseProperties).\n * Functions (lazy relations) pass through unchanged.\n */\ntype StripClassified<T> = {\n [K in keyof T]: T[K] extends ClassifiedProperty\n ? Omit<T[K], '__forklaunch_classified'>\n : T[K];\n};\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 * Call signature: enforces ClassifiedProperty brand on input properties.\n * Return type: uses StripClassified to remove the brand so InferEntity\n * sees pure PropertyChain types across package boundaries.\n */\nexport function defineComplianceEntity<\n const TName extends string,\n const TTableName extends string,\n const TProperties extends Record<string, unknown>,\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 & RejectUnclassified<TProperties>,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >\n): EntitySchemaWithMeta<\n TName,\n TTableName,\n InferEntityFromProperties<\n TProperties,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >,\n TBase,\n TProperties\n>;\n\n/**\n * Implementation: extracts compliance metadata, then delegates to defineEntity.\n */\nexport function defineComplianceEntity<\n const TName extends string,\n const TTableName extends string,\n const TProperties extends Record<string, unknown>,\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 =\n 'name' in meta ? (meta.name as string) : 'Unknown';\n const complianceFields = new Map<string, ComplianceLevel>();\n\n const rawProperties = meta.properties;\n const resolvedProperties: Record<string, unknown> =\n typeof rawProperties === 'function' ? rawProperties(p) : rawProperties;\n\n for (const [fieldName, rawProp] of Object.entries(resolvedProperties)) {\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 registerEntityCompliance(entityName, complianceFields);\n\n return defineEntity(meta);\n}\n\nexport type { InferEntity };\n","import type { PropertyChain } from '@mikro-orm/core';\nimport type { ClassifiedProperty, NeedsClassification } from './complianceTypes';\n\n/**\n * Rejects properties with NeedsClassification brand (same check\n * as defineComplianceEntity).\n */\ntype RejectUnclassified<T> = {\n [K in keyof T]: T[K] extends NeedsClassification ? never : T[K];\n};\n\n/**\n * Strips the ClassifiedProperty brand from a property type,\n * returning the underlying PropertyChain.\n */\ntype StripBrand<T> = T extends PropertyChain<infer V, infer O> &\n ClassifiedProperty\n ? PropertyChain<V, O>\n : T;\n\n/**\n * Wraps shared base properties to:\n * 1. Validate all properties have `.compliance()` called (compile check)\n * 2. Strip the ClassifiedProperty brand from the output types\n *\n * Without this wrapper, spreading base properties into entities\n * leaks the ClassifiedProperty brand across package boundaries,\n * causing type identity mismatches.\n *\n * @example\n * ```typescript\n * export const sqlBaseProperties = defineBaseProperties({\n * id: fp.uuid().primary().onCreate(() => v4()).compliance('none'),\n * createdAt: fp.datetime().onCreate(() => new Date()).compliance('none'),\n * updatedAt: fp.datetime().onCreate(() => new Date())\n * .onUpdate(() => new Date()).compliance('none')\n * });\n * ```\n */\nexport function defineBaseProperties<\n T extends Record<string, unknown>\n>(properties: T & RejectUnclassified<T>): { [K in keyof T]: StripBrand<T[K]> } {\n return properties as { [K in keyof T]: StripBrand<T[K]> };\n}\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 EntityMetadata,\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 object>(em: T): 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: {\n em: Pick<EntityManager, 'addFilter'>;\n}): 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(\n em: Pick<EntityManager, 'fork'>\n): ReturnType<EntityManager['fork']> {\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 Transaction,\n TransactionEventArgs\n} from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n/**\n * Structural subset of {@link TransactionEventArgs} used by\n * `RlsEventSubscriber` so that callers (and tests) only need to provide\n * the properties the subscriber actually reads.\n */\nexport interface RlsTransactionEventArgs {\n em: {\n getFilterParams: EntityManager['getFilterParams'];\n getConnection(): {\n execute(\n query: string,\n params?: unknown[],\n method?: 'all' | 'get' | 'run',\n ctx?: unknown\n ): Promise<unknown>;\n };\n };\n transaction?: unknown;\n}\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: RlsTransactionEventArgs): 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(\n em: Pick<EntityManager, 'getFilterParams'>\n ): 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.9",
|
|
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/common": "1.0.
|
|
109
|
-
"@forklaunch/validator": "1.0.
|
|
108
|
+
"@forklaunch/common": "1.0.9",
|
|
109
|
+
"@forklaunch/validator": "1.0.9"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
112
|
"@eslint/js": "^10.0.1",
|