@forklaunch/core 1.5.20 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/persistence/fieldEncryptor.d.ts +137 -9
- package/lib/persistence/fieldEncryptor.d.ts.map +1 -1
- package/lib/persistence/index.d.ts +2 -1
- package/lib/persistence/index.d.ts.map +1 -1
- package/lib/persistence/index.js +409 -26
- package/lib/persistence/index.js.map +1 -1
- package/lib/persistence/index.mjs +391 -26
- package/lib/persistence/index.mjs.map +1 -1
- package/lib/persistence/keyRotation.d.ts +118 -0
- package/lib/persistence/keyRotation.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../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":["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":"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","names":["p","Type","p","ENCRYPTED_LEVELS","Type","p","p","ENCRYPTED_LEVELS","resolveEntityName","p"]}
|
|
1
|
+
{"version":3,"sources":["../../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/keyRotation.ts","../../src/persistence/tenantFilter.ts","../../src/persistence/rls.ts","../../src/persistence/tenantEm.ts"],"sourcesContent":["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:', 'v3:'] 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\nconst KEY_ID_HEX_CHARS = 12;\nconst DERIVED_KEY_CACHE_LIMIT = 4096;\n\n/**\n * Ciphertext envelopes the encryptor reads. Only `v3` names its key.\n *\n * v1:{iv}:{tag}:{data} random IV (legacy)\n * v2:{iv}:{tag}:{data} deterministic IV\n * v3:{keyId}:{iv}:{tag}:{data} deterministic IV, stamped with the key fingerprint\n */\nexport const ENCRYPTED_PREFIXES = ['v1:', 'v2:', 'v3:'] as const;\n\n/** True when the string carries one of the encryptor's envelopes. */\nexport function isEncryptedCiphertext(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n ENCRYPTED_PREFIXES.some((p) => value.startsWith(p))\n );\n}\n\n/**\n * Which envelope new values are written in.\n *\n * 'v2' unstamped. Default. Byte-compatible with every existing row, so\n * equality lookups keep matching rows written before the ring existed.\n * 'v3' stamped with the writing key's fingerprint. Reads resolve the key\n * directly, a missing key fails by name, and the rows still under a\n * key can be counted with a LIKE. Switch to it only after a rotation\n * sweep has rewritten every row: a v3 write does not byte-match a v2\n * row of the same plaintext, so lookups miss until the sweep is done.\n */\nexport type CiphertextFormat = 'v2' | 'v3';\n\n/** Environment variable holding the key new values are written with. */\nexport const ENCRYPTION_KEY_ENV = 'ENCRYPTION_KEY';\n/** Environment variable holding every previous key, comma separated. */\nexport const LEGACY_ENCRYPTION_KEYS_ENV = 'LEGACY_ENCRYPTION_KEYS';\n/** Older single-key spelling of {@link LEGACY_ENCRYPTION_KEYS_ENV}; still honoured. */\nexport const LEGACY_ENCRYPTION_KEY_ENV = 'LEGACY_ENCRYPTION_KEY';\n/** Environment variable selecting the write format, `v2` (default) or `v3`. */\nexport const ENCRYPTION_FORMAT_ENV = 'ENCRYPTION_FORMAT';\n\n/**\n * Split a key list as it appears in an environment variable: comma,\n * semicolon or whitespace separated, optionally quoted. Blanks and\n * duplicates are dropped, order is kept.\n */\nexport function parseEncryptionKeyList(\n ...sources: (string | undefined | null)[]\n): string[] {\n const keys: string[] = [];\n for (const source of sources) {\n for (const part of (source ?? '').split(/[\\s,;]+/)) {\n const key = part.trim().replace(/^[\"']|[\"']$/g, '');\n if (key && !keys.includes(key)) keys.push(key);\n }\n }\n return keys;\n}\n\n/**\n * A short, non-reversible fingerprint of a master key, for logs, reports\n * and the `v3` envelope. Two deployments with the same key get the same\n * id; the id reveals nothing about the key.\n */\nexport function encryptionKeyId(masterKey: string): string {\n return crypto\n .createHash('sha256')\n .update(masterKey)\n .digest('hex')\n .slice(0, KEY_ID_HEX_CHARS);\n}\n\n/**\n * The key fingerprint a stored value is stamped with, or null for `v1`/`v2`\n * values, which name no key.\n */\nexport function stampedKeyId(ciphertext: string): string | null {\n if (!ciphertext.startsWith('v3:')) return null;\n const parts = ciphertext.split(':');\n return parts.length === 5 ? parts[1] : null;\n}\n\n/** SQL LIKE pattern matching every value stamped with `keyId`. */\nexport function stampedPrefix(keyId: string): string {\n return `v3:${keyId}:`;\n}\n\nexport interface FieldEncryptorOptions {\n /**\n * Keys that data may still be encrypted under, newest first. Values are\n * never written with these; they are only tried when the current key\n * cannot open a ciphertext. Keep a key here until a rotation sweep\n * (`reencryptEncryptedColumns`) reports nothing left under it.\n */\n previousKeys?: readonly string[];\n /** Envelope for new values. See {@link CiphertextFormat}. Default `v2`. */\n format?: CiphertextFormat;\n}\n\n/** What {@link FieldEncryptor.open} learned about a ciphertext. */\nexport interface OpenedCiphertext {\n plaintext: string;\n /** Fingerprint of the key that opened it; see {@link encryptionKeyId}. */\n keyId: string;\n /** True when the current key opened it. */\n current: boolean;\n /** Envelope the value was stored in. */\n version: 'v1' | 'v2' | 'v3';\n /**\n * True when a rewrite would change the stored bytes: the value is under\n * a previous key, or it is unstamped while the encryptor writes `v3`.\n */\n stale: boolean;\n}\n\ninterface KeyMaterial {\n readonly id: string;\n readonly masterKey: string;\n}\n\n// ---------------------------------------------------------------------------\n// FieldEncryptor\n// ---------------------------------------------------------------------------\n\n/**\n * AES-256-GCM field encryption with per-tenant keys derived by HKDF from a\n * master key, and a key ring for rotation.\n *\n * The ring has one current key and any number of previous keys. Every\n * write uses the current key. A `v3` value names its key, so a read looks\n * it up directly and a missing key fails by name. A `v1`/`v2` value names\n * nothing, so a read tries the current key and then each previous key in\n * order; AES-GCM's authentication tag makes \"this key opens it\" a real\n * check rather than a guess. Either way the master key can change without\n * a downtime window: add the old key to `previousKeys`, deploy, run the\n * rotation sweep, then drop the old key.\n */\nexport class FieldEncryptor {\n private readonly ring: readonly KeyMaterial[];\n private readonly byId: ReadonlyMap<string, KeyMaterial>;\n private readonly derived = new Map<string, Buffer>();\n readonly format: CiphertextFormat;\n\n constructor(masterKey: string, options: FieldEncryptorOptions = {}) {\n if (!masterKey) {\n throw new MissingEncryptionKeyError();\n }\n const ring: KeyMaterial[] = [{ id: encryptionKeyId(masterKey), masterKey }];\n for (const key of options.previousKeys ?? []) {\n if (!key || ring.some((k) => k.masterKey === key)) continue;\n ring.push({ id: encryptionKeyId(key), masterKey: key });\n }\n this.ring = ring;\n this.byId = new Map(ring.map((k) => [k.id, k]));\n this.format = options.format ?? 'v2';\n }\n\n /**\n * Build the ring from the environment: `ENCRYPTION_KEY` plus\n * `LEGACY_ENCRYPTION_KEYS` (comma separated) and the older\n * `LEGACY_ENCRYPTION_KEY`; `ENCRYPTION_FORMAT` selects the envelope.\n */\n static fromEnv(env: NodeJS.ProcessEnv = process.env): FieldEncryptor {\n const masterKey = env[ENCRYPTION_KEY_ENV];\n if (!masterKey) {\n throw new MissingEncryptionKeyError(`${ENCRYPTION_KEY_ENV} must be set`);\n }\n const format = env[ENCRYPTION_FORMAT_ENV]?.trim().toLowerCase();\n if (format && format !== 'v2' && format !== 'v3') {\n throw new Error(\n `${ENCRYPTION_FORMAT_ENV} must be \"v2\" or \"v3\", got \"${format}\"`\n );\n }\n return new FieldEncryptor(masterKey, {\n previousKeys: parseEncryptionKeyList(\n env[LEGACY_ENCRYPTION_KEYS_ENV],\n env[LEGACY_ENCRYPTION_KEY_ENV]\n ),\n format: (format as CiphertextFormat | undefined) ?? 'v2'\n });\n }\n\n /** Fingerprints of the ring, current first. Safe to log. */\n get keyIds(): { current: string; previous: string[] } {\n return {\n current: this.ring[0].id,\n previous: this.ring.slice(1).map((k) => k.id)\n };\n }\n\n /** A copy of this encryptor with a different set of previous keys. */\n withPreviousKeys(previousKeys: readonly string[]): FieldEncryptor {\n return new FieldEncryptor(this.ring[0].masterKey, {\n previousKeys,\n format: this.format\n });\n }\n\n /** A copy of this encryptor writing a different envelope. */\n withFormat(format: CiphertextFormat): FieldEncryptor {\n return new FieldEncryptor(this.ring[0].masterKey, {\n previousKeys: this.ring.slice(1).map((k) => k.masterKey),\n format\n });\n }\n\n /**\n * Derive a per-tenant 32-byte key using HKDF-SHA256 from the CURRENT\n * master key. The master key is the input key material and the tenantId\n * the info context.\n */\n deriveKey(tenantId: string): Buffer {\n return this.deriveKeyFor(this.ring[0], tenantId);\n }\n\n private deriveKeyFor(key: KeyMaterial, tenantId: string): Buffer {\n const cacheKey = `${key.id} ${tenantId}`;\n const cached = this.derived.get(cacheKey);\n if (cached) return cached;\n const derived = Buffer.from(\n crypto.hkdfSync(HKDF_HASH, key.masterKey, HKDF_SALT, tenantId, KEY_BYTES)\n );\n if (this.derived.size >= DERIVED_KEY_CACHE_LIMIT) this.derived.clear();\n this.derived.set(cacheKey, derived);\n return derived;\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 under the current key.\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 `v2:{iv}:{tag}:{data}`, or `v3:{keyId}:{iv}:{tag}:{data}` when\n * the encryptor's format is `v3`. All parts base64.\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 const body = [\n iv.toString('base64'),\n authTag.toString('base64'),\n encrypted.toString('base64')\n ];\n return this.format === 'v3'\n ? ['v3', this.ring[0].id, ...body].join(':')\n : ['v2', ...body].join(':');\n }\n\n /**\n * Decrypt a ciphertext string produced by {@link encrypt}, in any\n * envelope: `v3` by its stamped key, `v1`/`v2` by trying every key in\n * the ring.\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 return this.open(ciphertext, tenantId ?? '').plaintext;\n }\n\n /**\n * Decrypt and report which key opened the value and whether a rewrite\n * would change it. Throws {@link DecryptionError} when no key in the\n * ring does.\n */\n open(ciphertext: string, tenantId = ''): OpenedCiphertext {\n const parts = ciphertext.split(':');\n const version = parts[0];\n let candidates: readonly KeyMaterial[];\n let body: string[];\n\n if (version === 'v3' && parts.length === 5) {\n const key = this.byId.get(parts[1]);\n if (!key) {\n throw new DecryptionError(\n `Decryption failed: value is stamped with key ${parts[1]}, which is not in the ring (current ${this.ring[0].id}; previous ${\n this.ring\n .slice(1)\n .map((k) => k.id)\n .join(', ') || 'none'\n }). Add it to ${LEGACY_ENCRYPTION_KEYS_ENV}.`\n );\n }\n candidates = [key];\n body = parts.slice(2);\n } else if ((version === 'v1' || version === 'v2') && parts.length === 4) {\n candidates = this.ring;\n body = parts.slice(1);\n } else {\n throw new DecryptionError(\n `Unknown ciphertext version or malformed format`\n );\n }\n\n const [ivB64, authTagB64, encryptedB64] = body;\n const iv = Buffer.from(ivB64, 'base64');\n const authTag = Buffer.from(authTagB64, 'base64');\n const encrypted = Buffer.from(encryptedB64, 'base64');\n\n for (const key of candidates) {\n try {\n const decipher = crypto.createDecipheriv(\n ALGORITHM,\n this.deriveKeyFor(key, tenantId),\n iv\n );\n decipher.setAuthTag(authTag);\n const decrypted = Buffer.concat([\n decipher.update(encrypted),\n decipher.final()\n ]);\n const current = key === this.ring[0];\n return {\n plaintext: decrypted.toString('utf8'),\n keyId: key.id,\n current,\n version: version as 'v1' | 'v2' | 'v3',\n stale: !current || (this.format === 'v3' && version !== 'v3')\n };\n } catch {\n // wrong key (or wrong tenant) for this value; try the next candidate\n }\n }\n throw new DecryptionError(\n candidates.length > 1\n ? `Decryption failed: none of the ${candidates.length} keys in the ring opens this value (ciphertext corrupted, wrong tenant, or the key it was written with is not in ${LEGACY_ENCRYPTION_KEYS_ENV})`\n : `Decryption failed: key ${candidates[0].id} does not open this value (ciphertext corrupted or wrong tenant)`\n );\n }\n\n /**\n * True when a rewrite would change the stored bytes: the value is under\n * a previous key, or unstamped while this encryptor writes `v3`. Throws\n * when no key opens it.\n */\n needsRotation(ciphertext: string, tenantId = ''): boolean {\n return this.open(ciphertext, tenantId).stale;\n }\n\n /**\n * Rewrite a value under the current key in the current format. Returns\n * the input unchanged when nothing would change, so callers can compare\n * to detect writes.\n */\n rotate(ciphertext: string | null, tenantId = ''): string | null {\n if (ciphertext === null || ciphertext === undefined) return null;\n const opened = this.open(ciphertext, tenantId);\n if (!opened.stale) return ciphertext;\n return this.encrypt(opened.plaintext, tenantId);\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 { EntityManager } from '@mikro-orm/core';\nimport { getEntityComplianceFields } from './complianceTypes';\nimport {\n isEncryptedCiphertext,\n stampedKeyId,\n type FieldEncryptor\n} from './fieldEncryptor';\n\n/**\n * Rotation sweep: rewrite every compliance-encrypted column that is still\n * under a previous master key so it is under the current one.\n *\n * Rotating a key is two steps. First the old key moves into the ring\n * (`LEGACY_ENCRYPTION_KEYS`) and the new key becomes `ENCRYPTION_KEY`; the\n * service keeps reading everything because {@link FieldEncryptor} tries\n * every key in the ring. Then this sweep runs, usually from a migration,\n * and rewrites what the old key still holds. When the report shows nothing\n * left under the old key it can be dropped from the ring.\n *\n * Per value:\n * plaintext - not a `v1:`/`v2:` ciphertext; left alone\n * current - opens with the current key in the current format; left alone\n * rewritten - opens only with a previous key, or is unstamped while the\n * encryptor writes v3; re-encrypted under the current key in\n * the current format with the SAME tenant id it was written with\n * unreadable - opens with no key under any candidate tenant; left alone\n * and reported by row id\n *\n * Tenants: keys are derived per tenant, so a value only opens under the\n * tenant it was written with. Each row is tried under its likely tenants\n * first (its `organization_id`, then the empty tenant, extended by\n * `tenantIdsFor`) and, only when those fail, under `fallbackTenantIds`\n * (typically every organization the database knows, see\n * {@link collectFallbackTenantIds}). That catches rows written under a\n * leaked or mismatched context.\n *\n * Raw SQL throughout: going through entities would decrypt on hydrate and\n * re-encrypt on flush, which is both slower and blind to which key opened\n * the value.\n */\n\nconst ENCRYPTED_LEVELS = new Set(['pii', 'phi', 'pci']);\n\nexport const isEncryptedValue = isEncryptedCiphertext;\n\nexport type RotationOutcome =\n | { kind: 'plaintext' }\n | { kind: 'current'; tenantId: string; keyId: string }\n | { kind: 'rewritten'; tenantId: string; keyId: string; next: string }\n | { kind: 'unreadable' };\n\nconst tryOpen = (\n encryptor: FieldEncryptor,\n value: string,\n tenantId: string\n) => {\n try {\n return encryptor.open(value, tenantId);\n } catch {\n return null;\n }\n};\n\n/**\n * Classify one stored value against the encryptor's ring. `tenantIds` are\n * the row's likely tenants, most likely first; `fallbackTenantIds` are\n * tried only when none of those open the value with any key.\n */\nexport function classifyEncryptedValue(\n value: unknown,\n encryptor: FieldEncryptor,\n tenantIds: readonly string[],\n fallbackTenantIds: readonly string[] = []\n): RotationOutcome {\n if (!isEncryptedValue(value)) return { kind: 'plaintext' };\n const primary = tenantIds.length ? tenantIds : [''];\n const candidates = [\n ...primary,\n ...fallbackTenantIds.filter((t) => !primary.includes(t))\n ];\n for (const tenantId of candidates) {\n const opened = tryOpen(encryptor, value, tenantId);\n if (!opened) continue;\n if (!opened.stale) {\n return { kind: 'current', tenantId, keyId: opened.keyId };\n }\n const next = encryptor.encrypt(opened.plaintext, tenantId);\n return next\n ? { kind: 'rewritten', tenantId, keyId: opened.keyId, next }\n : { kind: 'unreadable' };\n }\n return { kind: 'unreadable' };\n}\n\nexport interface RotationTableReport {\n table: string;\n columns: string[];\n scanned: number;\n plaintext: number;\n current: number;\n rewritten: number;\n unreadable: number;\n /** Row ids (primary key as string) with at least one unreadable value. */\n unreadableIds: string[];\n /** How many values each previous key still held before this pass. */\n byKeyId: Record<string, number>;\n /** Stamped key ids seen on unreadable values: keys that must be added to the ring. */\n missingKeyIds: Record<string, number>;\n}\n\nexport type SqlExecute = (sql: string, params?: unknown[]) => Promise<unknown>;\n\nexport interface ReencryptOptions {\n em: EntityManager;\n /** The ring: current key plus every previous key. */\n encryptor: FieldEncryptor;\n /** Report only; write nothing. */\n dryRun?: boolean;\n /**\n * Candidate tenant ids for a row, most likely first. The default tries\n * the row's `organization_id` (when the table has one) and then the empty\n * tenant. Modules with other tenant shapes (an organization is its own\n * tenant) extend it.\n */\n tenantIdsFor?: (entityName: string, row: Record<string, unknown>) => string[];\n /**\n * Tenants to try only when a row's own candidates open nothing. Pass the\n * result of {@link collectFallbackTenantIds} to catch rows written under a\n * mismatched context. Empty by default.\n */\n fallbackTenantIds?: readonly string[];\n /**\n * How to run SQL. A migration must pass its own `this.execute`: MikroORM\n * applies pending migrations inside one transaction, and a pooled\n * connection cannot see tables that earlier migrations created but have\n * not committed yet. Defaults to the entity manager's connection.\n */\n execute?: SqlExecute;\n log?: (message: string) => void;\n}\n\n/** `organization_id` first when the row carries one, then the empty tenant. */\nexport function defaultTenantIdsFor(\n _entityName: string,\n row: Record<string, unknown>\n): string[] {\n const candidates: string[] = [];\n for (const key of ['organization_id', 'organizationId']) {\n const v = row[key];\n if (typeof v === 'string' && v) candidates.push(v);\n }\n candidates.push('');\n return [...new Set(candidates)];\n}\n\nconst quote = (id: string) => `\"${id.replace(/\"/g, '\"\"')}\"`;\n\n/**\n * Every tenant id the database knows: the distinct `organization_id` of\n * every table in the current schema that has one, plus `organization.id`\n * where the module owns organizations. Used as the last-resort tenant list.\n * PostgreSQL only (information_schema + to_regclass).\n */\nexport async function collectFallbackTenantIds(\n execute: SqlExecute\n): Promise<string[]> {\n const ids = new Set<string>();\n const tables = (await execute(\n `select table_name from information_schema.columns where table_schema = current_schema() and column_name = 'organization_id'`\n )) as { table_name: string }[];\n for (const { table_name } of tables) {\n const rows = (await execute(\n `select distinct \"organization_id\" as id from ${quote(table_name)} where \"organization_id\" is not null`\n )) as { id: unknown }[];\n for (const { id } of rows) if (typeof id === 'string' && id) ids.add(id);\n }\n const org = (await execute(`select to_regclass('\"organization\"') as t`)) as {\n t: string | null;\n }[];\n if (org[0]?.t) {\n const rows = (await execute(`select \"id\" from \"organization\"`)) as {\n id: unknown;\n }[];\n for (const { id } of rows) if (typeof id === 'string' && id) ids.add(id);\n }\n return [...ids];\n}\n\nexport interface EntityMetadataLike {\n className: string;\n tableName?: string;\n abstract?: boolean;\n embeddable?: boolean;\n primaryKeys: readonly string[];\n properties: Record<\n string,\n { fieldNames: string[]; columnTypes?: string[] } | undefined\n >;\n}\n\n/**\n * Every entity MikroORM discovered. `getMetadata().getAll()` is a Map in\n * MikroORM 7 and was a plain object before; accept both shapes.\n */\nexport function entityMetadataList(em: EntityManager): EntityMetadataLike[] {\n const all = em.getMetadata().getAll() as unknown;\n if (all instanceof Map) return [...all.values()] as EntityMetadataLike[];\n if (Array.isArray(all)) return all as EntityMetadataLike[];\n if (all && typeof all === 'object')\n return Object.values(all as Record<string, EntityMetadataLike>);\n return [];\n}\n\n/**\n * Walk every registered entity with encrypted compliance fields and rewrite\n * previous-key ciphertexts under the current key. Safe to re-run: every\n * value is classified before anything is written.\n */\nexport async function reencryptEncryptedColumns(\n options: ReencryptOptions\n): Promise<RotationTableReport[]> {\n const { em, encryptor, dryRun = false, log = () => undefined } = options;\n const tenantIdsFor = options.tenantIdsFor ?? defaultTenantIdsFor;\n const fallbackTenantIds = options.fallbackTenantIds ?? [];\n const connection = em.getConnection();\n const execute: SqlExecute =\n options.execute ??\n ((sql: string, params?: unknown[]) => connection.execute(sql, params));\n const reports: RotationTableReport[] = [];\n const ids = encryptor.keyIds;\n\n log(\n `[key-rotation] current key ${ids.current}; previous: ${ids.previous.join(', ') || 'none'}; ${fallbackTenantIds.length} fallback tenant(s)${dryRun ? '; DRY RUN' : ''}`\n );\n\n const metas = entityMetadataList(em);\n if (metas.length === 0) {\n throw new Error(\n '[key-rotation] MikroORM reported no entities; refusing to report a sweep over nothing'\n );\n }\n for (const meta of metas) {\n if (meta.abstract || meta.embeddable || !meta.tableName) continue;\n const fields = getEntityComplianceFields(meta.className);\n if (!fields) continue;\n const props = meta.properties;\n const encryptedProps = [...fields.entries()]\n .filter(([, level]) => ENCRYPTED_LEVELS.has(level))\n .map(([prop]) => props[prop])\n .filter(\n (p): p is { fieldNames: string[]; columnTypes?: string[] } =>\n !!p && Array.isArray(p.fieldNames) && p.fieldNames.length === 1\n );\n if (encryptedProps.length === 0) continue;\n\n const pkProp = props[meta.primaryKeys[0] as string];\n if (!pkProp) continue;\n const pkColumn = pkProp.fieldNames[0];\n const columns = encryptedProps.map((p) => p.fieldNames[0]);\n const report: RotationTableReport = {\n table: meta.tableName,\n columns,\n scanned: 0,\n plaintext: 0,\n current: 0,\n rewritten: 0,\n unreadable: 0,\n unreadableIds: [],\n byKeyId: {},\n missingKeyIds: {}\n };\n\n // An entity can be registered before its table exists (some auth\n // libraries create tables at runtime; a fresh harness runs this first).\n const exists = (await execute(`select to_regclass(?) as t`, [\n quote(meta.tableName)\n ])) as { t: string | null }[];\n if (!exists[0]?.t) {\n log(\n `[key-rotation] ${meta.tableName}: table does not exist yet, skipped`\n );\n continue;\n }\n\n const rows = (await execute(\n `select * from ${quote(meta.tableName)}`\n )) as Record<string, unknown>[];\n\n for (const row of rows) {\n report.scanned += 1;\n const tenantIds = tenantIdsFor(meta.className, row);\n const updates: { column: string; next: string; jsonb: boolean }[] = [];\n let rowUnreadable = false;\n for (const prop of encryptedProps) {\n const column = prop.fieldNames[0];\n const outcome = classifyEncryptedValue(\n row[column],\n encryptor,\n tenantIds,\n fallbackTenantIds\n );\n if (outcome.kind === 'plaintext') report.plaintext += 1;\n else if (outcome.kind === 'current') report.current += 1;\n else if (outcome.kind === 'rewritten') {\n report.rewritten += 1;\n report.byKeyId[outcome.keyId] =\n (report.byKeyId[outcome.keyId] ?? 0) + 1;\n updates.push({\n column,\n next: outcome.next,\n jsonb: (prop.columnTypes?.[0] ?? '')\n .toLowerCase()\n .startsWith('json')\n });\n } else {\n report.unreadable += 1;\n rowUnreadable = true;\n const missing = stampedKeyId(String(row[column]));\n if (missing)\n report.missingKeyIds[missing] =\n (report.missingKeyIds[missing] ?? 0) + 1;\n }\n }\n if (rowUnreadable) report.unreadableIds.push(String(row[pkColumn]));\n if (updates.length && !dryRun) {\n const sets = updates.map(\n (u) => `${quote(u.column)} = ${u.jsonb ? 'to_jsonb(?::text)' : '?'}`\n );\n await execute(\n `update ${quote(meta.tableName)} set ${sets.join(', ')} where ${quote(pkColumn)} = ?`,\n [...updates.map((u) => u.next), row[pkColumn]]\n );\n }\n }\n\n const byKey = Object.entries(report.byKeyId)\n .map(([k, n]) => `${k}: ${n}`)\n .join(', ');\n const missing = Object.entries(report.missingKeyIds)\n .map(([k, n]) => `${k}: ${n}`)\n .join(', ');\n log(\n `[key-rotation] ${meta.tableName}: scanned ${report.scanned} rows, ` +\n `${report.rewritten} values ${dryRun ? 'would be ' : ''}rewritten${byKey ? ` (from ${byKey})` : ''}, ` +\n `${report.current} already current, ${report.plaintext} plaintext, ` +\n `${report.unreadable} unreadable${missing ? ` (stamped with keys not in the ring: ${missing})` : ''}${report.unreadableIds.length ? ` (rows ${report.unreadableIds.slice(0, 10).join(', ')}${report.unreadableIds.length > 10 ? ', ...' : ''})` : ''}`\n );\n reports.push(report);\n }\n return reports;\n}\n\n/**\n * How many values in a column are stamped with each key (`v3`), and how\n * many are unstamped (`v1`/`v2`). Cheap, read-only, no decryption: the\n * answer to \"can this key be dropped yet?\" once a table is on `v3`.\n */\nexport async function countValuesByKeyId(\n execute: SqlExecute,\n table: string,\n column: string\n): Promise<{\n byKeyId: Record<string, number>;\n unstamped: number;\n plaintext: number;\n}> {\n const rows = (await execute(\n `select case when ${quote(column)} like 'v3:%' then split_part(${quote(column)}, ':', 2) when ${quote(column)} like 'v1:%' or ${quote(column)} like 'v2:%' then '' else null end as key_id, count(*)::int as n from ${quote(table)} group by 1`\n )) as { key_id: string | null; n: number }[];\n const byKeyId: Record<string, number> = {};\n let unstamped = 0;\n let plaintext = 0;\n for (const { key_id, n } of rows) {\n if (key_id === null) plaintext += Number(n);\n else if (key_id === '') unstamped += Number(n);\n else byKeyId[key_id] = (byKeyId[key_id] ?? 0) + Number(n);\n }\n return { byKeyId, unstamped, plaintext };\n}\n\n/** Sum a report field across tables. */\nexport function rotationTotals(reports: readonly RotationTableReport[]) {\n const sum = (pick: (r: RotationTableReport) => number) =>\n reports.reduce((n, r) => n + pick(r), 0);\n return {\n tables: reports.length,\n scanned: sum((r) => r.scanned),\n rewritten: sum((r) => r.rewritten),\n current: sum((r) => r.current),\n plaintext: sum((r) => r.plaintext),\n unreadable: sum((r) => r.unreadable)\n };\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":["p","Type","p","ENCRYPTED_LEVELS","Type","p","p","ENCRYPTED_PREFIXES","p","ENCRYPTED_LEVELS","resolveEntityName","ENCRYPTED_LEVELS","p","missing","p"]}
|