@forklaunch/core 1.5.16 → 1.5.17

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.
Files changed (32) hide show
  1. package/lib/http/index.js +29 -18
  2. package/lib/http/index.js.map +1 -1
  3. package/lib/http/index.mjs +28 -18
  4. package/lib/http/index.mjs.map +1 -1
  5. package/lib/http/router/expressLikeRouter.d.ts +3 -1
  6. package/lib/http/router/expressLikeRouter.d.ts.map +1 -1
  7. package/lib/persistence/complianceTypes.d.ts +51 -2
  8. package/lib/persistence/complianceTypes.d.ts.map +1 -1
  9. package/lib/persistence/defineComplianceEntity.d.ts +3 -3
  10. package/lib/persistence/defineComplianceEntity.d.ts.map +1 -1
  11. package/lib/persistence/encryptionSafeEm.d.ts +82 -0
  12. package/lib/persistence/encryptionSafeEm.d.ts.map +1 -0
  13. package/lib/persistence/forgivingDecryptionEm.d.ts +12 -0
  14. package/lib/persistence/forgivingDecryptionEm.d.ts.map +1 -0
  15. package/lib/persistence/index.d.ts +3 -1
  16. package/lib/persistence/index.d.ts.map +1 -1
  17. package/lib/persistence/index.js +88 -6
  18. package/lib/persistence/index.js.map +1 -1
  19. package/lib/persistence/index.mjs +86 -6
  20. package/lib/persistence/index.mjs.map +1 -1
  21. package/lib/persistence/mikroOrm.types.d.ts +7 -1
  22. package/lib/persistence/mikroOrm.types.d.ts.map +1 -1
  23. package/lib/services/configInjector.d.ts.map +1 -1
  24. package/lib/services/index.js +6 -0
  25. package/lib/services/index.js.map +1 -1
  26. package/lib/services/index.mjs +6 -0
  27. package/lib/services/index.mjs.map +1 -1
  28. package/lib/services/retentionService.d.ts +3 -2
  29. package/lib/services/retentionService.d.ts.map +1 -1
  30. package/lib/services/runRetentionEnforcement.d.ts +2 -1
  31. package/lib/services/runRetentionEnforcement.d.ts.map +1 -1
  32. package/package.json +9 -10
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/persistence/index.ts","../../src/persistence/complianceTypes.ts","../../src/persistence/compliancePropertyBuilder.ts","../../src/persistence/encryptedType.ts","../../src/persistence/defineComplianceEntity.ts","../../src/persistence/fieldEncryptor.ts","../../src/persistence/complianceEventSubscriber.ts","../../src/persistence/tenantFilter.ts","../../src/persistence/rls.ts","../../src/persistence/tenantEm.ts"],"sourcesContent":["// Compliance types and registry\nexport {\n ComplianceLevel,\n type ComplianceLevel as ComplianceLevelType,\n getComplianceMetadata,\n getEntityComplianceFields,\n entityHasEncryptedFields,\n // Retention\n RetentionAction,\n type RetentionAction as RetentionActionType,\n type RetentionPolicy,\n RetentionDuration,\n parseDuration,\n subtractDuration,\n type ParsedDuration,\n getEntityRetention,\n getAllRetentionPolicies,\n getEntityUserIdField,\n getAllUserIdFields\n} from './complianceTypes';\n\n// Compliance-aware property builder (drop-in replacement for MikroORM's p)\nexport { fp } from './compliancePropertyBuilder';\n\n// Compliance-aware entity definition (drop-in replacement for MikroORM's defineEntity)\nexport { defineComplianceEntity } from './defineComplianceEntity';\n\n// Native query blocking (prevents bypassing EncryptedType via raw queries)\nexport { wrapEmWithNativeQueryBlocking } from './complianceEventSubscriber';\n\n// Encrypted custom type (transparent field encryption at the MikroORM data layer)\nexport {\n EncryptedType,\n registerEncryptor,\n setEncryptionTenantId,\n withEncryptionContext,\n getCurrentTenantId\n} from './encryptedType';\n\n// Field encryption\nexport {\n FieldEncryptor,\n MissingEncryptionKeyError,\n DecryptionError,\n EncryptionRequiredError\n} from './fieldEncryptor';\n\n// Tenant isolation filter\nexport {\n setupTenantFilter,\n getSuperAdminContext,\n createTenantFilterDef,\n TENANT_FILTER_NAME\n} from './tenantFilter';\n\n// PostgreSQL Row-Level Security\nexport { setupRls, RlsEventSubscriber, type RlsConfig } from './rls';\n\nexport type { AnyMikroORM, ResolvedEntity } from './mikroOrm.types';\n\n// Tenant-scoped EM proxy (wraps every operation in withEncryptionContext to\n// survive AsyncLocalStorage propagation through pg connection pool callbacks)\nexport { wrapEmWithTenantContext } from './tenantEm';\n","export 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// Module augmentation — adds .compliance() via PropertyOptions\n// ---------------------------------------------------------------------------\n\n/**\n * Adds `compliance` to PropertyOptions, which flows into IncludeKeys\n * for all scalar/enum/embedded builders (PropertyOptions is extended by\n * EnumOptions and EmbeddedOptions). Relation builders use ReferenceOptions\n * instead, so they don't get .compliance() — which is what we want.\n */\ndeclare module '@mikro-orm/core' {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface PropertyOptions<Owner> {\n compliance?: ComplianceLevel;\n }\n\n interface UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys> {\n compliance(\n level: ComplianceLevel\n ): Pick<\n UniversalPropertyOptionsBuilder<\n Value,\n Options & { readonly '~c': true },\n IncludeKeys\n >,\n IncludeKeys & keyof UniversalPropertyOptionsBuilder<never, never, never>\n >;\n }\n}\n","import { safeParse, safeStringify } from '@forklaunch/common';\nimport { p, Type, type PropertyBuilders } from '@mikro-orm/core';\nimport { COMPLIANCE_KEY, type ComplianceLevel } from './complianceTypes';\nimport { EncryptedType, resolveTypeInstance } from './encryptedType';\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 * Compliance levels that require field-level encryption via EncryptedType.\n */\nconst ENCRYPTED_LEVELS: ReadonlySet<ComplianceLevel> = new Set([\n 'pii',\n 'phi',\n 'pci'\n]);\n\n/**\n * Resolve the element runtimeType and isArray flag from builder options.\n * Uses the actual MikroORM Type instance's runtimeType — no manual\n * type-name enumeration needed.\n */\nfunction resolveEncryptedTypeArgs(options: Record<string, unknown>): {\n elementRuntimeType: string;\n isArray: boolean;\n} {\n const type = options.type;\n const isArray = options.array === true;\n\n // For arrays, the type is the element type (constructor or instance).\n // For p.array() with no chained element type, type is an ArrayType instance.\n const resolved = resolveTypeInstance(type);\n if (resolved) {\n const rt = resolved.runtimeType;\n // ArrayType's runtimeType is 'string[]' — treat as array of strings\n if (rt.endsWith('[]')) {\n return { elementRuntimeType: rt.slice(0, -2) || 'string', isArray: true };\n }\n return { elementRuntimeType: rt, isArray };\n }\n\n // No type set (e.g., plain enum) — default to string\n return { elementRuntimeType: 'string', isArray };\n}\n\n/**\n * Wraps a builder that has been classified via `.compliance()`.\n * Stores the compliance level under `~compliance` for `defineComplianceEntity`.\n * For encrypted levels (pii/phi/pci), applies EncryptedType to the builder\n * so MikroORM handles encryption at the data conversion layer.\n * Chaining after `.compliance()` propagates the level through subsequent builders.\n */\nfunction wrapClassified(builder: object, level: ComplianceLevel): unknown {\n // Apply EncryptedType for encrypted compliance levels\n if (ENCRYPTED_LEVELS.has(level)) {\n const options = (builder as Record<string | symbol, unknown>)[\n '~options'\n ] as Record<string, unknown> | undefined;\n if (options) {\n const { elementRuntimeType, isArray } = resolveEncryptedTypeArgs(options);\n\n // For enum fields, extract allowed values for app-level validation\n // then remove enum metadata so MikroORM won't generate a DB check\n // constraint (the encrypted ciphertext would never satisfy it).\n let enumValues: unknown[] | undefined;\n if (options.items) {\n enumValues = extractEnumValues(options.items);\n // Remove all enum metadata so MikroORM won't generate a DB\n // check constraint (encrypted ciphertext never matches plaintext).\n delete options.items;\n delete options.nativeEnumName;\n delete options.enum;\n }\n\n options.type = new EncryptedType(elementRuntimeType, isArray, enumValues);\n // Force column type to text since encrypted output is always a string\n options.columnType = 'text';\n options.runtimeType = isArray ? 'object' : elementRuntimeType;\n }\n }\n\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// Enum type inference\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the raw values from enum items (array, native TS enum object,\n * or lazy factory function `() => EnumObj`).\n */\nfunction extractEnumValues(items: unknown): unknown[] {\n // Resolve lazy enum factory: p.enum(() => MyEnum) stores the function\n if (typeof items === 'function') {\n return extractEnumValues((items as () => unknown)());\n }\n if (Array.isArray(items)) return items;\n if (items != null && typeof items === 'object') {\n // Native TS numeric enums have reverse mappings (value → key).\n // Filter those out by keeping only values whose key is not itself a value.\n const obj = items as Record<string, unknown>;\n const allValues = Object.values(obj);\n return allValues.filter((v) => typeof v !== 'string' || !(v in obj));\n }\n return [];\n}\n\n/**\n * MikroORM custom Type that stores mixed-type enum values as text\n * using safeStringify/safeParse for round-trip fidelity.\n */\nclass EnumTextType extends Type<unknown, string> {\n override convertToDatabaseValue(value: unknown): string {\n return safeStringify(value);\n }\n\n override convertToJSValue(value: unknown): unknown {\n return safeParse(value);\n }\n\n override getColumnType(): string {\n return 'text';\n }\n\n get runtimeType(): string {\n return 'string';\n }\n}\n\n/**\n * Infer the MikroORM type for an enum property from its items.\n *\n * - All items are strings → 'text'\n * - All items are numbers → 'integer'\n * - Mixed types → EnumTextType (safeStringify/safeParse round-trip)\n * - No items → 'text'\n */\nfunction inferEnumType(items: unknown): string | EnumTextType {\n const values = extractEnumValues(items);\n if (values.length === 0) return 'text';\n\n const allStrings = values.every((v) => typeof v === 'string');\n if (allStrings) return 'text';\n\n const allNumbers = values.every((v) => typeof v === 'number');\n if (allNumbers) return 'integer';\n\n return new EnumTextType();\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: PropertyBuilders = 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 if (isBuilder(result)) {\n // For enum(), infer a default type so MikroORM metadata\n // discovery doesn't require an explicit .type() call.\n if (prop === 'enum') {\n const options = (result as Record<string | symbol, unknown>)[\n '~options'\n ] as Record<string, unknown> | undefined;\n if (options && !options.type) {\n options.type = inferEnumType(options.items);\n }\n }\n return wrapUnclassified(result);\n }\n return result;\n };\n }\n}) as PropertyBuilders;\n","import { Type, type Platform, type TransformContext } from '@mikro-orm/core';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { FieldEncryptor } from './fieldEncryptor';\n\nconst ENCRYPTED_PREFIXES = ['v1:', 'v2:'] as const;\n\nfunction isEncrypted(value: string): boolean {\n return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state\n// ---------------------------------------------------------------------------\n\nlet _encryptor: FieldEncryptor | undefined;\n\n/**\n * AsyncLocalStorage for the current tenant ID. This is set automatically\n * by `withEncryptionContext()` — users don't interact with it directly.\n */\nconst _tenantContext = new AsyncLocalStorage<{ tenantId: string }>();\n\n/**\n * Register the FieldEncryptor instance for use by EncryptedType.\n * Call this once at application bootstrap (e.g., in mikro-orm.config.ts).\n */\nexport function registerEncryptor(encryptor: FieldEncryptor): void {\n _encryptor = encryptor;\n}\n\n/**\n * Set the encryption tenant ID for the current async context.\n *\n * IMPORTANT: this uses `AsyncLocalStorage.enterWith`, which mutates the\n * current async resource's store. It does NOT reliably propagate through\n * `pg` connection pool callback async resources — pooled connections are\n * long-lived async resources created at pool init, and resumption\n * callbacks run in the pool's resource, not the caller's. As a result,\n * MikroORM hydration of encrypted columns can read an empty / wrong\n * tenant id from `getCurrentTenantId()` even when this was called\n * correctly at the start of the request.\n *\n * Prefer `withEncryptionContext(tenantId, fn)` (or the\n * `wrapEmWithTenantContext` helper) for any code path that hits the\n * database. `setEncryptionTenantId` remains useful as a best-effort seed\n * for purely synchronous code paths.\n */\nexport function setEncryptionTenantId(tenantId: string): void {\n _tenantContext.enterWith({ tenantId });\n}\n\n/**\n * Run `fn` inside a fresh AsyncLocalStorage scope bound to `tenantId`.\n *\n * Unlike `setEncryptionTenantId`, this uses `AsyncLocalStorage.run`, which\n * creates a new async resource bound to the store. Node's promise hooks\n * propagate the store forward through pool callback async resources, so\n * MikroORM hydration of encrypted columns inside `fn` (even after multiple\n * awaits and a pg pool roundtrip) sees the correct tenant id.\n *\n * Most application code should not call this directly — use\n * `wrapEmWithTenantContext(em, tenantId)` in your DI EntityManager\n * factory and every method on the returned EM will be wrapped\n * automatically.\n */\nexport function withEncryptionContext<T>(tenantId: string, fn: () => T): T {\n return _tenantContext.run({ tenantId }, fn);\n}\n\n/**\n * Get the current tenant ID. Returns empty string when no context is set\n * (startup, seeders, better-auth, background jobs).\n */\nexport function getCurrentTenantId(): string {\n return _tenantContext.getStore()?.tenantId ?? '';\n}\n\n// ---------------------------------------------------------------------------\n// Type resolution helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a MikroORM type value (string, constructor, or instance) to a\n * Type instance. Returns undefined if unresolvable.\n */\nexport function resolveTypeInstance(\n type: unknown\n): Type<unknown, unknown> | undefined {\n if (type instanceof Type) return type;\n if (typeof type === 'function') {\n try {\n const inst = new (type as new () => unknown)();\n return inst instanceof Type ? inst : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n}\n\n/**\n * Hydrate a raw value (after JSON.parse or string) to the JS type\n * indicated by `runtimeType`. This covers the cases where MikroORM's\n * own convertToJSValue is a NOOP (relies on the DB driver).\n */\nfunction hydrateValue(value: unknown, runtimeType: string): unknown {\n if (value === null || value === undefined) return value;\n switch (runtimeType) {\n case 'string':\n return typeof value === 'string' ? value : String(value);\n case 'number':\n return typeof value === 'number' ? value : Number(value);\n case 'boolean':\n return typeof value === 'boolean'\n ? value\n : typeof value === 'string'\n ? value === 'true'\n : Boolean(value);\n case 'Date':\n return value instanceof Date ? value : new Date(value as string | number);\n case 'bigint':\n return typeof value === 'bigint' ? value : BigInt(value as string);\n case 'Buffer':\n return Buffer.isBuffer(value)\n ? value\n : typeof value === 'string'\n ? Buffer.from(value, 'base64')\n : Buffer.from(value as Uint8Array);\n case 'any': // json — return as-is after JSON.parse\n default:\n return value;\n }\n}\n\n// ---------------------------------------------------------------------------\n// EncryptedType\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM custom Type that transparently encrypts/decrypts values.\n *\n * Works with any JS type. Non-string values are serialized before\n * encryption and hydrated after decryption using the original Type's\n * runtimeType for correct JS type reconstruction.\n *\n * DB column is always `text` — the encrypted ciphertext is a string\n * regardless of the original JS type.\n *\n * For array container types, the element type is tracked separately\n * so each element is hydrated individually after decryption.\n */\nexport class EncryptedType extends Type<unknown, string | null> {\n // NOTE: Using regular properties instead of #private fields to avoid\n // \"Cannot read private member\" errors when multiple copies of this class\n // are loaded (dual-package hazard with CJS/ESM or duplicate node_modules).\n readonly _elementRuntimeType: string;\n readonly _isArray: boolean;\n readonly _enumValues: unknown[] | undefined;\n\n /**\n * @param elementRuntimeType - The runtimeType of the (element) type,\n * e.g. 'string', 'number', 'Date', 'bigint', 'Buffer', 'any'.\n * @param isArray - Whether this is an array container type.\n * @param enumValues - Optional list of allowed enum values for app-level\n * validation (used when an enum field has encrypted compliance).\n */\n constructor(\n elementRuntimeType: string = 'string',\n isArray: boolean = false,\n enumValues?: unknown[]\n ) {\n super();\n this._elementRuntimeType = elementRuntimeType;\n this._isArray = isArray;\n this._enumValues = enumValues;\n }\n\n override convertToDatabaseValue(\n value: unknown,\n _platform: Platform,\n _context?: TransformContext\n ): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value === 'string' && value.length === 0) return '';\n\n // Validate enum values at app level before encryption replaces them\n // with ciphertext that would never pass a DB check constraint.\n if (this._enumValues) {\n const valuesToCheck =\n this._isArray && Array.isArray(value) ? value : [value];\n for (const v of valuesToCheck) {\n if (!this._enumValues.includes(v)) {\n throw new Error(\n `Invalid enum value: ${String(v)}. Allowed values: ${this._enumValues.join(', ')}`\n );\n }\n }\n }\n\n if (!_encryptor) {\n return this.serialize(value);\n }\n\n if (typeof value === 'string' && isEncrypted(value)) {\n return value;\n }\n\n const serialized = this.serialize(value);\n return _encryptor.encrypt(serialized, getCurrentTenantId()) ?? serialized;\n }\n\n override convertToJSValue(\n value: string | null,\n _platform: Platform\n ): unknown {\n if (value === null || value === undefined) return null;\n if (typeof value !== 'string') return value;\n\n if (!isEncrypted(value)) {\n return this.deserialize(value);\n }\n\n if (!_encryptor) {\n throw new Error(\n 'EncryptedType: no encryptor registered but database contains encrypted value. ' +\n 'Call registerEncryptor() at application bootstrap.'\n );\n }\n\n let decrypted: string | null;\n const tenantId = getCurrentTenantId();\n try {\n decrypted = _encryptor.decrypt(value, tenantId);\n } catch (err) {\n throw new Error(\n `Failed to decrypt encrypted column value: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err }\n );\n }\n if (decrypted === null) return null;\n return this.deserialize(decrypted);\n }\n\n override getColumnType(): string {\n return 'text';\n }\n\n override get runtimeType(): string {\n if (this._isArray) return 'object';\n return this._elementRuntimeType;\n }\n\n override ensureComparable(): boolean {\n return false;\n }\n\n // ---------------------------------------------------------------------------\n // Serialization helpers\n // ---------------------------------------------------------------------------\n\n private serialize(value: unknown): string {\n if (typeof value === 'string') return value;\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'bigint') return value.toString();\n if (Buffer.isBuffer(value) || value instanceof Uint8Array)\n return Buffer.from(value as Uint8Array).toString('base64');\n return JSON.stringify(value);\n }\n\n private deserialize(value: string): unknown {\n if (this._isArray) {\n try {\n const arr = JSON.parse(value);\n if (!Array.isArray(arr)) return arr;\n return arr.map((el: unknown) =>\n hydrateValue(el, this._elementRuntimeType)\n );\n } catch {\n return value;\n }\n }\n\n switch (this._elementRuntimeType) {\n case 'string':\n return value;\n case 'any':\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n // Types serialized via toString/toISOString/base64 — hydrate from string directly\n case 'bigint':\n case 'Date':\n case 'Buffer':\n return hydrateValue(value, this._elementRuntimeType);\n // Types where JSON.parse recovers the native JS value (number, boolean)\n default:\n return hydrateValue(this.tryJsonParse(value), this._elementRuntimeType);\n }\n }\n\n private tryJsonParse(value: string): unknown {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n}\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\n { '~options': { readonly '~c': true } } | ((...args: never[]) => unknown)\n ? T[K]\n : { '~options': { readonly '~c': 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 | 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 if (typeof rawProp === 'function') {\n // Relations are arrow-wrapped and auto-classified as 'none' — don't\n // call the arrow, since the referenced entity may not be initialized\n // yet (circular reference). MikroORM resolves these lazily.\n complianceFields.set(fieldName, 'none');\n continue;\n }\n const level = readComplianceLevel(rawProp);\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","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 * Derive a deterministic IV from the key and plaintext using HMAC-SHA256,\n * truncated to IV_BYTES. Same plaintext + same key → same IV → same\n * ciphertext. This enables WHERE clause matching on encrypted columns\n * while maintaining AES-256-GCM authenticated encryption.\n *\n * Meets SOC 2, HIPAA, PCI DSS, GDPR requirements for encryption at rest.\n */\n private deriveDeterministicIv(key: Buffer, plaintext: string): Buffer {\n return crypto\n .createHmac('sha256', key)\n .update(plaintext)\n .digest()\n .subarray(0, IV_BYTES);\n }\n\n /**\n * Encrypt a plaintext string.\n *\n * Uses deterministic encryption (HMAC-derived IV) so the same plaintext\n * always produces the same ciphertext. This enables database WHERE clause\n * matching and UNIQUE constraints on encrypted columns.\n *\n * @returns Format: `v2:{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 = this.deriveDeterministicIv(key, plaintext);\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 'v2',\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 * Supports both v1 (random IV) and v2 (deterministic IV) formats.\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' && parts[0] !== 'v2')) {\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 { EncryptionRequiredError } from './fieldEncryptor';\nimport {\n type ComplianceLevel,\n getEntityComplianceFields\n} from './complianceTypes';\n\n/**\n * Compliance levels that require field-level encryption.\n */\nconst ENCRYPTED_LEVELS: ReadonlySet<ComplianceLevel> = new Set([\n 'pii',\n 'phi',\n 'pci'\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 PII, PHI, or PCI compliance fields.\n *\n * This prevents bypassing the EncryptedType's encryption by using raw\n * queries. Call this in the tenant context middleware when creating the\n * 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 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 */\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 <column> = :tenantId` to all queries\n * on entities that have the configured tenant column (or its relation).\n * Entities without the property are unaffected (empty condition).\n *\n * @param column - The entity property name used for tenant isolation.\n * Defaults to `'organizationId'`. The relation name\n * (without `Id` suffix) is also checked.\n */\nexport function createTenantFilterDef(\n column: string = 'organizationId',\n relationName?: string\n): FilterDef {\n const relation = relationName ?? column.replace(/Id$/, '');\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 // If no tenant context is set (startup, background jobs, better-auth),\n // skip filtering entirely. This is safe: tenant-scoped endpoints always\n // set filter params before querying.\n if (!args?.tenantId) {\n return {};\n }\n\n try {\n const metadata = em.getMetadata().getByClassName(entityName, false);\n if (!metadata) {\n return {};\n }\n\n const hasColumn = metadata.properties[column] != null;\n const hasRelation =\n relation !== column && metadata.properties[relation] != null;\n\n if (hasColumn) {\n return { [column]: args.tenantId };\n }\n if (hasRelation) {\n return { [relation]: args.tenantId };\n }\n } catch {\n // Entity not found in metadata — skip filtering\n }\n\n return {};\n },\n default: true,\n args: false\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(\n orm: {\n em: Pick<EntityManager, 'addFilter'>;\n },\n config: {\n /** Entity property name used for tenant isolation. Defaults to `'organizationId'`. */\n column?: string;\n /** Relation property name if different from column without 'Id' suffix. E.g., if column is 'orgId' but relation is 'parentOrg'. */\n relation?: string;\n logger?: { info: (msg: string, ...args: unknown[]) => void };\n } = {}\n): void {\n orm.em.addFilter(createTenantFilterDef(config.column, config.relation));\n (config.logger ?? console).info(\n `[compliance] Tenant isolation filter registered on column '${config.column ?? 'organizationId'}'`\n );\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 } from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n/**\n * Structural subset of `MikroORM` used by RLS setup. Typed structurally\n * (rather than as `MikroORM`) because the class's `Entities` type parameter\n * defaults to a mutable array while `MikroORM.init()` returns a readonly one,\n * making concrete instances unassignable to a bare `MikroORM` parameter.\n */\nexport interface RlsOrm {\n em: Pick<\n EntityManager,\n 'getEventManager' | 'getMetadata' | 'getConnection' | 'getPlatform'\n >;\n}\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 /** Optional logger for compliance messages. Falls back to console. */\n logger?: {\n info: (msg: string, ...args: unknown[]) => void;\n warn: (msg: string, ...args: unknown[]) => void;\n };\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 } | 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: RlsOrm, config: RlsConfig = {}): void {\n const isPostgres = isPostgresDriver(orm);\n const enabled = config.enabled ?? isPostgres;\n const log = config.logger ?? { info: console.info, warn: console.warn };\n\n if (!enabled) {\n if (!isPostgres) {\n // Non-PostgreSQL — RLS not available, ORM filter is the sole enforcement\n log.info(\n '[compliance] Non-PostgreSQL database detected. RLS is not available; ORM filter is the sole tenant enforcement layer.'\n );\n return;\n }\n // PostgreSQL but explicitly disabled\n log.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 log.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 log.info('[compliance] PostgreSQL RLS event subscriber registered');\n\n // Validate RLS policies exist\n validateRlsPolicies(orm).catch((err) => {\n log.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: RlsOrm): 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: RlsOrm): 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","import type { EntityManager } from '@mikro-orm/core';\nimport { setEncryptionTenantId, withEncryptionContext } from './encryptedType';\n\n/**\n * Wrap a tenant-scoped MikroORM `EntityManager` so that every operation on\n * it executes inside `withEncryptionContext(tenantId, …)`.\n *\n * # Why this exists\n *\n * `setEncryptionTenantId` (and other places that need to record the\n * current tenant for `EncryptedType` hydration) uses\n * `AsyncLocalStorage.enterWith`. `enterWith` mutates the *current* async\n * resource's store. That works for plain Promise chains, but it does NOT\n * propagate through `pg` connection pool callback async resources.\n *\n * Pooled connections are long-lived async resources created at pool init.\n * When MikroORM hydration runs in a connection's resolution callback,\n * `getCurrentTenantId()` reads whatever store was bound to that pool\n * resource at creation time (typically empty), not the request's value.\n * The result is intermittent decrypt failures on encrypted columns even\n * though the EM is \"tenant-scoped\" everywhere in app code, with no obvious\n * culprit at any single call site.\n *\n * `withEncryptionContext(tenantId, fn)` uses `als.run(...)` instead, which\n * creates a fresh async resource bound to the store. Node's promise hooks\n * then propagate the store forward through the pool callback boundary, so\n * `getCurrentTenantId()` returns the right value at hydration time\n * regardless of which pooled connection serviced the query.\n *\n * By proxying the EM, every `find / findOne / populate / flush / persist`\n * etc executes inside its own bound `als.run` callback. Single point of\n * truth for tenant scoping; call sites stay clean.\n *\n * # Usage\n *\n * Use this in your DI `EntityManager` factory:\n *\n * ```ts\n * EntityManager: {\n * lifetime: Lifetime.Scoped,\n * type: EntityManager,\n * factory: (\n * { Orm },\n * context?: { entityManagerOptions?: ForkOptions; tenantId?: string }\n * ) =>\n * wrapEmWithTenantContext(\n * Orm.em.fork(context?.entityManagerOptions),\n * context?.tenantId\n * )\n * }\n * ```\n *\n * If `tenantId` is `undefined` or empty, the original EM is returned\n * unwrapped — useful for super-admin / lookup paths that need an\n * unscoped query before they know the tenant.\n *\n * @param em a freshly forked `EntityManager` from `orm.em.fork(...)`\n * @param tenantId the org/tenant id to bind for filter params + ALS;\n * pass `undefined` to skip wrapping entirely\n */\nexport function wrapEmWithTenantContext(\n em: EntityManager,\n tenantId: string | undefined\n): EntityManager {\n if (!tenantId) {\n return em;\n }\n\n // Apply MikroORM tenant filter (used for row-level org isolation in\n // queries) and seed AsyncLocalStorage as a best-effort for any\n // synchronous code paths that read the tenant before invoking an EM\n // method. The Proxy below is the load-bearing part for async / pg pool\n // paths.\n em.setFilterParams('tenant', { tenantId });\n setEncryptionTenantId(tenantId);\n\n return new Proxy(em, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n if (typeof value !== 'function') {\n return value;\n }\n return function tenantBoundMethod(...args: unknown[]) {\n return withEncryptionContext(tenantId, () =>\n (value as (...a: unknown[]) => unknown).apply(target, args)\n );\n };\n }\n }) as EntityManager;\n}\n"],"mappings":"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","names":["import_core","p","import_core","crypto","ENCRYPTED_LEVELS","p"]}
1
+ {"version":3,"sources":["../../src/persistence/index.ts","../../src/persistence/complianceTypes.ts","../../src/persistence/forgivingDecryptionEm.ts","../../src/persistence/encryptionSafeEm.ts","../../src/persistence/compliancePropertyBuilder.ts","../../src/persistence/encryptedType.ts","../../src/persistence/defineComplianceEntity.ts","../../src/persistence/fieldEncryptor.ts","../../src/persistence/complianceEventSubscriber.ts","../../src/persistence/tenantFilter.ts","../../src/persistence/rls.ts","../../src/persistence/tenantEm.ts"],"sourcesContent":["// Compliance types and registry\nexport {\n ComplianceLevel,\n type ComplianceLevel as ComplianceLevelType,\n getComplianceMetadata,\n getEntityComplianceFields,\n entityHasEncryptedFields,\n // Retention\n RetentionAction,\n type RetentionAction as RetentionActionType,\n type RetentionPolicy,\n RetentionDuration,\n parseDuration,\n subtractDuration,\n type ParsedDuration,\n getEntityRetention,\n getAllRetentionPolicies,\n getEntityUserIdField,\n getAllUserIdFields,\n type EncryptedComplianceLevel,\n type EncryptedKeysOf,\n type RequiresEncryptionContext,\n type SelectionAvoidsEncryptedColumns\n} from './complianceTypes';\nexport {\n wrapEmWithForgivingDecryption,\n type ForgivingDecryptionLogger\n} from './forgivingDecryptionEm';\nexport {\n asEncryptionSafe,\n type ContextFreeKeysOfSchema,\n type ContextFreeReadOptions,\n type DecryptingReadOptions,\n type EncryptedKeysOfSchema,\n type EncryptionAwareReadOptions,\n type EncryptionSafeEntityManager,\n type HydratingReadMethod,\n type PropertiesOfSchema,\n type SchemaRequiresEncryptionContext\n} from './encryptionSafeEm';\n\n// Compliance-aware property builder (drop-in replacement for MikroORM's p)\nexport { fp } from './compliancePropertyBuilder';\n\n// Compliance-aware entity definition (drop-in replacement for MikroORM's defineEntity)\nexport { defineComplianceEntity } from './defineComplianceEntity';\n\n// Native query blocking (prevents bypassing EncryptedType via raw queries)\nexport { wrapEmWithNativeQueryBlocking } from './complianceEventSubscriber';\n\n// Encrypted custom type (transparent field encryption at the MikroORM data layer)\nexport {\n EncryptedType,\n registerEncryptor,\n setEncryptionTenantId,\n withEncryptionContext,\n getCurrentTenantId\n} from './encryptedType';\n\n// Field encryption\nexport {\n FieldEncryptor,\n MissingEncryptionKeyError,\n DecryptionError,\n EncryptionRequiredError\n} from './fieldEncryptor';\n\n// Tenant isolation filter\nexport {\n setupTenantFilter,\n getSuperAdminContext,\n createTenantFilterDef,\n TENANT_FILTER_NAME\n} from './tenantFilter';\n\n// PostgreSQL Row-Level Security\nexport { setupRls, RlsEventSubscriber, type RlsConfig } from './rls';\n\nexport type { AnyMikroORM, ResolvedEntity } from './mikroOrm.types';\n\n// Tenant-scoped EM proxy (wraps every operation in withEncryptionContext to\n// survive AsyncLocalStorage propagation through pg connection pool callbacks)\nexport { wrapEmWithTenantContext } from './tenantEm';\n","export 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// Module augmentation — adds .compliance() via PropertyOptions\n// ---------------------------------------------------------------------------\n\n/**\n * Adds `compliance` to PropertyOptions, which flows into IncludeKeys\n * for all scalar/enum/embedded builders (PropertyOptions is extended by\n * EnumOptions and EmbeddedOptions). Relation builders use ReferenceOptions\n * instead, so they don't get .compliance() — which is what we want.\n */\ndeclare module '@mikro-orm/core' {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface PropertyOptions<Owner> {\n compliance?: ComplianceLevel;\n }\n\n interface UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys> {\n /**\n * The level is captured as a literal type parameter rather than widened to\n * `ComplianceLevel`, so the marker records WHICH classification a property\n * carries and not merely that it has one.\n *\n * That distinction is what makes {@link EncryptedKeysOf} possible: `pii`,\n * `phi` and `pci` are encrypted at rest and `none` is not, and a `true`\n * marker cannot tell them apart. See {@link RequiresEncryptionContext} for\n * why the difference matters at a call site.\n */\n compliance<const L extends ComplianceLevel>(\n level: L\n ): Pick<\n UniversalPropertyOptionsBuilder<\n Value,\n Options & { readonly '~c': L },\n IncludeKeys\n >,\n IncludeKeys & keyof UniversalPropertyOptionsBuilder<never, never, never>\n >;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Type-level compliance introspection\n// ---------------------------------------------------------------------------\n\n/**\n * The classifications that cause a column to be encrypted at rest by\n * `EncryptedType`. `none` is deliberately excluded — it is a real, deliberate\n * classification, just not an encrypting one.\n */\nexport type EncryptedComplianceLevel = Extract<\n ComplianceLevel,\n 'pii' | 'phi' | 'pci'\n>;\n\n/**\n * The property names of `TProperties` whose classification means the column is\n * encrypted at rest.\n *\n * Reading such a column requires the same encryption context it was written\n * under, because `FieldEncryptor` derives its key per tenant. When a read\n * happens BEFORE that context is known — a lookup whose whole purpose is to\n * discover which organisation a row belongs to — hydrating these columns cannot\n * succeed, and fails with \"ciphertext is corrupted or the wrong key was used\".\n *\n * The remedy at such a call site is a partial select that omits these keys, so\n * hydration never touches them. This type is what lets that be checked rather\n * than remembered.\n */\nexport type EncryptedKeysOf<TProperties> = {\n [K in keyof TProperties]: TProperties[K] extends {\n '~options': { readonly '~c': infer L };\n }\n ? L extends EncryptedComplianceLevel\n ? K\n : never\n : never;\n}[keyof TProperties];\n\n/**\n * True when reading `TProperties` in full would decrypt something, and so\n * requires an encryption context to already be established.\n */\nexport type RequiresEncryptionContext<TProperties> = [\n EncryptedKeysOf<TProperties>\n] extends [never]\n ? false\n : true;\n\n/**\n * True when `TFields` is a selection that provably avoids every encrypted\n * column of `TProperties` — the shape that makes a context-free read safe.\n */\nexport type SelectionAvoidsEncryptedColumns<\n TProperties,\n TFields extends readonly PropertyKey[]\n> = [Extract<TFields[number], EncryptedKeysOf<TProperties>>] extends [never]\n ? true\n : false;\n","import {\n getEntityComplianceFields,\n type ComplianceLevel\n} from './complianceTypes';\n\n/**\n * Turns a read that cannot decrypt into a read that returns everything it CAN\n * decrypt, and says exactly how to fix it.\n *\n * Without this, a read of an entity whose encrypted columns were written under\n * a different tenant throws:\n *\n * Failed to decrypt encrypted column value: Decryption failed: ciphertext is\n * corrupted or the wrong key was used\n *\n * which surfaces as a 500. Three separate endpoints hit it in one week —\n * sign-up, the onboarding /me call, and an invitation lookup — because the\n * mistake is invisible until a tenant id is actually in play.\n *\n * ## Why this retries rather than nulling the fields\n *\n * The obvious implementation is for `EncryptedType.convertToJSValue` to return\n * `undefined` instead of throwing. That DESTROYS DATA, verified against a real\n * database: MikroORM hydrates the property as `undefined`, and the next flush\n * of any unrelated field writes NULL over the ciphertext. A `Type` only ever\n * sees a value — it has no handle on the entity, so it cannot mark a property\n * as not-loaded, and anything it returns is treated as the loaded value.\n *\n * A `fields` selection is different: MikroORM never hydrates the omitted\n * columns, knows they are absent, and leaves them out of the UPDATE. Also\n * verified — the ciphertext survives a flush untouched. So the retry re-issues\n * the same query with the encrypted columns excluded, which is the one\n * mechanism that both degrades and preserves the data.\n *\n * The cost is one extra query, and only on the failing path.\n *\n * ## What it does not hide\n *\n * Genuine ciphertext corruption produces the same error as a wrong tenant —\n * AES-GCM cannot tell them apart. Degrading silently would therefore hide real\n * data damage, so every degraded read logs a warning naming the entity, the\n * columns that were dropped, and the two ways to fix it properly.\n */\n\n/** Classifications whose columns are encrypted at rest. */\nconst ENCRYPTED_LEVELS: ReadonlySet<ComplianceLevel> = new Set([\n 'pii',\n 'phi',\n 'pci'\n]);\n\n/** Reads that hydrate entities, and so can hit a decryption failure. */\nconst HYDRATING_READS = new Set([\n 'findOne',\n 'findOneOrFail',\n 'find',\n 'findAll',\n 'findAndCount'\n]);\n\nconst isDecryptionFailure = (error: unknown): boolean =>\n error instanceof Error &&\n (error.message.includes('Failed to decrypt encrypted column value') ||\n error.message.includes(\n 'Decryption failed: ciphertext is corrupted or the wrong key was used'\n ));\n\n/** Best-effort entity name from whatever a caller passed as the first argument. */\nconst resolveEntityName = (entity: unknown): string | undefined => {\n if (typeof entity === 'string') return entity;\n if (entity == null) return undefined;\n if (typeof entity === 'object') {\n const named = entity as { name?: unknown; className?: unknown };\n if (typeof named.name === 'string') return named.name;\n if (typeof named.className === 'string') return named.className;\n }\n if (typeof entity === 'function') {\n return (entity as { name?: string }).name;\n }\n return undefined;\n};\n\n/** The encrypted-at-rest column names registered for an entity. */\nconst encryptedColumnsOf = (entityName: string): string[] => {\n const fields = getEntityComplianceFields(entityName);\n if (!fields) return [];\n return [...fields.entries()]\n .filter(([, level]) => ENCRYPTED_LEVELS.has(level))\n .map(([field]) => field);\n};\n\n/** Minimal logger shape, so this does not depend on the telemetry package. */\nexport interface ForgivingDecryptionLogger {\n warn(message: string, meta?: Record<string, unknown>): void;\n}\n\nconst buildWarning = (\n entityName: string,\n dropped: string[]\n): [string, Record<string, unknown>] => [\n `[encryption] Read of '${entityName}' could not decrypt ${dropped.length} ` +\n `column(s) and returned without them: ${dropped.join(', ')}. ` +\n 'The row was NOT modified. Fix this properly by either (a) binding the ' +\n 'owning tenant — wrapEmWithTenantContext(em, tenantId), or withEncryptionContext(tenantId, ...) — ' +\n 'before the read, or (b) if the tenant genuinely is not known yet, because ' +\n 'this read is what discovers it, selecting only the columns you need: ' +\n `findOne(${entityName}, where, { fields: [...] }). ` +\n 'If the tenant WAS bound correctly, this is not a context problem — ' +\n 'suspect real ciphertext corruption and investigate the row.',\n { entity: entityName, droppedColumns: dropped }\n];\n\n/**\n * Wraps an EntityManager so hydrating reads degrade instead of throwing.\n *\n * Returns a Proxy; every non-read member passes through untouched. Reads that\n * succeed are unaffected and pay nothing.\n */\nexport function wrapEmWithForgivingDecryption<TEntityManager extends object>(\n em: TEntityManager,\n logger: ForgivingDecryptionLogger\n): TEntityManager {\n return new Proxy(em, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof prop !== 'string' || !HYDRATING_READS.has(prop)) {\n return typeof value === 'function' ? value.bind(target) : value;\n }\n if (typeof value !== 'function') return value;\n\n const read = value as (...args: unknown[]) => Promise<unknown>;\n\n return async (...args: unknown[]) => {\n try {\n return await read.apply(target, args);\n } catch (error) {\n if (!isDecryptionFailure(error)) throw error;\n\n const entityName = resolveEntityName(args[0]);\n const dropped = entityName ? encryptedColumnsOf(entityName) : [];\n\n // Nothing known to drop means the retry would be identical, so the\n // original error is the honest outcome.\n if (!entityName || dropped.length === 0) throw error;\n\n const metadata = (\n target as { getMetadata?: () => { find?: (n: string) => unknown } }\n ).getMetadata?.();\n const meta = metadata?.find?.(entityName) as\n { props?: Record<string, unknown> } | undefined;\n const allProps = meta?.props ? Object.keys(meta.props) : [];\n const safeFields = allProps.filter((p) => !dropped.includes(p));\n\n // Without metadata there is no way to name the complement, and a\n // guess could omit something the caller needs. Surface the original.\n if (safeFields.length === 0) throw error;\n\n const [message, meta_] = buildWarning(entityName, dropped);\n logger.warn(message, meta_);\n\n const [entity, where, options] = args as [\n unknown,\n unknown,\n Record<string, unknown> | undefined\n ];\n return await read.call(target, entity, where, {\n ...(options ?? {}),\n fields: safeFields\n });\n }\n };\n }\n });\n}\n","import type { EncryptedKeysOf } from './complianceTypes';\n\n/**\n * Makes \"this read would decrypt, and nothing has bound a context\" a compile\n * error instead of a production 500.\n *\n * The failure it prevents, twice seen in production: an entity carries columns\n * encrypted under an ORGANISATION's key, and a lookup reads that row to\n * discover which organisation it belongs to. The context needed to decrypt is\n * exactly what the read is trying to find, so hydration cannot succeed and\n * fails with \"ciphertext is corrupted or the wrong key was used\". The remedy is\n * a partial select omitting those columns — easy to apply, easy to forget, and\n * invisible until a tenant id is in play.\n *\n * Two things had to be true for a type to catch it, and both now are: the\n * classification level is recorded in the type (see `EncryptedKeysOf`), and an\n * entity schema exposes its properties as `readonly properties: TProperties`.\n *\n * Adoption is per call site by design. Wrapping an EntityManager here narrows\n * only that reference, so a service can move over one file at a time rather\n * than the whole repository failing to compile at once.\n */\n\n/** The property map behind an entity schema from `defineComplianceEntity`. */\nexport type PropertiesOfSchema<TSchema> = TSchema extends {\n readonly properties: infer TProperties;\n}\n ? TProperties\n : never;\n\n/** The encrypted-at-rest property names of an entity schema. */\nexport type EncryptedKeysOfSchema<TSchema> = EncryptedKeysOf<\n PropertiesOfSchema<TSchema>\n>;\n\n/** True when reading this schema in full would decrypt something. */\nexport type SchemaRequiresEncryptionContext<TSchema> = [\n EncryptedKeysOfSchema<TSchema>\n] extends [never]\n ? false\n : true;\n\n/** The property names of a schema that are safe to select without a context. */\nexport type ContextFreeKeysOfSchema<TSchema> = Exclude<\n keyof PropertiesOfSchema<TSchema>,\n EncryptedKeysOfSchema<TSchema>\n>;\n\n/**\n * Read options that provably avoid every encrypted column.\n *\n * `fields` is the same option MikroORM already takes; the constraint is only on\n * which names may appear in it.\n */\nexport type ContextFreeReadOptions<TSchema> = {\n fields: readonly ContextFreeKeysOfSchema<TSchema>[];\n};\n\n/**\n * Carried by a caller that has already bound an encryption context and so may\n * decrypt. It is a type-level acknowledgement, not a runtime flag: nothing can\n * observe an AsyncLocalStorage binding from a signature, so the honest design\n * is to make the claim explicit and greppable rather than to pretend to check\n * it.\n */\nexport type DecryptingReadOptions = {\n /**\n * Set only inside `withEncryptionContext(...)`, or where the EntityManager\n * came from `wrapEmWithTenantContext(em, tenantId)` with a real tenant.\n */\n encryptionContextIsBound: true;\n};\n\n/**\n * The options a read may pass, given what the schema carries.\n *\n * - nothing encrypted: options are unconstrained, exactly as before\n * - something encrypted: either select around it, or declare a bound context\n */\nexport type EncryptionAwareReadOptions<TSchema, TOptions> = [\n EncryptedKeysOfSchema<TSchema>\n] extends [never]\n ? TOptions\n : TOptions & (ContextFreeReadOptions<TSchema> | DecryptingReadOptions);\n\n/** Read methods that hydrate entities, and therefore decrypt. */\nexport type HydratingReadMethod =\n 'findOne' | 'findOneOrFail' | 'find' | 'findAll' | 'findAndCount';\n\n/**\n * An EntityManager whose hydrating reads carry the constraint above. Every\n * other member is passed through untouched, so this stays a drop-in narrowing\n * of an existing reference.\n */\nexport type EncryptionSafeEntityManager<TEntityManager> = Omit<\n TEntityManager,\n HydratingReadMethod\n> & {\n [\n TMethod in HydratingReadMethod & keyof TEntityManager\n ]: TEntityManager[TMethod] extends (\n entity: infer TSchema,\n where: infer TWhere,\n options?: infer TOptions\n ) => infer TResult\n ? <TActualSchema extends TSchema>(\n entity: TActualSchema,\n where: TWhere,\n options: EncryptionAwareReadOptions<TActualSchema, TOptions>\n ) => TResult\n : TEntityManager[TMethod];\n};\n\n/**\n * Narrows an EntityManager so its hydrating reads must account for encrypted\n * columns. Purely a type-level change — the same object is returned, so there\n * is no proxy, no allocation and no behavioural difference at runtime.\n */\nexport function asEncryptionSafe<TEntityManager extends object>(\n em: TEntityManager\n): EncryptionSafeEntityManager<TEntityManager> {\n return em as EncryptionSafeEntityManager<TEntityManager>;\n}\n","import { safeParse, safeStringify } from '@forklaunch/common';\nimport { p, Type, type PropertyBuilders } from '@mikro-orm/core';\nimport { COMPLIANCE_KEY, type ComplianceLevel } from './complianceTypes';\nimport { EncryptedType, resolveTypeInstance } from './encryptedType';\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 * Compliance levels that require field-level encryption via EncryptedType.\n */\nconst ENCRYPTED_LEVELS: ReadonlySet<ComplianceLevel> = new Set([\n 'pii',\n 'phi',\n 'pci'\n]);\n\n/**\n * Resolve the element runtimeType and isArray flag from builder options.\n * Uses the actual MikroORM Type instance's runtimeType — no manual\n * type-name enumeration needed.\n */\nfunction resolveEncryptedTypeArgs(options: Record<string, unknown>): {\n elementRuntimeType: string;\n isArray: boolean;\n} {\n const type = options.type;\n const isArray = options.array === true;\n\n // For arrays, the type is the element type (constructor or instance).\n // For p.array() with no chained element type, type is an ArrayType instance.\n const resolved = resolveTypeInstance(type);\n if (resolved) {\n const rt = resolved.runtimeType;\n // ArrayType's runtimeType is 'string[]' — treat as array of strings\n if (rt.endsWith('[]')) {\n return { elementRuntimeType: rt.slice(0, -2) || 'string', isArray: true };\n }\n return { elementRuntimeType: rt, isArray };\n }\n\n // No type set (e.g., plain enum) — default to string\n return { elementRuntimeType: 'string', isArray };\n}\n\n/**\n * Wraps a builder that has been classified via `.compliance()`.\n * Stores the compliance level under `~compliance` for `defineComplianceEntity`.\n * For encrypted levels (pii/phi/pci), applies EncryptedType to the builder\n * so MikroORM handles encryption at the data conversion layer.\n * Chaining after `.compliance()` propagates the level through subsequent builders.\n */\nfunction wrapClassified(builder: object, level: ComplianceLevel): unknown {\n // Apply EncryptedType for encrypted compliance levels\n if (ENCRYPTED_LEVELS.has(level)) {\n const options = (builder as Record<string | symbol, unknown>)[\n '~options'\n ] as Record<string, unknown> | undefined;\n if (options) {\n const { elementRuntimeType, isArray } = resolveEncryptedTypeArgs(options);\n\n // For enum fields, extract allowed values for app-level validation\n // then remove enum metadata so MikroORM won't generate a DB check\n // constraint (the encrypted ciphertext would never satisfy it).\n let enumValues: unknown[] | undefined;\n if (options.items) {\n enumValues = extractEnumValues(options.items);\n // Remove all enum metadata so MikroORM won't generate a DB\n // check constraint (encrypted ciphertext never matches plaintext).\n delete options.items;\n delete options.nativeEnumName;\n delete options.enum;\n }\n\n options.type = new EncryptedType(elementRuntimeType, isArray, enumValues);\n // Force column type to text since encrypted output is always a string\n options.columnType = 'text';\n options.runtimeType = isArray ? 'object' : elementRuntimeType;\n }\n }\n\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// Enum type inference\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the raw values from enum items (array, native TS enum object,\n * or lazy factory function `() => EnumObj`).\n */\nfunction extractEnumValues(items: unknown): unknown[] {\n // Resolve lazy enum factory: p.enum(() => MyEnum) stores the function\n if (typeof items === 'function') {\n return extractEnumValues((items as () => unknown)());\n }\n if (Array.isArray(items)) return items;\n if (items != null && typeof items === 'object') {\n // Native TS numeric enums have reverse mappings (value → key).\n // Filter those out by keeping only values whose key is not itself a value.\n const obj = items as Record<string, unknown>;\n const allValues = Object.values(obj);\n return allValues.filter((v) => typeof v !== 'string' || !(v in obj));\n }\n return [];\n}\n\n/**\n * MikroORM custom Type that stores mixed-type enum values as text\n * using safeStringify/safeParse for round-trip fidelity.\n */\nclass EnumTextType extends Type<unknown, string> {\n override convertToDatabaseValue(value: unknown): string {\n return safeStringify(value);\n }\n\n override convertToJSValue(value: unknown): unknown {\n return safeParse(value);\n }\n\n override getColumnType(): string {\n return 'text';\n }\n\n get runtimeType(): string {\n return 'string';\n }\n}\n\n/**\n * Infer the MikroORM type for an enum property from its items.\n *\n * - All items are strings → 'text'\n * - All items are numbers → 'integer'\n * - Mixed types → EnumTextType (safeStringify/safeParse round-trip)\n * - No items → 'text'\n */\nfunction inferEnumType(items: unknown): string | EnumTextType {\n const values = extractEnumValues(items);\n if (values.length === 0) return 'text';\n\n const allStrings = values.every((v) => typeof v === 'string');\n if (allStrings) return 'text';\n\n const allNumbers = values.every((v) => typeof v === 'number');\n if (allNumbers) return 'integer';\n\n return new EnumTextType();\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: PropertyBuilders = 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 if (isBuilder(result)) {\n // For enum(), infer a default type so MikroORM metadata\n // discovery doesn't require an explicit .type() call.\n if (prop === 'enum') {\n const options = (result as Record<string | symbol, unknown>)[\n '~options'\n ] as Record<string, unknown> | undefined;\n if (options && !options.type) {\n options.type = inferEnumType(options.items);\n }\n }\n return wrapUnclassified(result);\n }\n return result;\n };\n }\n}) as PropertyBuilders;\n","import { Type, type Platform, type TransformContext } from '@mikro-orm/core';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { FieldEncryptor } from './fieldEncryptor';\n\nconst ENCRYPTED_PREFIXES = ['v1:', 'v2:'] as const;\n\nfunction isEncrypted(value: string): boolean {\n return ENCRYPTED_PREFIXES.some((p) => value.startsWith(p));\n}\n\n// ---------------------------------------------------------------------------\n// Module-level state\n// ---------------------------------------------------------------------------\n\nlet _encryptor: FieldEncryptor | undefined;\n\n/**\n * AsyncLocalStorage for the current tenant ID. This is set automatically\n * by `withEncryptionContext()` — users don't interact with it directly.\n */\nconst _tenantContext = new AsyncLocalStorage<{ tenantId: string }>();\n\n/**\n * Register the FieldEncryptor instance for use by EncryptedType.\n * Call this once at application bootstrap (e.g., in mikro-orm.config.ts).\n */\nexport function registerEncryptor(encryptor: FieldEncryptor): void {\n _encryptor = encryptor;\n}\n\n/**\n * Set the encryption tenant ID for the current async context.\n *\n * IMPORTANT: this uses `AsyncLocalStorage.enterWith`, which mutates the\n * current async resource's store. It does NOT reliably propagate through\n * `pg` connection pool callback async resources — pooled connections are\n * long-lived async resources created at pool init, and resumption\n * callbacks run in the pool's resource, not the caller's. As a result,\n * MikroORM hydration of encrypted columns can read an empty / wrong\n * tenant id from `getCurrentTenantId()` even when this was called\n * correctly at the start of the request.\n *\n * Prefer `withEncryptionContext(tenantId, fn)` (or the\n * `wrapEmWithTenantContext` helper) for any code path that hits the\n * database. `setEncryptionTenantId` remains useful as a best-effort seed\n * for purely synchronous code paths.\n */\nexport function setEncryptionTenantId(tenantId: string): void {\n _tenantContext.enterWith({ tenantId });\n}\n\n/**\n * Run `fn` inside a fresh AsyncLocalStorage scope bound to `tenantId`.\n *\n * Unlike `setEncryptionTenantId`, this uses `AsyncLocalStorage.run`, which\n * creates a new async resource bound to the store. Node's promise hooks\n * propagate the store forward through pool callback async resources, so\n * MikroORM hydration of encrypted columns inside `fn` (even after multiple\n * awaits and a pg pool roundtrip) sees the correct tenant id.\n *\n * Most application code should not call this directly — use\n * `wrapEmWithTenantContext(em, tenantId)` in your DI EntityManager\n * factory and every method on the returned EM will be wrapped\n * automatically.\n */\nexport function withEncryptionContext<T>(tenantId: string, fn: () => T): T {\n return _tenantContext.run({ tenantId }, fn);\n}\n\n/**\n * Get the current tenant ID. Returns empty string when no context is set\n * (startup, seeders, better-auth, background jobs).\n */\nexport function getCurrentTenantId(): string {\n return _tenantContext.getStore()?.tenantId ?? '';\n}\n\n// ---------------------------------------------------------------------------\n// Type resolution helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a MikroORM type value (string, constructor, or instance) to a\n * Type instance. Returns undefined if unresolvable.\n */\nexport function resolveTypeInstance(\n type: unknown\n): Type<unknown, unknown> | undefined {\n if (type instanceof Type) return type;\n if (typeof type === 'function') {\n try {\n const inst = new (type as new () => unknown)();\n return inst instanceof Type ? inst : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n}\n\n/**\n * Hydrate a raw value (after JSON.parse or string) to the JS type\n * indicated by `runtimeType`. This covers the cases where MikroORM's\n * own convertToJSValue is a NOOP (relies on the DB driver).\n */\nfunction hydrateValue(value: unknown, runtimeType: string): unknown {\n if (value === null || value === undefined) return value;\n switch (runtimeType) {\n case 'string':\n return typeof value === 'string' ? value : String(value);\n case 'number':\n return typeof value === 'number' ? value : Number(value);\n case 'boolean':\n return typeof value === 'boolean'\n ? value\n : typeof value === 'string'\n ? value === 'true'\n : Boolean(value);\n case 'Date':\n return value instanceof Date ? value : new Date(value as string | number);\n case 'bigint':\n return typeof value === 'bigint' ? value : BigInt(value as string);\n case 'Buffer':\n return Buffer.isBuffer(value)\n ? value\n : typeof value === 'string'\n ? Buffer.from(value, 'base64')\n : Buffer.from(value as Uint8Array);\n case 'any': // json — return as-is after JSON.parse\n default:\n return value;\n }\n}\n\n// ---------------------------------------------------------------------------\n// EncryptedType\n// ---------------------------------------------------------------------------\n\n/**\n * MikroORM custom Type that transparently encrypts/decrypts values.\n *\n * Works with any JS type. Non-string values are serialized before\n * encryption and hydrated after decryption using the original Type's\n * runtimeType for correct JS type reconstruction.\n *\n * DB column is always `text` — the encrypted ciphertext is a string\n * regardless of the original JS type.\n *\n * For array container types, the element type is tracked separately\n * so each element is hydrated individually after decryption.\n */\nexport class EncryptedType extends Type<unknown, string | null> {\n // NOTE: Using regular properties instead of #private fields to avoid\n // \"Cannot read private member\" errors when multiple copies of this class\n // are loaded (dual-package hazard with CJS/ESM or duplicate node_modules).\n readonly _elementRuntimeType: string;\n readonly _isArray: boolean;\n readonly _enumValues: unknown[] | undefined;\n\n /**\n * @param elementRuntimeType - The runtimeType of the (element) type,\n * e.g. 'string', 'number', 'Date', 'bigint', 'Buffer', 'any'.\n * @param isArray - Whether this is an array container type.\n * @param enumValues - Optional list of allowed enum values for app-level\n * validation (used when an enum field has encrypted compliance).\n */\n constructor(\n elementRuntimeType: string = 'string',\n isArray: boolean = false,\n enumValues?: unknown[]\n ) {\n super();\n this._elementRuntimeType = elementRuntimeType;\n this._isArray = isArray;\n this._enumValues = enumValues;\n }\n\n override convertToDatabaseValue(\n value: unknown,\n _platform: Platform,\n _context?: TransformContext\n ): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value === 'string' && value.length === 0) return '';\n\n // Validate enum values at app level before encryption replaces them\n // with ciphertext that would never pass a DB check constraint.\n if (this._enumValues) {\n const valuesToCheck =\n this._isArray && Array.isArray(value) ? value : [value];\n for (const v of valuesToCheck) {\n if (!this._enumValues.includes(v)) {\n throw new Error(\n `Invalid enum value: ${String(v)}. Allowed values: ${this._enumValues.join(', ')}`\n );\n }\n }\n }\n\n if (!_encryptor) {\n return this.serialize(value);\n }\n\n if (typeof value === 'string' && isEncrypted(value)) {\n return value;\n }\n\n const serialized = this.serialize(value);\n return _encryptor.encrypt(serialized, getCurrentTenantId()) ?? serialized;\n }\n\n override convertToJSValue(\n value: string | null,\n _platform: Platform\n ): unknown {\n if (value === null || value === undefined) return null;\n if (typeof value !== 'string') return value;\n\n if (!isEncrypted(value)) {\n return this.deserialize(value);\n }\n\n if (!_encryptor) {\n throw new Error(\n 'EncryptedType: no encryptor registered but database contains encrypted value. ' +\n 'Call registerEncryptor() at application bootstrap.'\n );\n }\n\n let decrypted: string | null;\n const tenantId = getCurrentTenantId();\n try {\n decrypted = _encryptor.decrypt(value, tenantId);\n } catch (err) {\n throw new Error(\n `Failed to decrypt encrypted column value: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err }\n );\n }\n if (decrypted === null) return null;\n return this.deserialize(decrypted);\n }\n\n override getColumnType(): string {\n return 'text';\n }\n\n override get runtimeType(): string {\n if (this._isArray) return 'object';\n return this._elementRuntimeType;\n }\n\n override ensureComparable(): boolean {\n return false;\n }\n\n // ---------------------------------------------------------------------------\n // Serialization helpers\n // ---------------------------------------------------------------------------\n\n private serialize(value: unknown): string {\n if (typeof value === 'string') return value;\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'bigint') return value.toString();\n if (Buffer.isBuffer(value) || value instanceof Uint8Array)\n return Buffer.from(value as Uint8Array).toString('base64');\n return JSON.stringify(value);\n }\n\n private deserialize(value: string): unknown {\n if (this._isArray) {\n try {\n const arr = JSON.parse(value);\n if (!Array.isArray(arr)) return arr;\n return arr.map((el: unknown) =>\n hydrateValue(el, this._elementRuntimeType)\n );\n } catch {\n return value;\n }\n }\n\n switch (this._elementRuntimeType) {\n case 'string':\n return value;\n case 'any':\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n // Types serialized via toString/toISOString/base64 — hydrate from string directly\n case 'bigint':\n case 'Date':\n case 'Buffer':\n return hydrateValue(value, this._elementRuntimeType);\n // Types where JSON.parse recovers the native JS value (number, boolean)\n default:\n return hydrateValue(this.tryJsonParse(value), this._elementRuntimeType);\n }\n }\n\n private tryJsonParse(value: string): unknown {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n}\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\n// Accepts any classification level rather than a bare `true`, now that\n// `.compliance()` records which one was chosen. The requirement is unchanged:\n// every property must have been classified.\ntype ValidateProperties<T> = {\n [K in keyof T]: T[K] extends\n | { '~options': { readonly '~c': ComplianceLevel } }\n | ((...args: never[]) => unknown)\n ? T[K]\n : { '~options': { readonly '~c': ComplianceLevel } };\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 | 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 if (typeof rawProp === 'function') {\n // Relations are arrow-wrapped and auto-classified as 'none' — don't\n // call the arrow, since the referenced entity may not be initialized\n // yet (circular reference). MikroORM resolves these lazily.\n complianceFields.set(fieldName, 'none');\n continue;\n }\n const level = readComplianceLevel(rawProp);\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","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 * Derive a deterministic IV from the key and plaintext using HMAC-SHA256,\n * truncated to IV_BYTES. Same plaintext + same key → same IV → same\n * ciphertext. This enables WHERE clause matching on encrypted columns\n * while maintaining AES-256-GCM authenticated encryption.\n *\n * Meets SOC 2, HIPAA, PCI DSS, GDPR requirements for encryption at rest.\n */\n private deriveDeterministicIv(key: Buffer, plaintext: string): Buffer {\n return crypto\n .createHmac('sha256', key)\n .update(plaintext)\n .digest()\n .subarray(0, IV_BYTES);\n }\n\n /**\n * Encrypt a plaintext string.\n *\n * Uses deterministic encryption (HMAC-derived IV) so the same plaintext\n * always produces the same ciphertext. This enables database WHERE clause\n * matching and UNIQUE constraints on encrypted columns.\n *\n * @returns Format: `v2:{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 = this.deriveDeterministicIv(key, plaintext);\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 'v2',\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 * Supports both v1 (random IV) and v2 (deterministic IV) formats.\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' && parts[0] !== 'v2')) {\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 { EncryptionRequiredError } from './fieldEncryptor';\nimport {\n type ComplianceLevel,\n getEntityComplianceFields\n} from './complianceTypes';\n\n/**\n * Compliance levels that require field-level encryption.\n */\nconst ENCRYPTED_LEVELS: ReadonlySet<ComplianceLevel> = new Set([\n 'pii',\n 'phi',\n 'pci'\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 PII, PHI, or PCI compliance fields.\n *\n * This prevents bypassing the EncryptedType's encryption by using raw\n * queries. Call this in the tenant context middleware when creating the\n * 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 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 */\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 <column> = :tenantId` to all queries\n * on entities that have the configured tenant column (or its relation).\n * Entities without the property are unaffected (empty condition).\n *\n * @param column - The entity property name used for tenant isolation.\n * Defaults to `'organizationId'`. The relation name\n * (without `Id` suffix) is also checked.\n */\nexport function createTenantFilterDef(\n column: string = 'organizationId',\n relationName?: string\n): FilterDef {\n const relation = relationName ?? column.replace(/Id$/, '');\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 // If no tenant context is set (startup, background jobs, better-auth),\n // skip filtering entirely. This is safe: tenant-scoped endpoints always\n // set filter params before querying.\n if (!args?.tenantId) {\n return {};\n }\n\n try {\n const metadata = em.getMetadata().getByClassName(entityName, false);\n if (!metadata) {\n return {};\n }\n\n const hasColumn = metadata.properties[column] != null;\n const hasRelation =\n relation !== column && metadata.properties[relation] != null;\n\n if (hasColumn) {\n return { [column]: args.tenantId };\n }\n if (hasRelation) {\n return { [relation]: args.tenantId };\n }\n } catch {\n // Entity not found in metadata — skip filtering\n }\n\n return {};\n },\n default: true,\n args: false\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(\n orm: {\n em: Pick<EntityManager, 'addFilter'>;\n },\n config: {\n /** Entity property name used for tenant isolation. Defaults to `'organizationId'`. */\n column?: string;\n /** Relation property name if different from column without 'Id' suffix. E.g., if column is 'orgId' but relation is 'parentOrg'. */\n relation?: string;\n logger?: { info: (msg: string, ...args: unknown[]) => void };\n } = {}\n): void {\n orm.em.addFilter(createTenantFilterDef(config.column, config.relation));\n (config.logger ?? console).info(\n `[compliance] Tenant isolation filter registered on column '${config.column ?? 'organizationId'}'`\n );\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 } from '@mikro-orm/core';\nimport { TENANT_FILTER_NAME } from './tenantFilter';\n\n/**\n * Structural subset of `MikroORM` used by RLS setup. Typed structurally\n * (rather than as `MikroORM`) because the class's `Entities` type parameter\n * defaults to a mutable array while `MikroORM.init()` returns a readonly one,\n * making concrete instances unassignable to a bare `MikroORM` parameter.\n */\nexport interface RlsOrm {\n em: Pick<\n EntityManager,\n 'getEventManager' | 'getMetadata' | 'getConnection' | 'getPlatform'\n >;\n}\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 /** Optional logger for compliance messages. Falls back to console. */\n logger?: {\n info: (msg: string, ...args: unknown[]) => void;\n warn: (msg: string, ...args: unknown[]) => void;\n };\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 } | 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: RlsOrm, config: RlsConfig = {}): void {\n const isPostgres = isPostgresDriver(orm);\n const enabled = config.enabled ?? isPostgres;\n const log = config.logger ?? { info: console.info, warn: console.warn };\n\n if (!enabled) {\n if (!isPostgres) {\n // Non-PostgreSQL — RLS not available, ORM filter is the sole enforcement\n log.info(\n '[compliance] Non-PostgreSQL database detected. RLS is not available; ORM filter is the sole tenant enforcement layer.'\n );\n return;\n }\n // PostgreSQL but explicitly disabled\n log.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 log.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 log.info('[compliance] PostgreSQL RLS event subscriber registered');\n\n // Validate RLS policies exist\n validateRlsPolicies(orm).catch((err) => {\n log.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: RlsOrm): 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: RlsOrm): 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","import type { EntityManager } from '@mikro-orm/core';\nimport { setEncryptionTenantId, withEncryptionContext } from './encryptedType';\n\n/**\n * Wrap a tenant-scoped MikroORM `EntityManager` so that every operation on\n * it executes inside `withEncryptionContext(tenantId, …)`.\n *\n * # Why this exists\n *\n * `setEncryptionTenantId` (and other places that need to record the\n * current tenant for `EncryptedType` hydration) uses\n * `AsyncLocalStorage.enterWith`. `enterWith` mutates the *current* async\n * resource's store. That works for plain Promise chains, but it does NOT\n * propagate through `pg` connection pool callback async resources.\n *\n * Pooled connections are long-lived async resources created at pool init.\n * When MikroORM hydration runs in a connection's resolution callback,\n * `getCurrentTenantId()` reads whatever store was bound to that pool\n * resource at creation time (typically empty), not the request's value.\n * The result is intermittent decrypt failures on encrypted columns even\n * though the EM is \"tenant-scoped\" everywhere in app code, with no obvious\n * culprit at any single call site.\n *\n * `withEncryptionContext(tenantId, fn)` uses `als.run(...)` instead, which\n * creates a fresh async resource bound to the store. Node's promise hooks\n * then propagate the store forward through the pool callback boundary, so\n * `getCurrentTenantId()` returns the right value at hydration time\n * regardless of which pooled connection serviced the query.\n *\n * By proxying the EM, every `find / findOne / populate / flush / persist`\n * etc executes inside its own bound `als.run` callback. Single point of\n * truth for tenant scoping; call sites stay clean.\n *\n * # Usage\n *\n * Use this in your DI `EntityManager` factory:\n *\n * ```ts\n * EntityManager: {\n * lifetime: Lifetime.Scoped,\n * type: EntityManager,\n * factory: (\n * { Orm },\n * context?: { entityManagerOptions?: ForkOptions; tenantId?: string }\n * ) =>\n * wrapEmWithTenantContext(\n * Orm.em.fork(context?.entityManagerOptions),\n * context?.tenantId\n * )\n * }\n * ```\n *\n * If `tenantId` is `undefined` or empty, the original EM is returned\n * unwrapped — useful for super-admin / lookup paths that need an\n * unscoped query before they know the tenant.\n *\n * @param em a freshly forked `EntityManager` from `orm.em.fork(...)`\n * @param tenantId the org/tenant id to bind for filter params + ALS;\n * pass `undefined` to skip wrapping entirely\n */\nexport function wrapEmWithTenantContext(\n em: EntityManager,\n tenantId: string | undefined\n): EntityManager {\n if (!tenantId) {\n return em;\n }\n\n // Apply MikroORM tenant filter (used for row-level org isolation in\n // queries) and seed AsyncLocalStorage as a best-effort for any\n // synchronous code paths that read the tenant before invoking an EM\n // method. The Proxy below is the load-bearing part for async / pg pool\n // paths.\n em.setFilterParams('tenant', { tenantId });\n setEncryptionTenantId(tenantId);\n\n return new Proxy(em, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n if (typeof value !== 'function') {\n return value;\n }\n return function tenantBoundMethod(...args: unknown[]) {\n return withEncryptionContext(tenantId, () =>\n (value as (...a: unknown[]) => unknown).apply(target, args)\n );\n };\n }\n }) as EntityManager;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAO,IAAM,kBAAkB;AAAA,EAC7B,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AACR;AAIO,IAAM,iBAAiB;AAM9B,IAAM,qBAAqB,oBAAI,IAA0C;AAElE,SAAS,yBACd,YACA,QACM;AACN,qBAAmB,IAAI,YAAY,MAAM;AAC3C;AAEO,SAAS,sBACd,YACA,WACiB;AACjB,SAAO,mBAAmB,IAAI,UAAU,GAAG,IAAI,SAAS,KAAK;AAC/D;AAEO,SAAS,0BACd,YAC0C;AAC1C,SAAO,mBAAmB,IAAI,UAAU;AAC1C;AAEO,SAAS,yBAAyB,YAA6B;AACpE,QAAM,SAAS,mBAAmB,IAAI,UAAU;AAChD,MAAI,CAAC,OAAQ,QAAO;AACpB,aAAW,SAAS,OAAO,OAAO,GAAG;AACnC,QAAI,UAAU,SAAS,UAAU,MAAO,QAAO;AAAA,EACjD;AACA,SAAO;AACT;AAMO,IAAM,kBAAkB;AAAA,EAC7B,QAAQ;AAAA,EACR,WAAW;AACb;AASO,IAAM,oBAAoB;AAAA,EAC/B,MAAM,CAAC,MAAsB,IAAI,CAAC;AAAA,EAClC,QAAQ,CAAC,MAAsB,IAAI,CAAC;AAAA,EACpC,OAAO,CAAC,MAAsB,IAAI,CAAC;AACrC;AAQA,IAAM,iBAAiB;AAMhB,SAAS,cAAc,KAA6B;AACzD,QAAM,QAAQ,eAAe,KAAK,GAAG;AACrC,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,+BAA+B,GAAG;AAAA,IACpC;AAAA,EACF;AACA,QAAM,QAAQ,SAAS,MAAM,CAAC,KAAK,KAAK,EAAE;AAC1C,QAAM,SAAS,SAAS,MAAM,CAAC,KAAK,KAAK,EAAE;AAC3C,QAAM,OAAO,SAAS,MAAM,CAAC,KAAK,KAAK,EAAE;AAGzC,QAAM,aAAa,QAAQ,MAAM,SAAS,KAAK;AAC/C,MAAI,aAAa,GAAG;AAClB,UAAM,IAAI;AAAA,MACR,oDAAoD,GAAG,MAAM,UAAU;AAAA,IACzE;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,QAAQ,KAAK;AAC/B;AAMO,SAAS,iBAAiB,MAAY,UAAgC;AAC3E,QAAM,SAAS,IAAI,KAAK,IAAI;AAC5B,SAAO,YAAY,OAAO,YAAY,IAAI,SAAS,KAAK;AACxD,SAAO,SAAS,OAAO,SAAS,IAAI,SAAS,MAAM;AACnD,SAAO,QAAQ,OAAO,QAAQ,IAAI,SAAS,IAAI;AAC/C,SAAO;AACT;AAEA,IAAM,oBAAoB,oBAAI,IAA6B;AAM3D,IAAM,wBAAwB;AAC9B,IAAM,sBAAsB,oBAAI,IAAoB;AAE7C,SAAS,0BACd,YACA,OACM;AACN,sBAAoB,IAAI,YAAY,KAAK;AAC3C;AAEO,SAAS,qBAAqB,YAA4B;AAC/D,SAAO,oBAAoB,IAAI,UAAU,KAAK;AAChD;AAEO,SAAS,qBAAkD;AAChE,SAAO;AACT;AAEO,SAAS,wBACd,YACA,QACM;AACN,oBAAkB,IAAI,YAAY,MAAM;AAC1C;AAEO,SAAS,mBACd,YAC6B;AAC7B,SAAO,kBAAkB,IAAI,UAAU;AACzC;AAEO,SAAS,0BAGd;AACA,SAAO;AACT;;;AC/GA,IAAM,mBAAiD,oBAAI,IAAI;AAAA,EAC7D;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,IAAM,kBAAkB,oBAAI,IAAI;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,sBAAsB,CAAC,UAC3B,iBAAiB,UAChB,MAAM,QAAQ,SAAS,0CAA0C,KAChE,MAAM,QAAQ;AAAA,EACZ;AACF;AAGJ,IAAM,oBAAoB,CAAC,WAAwC;AACjE,MAAI,OAAO,WAAW,SAAU,QAAO;AACvC,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,OAAO,WAAW,UAAU;AAC9B,UAAM,QAAQ;AACd,QAAI,OAAO,MAAM,SAAS,SAAU,QAAO,MAAM;AACjD,QAAI,OAAO,MAAM,cAAc,SAAU,QAAO,MAAM;AAAA,EACxD;AACA,MAAI,OAAO,WAAW,YAAY;AAChC,WAAQ,OAA6B;AAAA,EACvC;AACA,SAAO;AACT;AAGA,IAAM,qBAAqB,CAAC,eAAiC;AAC3D,QAAM,SAAS,0BAA0B,UAAU;AACnD,MAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,SAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,EACxB,OAAO,CAAC,CAAC,EAAE,KAAK,MAAM,iBAAiB,IAAI,KAAK,CAAC,EACjD,IAAI,CAAC,CAAC,KAAK,MAAM,KAAK;AAC3B;AAOA,IAAM,eAAe,CACnB,YACA,YACsC;AAAA,EACtC,yBAAyB,UAAU,uBAAuB,QAAQ,MAAM,yCAC9B,QAAQ,KAAK,IAAI,CAAC,6UAK/C,UAAU;AAAA,EAGvB,EAAE,QAAQ,YAAY,gBAAgB,QAAQ;AAChD;AAQO,SAAS,8BACd,IACA,QACgB;AAChB,SAAO,IAAI,MAAM,IAAI;AAAA,IACnB,IAAI,QAAQ,MAAM,UAAU;AAC1B,YAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAEhD,UAAI,OAAO,SAAS,YAAY,CAAC,gBAAgB,IAAI,IAAI,GAAG;AAC1D,eAAO,OAAO,UAAU,aAAa,MAAM,KAAK,MAAM,IAAI;AAAA,MAC5D;AACA,UAAI,OAAO,UAAU,WAAY,QAAO;AAExC,YAAM,OAAO;AAEb,aAAO,UAAU,SAAoB;AACnC,YAAI;AACF,iBAAO,MAAM,KAAK,MAAM,QAAQ,IAAI;AAAA,QACtC,SAAS,OAAO;AACd,cAAI,CAAC,oBAAoB,KAAK,EAAG,OAAM;AAEvC,gBAAM,aAAa,kBAAkB,KAAK,CAAC,CAAC;AAC5C,gBAAM,UAAU,aAAa,mBAAmB,UAAU,IAAI,CAAC;AAI/D,cAAI,CAAC,cAAc,QAAQ,WAAW,EAAG,OAAM;AAE/C,gBAAM,WACJ,OACA,cAAc;AAChB,gBAAM,OAAO,UAAU,OAAO,UAAU;AAExC,gBAAM,WAAW,MAAM,QAAQ,OAAO,KAAK,KAAK,KAAK,IAAI,CAAC;AAC1D,gBAAM,aAAa,SAAS,OAAO,CAACA,OAAM,CAAC,QAAQ,SAASA,EAAC,CAAC;AAI9D,cAAI,WAAW,WAAW,EAAG,OAAM;AAEnC,gBAAM,CAAC,SAAS,KAAK,IAAI,aAAa,YAAY,OAAO;AACzD,iBAAO,KAAK,SAAS,KAAK;AAE1B,gBAAM,CAAC,QAAQ,OAAO,OAAO,IAAI;AAKjC,iBAAO,MAAM,KAAK,KAAK,QAAQ,QAAQ,OAAO;AAAA,YAC5C,GAAI,WAAW,CAAC;AAAA,YAChB,QAAQ;AAAA,UACV,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;ACxDO,SAAS,iBACd,IAC6C;AAC7C,SAAO;AACT;;;AC1HA,oBAAyC;AACzC,IAAAC,eAA+C;;;ACD/C,kBAA2D;AAC3D,8BAAkC;AAGlC,IAAM,qBAAqB,CAAC,OAAO,KAAK;AAExC,SAAS,YAAY,OAAwB;AAC3C,SAAO,mBAAmB,KAAK,CAACC,OAAM,MAAM,WAAWA,EAAC,CAAC;AAC3D;AAMA,IAAI;AAMJ,IAAM,iBAAiB,IAAI,0CAAwC;AAM5D,SAAS,kBAAkB,WAAiC;AACjE,eAAa;AACf;AAmBO,SAAS,sBAAsB,UAAwB;AAC5D,iBAAe,UAAU,EAAE,SAAS,CAAC;AACvC;AAgBO,SAAS,sBAAyB,UAAkB,IAAgB;AACzE,SAAO,eAAe,IAAI,EAAE,SAAS,GAAG,EAAE;AAC5C;AAMO,SAAS,qBAA6B;AAC3C,SAAO,eAAe,SAAS,GAAG,YAAY;AAChD;AAUO,SAAS,oBACd,MACoC;AACpC,MAAI,gBAAgB,iBAAM,QAAO;AACjC,MAAI,OAAO,SAAS,YAAY;AAC9B,QAAI;AACF,YAAM,OAAO,IAAK,KAA2B;AAC7C,aAAO,gBAAgB,mBAAO,OAAO;AAAA,IACvC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAOA,SAAS,aAAa,OAAgB,aAA8B;AAClE,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,UAAQ,aAAa;AAAA,IACnB,KAAK;AACH,aAAO,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAK;AAAA,IACzD,KAAK;AACH,aAAO,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAK;AAAA,IACzD,KAAK;AACH,aAAO,OAAO,UAAU,YACpB,QACA,OAAO,UAAU,WACf,UAAU,SACV,QAAQ,KAAK;AAAA,IACrB,KAAK;AACH,aAAO,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAwB;AAAA,IAC1E,KAAK;AACH,aAAO,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAe;AAAA,IACnE,KAAK;AACH,aAAO,OAAO,SAAS,KAAK,IACxB,QACA,OAAO,UAAU,WACf,OAAO,KAAK,OAAO,QAAQ,IAC3B,OAAO,KAAK,KAAmB;AAAA,IACvC,KAAK;AAAA;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;AAmBO,IAAM,gBAAN,cAA4B,iBAA6B;AAAA;AAAA;AAAA;AAAA,EAIrD;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAST,YACE,qBAA6B,UAC7B,UAAmB,OACnB,YACA;AACA,UAAM;AACN,SAAK,sBAAsB;AAC3B,SAAK,WAAW;AAChB,SAAK,cAAc;AAAA,EACrB;AAAA,EAES,uBACP,OACA,WACA,UACe;AACf,QAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAI5D,QAAI,KAAK,aAAa;AACpB,YAAM,gBACJ,KAAK,YAAY,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AACxD,iBAAW,KAAK,eAAe;AAC7B,YAAI,CAAC,KAAK,YAAY,SAAS,CAAC,GAAG;AACjC,gBAAM,IAAI;AAAA,YACR,uBAAuB,OAAO,CAAC,CAAC,qBAAqB,KAAK,YAAY,KAAK,IAAI,CAAC;AAAA,UAClF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,YAAY;AACf,aAAO,KAAK,UAAU,KAAK;AAAA,IAC7B;AAEA,QAAI,OAAO,UAAU,YAAY,YAAY,KAAK,GAAG;AACnD,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,KAAK,UAAU,KAAK;AACvC,WAAO,WAAW,QAAQ,YAAY,mBAAmB,CAAC,KAAK;AAAA,EACjE;AAAA,EAES,iBACP,OACA,WACS;AACT,QAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAI,OAAO,UAAU,SAAU,QAAO;AAEtC,QAAI,CAAC,YAAY,KAAK,GAAG;AACvB,aAAO,KAAK,YAAY,KAAK;AAAA,IAC/B;AAEA,QAAI,CAAC,YAAY;AACf,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAEA,QAAI;AACJ,UAAM,WAAW,mBAAmB;AACpC,QAAI;AACF,kBAAY,WAAW,QAAQ,OAAO,QAAQ;AAAA,IAChD,SAAS,KAAK;AACZ,YAAM,IAAI;AAAA,QACR,6CAA6C,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,QAC7F,EAAE,OAAO,IAAI;AAAA,MACf;AAAA,IACF;AACA,QAAI,cAAc,KAAM,QAAO;AAC/B,WAAO,KAAK,YAAY,SAAS;AAAA,EACnC;AAAA,EAES,gBAAwB;AAC/B,WAAO;AAAA,EACT;AAAA,EAEA,IAAa,cAAsB;AACjC,QAAI,KAAK,SAAU,QAAO;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EAES,mBAA4B;AACnC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMQ,UAAU,OAAwB;AACxC,QAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,QAAI,OAAO,UAAU,SAAU,QAAO,MAAM,SAAS;AACrD,QAAI,OAAO,SAAS,KAAK,KAAK,iBAAiB;AAC7C,aAAO,OAAO,KAAK,KAAmB,EAAE,SAAS,QAAQ;AAC3D,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AAAA,EAEQ,YAAY,OAAwB;AAC1C,QAAI,KAAK,UAAU;AACjB,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,KAAK;AAC5B,YAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO;AAChC,eAAO,IAAI;AAAA,UAAI,CAAC,OACd,aAAa,IAAI,KAAK,mBAAmB;AAAA,QAC3C;AAAA,MACF,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAEA,YAAQ,KAAK,qBAAqB;AAAA,MAChC,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,YAAI;AACF,iBAAO,KAAK,MAAM,KAAK;AAAA,QACzB,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA;AAAA,MAEF,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO,aAAa,OAAO,KAAK,mBAAmB;AAAA;AAAA,MAErD;AACE,eAAO,aAAa,KAAK,aAAa,KAAK,GAAG,KAAK,mBAAmB;AAAA,IAC1E;AAAA,EACF;AAAA,EAEQ,aAAa,OAAwB;AAC3C,QAAI;AACF,aAAO,KAAK,MAAM,KAAK;AAAA,IACzB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;ADzSA,SAAS,UAAU,OAAiC;AAClD,SACE,SAAS,QACT,OAAO,UAAU,YACjB,cAAe;AAEnB;AAQA,SAAS,iBAAiB,SAA2B;AACnD,SAAO,IAAI,MAAM,SAAmB;AAAA,IAClC,IAAI,QAA0C,MAAM;AAClD,UAAI,SAAS,cAAc;AACzB,eAAO,CAAC,UAA2B,eAAe,QAAQ,KAAK;AAAA,MACjE;AACA,UAAI,SAAS,WAAY,QAAO,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAChE,UAAI,SAAS,eAAgB,QAAO;AAEpC,YAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAC9C,UAAI,OAAO,UAAU,YAAY;AAC/B,eAAO,IAAI,SAAoB;AAC7B,gBAAM,SAAU,MAA0C;AAAA,YACxD;AAAA,YACA;AAAA,UACF;AACA,iBAAO,UAAU,MAAM,IAAI,iBAAiB,MAAM,IAAI;AAAA,QACxD;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAKA,IAAMC,oBAAiD,oBAAI,IAAI;AAAA,EAC7D;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAOD,SAAS,yBAAyB,SAGhC;AACA,QAAM,OAAO,QAAQ;AACrB,QAAM,UAAU,QAAQ,UAAU;AAIlC,QAAM,WAAW,oBAAoB,IAAI;AACzC,MAAI,UAAU;AACZ,UAAM,KAAK,SAAS;AAEpB,QAAI,GAAG,SAAS,IAAI,GAAG;AACrB,aAAO,EAAE,oBAAoB,GAAG,MAAM,GAAG,EAAE,KAAK,UAAU,SAAS,KAAK;AAAA,IAC1E;AACA,WAAO,EAAE,oBAAoB,IAAI,QAAQ;AAAA,EAC3C;AAGA,SAAO,EAAE,oBAAoB,UAAU,QAAQ;AACjD;AASA,SAAS,eAAe,SAAiB,OAAiC;AAExE,MAAIA,kBAAiB,IAAI,KAAK,GAAG;AAC/B,UAAM,UAAW,QACf,UACF;AACA,QAAI,SAAS;AACX,YAAM,EAAE,oBAAoB,QAAQ,IAAI,yBAAyB,OAAO;AAKxE,UAAI;AACJ,UAAI,QAAQ,OAAO;AACjB,qBAAa,kBAAkB,QAAQ,KAAK;AAG5C,eAAO,QAAQ;AACf,eAAO,QAAQ;AACf,eAAO,QAAQ;AAAA,MACjB;AAEA,cAAQ,OAAO,IAAI,cAAc,oBAAoB,SAAS,UAAU;AAExE,cAAQ,aAAa;AACrB,cAAQ,cAAc,UAAU,WAAW;AAAA,IAC7C;AAAA,EACF;AAEA,SAAO,IAAI,MAAM,SAAS;AAAA,IACxB,IAAI,QAA0C,MAAM;AAClD,UAAI,SAAS,eAAgB,QAAO;AACpC,UAAI,SAAS,WAAY,QAAO,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAEhE,YAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAC9C,UAAI,OAAO,UAAU,YAAY;AAC/B,eAAO,IAAI,SAAoB;AAC7B,gBAAM,SAAU,MAA0C;AAAA,YACxD;AAAA,YACA;AAAA,UACF;AACA,iBAAO,UAAU,MAAM,IAAI,eAAe,QAAQ,KAAK,IAAI;AAAA,QAC7D;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAOA,SAAS,aAAa,SAA0B;AAC9C,SAAO,eAAe,SAAS,MAAM;AACvC;AAMA,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,iBAAiB,MAAgC;AACxD,SAAO,OAAO,SAAS,YAAY,iBAAiB,IAAI,IAAI;AAC9D;AAUA,SAAS,kBAAkB,OAA2B;AAEpD,MAAI,OAAO,UAAU,YAAY;AAC/B,WAAO,kBAAmB,MAAwB,CAAC;AAAA,EACrD;AACA,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AACjC,MAAI,SAAS,QAAQ,OAAO,UAAU,UAAU;AAG9C,UAAM,MAAM;AACZ,UAAM,YAAY,OAAO,OAAO,GAAG;AACnC,WAAO,UAAU,OAAO,CAAC,MAAM,OAAO,MAAM,YAAY,EAAE,KAAK,IAAI;AAAA,EACrE;AACA,SAAO,CAAC;AACV;AAMA,IAAM,eAAN,cAA2B,kBAAsB;AAAA,EACtC,uBAAuB,OAAwB;AACtD,eAAO,6BAAc,KAAK;AAAA,EAC5B;AAAA,EAES,iBAAiB,OAAyB;AACjD,eAAO,yBAAU,KAAK;AAAA,EACxB;AAAA,EAES,gBAAwB;AAC/B,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,cAAsB;AACxB,WAAO;AAAA,EACT;AACF;AAUA,SAAS,cAAc,OAAuC;AAC5D,QAAM,SAAS,kBAAkB,KAAK;AACtC,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAM,aAAa,OAAO,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AAC5D,MAAI,WAAY,QAAO;AAEvB,QAAM,aAAa,OAAO,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AAC5D,MAAI,WAAY,QAAO;AAEvB,SAAO,IAAI,aAAa;AAC1B;AA6BO,IAAM,KAAuB,IAAI,MAAM,gBAAG;AAAA,EAC/C,IAAI,QAA0C,MAAM;AAClD,UAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,MAAM;AAC9C,QAAI,OAAO,UAAU,WAAY,QAAO;AAExC,QAAI,iBAAiB,IAAI,GAAG;AAE1B,aAAO,IAAI,SAAoB;AAC7B,cAAM,SAAU,MAA0C;AAAA,UACxD;AAAA,UACA;AAAA,QACF;AACA,eAAO,UAAU,MAAM,IAAI,aAAa,MAAM,IAAI;AAAA,MACpD;AAAA,IACF;AAGA,WAAO,IAAI,SAAoB;AAC7B,YAAM,SAAU,MAA0C;AAAA,QACxD;AAAA,QACA;AAAA,MACF;AACA,UAAI,UAAU,MAAM,GAAG;AAGrB,YAAI,SAAS,QAAQ;AACnB,gBAAM,UAAW,OACf,UACF;AACA,cAAI,WAAW,CAAC,QAAQ,MAAM;AAC5B,oBAAQ,OAAO,cAAc,QAAQ,KAAK;AAAA,UAC5C;AAAA,QACF;AACA,eAAO,iBAAiB,MAAM;AAAA,MAChC;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF,CAAC;;;AE5SD,IAAAC,eAMO;AAsBP,SAAS,oBAAoB,SAA+C;AAC1E,MAAI,WAAW,QAAQ,OAAO,YAAY,SAAU,QAAO;AAC3D,SAAQ,QAAoC,cAAc;AAE5D;AAEO,SAAS,uBASd,MAeA;AACA,QAAM,aAAa,UAAU,OAAQ,KAAK,OAAkB;AAC5D,QAAM,mBAAmB,oBAAI,IAA6B;AAE1D,QAAM,gBAAgB,KAAK;AAC3B,QAAM,qBACJ,OAAO,kBAAkB,aAAa,cAAc,cAAC,IAAI;AAE3D,aAAW,CAAC,WAAW,OAAO,KAAK,OAAO,QAAQ,kBAAkB,GAAG;AACrE,QAAI,OAAO,YAAY,YAAY;AAIjC,uBAAiB,IAAI,WAAW,MAAM;AACtC;AAAA,IACF;AACA,UAAM,QAAQ,oBAAoB,OAAO;AAEzC,QAAI,SAAS,MAAM;AACjB,YAAM,IAAI;AAAA,QACR,UAAU,UAAU,IAAI,SAAS;AAAA,MAGnC;AAAA,IACF;AACA,qBAAiB,IAAI,WAAW,KAAK;AAAA,EACvC;AAEA,2BAAyB,YAAY,gBAAgB;AAGrD,MAAI,KAAK,WAAW;AAClB,kBAAc,KAAK,UAAU,QAAQ;AAErC,QAAI,CAAC,mBAAmB,WAAW,GAAG;AACpC,YAAM,IAAI;AAAA,QACR,WAAW,UAAU;AAAA,MAEvB;AAAA,IACF;AAEA,4BAAwB,YAAY,KAAK,SAAS;AAAA,EACpD;AAGA,MAAI,KAAK,aAAa;AACpB,8BAA0B,YAAY,KAAK,WAAW;AAAA,EACxD;AAEA,aAAO;AAAA,IACL;AAAA,EASF;AAaF;;;AClIA,oBAAmB;AAMZ,IAAM,4BAAN,cAAwC,MAAM;AAAA,EAC1C,OAAO;AAAA,EAChB,YAAY,UAAU,0CAA0C;AAC9D,UAAM,OAAO;AAAA,EACf;AACF;AAEO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAChC,OAAO;AAAA,EAChB,YACE,UAAU,wEACV;AACA,UAAM,OAAO;AAAA,EACf;AACF;AAEO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EACxC,OAAO;AAAA,EAChB,YACE,UAAU,kEACV;AACA,UAAM,OAAO;AAAA,EACf;AACF;AAMA,IAAM,YAAY;AAClB,IAAM,WAAW;AACjB,IAAM,YAAY;AAClB,IAAM,YAAY;AAClB,IAAM,YAAY,OAAO,MAAM,CAAC;AAMzB,IAAM,iBAAN,MAAqB;AAAA,EACT;AAAA,EAEjB,YAAY,WAAmB;AAC7B,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,0BAA0B;AAAA,IACtC;AACA,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,UAA0B;AAClC,WAAO,OAAO;AAAA,MACZ,cAAAC,QAAO,SAAS,WAAW,KAAK,WAAW,WAAW,UAAU,SAAS;AAAA,IAC3E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,sBAAsB,KAAa,WAA2B;AACpE,WAAO,cAAAA,QACJ,WAAW,UAAU,GAAG,EACxB,OAAO,SAAS,EAChB,OAAO,EACP,SAAS,GAAG,QAAQ;AAAA,EACzB;AAAA,EAaA,QAAQ,WAA0B,UAAkC;AAClE,QAAI,cAAc,QAAQ,cAAc,QAAW;AACjD,aAAO;AAAA,IACT;AAEA,UAAM,MAAM,KAAK,UAAU,YAAY,EAAE;AACzC,UAAM,KAAK,KAAK,sBAAsB,KAAK,SAAS;AAEpD,UAAM,SAAS,cAAAA,QAAO,eAAe,WAAW,KAAK,EAAE;AACvD,UAAM,YAAY,OAAO,OAAO;AAAA,MAC9B,OAAO,OAAO,WAAW,MAAM;AAAA,MAC/B,OAAO,MAAM;AAAA,IACf,CAAC;AACD,UAAM,UAAU,OAAO,WAAW;AAElC,WAAO;AAAA,MACL;AAAA,MACA,GAAG,SAAS,QAAQ;AAAA,MACpB,QAAQ,SAAS,QAAQ;AAAA,MACzB,UAAU,SAAS,QAAQ;AAAA,IAC7B,EAAE,KAAK,GAAG;AAAA,EACZ;AAAA,EAQA,QAAQ,YAA2B,UAAkC;AACnE,QAAI,eAAe,QAAQ,eAAe,QAAW;AACnD,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,WAAW,MAAM,GAAG;AAClC,QAAI,MAAM,WAAW,KAAM,MAAM,CAAC,MAAM,QAAQ,MAAM,CAAC,MAAM,MAAO;AAClE,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,UAAM,CAAC,EAAE,OAAO,YAAY,YAAY,IAAI;AAC5C,UAAM,KAAK,OAAO,KAAK,OAAO,QAAQ;AACtC,UAAM,UAAU,OAAO,KAAK,YAAY,QAAQ;AAChD,UAAM,YAAY,OAAO,KAAK,cAAc,QAAQ;AACpD,UAAM,MAAM,KAAK,UAAU,YAAY,EAAE;AAEzC,QAAI;AACF,YAAM,WAAW,cAAAA,QAAO,iBAAiB,WAAW,KAAK,EAAE;AAC3D,eAAS,WAAW,OAAO;AAC3B,YAAM,YAAY,OAAO,OAAO;AAAA,QAC9B,SAAS,OAAO,SAAS;AAAA,QACzB,SAAS,MAAM;AAAA,MACjB,CAAC;AACD,aAAO,UAAU,SAAS,MAAM;AAAA,IAClC,QAAQ;AACN,YAAM,IAAI,gBAAgB;AAAA,IAC5B;AAAA,EACF;AACF;;;AC9IA,IAAMC,oBAAiD,oBAAI,IAAI;AAAA,EAC7D;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAiBM,SAAS,8BAAgD,IAAU;AACxE,QAAM,kBAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,IAAI,MAAM,IAAI;AAAA,IACnB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UACE,OAAO,SAAS,YAChB,gBAAgB,SAAS,IAAwC,GACjE;AACA,eAAO,CAAC,uBAAgC,SAAoB;AAC1D,gBAAM,aAAaC,mBAAkB,kBAAkB;AACvD,cAAI,YAAY;AACd,kBAAM,SAAS,0BAA0B,UAAU;AACnD,gBAAI,QAAQ;AACV,yBAAW,CAAC,WAAW,KAAK,KAAK,QAAQ;AACvC,oBAAID,kBAAiB,IAAI,KAAK,GAAG;AAC/B,wBAAM,IAAI;AAAA,oBACR,GAAG,IAAI,yBAAyB,UAAU,oBACpC,SAAS,2BAA2B,KAAK;AAAA,kBAEjD;AAAA,gBACF;AAAA,cACF;AAAA,YACF;AAAA,UACF;AACA,gBAAM,SAAS,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AACjD,iBAAQ,OAA2C;AAAA,YACjD;AAAA,YACA;AAAA,YACA,GAAG;AAAA,UACL;AAAA,QACF;AAAA,MACF;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;AAKA,SAASC,mBAAkB,oBAAiD;AAC1E,MAAI,OAAO,uBAAuB,UAAU;AAC1C,WAAO;AAAA,EACT;AACA,MAAI,OAAO,uBAAuB,YAAY;AAC5C,WAAQ,mBAAyC;AAAA,EACnD;AACA,MACE,sBAAsB,QACtB,OAAO,uBAAuB,YAC9B,iBAAiB,oBACjB;AACA,WAAQ,mBAAmB,YAAkC;AAAA,EAC/D;AACA,SAAO;AACT;;;ACrFO,IAAM,qBAAqB;AAa3B,SAAS,sBACd,SAAiB,kBACjB,cACW;AACX,QAAM,WAAW,gBAAgB,OAAO,QAAQ,OAAO,EAAE;AACzD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,KACE,MACA,OACA,IACA,UACA,YACA;AACA,UAAI,CAAC,YAAY;AACf,eAAO,CAAC;AAAA,MACV;AAKA,UAAI,CAAC,MAAM,UAAU;AACnB,eAAO,CAAC;AAAA,MACV;AAEA,UAAI;AACF,cAAM,WAAW,GAAG,YAAY,EAAE,eAAe,YAAY,KAAK;AAClE,YAAI,CAAC,UAAU;AACb,iBAAO,CAAC;AAAA,QACV;AAEA,cAAM,YAAY,SAAS,WAAW,MAAM,KAAK;AACjD,cAAM,cACJ,aAAa,UAAU,SAAS,WAAW,QAAQ,KAAK;AAE1D,YAAI,WAAW;AACb,iBAAO,EAAE,CAAC,MAAM,GAAG,KAAK,SAAS;AAAA,QACnC;AACA,YAAI,aAAa;AACf,iBAAO,EAAE,CAAC,QAAQ,GAAG,KAAK,SAAS;AAAA,QACrC;AAAA,MACF,QAAQ;AAAA,MAER;AAEA,aAAO,CAAC;AAAA,IACV;AAAA,IACA,SAAS;AAAA,IACT,MAAM;AAAA,EACR;AACF;AAaO,SAAS,kBACd,KAGA,SAMI,CAAC,GACC;AACN,MAAI,GAAG,UAAU,sBAAsB,OAAO,QAAQ,OAAO,QAAQ,CAAC;AACtE,GAAC,OAAO,UAAU,SAAS;AAAA,IACzB,8DAA8D,OAAO,UAAU,gBAAgB;AAAA,EACjG;AACF;AAQO,SAAS,qBACd,IACmC;AACnC,QAAM,SAAS,GAAG,KAAK;AACvB,SAAO,gBAAgB,oBAAoB,EAAE,UAAU,OAAU,CAAC;AAIlE,SAAO,UAAU;AAAA,IACf,MAAM;AAAA,IACN,MAAM,CAAC;AAAA,IACP,SAAS;AAAA,EACX,CAAC;AACD,SAAO;AACT;;;ACnDO,IAAM,qBAAN,MAAoD;AAAA,EACzD,MAAM,sBAAsB,MAA8C;AACxE,UAAM,WAAW,KAAK,YAAY,KAAK,EAAE;AACzC,QAAI,CAAC,UAAU;AAEb;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,GAAG,cAAc;AAGzC,UAAM,WAAW;AAAA,MACf,8BAA8B,gBAAgB,QAAQ,CAAC;AAAA,MACvD,CAAC;AAAA,MACD;AAAA,MACA,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EAEQ,YACN,IACoB;AACpB,UAAM,SAAS,GAAG,gBAAgB,kBAAkB;AAEpD,WAAO,QAAQ;AAAA,EACjB;AACF;AAkBO,SAAS,SAAS,KAAa,SAAoB,CAAC,GAAS;AAClE,QAAM,aAAa,iBAAiB,GAAG;AACvC,QAAM,UAAU,OAAO,WAAW;AAClC,QAAM,MAAM,OAAO,UAAU,EAAE,MAAM,QAAQ,MAAM,MAAM,QAAQ,KAAK;AAEtE,MAAI,CAAC,SAAS;AACZ,QAAI,CAAC,YAAY;AAEf,UAAI;AAAA,QACF;AAAA,MACF;AACA;AAAA,IACF;AAEA,QAAI;AAAA,MACF;AAAA,IACF;AACA;AAAA,EACF;AAEA,MAAI,CAAC,YAAY;AACf,QAAI;AAAA,MACF;AAAA,IAEF;AACA;AAAA,EACF;AAGA,MAAI,GAAG,gBAAgB,EAAE,mBAAmB,IAAI,mBAAmB,CAAC;AACpE,MAAI,KAAK,yDAAyD;AAGlE,sBAAoB,GAAG,EAAE,MAAM,CAAC,QAAQ;AACtC,QAAI,KAAK,iDAAiD,GAAG;AAAA,EAC/D,CAAC;AACH;AAUA,eAAe,oBAAoB,KAA4B;AAC7D,QAAM,WAAW,IAAI,GAAG,YAAY,EAAE,OAAO;AAE7C,aAAW,QAAQ,OAAO,OAAO,QAAQ,GAAG;AAC1C,UAAM,WAAW,KAAK,WAAW,gBAAgB,KAAK;AACtD,UAAM,SAAS,KAAK,WAAW,cAAc,KAAK;AAElD,QAAI,CAAC,YAAY,CAAC,OAAQ;AAE1B,UAAM,YAAY,KAAK;AACvB,QAAI;AACF,YAAM,aAAa,IAAI,GAAG,cAAc;AACxC,YAAM,SAAS,MAAM,WAAW;AAAA,QAC9B,yDAAyD,gBAAgB,SAAS,CAAC;AAAA,QACnF,CAAC;AAAA,QACD;AAAA,MACF;AAEA,YAAM,WAAW,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC;AACnD,YAAM,kBAAkB,SAAS;AAAA,QAAK,CAACC,OACrCA,GAAE,WAAW,SAAS,QAAQ;AAAA,MAChC;AAEA,UAAI,CAAC,iBAAiB;AACpB,gBAAQ;AAAA,UACN,qDAAqD,SAAS;AAAA,iBAE1C,SAAS;AAAA,uCACa,SAAS;AAAA;AAAA,QAErD;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAGR;AAAA,EACF;AACF;AAUA,SAAS,iBAAiB,KAAsB;AAC9C,MAAI;AACF,UAAM,WAAW,IAAI,GAAG,YAAY;AACpC,UAAM,OAAO,SAAS,YAAY,KAAK,YAAY;AACnD,WAAO,KAAK,SAAS,SAAS;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,SAAS,gBAAgB,OAAuB;AAC9C,SAAO,MAAM,QAAQ,MAAM,IAAI;AACjC;;;ACjKO,SAAS,wBACd,IACA,UACe;AACf,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AAOA,KAAG,gBAAgB,UAAU,EAAE,SAAS,CAAC;AACzC,wBAAsB,QAAQ;AAE9B,SAAO,IAAI,MAAM,IAAI;AAAA,IACnB,IAAI,QAAQ,MAAM,UAAU;AAC1B,YAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAChD,UAAI,OAAO,UAAU,YAAY;AAC/B,eAAO;AAAA,MACT;AACA,aAAO,SAAS,qBAAqB,MAAiB;AACpD,eAAO;AAAA,UAAsB;AAAA,UAAU,MACpC,MAAuC,MAAM,QAAQ,IAAI;AAAA,QAC5D;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;","names":["p","import_core","p","ENCRYPTED_LEVELS","import_core","crypto","ENCRYPTED_LEVELS","resolveEntityName","p"]}
@@ -81,6 +81,84 @@ function getAllRetentionPolicies() {
81
81
  return retentionRegistry;
82
82
  }
83
83
 
84
+ // src/persistence/forgivingDecryptionEm.ts
85
+ var ENCRYPTED_LEVELS = /* @__PURE__ */ new Set([
86
+ "pii",
87
+ "phi",
88
+ "pci"
89
+ ]);
90
+ var HYDRATING_READS = /* @__PURE__ */ new Set([
91
+ "findOne",
92
+ "findOneOrFail",
93
+ "find",
94
+ "findAll",
95
+ "findAndCount"
96
+ ]);
97
+ var isDecryptionFailure = (error) => error instanceof Error && (error.message.includes("Failed to decrypt encrypted column value") || error.message.includes(
98
+ "Decryption failed: ciphertext is corrupted or the wrong key was used"
99
+ ));
100
+ var resolveEntityName = (entity) => {
101
+ if (typeof entity === "string") return entity;
102
+ if (entity == null) return void 0;
103
+ if (typeof entity === "object") {
104
+ const named = entity;
105
+ if (typeof named.name === "string") return named.name;
106
+ if (typeof named.className === "string") return named.className;
107
+ }
108
+ if (typeof entity === "function") {
109
+ return entity.name;
110
+ }
111
+ return void 0;
112
+ };
113
+ var encryptedColumnsOf = (entityName) => {
114
+ const fields = getEntityComplianceFields(entityName);
115
+ if (!fields) return [];
116
+ return [...fields.entries()].filter(([, level]) => ENCRYPTED_LEVELS.has(level)).map(([field]) => field);
117
+ };
118
+ var buildWarning = (entityName, dropped) => [
119
+ `[encryption] Read of '${entityName}' could not decrypt ${dropped.length} column(s) and returned without them: ${dropped.join(", ")}. The row was NOT modified. Fix this properly by either (a) binding the owning tenant \u2014 wrapEmWithTenantContext(em, tenantId), or withEncryptionContext(tenantId, ...) \u2014 before the read, or (b) if the tenant genuinely is not known yet, because this read is what discovers it, selecting only the columns you need: findOne(${entityName}, where, { fields: [...] }). If the tenant WAS bound correctly, this is not a context problem \u2014 suspect real ciphertext corruption and investigate the row.`,
120
+ { entity: entityName, droppedColumns: dropped }
121
+ ];
122
+ function wrapEmWithForgivingDecryption(em, logger) {
123
+ return new Proxy(em, {
124
+ get(target, prop, receiver) {
125
+ const value = Reflect.get(target, prop, receiver);
126
+ if (typeof prop !== "string" || !HYDRATING_READS.has(prop)) {
127
+ return typeof value === "function" ? value.bind(target) : value;
128
+ }
129
+ if (typeof value !== "function") return value;
130
+ const read = value;
131
+ return async (...args) => {
132
+ try {
133
+ return await read.apply(target, args);
134
+ } catch (error) {
135
+ if (!isDecryptionFailure(error)) throw error;
136
+ const entityName = resolveEntityName(args[0]);
137
+ const dropped = entityName ? encryptedColumnsOf(entityName) : [];
138
+ if (!entityName || dropped.length === 0) throw error;
139
+ const metadata = target.getMetadata?.();
140
+ const meta = metadata?.find?.(entityName);
141
+ const allProps = meta?.props ? Object.keys(meta.props) : [];
142
+ const safeFields = allProps.filter((p3) => !dropped.includes(p3));
143
+ if (safeFields.length === 0) throw error;
144
+ const [message, meta_] = buildWarning(entityName, dropped);
145
+ logger.warn(message, meta_);
146
+ const [entity, where, options] = args;
147
+ return await read.call(target, entity, where, {
148
+ ...options ?? {},
149
+ fields: safeFields
150
+ });
151
+ }
152
+ };
153
+ }
154
+ });
155
+ }
156
+
157
+ // src/persistence/encryptionSafeEm.ts
158
+ function asEncryptionSafe(em) {
159
+ return em;
160
+ }
161
+
84
162
  // src/persistence/compliancePropertyBuilder.ts
85
163
  import { safeParse, safeStringify } from "@forklaunch/common";
86
164
  import { p, Type as Type2 } from "@mikro-orm/core";
@@ -292,7 +370,7 @@ function wrapUnclassified(builder) {
292
370
  }
293
371
  });
294
372
  }
295
- var ENCRYPTED_LEVELS = /* @__PURE__ */ new Set([
373
+ var ENCRYPTED_LEVELS2 = /* @__PURE__ */ new Set([
296
374
  "pii",
297
375
  "phi",
298
376
  "pci"
@@ -311,7 +389,7 @@ function resolveEncryptedTypeArgs(options) {
311
389
  return { elementRuntimeType: "string", isArray };
312
390
  }
313
391
  function wrapClassified(builder, level) {
314
- if (ENCRYPTED_LEVELS.has(level)) {
392
+ if (ENCRYPTED_LEVELS2.has(level)) {
315
393
  const options = builder["~options"];
316
394
  if (options) {
317
395
  const { elementRuntimeType, isArray } = resolveEncryptedTypeArgs(options);
@@ -572,7 +650,7 @@ var FieldEncryptor = class {
572
650
  };
573
651
 
574
652
  // src/persistence/complianceEventSubscriber.ts
575
- var ENCRYPTED_LEVELS2 = /* @__PURE__ */ new Set([
653
+ var ENCRYPTED_LEVELS3 = /* @__PURE__ */ new Set([
576
654
  "pii",
577
655
  "phi",
578
656
  "pci"
@@ -587,12 +665,12 @@ function wrapEmWithNativeQueryBlocking(em) {
587
665
  get(target, prop, receiver) {
588
666
  if (typeof prop === "string" && BLOCKED_METHODS.includes(prop)) {
589
667
  return (entityNameOrEntity, ...rest) => {
590
- const entityName = resolveEntityName(entityNameOrEntity);
668
+ const entityName = resolveEntityName2(entityNameOrEntity);
591
669
  if (entityName) {
592
670
  const fields = getEntityComplianceFields(entityName);
593
671
  if (fields) {
594
672
  for (const [fieldName, level] of fields) {
595
- if (ENCRYPTED_LEVELS2.has(level)) {
673
+ if (ENCRYPTED_LEVELS3.has(level)) {
596
674
  throw new EncryptionRequiredError(
597
675
  `${prop}() blocked on entity '${entityName}' because field '${fieldName}' has compliance level '${level}'. Use em.create() + em.flush() instead to ensure encryption.`
598
676
  );
@@ -612,7 +690,7 @@ function wrapEmWithNativeQueryBlocking(em) {
612
690
  }
613
691
  });
614
692
  }
615
- function resolveEntityName(entityNameOrEntity) {
693
+ function resolveEntityName2(entityNameOrEntity) {
616
694
  if (typeof entityNameOrEntity === "string") {
617
695
  return entityNameOrEntity;
618
696
  }
@@ -800,6 +878,7 @@ export {
800
878
  RetentionDuration,
801
879
  RlsEventSubscriber,
802
880
  TENANT_FILTER_NAME,
881
+ asEncryptionSafe,
803
882
  createTenantFilterDef,
804
883
  defineComplianceEntity,
805
884
  entityHasEncryptedFields,
@@ -819,6 +898,7 @@ export {
819
898
  setupTenantFilter,
820
899
  subtractDuration,
821
900
  withEncryptionContext,
901
+ wrapEmWithForgivingDecryption,
822
902
  wrapEmWithNativeQueryBlocking,
823
903
  wrapEmWithTenantContext
824
904
  };