@forklaunch/core 1.1.0 → 1.1.1

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.
@@ -25,7 +25,23 @@ declare const RetentionDuration: {
25
25
  readonly months: (n: number) => string;
26
26
  readonly years: (n: number) => string;
27
27
  };
28
- declare function parseDuration(iso: string): number;
28
+ interface ParsedDuration {
29
+ years: number;
30
+ months: number;
31
+ days: number;
32
+ }
33
+ /**
34
+ * Parse an ISO 8601 duration string into calendar units.
35
+ * Returns structured units to enable calendar-aware date arithmetic.
36
+ */
37
+ declare function parseDuration(iso: string): ParsedDuration;
38
+ /**
39
+ * Subtract a parsed duration from a date using calendar-aware arithmetic.
40
+ * Handles month-end clamping and leap years correctly.
41
+ */
42
+ declare function subtractDuration(from: Date, duration: ParsedDuration): Date;
43
+ declare function getEntityUserIdField(entityName: string): string;
44
+ declare function getAllUserIdFields(): ReadonlyMap<string, string>;
29
45
  declare function getEntityRetention(entityName: string): RetentionPolicy | undefined;
30
46
  declare function getAllRetentionPolicies(): ReadonlyMap<string, RetentionPolicy>;
31
47
  /**
@@ -120,6 +136,7 @@ type ValidateProperties<T> = {
120
136
  };
121
137
  declare function defineComplianceEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, unknown>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties & ValidateProperties<TProperties>, TPK, TBase, TRepository, TForceObject> & {
122
138
  retention?: RetentionPolicy;
139
+ userIdField?: string;
123
140
  }): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
124
141
 
125
142
  declare class FieldEncryptor {
@@ -269,4 +286,4 @@ declare class RlsEventSubscriber implements EventSubscriber {
269
286
  */
270
287
  declare function setupRls(orm: MikroORM, config?: RlsConfig): void;
271
288
 
272
- export { type ClassifiedProperty, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ExtractInner, type ForklaunchPropertyBuilders, RetentionAction, RetentionAction as RetentionActionType, RetentionDuration, type RetentionPolicy, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, type WithCompliance, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getAllRetentionPolicies, getComplianceMetadata, getEntityComplianceFields, getEntityRetention, getSuperAdminContext, parseDuration, setupRls, setupTenantFilter, wrapEmWithNativeQueryBlocking };
289
+ export { type ClassifiedProperty, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ExtractInner, type ForklaunchPropertyBuilders, type ParsedDuration, RetentionAction, RetentionAction as RetentionActionType, RetentionDuration, type RetentionPolicy, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, type WithCompliance, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getAllRetentionPolicies, getAllUserIdFields, getComplianceMetadata, getEntityComplianceFields, getEntityRetention, getEntityUserIdField, getSuperAdminContext, parseDuration, setupRls, setupTenantFilter, subtractDuration, wrapEmWithNativeQueryBlocking };
@@ -25,7 +25,23 @@ declare const RetentionDuration: {
25
25
  readonly months: (n: number) => string;
26
26
  readonly years: (n: number) => string;
27
27
  };
28
- declare function parseDuration(iso: string): number;
28
+ interface ParsedDuration {
29
+ years: number;
30
+ months: number;
31
+ days: number;
32
+ }
33
+ /**
34
+ * Parse an ISO 8601 duration string into calendar units.
35
+ * Returns structured units to enable calendar-aware date arithmetic.
36
+ */
37
+ declare function parseDuration(iso: string): ParsedDuration;
38
+ /**
39
+ * Subtract a parsed duration from a date using calendar-aware arithmetic.
40
+ * Handles month-end clamping and leap years correctly.
41
+ */
42
+ declare function subtractDuration(from: Date, duration: ParsedDuration): Date;
43
+ declare function getEntityUserIdField(entityName: string): string;
44
+ declare function getAllUserIdFields(): ReadonlyMap<string, string>;
29
45
  declare function getEntityRetention(entityName: string): RetentionPolicy | undefined;
30
46
  declare function getAllRetentionPolicies(): ReadonlyMap<string, RetentionPolicy>;
31
47
  /**
@@ -120,6 +136,7 @@ type ValidateProperties<T> = {
120
136
  };
121
137
  declare function defineComplianceEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, unknown>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties & ValidateProperties<TProperties>, TPK, TBase, TRepository, TForceObject> & {
122
138
  retention?: RetentionPolicy;
139
+ userIdField?: string;
123
140
  }): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
124
141
 
125
142
  declare class FieldEncryptor {
@@ -269,4 +286,4 @@ declare class RlsEventSubscriber implements EventSubscriber {
269
286
  */
270
287
  declare function setupRls(orm: MikroORM, config?: RlsConfig): void;
271
288
 
272
- export { type ClassifiedProperty, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ExtractInner, type ForklaunchPropertyBuilders, RetentionAction, RetentionAction as RetentionActionType, RetentionDuration, type RetentionPolicy, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, type WithCompliance, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getAllRetentionPolicies, getComplianceMetadata, getEntityComplianceFields, getEntityRetention, getSuperAdminContext, parseDuration, setupRls, setupTenantFilter, wrapEmWithNativeQueryBlocking };
289
+ export { type ClassifiedProperty, ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, type ExtractInner, type ForklaunchPropertyBuilders, type ParsedDuration, RetentionAction, RetentionAction as RetentionActionType, RetentionDuration, type RetentionPolicy, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, type WithCompliance, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getAllRetentionPolicies, getAllUserIdFields, getComplianceMetadata, getEntityComplianceFields, getEntityRetention, getEntityUserIdField, getSuperAdminContext, parseDuration, setupRls, setupTenantFilter, subtractDuration, wrapEmWithNativeQueryBlocking };
@@ -31,13 +31,16 @@ __export(persistence_exports, {
31
31
  entityHasEncryptedFields: () => entityHasEncryptedFields,
32
32
  fp: () => fp,
33
33
  getAllRetentionPolicies: () => getAllRetentionPolicies,
34
+ getAllUserIdFields: () => getAllUserIdFields,
34
35
  getComplianceMetadata: () => getComplianceMetadata,
35
36
  getEntityComplianceFields: () => getEntityComplianceFields,
36
37
  getEntityRetention: () => getEntityRetention,
38
+ getEntityUserIdField: () => getEntityUserIdField,
37
39
  getSuperAdminContext: () => getSuperAdminContext,
38
40
  parseDuration: () => parseDuration,
39
41
  setupRls: () => setupRls,
40
42
  setupTenantFilter: () => setupTenantFilter,
43
+ subtractDuration: () => subtractDuration,
41
44
  wrapEmWithNativeQueryBlocking: () => wrapEmWithNativeQueryBlocking
42
45
  });
43
46
  module.exports = __toCommonJS(persistence_exports);
@@ -78,8 +81,6 @@ var RetentionDuration = {
78
81
  years: (n) => `P${n}Y`
79
82
  };
80
83
  var DURATION_REGEX = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?$/;
81
- var MS_PER_DAY = 864e5;
82
- var MIN_DURATION_MS = MS_PER_DAY;
83
84
  function parseDuration(iso) {
84
85
  const match = DURATION_REGEX.exec(iso);
85
86
  if (!match) {
@@ -90,16 +91,33 @@ function parseDuration(iso) {
90
91
  const years = parseInt(match[1] || "0", 10);
91
92
  const months = parseInt(match[2] || "0", 10);
92
93
  const days = parseInt(match[3] || "0", 10);
93
- const totalDays = years * 365 + months * 30 + days;
94
- const ms = totalDays * MS_PER_DAY;
95
- if (ms < MIN_DURATION_MS) {
94
+ const approxDays = years * 365 + months * 30 + days;
95
+ if (approxDays < 1) {
96
96
  throw new Error(
97
- `Retention duration must be >= 1 day (P1D). Got: '${iso}' (${totalDays} days)`
97
+ `Retention duration must be >= 1 day (P1D). Got: '${iso}' (${approxDays} approx days)`
98
98
  );
99
99
  }
100
- return ms;
100
+ return { years, months, days };
101
+ }
102
+ function subtractDuration(from, duration) {
103
+ const result = new Date(from);
104
+ result.setFullYear(result.getFullYear() - duration.years);
105
+ result.setMonth(result.getMonth() - duration.months);
106
+ result.setDate(result.getDate() - duration.days);
107
+ return result;
101
108
  }
102
109
  var retentionRegistry = /* @__PURE__ */ new Map();
110
+ var DEFAULT_USER_ID_FIELD = "userId";
111
+ var userIdFieldRegistry = /* @__PURE__ */ new Map();
112
+ function registerEntityUserIdField(entityName, field) {
113
+ userIdFieldRegistry.set(entityName, field);
114
+ }
115
+ function getEntityUserIdField(entityName) {
116
+ return userIdFieldRegistry.get(entityName) ?? DEFAULT_USER_ID_FIELD;
117
+ }
118
+ function getAllUserIdFields() {
119
+ return userIdFieldRegistry;
120
+ }
103
121
  function registerEntityRetention(entityName, policy) {
104
122
  retentionRegistry.set(entityName, policy);
105
123
  }
@@ -223,7 +241,10 @@ function defineComplianceEntity(meta) {
223
241
  }
224
242
  registerEntityRetention(entityName, meta.retention);
225
243
  }
226
- const { retention: _retention, ...mikroMeta } = meta;
244
+ if (meta.userIdField) {
245
+ registerEntityUserIdField(entityName, meta.userIdField);
246
+ }
247
+ const { retention: _retention, userIdField: _userIdField, ...mikroMeta } = meta;
227
248
  return (0, import_core2.defineEntity)(
228
249
  mikroMeta
229
250
  );
@@ -516,13 +537,16 @@ function escapeSqlString(value) {
516
537
  entityHasEncryptedFields,
517
538
  fp,
518
539
  getAllRetentionPolicies,
540
+ getAllUserIdFields,
519
541
  getComplianceMetadata,
520
542
  getEntityComplianceFields,
521
543
  getEntityRetention,
544
+ getEntityUserIdField,
522
545
  getSuperAdminContext,
523
546
  parseDuration,
524
547
  setupRls,
525
548
  setupTenantFilter,
549
+ subtractDuration,
526
550
  wrapEmWithNativeQueryBlocking
527
551
  });
528
552
  //# sourceMappingURL=index.js.map
@@ -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 ExtractInner,\n type ForklaunchPropertyBuilders,\n type WithCompliance,\n getComplianceMetadata,\n getEntityComplianceFields,\n entityHasEncryptedFields,\n // Retention\n RetentionAction,\n type RetentionAction as RetentionActionType,\n type RetentionPolicy,\n RetentionDuration,\n parseDuration,\n getEntityRetention,\n getAllRetentionPolicies\n} from './complianceTypes';\n\n// Compliance-aware property builder (drop-in replacement for MikroORM's p)\nexport { fp } from './compliancePropertyBuilder';\n\n// Compliance-aware entity definition (drop-in replacement for MikroORM's defineEntity)\nexport { defineComplianceEntity } from './defineComplianceEntity';\n\n// Compliance EventSubscriber (encrypt on persist, decrypt on load)\nexport {\n ComplianceEventSubscriber,\n wrapEmWithNativeQueryBlocking\n} from './complianceEventSubscriber';\n\n// Re-export InferEntity from MikroORM for convenience\nexport type { InferEntity } from '@mikro-orm/core';\n\n// Tenant isolation filter\nexport {\n setupTenantFilter,\n getSuperAdminContext,\n createTenantFilterDef,\n TENANT_FILTER_NAME\n} from './tenantFilter';\n\n// PostgreSQL Row-Level Security\nexport { setupRls, RlsEventSubscriber, type RlsConfig } from './rls';\n","import type {\n EmptyOptions,\n PropertyBuilders,\n PropertyChain\n} from '@mikro-orm/core';\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\nexport const COMPLIANCE_KEY = '~compliance' as const;\n\n// ---------------------------------------------------------------------------\n// Registry\n// ---------------------------------------------------------------------------\n\nconst complianceRegistry = new Map<string, Map<string, ComplianceLevel>>();\n\nexport function registerEntityCompliance(\n entityName: string,\n fields: Map<string, ComplianceLevel>\n): void {\n complianceRegistry.set(entityName, fields);\n}\n\nexport function getComplianceMetadata(\n entityName: string,\n fieldName: string\n): ComplianceLevel {\n return complianceRegistry.get(entityName)?.get(fieldName) ?? 'none';\n}\n\nexport function getEntityComplianceFields(\n entityName: string\n): Map<string, ComplianceLevel> | undefined {\n return complianceRegistry.get(entityName);\n}\n\nexport function entityHasEncryptedFields(entityName: string): boolean {\n const fields = complianceRegistry.get(entityName);\n if (!fields) return false;\n for (const level of fields.values()) {\n if (level === 'phi' || level === 'pci') return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Retention types and registry\n// ---------------------------------------------------------------------------\n\nexport const RetentionAction = {\n delete: 'delete',\n anonymize: 'anonymize'\n} as const;\nexport type RetentionAction =\n (typeof RetentionAction)[keyof typeof RetentionAction];\n\nexport interface RetentionPolicy {\n duration: string;\n action: RetentionAction;\n}\n\nexport const RetentionDuration = {\n days: (n: number): string => `P${n}D`,\n months: (n: number): string => `P${n}M`,\n years: (n: number): string => `P${n}Y`\n} as const;\n\nconst DURATION_REGEX = /^P(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)D)?$/;\nconst MS_PER_DAY = 86_400_000;\nconst MIN_DURATION_MS = MS_PER_DAY;\n\nexport function parseDuration(iso: string): number {\n const match = DURATION_REGEX.exec(iso);\n if (!match) {\n throw new Error(\n `Invalid ISO 8601 duration: '${iso}'. Expected format: P[n]Y[n]M[n]D`\n );\n }\n const years = parseInt(match[1] || '0', 10);\n const months = parseInt(match[2] || '0', 10);\n const days = parseInt(match[3] || '0', 10);\n const totalDays = years * 365 + months * 30 + days;\n const ms = totalDays * MS_PER_DAY;\n if (ms < MIN_DURATION_MS) {\n throw new Error(\n `Retention duration must be >= 1 day (P1D). Got: '${iso}' (${totalDays} days)`\n );\n }\n return ms;\n}\n\nconst retentionRegistry = new Map<string, RetentionPolicy>();\n\nexport function registerEntityRetention(\n entityName: string,\n policy: RetentionPolicy\n): void {\n retentionRegistry.set(entityName, policy);\n}\n\nexport function getEntityRetention(\n entityName: string\n): RetentionPolicy | undefined {\n return retentionRegistry.get(entityName);\n}\n\nexport function getAllRetentionPolicies(): ReadonlyMap<\n string,\n RetentionPolicy\n> {\n return retentionRegistry;\n}\n\n// ---------------------------------------------------------------------------\n// ClassifiedProperty — tagged wrapper: inner builder + compliance level\n// ---------------------------------------------------------------------------\n\n/**\n * Tagged wrapper returned by `.compliance()`.\n * `__inner` is the EXACT MikroORM builder type.\n * `defineComplianceEntity` checks for this wrapper and extracts `__inner`.\n */\nexport interface ClassifiedProperty<\n Builder = unknown,\n Value = unknown,\n Options = unknown\n> {\n readonly __inner: Builder;\n readonly __compliance: ComplianceLevel;\n readonly '~type'?: { value: Value };\n readonly '~options': Options;\n}\n\n// ---------------------------------------------------------------------------\n// ExtractInner — unwraps ClassifiedProperty to get the raw builder\n// ---------------------------------------------------------------------------\n\n/**\n * For each property, if it's a ClassifiedProperty, extract __inner.\n * Functions (lazy relations) and raw PropertyChain (relations) pass through.\n */\nexport type ExtractInner<T> = {\n [K in keyof T]: T[K] extends ClassifiedProperty<infer B> ? B : T[K];\n};\n\n// ---------------------------------------------------------------------------\n// WithCompliance — adds .compliance() to any MikroORM builder\n// ---------------------------------------------------------------------------\n\n/**\n * Adds `.compliance()` to a builder AND remaps chain methods so\n * `.compliance()` persists. Each chain method returns WithCompliance\n * wrapping the chain result. `.compliance()` captures the current\n * builder as-is into ClassifiedProperty.\n */\nexport type WithCompliance<T> = {\n [K in keyof T]: T[K] extends (...args: infer A) => infer R\n ? R extends { '~type'?: unknown; '~options': unknown }\n ? (...args: A) => WithCompliance<R>\n : T[K]\n : T[K];\n} & {\n compliance(\n level: ComplianceLevel\n ): T extends { '~type'?: { value: infer V }; '~options': infer O }\n ? ClassifiedProperty<T, V, O>\n : ClassifiedProperty<T>;\n};\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyBuilders — the type of `fp`\n// ---------------------------------------------------------------------------\n\ntype RelationBuilderKeys =\n | 'manyToOne'\n | 'oneToMany'\n | 'manyToMany'\n | 'oneToOne'\n | 'embedded';\n\n// Generic methods whose type params get collapsed by mapped types\ntype GenericBuilderKeys =\n | 'json'\n | 'formula'\n | 'type'\n | 'enum'\n | 'bigint'\n | 'array'\n | 'decimal';\n\n/**\n * Each scalar method wraps the EXACT MikroORM return type with\n * WithCompliance. Relations pass through unchanged.\n * Generic methods have explicit signatures to preserve type params.\n */\nexport type ForklaunchPropertyBuilders = {\n [K in Exclude<\n keyof PropertyBuilders,\n RelationBuilderKeys | GenericBuilderKeys\n >]: PropertyBuilders[K] extends (...args: infer A) => infer R\n ? (...args: A) => WithCompliance<R>\n : PropertyBuilders[K];\n} & {\n [K in RelationBuilderKeys]: PropertyBuilders[K];\n} & {\n json: <T>() => WithCompliance<PropertyChain<T, EmptyOptions>>;\n formula: <T>(\n formula: string | ((...args: never[]) => string)\n ) => WithCompliance<PropertyChain<T, EmptyOptions>>;\n type: <T>(type: T) => WithCompliance<PropertyChain<T, EmptyOptions>>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n enum: <const T extends (number | string)[] | (() => Record<string, any>)>(\n items?: T\n ) => WithCompliance<\n PropertyChain<\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n T extends () => Record<string, any>\n ? T extends () => infer R\n ? R[keyof R]\n : never\n : T extends (infer Value)[]\n ? Value\n : T,\n EmptyOptions & { kind: 'enum' }\n >\n >;\n bigint: <Mode extends 'bigint' | 'number' | 'string' = 'bigint'>(\n mode?: Mode\n ) => WithCompliance<\n PropertyChain<\n Mode extends 'bigint' ? bigint : Mode extends 'number' ? number : string,\n EmptyOptions\n >\n >;\n array: <T = string>(\n toJsValue?: (i: string) => T,\n toDbValue?: (i: T) => string\n ) => WithCompliance<PropertyChain<T[], EmptyOptions>>;\n decimal: <Mode extends 'number' | 'string' = 'string'>(\n mode?: Mode\n ) => WithCompliance<\n PropertyChain<Mode extends 'number' ? number : string, EmptyOptions>\n >;\n};\n","import { p } from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n type ComplianceLevel,\n type ForklaunchPropertyBuilders\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Runtime Proxy implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a value is a MikroORM property builder (has ~options).\n */\nfunction isBuilder(value: unknown): value is object {\n return (\n value != null &&\n typeof value === 'object' &&\n '~options' in (value as Record<string, unknown>)\n );\n}\n\n/**\n * Wraps a MikroORM scalar PropertyBuilder in a Proxy that:\n * 1. Adds a `.compliance(level)` method\n * 2. Forwards all other method calls to the underlying builder\n * 3. Re-wraps returned builders so `.compliance()` persists through chains\n */\nfunction wrapUnclassified(builder: unknown): unknown {\n return new Proxy(builder as object, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === 'compliance') {\n return (level: ComplianceLevel) => wrapClassified(target, level);\n }\n if (prop === '~options') return Reflect.get(target, prop, target);\n if (prop === COMPLIANCE_KEY) return undefined;\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a builder that has been classified via `.compliance()`.\n * Stores the compliance level under `~compliance` for `defineComplianceEntity`.\n * Chaining after `.compliance()` propagates the level through subsequent builders.\n */\nfunction wrapClassified(builder: object, level: ComplianceLevel): unknown {\n return new Proxy(builder, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === COMPLIANCE_KEY) return level;\n if (prop === '~options') return Reflect.get(target, prop, target);\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapClassified(result, level) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a relation PropertyBuilder (manyToOne, oneToMany, etc.).\n * Auto-classified as 'none' — no `.compliance()` call needed.\n * All chained methods preserve the auto-classification.\n */\nfunction wrapRelation(builder: object): unknown {\n return wrapClassified(builder, 'none');\n}\n\n// ---------------------------------------------------------------------------\n// Relation method detection\n// ---------------------------------------------------------------------------\n\nconst RELATION_METHODS = new Set([\n 'manyToOne',\n 'oneToMany',\n 'manyToMany',\n 'oneToOne',\n 'embedded'\n]);\n\nfunction isRelationMethod(prop: string | symbol): boolean {\n return typeof prop === 'string' && RELATION_METHODS.has(prop);\n}\n\n// ---------------------------------------------------------------------------\n// fp — the ForkLaunch property builder\n// ---------------------------------------------------------------------------\n\n/**\n * ForkLaunch property builder. Drop-in replacement for MikroORM's `p`\n * that adds `.compliance(level)` to every scalar property builder\n * and auto-classifies relation builders as 'none'.\n *\n * - Scalar fields: `fp.string().compliance('pii')` — must call `.compliance()`\n * - Relation fields: `fp.manyToOne(Target)` — auto-classified, no `.compliance()` needed\n *\n * @example\n * ```typescript\n * import { defineComplianceEntity, fp } from '@forklaunch/core/persistence';\n *\n * const User = defineComplianceEntity({\n * name: 'User',\n * properties: {\n * id: fp.uuid().primary().compliance('none'),\n * email: fp.string().unique().compliance('pii'),\n * medicalRecord: fp.string().nullable().compliance('phi'),\n * organization: () => fp.manyToOne(Organization).nullable(),\n * }\n * });\n * ```\n */\nexport const fp: ForklaunchPropertyBuilders = new Proxy(p, {\n get(target: Record<string | symbol, unknown>, prop) {\n const value = Reflect.get(target, prop, target);\n if (typeof value !== 'function') return value;\n\n if (isRelationMethod(prop)) {\n // Relation methods: call the original, wrap result as auto-classified 'none'\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapRelation(result) : result;\n };\n }\n\n // Scalar methods: call the original, wrap result with .compliance()\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n}) as ForklaunchPropertyBuilders;\n","import type { InferEntity } from '@mikro-orm/core';\nimport {\n defineEntity,\n p,\n type EntityMetadataWithProperties,\n type EntitySchemaWithMeta,\n type InferEntityFromProperties\n} from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n parseDuration,\n registerEntityCompliance,\n registerEntityRetention,\n type ClassifiedProperty,\n type ComplianceLevel,\n type RetentionPolicy\n} from './complianceTypes';\n\ntype ValidateProperties<T> = {\n [K in keyof T]: T[K] extends ClassifiedProperty\n ? T[K]\n : T[K] extends (...args: never[]) => unknown\n ? T[K]\n : ClassifiedProperty;\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\nexport function defineComplianceEntity<\n const TName extends string,\n const TTableName extends string,\n const TProperties extends Record<string, unknown>,\n const TPK extends (keyof TProperties)[] | undefined = undefined,\n const TBase = never,\n const TRepository = never,\n const TForceObject extends boolean = false\n>(\n meta: EntityMetadataWithProperties<\n TName,\n TTableName,\n TProperties & ValidateProperties<TProperties>,\n TPK,\n TBase,\n TRepository,\n TForceObject\n > & { retention?: RetentionPolicy }\n): EntitySchemaWithMeta<\n TName,\n TTableName,\n InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>,\n TBase,\n TProperties\n> {\n const entityName = 'name' in meta ? (meta.name as string) : 'Unknown';\n const complianceFields = new Map<string, ComplianceLevel>();\n\n const rawProperties = meta.properties;\n const resolvedProperties: Record<string, unknown> =\n typeof rawProperties === 'function' ? rawProperties(p) : rawProperties;\n\n for (const [fieldName, rawProp] of Object.entries(resolvedProperties)) {\n const prop = typeof rawProp === 'function' ? rawProp() : rawProp;\n const level = readComplianceLevel(prop);\n\n if (level == null) {\n throw new Error(\n `Field '${entityName}.${fieldName}' is missing compliance classification. ` +\n `Call .compliance('pii' | 'phi' | 'pci' | 'none') on this property, ` +\n `or use a relation method (fp.manyToOne, etc.) which is auto-classified.`\n );\n }\n complianceFields.set(fieldName, level);\n }\n\n registerEntityCompliance(entityName, complianceFields);\n\n // Handle retention policy\n if (meta.retention) {\n parseDuration(meta.retention.duration); // validates at boot — throws if invalid\n\n if (!resolvedProperties['createdAt']) {\n throw new Error(\n `Entity '${entityName}' has a retention policy but no 'createdAt' property. ` +\n `Retention requires createdAt to compute expiration.`\n );\n }\n\n registerEntityRetention(entityName, meta.retention);\n }\n\n // Strip retention before passing to MikroORM's defineEntity\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { retention: _retention, ...mikroMeta } = meta;\n\n return defineEntity(\n mikroMeta as unknown as typeof meta\n ) as EntitySchemaWithMeta<\n TName,\n TTableName,\n InferEntityFromProperties<\n TProperties,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >,\n TBase,\n TProperties\n >;\n}\n\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 object>(em: T): T {\n const BLOCKED_METHODS = [\n 'nativeInsert',\n 'nativeUpdate',\n 'nativeDelete'\n ] as const;\n\n return new Proxy(em, {\n get(target, prop, receiver) {\n if (\n typeof prop === 'string' &&\n BLOCKED_METHODS.includes(prop as (typeof BLOCKED_METHODS)[number])\n ) {\n return (entityNameOrEntity: unknown, ...rest: unknown[]) => {\n const entityName = resolveEntityName(entityNameOrEntity);\n if (entityName) {\n const fields = getEntityComplianceFields(entityName);\n if (fields) {\n for (const [fieldName, level] of fields) {\n if (ENCRYPTED_LEVELS.has(level)) {\n throw new EncryptionRequiredError(\n `${prop}() blocked on entity '${entityName}' because field ` +\n `'${fieldName}' has compliance level '${level}'. ` +\n `Use em.create() + em.flush() instead to ensure encryption.`\n );\n }\n }\n }\n }\n // No compliance fields requiring encryption — allow the native query\n const method = Reflect.get(target, prop, receiver);\n return (method as (...args: unknown[]) => unknown).call(\n target,\n entityNameOrEntity,\n ...rest\n );\n };\n }\n return Reflect.get(target, prop, receiver);\n }\n });\n}\n\n/**\n * Resolve an entity name from the first argument to nativeInsert/Update/Delete.\n * MikroORM accepts entity name strings, entity class references, or entity instances.\n */\nfunction resolveEntityName(entityNameOrEntity: unknown): string | undefined {\n if (typeof entityNameOrEntity === 'string') {\n return entityNameOrEntity;\n }\n if (typeof entityNameOrEntity === 'function') {\n return (entityNameOrEntity as { name?: string }).name;\n }\n if (\n entityNameOrEntity != null &&\n typeof entityNameOrEntity === 'object' &&\n 'constructor' in entityNameOrEntity\n ) {\n return (entityNameOrEntity.constructor as { name?: string }).name;\n }\n return undefined;\n}\n","import type { Dictionary, EntityManager, FilterDef } from '@mikro-orm/core';\n\n/**\n * The name used to register the tenant isolation filter.\n */\nexport const TENANT_FILTER_NAME = 'tenant';\n\n/**\n * Creates the tenant filter definition.\n *\n * The filter adds `WHERE organizationId = :tenantId` to all queries\n * on entities that have an `organizationId` or `organization` property.\n * Entities without either property are unaffected (empty condition).\n */\nexport function createTenantFilterDef(): FilterDef {\n return {\n name: TENANT_FILTER_NAME,\n cond(\n args: Dictionary,\n _type: 'read' | 'update' | 'delete',\n em: EntityManager,\n _options?: unknown,\n entityName?: string\n ) {\n if (!entityName) {\n return {};\n }\n\n try {\n const metadata = em.getMetadata().getByClassName(entityName, false);\n if (!metadata) {\n return {};\n }\n\n const hasOrganizationId = metadata.properties['organizationId'] != null;\n const hasOrganization = metadata.properties['organization'] != null;\n\n if (hasOrganizationId || hasOrganization) {\n return { organizationId: args.tenantId };\n }\n } catch {\n // Entity not found in metadata — skip filtering\n }\n\n return {};\n },\n default: true,\n args: true\n };\n}\n\n/**\n * Registers the global tenant isolation filter on the ORM's entity manager.\n * Call this once at application bootstrap after `MikroORM.init()`.\n *\n * After calling this, every fork of the EM will inherit the filter.\n * Set the tenant ID per-request via:\n *\n * ```ts\n * em.setFilterParams('tenant', { tenantId: 'org-123' });\n * ```\n */\nexport function setupTenantFilter(orm: {\n em: Pick<EntityManager, 'addFilter'>;\n}): void {\n orm.em.addFilter(createTenantFilterDef());\n}\n\n/**\n * Returns a forked EntityManager with the tenant filter disabled.\n *\n * Use this only from code paths that have verified super-admin permissions.\n * Queries executed through the returned EM will return cross-tenant data.\n */\nexport function getSuperAdminContext(\n em: Pick<EntityManager, 'fork'>\n): ReturnType<EntityManager['fork']> {\n const forked = em.fork();\n forked.setFilterParams(TENANT_FILTER_NAME, { tenantId: undefined });\n // Disable the filter by passing false for the filter in each query isn't\n // sufficient globally; instead we add the filter with enabled = false.\n // The cleanest way is to re-add the filter as disabled on this fork.\n forked.addFilter({\n name: TENANT_FILTER_NAME,\n cond: {},\n default: false\n });\n return forked;\n}\n","import type { EntityManager, EventSubscriber, MikroORM } from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n/**\n * Structural subset of {@link TransactionEventArgs} used by\n * `RlsEventSubscriber` so that callers (and tests) only need to provide\n * the properties the subscriber actually reads.\n */\nexport interface RlsTransactionEventArgs {\n em: {\n getFilterParams: EntityManager['getFilterParams'];\n getConnection(): {\n execute(\n query: string,\n params?: unknown[],\n method?: 'all' | 'get' | 'run',\n ctx?: unknown\n ): Promise<unknown>;\n };\n };\n transaction?: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface RlsConfig {\n /**\n * Whether to enable PostgreSQL Row-Level Security.\n * Defaults to `true` when the driver is PostgreSQL, `false` otherwise.\n * Set to `false` to opt out even on PostgreSQL.\n */\n enabled?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// RLS EventSubscriber\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM EventSubscriber that executes `SET LOCAL app.tenant_id = :tenantId`\n * at the start of every transaction when PostgreSQL RLS is enabled.\n *\n * This ensures that even if the MikroORM global filter is somehow bypassed,\n * the database-level RLS policy enforces tenant isolation.\n *\n * The tenant ID is read from the EntityManager's filter parameters\n * (set by the tenant context middleware).\n */\nexport class RlsEventSubscriber implements EventSubscriber {\n async afterTransactionStart(args: RlsTransactionEventArgs): Promise<void> {\n const tenantId = this.getTenantId(args.em);\n if (!tenantId) {\n // No tenant context (e.g., super-admin or public route) — skip SET LOCAL\n return;\n }\n\n const connection = args.em.getConnection();\n // Execute SET LOCAL within the transaction context\n // SET LOCAL only persists for the current transaction — no connection leakage\n await connection.execute(\n `SET LOCAL app.tenant_id = '${escapeSqlString(tenantId)}'`,\n [],\n 'run',\n args.transaction\n );\n }\n\n private getTenantId(\n em: Pick<EntityManager, 'getFilterParams'>\n ): string | undefined {\n const params = em.getFilterParams(TENANT_FILTER_NAME) as\n | { tenantId?: string }\n | undefined;\n return params?.tenantId;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Setup\n// ---------------------------------------------------------------------------\n\n/**\n * Sets up PostgreSQL Row-Level Security integration.\n *\n * 1. Registers the `RlsEventSubscriber` to run `SET LOCAL app.tenant_id`\n * at the start of every transaction.\n * 2. Validates that RLS policies exist on tenant-scoped tables (warns if missing).\n *\n * Call this at application bootstrap after `MikroORM.init()` and `setupTenantFilter()`.\n *\n * @param orm - The initialized MikroORM instance\n * @param config - RLS configuration (enabled defaults to auto-detect PostgreSQL)\n */\nexport function setupRls(orm: MikroORM, config: RlsConfig = {}): void {\n const isPostgres = isPostgresDriver(orm);\n const enabled = config.enabled ?? isPostgres;\n\n if (!enabled) {\n if (!isPostgres) {\n // Non-PostgreSQL — RLS not available, ORM filter is the sole enforcement\n return;\n }\n // PostgreSQL but explicitly disabled\n console.info(\n '[compliance] PostgreSQL RLS disabled by configuration. ORM filter is the sole tenant enforcement layer.'\n );\n return;\n }\n\n if (!isPostgres) {\n console.warn(\n '[compliance] RLS enabled but database driver is not PostgreSQL. ' +\n 'RLS is only supported on PostgreSQL. Falling back to ORM filter only.'\n );\n return;\n }\n\n // Register the RLS transaction subscriber\n orm.em.getEventManager().registerSubscriber(new RlsEventSubscriber());\n\n // Validate RLS policies exist\n validateRlsPolicies(orm).catch((err) => {\n console.warn('[compliance] Failed to validate RLS policies:', err);\n });\n}\n\n// ---------------------------------------------------------------------------\n// RLS policy validation\n// ---------------------------------------------------------------------------\n\n/**\n * Checks that tenant-scoped entities have RLS policies on their tables.\n * Logs warnings with the SQL needed to create missing policies.\n */\nasync function validateRlsPolicies(orm: MikroORM): Promise<void> {\n const metadata = orm.em.getMetadata().getAll();\n\n for (const meta of Object.values(metadata)) {\n const hasOrgId = meta.properties['organizationId'] != null;\n const hasOrg = meta.properties['organization'] != null;\n\n if (!hasOrgId && !hasOrg) continue;\n\n const tableName = meta.tableName;\n try {\n const connection = orm.em.getConnection();\n const result = await connection.execute<{ policyname: string }[]>(\n `SELECT policyname FROM pg_policies WHERE tablename = '${escapeSqlString(tableName)}'`,\n [],\n 'all'\n );\n\n const policies = Array.isArray(result) ? result : [];\n const hasTenantPolicy = policies.some((p: { policyname: string }) =>\n p.policyname.includes('tenant')\n );\n\n if (!hasTenantPolicy) {\n console.warn(\n `[compliance] No tenant RLS policy found on table '${tableName}'. ` +\n `Create one with:\\n` +\n ` ALTER TABLE \"${tableName}\" ENABLE ROW LEVEL SECURITY;\\n` +\n ` CREATE POLICY tenant_isolation ON \"${tableName}\"\\n` +\n ` USING (organization_id = current_setting('app.tenant_id'));`\n );\n }\n } catch {\n // Query failed — likely not connected yet or table doesn't exist\n // Skip validation for this table\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the ORM is using a PostgreSQL driver.\n * Checks the platform constructor name which is 'PostgreSqlPlatform' for PG.\n */\nfunction isPostgresDriver(orm: MikroORM): boolean {\n try {\n const platform = orm.em.getPlatform();\n const name = platform.constructor.name.toLowerCase();\n return name.includes('postgre');\n } catch {\n return false;\n }\n}\n\n/**\n * Escape a string for safe inclusion in SQL. Prevents SQL injection in\n * the SET LOCAL statement.\n */\nfunction escapeSqlString(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACMO,IAAM,kBAAkB;AAAA,EAC7B,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AACR;AAIO,IAAM,iBAAiB;AAM9B,IAAM,qBAAqB,oBAAI,IAA0C;AAElE,SAAS,yBACd,YACA,QACM;AACN,qBAAmB,IAAI,YAAY,MAAM;AAC3C;AAEO,SAAS,sBACd,YACA,WACiB;AACjB,SAAO,mBAAmB,IAAI,UAAU,GAAG,IAAI,SAAS,KAAK;AAC/D;AAEO,SAAS,0BACd,YAC0C;AAC1C,SAAO,mBAAmB,IAAI,UAAU;AAC1C;AAEO,SAAS,yBAAyB,YAA6B;AACpE,QAAM,SAAS,mBAAmB,IAAI,UAAU;AAChD,MAAI,CAAC,OAAQ,QAAO;AACpB,aAAW,SAAS,OAAO,OAAO,GAAG;AACnC,QAAI,UAAU,SAAS,UAAU,MAAO,QAAO;AAAA,EACjD;AACA,SAAO;AACT;AAMO,IAAM,kBAAkB;AAAA,EAC7B,QAAQ;AAAA,EACR,WAAW;AACb;AASO,IAAM,oBAAoB;AAAA,EAC/B,MAAM,CAAC,MAAsB,IAAI,CAAC;AAAA,EAClC,QAAQ,CAAC,MAAsB,IAAI,CAAC;AAAA,EACpC,OAAO,CAAC,MAAsB,IAAI,CAAC;AACrC;AAEA,IAAM,iBAAiB;AACvB,IAAM,aAAa;AACnB,IAAM,kBAAkB;AAEjB,SAAS,cAAc,KAAqB;AACjD,QAAM,QAAQ,eAAe,KAAK,GAAG;AACrC,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,+BAA+B,GAAG;AAAA,IACpC;AAAA,EACF;AACA,QAAM,QAAQ,SAAS,MAAM,CAAC,KAAK,KAAK,EAAE;AAC1C,QAAM,SAAS,SAAS,MAAM,CAAC,KAAK,KAAK,EAAE;AAC3C,QAAM,OAAO,SAAS,MAAM,CAAC,KAAK,KAAK,EAAE;AACzC,QAAM,YAAY,QAAQ,MAAM,SAAS,KAAK;AAC9C,QAAM,KAAK,YAAY;AACvB,MAAI,KAAK,iBAAiB;AACxB,UAAM,IAAI;AAAA,MACR,oDAAoD,GAAG,MAAM,SAAS;AAAA,IACxE;AAAA,EACF;AACA,SAAO;AACT;AAEA,IAAM,oBAAoB,oBAAI,IAA6B;AAEpD,SAAS,wBACd,YACA,QACM;AACN,oBAAkB,IAAI,YAAY,MAAM;AAC1C;AAEO,SAAS,mBACd,YAC6B;AAC7B,SAAO,kBAAkB,IAAI,UAAU;AACzC;AAEO,SAAS,0BAGd;AACA,SAAO;AACT;;;ACtHA,kBAAkB;AAclB,SAAS,UAAU,OAAiC;AAClD,SACE,SAAS,QACT,OAAO,UAAU,YACjB,cAAe;AAEnB;AAQA,SAAS,iBAAiB,SAA2B;AACnD,SAAO,IAAI,MAAM,SAAmB;AAAA,IAClC,IAAI,QAA0C,MAAM;AAClD,UAAI,SAAS,cAAc;AACzB,eAAO,CAAC,UAA2B,eAAe,QAAQ,KAAK;AAAA,MACjE;AACA,UAAI,SAAS,WAAY,QAAO,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAChE,UAAI,SAAS,eAAgB,QAAO;AAEpC,YAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAC9C,UAAI,OAAO,UAAU,YAAY;AAC/B,eAAO,IAAI,SAAoB;AAC7B,gBAAM,SAAU,MAA0C;AAAA,YACxD;AAAA,YACA;AAAA,UACF;AACA,iBAAO,UAAU,MAAM,IAAI,iBAAiB,MAAM,IAAI;AAAA,QACxD;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAOA,SAAS,eAAe,SAAiB,OAAiC;AACxE,SAAO,IAAI,MAAM,SAAS;AAAA,IACxB,IAAI,QAA0C,MAAM;AAClD,UAAI,SAAS,eAAgB,QAAO;AACpC,UAAI,SAAS,WAAY,QAAO,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAEhE,YAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAC9C,UAAI,OAAO,UAAU,YAAY;AAC/B,eAAO,IAAI,SAAoB;AAC7B,gBAAM,SAAU,MAA0C;AAAA,YACxD;AAAA,YACA;AAAA,UACF;AACA,iBAAO,UAAU,MAAM,IAAI,eAAe,QAAQ,KAAK,IAAI;AAAA,QAC7D;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAOA,SAAS,aAAa,SAA0B;AAC9C,SAAO,eAAe,SAAS,MAAM;AACvC;AAMA,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,iBAAiB,MAAgC;AACxD,SAAO,OAAO,SAAS,YAAY,iBAAiB,IAAI,IAAI;AAC9D;AA6BO,IAAM,KAAiC,IAAI,MAAM,eAAG;AAAA,EACzD,IAAI,QAA0C,MAAM;AAClD,UAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAC9C,QAAI,OAAO,UAAU,WAAY,QAAO;AAExC,QAAI,iBAAiB,IAAI,GAAG;AAE1B,aAAO,IAAI,SAAoB;AAC7B,cAAM,SAAU,MAA0C;AAAA,UACxD;AAAA,UACA;AAAA,QACF;AACA,eAAO,UAAU,MAAM,IAAI,aAAa,MAAM,IAAI;AAAA,MACpD;AAAA,IACF;AAGA,WAAO,IAAI,SAAoB;AAC7B,YAAM,SAAU,MAA0C;AAAA,QACxD;AAAA,QACA;AAAA,MACF;AACA,aAAO,UAAU,MAAM,IAAI,iBAAiB,MAAM,IAAI;AAAA,IACxD;AAAA,EACF;AACF,CAAC;;;AC1JD,IAAAA,eAMO;AAmBP,SAAS,oBAAoB,SAA+C;AAC1E,MAAI,WAAW,QAAQ,OAAO,YAAY,SAAU,QAAO;AAC3D,SAAQ,QAAoC,cAAc;AAG5D;AAEO,SAAS,uBASd,MAeA;AACA,QAAM,aAAa,UAAU,OAAQ,KAAK,OAAkB;AAC5D,QAAM,mBAAmB,oBAAI,IAA6B;AAE1D,QAAM,gBAAgB,KAAK;AAC3B,QAAM,qBACJ,OAAO,kBAAkB,aAAa,cAAc,cAAC,IAAI;AAE3D,aAAW,CAAC,WAAW,OAAO,KAAK,OAAO,QAAQ,kBAAkB,GAAG;AACrE,UAAM,OAAO,OAAO,YAAY,aAAa,QAAQ,IAAI;AACzD,UAAM,QAAQ,oBAAoB,IAAI;AAEtC,QAAI,SAAS,MAAM;AACjB,YAAM,IAAI;AAAA,QACR,UAAU,UAAU,IAAI,SAAS;AAAA,MAGnC;AAAA,IACF;AACA,qBAAiB,IAAI,WAAW,KAAK;AAAA,EACvC;AAEA,2BAAyB,YAAY,gBAAgB;AAGrD,MAAI,KAAK,WAAW;AAClB,kBAAc,KAAK,UAAU,QAAQ;AAErC,QAAI,CAAC,mBAAmB,WAAW,GAAG;AACpC,YAAM,IAAI;AAAA,QACR,WAAW,UAAU;AAAA,MAEvB;AAAA,IACF;AAEA,4BAAwB,YAAY,KAAK,SAAS;AAAA,EACpD;AAIA,QAAM,EAAE,WAAW,YAAY,GAAG,UAAU,IAAI;AAEhD,aAAO;AAAA,IACL;AAAA,EACF;AAaF;;;ACrGO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAChC,OAAO;AAAA,EAChB,YACE,UAAU,wEACV;AACA,UAAM,OAAO;AAAA,EACf;AACF;AAEO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EACxC,OAAO;AAAA,EAChB,YACE,UAAU,kEACV;AACA,UAAM,OAAO;AAAA,EACf;AACF;AAUA,IAAM,YAAY,OAAO,MAAM,CAAC;;;ACpBhC,IAAM,mBAAmB;AAMzB,IAAM,mBAAiD,oBAAI,IAAI,CAAC,OAAO,KAAK,CAAC;AAoBtE,IAAM,4BAAN,MAA2D;AAAA,EAC/C;AAAA,EAEjB,YAAY,WAA2B;AACrC,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAM,aAAa,MAAyC;AAC1D,SAAK,cAAc,IAAI;AAAA,EACzB;AAAA,EAEA,MAAM,aAAa,MAAyC;AAC1D,SAAK,cAAc,IAAI;AAAA,EACzB;AAAA,EAEA,MAAM,OAAO,MAAyC;AACpD,SAAK,cAAc,IAAI;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,MAAgC;AACpD,UAAM,aAAa,KAAK,KAAK;AAC7B,UAAM,mBAAmB,0BAA0B,UAAU;AAC7D,QAAI,CAAC,iBAAkB;AAEvB,UAAM,WAAW,KAAK,YAAY,KAAK,EAAE;AACzC,UAAM,SAAS,KAAK;AAEpB,eAAW,CAAC,WAAW,KAAK,KAAK,kBAAkB;AACjD,UAAI,CAAC,iBAAiB,IAAI,KAAK,EAAG;AAElC,YAAM,QAAQ,OAAO,SAAS;AAC9B,UAAI,UAAU,QAAQ,UAAU,OAAW;AAC3C,UAAI,OAAO,UAAU,SAAU;AAG/B,UAAI,MAAM,WAAW,gBAAgB,EAAG;AAExC,aAAO,SAAS,IAAI,KAAK,UAAU,QAAQ,OAAO,QAAQ;AAAA,IAC5D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,MAAgC;AACpD,UAAM,aAAa,KAAK,KAAK;AAC7B,UAAM,mBAAmB,0BAA0B,UAAU;AAC7D,QAAI,CAAC,iBAAkB;AAEvB,UAAM,WAAW,KAAK,YAAY,KAAK,EAAE;AACzC,UAAM,SAAS,KAAK;AAEpB,eAAW,CAAC,WAAW,KAAK,KAAK,kBAAkB;AACjD,UAAI,CAAC,iBAAiB,IAAI,KAAK,EAAG;AAElC,YAAM,QAAQ,OAAO,SAAS;AAC9B,UAAI,UAAU,QAAQ,UAAU,OAAW;AAC3C,UAAI,OAAO,UAAU,SAAU;AAE/B,UAAI,MAAM,WAAW,gBAAgB,GAAG;AAEtC,YAAI;AACF,iBAAO,SAAS,IAAI,KAAK,UAAU,QAAQ,OAAO,QAAQ;AAAA,QAC5D,SAAS,KAAK;AACZ,cAAI,eAAe,iBAAiB;AAClC,kBAAM,IAAI;AAAA,cACR,qBAAqB,UAAU,IAAI,SAAS,KAAK,IAAI,OAAO;AAAA,YAC9D;AAAA,UACF;AACA,gBAAM;AAAA,QACR;AAAA,MACF,OAAO;AAEL,gBAAQ;AAAA,UACN,gBAAgB,UAAU,IAAI,SAAS,yBAAyB,KAAK;AAAA,QAEvE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,YAAY,IAA2B;AAC7C,UAAM,UAAU,GAAG,gBAAgB,QAAQ;AAG3C,UAAM,WAAW,SAAS;AAC1B,QAAI,CAAC,UAAU;AACb,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;AAiBO,SAAS,8BAAgD,IAAU;AACxE,QAAM,kBAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,IAAI,MAAM,IAAI;AAAA,IACnB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UACE,OAAO,SAAS,YAChB,gBAAgB,SAAS,IAAwC,GACjE;AACA,eAAO,CAAC,uBAAgC,SAAoB;AAC1D,gBAAM,aAAa,kBAAkB,kBAAkB;AACvD,cAAI,YAAY;AACd,kBAAM,SAAS,0BAA0B,UAAU;AACnD,gBAAI,QAAQ;AACV,yBAAW,CAAC,WAAW,KAAK,KAAK,QAAQ;AACvC,oBAAI,iBAAiB,IAAI,KAAK,GAAG;AAC/B,wBAAM,IAAI;AAAA,oBACR,GAAG,IAAI,yBAAyB,UAAU,oBACpC,SAAS,2BAA2B,KAAK;AAAA,kBAEjD;AAAA,gBACF;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAEA,gBAAM,SAAS,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AACjD,iBAAQ,OAA2C;AAAA,YACjD;AAAA,YACA;AAAA,YACA,GAAG;AAAA,UACL;AAAA,QACF;AAAA,MACF;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;AAMA,SAAS,kBAAkB,oBAAiD;AAC1E,MAAI,OAAO,uBAAuB,UAAU;AAC1C,WAAO;AAAA,EACT;AACA,MAAI,OAAO,uBAAuB,YAAY;AAC5C,WAAQ,mBAAyC;AAAA,EACnD;AACA,MACE,sBAAsB,QACtB,OAAO,uBAAuB,YAC9B,iBAAiB,oBACjB;AACA,WAAQ,mBAAmB,YAAkC;AAAA,EAC/D;AACA,SAAO;AACT;;;AClOO,IAAM,qBAAqB;AAS3B,SAAS,wBAAmC;AACjD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,KACE,MACA,OACA,IACA,UACA,YACA;AACA,UAAI,CAAC,YAAY;AACf,eAAO,CAAC;AAAA,MACV;AAEA,UAAI;AACF,cAAM,WAAW,GAAG,YAAY,EAAE,eAAe,YAAY,KAAK;AAClE,YAAI,CAAC,UAAU;AACb,iBAAO,CAAC;AAAA,QACV;AAEA,cAAM,oBAAoB,SAAS,WAAW,gBAAgB,KAAK;AACnE,cAAM,kBAAkB,SAAS,WAAW,cAAc,KAAK;AAE/D,YAAI,qBAAqB,iBAAiB;AACxC,iBAAO,EAAE,gBAAgB,KAAK,SAAS;AAAA,QACzC;AAAA,MACF,QAAQ;AAAA,MAER;AAEA,aAAO,CAAC;AAAA,IACV;AAAA,IACA,SAAS;AAAA,IACT,MAAM;AAAA,EACR;AACF;AAaO,SAAS,kBAAkB,KAEzB;AACP,MAAI,GAAG,UAAU,sBAAsB,CAAC;AAC1C;AAQO,SAAS,qBACd,IACmC;AACnC,QAAM,SAAS,GAAG,KAAK;AACvB,SAAO,gBAAgB,oBAAoB,EAAE,UAAU,OAAU,CAAC;AAIlE,SAAO,UAAU;AAAA,IACf,MAAM;AAAA,IACN,MAAM,CAAC;AAAA,IACP,SAAS;AAAA,EACX,CAAC;AACD,SAAO;AACT;;;ACtCO,IAAM,qBAAN,MAAoD;AAAA,EACzD,MAAM,sBAAsB,MAA8C;AACxE,UAAM,WAAW,KAAK,YAAY,KAAK,EAAE;AACzC,QAAI,CAAC,UAAU;AAEb;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,GAAG,cAAc;AAGzC,UAAM,WAAW;AAAA,MACf,8BAA8B,gBAAgB,QAAQ,CAAC;AAAA,MACvD,CAAC;AAAA,MACD;AAAA,MACA,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EAEQ,YACN,IACoB;AACpB,UAAM,SAAS,GAAG,gBAAgB,kBAAkB;AAGpD,WAAO,QAAQ;AAAA,EACjB;AACF;AAkBO,SAAS,SAAS,KAAe,SAAoB,CAAC,GAAS;AACpE,QAAM,aAAa,iBAAiB,GAAG;AACvC,QAAM,UAAU,OAAO,WAAW;AAElC,MAAI,CAAC,SAAS;AACZ,QAAI,CAAC,YAAY;AAEf;AAAA,IACF;AAEA,YAAQ;AAAA,MACN;AAAA,IACF;AACA;AAAA,EACF;AAEA,MAAI,CAAC,YAAY;AACf,YAAQ;AAAA,MACN;AAAA,IAEF;AACA;AAAA,EACF;AAGA,MAAI,GAAG,gBAAgB,EAAE,mBAAmB,IAAI,mBAAmB,CAAC;AAGpE,sBAAoB,GAAG,EAAE,MAAM,CAAC,QAAQ;AACtC,YAAQ,KAAK,iDAAiD,GAAG;AAAA,EACnE,CAAC;AACH;AAUA,eAAe,oBAAoB,KAA8B;AAC/D,QAAM,WAAW,IAAI,GAAG,YAAY,EAAE,OAAO;AAE7C,aAAW,QAAQ,OAAO,OAAO,QAAQ,GAAG;AAC1C,UAAM,WAAW,KAAK,WAAW,gBAAgB,KAAK;AACtD,UAAM,SAAS,KAAK,WAAW,cAAc,KAAK;AAElD,QAAI,CAAC,YAAY,CAAC,OAAQ;AAE1B,UAAM,YAAY,KAAK;AACvB,QAAI;AACF,YAAM,aAAa,IAAI,GAAG,cAAc;AACxC,YAAM,SAAS,MAAM,WAAW;AAAA,QAC9B,yDAAyD,gBAAgB,SAAS,CAAC;AAAA,QACnF,CAAC;AAAA,QACD;AAAA,MACF;AAEA,YAAM,WAAW,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC;AACnD,YAAM,kBAAkB,SAAS;AAAA,QAAK,CAACC,OACrCA,GAAE,WAAW,SAAS,QAAQ;AAAA,MAChC;AAEA,UAAI,CAAC,iBAAiB;AACpB,gBAAQ;AAAA,UACN,qDAAqD,SAAS;AAAA,iBAE1C,SAAS;AAAA,uCACa,SAAS;AAAA;AAAA,QAErD;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAGR;AAAA,EACF;AACF;AAUA,SAAS,iBAAiB,KAAwB;AAChD,MAAI;AACF,UAAM,WAAW,IAAI,GAAG,YAAY;AACpC,UAAM,OAAO,SAAS,YAAY,KAAK,YAAY;AACnD,WAAO,KAAK,SAAS,SAAS;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,SAAS,gBAAgB,OAAuB;AAC9C,SAAO,MAAM,QAAQ,MAAM,IAAI;AACjC;","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 ExtractInner,\n type ForklaunchPropertyBuilders,\n type WithCompliance,\n getComplianceMetadata,\n getEntityComplianceFields,\n entityHasEncryptedFields,\n // Retention\n RetentionAction,\n type RetentionAction as RetentionActionType,\n type RetentionPolicy,\n RetentionDuration,\n parseDuration,\n subtractDuration,\n type ParsedDuration,\n getEntityRetention,\n getAllRetentionPolicies,\n getEntityUserIdField,\n getAllUserIdFields\n} from './complianceTypes';\n\n// Compliance-aware property builder (drop-in replacement for MikroORM's p)\nexport { fp } from './compliancePropertyBuilder';\n\n// Compliance-aware entity definition (drop-in replacement for MikroORM's defineEntity)\nexport { defineComplianceEntity } from './defineComplianceEntity';\n\n// Compliance EventSubscriber (encrypt on persist, decrypt on load)\nexport {\n ComplianceEventSubscriber,\n wrapEmWithNativeQueryBlocking\n} from './complianceEventSubscriber';\n\n// Re-export InferEntity from MikroORM for convenience\nexport type { InferEntity } from '@mikro-orm/core';\n\n// Tenant isolation filter\nexport {\n setupTenantFilter,\n getSuperAdminContext,\n createTenantFilterDef,\n TENANT_FILTER_NAME\n} from './tenantFilter';\n\n// PostgreSQL Row-Level Security\nexport { setupRls, RlsEventSubscriber, type RlsConfig } from './rls';\n","import type {\n EmptyOptions,\n PropertyBuilders,\n PropertyChain\n} from '@mikro-orm/core';\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\nexport const COMPLIANCE_KEY = '~compliance' as const;\n\n// ---------------------------------------------------------------------------\n// Registry\n// ---------------------------------------------------------------------------\n\nconst complianceRegistry = new Map<string, Map<string, ComplianceLevel>>();\n\nexport function registerEntityCompliance(\n entityName: string,\n fields: Map<string, ComplianceLevel>\n): void {\n complianceRegistry.set(entityName, fields);\n}\n\nexport function getComplianceMetadata(\n entityName: string,\n fieldName: string\n): ComplianceLevel {\n return complianceRegistry.get(entityName)?.get(fieldName) ?? 'none';\n}\n\nexport function getEntityComplianceFields(\n entityName: string\n): Map<string, ComplianceLevel> | undefined {\n return complianceRegistry.get(entityName);\n}\n\nexport function entityHasEncryptedFields(entityName: string): boolean {\n const fields = complianceRegistry.get(entityName);\n if (!fields) return false;\n for (const level of fields.values()) {\n if (level === 'phi' || level === 'pci') return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Retention types and registry\n// ---------------------------------------------------------------------------\n\nexport const RetentionAction = {\n delete: 'delete',\n anonymize: 'anonymize'\n} as const;\nexport type RetentionAction =\n (typeof RetentionAction)[keyof typeof RetentionAction];\n\nexport interface RetentionPolicy {\n duration: string;\n action: RetentionAction;\n}\n\nexport const RetentionDuration = {\n days: (n: number): string => `P${n}D`,\n months: (n: number): string => `P${n}M`,\n years: (n: number): string => `P${n}Y`\n} as const;\n\nexport interface ParsedDuration {\n years: number;\n months: number;\n days: number;\n}\n\nconst DURATION_REGEX = /^P(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)D)?$/;\n\n/**\n * Parse an ISO 8601 duration string into calendar units.\n * Returns structured units to enable calendar-aware date arithmetic.\n */\nexport function parseDuration(iso: string): ParsedDuration {\n const match = DURATION_REGEX.exec(iso);\n if (!match) {\n throw new Error(\n `Invalid ISO 8601 duration: '${iso}'. Expected format: P[n]Y[n]M[n]D`\n );\n }\n const years = parseInt(match[1] || '0', 10);\n const months = parseInt(match[2] || '0', 10);\n const days = parseInt(match[3] || '0', 10);\n\n // Approximate total days for minimum validation only\n const approxDays = years * 365 + months * 30 + days;\n if (approxDays < 1) {\n throw new Error(\n `Retention duration must be >= 1 day (P1D). Got: '${iso}' (${approxDays} approx days)`\n );\n }\n\n return { years, months, days };\n}\n\n/**\n * Subtract a parsed duration from a date using calendar-aware arithmetic.\n * Handles month-end clamping and leap years correctly.\n */\nexport function subtractDuration(from: Date, duration: ParsedDuration): Date {\n const result = new Date(from);\n result.setFullYear(result.getFullYear() - duration.years);\n result.setMonth(result.getMonth() - duration.months);\n result.setDate(result.getDate() - duration.days);\n return result;\n}\n\nconst retentionRegistry = new Map<string, RetentionPolicy>();\n\n// ---------------------------------------------------------------------------\n// User ID field registry — maps entity name to the field linking records to a user\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_USER_ID_FIELD = 'userId';\nconst userIdFieldRegistry = new Map<string, string>();\n\nexport function registerEntityUserIdField(\n entityName: string,\n field: string\n): void {\n userIdFieldRegistry.set(entityName, field);\n}\n\nexport function getEntityUserIdField(entityName: string): string {\n return userIdFieldRegistry.get(entityName) ?? DEFAULT_USER_ID_FIELD;\n}\n\nexport function getAllUserIdFields(): ReadonlyMap<string, string> {\n return userIdFieldRegistry;\n}\n\nexport function registerEntityRetention(\n entityName: string,\n policy: RetentionPolicy\n): void {\n retentionRegistry.set(entityName, policy);\n}\n\nexport function getEntityRetention(\n entityName: string\n): RetentionPolicy | undefined {\n return retentionRegistry.get(entityName);\n}\n\nexport function getAllRetentionPolicies(): ReadonlyMap<\n string,\n RetentionPolicy\n> {\n return retentionRegistry;\n}\n\n// ---------------------------------------------------------------------------\n// ClassifiedProperty — tagged wrapper: inner builder + compliance level\n// ---------------------------------------------------------------------------\n\n/**\n * Tagged wrapper returned by `.compliance()`.\n * `__inner` is the EXACT MikroORM builder type.\n * `defineComplianceEntity` checks for this wrapper and extracts `__inner`.\n */\nexport interface ClassifiedProperty<\n Builder = unknown,\n Value = unknown,\n Options = unknown\n> {\n readonly __inner: Builder;\n readonly __compliance: ComplianceLevel;\n readonly '~type'?: { value: Value };\n readonly '~options': Options;\n}\n\n// ---------------------------------------------------------------------------\n// ExtractInner — unwraps ClassifiedProperty to get the raw builder\n// ---------------------------------------------------------------------------\n\n/**\n * For each property, if it's a ClassifiedProperty, extract __inner.\n * Functions (lazy relations) and raw PropertyChain (relations) pass through.\n */\nexport type ExtractInner<T> = {\n [K in keyof T]: T[K] extends ClassifiedProperty<infer B> ? B : T[K];\n};\n\n// ---------------------------------------------------------------------------\n// WithCompliance — adds .compliance() to any MikroORM builder\n// ---------------------------------------------------------------------------\n\n/**\n * Adds `.compliance()` to a builder AND remaps chain methods so\n * `.compliance()` persists. Each chain method returns WithCompliance\n * wrapping the chain result. `.compliance()` captures the current\n * builder as-is into ClassifiedProperty.\n */\nexport type WithCompliance<T> = {\n [K in keyof T]: T[K] extends (...args: infer A) => infer R\n ? R extends { '~type'?: unknown; '~options': unknown }\n ? (...args: A) => WithCompliance<R>\n : T[K]\n : T[K];\n} & {\n compliance(\n level: ComplianceLevel\n ): T extends { '~type'?: { value: infer V }; '~options': infer O }\n ? ClassifiedProperty<T, V, O>\n : ClassifiedProperty<T>;\n};\n\n// ---------------------------------------------------------------------------\n// ForklaunchPropertyBuilders — the type of `fp`\n// ---------------------------------------------------------------------------\n\ntype RelationBuilderKeys =\n | 'manyToOne'\n | 'oneToMany'\n | 'manyToMany'\n | 'oneToOne'\n | 'embedded';\n\n// Generic methods whose type params get collapsed by mapped types\ntype GenericBuilderKeys =\n | 'json'\n | 'formula'\n | 'type'\n | 'enum'\n | 'bigint'\n | 'array'\n | 'decimal';\n\n/**\n * Each scalar method wraps the EXACT MikroORM return type with\n * WithCompliance. Relations pass through unchanged.\n * Generic methods have explicit signatures to preserve type params.\n */\nexport type ForklaunchPropertyBuilders = {\n [K in Exclude<\n keyof PropertyBuilders,\n RelationBuilderKeys | GenericBuilderKeys\n >]: PropertyBuilders[K] extends (...args: infer A) => infer R\n ? (...args: A) => WithCompliance<R>\n : PropertyBuilders[K];\n} & {\n [K in RelationBuilderKeys]: PropertyBuilders[K];\n} & {\n json: <T>() => WithCompliance<PropertyChain<T, EmptyOptions>>;\n formula: <T>(\n formula: string | ((...args: never[]) => string)\n ) => WithCompliance<PropertyChain<T, EmptyOptions>>;\n type: <T>(type: T) => WithCompliance<PropertyChain<T, EmptyOptions>>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n enum: <const T extends (number | string)[] | (() => Record<string, any>)>(\n items?: T\n ) => WithCompliance<\n PropertyChain<\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n T extends () => Record<string, any>\n ? T extends () => infer R\n ? R[keyof R]\n : never\n : T extends (infer Value)[]\n ? Value\n : T,\n EmptyOptions & { kind: 'enum' }\n >\n >;\n bigint: <Mode extends 'bigint' | 'number' | 'string' = 'bigint'>(\n mode?: Mode\n ) => WithCompliance<\n PropertyChain<\n Mode extends 'bigint' ? bigint : Mode extends 'number' ? number : string,\n EmptyOptions\n >\n >;\n array: <T = string>(\n toJsValue?: (i: string) => T,\n toDbValue?: (i: T) => string\n ) => WithCompliance<PropertyChain<T[], EmptyOptions>>;\n decimal: <Mode extends 'number' | 'string' = 'string'>(\n mode?: Mode\n ) => WithCompliance<\n PropertyChain<Mode extends 'number' ? number : string, EmptyOptions>\n >;\n};\n","import { p } from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n type ComplianceLevel,\n type ForklaunchPropertyBuilders\n} from './complianceTypes';\n\n// ---------------------------------------------------------------------------\n// Runtime Proxy implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a value is a MikroORM property builder (has ~options).\n */\nfunction isBuilder(value: unknown): value is object {\n return (\n value != null &&\n typeof value === 'object' &&\n '~options' in (value as Record<string, unknown>)\n );\n}\n\n/**\n * Wraps a MikroORM scalar PropertyBuilder in a Proxy that:\n * 1. Adds a `.compliance(level)` method\n * 2. Forwards all other method calls to the underlying builder\n * 3. Re-wraps returned builders so `.compliance()` persists through chains\n */\nfunction wrapUnclassified(builder: unknown): unknown {\n return new Proxy(builder as object, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === 'compliance') {\n return (level: ComplianceLevel) => wrapClassified(target, level);\n }\n if (prop === '~options') return Reflect.get(target, prop, target);\n if (prop === COMPLIANCE_KEY) return undefined;\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a builder that has been classified via `.compliance()`.\n * Stores the compliance level under `~compliance` for `defineComplianceEntity`.\n * Chaining after `.compliance()` propagates the level through subsequent builders.\n */\nfunction wrapClassified(builder: object, level: ComplianceLevel): unknown {\n return new Proxy(builder, {\n get(target: Record<string | symbol, unknown>, prop) {\n if (prop === COMPLIANCE_KEY) return level;\n if (prop === '~options') return Reflect.get(target, prop, target);\n\n const value = Reflect.get(target, prop, target);\n if (typeof value === 'function') {\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapClassified(result, level) : result;\n };\n }\n return value;\n }\n });\n}\n\n/**\n * Wraps a relation PropertyBuilder (manyToOne, oneToMany, etc.).\n * Auto-classified as 'none' — no `.compliance()` call needed.\n * All chained methods preserve the auto-classification.\n */\nfunction wrapRelation(builder: object): unknown {\n return wrapClassified(builder, 'none');\n}\n\n// ---------------------------------------------------------------------------\n// Relation method detection\n// ---------------------------------------------------------------------------\n\nconst RELATION_METHODS = new Set([\n 'manyToOne',\n 'oneToMany',\n 'manyToMany',\n 'oneToOne',\n 'embedded'\n]);\n\nfunction isRelationMethod(prop: string | symbol): boolean {\n return typeof prop === 'string' && RELATION_METHODS.has(prop);\n}\n\n// ---------------------------------------------------------------------------\n// fp — the ForkLaunch property builder\n// ---------------------------------------------------------------------------\n\n/**\n * ForkLaunch property builder. Drop-in replacement for MikroORM's `p`\n * that adds `.compliance(level)` to every scalar property builder\n * and auto-classifies relation builders as 'none'.\n *\n * - Scalar fields: `fp.string().compliance('pii')` — must call `.compliance()`\n * - Relation fields: `fp.manyToOne(Target)` — auto-classified, no `.compliance()` needed\n *\n * @example\n * ```typescript\n * import { defineComplianceEntity, fp } from '@forklaunch/core/persistence';\n *\n * const User = defineComplianceEntity({\n * name: 'User',\n * properties: {\n * id: fp.uuid().primary().compliance('none'),\n * email: fp.string().unique().compliance('pii'),\n * medicalRecord: fp.string().nullable().compliance('phi'),\n * organization: () => fp.manyToOne(Organization).nullable(),\n * }\n * });\n * ```\n */\nexport const fp: ForklaunchPropertyBuilders = new Proxy(p, {\n get(target: Record<string | symbol, unknown>, prop) {\n const value = Reflect.get(target, prop, target);\n if (typeof value !== 'function') return value;\n\n if (isRelationMethod(prop)) {\n // Relation methods: call the original, wrap result as auto-classified 'none'\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapRelation(result) : result;\n };\n }\n\n // Scalar methods: call the original, wrap result with .compliance()\n return (...args: unknown[]) => {\n const result = (value as (...args: unknown[]) => unknown).apply(\n target,\n args\n );\n return isBuilder(result) ? wrapUnclassified(result) : result;\n };\n }\n}) as ForklaunchPropertyBuilders;\n","import type { InferEntity } from '@mikro-orm/core';\nimport {\n defineEntity,\n p,\n type EntityMetadataWithProperties,\n type EntitySchemaWithMeta,\n type InferEntityFromProperties\n} from '@mikro-orm/core';\nimport {\n COMPLIANCE_KEY,\n parseDuration,\n registerEntityCompliance,\n registerEntityRetention,\n registerEntityUserIdField,\n type ClassifiedProperty,\n type ComplianceLevel,\n type RetentionPolicy\n} from './complianceTypes';\n\ntype ValidateProperties<T> = {\n [K in keyof T]: T[K] extends ClassifiedProperty\n ? T[K]\n : T[K] extends (...args: never[]) => unknown\n ? T[K]\n : ClassifiedProperty;\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\nexport function defineComplianceEntity<\n const TName extends string,\n const TTableName extends string,\n const TProperties extends Record<string, unknown>,\n const TPK extends (keyof TProperties)[] | undefined = undefined,\n const TBase = never,\n const TRepository = never,\n const TForceObject extends boolean = false\n>(\n meta: EntityMetadataWithProperties<\n TName,\n TTableName,\n TProperties & ValidateProperties<TProperties>,\n TPK,\n TBase,\n TRepository,\n TForceObject\n > & { retention?: RetentionPolicy; userIdField?: string }\n): EntitySchemaWithMeta<\n TName,\n TTableName,\n InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>,\n TBase,\n TProperties\n> {\n const entityName = 'name' in meta ? (meta.name as string) : 'Unknown';\n const complianceFields = new Map<string, ComplianceLevel>();\n\n const rawProperties = meta.properties;\n const resolvedProperties: Record<string, unknown> =\n typeof rawProperties === 'function' ? rawProperties(p) : rawProperties;\n\n for (const [fieldName, rawProp] of Object.entries(resolvedProperties)) {\n const prop = typeof rawProp === 'function' ? rawProp() : rawProp;\n const level = readComplianceLevel(prop);\n\n if (level == null) {\n throw new Error(\n `Field '${entityName}.${fieldName}' is missing compliance classification. ` +\n `Call .compliance('pii' | 'phi' | 'pci' | 'none') on this property, ` +\n `or use a relation method (fp.manyToOne, etc.) which is auto-classified.`\n );\n }\n complianceFields.set(fieldName, level);\n }\n\n registerEntityCompliance(entityName, complianceFields);\n\n // Handle retention policy\n if (meta.retention) {\n parseDuration(meta.retention.duration); // validates at boot — throws if invalid\n\n if (!resolvedProperties['createdAt']) {\n throw new Error(\n `Entity '${entityName}' has a retention policy but no 'createdAt' property. ` +\n `Retention requires createdAt to compute expiration.`\n );\n }\n\n registerEntityRetention(entityName, meta.retention);\n }\n\n // Register userIdField (defaults to 'userId' if not specified)\n if (meta.userIdField) {\n registerEntityUserIdField(entityName, meta.userIdField);\n }\n\n // Strip custom fields before passing to MikroORM's defineEntity\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { retention: _retention, userIdField: _userIdField, ...mikroMeta } = meta;\n\n return defineEntity(\n mikroMeta as unknown as typeof meta\n ) as EntitySchemaWithMeta<\n TName,\n TTableName,\n InferEntityFromProperties<\n TProperties,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >,\n TBase,\n TProperties\n >;\n}\n\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 object>(em: T): T {\n const BLOCKED_METHODS = [\n 'nativeInsert',\n 'nativeUpdate',\n 'nativeDelete'\n ] as const;\n\n return new Proxy(em, {\n get(target, prop, receiver) {\n if (\n typeof prop === 'string' &&\n BLOCKED_METHODS.includes(prop as (typeof BLOCKED_METHODS)[number])\n ) {\n return (entityNameOrEntity: unknown, ...rest: unknown[]) => {\n const entityName = resolveEntityName(entityNameOrEntity);\n if (entityName) {\n const fields = getEntityComplianceFields(entityName);\n if (fields) {\n for (const [fieldName, level] of fields) {\n if (ENCRYPTED_LEVELS.has(level)) {\n throw new EncryptionRequiredError(\n `${prop}() blocked on entity '${entityName}' because field ` +\n `'${fieldName}' has compliance level '${level}'. ` +\n `Use em.create() + em.flush() instead to ensure encryption.`\n );\n }\n }\n }\n }\n // No compliance fields requiring encryption — allow the native query\n const method = Reflect.get(target, prop, receiver);\n return (method as (...args: unknown[]) => unknown).call(\n target,\n entityNameOrEntity,\n ...rest\n );\n };\n }\n return Reflect.get(target, prop, receiver);\n }\n });\n}\n\n/**\n * Resolve an entity name from the first argument to nativeInsert/Update/Delete.\n * MikroORM accepts entity name strings, entity class references, or entity instances.\n */\nfunction resolveEntityName(entityNameOrEntity: unknown): string | undefined {\n if (typeof entityNameOrEntity === 'string') {\n return entityNameOrEntity;\n }\n if (typeof entityNameOrEntity === 'function') {\n return (entityNameOrEntity as { name?: string }).name;\n }\n if (\n entityNameOrEntity != null &&\n typeof entityNameOrEntity === 'object' &&\n 'constructor' in entityNameOrEntity\n ) {\n return (entityNameOrEntity.constructor as { name?: string }).name;\n }\n return undefined;\n}\n","import type { Dictionary, EntityManager, FilterDef } from '@mikro-orm/core';\n\n/**\n * The name used to register the tenant isolation filter.\n */\nexport const TENANT_FILTER_NAME = 'tenant';\n\n/**\n * Creates the tenant filter definition.\n *\n * The filter adds `WHERE organizationId = :tenantId` to all queries\n * on entities that have an `organizationId` or `organization` property.\n * Entities without either property are unaffected (empty condition).\n */\nexport function createTenantFilterDef(): FilterDef {\n return {\n name: TENANT_FILTER_NAME,\n cond(\n args: Dictionary,\n _type: 'read' | 'update' | 'delete',\n em: EntityManager,\n _options?: unknown,\n entityName?: string\n ) {\n if (!entityName) {\n return {};\n }\n\n try {\n const metadata = em.getMetadata().getByClassName(entityName, false);\n if (!metadata) {\n return {};\n }\n\n const hasOrganizationId = metadata.properties['organizationId'] != null;\n const hasOrganization = metadata.properties['organization'] != null;\n\n if (hasOrganizationId || hasOrganization) {\n return { organizationId: args.tenantId };\n }\n } catch {\n // Entity not found in metadata — skip filtering\n }\n\n return {};\n },\n default: true,\n args: true\n };\n}\n\n/**\n * Registers the global tenant isolation filter on the ORM's entity manager.\n * Call this once at application bootstrap after `MikroORM.init()`.\n *\n * After calling this, every fork of the EM will inherit the filter.\n * Set the tenant ID per-request via:\n *\n * ```ts\n * em.setFilterParams('tenant', { tenantId: 'org-123' });\n * ```\n */\nexport function setupTenantFilter(orm: {\n em: Pick<EntityManager, 'addFilter'>;\n}): void {\n orm.em.addFilter(createTenantFilterDef());\n}\n\n/**\n * Returns a forked EntityManager with the tenant filter disabled.\n *\n * Use this only from code paths that have verified super-admin permissions.\n * Queries executed through the returned EM will return cross-tenant data.\n */\nexport function getSuperAdminContext(\n em: Pick<EntityManager, 'fork'>\n): ReturnType<EntityManager['fork']> {\n const forked = em.fork();\n forked.setFilterParams(TENANT_FILTER_NAME, { tenantId: undefined });\n // Disable the filter by passing false for the filter in each query isn't\n // sufficient globally; instead we add the filter with enabled = false.\n // The cleanest way is to re-add the filter as disabled on this fork.\n forked.addFilter({\n name: TENANT_FILTER_NAME,\n cond: {},\n default: false\n });\n return forked;\n}\n","import type { EntityManager, EventSubscriber, MikroORM } from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n/**\n * Structural subset of {@link TransactionEventArgs} used by\n * `RlsEventSubscriber` so that callers (and tests) only need to provide\n * the properties the subscriber actually reads.\n */\nexport interface RlsTransactionEventArgs {\n em: {\n getFilterParams: EntityManager['getFilterParams'];\n getConnection(): {\n execute(\n query: string,\n params?: unknown[],\n method?: 'all' | 'get' | 'run',\n ctx?: unknown\n ): Promise<unknown>;\n };\n };\n transaction?: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface RlsConfig {\n /**\n * Whether to enable PostgreSQL Row-Level Security.\n * Defaults to `true` when the driver is PostgreSQL, `false` otherwise.\n * Set to `false` to opt out even on PostgreSQL.\n */\n enabled?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// RLS EventSubscriber\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM EventSubscriber that executes `SET LOCAL app.tenant_id = :tenantId`\n * at the start of every transaction when PostgreSQL RLS is enabled.\n *\n * This ensures that even if the MikroORM global filter is somehow bypassed,\n * the database-level RLS policy enforces tenant isolation.\n *\n * The tenant ID is read from the EntityManager's filter parameters\n * (set by the tenant context middleware).\n */\nexport class RlsEventSubscriber implements EventSubscriber {\n async afterTransactionStart(args: RlsTransactionEventArgs): Promise<void> {\n const tenantId = this.getTenantId(args.em);\n if (!tenantId) {\n // No tenant context (e.g., super-admin or public route) — skip SET LOCAL\n return;\n }\n\n const connection = args.em.getConnection();\n // Execute SET LOCAL within the transaction context\n // SET LOCAL only persists for the current transaction — no connection leakage\n await connection.execute(\n `SET LOCAL app.tenant_id = '${escapeSqlString(tenantId)}'`,\n [],\n 'run',\n args.transaction\n );\n }\n\n private getTenantId(\n em: Pick<EntityManager, 'getFilterParams'>\n ): string | undefined {\n const params = em.getFilterParams(TENANT_FILTER_NAME) as\n | { tenantId?: string }\n | undefined;\n return params?.tenantId;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Setup\n// ---------------------------------------------------------------------------\n\n/**\n * Sets up PostgreSQL Row-Level Security integration.\n *\n * 1. Registers the `RlsEventSubscriber` to run `SET LOCAL app.tenant_id`\n * at the start of every transaction.\n * 2. Validates that RLS policies exist on tenant-scoped tables (warns if missing).\n *\n * Call this at application bootstrap after `MikroORM.init()` and `setupTenantFilter()`.\n *\n * @param orm - The initialized MikroORM instance\n * @param config - RLS configuration (enabled defaults to auto-detect PostgreSQL)\n */\nexport function setupRls(orm: MikroORM, config: RlsConfig = {}): void {\n const isPostgres = isPostgresDriver(orm);\n const enabled = config.enabled ?? isPostgres;\n\n if (!enabled) {\n if (!isPostgres) {\n // Non-PostgreSQL — RLS not available, ORM filter is the sole enforcement\n return;\n }\n // PostgreSQL but explicitly disabled\n console.info(\n '[compliance] PostgreSQL RLS disabled by configuration. ORM filter is the sole tenant enforcement layer.'\n );\n return;\n }\n\n if (!isPostgres) {\n console.warn(\n '[compliance] RLS enabled but database driver is not PostgreSQL. ' +\n 'RLS is only supported on PostgreSQL. Falling back to ORM filter only.'\n );\n return;\n }\n\n // Register the RLS transaction subscriber\n orm.em.getEventManager().registerSubscriber(new RlsEventSubscriber());\n\n // Validate RLS policies exist\n validateRlsPolicies(orm).catch((err) => {\n console.warn('[compliance] Failed to validate RLS policies:', err);\n });\n}\n\n// ---------------------------------------------------------------------------\n// RLS policy validation\n// ---------------------------------------------------------------------------\n\n/**\n * Checks that tenant-scoped entities have RLS policies on their tables.\n * Logs warnings with the SQL needed to create missing policies.\n */\nasync function validateRlsPolicies(orm: MikroORM): Promise<void> {\n const metadata = orm.em.getMetadata().getAll();\n\n for (const meta of Object.values(metadata)) {\n const hasOrgId = meta.properties['organizationId'] != null;\n const hasOrg = meta.properties['organization'] != null;\n\n if (!hasOrgId && !hasOrg) continue;\n\n const tableName = meta.tableName;\n try {\n const connection = orm.em.getConnection();\n const result = await connection.execute<{ policyname: string }[]>(\n `SELECT policyname FROM pg_policies WHERE tablename = '${escapeSqlString(tableName)}'`,\n [],\n 'all'\n );\n\n const policies = Array.isArray(result) ? result : [];\n const hasTenantPolicy = policies.some((p: { policyname: string }) =>\n p.policyname.includes('tenant')\n );\n\n if (!hasTenantPolicy) {\n console.warn(\n `[compliance] No tenant RLS policy found on table '${tableName}'. ` +\n `Create one with:\\n` +\n ` ALTER TABLE \"${tableName}\" ENABLE ROW LEVEL SECURITY;\\n` +\n ` CREATE POLICY tenant_isolation ON \"${tableName}\"\\n` +\n ` USING (organization_id = current_setting('app.tenant_id'));`\n );\n }\n } catch {\n // Query failed — likely not connected yet or table doesn't exist\n // Skip validation for this table\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the ORM is using a PostgreSQL driver.\n * Checks the platform constructor name which is 'PostgreSqlPlatform' for PG.\n */\nfunction isPostgresDriver(orm: MikroORM): boolean {\n try {\n const platform = orm.em.getPlatform();\n const name = platform.constructor.name.toLowerCase();\n return name.includes('postgre');\n } catch {\n return false;\n }\n}\n\n/**\n * Escape a string for safe inclusion in SQL. Prevents SQL injection in\n * the SET LOCAL statement.\n */\nfunction escapeSqlString(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n"],"mappings":"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","names":["import_core","p"]}
@@ -34,8 +34,6 @@ var RetentionDuration = {
34
34
  years: (n) => `P${n}Y`
35
35
  };
36
36
  var DURATION_REGEX = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?$/;
37
- var MS_PER_DAY = 864e5;
38
- var MIN_DURATION_MS = MS_PER_DAY;
39
37
  function parseDuration(iso) {
40
38
  const match = DURATION_REGEX.exec(iso);
41
39
  if (!match) {
@@ -46,16 +44,33 @@ function parseDuration(iso) {
46
44
  const years = parseInt(match[1] || "0", 10);
47
45
  const months = parseInt(match[2] || "0", 10);
48
46
  const days = parseInt(match[3] || "0", 10);
49
- const totalDays = years * 365 + months * 30 + days;
50
- const ms = totalDays * MS_PER_DAY;
51
- if (ms < MIN_DURATION_MS) {
47
+ const approxDays = years * 365 + months * 30 + days;
48
+ if (approxDays < 1) {
52
49
  throw new Error(
53
- `Retention duration must be >= 1 day (P1D). Got: '${iso}' (${totalDays} days)`
50
+ `Retention duration must be >= 1 day (P1D). Got: '${iso}' (${approxDays} approx days)`
54
51
  );
55
52
  }
56
- return ms;
53
+ return { years, months, days };
54
+ }
55
+ function subtractDuration(from, duration) {
56
+ const result = new Date(from);
57
+ result.setFullYear(result.getFullYear() - duration.years);
58
+ result.setMonth(result.getMonth() - duration.months);
59
+ result.setDate(result.getDate() - duration.days);
60
+ return result;
57
61
  }
58
62
  var retentionRegistry = /* @__PURE__ */ new Map();
63
+ var DEFAULT_USER_ID_FIELD = "userId";
64
+ var userIdFieldRegistry = /* @__PURE__ */ new Map();
65
+ function registerEntityUserIdField(entityName, field) {
66
+ userIdFieldRegistry.set(entityName, field);
67
+ }
68
+ function getEntityUserIdField(entityName) {
69
+ return userIdFieldRegistry.get(entityName) ?? DEFAULT_USER_ID_FIELD;
70
+ }
71
+ function getAllUserIdFields() {
72
+ return userIdFieldRegistry;
73
+ }
59
74
  function registerEntityRetention(entityName, policy) {
60
75
  retentionRegistry.set(entityName, policy);
61
76
  }
@@ -182,7 +197,10 @@ function defineComplianceEntity(meta) {
182
197
  }
183
198
  registerEntityRetention(entityName, meta.retention);
184
199
  }
185
- const { retention: _retention, ...mikroMeta } = meta;
200
+ if (meta.userIdField) {
201
+ registerEntityUserIdField(entityName, meta.userIdField);
202
+ }
203
+ const { retention: _retention, userIdField: _userIdField, ...mikroMeta } = meta;
186
204
  return defineEntity(
187
205
  mikroMeta
188
206
  );
@@ -474,13 +492,16 @@ export {
474
492
  entityHasEncryptedFields,
475
493
  fp,
476
494
  getAllRetentionPolicies,
495
+ getAllUserIdFields,
477
496
  getComplianceMetadata,
478
497
  getEntityComplianceFields,
479
498
  getEntityRetention,
499
+ getEntityUserIdField,
480
500
  getSuperAdminContext,
481
501
  parseDuration,
482
502
  setupRls,
483
503
  setupTenantFilter,
504
+ subtractDuration,
484
505
  wrapEmWithNativeQueryBlocking
485
506
  };
486
507
  //# sourceMappingURL=index.mjs.map