@forklaunch/core 1.0.1 → 1.0.3

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