Skip to content

Providers

Providers are the pluggable backends that turn an incoming proof into a permitted session. X511 ships two — Self and ZKPassport — and exposes defineProvider() for building your own.

Every provider factory is re-exported from each framework adapter, so the import line you actually write looks like:

ts
import { self, zkpassport, defineProvider } from 'x511/hono'
// or 'x511/elysia', 'x511/next', 'x511/adonis'

self()

Self (self.xyz) verifies passport-derived attestations using @selfxyz/core on the server. The user scans a QR with the Self mobile app, which generates and submits a proof.

ts
function self(config: SelfConfig): Provider<SelfPayload>
ts
interface SelfConfig {
  route?: string                       // default: '/verify/self'
  appName?: string                     // default: 'X511' — shown in the Self app
  scope: string                        // required — your Self app scope identifier
  disclosures?: ResolvedDisclosures    // overrides the global disclosures for this provider
}

interface SelfPayload {
  link: string                         // universal link served to the verification page
}

Behaviour

  • Builds a SelfBackendVerifier with endpoint = ${ctx.domain}${ctx.basePath}${route} and userIdType = 'uuid'.
  • Honours disclosures.minAge (passed as minimumAge into Self's DefaultConfigStore).
  • Validates disclosures.nationality and disclosures.issuingState after Self returns the disclosed country, going through isSelfCountryAllowed so Self's internal ICAO codes (D<< etc.) match the ISO codes you configured.
  • The nullifier from the disclose output is passed to permit() as the unique identifier — your application sees it as a stable, hashed uniqueId.

zkpassport()

ZKPassport (zkpassport.id) verifies ZK passport proofs using @zkpassport/sdk.

ts
function zkpassport(config?: ZKPassportConfig): Provider<ZKPassportPayload>
ts
interface ZKPassportConfig {
  route?: string                       // default: '/verify/zk'
  disclosures?: ResolvedDisclosures    // provider-level overrides
}

interface ZKPassportPayload {
  domain: string                       // forwarded to the ZKPassport client SDK
}

Behaviour

  • Constructs new ZKPassport(ctx.domain) and calls .verify({ devMode, originalQuery, proofs, queryResult }).
  • The disclosure query (gte, in, out) is built on the client by the verification page; the server simply checks the result.
  • The uniqueIdentifier returned by zk.verify is passed to permit() as the unique identifier.

The ZKPassport SDK is loaded via Node's createRequire, so this provider is Node-only. See the Next.js adapter for the bundling caveat.

defineProvider()

ts
function defineProvider<TPayload>(provider: Provider<TPayload>): Provider<TPayload>

A validation wrapper. Pass a Provider and receive the same object back, after defineProvider has confirmed:

  • kind is a non-empty string.
  • route starts with /.

Use it whenever you implement a custom provider — the built-in self() and zkpassport() factories also go through it.

ts
import { defineProvider } from 'x511/hono'

export const my = defineProvider({
  kind: 'my-provider',
  route: '/verify/my',

  async verify(req, ctx) {
    const { sessionId, proof } = await req.json()
    const ok = await myCheck(proof)
    if (!ok) {
      return new Response('nope', { status: 403 })
    }
    await ctx.permit(sessionId, /* optional uniqueId */)
    return new Response('{"ok":true}', {
      headers: { 'content-type': 'application/json' },
    })
  },

  buildPayload(sessionId, ctx) {
    return { url: `${ctx.domain}/launch?session=${sessionId}` }
  },
})

Provider

ts
interface Provider<TPayload = unknown> {
  kind: string                         // unique identifier; surfaced to the client page
  route: string                        // mounted at `${basePath}${route}` (POST)
  disclosures?: ResolvedDisclosures    // overrides global disclosures for this provider
  verify(req: Request, ctx: ProviderContext): Promise<Response>
  buildPayload(
    sessionId: string,
    ctx: ProviderContext,
  ): Promise<TPayload> | TPayload
}
  • verify(req, ctx) — runs on POST ${basePath}${route}. Validate the incoming proof, then call ctx.permit(sessionId, uniqueId?) on success and return any Response (the verification page only inspects HTTP status / response body for error display).
  • buildPayload(sessionId, ctx) — runs once per pending session, when the 511 page is rendered. Returns whatever data the page needs to drive the client side of the flow (universal link, domain, etc.).

ProviderContext

ts
interface ProviderContext {
  permit: (sessionId: string, uniqueId?: string) => Promise<void>
  disclosures: ResolvedDisclosures     // global + provider-level overrides merged
  basePath: string
  domain: string
  dev: boolean
}

Calling ctx.permit(sessionId, uniqueId?):

  1. Generates a fresh access token (UUID) and stores it under session:<sessionId> with TTL 60 seconds.
  2. If uniqueId is supplied, hashes it with SHA-256 and stores the digest under uid:<sessionId> for the same TTL.

The verification page is polling /sse for that access token, then exchanges it for the x511 cookie via /claim. Storage is delegated to your SessionAdapter.