@forklaunch/core 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/persistence/index.d.mts +12 -32
- package/lib/persistence/index.d.ts +12 -32
- package/lib/persistence/index.js +1 -6
- package/lib/persistence/index.js.map +1 -1
- package/lib/persistence/index.mjs +1 -6
- package/lib/persistence/index.mjs.map +1 -1
- package/lib/services/index.js.map +1 -1
- package/lib/services/index.mjs.map +1 -1
- package/package.json +17 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/persistence/complianceTypes.ts","../../src/persistence/compliancePropertyBuilder.ts","../../src/persistence/defineComplianceEntity.ts","../../src/persistence/fieldEncryptor.ts","../../src/persistence/complianceEventSubscriber.ts","../../src/persistence/tenantFilter.ts","../../src/persistence/rls.ts"],"sourcesContent":["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 {\n retention: _retention,\n userIdField: _userIdField,\n ...mikroMeta\n } = 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 './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":["p","p","p"]}
|
|
1
|
+
{"version":3,"sources":["../../src/persistence/complianceTypes.ts","../../src/persistence/compliancePropertyBuilder.ts","../../src/persistence/defineComplianceEntity.ts","../../src/persistence/fieldEncryptor.ts","../../src/persistence/complianceEventSubscriber.ts","../../src/persistence/tenantFilter.ts","../../src/persistence/rls.ts"],"sourcesContent":["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// WithCompliance — adds .compliance() to any MikroORM builder\n// ---------------------------------------------------------------------------\n\n/**\n * Adds `.compliance()` to a builder AND remaps chain methods so\n * `.compliance()` persists through chains. `.compliance()` returns\n * the builder intersected with a `__classified` brand — structurally\n * identical to the plain PropertyChain, avoiding TS7056 on large entities.\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(level: ComplianceLevel): T & { readonly __classified: true };\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 {\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 ComplianceLevel,\n type RetentionPolicy\n} from './complianceTypes';\n\ntype ValidateProperties<T> = {\n [K in keyof T]: T[K] extends { readonly __classified: true }\n ? T[K]\n : T[K] extends (...args: never[]) => unknown\n ? T[K]\n : { readonly __classified: true };\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 return defineEntity(\n meta as EntityMetadataWithProperties<\n TName,\n TTableName,\n TProperties & ValidateProperties<TProperties>,\n TPK,\n TBase,\n TRepository,\n TForceObject\n >\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\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 './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":["p","p","p"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/index.ts","../../../common/src/getEnvVar.ts","../../src/environment/loadCascadingEnv.ts","../../src/services/configInjector.ts","../../src/services/guards/isConstructed.ts","../../src/services/types/configInjector.types.ts","../../src/services/guards/isConstructedSingleton.ts","../../src/services/guards/isConstructor.ts","../../src/persistence/complianceTypes.ts","../../src/services/retentionService.ts","../../src/services/complianceDataService.ts"],"sourcesContent":["export * from '../../../common/src/getEnvVar';\nexport * from '../environment';\nexport { createConfigInjector } from './configInjector';\nexport type { ConfigInjector, ValidConfigInjector } from './configInjector';\nexport * from './types/configInjector.types';\nexport * from './types/entityManager.types';\nexport * from './types/service.types';\nexport {\n RetentionService,\n type EnforcementOptions,\n type EnforcementResult\n} from './retentionService';\nexport {\n ComplianceDataService,\n type EraseResult,\n type ExportResult,\n type UserIdFieldOverrides\n} from './complianceDataService';\n","/**\n * Gets an environment variable and casts it to a string.\n *\n * It casts a potentially undefined value to a string, since it will be validated\n * in order to be bootstrapped.\n *\n * @param name - The name of the environment variable to retrieve\n * @returns The environment variable value as a string\n */\nexport function getEnvVar(name: string): string {\n const value = process.env[name];\n return value as string;\n}\n","import dotenv from 'dotenv';\nimport { existsSync } from 'fs';\nimport { dirname, resolve } from 'path';\n\n/**\n * Gets cascading environment file paths: collects all .env.local files from project directory up to root\n * Root detection uses: .forklaunch/manifest.toml\n */\nexport function getCascadingEnvPaths(\n projectEnvPath: string | undefined,\n projectRoot: string = process.cwd()\n): {\n rootEnvExists: boolean;\n projectEnvExists: boolean;\n rootEnvPath?: string;\n projectEnvFilePath?: string;\n loadOrder: string[];\n} {\n const result = {\n rootEnvExists: false,\n projectEnvExists: false,\n rootEnvPath: undefined as string | undefined,\n projectEnvFilePath: undefined as string | undefined,\n loadOrder: [] as string[]\n };\n\n const applicationRoot = findApplicationRoot(projectRoot);\n\n const envLocalFiles = collectEnvLocalFiles(projectRoot, applicationRoot);\n result.loadOrder.push(...envLocalFiles);\n\n const rootEnvPath = resolve(applicationRoot, '.env.local');\n if (envLocalFiles.includes(rootEnvPath)) {\n result.rootEnvExists = true;\n result.rootEnvPath = rootEnvPath;\n }\n\n if (projectEnvPath) {\n const fullProjectEnvPath = resolve(projectRoot, projectEnvPath);\n if (existsSync(fullProjectEnvPath)) {\n result.projectEnvExists = true;\n result.projectEnvFilePath = fullProjectEnvPath;\n if (!result.loadOrder.includes(fullProjectEnvPath)) {\n result.loadOrder.push(fullProjectEnvPath);\n }\n }\n }\n\n return result;\n}\n\n/**\n * Loads environment variables with cascading precedence: all .env.local files from root to project, then project env file\n */\nexport function loadCascadingEnv(\n projectEnvPath: string | undefined,\n projectRoot: string = process.cwd()\n): {\n rootEnvLoaded: boolean;\n projectEnvLoaded: boolean;\n rootEnvPath?: string;\n projectEnvFilePath?: string;\n envFilesLoaded: string[];\n totalEnvFilesLoaded: number;\n} {\n const paths = getCascadingEnvPaths(projectEnvPath, projectRoot);\n const result = {\n rootEnvLoaded: false,\n projectEnvLoaded: false,\n rootEnvPath: paths.rootEnvPath,\n projectEnvFilePath: paths.projectEnvFilePath,\n envFilesLoaded: [] as string[],\n totalEnvFilesLoaded: 0\n };\n\n const seen = new Set<string>();\n for (const envPath of paths.loadOrder) {\n if (seen.has(envPath)) {\n continue;\n }\n seen.add(envPath);\n\n const envResult = dotenv.config({\n path: envPath,\n override: true\n });\n\n if (!envResult?.error) {\n result.envFilesLoaded.push(envPath);\n result.totalEnvFilesLoaded++;\n\n if (envPath === paths.rootEnvPath) {\n result.rootEnvLoaded = true;\n }\n if (envPath === paths.projectEnvFilePath) {\n result.projectEnvLoaded = true;\n }\n }\n }\n\n return result;\n}\n\n/**\n * Collects all .env.local files from project directory up to application root\n * Returns paths in order from root to project (for proper precedence)\n */\nfunction collectEnvLocalFiles(\n projectRoot: string,\n applicationRoot: string\n): string[] {\n const envLocalPaths: string[] = [];\n\n let currentPath = resolve(projectRoot);\n const normalizedAppRoot = resolve(applicationRoot);\n\n while (currentPath.length >= normalizedAppRoot.length) {\n const envLocalPath = resolve(currentPath, '.env.local');\n if (existsSync(envLocalPath)) {\n envLocalPaths.push(envLocalPath);\n }\n\n if (currentPath === normalizedAppRoot) {\n break;\n }\n\n const parentPath = dirname(currentPath);\n if (parentPath === currentPath) {\n break;\n }\n\n currentPath = parentPath;\n }\n\n return envLocalPaths.reverse();\n}\n\n/**\n * Finds application root by looking for .forklaunch/manifest.toml\n */\nexport function findApplicationRoot(startPath: string): string {\n let currentPath = resolve(startPath);\n const originalStart = currentPath;\n const maxDepth = 10;\n let depth = 0;\n\n while (depth < maxDepth) {\n const forklaunchManifest = resolve(\n currentPath,\n '.forklaunch',\n 'manifest.toml'\n );\n if (existsSync(forklaunchManifest)) {\n return currentPath;\n }\n\n const parentPath = dirname(currentPath);\n if (parentPath === currentPath) {\n break;\n }\n\n currentPath = parentPath;\n depth++;\n }\n\n return originalStart;\n}\n","import { extractArgumentNames, isNever } from '@forklaunch/common';\nimport {\n AnySchemaValidator,\n IdiomaticSchema,\n ParseResult,\n prettyPrintParseErrors,\n SchemaValidator\n} from '@forklaunch/validator';\nimport { isConstructed } from './guards/isConstructed';\nimport { isConstructedSingleton } from './guards/isConstructedSingleton';\nimport { isConstructor } from './guards/isConstructor';\nimport {\n ConfigValidator,\n Constructed,\n ConstructedSingleton,\n Lifetime,\n ResolvedConfigValidator,\n SchemaConstructor,\n SchemaFunction,\n Singleton\n} from './types/configInjector.types';\n\nexport function createConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n>(\n schemaValidator: SV,\n dependenciesDefinition: {\n [K in keyof CV]:\n | Singleton<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n | Constructed<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >;\n }\n) {\n return new ConfigInjector<SV, CV>(\n schemaValidator,\n dependenciesDefinition\n ).load();\n}\n\nexport class ConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n> {\n instances: {\n [K in keyof CV]?: ResolvedConfigValidator<SV, CV>[K];\n } = {};\n\n readonly configShapes: CV;\n\n load(inheritedScopeInstances?: {\n [K in keyof CV]?: ResolvedConfigValidator<SV, CV>[K];\n }): this {\n for (const token in inheritedScopeInstances) {\n this.instances[token] = inheritedScopeInstances[token];\n }\n\n for (const token in this.dependenciesDefinition) {\n const definition = this.dependenciesDefinition[token];\n if (\n definition.lifetime === Lifetime.Singleton &&\n !this.instances[token]\n ) {\n if (\n isConstructedSingleton<\n CV[typeof token],\n Omit<ResolvedConfigValidator<SV, CV>, typeof token>,\n ResolvedConfigValidator<SV, CV>[typeof token]\n >(definition)\n ) {\n this.instances[token] = this.resolveInstance<typeof token>(\n token,\n definition\n );\n } else {\n this.instances[token] = definition.value;\n }\n }\n }\n return this;\n }\n\n private resolveInstance<T extends keyof CV>(\n token: T,\n definition:\n | ConstructedSingleton<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >\n | Constructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): ResolvedConfigValidator<SV, CV>[T] {\n if (process.env.FORKLAUNCH_MODE === 'openapi') {\n const noopProxy: Record<string, unknown> = new Proxy(\n function () {} as unknown as Record<string, unknown>,\n {\n get(_target, prop) {\n if (prop === Symbol.toPrimitive) return () => '';\n if (prop === 'toString' || prop === 'valueOf') return () => '';\n if (prop === 'then') return undefined;\n if (prop === Symbol.iterator) return function* () {};\n if (prop === Symbol.asyncIterator) return async function* () {};\n return noopProxy;\n },\n set() {\n return true;\n },\n has() {\n return true;\n },\n deleteProperty() {\n return true;\n },\n ownKeys() {\n return ['prototype'];\n },\n getOwnPropertyDescriptor(_target, prop) {\n if (prop === 'prototype') {\n return { configurable: false, enumerable: false, writable: true };\n }\n return { configurable: true, enumerable: true };\n },\n apply() {\n return noopProxy;\n },\n construct() {\n return noopProxy as object;\n }\n }\n );\n return noopProxy as ResolvedConfigValidator<SV, CV>[T];\n }\n\n const rawInjectorArgument = extractArgumentNames(definition.factory)[0];\n // short circuit as no args\n if (!rawInjectorArgument || rawInjectorArgument === '_args') {\n return definition.factory(\n {} as Omit<ResolvedConfigValidator<SV, CV>, T>,\n this.resolve.bind(this),\n context ?? ({} as Record<string, unknown>)\n );\n }\n\n // Normalize whitespace to handle both single-line and multiline formatting\n const injectorArgument = rawInjectorArgument.replace(/\\s+/g, '');\n\n if (!injectorArgument.startsWith('{') || !injectorArgument.endsWith('}')) {\n throw new Error(\n `Invalid injector argument for ${String(\n token\n )}: ${injectorArgument}. Please use object destructuring syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment.`\n );\n }\n const resolvedArguments = Object.fromEntries(\n injectorArgument\n .slice(1, -1)\n .split(',')\n .filter((arg) => arg.length > 0)\n .map((arg) => arg.split(':')[0].trim())\n .map((arg) => {\n const newResolutionPath = [...resolutionPath, token];\n if (resolutionPath.includes(arg)) {\n throw new Error(\n `Circular dependency detected: ${newResolutionPath.join(\n ' -> '\n )} -> ${arg}`\n );\n }\n const resolvedArg = this.resolve(arg, context, newResolutionPath);\n return [arg, resolvedArg];\n })\n ) as unknown as Omit<ResolvedConfigValidator<SV, CV>, T>;\n return definition.factory(\n resolvedArguments,\n this.resolve.bind(this),\n context ?? ({} as Record<string, unknown>)\n );\n }\n\n constructor(\n private schemaValidator: SV,\n private dependenciesDefinition: {\n [K in keyof CV]: (\n | Singleton<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n | Constructed<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n ) & {\n type: CV[K];\n };\n }\n ) {\n this.configShapes = Object.entries(this.dependenciesDefinition).reduce(\n (acc, [key, { type }]) => ({\n ...acc,\n [key]: type\n }),\n {} as Record<keyof CV, CV[keyof CV]>\n ) as CV;\n }\n\n safeValidateConfigSingletons(): ParseResult<ValidConfigInjector<SV, CV>> {\n const validNonSchemaSingletons = Object.entries(this.configShapes).reduce<\n ParseResult<ResolvedConfigValidator<SV, CV>>\n >(\n (acc, [key, value]) => {\n if (\n this.dependenciesDefinition[key].lifetime === Lifetime.Singleton &&\n !(this.schemaValidator as SchemaValidator).isSchema<\n SchemaFunction<SV> | SchemaConstructor<SV> | IdiomaticSchema<SV>\n >(value) &&\n isConstructor(value)\n ) {\n if (!(this.instances[key] instanceof value)) {\n const expected = value.name;\n const receivedValue: unknown = this.instances[key];\n const received = isConstructed(receivedValue)\n ? receivedValue.constructor.name\n : typeof receivedValue;\n\n if (acc.ok) {\n acc = {\n ok: false,\n errors: []\n };\n }\n acc.errors?.push({\n message: `Expected ${expected}, received ${received}`,\n path: [key]\n });\n } else {\n if (acc.ok) {\n acc = {\n ok: true,\n value: {\n ...acc.value,\n [key]: this.instances[key]\n }\n };\n }\n }\n return acc;\n }\n return acc;\n },\n {\n ok: true,\n value: {} as ResolvedConfigValidator<SV, CV>\n }\n );\n\n const singletons = Object.fromEntries(\n Object.entries(this.configShapes).filter(\n ([key, value]) =>\n this.dependenciesDefinition[key].lifetime === Lifetime.Singleton &&\n (this.schemaValidator as SchemaValidator).isSchema(value)\n )\n );\n const schemaSingletonParseResult = (\n this.schemaValidator as SchemaValidator\n ).parse(\n (this.schemaValidator as SchemaValidator).schemify(singletons),\n Object.fromEntries(\n Object.keys(singletons).map((key) => {\n const dependency = this.dependenciesDefinition[key];\n return [\n key,\n dependency.lifetime === Lifetime.Singleton\n ? this.instances[key]\n : undefined\n ];\n })\n )\n );\n\n const configKeys = Object.keys(this.configShapes);\n\n return validNonSchemaSingletons.ok && schemaSingletonParseResult.ok\n ? {\n ok: true as const,\n value: new ValidConfigInjector<SV, CV>(\n this.schemaValidator,\n this.dependenciesDefinition\n ).load({ ...this.instances })\n }\n : {\n ok: false as const,\n errors: [\n ...(!validNonSchemaSingletons.ok && validNonSchemaSingletons.errors\n ? validNonSchemaSingletons.errors\n : []),\n ...(!schemaSingletonParseResult.ok &&\n schemaSingletonParseResult.errors\n ? schemaSingletonParseResult.errors\n : [])\n ].sort(\n (a, b) =>\n configKeys.indexOf(a.path[0]) - configKeys.indexOf(b.path[0])\n )\n };\n }\n\n validateConfigSingletons(configName: string): ValidConfigInjector<SV, CV> {\n if (process.env.FORKLAUNCH_MODE === 'openapi') {\n return this.createScope() as ValidConfigInjector<SV, CV>;\n }\n\n const safeValidateResult = this.safeValidateConfigSingletons();\n\n if (safeValidateResult.ok) {\n return safeValidateResult.value;\n }\n\n throw new Error(\n prettyPrintParseErrors(safeValidateResult.errors, configName)\n );\n }\n\n resolve<T extends keyof CV>(\n token: T,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): ResolvedConfigValidator<SV, CV>[T] {\n const instance = this.instances[token];\n if (!instance) {\n const definition = this.dependenciesDefinition[token];\n\n if (!definition) {\n throw new Error(`Unable to resolve dependency ${String(token)}`);\n }\n\n switch (definition.lifetime) {\n case Lifetime.Singleton: {\n if (\n isConstructedSingleton<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition) &&\n !this.instances[token]\n ) {\n this.instances[token] = this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n }\n return this.instances[token] as ResolvedConfigValidator<SV, CV>[T];\n }\n case Lifetime.Scoped: {\n if (\n !isConstructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition)\n ) {\n throw new Error(\n `Invalid dependency definition for ${String(token)}`\n );\n }\n\n const scopedInstance = this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n this.instances[token] = scopedInstance;\n return scopedInstance;\n }\n case Lifetime.Transient: {\n if (\n !isConstructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition)\n ) {\n throw new Error(\n `Invalid dependency definition for ${String(token)}`\n );\n }\n\n return this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n }\n default: {\n isNever(definition);\n throw new Error(\n `Unable to resolve lifetime for dependency ${String(\n token\n )}, ${resolutionPath}`\n );\n }\n }\n } else {\n return instance;\n }\n }\n\n scopedResolver<T extends keyof CV>(\n token: T,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): (scope?: ConfigInjector<SV, CV>) => ResolvedConfigValidator<SV, CV>[T] {\n return (scope) =>\n (scope ?? this.createScope()).resolve<T>(token, context, resolutionPath);\n }\n\n createScope(): ConfigInjector<SV, CV> {\n const singletons: Record<string, unknown> = {};\n for (const dependency in this.dependenciesDefinition) {\n if (\n this.dependenciesDefinition[dependency].lifetime === Lifetime.Singleton\n ) {\n singletons[dependency] = this.instances[dependency];\n }\n }\n return new ConfigInjector<SV, CV>(\n this.schemaValidator,\n this.dependenciesDefinition\n ).load(singletons as ResolvedConfigValidator<SV, CV>);\n }\n\n dispose(): void {\n this.instances = {};\n this.load();\n }\n\n chain<ChainedCV extends ConfigValidator<SV>>(dependenciesDefinition: {\n [K in keyof ChainedCV]: {\n type: ChainedCV[K];\n } & (\n | Singleton<\n ChainedCV[K],\n Omit<ResolvedConfigValidator<SV, CV & ChainedCV>, K>,\n ResolvedConfigValidator<SV, ChainedCV>[K]\n >\n | Constructed<\n ChainedCV[K],\n Omit<ResolvedConfigValidator<SV, CV & ChainedCV>, K>,\n ResolvedConfigValidator<SV, ChainedCV>[K]\n >\n );\n }): ConfigInjector<SV, CV & ChainedCV> {\n return new ConfigInjector<SV, CV>(this.schemaValidator, {\n ...this.dependenciesDefinition,\n ...dependenciesDefinition\n }).load({ ...this.instances }) as unknown as ConfigInjector<\n SV,\n CV & ChainedCV\n >;\n }\n\n tokens(): {\n [K in keyof CV]: K;\n } {\n return Object.fromEntries(\n Object.keys(this.dependenciesDefinition).map((key) => [key, key])\n ) as {\n [K in keyof CV]: K;\n };\n }\n}\n\nexport class ValidConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n> extends ConfigInjector<SV, CV> {\n validConfigInjector!: void;\n}\n","import { Constructed } from '../types/configInjector.types';\n\nexport function isConstructed<Type, Args, Return>(\n value: unknown\n): value is Constructed<Type, Args, Return> {\n return (\n typeof value === 'object' &&\n value != null &&\n 'constructor' in value &&\n value.constructor != null\n );\n}\n","import {\n AnySchemaValidator,\n IdiomaticSchema,\n Schema\n} from '@forklaunch/validator';\nimport { ConfigInjector } from '../configInjector';\n\nexport const Lifetime = {\n Singleton: 0,\n Transient: 1,\n Scoped: 2\n} as const;\nexport type Lifetime = (typeof Lifetime)[keyof typeof Lifetime];\n\nexport type Singleton<Type, Args, Value> =\n | {\n lifetime: typeof Lifetime.Singleton;\n type: Type;\n value: Value;\n }\n | ConstructedSingleton<Type, Args, Value>;\n\nexport type ConstructedSingleton<Type, Args, Return> = {\n lifetime: typeof Lifetime.Singleton;\n type: Type;\n factory: (\n args: Args,\n resolve: <T extends keyof Args>(\n token: T,\n context?: Record<string, unknown>\n ) => Args[T],\n context: Record<string, unknown>\n ) => Return;\n};\n\nexport type Constructed<Type, Args, Return> = {\n lifetime: typeof Lifetime.Transient | typeof Lifetime.Scoped;\n type: Type;\n factory: (\n args: Args,\n resolve: <T extends keyof Args>(\n token: T,\n context?: Record<string, unknown>\n ) => Args[T],\n context: Record<string, unknown>\n ) => Return;\n};\n\nexport type Constructor = new (...args: never[]) => unknown;\nexport type SchemaConstructor<SV extends AnySchemaValidator> = new (\n ...args: unknown[]\n) => IdiomaticSchema<SV>;\nexport type Function = (...args: never[]) => unknown;\nexport type FunctionToConstructor = (\n ...args: never[]\n) => new (...args: never[]) => unknown;\nexport type SchemaFunction<SV extends AnySchemaValidator> = (\n args: unknown\n) => IdiomaticSchema<SV>;\n\nexport type ConfigTypes<SV extends AnySchemaValidator> =\n | Function\n | SchemaFunction<SV>\n | Constructor\n | SchemaConstructor<SV>\n | IdiomaticSchema<SV>;\n\nexport type ConfigValidator<SV extends AnySchemaValidator> = Record<\n string,\n ConfigTypes<SV> | Record<string, ConfigTypes<SV>>\n>;\n\ntype ResolveConfigValue<SV extends AnySchemaValidator, T> =\n T extends SchemaConstructor<SV>\n ? Schema<InstanceType<T>, SV>\n : T extends SchemaFunction<SV>\n ? (...args: Parameters<T>) => Schema<ReturnType<T>, SV>\n : T extends FunctionToConstructor\n ? (...args: Parameters<T>) => InstanceType<ReturnType<T>>\n : T extends Function\n ? (...args: Parameters<T>) => ReturnType<T>\n : T extends Constructor\n ? InstanceType<T>\n : T extends IdiomaticSchema<SV>\n ? Schema<T, SV>\n : T extends Record<string, ConfigTypes<SV>>\n ? {\n [K in keyof T]: ResolveConfigValue<SV, T[K]>;\n }\n : Schema<T, SV>;\n\nexport type ResolvedConfigValidator<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n> = {\n [M in keyof CV]: ResolveConfigValue<SV, CV[M]>;\n};\n\nexport type ScopedDependencyFactory<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>,\n M extends keyof CV\n> = (scope?: ConfigInjector<SV, CV>) => ResolvedConfigValidator<SV, CV>[M];\n","import { ConstructedSingleton, Lifetime } from '../types/configInjector.types';\n\nexport function isConstructedSingleton<Type, Args, Return>(\n value: unknown\n): value is ConstructedSingleton<Type, Args, Return> {\n return (\n typeof value === 'object' &&\n value != null &&\n 'lifetime' in value &&\n value.lifetime === Lifetime.Singleton &&\n 'factory' in value\n );\n}\n","import { Constructor } from '../types/configInjector.types';\n\nexport function isConstructor(value: unknown): value is Constructor {\n return (\n typeof value === 'function' &&\n value.constructor != null &&\n value.prototype != null\n );\n}\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 type { MikroORM } from '@mikro-orm/core';\nimport type { OpenTelemetryCollector } from '../http/telemetry/openTelemetryCollector';\nimport {\n getAllRetentionPolicies,\n getEntityComplianceFields,\n parseDuration,\n subtractDuration,\n type RetentionPolicy\n} from '../persistence/complianceTypes';\n\nexport interface EnforcementOptions {\n /** Filter to specific entity names (default: all with retention policies) */\n entities?: string[];\n /** Records per batch (default: 1000) */\n batchSize?: number;\n /** Log what would happen without mutating */\n dryRun?: boolean;\n}\n\nexport interface EnforcementResult {\n processed: number;\n deleted: number;\n anonymized: number;\n errors: number;\n byEntity: Record<\n string,\n { deleted: number; anonymized: number; errors: number }\n >;\n durationMs: number;\n}\n\nexport class RetentionService {\n private readonly DEFAULT_BATCH_SIZE = 1000;\n\n constructor(\n private readonly orm: MikroORM,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private readonly otel: OpenTelemetryCollector<any>\n ) {}\n\n async enforce(options?: EnforcementOptions): Promise<EnforcementResult> {\n const start = Date.now();\n const batchSize = options?.batchSize ?? this.DEFAULT_BATCH_SIZE;\n const dryRun = options?.dryRun ?? false;\n const policies = getAllRetentionPolicies();\n const result: EnforcementResult = {\n processed: 0,\n deleted: 0,\n anonymized: 0,\n errors: 0,\n byEntity: {},\n durationMs: 0\n };\n\n if (policies.size === 0) {\n this.otel.info('[RetentionService] No retention policies registered');\n result.durationMs = Date.now() - start;\n return result;\n }\n\n this.otel.info('[RetentionService] Starting enforcement', {\n entityCount: policies.size,\n batchSize,\n dryRun\n });\n\n for (const [entityName, policy] of policies) {\n if (options?.entities && !options.entities.includes(entityName)) continue;\n\n const entityResult = { deleted: 0, anonymized: 0, errors: 0 };\n\n try {\n await this.enforceEntity(\n entityName,\n policy,\n batchSize,\n entityResult,\n dryRun\n );\n } catch (err) {\n entityResult.errors++;\n this.otel.error('[RetentionService] Entity-level failure', {\n entityName,\n error: String(err)\n });\n }\n\n result.byEntity[entityName] = entityResult;\n result.deleted += entityResult.deleted;\n result.anonymized += entityResult.anonymized;\n result.errors += entityResult.errors;\n result.processed += entityResult.deleted + entityResult.anonymized;\n }\n\n result.durationMs = Date.now() - start;\n this.otel.info('[RetentionService] Enforcement complete', {\n processed: result.processed,\n deleted: result.deleted,\n anonymized: result.anonymized,\n errors: result.errors,\n durationMs: result.durationMs\n });\n\n return result;\n }\n\n private async enforceEntity(\n entityName: string,\n policy: RetentionPolicy,\n batchSize: number,\n stats: { deleted: number; anonymized: number; errors: number },\n dryRun: boolean\n ): Promise<void> {\n const metadata = [...this.orm.getMetadata().getAll().values()].find(\n (m) => m.className === entityName\n );\n\n if (!metadata) {\n this.otel.warn(\n '[RetentionService] Entity not in MikroORM metadata — skipping',\n { entityName }\n );\n return;\n }\n\n const duration = parseDuration(policy.duration);\n const cutoff = subtractDuration(new Date(), duration);\n const entityClass = metadata.class ?? metadata.className;\n\n // For anonymize: only null out PII/PHI/PCI fields that are nullable.\n // Non-nullable fields are skipped to avoid flush errors.\n const piiFieldNames: string[] = [];\n if (policy.action === 'anonymize') {\n const complianceFields = getEntityComplianceFields(entityName);\n if (complianceFields) {\n for (const [fieldName, level] of complianceFields) {\n if (level === 'none') continue;\n const prop = metadata.properties[fieldName];\n if (prop && prop.nullable) {\n piiFieldNames.push(fieldName);\n } else {\n this.otel.warn(\n '[RetentionService] Skipping non-nullable PII field for anonymize',\n { entityName, fieldName, level }\n );\n }\n }\n }\n }\n\n const filter: Record<string, unknown> = {\n createdAt: { $lt: cutoff }\n };\n if (policy.action === 'anonymize') {\n filter['retentionAnonymizedAt'] = null;\n }\n\n // Dry run: use count instead of fetching records\n if (dryRun) {\n const em = this.orm.em.fork();\n try {\n const count = await em.count(entityClass, filter);\n this.otel.info('[RetentionService] Dry run — would process', {\n entityName,\n action: policy.action,\n count\n });\n if (policy.action === 'delete') stats.deleted += count;\n else stats.anonymized += count;\n } catch (err) {\n stats.errors++;\n this.otel.error('[RetentionService] Dry run count failed', {\n entityName,\n error: String(err)\n });\n }\n return;\n }\n\n let batchNum = 0;\n\n while (true) {\n batchNum++;\n const em = this.orm.em.fork();\n\n try {\n const records = await em.find(entityClass, filter, {\n limit: batchSize,\n orderBy: { createdAt: 'ASC' }\n });\n\n if (records.length === 0) break;\n\n if (policy.action === 'delete') {\n records.forEach((r) => em.remove(r));\n await em.flush();\n stats.deleted += records.length;\n } else {\n for (const record of records) {\n const rec = record as Record<string, unknown>;\n for (const field of piiFieldNames) {\n rec[field] = null;\n }\n rec['retentionAnonymizedAt'] = new Date();\n }\n await em.flush();\n stats.anonymized += records.length;\n }\n\n this.otel.info('[RetentionService] Batch processed', {\n entityName,\n action: policy.action,\n batch: batchNum,\n count: records.length\n });\n } catch (err) {\n stats.errors++;\n this.otel.error('[RetentionService] Batch failed', {\n entityName,\n batch: batchNum,\n error: String(err)\n });\n break; // stop batching this entity, continue to next\n }\n }\n }\n}\n","import type { MikroORM } from '@mikro-orm/core';\nimport type { OpenTelemetryCollector } from '../http/telemetry/openTelemetryCollector';\nimport { MetricsDefinition } from '../http/types/openTelemetryCollector.types';\nimport {\n getEntityComplianceFields,\n getEntityUserIdField\n} from '../persistence/complianceTypes';\n\nexport interface EraseResult {\n entitiesAffected: string[];\n recordsDeleted: number;\n}\n\nexport interface ExportResult {\n userId: string;\n entities: Record<string, unknown[]>;\n}\n\n/**\n * Per-entity userIdField overrides.\n * Keys are entity names, values are the field name linking records to a user.\n *\n * @example\n * {\n * User: 'id', // the User entity IS the user record\n * Subscription: 'partyId', // billing links via partyId\n * Account: 'userId', // default, can be omitted\n * }\n */\nexport type UserIdFieldOverrides = Record<string, string>;\n\n/**\n * Common field names that link an entity to a user, tried in order.\n * Used for optimistic search when no explicit userIdField is configured.\n */\nconst CANDIDATE_USER_FIELDS = [\n 'userId',\n 'user',\n 'id',\n 'partyId',\n 'customerId',\n 'ownerId',\n 'createdBy',\n 'email'\n];\n\n/**\n * Generic compliance data service that walks all compliance-registered entities\n * and erases or exports PII/PHI/PCI data for a given user.\n *\n * Resolution order for userIdField per entity:\n * 1. Constructor overrides (highest priority)\n * 2. defineComplianceEntity({ userIdField }) registry\n * 3. Optimistic search: first CANDIDATE_USER_FIELDS match in entity metadata\n * 4. Skip entity (no user link found)\n */\nexport class ComplianceDataService {\n private readonly userIdFieldOverrides: UserIdFieldOverrides;\n\n constructor(\n private readonly orm: MikroORM,\n private readonly otel: OpenTelemetryCollector<MetricsDefinition>,\n userIdFieldOverrides?: UserIdFieldOverrides\n ) {\n this.userIdFieldOverrides = userIdFieldOverrides ?? {};\n }\n\n /**\n * Resolve the field linking an entity to a user.\n * Returns undefined if no link can be determined.\n */\n private resolveUserIdField(\n entityName: string,\n entityProperties: Record<string, unknown>\n ): string | undefined {\n // 1. Constructor override\n const override = this.userIdFieldOverrides[entityName];\n if (override) return override;\n\n // 2. Registry (from defineComplianceEntity({ userIdField }))\n const registered = getEntityUserIdField(entityName);\n if (registered !== 'userId' || entityProperties['userId']) {\n // Registry returned a non-default value, or the default 'userId' exists\n if (entityProperties[registered]) return registered;\n }\n\n // 3. Optimistic search\n for (const candidate of CANDIDATE_USER_FIELDS) {\n if (entityProperties[candidate]) return candidate;\n }\n\n // 4. No link found\n return undefined;\n }\n\n async erase(userId: string): Promise<EraseResult> {\n const em = this.orm.em.fork();\n const entitiesAffected: string[] = [];\n let recordsDeleted = 0;\n\n const allMetadata = [...this.orm.getMetadata().getAll().values()];\n\n for (const metadata of allMetadata) {\n const entityName = metadata.className;\n const fields = getEntityComplianceFields(entityName);\n if (!fields) continue;\n\n const hasPii = [...fields.values()].some(\n (level) => level === 'pii' || level === 'phi' || level === 'pci'\n );\n if (!hasPii) continue;\n\n const userIdField = this.resolveUserIdField(\n entityName,\n metadata.properties\n );\n\n if (!userIdField) {\n this.otel.warn(\n '[ComplianceDataService] No user-linking field found — skipping',\n { entityName, candidates: CANDIDATE_USER_FIELDS }\n );\n continue;\n }\n\n try {\n const entityClass = metadata.class ?? metadata.className;\n const records = await em.find(entityClass, {\n [userIdField]: userId\n });\n\n if (records.length > 0) {\n entitiesAffected.push(entityName);\n recordsDeleted += records.length;\n records.forEach((r) => em.remove(r));\n }\n } catch (err) {\n this.otel.error('[ComplianceDataService] Failed to erase entity', {\n entityName,\n userIdField,\n error: String(err)\n });\n }\n }\n\n if (recordsDeleted > 0) {\n await em.flush();\n }\n\n this.otel.info('[ComplianceDataService] Erase complete', {\n userId,\n entitiesAffected: entitiesAffected.join(','),\n recordsDeleted\n });\n\n return { entitiesAffected, recordsDeleted };\n }\n\n async export(userId: string): Promise<ExportResult> {\n const em = this.orm.em.fork();\n const entities: Record<string, unknown[]> = {};\n\n const allMetadata = [...this.orm.getMetadata().getAll().values()];\n\n for (const metadata of allMetadata) {\n const entityName = metadata.className;\n const fields = getEntityComplianceFields(entityName);\n if (!fields) continue;\n\n const hasPii = [...fields.values()].some(\n (level) => level === 'pii' || level === 'phi' || level === 'pci'\n );\n if (!hasPii) continue;\n\n const userIdField = this.resolveUserIdField(\n entityName,\n metadata.properties\n );\n\n if (!userIdField) {\n continue;\n }\n\n try {\n const entityClass = metadata.class ?? metadata.className;\n const records = await em.find(entityClass, {\n [userIdField]: userId\n });\n\n if (records.length > 0) {\n const piiFieldNames = [...fields.entries()]\n .filter(([, level]) => level !== 'none')\n .map(([name]) => name);\n\n entities[entityName] = records.map((record) => {\n const filtered: Record<string, unknown> = {};\n filtered['id'] = (record as Record<string, unknown>)['id'];\n for (const fieldName of piiFieldNames) {\n filtered[fieldName] = (record as Record<string, unknown>)[\n fieldName\n ];\n }\n return filtered;\n });\n }\n } catch (err) {\n this.otel.error('[ComplianceDataService] Failed to export entity', {\n entityName,\n userIdField,\n error: String(err)\n });\n }\n }\n\n this.otel.info('[ComplianceDataService] Export complete', {\n userId,\n entityCount: Object.keys(entities).length\n });\n\n return { userId, entities };\n }\n}\n"],"mappings":"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","names":["dotenv"]}
|
|
1
|
+
{"version":3,"sources":["../../src/services/index.ts","../../../common/src/getEnvVar.ts","../../src/environment/loadCascadingEnv.ts","../../src/services/configInjector.ts","../../src/services/guards/isConstructed.ts","../../src/services/types/configInjector.types.ts","../../src/services/guards/isConstructedSingleton.ts","../../src/services/guards/isConstructor.ts","../../src/persistence/complianceTypes.ts","../../src/services/retentionService.ts","../../src/services/complianceDataService.ts"],"sourcesContent":["export * from '../../../common/src/getEnvVar';\nexport * from '../environment';\nexport { createConfigInjector } from './configInjector';\nexport type { ConfigInjector, ValidConfigInjector } from './configInjector';\nexport * from './types/configInjector.types';\nexport * from './types/entityManager.types';\nexport * from './types/service.types';\nexport {\n RetentionService,\n type EnforcementOptions,\n type EnforcementResult\n} from './retentionService';\nexport {\n ComplianceDataService,\n type EraseResult,\n type ExportResult,\n type UserIdFieldOverrides\n} from './complianceDataService';\n","/**\n * Gets an environment variable and casts it to a string.\n *\n * It casts a potentially undefined value to a string, since it will be validated\n * in order to be bootstrapped.\n *\n * @param name - The name of the environment variable to retrieve\n * @returns The environment variable value as a string\n */\nexport function getEnvVar(name: string): string {\n const value = process.env[name];\n return value as string;\n}\n","import dotenv from 'dotenv';\nimport { existsSync } from 'fs';\nimport { dirname, resolve } from 'path';\n\n/**\n * Gets cascading environment file paths: collects all .env.local files from project directory up to root\n * Root detection uses: .forklaunch/manifest.toml\n */\nexport function getCascadingEnvPaths(\n projectEnvPath: string | undefined,\n projectRoot: string = process.cwd()\n): {\n rootEnvExists: boolean;\n projectEnvExists: boolean;\n rootEnvPath?: string;\n projectEnvFilePath?: string;\n loadOrder: string[];\n} {\n const result = {\n rootEnvExists: false,\n projectEnvExists: false,\n rootEnvPath: undefined as string | undefined,\n projectEnvFilePath: undefined as string | undefined,\n loadOrder: [] as string[]\n };\n\n const applicationRoot = findApplicationRoot(projectRoot);\n\n const envLocalFiles = collectEnvLocalFiles(projectRoot, applicationRoot);\n result.loadOrder.push(...envLocalFiles);\n\n const rootEnvPath = resolve(applicationRoot, '.env.local');\n if (envLocalFiles.includes(rootEnvPath)) {\n result.rootEnvExists = true;\n result.rootEnvPath = rootEnvPath;\n }\n\n if (projectEnvPath) {\n const fullProjectEnvPath = resolve(projectRoot, projectEnvPath);\n if (existsSync(fullProjectEnvPath)) {\n result.projectEnvExists = true;\n result.projectEnvFilePath = fullProjectEnvPath;\n if (!result.loadOrder.includes(fullProjectEnvPath)) {\n result.loadOrder.push(fullProjectEnvPath);\n }\n }\n }\n\n return result;\n}\n\n/**\n * Loads environment variables with cascading precedence: all .env.local files from root to project, then project env file\n */\nexport function loadCascadingEnv(\n projectEnvPath: string | undefined,\n projectRoot: string = process.cwd()\n): {\n rootEnvLoaded: boolean;\n projectEnvLoaded: boolean;\n rootEnvPath?: string;\n projectEnvFilePath?: string;\n envFilesLoaded: string[];\n totalEnvFilesLoaded: number;\n} {\n const paths = getCascadingEnvPaths(projectEnvPath, projectRoot);\n const result = {\n rootEnvLoaded: false,\n projectEnvLoaded: false,\n rootEnvPath: paths.rootEnvPath,\n projectEnvFilePath: paths.projectEnvFilePath,\n envFilesLoaded: [] as string[],\n totalEnvFilesLoaded: 0\n };\n\n const seen = new Set<string>();\n for (const envPath of paths.loadOrder) {\n if (seen.has(envPath)) {\n continue;\n }\n seen.add(envPath);\n\n const envResult = dotenv.config({\n path: envPath,\n override: true\n });\n\n if (!envResult?.error) {\n result.envFilesLoaded.push(envPath);\n result.totalEnvFilesLoaded++;\n\n if (envPath === paths.rootEnvPath) {\n result.rootEnvLoaded = true;\n }\n if (envPath === paths.projectEnvFilePath) {\n result.projectEnvLoaded = true;\n }\n }\n }\n\n return result;\n}\n\n/**\n * Collects all .env.local files from project directory up to application root\n * Returns paths in order from root to project (for proper precedence)\n */\nfunction collectEnvLocalFiles(\n projectRoot: string,\n applicationRoot: string\n): string[] {\n const envLocalPaths: string[] = [];\n\n let currentPath = resolve(projectRoot);\n const normalizedAppRoot = resolve(applicationRoot);\n\n while (currentPath.length >= normalizedAppRoot.length) {\n const envLocalPath = resolve(currentPath, '.env.local');\n if (existsSync(envLocalPath)) {\n envLocalPaths.push(envLocalPath);\n }\n\n if (currentPath === normalizedAppRoot) {\n break;\n }\n\n const parentPath = dirname(currentPath);\n if (parentPath === currentPath) {\n break;\n }\n\n currentPath = parentPath;\n }\n\n return envLocalPaths.reverse();\n}\n\n/**\n * Finds application root by looking for .forklaunch/manifest.toml\n */\nexport function findApplicationRoot(startPath: string): string {\n let currentPath = resolve(startPath);\n const originalStart = currentPath;\n const maxDepth = 10;\n let depth = 0;\n\n while (depth < maxDepth) {\n const forklaunchManifest = resolve(\n currentPath,\n '.forklaunch',\n 'manifest.toml'\n );\n if (existsSync(forklaunchManifest)) {\n return currentPath;\n }\n\n const parentPath = dirname(currentPath);\n if (parentPath === currentPath) {\n break;\n }\n\n currentPath = parentPath;\n depth++;\n }\n\n return originalStart;\n}\n","import { extractArgumentNames, isNever } from '@forklaunch/common';\nimport {\n AnySchemaValidator,\n IdiomaticSchema,\n ParseResult,\n prettyPrintParseErrors,\n SchemaValidator\n} from '@forklaunch/validator';\nimport { isConstructed } from './guards/isConstructed';\nimport { isConstructedSingleton } from './guards/isConstructedSingleton';\nimport { isConstructor } from './guards/isConstructor';\nimport {\n ConfigValidator,\n Constructed,\n ConstructedSingleton,\n Lifetime,\n ResolvedConfigValidator,\n SchemaConstructor,\n SchemaFunction,\n Singleton\n} from './types/configInjector.types';\n\nexport function createConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n>(\n schemaValidator: SV,\n dependenciesDefinition: {\n [K in keyof CV]:\n | Singleton<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n | Constructed<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >;\n }\n) {\n return new ConfigInjector<SV, CV>(\n schemaValidator,\n dependenciesDefinition\n ).load();\n}\n\nexport class ConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n> {\n instances: {\n [K in keyof CV]?: ResolvedConfigValidator<SV, CV>[K];\n } = {};\n\n readonly configShapes: CV;\n\n load(inheritedScopeInstances?: {\n [K in keyof CV]?: ResolvedConfigValidator<SV, CV>[K];\n }): this {\n for (const token in inheritedScopeInstances) {\n this.instances[token] = inheritedScopeInstances[token];\n }\n\n for (const token in this.dependenciesDefinition) {\n const definition = this.dependenciesDefinition[token];\n if (\n definition.lifetime === Lifetime.Singleton &&\n !this.instances[token]\n ) {\n if (\n isConstructedSingleton<\n CV[typeof token],\n Omit<ResolvedConfigValidator<SV, CV>, typeof token>,\n ResolvedConfigValidator<SV, CV>[typeof token]\n >(definition)\n ) {\n this.instances[token] = this.resolveInstance<typeof token>(\n token,\n definition\n );\n } else {\n this.instances[token] = definition.value;\n }\n }\n }\n return this;\n }\n\n private resolveInstance<T extends keyof CV>(\n token: T,\n definition:\n | ConstructedSingleton<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >\n | Constructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): ResolvedConfigValidator<SV, CV>[T] {\n if (process.env.FORKLAUNCH_MODE === 'openapi') {\n const noopProxy: Record<string, unknown> = new Proxy(\n function () {} as unknown as Record<string, unknown>,\n {\n get(_target, prop) {\n if (prop === Symbol.toPrimitive) return () => '';\n if (prop === 'toString' || prop === 'valueOf') return () => '';\n if (prop === 'then') return undefined;\n if (prop === Symbol.iterator) return function* () {};\n if (prop === Symbol.asyncIterator) return async function* () {};\n return noopProxy;\n },\n set() {\n return true;\n },\n has() {\n return true;\n },\n deleteProperty() {\n return true;\n },\n ownKeys() {\n return ['prototype'];\n },\n getOwnPropertyDescriptor(_target, prop) {\n if (prop === 'prototype') {\n return { configurable: false, enumerable: false, writable: true };\n }\n return { configurable: true, enumerable: true };\n },\n apply() {\n return noopProxy;\n },\n construct() {\n return noopProxy as object;\n }\n }\n );\n return noopProxy as ResolvedConfigValidator<SV, CV>[T];\n }\n\n const rawInjectorArgument = extractArgumentNames(definition.factory)[0];\n // short circuit as no args\n if (!rawInjectorArgument || rawInjectorArgument === '_args') {\n return definition.factory(\n {} as Omit<ResolvedConfigValidator<SV, CV>, T>,\n this.resolve.bind(this),\n context ?? ({} as Record<string, unknown>)\n );\n }\n\n // Normalize whitespace to handle both single-line and multiline formatting\n const injectorArgument = rawInjectorArgument.replace(/\\s+/g, '');\n\n if (!injectorArgument.startsWith('{') || !injectorArgument.endsWith('}')) {\n throw new Error(\n `Invalid injector argument for ${String(\n token\n )}: ${injectorArgument}. Please use object destructuring syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment.`\n );\n }\n const resolvedArguments = Object.fromEntries(\n injectorArgument\n .slice(1, -1)\n .split(',')\n .filter((arg) => arg.length > 0)\n .map((arg) => arg.split(':')[0].trim())\n .map((arg) => {\n const newResolutionPath = [...resolutionPath, token];\n if (resolutionPath.includes(arg)) {\n throw new Error(\n `Circular dependency detected: ${newResolutionPath.join(\n ' -> '\n )} -> ${arg}`\n );\n }\n const resolvedArg = this.resolve(arg, context, newResolutionPath);\n return [arg, resolvedArg];\n })\n ) as unknown as Omit<ResolvedConfigValidator<SV, CV>, T>;\n return definition.factory(\n resolvedArguments,\n this.resolve.bind(this),\n context ?? ({} as Record<string, unknown>)\n );\n }\n\n constructor(\n private schemaValidator: SV,\n private dependenciesDefinition: {\n [K in keyof CV]: (\n | Singleton<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n | Constructed<\n CV[K],\n Omit<ResolvedConfigValidator<SV, CV>, K>,\n ResolvedConfigValidator<SV, CV>[K]\n >\n ) & {\n type: CV[K];\n };\n }\n ) {\n this.configShapes = Object.entries(this.dependenciesDefinition).reduce(\n (acc, [key, { type }]) => ({\n ...acc,\n [key]: type\n }),\n {} as Record<keyof CV, CV[keyof CV]>\n ) as CV;\n }\n\n safeValidateConfigSingletons(): ParseResult<ValidConfigInjector<SV, CV>> {\n const validNonSchemaSingletons = Object.entries(this.configShapes).reduce<\n ParseResult<ResolvedConfigValidator<SV, CV>>\n >(\n (acc, [key, value]) => {\n if (\n this.dependenciesDefinition[key].lifetime === Lifetime.Singleton &&\n !(this.schemaValidator as SchemaValidator).isSchema<\n SchemaFunction<SV> | SchemaConstructor<SV> | IdiomaticSchema<SV>\n >(value) &&\n isConstructor(value)\n ) {\n if (!(this.instances[key] instanceof value)) {\n const expected = value.name;\n const receivedValue: unknown = this.instances[key];\n const received = isConstructed(receivedValue)\n ? receivedValue.constructor.name\n : typeof receivedValue;\n\n if (acc.ok) {\n acc = {\n ok: false,\n errors: []\n };\n }\n acc.errors?.push({\n message: `Expected ${expected}, received ${received}`,\n path: [key]\n });\n } else {\n if (acc.ok) {\n acc = {\n ok: true,\n value: {\n ...acc.value,\n [key]: this.instances[key]\n }\n };\n }\n }\n return acc;\n }\n return acc;\n },\n {\n ok: true,\n value: {} as ResolvedConfigValidator<SV, CV>\n }\n );\n\n const singletons = Object.fromEntries(\n Object.entries(this.configShapes).filter(\n ([key, value]) =>\n this.dependenciesDefinition[key].lifetime === Lifetime.Singleton &&\n (this.schemaValidator as SchemaValidator).isSchema(value)\n )\n );\n const schemaSingletonParseResult = (\n this.schemaValidator as SchemaValidator\n ).parse(\n (this.schemaValidator as SchemaValidator).schemify(singletons),\n Object.fromEntries(\n Object.keys(singletons).map((key) => {\n const dependency = this.dependenciesDefinition[key];\n return [\n key,\n dependency.lifetime === Lifetime.Singleton\n ? this.instances[key]\n : undefined\n ];\n })\n )\n );\n\n const configKeys = Object.keys(this.configShapes);\n\n return validNonSchemaSingletons.ok && schemaSingletonParseResult.ok\n ? {\n ok: true as const,\n value: new ValidConfigInjector<SV, CV>(\n this.schemaValidator,\n this.dependenciesDefinition\n ).load({ ...this.instances })\n }\n : {\n ok: false as const,\n errors: [\n ...(!validNonSchemaSingletons.ok && validNonSchemaSingletons.errors\n ? validNonSchemaSingletons.errors\n : []),\n ...(!schemaSingletonParseResult.ok &&\n schemaSingletonParseResult.errors\n ? schemaSingletonParseResult.errors\n : [])\n ].sort(\n (a, b) =>\n configKeys.indexOf(a.path[0]) - configKeys.indexOf(b.path[0])\n )\n };\n }\n\n validateConfigSingletons(configName: string): ValidConfigInjector<SV, CV> {\n if (process.env.FORKLAUNCH_MODE === 'openapi') {\n return this.createScope() as ValidConfigInjector<SV, CV>;\n }\n\n const safeValidateResult = this.safeValidateConfigSingletons();\n\n if (safeValidateResult.ok) {\n return safeValidateResult.value;\n }\n\n throw new Error(\n prettyPrintParseErrors(safeValidateResult.errors, configName)\n );\n }\n\n resolve<T extends keyof CV>(\n token: T,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): ResolvedConfigValidator<SV, CV>[T] {\n const instance = this.instances[token];\n if (!instance) {\n const definition = this.dependenciesDefinition[token];\n\n if (!definition) {\n throw new Error(`Unable to resolve dependency ${String(token)}`);\n }\n\n switch (definition.lifetime) {\n case Lifetime.Singleton: {\n if (\n isConstructedSingleton<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition) &&\n !this.instances[token]\n ) {\n this.instances[token] = this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n }\n return this.instances[token] as ResolvedConfigValidator<SV, CV>[T];\n }\n case Lifetime.Scoped: {\n if (\n !isConstructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition)\n ) {\n throw new Error(\n `Invalid dependency definition for ${String(token)}`\n );\n }\n\n const scopedInstance = this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n this.instances[token] = scopedInstance;\n return scopedInstance;\n }\n case Lifetime.Transient: {\n if (\n !isConstructed<\n CV[T],\n Omit<ResolvedConfigValidator<SV, CV>, T>,\n ResolvedConfigValidator<SV, CV>[T]\n >(definition)\n ) {\n throw new Error(\n `Invalid dependency definition for ${String(token)}`\n );\n }\n\n return this.resolveInstance<T>(\n token,\n definition,\n context,\n resolutionPath\n );\n }\n default: {\n isNever(definition);\n throw new Error(\n `Unable to resolve lifetime for dependency ${String(\n token\n )}, ${resolutionPath}`\n );\n }\n }\n } else {\n return instance;\n }\n }\n\n scopedResolver<T extends keyof CV>(\n token: T,\n context?: Record<string, unknown>,\n resolutionPath: (keyof CV)[] = []\n ): (scope?: ConfigInjector<SV, CV>) => ResolvedConfigValidator<SV, CV>[T] {\n return (scope) =>\n (scope ?? this.createScope()).resolve<T>(token, context, resolutionPath);\n }\n\n createScope(): ConfigInjector<SV, CV> {\n const singletons: Record<string, unknown> = {};\n for (const dependency in this.dependenciesDefinition) {\n if (\n this.dependenciesDefinition[dependency].lifetime === Lifetime.Singleton\n ) {\n singletons[dependency] = this.instances[dependency];\n }\n }\n return new ConfigInjector<SV, CV>(\n this.schemaValidator,\n this.dependenciesDefinition\n ).load(singletons as ResolvedConfigValidator<SV, CV>);\n }\n\n dispose(): void {\n this.instances = {};\n this.load();\n }\n\n chain<ChainedCV extends ConfigValidator<SV>>(dependenciesDefinition: {\n [K in keyof ChainedCV]: {\n type: ChainedCV[K];\n } & (\n | Singleton<\n ChainedCV[K],\n Omit<ResolvedConfigValidator<SV, CV & ChainedCV>, K>,\n ResolvedConfigValidator<SV, ChainedCV>[K]\n >\n | Constructed<\n ChainedCV[K],\n Omit<ResolvedConfigValidator<SV, CV & ChainedCV>, K>,\n ResolvedConfigValidator<SV, ChainedCV>[K]\n >\n );\n }): ConfigInjector<SV, CV & ChainedCV> {\n return new ConfigInjector<SV, CV>(this.schemaValidator, {\n ...this.dependenciesDefinition,\n ...dependenciesDefinition\n }).load({ ...this.instances }) as unknown as ConfigInjector<\n SV,\n CV & ChainedCV\n >;\n }\n\n tokens(): {\n [K in keyof CV]: K;\n } {\n return Object.fromEntries(\n Object.keys(this.dependenciesDefinition).map((key) => [key, key])\n ) as {\n [K in keyof CV]: K;\n };\n }\n}\n\nexport class ValidConfigInjector<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n> extends ConfigInjector<SV, CV> {\n validConfigInjector!: void;\n}\n","import { Constructed } from '../types/configInjector.types';\n\nexport function isConstructed<Type, Args, Return>(\n value: unknown\n): value is Constructed<Type, Args, Return> {\n return (\n typeof value === 'object' &&\n value != null &&\n 'constructor' in value &&\n value.constructor != null\n );\n}\n","import {\n AnySchemaValidator,\n IdiomaticSchema,\n Schema\n} from '@forklaunch/validator';\nimport { ConfigInjector } from '../configInjector';\n\nexport const Lifetime = {\n Singleton: 0,\n Transient: 1,\n Scoped: 2\n} as const;\nexport type Lifetime = (typeof Lifetime)[keyof typeof Lifetime];\n\nexport type Singleton<Type, Args, Value> =\n | {\n lifetime: typeof Lifetime.Singleton;\n type: Type;\n value: Value;\n }\n | ConstructedSingleton<Type, Args, Value>;\n\nexport type ConstructedSingleton<Type, Args, Return> = {\n lifetime: typeof Lifetime.Singleton;\n type: Type;\n factory: (\n args: Args,\n resolve: <T extends keyof Args>(\n token: T,\n context?: Record<string, unknown>\n ) => Args[T],\n context: Record<string, unknown>\n ) => Return;\n};\n\nexport type Constructed<Type, Args, Return> = {\n lifetime: typeof Lifetime.Transient | typeof Lifetime.Scoped;\n type: Type;\n factory: (\n args: Args,\n resolve: <T extends keyof Args>(\n token: T,\n context?: Record<string, unknown>\n ) => Args[T],\n context: Record<string, unknown>\n ) => Return;\n};\n\nexport type Constructor = new (...args: never[]) => unknown;\nexport type SchemaConstructor<SV extends AnySchemaValidator> = new (\n ...args: unknown[]\n) => IdiomaticSchema<SV>;\nexport type Function = (...args: never[]) => unknown;\nexport type FunctionToConstructor = (\n ...args: never[]\n) => new (...args: never[]) => unknown;\nexport type SchemaFunction<SV extends AnySchemaValidator> = (\n args: unknown\n) => IdiomaticSchema<SV>;\n\nexport type ConfigTypes<SV extends AnySchemaValidator> =\n | Function\n | SchemaFunction<SV>\n | Constructor\n | SchemaConstructor<SV>\n | IdiomaticSchema<SV>;\n\nexport type ConfigValidator<SV extends AnySchemaValidator> = Record<\n string,\n ConfigTypes<SV> | Record<string, ConfigTypes<SV>>\n>;\n\ntype ResolveConfigValue<SV extends AnySchemaValidator, T> =\n T extends SchemaConstructor<SV>\n ? Schema<InstanceType<T>, SV>\n : T extends SchemaFunction<SV>\n ? (...args: Parameters<T>) => Schema<ReturnType<T>, SV>\n : T extends FunctionToConstructor\n ? (...args: Parameters<T>) => InstanceType<ReturnType<T>>\n : T extends Function\n ? (...args: Parameters<T>) => ReturnType<T>\n : T extends Constructor\n ? InstanceType<T>\n : T extends IdiomaticSchema<SV>\n ? Schema<T, SV>\n : T extends Record<string, ConfigTypes<SV>>\n ? {\n [K in keyof T]: ResolveConfigValue<SV, T[K]>;\n }\n : Schema<T, SV>;\n\nexport type ResolvedConfigValidator<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>\n> = {\n [M in keyof CV]: ResolveConfigValue<SV, CV[M]>;\n};\n\nexport type ScopedDependencyFactory<\n SV extends AnySchemaValidator,\n CV extends ConfigValidator<SV>,\n M extends keyof CV\n> = (scope?: ConfigInjector<SV, CV>) => ResolvedConfigValidator<SV, CV>[M];\n","import { ConstructedSingleton, Lifetime } from '../types/configInjector.types';\n\nexport function isConstructedSingleton<Type, Args, Return>(\n value: unknown\n): value is ConstructedSingleton<Type, Args, Return> {\n return (\n typeof value === 'object' &&\n value != null &&\n 'lifetime' in value &&\n value.lifetime === Lifetime.Singleton &&\n 'factory' in value\n );\n}\n","import { Constructor } from '../types/configInjector.types';\n\nexport function isConstructor(value: unknown): value is Constructor {\n return (\n typeof value === 'function' &&\n value.constructor != null &&\n value.prototype != null\n );\n}\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// WithCompliance — adds .compliance() to any MikroORM builder\n// ---------------------------------------------------------------------------\n\n/**\n * Adds `.compliance()` to a builder AND remaps chain methods so\n * `.compliance()` persists through chains. `.compliance()` returns\n * the builder intersected with a `__classified` brand — structurally\n * identical to the plain PropertyChain, avoiding TS7056 on large entities.\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(level: ComplianceLevel): T & { readonly __classified: true };\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 type { MikroORM } from '@mikro-orm/core';\nimport type { OpenTelemetryCollector } from '../http/telemetry/openTelemetryCollector';\nimport {\n getAllRetentionPolicies,\n getEntityComplianceFields,\n parseDuration,\n subtractDuration,\n type RetentionPolicy\n} from '../persistence/complianceTypes';\n\nexport interface EnforcementOptions {\n /** Filter to specific entity names (default: all with retention policies) */\n entities?: string[];\n /** Records per batch (default: 1000) */\n batchSize?: number;\n /** Log what would happen without mutating */\n dryRun?: boolean;\n}\n\nexport interface EnforcementResult {\n processed: number;\n deleted: number;\n anonymized: number;\n errors: number;\n byEntity: Record<\n string,\n { deleted: number; anonymized: number; errors: number }\n >;\n durationMs: number;\n}\n\nexport class RetentionService {\n private readonly DEFAULT_BATCH_SIZE = 1000;\n\n constructor(\n private readonly orm: MikroORM,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private readonly otel: OpenTelemetryCollector<any>\n ) {}\n\n async enforce(options?: EnforcementOptions): Promise<EnforcementResult> {\n const start = Date.now();\n const batchSize = options?.batchSize ?? this.DEFAULT_BATCH_SIZE;\n const dryRun = options?.dryRun ?? false;\n const policies = getAllRetentionPolicies();\n const result: EnforcementResult = {\n processed: 0,\n deleted: 0,\n anonymized: 0,\n errors: 0,\n byEntity: {},\n durationMs: 0\n };\n\n if (policies.size === 0) {\n this.otel.info('[RetentionService] No retention policies registered');\n result.durationMs = Date.now() - start;\n return result;\n }\n\n this.otel.info('[RetentionService] Starting enforcement', {\n entityCount: policies.size,\n batchSize,\n dryRun\n });\n\n for (const [entityName, policy] of policies) {\n if (options?.entities && !options.entities.includes(entityName)) continue;\n\n const entityResult = { deleted: 0, anonymized: 0, errors: 0 };\n\n try {\n await this.enforceEntity(\n entityName,\n policy,\n batchSize,\n entityResult,\n dryRun\n );\n } catch (err) {\n entityResult.errors++;\n this.otel.error('[RetentionService] Entity-level failure', {\n entityName,\n error: String(err)\n });\n }\n\n result.byEntity[entityName] = entityResult;\n result.deleted += entityResult.deleted;\n result.anonymized += entityResult.anonymized;\n result.errors += entityResult.errors;\n result.processed += entityResult.deleted + entityResult.anonymized;\n }\n\n result.durationMs = Date.now() - start;\n this.otel.info('[RetentionService] Enforcement complete', {\n processed: result.processed,\n deleted: result.deleted,\n anonymized: result.anonymized,\n errors: result.errors,\n durationMs: result.durationMs\n });\n\n return result;\n }\n\n private async enforceEntity(\n entityName: string,\n policy: RetentionPolicy,\n batchSize: number,\n stats: { deleted: number; anonymized: number; errors: number },\n dryRun: boolean\n ): Promise<void> {\n const metadata = [...this.orm.getMetadata().getAll().values()].find(\n (m) => m.className === entityName\n );\n\n if (!metadata) {\n this.otel.warn(\n '[RetentionService] Entity not in MikroORM metadata — skipping',\n { entityName }\n );\n return;\n }\n\n const duration = parseDuration(policy.duration);\n const cutoff = subtractDuration(new Date(), duration);\n const entityClass = metadata.class ?? metadata.className;\n\n // For anonymize: only null out PII/PHI/PCI fields that are nullable.\n // Non-nullable fields are skipped to avoid flush errors.\n const piiFieldNames: string[] = [];\n if (policy.action === 'anonymize') {\n const complianceFields = getEntityComplianceFields(entityName);\n if (complianceFields) {\n for (const [fieldName, level] of complianceFields) {\n if (level === 'none') continue;\n const prop = metadata.properties[fieldName];\n if (prop && prop.nullable) {\n piiFieldNames.push(fieldName);\n } else {\n this.otel.warn(\n '[RetentionService] Skipping non-nullable PII field for anonymize',\n { entityName, fieldName, level }\n );\n }\n }\n }\n }\n\n const filter: Record<string, unknown> = {\n createdAt: { $lt: cutoff }\n };\n if (policy.action === 'anonymize') {\n filter['retentionAnonymizedAt'] = null;\n }\n\n // Dry run: use count instead of fetching records\n if (dryRun) {\n const em = this.orm.em.fork();\n try {\n const count = await em.count(entityClass, filter);\n this.otel.info('[RetentionService] Dry run — would process', {\n entityName,\n action: policy.action,\n count\n });\n if (policy.action === 'delete') stats.deleted += count;\n else stats.anonymized += count;\n } catch (err) {\n stats.errors++;\n this.otel.error('[RetentionService] Dry run count failed', {\n entityName,\n error: String(err)\n });\n }\n return;\n }\n\n let batchNum = 0;\n\n while (true) {\n batchNum++;\n const em = this.orm.em.fork();\n\n try {\n const records = await em.find(entityClass, filter, {\n limit: batchSize,\n orderBy: { createdAt: 'ASC' }\n });\n\n if (records.length === 0) break;\n\n if (policy.action === 'delete') {\n records.forEach((r) => em.remove(r));\n await em.flush();\n stats.deleted += records.length;\n } else {\n for (const record of records) {\n const rec = record as Record<string, unknown>;\n for (const field of piiFieldNames) {\n rec[field] = null;\n }\n rec['retentionAnonymizedAt'] = new Date();\n }\n await em.flush();\n stats.anonymized += records.length;\n }\n\n this.otel.info('[RetentionService] Batch processed', {\n entityName,\n action: policy.action,\n batch: batchNum,\n count: records.length\n });\n } catch (err) {\n stats.errors++;\n this.otel.error('[RetentionService] Batch failed', {\n entityName,\n batch: batchNum,\n error: String(err)\n });\n break; // stop batching this entity, continue to next\n }\n }\n }\n}\n","import type { MikroORM } from '@mikro-orm/core';\nimport type { OpenTelemetryCollector } from '../http/telemetry/openTelemetryCollector';\nimport { MetricsDefinition } from '../http/types/openTelemetryCollector.types';\nimport {\n getEntityComplianceFields,\n getEntityUserIdField\n} from '../persistence/complianceTypes';\n\nexport interface EraseResult {\n entitiesAffected: string[];\n recordsDeleted: number;\n}\n\nexport interface ExportResult {\n userId: string;\n entities: Record<string, unknown[]>;\n}\n\n/**\n * Per-entity userIdField overrides.\n * Keys are entity names, values are the field name linking records to a user.\n *\n * @example\n * {\n * User: 'id', // the User entity IS the user record\n * Subscription: 'partyId', // billing links via partyId\n * Account: 'userId', // default, can be omitted\n * }\n */\nexport type UserIdFieldOverrides = Record<string, string>;\n\n/**\n * Common field names that link an entity to a user, tried in order.\n * Used for optimistic search when no explicit userIdField is configured.\n */\nconst CANDIDATE_USER_FIELDS = [\n 'userId',\n 'user',\n 'id',\n 'partyId',\n 'customerId',\n 'ownerId',\n 'createdBy',\n 'email'\n];\n\n/**\n * Generic compliance data service that walks all compliance-registered entities\n * and erases or exports PII/PHI/PCI data for a given user.\n *\n * Resolution order for userIdField per entity:\n * 1. Constructor overrides (highest priority)\n * 2. defineComplianceEntity({ userIdField }) registry\n * 3. Optimistic search: first CANDIDATE_USER_FIELDS match in entity metadata\n * 4. Skip entity (no user link found)\n */\nexport class ComplianceDataService {\n private readonly userIdFieldOverrides: UserIdFieldOverrides;\n\n constructor(\n private readonly orm: MikroORM,\n private readonly otel: OpenTelemetryCollector<MetricsDefinition>,\n userIdFieldOverrides?: UserIdFieldOverrides\n ) {\n this.userIdFieldOverrides = userIdFieldOverrides ?? {};\n }\n\n /**\n * Resolve the field linking an entity to a user.\n * Returns undefined if no link can be determined.\n */\n private resolveUserIdField(\n entityName: string,\n entityProperties: Record<string, unknown>\n ): string | undefined {\n // 1. Constructor override\n const override = this.userIdFieldOverrides[entityName];\n if (override) return override;\n\n // 2. Registry (from defineComplianceEntity({ userIdField }))\n const registered = getEntityUserIdField(entityName);\n if (registered !== 'userId' || entityProperties['userId']) {\n // Registry returned a non-default value, or the default 'userId' exists\n if (entityProperties[registered]) return registered;\n }\n\n // 3. Optimistic search\n for (const candidate of CANDIDATE_USER_FIELDS) {\n if (entityProperties[candidate]) return candidate;\n }\n\n // 4. No link found\n return undefined;\n }\n\n async erase(userId: string): Promise<EraseResult> {\n const em = this.orm.em.fork();\n const entitiesAffected: string[] = [];\n let recordsDeleted = 0;\n\n const allMetadata = [...this.orm.getMetadata().getAll().values()];\n\n for (const metadata of allMetadata) {\n const entityName = metadata.className;\n const fields = getEntityComplianceFields(entityName);\n if (!fields) continue;\n\n const hasPii = [...fields.values()].some(\n (level) => level === 'pii' || level === 'phi' || level === 'pci'\n );\n if (!hasPii) continue;\n\n const userIdField = this.resolveUserIdField(\n entityName,\n metadata.properties\n );\n\n if (!userIdField) {\n this.otel.warn(\n '[ComplianceDataService] No user-linking field found — skipping',\n { entityName, candidates: CANDIDATE_USER_FIELDS }\n );\n continue;\n }\n\n try {\n const entityClass = metadata.class ?? metadata.className;\n const records = await em.find(entityClass, {\n [userIdField]: userId\n });\n\n if (records.length > 0) {\n entitiesAffected.push(entityName);\n recordsDeleted += records.length;\n records.forEach((r) => em.remove(r));\n }\n } catch (err) {\n this.otel.error('[ComplianceDataService] Failed to erase entity', {\n entityName,\n userIdField,\n error: String(err)\n });\n }\n }\n\n if (recordsDeleted > 0) {\n await em.flush();\n }\n\n this.otel.info('[ComplianceDataService] Erase complete', {\n userId,\n entitiesAffected: entitiesAffected.join(','),\n recordsDeleted\n });\n\n return { entitiesAffected, recordsDeleted };\n }\n\n async export(userId: string): Promise<ExportResult> {\n const em = this.orm.em.fork();\n const entities: Record<string, unknown[]> = {};\n\n const allMetadata = [...this.orm.getMetadata().getAll().values()];\n\n for (const metadata of allMetadata) {\n const entityName = metadata.className;\n const fields = getEntityComplianceFields(entityName);\n if (!fields) continue;\n\n const hasPii = [...fields.values()].some(\n (level) => level === 'pii' || level === 'phi' || level === 'pci'\n );\n if (!hasPii) continue;\n\n const userIdField = this.resolveUserIdField(\n entityName,\n metadata.properties\n );\n\n if (!userIdField) {\n continue;\n }\n\n try {\n const entityClass = metadata.class ?? metadata.className;\n const records = await em.find(entityClass, {\n [userIdField]: userId\n });\n\n if (records.length > 0) {\n const piiFieldNames = [...fields.entries()]\n .filter(([, level]) => level !== 'none')\n .map(([name]) => name);\n\n entities[entityName] = records.map((record) => {\n const filtered: Record<string, unknown> = {};\n filtered['id'] = (record as Record<string, unknown>)['id'];\n for (const fieldName of piiFieldNames) {\n filtered[fieldName] = (record as Record<string, unknown>)[\n fieldName\n ];\n }\n return filtered;\n });\n }\n } catch (err) {\n this.otel.error('[ComplianceDataService] Failed to export entity', {\n entityName,\n userIdField,\n error: String(err)\n });\n }\n }\n\n this.otel.info('[ComplianceDataService] Export complete', {\n userId,\n entityCount: Object.keys(entities).length\n });\n\n return { userId, entities };\n }\n}\n"],"mappings":"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","names":["dotenv"]}
|