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:
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.
function self(config: SelfConfig): Provider<SelfPayload>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
SelfBackendVerifierwithendpoint = ${ctx.domain}${ctx.basePath}${route}anduserIdType = 'uuid'. - Honours
disclosures.minAge(passed asminimumAgeinto Self'sDefaultConfigStore). - Validates
disclosures.nationalityanddisclosures.issuingStateafter Self returns the disclosed country, going throughisSelfCountryAllowedso Self's internal ICAO codes (D<<etc.) match the ISO codes you configured. - The
nullifierfrom the disclose output is passed topermit()as the unique identifier — your application sees it as a stable, hasheduniqueId.
zkpassport()
ZKPassport (zkpassport.id) verifies ZK passport proofs using @zkpassport/sdk.
function zkpassport(config?: ZKPassportConfig): Provider<ZKPassportPayload>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
uniqueIdentifierreturned byzk.verifyis passed topermit()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()
function defineProvider<TPayload>(provider: Provider<TPayload>): Provider<TPayload>A validation wrapper. Pass a Provider and receive the same object back, after defineProvider has confirmed:
kindis a non-empty string.routestarts with/.
Use it whenever you implement a custom provider — the built-in self() and zkpassport() factories also go through it.
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
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 onPOST ${basePath}${route}. Validate the incoming proof, then callctx.permit(sessionId, uniqueId?)on success and return anyResponse(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
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?):
- Generates a fresh access token (UUID) and stores it under
session:<sessionId>with TTL60seconds. - If
uniqueIdis supplied, hashes it with SHA-256 and stores the digest underuid:<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.