Skip to content

feat(idos): Gov ID branch — sessions wiring, consumer SDK, kyc-token, credentials endpoint#34

Open
calebtuttle wants to merge 6 commits intodevfrom
feat/idos-impl
Open

feat(idos): Gov ID branch — sessions wiring, consumer SDK, kyc-token, credentials endpoint#34
calebtuttle wants to merge 6 commits intodevfrom
feat/idos-impl

Conversation

@calebtuttle
Copy link
Copy Markdown
Contributor

Adds the id-server side of the idOS Gov ID flow. Pairs with frontend PR holonym-foundation/human-id#196.

What lands

  • U1 — sessions (ea79634)

    • Add "idos" to the four idvProvider validation lists in src/services/sessions/endpoints.ts (POST /sessions/v3 paths and setIdvProvider).
    • Add an idos branch to handleIdvSessionCreation that persists the session row with no external SDK call. idOS has no applicant / token / webhook lifecycle.
  • U2 — consumer SDK wrapper (56c7f1e)

    • src/services/idos/consumer.ts: lazily-initialized idOSConsumer with a narrow public surface (listGrants, decryptCredential, getSharedCredential).
    • Reads IDOS_SIGNER (128-char hex nacl secret key) and IDOS_RECIPIENT_ENCRYPTION_PRIVATE_KEY on first use; throws clear errors on missing/malformed env vars. Optional IDOS_NODE_URL and IDOS_CHAIN_ID overrides.
    • package.json: add @idos-network/consumer ^1.1.0 and tweetnacl ^1.0.3.
    • Bun-test coverage of env validation, init memoization, and node/chain forwarding.
  • U9 — POST /idos/kyc-token (6c3f5dc)

    • ES512-signed Fractal KYC JWT for the verify page to embed in the Kraken iframe URL.
    • Body validation (0x-prefixed 20-byte walletAddress, bounded externalUserId), per-env rate limit, sandbox env-var fallback (IDOS_JWT_PRIVATE_KEY_SANDBOX, IDOS_FRACTAL_CLIENT_ID_SANDBOX).
    • No expiresIn per the parent plan U9 (Fractal expects tokens without expiration). Single seam if that ever changes.
    • Mounted at /idos/kyc-token and /sandbox/idos/kyc-token.
  • U3 + U4 — GET /idos/credentials/v3/:_id/:nullifier (4686c2f)

    • services/idos/credentials/utils.ts: extractCreds(vc) maps a decrypted VerifiableCredential<VerifiableCredentialSubject> into the Onfido-compatible { rawCreds, derivedCreds, fieldsInLeaf } shape. Hash composition is byte-identical to Onfido's extractCreds for equivalent inputs — enforced by parity tests against the Onfido extractor.
    • services/idos/credentials/v3.ts: mirrors services/onfido/credentials/v3.ts structurally — nullifier validation, ObjectId parse, session lookup, VERIFICATION_FAILED guard, 5-day nullifier replay path, sybil-resistance UUID + UserVerifications insert, issuev2KYC, ISSUED status. New: reads dataId + userAddress from the query string, filters the consumer's grant list by data_id, and trust-checks the on-chain credential's issuer_auth_public_key against the configured allow-list before any field hashing.
    • services/idos/issuers.ts: env-driven trust list (IDOS_ALLOWED_ISSUERS_JSON or IDOS_ALLOWED_ISSUER + IDOS_ALLOWED_ISSUER_PUBLIC_KEY).
    • schemas.ts + types.ts: extend NullifierAndCreds.idvSessionIds with idos.dataId (sandbox mirrored).

Plan

Implements U1, U2, U3, U4, U9 from the parent plan in the human-id repo: docs/plans/2026-04-30-003-feat-idos-as-onfido-alternative-in-gov-id-flow-plan.md (also visible on the linked frontend PR).

Env vars introduced

Var Purpose
IDOS_SIGNER hex-encoded nacl.sign secret key (128 hex chars) for the consumer's grant signer
IDOS_RECIPIENT_ENCRYPTION_PRIVATE_KEY consumer-side encryption private key
IDOS_NODE_URL (optional) Kwil node URL override
IDOS_CHAIN_ID (optional) Kwil chain id override
IDOS_JWT_PRIVATE_KEY PEM-encoded ES512 private key Fractal trusts
IDOS_FRACTAL_CLIENT_ID Fractal-issued client id (UUID)
IDOS_JWT_PRIVATE_KEY_SANDBOX (optional) sandbox override; falls back to prod
IDOS_FRACTAL_CLIENT_ID_SANDBOX (optional) sandbox override; falls back to prod
IDOS_ALLOWED_ISSUERS_JSON or IDOS_ALLOWED_ISSUER + IDOS_ALLOWED_ISSUER_PUBLIC_KEY issuer trust list

.env.example was not updated in this branch (permission-blocked when authored). Copy these into the deploy env-var contract before Slice U / staging smoke.

Test plan

  • bun install completes (adds @idos-network/consumer + tweetnacl)
  • bun test src/services/idos/consumer.test.ts — env validation + init memoization
  • bun test src/services/idos/kyc-token.test.ts — ES512 payload shape, sandbox env fallback, validation 400s
  • bun test src/services/idos/credentials/utils.test.ts — Onfido parity for nameHash / streetHash / addressHash / nameDobCitySubdivisionZipStreetExpireHash
  • bun --watch src/server.ts boots without idOS env vars set (lazy-init contract)
  • Hit POST /idos/kyc-token with a valid walletAddress and verify the returned JWT decodes to the expected payload (against IDOS_FRACTAL_CLIENT_ID + ES512 alg)
  • End-to-end smoke: frontend verify page (PR #196) → grant → GET /idos/credentials/v3/:sid/:nullifier returns issuance response equivalent to the Onfido path for the same logical input

Notes

  • This branch was force-recovered from a deleted worktree's submodule clone. The sequence of commits is identical in scope to the original (lost) work; only the SHAs are new.

🤖 Generated with Claude Code

…side init

- Add 'idos' to the four idvProvider validation lists in endpoints.ts (POST /sessions/v3 paths and the setIdvProvider recovery handler).
- Add an 'idos' branch to handleIdvSessionCreation that persists the session row with no external SDK call. idOS has no applicant/token/webhook lifecycle; the user grants access to a pre-existing credential later, fetched in GET /idos/credentials/v3/:_id/:nullifier.

Refs docs/plans/2026-04-30-003-feat-idos-as-onfido-alternative-in-gov-id-flow-plan.md U1.
- src/services/idos/consumer.ts: module-scoped, lazily-initialized idOSConsumer
  with a narrow public surface (listGrants, decryptCredential,
  getSharedCredential). Reads IDOS_SIGNER (128-char hex nacl secret key) and
  IDOS_RECIPIENT_ENCRYPTION_PRIVATE_KEY on first use; throws with clear
  messages on missing/malformed env vars. Optional IDOS_NODE_URL and
  IDOS_CHAIN_ID overrides are forwarded when set.
- src/services/idos/consumer.test.ts: covers env validation, init memoization,
  and node/chain override forwarding via bun:test mock.module on the SDK.
- package.json: add @idos-network/consumer ^1.1.0 and tweetnacl ^1.0.3.

The wrapper exists so the credentials endpoint (Slice 3 / parent plan U4) and
any future idOS-side reads call into one well-defined seam — also the seam
unit tests mock.

Refs docs/plans/2026-04-30-003-feat-idos-as-onfido-alternative-in-gov-id-flow-plan.md U2.
Adds the endpoint the verify page (Slice 4) calls to mint the JWT it embeds
in https://verify.fractal.id/kyc?token=... — the signing key stays on the
server, and per-user walletAddress / externalUserId fields are filled in
the payload.

- src/services/idos/kyc-token.ts: signKycToken(env, opts) helper plus
  issueKycTokenProd / issueKycTokenSandbox HTTP handlers. Validates body
  (0x-prefixed 20-byte walletAddress, bounded externalUserId), rate-limits
  the endpoint key separately for prod and sandbox, and surfaces a clear
  500 'not configured' message when IDOS_JWT_PRIVATE_KEY or
  IDOS_FRACTAL_CLIENT_ID is missing or malformed (no leak of the env-var
  validation messages otherwise). Sandbox env vars
  (IDOS_JWT_PRIVATE_KEY_SANDBOX, IDOS_FRACTAL_CLIENT_ID_SANDBOX) are read
  with a fallback to the prod values.
- src/routes/idos.ts: new prod + sandbox routers. Slice 3 (parent plan U4)
  will extend these with the credentials endpoint.
- src/index.ts: mount /idos and /sandbox/idos.
- src/services/idos/kyc-token.test.ts: covers payload shape, ES512 alg,
  optional-field passthrough, sandbox env-var fallback, env-var validation
  errors, and HTTP handler 400/500 paths. Generates a P-521 key per run
  via crypto.generateKeyPairSync so no fixture key is checked in.

No expiresIn on the JWT — per the parent plan U9, Fractal expects tokens
without expiration. If short-lived JWTs are required later, signKycToken
is the single seam to add expiresIn.

Refs docs/plans/2026-04-30-003-feat-idos-as-onfido-alternative-in-gov-id-flow-plan.md U9.
Combines U3 (extractCreds parity) + U4 (HTTP endpoint) for the idOS branch
of the Gov ID flow.

U3 — services/idos/credentials/utils.ts
- extractCreds(vc) maps a decrypted VerifiableCredential<Subject> into the
  Onfido-compatible { rawCreds, derivedCreds, fieldsInLeaf } shape.
- Hash composition order is byte-identical to Onfido's extractCreds for the
  same logical inputs (nameHash, streetHash, addressHash, and the composite
  nameDobCitySubdivisionZipStreetExpireHash). The parity is enforced by
  utils.test.ts which runs both extractors over shared fixtures.
- ISO 8601 birthdates are normalized to YYYY-MM-DD before getDateAsInt
  (Onfido's helper splits on '-' and chokes on full timestamps).
- expirationDate stays empty by design — Onfido's extractor currently zeros
  it; mirroring keeps the composite hash compatible. Both must change in
  lockstep if expiry is ever folded into the leaf.
- nationality fall-back when idDocumentCountry is absent.

U4 — services/idos/credentials/v3.ts
- Endpoint mirrors services/onfido/credentials/v3.ts structure: nullifier
  format check, ObjectId parse, session lookup, VERIFICATION_FAILED guard,
  5-day nullifier replay path, sybil-resistance UUID + UserVerifications
  insert, issuev2KYC issuance, ISSUED status.
- New: reads dataId + userAddress from the query string (frontend supplies
  them after a successful idOS access grant). dataId is filtered out of the
  consumer's grant list via paginated lookup.
- Trust check pulls the on-chain credential record (idOSCredential
  .issuer_auth_public_key) and rejects with 400 if it isn't in the
  configured allow-list — done before extractCreds so we never hash fields
  from an untrusted credential.
- Returns 404 (not VERIFICATION_FAILED) when no grant exists yet, so the
  frontend can retry without dirtying the session.
- Sandbox + prod handlers via the same factory.

Supporting additions
- services/idos/issuers.ts: env-driven CustomIssuerType allow-list with
  IDOS_ALLOWED_ISSUERS_JSON or IDOS_ALLOWED_ISSUER + IDOS_ALLOWED_ISSUER_PUBLIC_KEY.
  Cached + lazily validated; throws clear errors on misconfig.
- schemas.ts + types.ts: extend NullifierAndCreds idvSessionIds with
  idos.dataId so the 5-day replay lookup has a key. Mirrored on
  SandboxNullifierAndCredsSchema.
- routes/idos.ts: register the new GET handler on prod + sandbox routers.

Refs docs/plans/2026-04-30-003-feat-idos-as-onfido-alternative-in-gov-id-flow-plan.md U3 + U4.
Fractal rejects 'basic+uniqueness+idos' with 'Can't create KYC session
with both KYC and Uniqueness' — kyc:true already implies KYC, and the
+uniqueness component requires a separate session. Switch level to
'basic+idos' (KYC + idOS profile creation).

Uniqueness is enforced downstream by the sybil-resistance UUID +
UserVerifications insert in services/idos/credentials/v3.ts, so this
doesn't lower the bar — it just removes a session-type collision in
Fractal's API.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant