Files
simstudioai--sim/apps/sim/lib/guardrails/pii-entities.ts
T
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

342 lines
10 KiB
TypeScript

/**
* Client-safe catalog of Microsoft Presidio PII entity types. Single source of
* truth shared by the server-only validator (`validate_pii.ts`) and client
* settings UI — keep no node-only imports here.
*/
export const SUPPORTED_PII_ENTITIES = {
// Common/Global
CREDIT_CARD: 'Credit card number',
CRYPTO: 'Cryptocurrency wallet address',
DATE_TIME: 'Date or time',
EMAIL_ADDRESS: 'Email address',
IBAN_CODE: 'International Bank Account Number',
IP_ADDRESS: 'IP address',
NRP: 'Nationality, religious or political group',
LOCATION: 'Location',
PERSON: 'Person name',
PHONE_NUMBER: 'Phone number',
MEDICAL_LICENSE: 'Medical license number',
URL: 'URL',
VIN: 'Vehicle Identification Number',
// USA
US_BANK_NUMBER: 'US bank account number',
US_DRIVER_LICENSE: 'US driver license',
US_ITIN: 'US Individual Taxpayer Identification Number',
US_PASSPORT: 'US passport number',
US_SSN: 'US Social Security Number',
// UK
UK_NHS: 'UK NHS number',
UK_NINO: 'UK National Insurance Number',
// Other countries
ES_NIF: 'Spanish NIF number',
ES_NIE: 'Spanish NIE number',
IT_FISCAL_CODE: 'Italian fiscal code',
IT_DRIVER_LICENSE: 'Italian driver license',
IT_VAT_CODE: 'Italian VAT code',
IT_PASSPORT: 'Italian passport',
IT_IDENTITY_CARD: 'Italian identity card',
PL_PESEL: 'Polish PESEL number',
SG_NRIC_FIN: 'Singapore NRIC/FIN',
SG_UEN: 'Singapore Unique Entity Number',
AU_ABN: 'Australian Business Number',
AU_ACN: 'Australian Company Number',
AU_TFN: 'Australian Tax File Number',
AU_MEDICARE: 'Australian Medicare number',
IN_PAN: 'Indian Permanent Account Number',
IN_AADHAAR: 'Indian Aadhaar number',
IN_VEHICLE_REGISTRATION: 'Indian vehicle registration',
IN_VOTER: 'Indian voter ID',
IN_PASSPORT: 'Indian passport',
FI_PERSONAL_IDENTITY_CODE: 'Finnish Personal Identity Code',
} as const
export type PIIEntityType = keyof typeof SUPPORTED_PII_ENTITIES
/** Flat `{ value, label }` options for entity-type pickers, in catalog order. */
export const PII_ENTITY_OPTIONS: ReadonlyArray<{ value: PIIEntityType; label: string }> =
Object.entries(SUPPORTED_PII_ENTITIES).map(([value, label]) => ({
value: value as PIIEntityType,
label,
}))
/** Entity types grouped by region, for a grouped checkbox picker. */
export const PII_ENTITY_GROUPS: ReadonlyArray<{
label: string
entities: ReadonlyArray<{ value: PIIEntityType; label: string }>
}> = [
{
label: 'Common',
entities: [
'PERSON',
'EMAIL_ADDRESS',
'PHONE_NUMBER',
'CREDIT_CARD',
'IP_ADDRESS',
'LOCATION',
'DATE_TIME',
'URL',
'IBAN_CODE',
'CRYPTO',
'NRP',
'MEDICAL_LICENSE',
'VIN',
],
},
{
label: 'United States',
entities: ['US_SSN', 'US_PASSPORT', 'US_DRIVER_LICENSE', 'US_BANK_NUMBER', 'US_ITIN'],
},
{ label: 'United Kingdom', entities: ['UK_NHS', 'UK_NINO'] },
{
label: 'Other regions',
entities: [
'ES_NIF',
'ES_NIE',
'IT_FISCAL_CODE',
'IT_DRIVER_LICENSE',
'IT_VAT_CODE',
'IT_PASSPORT',
'IT_IDENTITY_CARD',
'PL_PESEL',
'SG_NRIC_FIN',
'SG_UEN',
'AU_ABN',
'AU_ACN',
'AU_TFN',
'AU_MEDICARE',
'IN_PAN',
'IN_AADHAAR',
'IN_VEHICLE_REGISTRATION',
'IN_VOTER',
'IN_PASSPORT',
'FI_PERSONAL_IDENTITY_CODE',
],
},
].map((group) => ({
label: group.label,
entities: group.entities.map((value) => ({
value: value as PIIEntityType,
label: SUPPORTED_PII_ENTITIES[value as PIIEntityType],
})),
}))
/**
* Languages the Presidio image has NLP models for. The analyzer only recognizes a
* language's entities when its model is loaded, so this set must match the image.
*/
export const PII_LANGUAGES = [
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Spanish' },
{ value: 'it', label: 'Italian' },
{ value: 'pl', label: 'Polish' },
{ value: 'fi', label: 'Finnish' },
] as const
export type PIILanguage = (typeof PII_LANGUAGES)[number]['value']
/** Non-empty tuple of language codes for schema/enum use. */
export const PII_LANGUAGE_CODES = PII_LANGUAGES.map((l) => l.value) as [
PIILanguage,
...PIILanguage[],
]
/** Default redaction language when a rule doesn't set one. */
export const DEFAULT_PII_LANGUAGE: PIILanguage = 'en'
/**
* Narrow a loosely-typed (stored/legacy) language to a supported code. Unknown or
* stale values (e.g. a dropped locale) return `undefined` so callers fall back to
* the default rather than forwarding an unsupported language to Presidio.
*/
export function coercePiiLanguage(value: string | undefined): PIILanguage | undefined {
return value && (PII_LANGUAGE_CODES as readonly string[]).includes(value)
? (value as PIILanguage)
: undefined
}
/**
* Entity types every served language recognizes: Presidio's global pattern
* recognizers, the spaCy NER entities (PERSON/LOCATION/NRP), and the native VIN
* recognizer (registered under every language in `apps/pii/server.py`).
*/
const GLOBAL_PII_ENTITIES: readonly PIIEntityType[] = [
'PERSON',
'LOCATION',
'NRP',
'CREDIT_CARD',
'CRYPTO',
'DATE_TIME',
'EMAIL_ADDRESS',
'IBAN_CODE',
'IP_ADDRESS',
'PHONE_NUMBER',
'URL',
'MEDICAL_LICENSE',
'VIN',
]
/**
* Entity types each language recognizes, mirroring the recognizer registration in
* `apps/pii/server.py`: globals + NER + VIN everywhere, plus the locale-specific
* id recognizers under the language they're registered for (US/UK/AU/IN/SG ids
* are English; es/it/pl/fi carry only their own national ids). Keep in sync with
* the image — a stale entry only no-ops (redaction fails safe), it never leaks.
* `/supportedentities` is the authoritative source if this ever needs to go live.
*/
export const PII_ENTITIES_BY_LANGUAGE: Record<PIILanguage, ReadonlySet<PIIEntityType>> = {
en: new Set<PIIEntityType>([
...GLOBAL_PII_ENTITIES,
'US_SSN',
'US_PASSPORT',
'US_DRIVER_LICENSE',
'US_BANK_NUMBER',
'US_ITIN',
'UK_NHS',
'UK_NINO',
'AU_ABN',
'AU_ACN',
'AU_TFN',
'AU_MEDICARE',
'IN_PAN',
'IN_AADHAAR',
'IN_VEHICLE_REGISTRATION',
'IN_VOTER',
'IN_PASSPORT',
'SG_NRIC_FIN',
'SG_UEN',
]),
es: new Set<PIIEntityType>([...GLOBAL_PII_ENTITIES, 'ES_NIF', 'ES_NIE']),
it: new Set<PIIEntityType>([
...GLOBAL_PII_ENTITIES,
'IT_FISCAL_CODE',
'IT_DRIVER_LICENSE',
'IT_VAT_CODE',
'IT_PASSPORT',
'IT_IDENTITY_CARD',
]),
pl: new Set<PIIEntityType>([...GLOBAL_PII_ENTITIES, 'PL_PESEL']),
fi: new Set<PIIEntityType>([...GLOBAL_PII_ENTITIES, 'FI_PERSONAL_IDENTITY_CODE']),
}
/** True when the entity has a recognizer for the given language. */
export function isEntitySupportedForLanguage(
entity: PIIEntityType,
language: PIILanguage
): boolean {
return PII_ENTITIES_BY_LANGUAGE[language].has(entity)
}
/** {@link PII_ENTITY_GROUPS} filtered to entities the language recognizes (empty groups dropped). */
export function getEntityGroupsForLanguage(language: PIILanguage) {
return PII_ENTITY_GROUPS.map((group) => ({
label: group.label,
entities: group.entities.filter((e) => isEntitySupportedForLanguage(e.value, language)),
})).filter((group) => group.entities.length > 0)
}
/** The PII redaction stages, in execution order. */
export const PII_STAGES = ['input', 'blockOutputs', 'logs'] as const
export type PiiStageKey = (typeof PII_STAGES)[number]
/** Per-stage redaction policy. `enabled: false` makes the stage a no-op. */
export interface PiiStagePolicy {
enabled: boolean
entityTypes: string[]
language: PIILanguage
}
export type PiiStages = Record<PiiStageKey, PiiStagePolicy>
/**
* Stage catalog driving the settings UI, in display order (Logs first — the
* safe, observability-only default). The execution-altering caveat for the
* input/blockOutputs stages is folded into their descriptions.
*/
export const PII_STAGE_META: ReadonlyArray<{
key: PiiStageKey
label: string
description: string
}> = [
{
key: 'logs',
label: 'Logs',
description: 'Redact workflow logs when they are persisted.',
},
{
key: 'input',
label: 'Workflow input',
description:
'Redact the workflow input before execution. Data is redacted during runtime and may affect workflow output.',
},
{
key: 'blockOutputs',
label: 'Block outputs',
description:
'Mask every block output before the next block reads it. Data is redacted during runtime and may affect workflow output and execution performance.',
},
]
/** Recognizers that over-redact (loose, no checksum); surfaced as UI guidance. */
export const RISKY_PII_ENTITIES: ReadonlySet<PIIEntityType> = new Set<PIIEntityType>([
'US_SSN',
'US_BANK_NUMBER',
'DATE_TIME',
])
/** A fully-disabled stage policy for new drafts. */
export function emptyStagePolicy(): PiiStagePolicy {
return { enabled: false, entityTypes: [], language: DEFAULT_PII_LANGUAGE }
}
/** A fully-disabled stage set for new drafts. */
export function emptyPiiStages(): PiiStages {
return {
input: emptyStagePolicy(),
blockOutputs: emptyStagePolicy(),
logs: emptyStagePolicy(),
}
}
/**
* Hydrate a stored rule into the per-stage shape. A legacy flat rule (no
* `stages`) becomes `logs` enabled with its entity types, the two new stages
* disabled — exactly its pre-stages behavior.
*/
export function normalizeRuleStages(rule: {
stages?: Partial<Record<PiiStageKey, Partial<PiiStagePolicy> | undefined>>
entityTypes?: string[]
language?: string
}): PiiStages {
const sanitize = (policy: Partial<PiiStagePolicy> | undefined): PiiStagePolicy => ({
enabled: Boolean(policy?.enabled),
entityTypes: Array.isArray(policy?.entityTypes)
? policy.entityTypes.filter((t): t is string => typeof t === 'string')
: [],
language: coercePiiLanguage(policy?.language) ?? DEFAULT_PII_LANGUAGE,
})
if (rule.stages) {
return {
input: sanitize(rule.stages.input),
blockOutputs: sanitize(rule.stages.blockOutputs),
logs: sanitize(rule.stages.logs),
}
}
const entityTypes = Array.isArray(rule.entityTypes)
? rule.entityTypes.filter((t): t is string => typeof t === 'string')
: []
return {
input: emptyStagePolicy(),
blockOutputs: emptyStagePolicy(),
logs: {
enabled: entityTypes.length > 0,
entityTypes,
language: coercePiiLanguage(rule.language) ?? DEFAULT_PII_LANGUAGE,
},
}
}