feat(aml): iDenfy as an Onfido-alternative branch for Clean Hands#49
Merged
Conversation
…hape iDenfy's /api/v2/data field placement (flat vs nested under `data`) is unconfirmed against a real sandbox response. extractCreds read fields flat while its own type doc described them nested; if the real shape is nested, every PII field came back empty and the falsy-country guard did not catch it, allowing empty-PII credential issuance. Read defensively from the nested object when present, else flat. Applies to the UUID helpers too. Part of the iDenfy Clean Hands branch (U10).
Add 'idenfy' to the AML idvProvider enum (live + sandbox schemas), the VALID_IDV_PROVIDERS list, and the IAmlChecksSession type union. Add an idenfySessionId field to the AML schemas (parallel to onfidoSessionId); the type interface already declared it. resolveIdvProvider now accepts it with no further change. iDenfy Clean Hands branch (U1).
When an AML session's idvProvider is 'idenfy', create or reuse a standalone IdenfySession (mirroring the Onfido branch), persist session.idenfySessionId, and return idenfySessionId in the pay response. Pricing reuses the $5 PAYMENT_SERVICE_CLEAN_HANDS_VERIFICATION (zk-passport stays $3). Reuse is not scoped by flow by design; the issuance handler (U4) always re-runs sanctions. iDenfy Clean Hands branch (U2).
Canonical sanctions.io + PEP screening extracted faithfully from issueCredsV4's inline block: identical query params, PEP-block-by-country logic, SanctionsResult persistence, whitelist handling, and statement generation. Returns a decision (clear | blocked | declaration-required) instead of writing the session/HTTP response, so the caller owns lifecycle. Queries name + DOB only (country is not a query input). Production Onfido/ZK paths are intentionally NOT migrated here (deferred refactor). Used by the iDenfy issuance handler (U4). iDenfy Clean Hands branch (U3).
Add GET /aml-sessions/:_id/credentials/idenfy/v4/:nullifier (+ sandbox), server-polled like the Onfido credentials endpoint. Guards: idvProvider==idenfy, paid (IN_PROGRESS) state, and idempotent recovery (a prior issuance for the same nullifier re-issues instead of double-issuing, so repeated polls are safe). Re-confirms the iDenfy decision live via fetchIdenfyStatus (not the webhook- cached status), pulls name+DOB via extractIdenfyNameDob, rejects empty PII before screening, runs the shared runSanctionsScreening, and issues Clean Hands creds in the same shape as the Onfido/ZK branches. No aggressive rate limit (poll endpoint). iDenfy Clean Hands branch (U4).
/session-status/v2 only looked in the gov-id SessionModel, so the shared /idenfy/verify page polling with a Clean Hands (AML) session id got 404 'Session not found'. Fall back to AMLChecksSessionModel when the gov-id lookup misses; the existing provider=idenfy branch then resolves the AML session's idenfySessionId the same way it does for gov-id. iDenfy Clean Hands branch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds iDenfy as a third identity-verification provider for the Clean Hands (AML) flow, as a drop-in replacement for the Onfido gov-id-scan branch. ZK Passport (NFC) is unchanged. Paired with the frontend PR in holonym-foundation/human-id (feat/idenfy-clean-hands).
What changed
idenfyis a valid AML provider: added to the AMLidvProviderenum (live + sandbox schemas),VALID_IDV_PROVIDERS, and theIAmlChecksSessiontype; newidenfySessionIdfield.resolveIdvProvideraccepts it with no change.payForSessionV4: creates/reuses anIdenfySessionwhenidvProvider==='idenfy'(mirrors the Onfido branch), returnsidenfySessionId, prices at $5 (reusesPAYMENT_SERVICE_CLEAN_HANDS_VERIFICATION; zk-passport stays $3). Reuse is intentionally not flow-scoped — matches Onfido; issuance always re-screens.runSanctionsScreeningmodule (services/aml-sessions/sanctions-screening.ts): faithful extraction of the inline sanctions.io + PEP block fromissueCredsV4— identical query params, PEP-block-by-country,SanctionsResultpersistence, whitelist handling, and statement generation. Returns a decision; caller owns session/HTTP. Queries name + DOB only.GET /aml-sessions/:_id/credentials/idenfy/v4/:nullifier(+ sandbox): server-polls iDenfy (like the Onfido credentials endpoint). Guards:idvProvider==='idenfy', paid (IN_PROGRESS) state, idempotent recovery (a prior issuance for the same nullifier re-issues instead of double-issuing). Re-confirms the decision live viafetchIdenfyStatus(not the webhook-cached status), pulls name+DOB viaextractIdenfyNameDob, rejects empty PII before screening, runsrunSanctionsScreening, issues Clean Hands creds in the same shape as Onfido/ZK.extractCredshardening: now tolerates iDenfy/api/v2/datafields whether flat or nested underdata(the real shape is unconfirmed —TODO(U11)); prevents silent empty-PII issuance. Also fixes the gov-id iDenfy flow.Production paths untouched
The Onfido (
issueCredsV4) and ZK Passport (verifyAndIssueZkPassport) sanctions blocks are not migrated onto the new module in this PR (deferred refactor) — zero behavioral diff to production AML issuance.How to verify
bun test src/services/idenfy/ src/services/aml-sessions/→ green (incl. newsanctions-screening.test.ts,extractCredsdual-shape +extractIdenfyNameDobtests)bun check:types→ clean?preferredProvider=idenfyuntil a one-line flip.Not in scope / follow-ups
/api/v2/datasandbox response and byte-compare againstextractCreds(resolves theTODO(U11)markers).runSanctionsScreening(separate refactor PR with characterization tests).🤖 Generated with Claude Code