From 31f90bf9143ae2b5598b1993b90954b1dddb29c3 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 9 Sep 2026 22:02:15 +0700 Subject: [PATCH 1/3] docs: add eSignet and OpenFn integration pages Adopters asked how Registry Stack fits beside an identity front door and a workflow engine. Two explanation pages answer that from the Registry Stack side: the eSignet provider that authenticates citizens through one governed Base Registry Engine lookup, and the OpenFn adaptors that wrap the Evidence Gateway, Base Registry Engine, and Registry Relay clients for Lightning workflows. Both are seated under "Use from applications" and cross-linked from the integration patterns page and the glossary. Signed-off-by: Jeremi Joslin --- docs/site/astro.config.mjs | 4 + .../scripts/information-architecture.test.mjs | 3 + .../esignet-authentication-over-breg.mdx | 280 ++++++++++++++++++ .../docs/explanation/integration-patterns.mdx | 4 +- .../docs/explanation/openfn-adaptors.mdx | 234 +++++++++++++++ .../src/content/docs/reference/glossary.mdx | 2 +- 6 files changed, 525 insertions(+), 2 deletions(-) create mode 100644 docs/site/src/content/docs/explanation/esignet-authentication-over-breg.mdx create mode 100644 docs/site/src/content/docs/explanation/openfn-adaptors.mdx diff --git a/docs/site/astro.config.mjs b/docs/site/astro.config.mjs index 09b27afc4a..96dc950172 100644 --- a/docs/site/astro.config.mjs +++ b/docs/site/astro.config.mjs @@ -325,6 +325,7 @@ export default defineConfig({ { label: 'Request an assertion', slug: 'tutorials/request-evidence-from-an-application' }, { label: 'Verify and retain assertions', slug: 'tutorials/verify-an-assertion-as-a-consumer' }, { label: 'Manage verifier trust', slug: 'tutorials/manage-evidence-verifier-trust' }, + { label: 'Automate with OpenFn', slug: 'explanation/openfn-adaptors' }, ], }, { @@ -409,6 +410,7 @@ export default defineConfig({ items: [ { label: 'Query with Python', slug: 'tutorials/query-relay-client' }, { label: 'Client API reference', slug: 'reference/client-api' }, + { label: 'Automate with OpenFn', slug: 'explanation/openfn-adaptors' }, ], }, { label: 'Run a deployment', slug: 'operate/relay' }, @@ -474,6 +476,8 @@ export default defineConfig({ items: [ { label: 'Query with Python and Node', slug: 'tutorials/query-breg-client' }, { label: 'Client API reference', slug: 'reference/client-api' }, + { label: 'Authenticate with eSignet', slug: 'explanation/esignet-authentication-over-breg' }, + { label: 'Automate with OpenFn', slug: 'explanation/openfn-adaptors' }, ], }, { label: 'Configuration reference', slug: 'reference/breg-configuration' }, diff --git a/docs/site/scripts/information-architecture.test.mjs b/docs/site/scripts/information-architecture.test.mjs index d888399d2a..232bbb04b8 100644 --- a/docs/site/scripts/information-architecture.test.mjs +++ b/docs/site/scripts/information-architecture.test.mjs @@ -217,6 +217,7 @@ test('keeps consumer and wallet-provider guidance in separate Evidence groups', 'tutorials/request-evidence-from-an-application', 'tutorials/verify-an-assertion-as-a-consumer', 'tutorials/manage-evidence-verifier-trust', + 'explanation/openfn-adaptors', ]); assert.deepEqual(wallet.items.map((item) => item.slug), [ 'configure/enable-sd-jwt-vc', @@ -411,6 +412,8 @@ test('keeps the BReg guide and references in one adoption path', () => { 'operate/breg-data', 'tutorials/query-breg-client', 'reference/client-api', + 'explanation/esignet-authentication-over-breg', + 'explanation/openfn-adaptors', 'reference/breg-configuration', 'reference/breg-api', 'reference/bregctl-publicschema-wizard', diff --git a/docs/site/src/content/docs/explanation/esignet-authentication-over-breg.mdx b/docs/site/src/content/docs/explanation/esignet-authentication-over-breg.mdx new file mode 100644 index 0000000000..67dc993ca1 --- /dev/null +++ b/docs/site/src/content/docs/explanation/esignet-authentication-over-breg.mdx @@ -0,0 +1,280 @@ +--- +title: Authenticate citizens with eSignet over Base Registry Engine (BReg) +description: How the eSignet Base Registry Engine provider turns a governed population registry lookup into a verified OpenID Connect login, what it discloses, and where its boundaries lie. +status: draft +owner: registry-docs +source_repos: [registry-stack, esignet-relay-authenticator] +last_reviewed: "2026-09-09" +doc_type: explanation +locale: en +standards_referenced: [govstack-digital-registries, universal-dpi-safeguards] +--- + +You run a population or civil registry on Base Registry Engine (BReg) and you want citizens to sign +in to public services with [eSignet](https://docs.esignet.io/), the open-source identity and +authentication service from the MOSIP project. The eSignet BReg provider is the piece that connects +the two. It lets eSignet resolve a verified identifier against your registry through one governed +lookup, and release only the demographic claims each citizen approves. This page explains what the +provider does, what your registry must grant it, and what it deliberately leaves out. It is the +adopter-facing account of version 0.3.0 of +[registrystack/esignet-relay-authenticator](https://github.com/registrystack/esignet-relay-authenticator). + +{/* TODO[evidence]: provider behaviour (challenge verifier interface, PSUT derivation, claim +mapping, error codes, build outputs) lives in the separate registrystack/esignet-relay-authenticator +repository and cannot be anchored from this one. Re-verify the statements below against that +repository's README, docs/esignet-configuration.md, and docs/breg-lookup-contract.md when it +changes. */} + +## Who owns what + +eSignet owns the login experience and the OpenID Connect (OIDC) surface: the relying-party +registration, the authorization and token endpoints, the consent screen, and the signing and optional +encryption of ID Tokens and UserInfo responses. Relying parties talk to eSignet only. They never see +BReg. + +BReg owns the registry: the population records, the access profiles that decide which client may +read which fields, the revision history, and the audit journal. It knows nothing about eSignet as a +product. From its side, the provider is one more client with one narrow grant. + +Registry Mint issues the short-lived access token the provider presents to BReg. The provider +authenticates to Mint as a registered `private_key_jwt` client and never holds a long-lived registry +credential. + +The provider itself is a Go module compiled into the eSignet host. eSignet selects it by setting the +authentication provider to `breg` and pointing it at one YAML configuration file. The provider +contributes three things: it verifies the citizen's challenge, it performs the governed lookup, and it +derives the claims and the pairwise subject that eSignet then signs. Everything else stays with +eSignet. + +{/* Evidence: crates/registry-mint/src/lib.rs, crates/registry-mint/src/assertion.rs, and +crates/registry-mint/src/clients.rs implement RFC 7523 private_key_jwt client authentication for +the client_credentials grant that the provider uses to obtain its BReg access token. */} + +## The login flow + +A login takes two round trips into your registry at most, and none until the citizen has proved +possession of their identifier. + +```mermaid +sequenceDiagram + participant C as Citizen + participant E as eSignet + participant P as BReg provider + participant M as Registry Mint + participant B as BReg + + C->>E: Enter identifier, receive challenge + C->>E: Submit challenge response + E->>P: Verify challenge + P->>M: private_key_jwt client_credentials + M-->>P: Short-lived access token + P->>B: Lookup account-check fields only + B-->>P: uin, status + E->>C: Consent screen (requested claims) + C->>E: Approve a subset + E->>P: Fetch approved claims + P->>B: Lookup approved fields only + B-->>P: Selected domain data + P-->>E: Claims plus pairwise subject + E-->>C: Signed ID Token and UserInfo +``` + +The order matters for data protection: + +1. **Challenge first.** The provider verifies the citizen's one-time code or other challenge before + any registry call. A wrong code never reaches BReg or Mint, so the registry is not an oracle for + guessing identifiers. +2. **Account check with the smallest projection.** After a correct challenge, the provider looks up + the identifier and asks BReg for only the fields named in `account_check_fields`, typically the + identifier and a status field. Which records resolve at all is the access profile's decision: a + profile scoped to active records makes inactive ones unresolvable, and the provider reports that + as a denied subject. +3. **Fresh consent every time.** eSignet shows the consent screen on every login. The provider + installs no remembered consent, so a citizen who approved their birthdate last month is asked + again today. +4. **Second lookup limited to the approved intersection.** The provider computes the intersection of + the claims the citizen approved, the claims named in `claim_map`, and the fields the access profile + provisions. It asks BReg for exactly that set. If the intersection is empty, it makes no call at + all. +5. **Only domain data becomes claims.** The provider reads the record's domain data and ignores + envelope metadata such as revision numbers, timestamps, and links. + +Authentication state between the challenge and consent steps is held server-side for five minutes and +bound to the relying party, the client, and the transaction. A context that expires or is presented +by a different party fails with a dedicated error code rather than a silent retry. + +## The governed lookup + +The provider reaches your registry through a single fixed BReg operation: a lookup on the route you +name, with the selector you name, under the access profile you grant. On the wire that is a `POST` +to the route's `:lookup` operation, with `accessProfile` and `$select` query parameters and a JSON +body carrying the selector and the verified identifier. + +```http +POST /v1/records/population:lookup?accessProfile=esignet-source&$select=uin,status +Authorization: Bearer +Content-Type: application/json +Accept: application/json + +{"selector":"by-uin","values":{"uin":""}} +``` + +The `$select` list is never wider than the configured provisioned fields, and BReg enforces the +same ceiling on its side through the access profile. A misconfigured provider that asked for more +than the profile grants receives a denial, not extra data. + +{/* Evidence: crates/registry-breg/src/compiler.rs maps Operation::Lookup to POST on the +"{base}:lookup" path, and crates/registry-breg/src/api/metadata.rs documents the accessProfile and +$select query parameters the lookup accepts. docs/site/src/content/docs/reference/breg-api.mdx is +the adopter-facing reference for the same route family. */} + +## What your registry must grant + +On the BReg side, integration is an access-control decision expressed in the registry project, not +a code change. The provider needs: + +- **One selector profile** on the population route, for example `by-uin`, that resolves exactly one + record from one identifier field. The provider fails rather than picking among several matches. +- **One access profile** dedicated to eSignet, for example `esignet-source`, granting only the + `lookup` operation and only the fields the provider may ever project. Leave `get`, `list`, and all + write operations out of it. The provider never lists or writes, so a profile that permits either is + wider than the integration needs. +- **A field list that mirrors the profile.** The provider's `provisioned_fields` must match what the + profile grants, and `account_check_fields` must be a non-empty subset of it that contains the + selector field. Keep the account check to the identifier and a status field. +- **A separate administrative client** for anyone who edits records. The eSignet client's key is + lookup-only by construction, and it stays that way only if nobody reuses it for change requests. + +On the Mint side, register the provider as a `private_key_jwt` client with an ES256 or RS256 key. +Mint issues tokens the BReg access profile trusts, and the provider caches each one for at most +five minutes. + +The pages on [BReg access profiles](../../configure/breg-access/), +[Registry Mint](../../configure/mint/), and +[governed registry actions](../governed-registry-actions/) explain the surrounding model. + +## Claims and the pairwise subject + +`claim_map` in the provider configuration ties each OIDC claim name to a registry field. Three +rules shape it: + +- **The subject is never a registry identifier.** `claim_map.sub` must be the literal `$psut`. The + provider derives a pairwise pseudonymous subject per relying party and client, as a base64url + HMAC-SHA256 over the tuple of a fixed version string, the relying party, the client, the + identifier type, and the identifier, keyed by a secret of at least 32 bytes read from a mounted + file. Two relying parties therefore receive different subjects for the same citizen, and neither + can recover the registry identifier from its value. +- **Only mapped fields can ever leave.** A field that the access profile grants but `claim_map` + omits is never requested. A claim the citizen approves that is not mapped is silently absent. +- **eSignet signs, the provider does not.** The provider returns plain claims and the subject. + Signing, JWE encryption for relying parties that request it, and key rotation are eSignet's. + +Registry Stack's own OIDC verifier already accounts for eSignet's token shape. Evidence Gateway and +BReg accept eSignet-issued ID Tokens and UserInfo JWTs that omit the `typ` header, which is how +eSignet emits them, without relaxing any other check. + +{/* Evidence: crates/registry-platform-oidc/src/lib.rs, enforce_optional_typ: the shared OIDC +verifier accepts JWTs without a typ header because eSignet omits it on ID Token and UserInfo JWTs, +while still enforcing every other header and claim check. */} + +## Failure behaviour + +The provider collapses registry-side failures into a fixed set of error codes so that a relying +party or a citizen never learns whether an identifier exists, is inactive, or is concealed: + +| Code | When | +|---|---| +| `registry_auth_invalid_request` | The request to the provider is malformed or the configuration rejects it. | +| `registry_auth_challenge_failed` | The one-time code or other challenge did not verify. No registry call was made. | +| `registry_auth_subject_denied` | The lookup resolved no record the profile may disclose. | +| `registry_auth_unavailable` | Mint or BReg did not answer within the configured timeout, returned an oversized body, redirected, or omitted a configured account-check field. | +| `registry_auth_context_invalid` | The authentication context does not match the relying party, client, or transaction. | +| `registry_auth_context_expired` | The five-minute authentication context has lapsed. | + +Outbound calls require HTTPS unless you explicitly allow insecure HTTP for a local fixture. They +carry a bounded timeout (10 seconds by default), a bounded response size (1 MiB by default), and +follow no redirects. The provider never logs selectors, tokens, or source values. + +## A configuration example + +The full grammar lives with the provider. This example shows the shape and the relationships the +earlier sections described. + +```yaml +subject_id_type: uin +psut_secret_file: /run/secrets/registry-psut +breg: + base_url: https://population.example.org + route: population + selector: by-uin + selector_field: uin + access_profile: esignet-source + provisioned_fields: [uin, status, givenName, familyName, birthdate, gender] + account_check_fields: [uin, status] +mint: + token_endpoint: https://mint.example.org/token + client_id: esignet-source + private_key_file: /run/secrets/registry-client.jwk +claim_map: + sub: $psut + given_name: givenName + family_name: familyName + birthdate: birthdate + gender: gender +http: + timeout_seconds: 10 + max_response_bytes: 1048576 +demo: + static_otp_enabled: false +``` + +Secrets arrive as mounted files, never as inline values or environment variables. The pairwise +subject secret and the Mint client key are the two the deployment must protect and rotate on its +own schedule. + +:::caution[The demo verifier is for fixtures only] +The `demo` block enables a synthetic one-time code verifier that reads codes from a static file. +It exists for fixtures such as Solmara Lab. Production deployments supply a real challenge verifier +through the provider's Go interface, and the host refuses to start with no verifier configured. +::: + +## Boundaries + +Version 0.3.0 draws its lines deliberately: + +- **BReg only.** The provider reads from a Base Registry Engine registry. Reading through Registry + Relay is deferred, and Evidence Gateway is not involved: an eSignet login is an authentication, + not a signed minimum-disclosure assertion. +- **No production challenge delivery.** Sending one-time codes over SMS or email, enrollment, + wallet-based login, and identity assurance levels are out of scope. Deployments bring their own + verifier. +- **No Java path.** Earlier releases shipped a Java authenticator and JAR injection. Version 0.3.0 + replaces them with the Go provider, strict YAML, and mounted secret files, with no compatibility + layer. +- **Source-integrated build.** The provider is compiled into a pinned eSignet release rather than + loaded as a plugin, and ships as an OCI image with checksums, an SBOM, and provenance. Publishing + that image and running it as a hosted service are separate operator actions. + +## Try it in Solmara Lab + +[Solmara Lab](../../tutorials/first-run-with-solmara-lab/) carries a synthetic population registry +authored for BReg and an isolated eSignet fixture that embeds this provider. Its journey exercises +each guarantee above in order: a wrong code is rejected before any registry call, the account check +projects only the identifier and status, consent is explicit and can be refused on a later login, a +missing required claim ends the login, and the relying party verifies the signed UserInfo and +correlates the pairwise subject. It also proves the negative: the eSignet client cannot list, cannot +write, and cannot read inactive or ungranted records or fields. + +{/* TODO[evidence]: the Solmara Lab eSignet topology and journey checks are maintained in the +separate registrystack/solmara-lab repository (docs/esignet.md) and cannot be anchored here. */} + +## Next + +- [Configure BReg access profiles](../../configure/breg-access/) to define the lookup-only grant. +- [Run Registry Mint](../../configure/mint/) and register the provider as a `private_key_jwt` + client. +- [Request an access token](../../configure/request-an-access-token/) to test the grant before + wiring eSignet. +- [BReg HTTP API reference](../../reference/breg-api/) for the lookup route and problem documents. +- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) for the + principles this integration applies. diff --git a/docs/site/src/content/docs/explanation/integration-patterns.mdx b/docs/site/src/content/docs/explanation/integration-patterns.mdx index 0934be2b33..1c47eb2736 100644 --- a/docs/site/src/content/docs/explanation/integration-patterns.mdx +++ b/docs/site/src/content/docs/explanation/integration-patterns.mdx @@ -325,7 +325,9 @@ signals whether a page is complete, and Relay is only the worked example. ## Workflow engines Examples: [Camunda](https://camunda.com/), [Flowable](https://www.flowable.com/), and other -casework or process orchestration engines. +casework or process orchestration engines. For [OpenFn](https://www.openfn.org/), maintained +adaptors wrap the Registry Stack clients; see [Automate registry workflows with +OpenFn](../openfn-adaptors/). Workflow engines own process state, task assignment, timers, branching, retries, escalation, and history. diff --git a/docs/site/src/content/docs/explanation/openfn-adaptors.mdx b/docs/site/src/content/docs/explanation/openfn-adaptors.mdx new file mode 100644 index 0000000000..6509b18e1f --- /dev/null +++ b/docs/site/src/content/docs/explanation/openfn-adaptors.mdx @@ -0,0 +1,234 @@ +--- +title: Automate registry workflows with OpenFn +description: How the Registry Stack adaptors for OpenFn expose Evidence Gateway, Base Registry Engine, and Registry Relay to Lightning workflows, and what the synthetic agriculture pilot proves. +status: draft +owner: registry-docs +source_repos: [registry-stack, openfn-language-registry-stack] +last_reviewed: "2026-09-09" +doc_type: explanation +locale: en +standards_referenced: [govstack-digital-registries] +--- + +You run [OpenFn](https://www.openfn.org/) Lightning to move data between government systems, and +you want its workflows to read from and write to Evidence Gateway, Base Registry Engine (BReg), and +Registry Relay without hand-writing HTTP calls or storing raw records in workflow state. The +[Registry Stack adaptors for OpenFn](https://github.com/registrystack/openfn-language-registry-stack) +give you that: three adaptor packages that wrap the maintained clients, plus a self-hosted synthetic +pilot that shows registration, reviewed correction, and event-driven downstream updates end to end. +This page explains what each adaptor does, how you configure it, how it fails, and what the pilot +does and does not establish. + +{/* TODO[evidence]: adaptor behaviour, operation lists, result branches, and pilot topology live in +the separate registrystack/openfn-language-registry-stack repository and cannot be anchored from +this one. The statements below were checked against that repository's README, the three package +READMEs, deployment/README.md, pilot/agriculture/README.md, and the two service READMEs at commit +e3083a02d60c6ed28f440d7a8117741e6c736660. Re-verify them when that repository changes. */} + +## Three adaptors, one client underneath + +OpenFn adaptors are Node packages whose exported operations become the vocabulary of a job +expression. The Registry Stack adaptors add three: + +| Adaptor | Talks to | Built on | +|---|---|---| +| `@openfn/language-registry-evidence` | Evidence Gateway | `evidence` namespace of `@registrystack/client` | +| `@openfn/language-registry-breg` | BReg | `breg` namespace of `@registrystack/client` | +| `@openfn/language-registry-relay` | Registry Relay | Plain HTTP over the protected Relay API | + +The Evidence and BReg adaptors pin `@registrystack/client` at version 0.27.0 and delegate every +security-relevant step to it. The native client owns request preparation, nonce generation, HTTP +transport, response bounds, pinned-key trust, revocation, signature verification, and the +`private_key_jwt` token exchange. The adaptors only resolve OpenFn inputs, map typed client failures +to workflow branches, redact selectors and credentials, and shape the result. They implement no +authorization logic and no alternative HTTP client of their own. + +That client is a Node-API native binding around the Rust clients, not a WebAssembly build. It ships +for macOS arm64, Linux amd64 with glibc, and Linux arm64 with glibc. Alpine and other musl images are +unsupported, which is why the pilot repackages the upstream worker on a glibc base. + +{/* Evidence: crates/registry-stack-client-node/package.json and +crates/registry-stack-client-node/index.js define the @registrystack/client facade and its +discovery, evidence, relay, and breg namespaces that the adaptors import. */} + +## Configuring credentials + +Every adaptor reads its configuration from the OpenFn credential attached to the job, never from +event data or job source. The shape differs per adaptor. + +**Evidence adaptor.** Use `configuration.evidence` in one of two modes: + +- **Explicit mode** names the service base URL, the reviewed signing keys in `trustedJwks`, any + `revokedKeyIds`, and exactly one token source: a `static` bearer token or a `privateKeyJwt` block + with the token endpoint, client ID, and a private JWK. The JWKS and revocation list are + relying-party trust configuration. Verification never fetches keys from Evidence Gateway and + never follows a key URL from a response. +- **Profile mode** points `profilePath` at a reviewed `registry.evidence-client-profile/v1` JSON + file mounted into the worker, optionally with `privateKeyJwk` from the credential. The profile + owns service expectations, verification policy, and contracts. The two modes cannot be combined. + +**BReg adaptor.** Use `configuration.breg` with the registry base URL and an `authorization` block +that is either `{ static }` or `{ privateKeyJwt }` with the same fields as above. Token and registry +TLS trust roots and timeouts are independent settings. + +**Relay adaptor.** Create a credential with `relay_base_url` and `token`. The adaptor sends the +token as a bearer credential and requires an explicit `purpose` on every read. + +Configuration stays available between operations inside one job so that `execute`, `each`, and +successive requests can authenticate. The OpenFn runtime removes it from the final output by +default. Record inputs and outputs stay in `data`, and the job is responsible for minimizing them +before the next step. + +{/* Evidence: crates/registry-evidence-client-node/src/lib.rs exposes from_profile and +crates/registry-evidence-client/src/profile.rs defines the registry.evidence-client-profile/v1 +document the adaptor's profile mode loads. */} + +## What each adaptor does + +### Evidence: one assertion, verified + +`requestEvidence` prepares a fixed request from the job's inputs, sends it, and returns only after +the native client has verified the signature, the time window, and the policy expectations. On +success the result carries the verified assertion, the exact flattened JWS bytes for retention, a +trace ID for support, the accepted subject bindings, and explicit verification flags. +`selectSupportedValue` picks one value by its governed concept identifier. + +Subject continuity is explicit. On first contact you either pass `acceptFirstUse` and persist the +returned pinned bindings, or in profile mode you receive an opaque continuity receipt. Later requests +pass those bindings or that receipt back, and only then does a matched result prove the assertion +concerns the same subject as before. + +Selector values referenced with `valueFrom` are removed from the returned data, including on +failure. The adaptor requests signed JWS only and publishes no native handles into workflow state. + +### BReg: governed reads and writes + +The BReg adaptor covers metadata discovery, exact lookups, single-page lists with explicit +continuation, record creation, patching, change-request creation, and lifecycle actions. It follows +the same rules a careful direct client would: + +- **Writes select their authority from metadata.** Every write fetches caller-filtered metadata + inside the operation and binds to the exact operation and access profile. No function accepts a + write URL, a serialized binding, or an arbitrary action URL as authority. +- **Every mutation carries its own idempotency key.** Derive it from the source event and the + operation. A temporary failure can leave an outcome unknown, and the only safe recovery is to + replay the exact request with the same key and preconditions. +- **Patches need the record's strong ETag.** Lifecycle actions need the separate action-specific + `ifMatch` value from the request's advertised actions, never the record ETag. The lifecycle helper + refetches the contract and the record and refuses to act on a changed proposal. +- **Drafts do not submit themselves.** Creating a change request produces a draft. Submitting, + approving, rejecting, requesting revision, revising, cancelling, and applying are separate + lifecycle operations, each subject to the actor and stage that the registry's metadata advertises. + +Patch members are written as field operations such as replace on a named field. The native client +builds and validates the HTTP patch document, so job authors never touch JSON Pointer paths. + +### Relay: protected reads + +The Relay adaptor is for workflows already authorized to read registry rows, metadata, +relationships, or aggregates directly. It offers single-record reads, filtered and bounded lists, +aggregate queries, dataset discovery, entity schemas, and evidence-offering discovery. Collection +reads require an explicit limit and at least one filter unless you opt out. Use it when the +workflow needs the data itself, and the Evidence adaptor when the workflow needs a trust decision. + +## How operations fail + +Every operation returns a state function that writes a result with a `branch` field. Check the +branch before consuming a value or starting a dependent mutation. + +The Evidence adaptor distinguishes `invalid_request`, `authentication_failed`, `not_authorized`, +`response_format_not_acceptable`, `evidence_not_available`, `retryable_infrastructure`, and +`failed`. An unresolved subject is `evidence_not_available`, and the adaptor never coerces that into +a Boolean `false`. The Evidence Gateway service reports the same condition as an +`evidence.unavailable` problem. + +The BReg adaptor distinguishes `invalid_request`, `auth_failed`, `denied`, `not_found`, `conflict`, +`retryable_infrastructure`, `protocol_failed`, and `failed`. Failures include a safe problem with a +code, a retryable flag, and an optional HTTP status. Exception messages, response problem prose, +credentials, and record values are excluded. + +Neither adaptor nor the native client retries or follows redirects. Retry policy belongs to the +workflow, where it can be tied to the idempotency key and the source-event correlation. + +{/* Evidence: crates/registry-evidence/src/problem.rs and crates/registry-evidence/src/contracts.rs +define the evidence.unavailable problem code the Evidence adaptor maps to its +evidence_not_available branch. */} + +## The synthetic agriculture pilot + +The repository ships a complete self-hosted pilot so you can see the adaptors run against real +services before designing your own workflows. It adapts the maintained agricultural-holdings core +starter, keeping its Farm, holder, responsibility, and reviewed name-correction model, and runs +upstream Lightning 2.18.2 with websocket worker 1.29.0 beside BReg, Mint, Evidence Gateway, an +authenticated event bridge, and a small SQLite destination. + +Three workflows carry the story: + +| Workflow | What it does | Boundary it demonstrates | +|---|---|---| +| Registration | Creates one Farm with a stable submission key and returns its UUID. | An exact retry returns the same record. Changed content under the same key conflicts. | +| Correction | Creates and submits a name-correction request and returns the request UUID. | A separate reviewer approves and then explicitly applies it. Submission and approval leave the Farm unchanged. | +| Committed Farm event | Verifies an HMAC-signed registry event, requests signed registration Evidence, and updates the destination. | Only the verified Boolean, the record reference, and the revision reach the destination. | + +Authority is split across principals on purpose: + +- `openfn-service` creates farms, resolves their local identifier, and creates and submits its own + corrections. It cannot review, apply, patch a Farm directly, list collections, or maintain holders. +- `reviewer` inspects, approves, and explicitly applies a proposed correction from a tools container + that holds its private key. OpenFn never receives that key. Approval binds to the inspected + proposal version, effect digest, and lifecycle ETag, and a stale snapshot is refused. +- `evidence-source` performs one exact Farm lookup that returns only the local identifier. It + cannot read names, list records, or write. +- `holding-registered` is the single Boolean concept Evidence Gateway signs. It means a unique + holding currently resolves in this synthetic register and claims nothing about eligibility, land + rights, activity, ownership, or a person's identity. + +The event path is where the products compose. BReg's transactional outbox delivers committed Farm +events as CloudEvents signed under the `breg-webhook-signature-v1` HMAC contract. The event bridge +verifies the signature in constant time, checks source, type, schema, delivery time, and payload +shape, and only then posts a minimized envelope to a fixed OpenFn webhook protected by a separate +API key. The destination deduplicates on source, event ID, and effect, refuses a changed payload +under the same key, and only advances a record to a higher revision, so at-least-once delivery never +regresses state. + +{/* Evidence: crates/registry-breg/src/webhook.rs defines SIGNATURE_DOMAIN as +breg-webhook-signature-v1 and crates/registry-breg/tests/postgres_webhook_delivery.rs exercises the +outbox delivery the pilot's event bridge verifies. products/breg/starters/agricultural-holdings/core/registry.yaml +is the starter the pilot adapts. */} + +## What the pilot does not establish + +- **It is synthetic and local.** Every person, holding, identifier, and credential is generated for + an isolated pilot. Only the Lightning UI and the two pilot service ports are published, all on host + loopback. +- **It runs on linux/amd64 only.** The released Lightning and worker images provide that platform + alone. Apple Silicon runs it under emulation. The adaptor layer loads natively on arm64, but that + does not establish arm64 worker support without an upstream arm64 image. +- **The Relay adaptor is not in the worker image.** The pilot packages the Evidence and BReg + adaptors with the upstream `common` and `http` adaptors. Relay reads are outside its journeys. +- **Setup is not acceptance.** Setup builds images, generates private inputs, initializes the + registry, and provisions the operator. The smoke run, not setup, is what proves the three + workflows, the reviewer path, the event-driven Evidence request, and the destination effect. +- **Inherited learning profiles are wide.** The starter's reader, editor, and reviewer grants keep + registry-wide collection and history access. A production deployment reviews its population scope + before reusing them. + +## Using the adaptors in your own OpenFn + +To load the adaptors into another compatible OpenFn installation, point the worker at a checkout of +the repository through its local-adaptor variables and reference the packages as +`@openfn/language-registry-evidence@local` and `@openfn/language-registry-breg@local`. The worker +must run Node on glibc for the native client to load. The individual adaptor READMEs carry the +credential schemas, operation options, and standalone composition rules. + +## Next + +- [Request evidence from an application](../../tutorials/request-evidence-from-an-application/) + for the direct client flow the Evidence adaptor wraps. +- [Query BReg with Python and Node](../../tutorials/query-breg-client/) for the direct client flow + the BReg adaptor wraps. +- [Send registry events to a webhook](../../tutorials/send-registry-events-to-a-webhook/) for the + outbox and signature contract the event bridge verifies. +- [Integration patterns](../integration-patterns/) for where a workflow engine sits among the other + ways to wire Registry Stack in. +- [Client API reference](../../reference/client-api/) for the namespaces the adaptors build on. diff --git a/docs/site/src/content/docs/reference/glossary.mdx b/docs/site/src/content/docs/reference/glossary.mdx index 0d050e0189..5c4885f27c 100644 --- a/docs/site/src/content/docs/reference/glossary.mdx +++ b/docs/site/src/content/docs/reference/glossary.mdx @@ -154,7 +154,7 @@ Product names are always in English, including on future translated pages.
The irreversible removal of retained detail from a Base Registry Engine registry under an approved retention request. `bregctl history erase` removes the retained revisions of one record up to a revision number, the correction context and retained outbox payloads that referenced them, and the cached idempotency responses that could replay them. `bregctl request-retention erase` removes the payload detail of one change-request proposal version. Neither touches the current record, later revisions, or the audit journal, and neither has a dry run. A rebaseline restores snapshot coverage afterwards.
eSignet
-
Open-source identity and authentication service (part of MOSIP). No current Registry Stack service integrates with it. Evidence Gateway authenticates callers against one configured OIDC issuer and key set named in its immutable bundle, and names no specific identity product.
+
Open-source identity and authentication service (part of MOSIP). No Registry Stack service integrates with it directly. Evidence Gateway authenticates callers against one configured OIDC issuer and key set named in its immutable bundle, and names no specific identity product. A separately maintained eSignet provider can authenticate citizens against a Base Registry Engine registry through one governed lookup; see Authenticate citizens with eSignet.
evidence offering
Metadata entry in a Registry Manifest dataset that describes a verification capability and the access path a client uses to reach it (schema versions `registry-manifest-evidence-offering/v1` and `registry-manifest-evidence-offerings/v1`). Registry Manifest compiles and publishes offerings; an offering is a pointer, and no maintained runtime in the current release serves offerings in its own metadata or evaluates the capability behind one.
From 087bff4c3858b19153431384e9a9419ed1ce72a2 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 9 Sep 2026 22:35:05 +0700 Subject: [PATCH 2/3] docs: correct integration claims flagged in review The eSignet page told operators a dedicated access profile isolates the provider, but `accessProfile` is caller-selected and an empty `requiredScopes` set admits any caller that satisfies the principal claim, so the profile now needs a scope registered on the provider's Mint client alone. It also claimed BReg and Evidence Gateway accept eSignet ID Tokens and UserInfo JWTs: both runtimes verify access tokens only, so that paragraph now says a citizen's ID Token is not a registry credential. The OpenFn page promised the adaptors keep raw records out of workflow state while later saying record data stays in `data`, described change application as always a separate lifecycle step when `automatic` and `planner` modes apply inline, and listed evidence-offering discovery as a current Relay capability though Relay V2 claims no Evidence family and the V1 route is retired. eSignet 0.3.0 is released, so the page pins its links to the tag and is no longer a draft, and the terms entry says which way the integration runs. Signed-off-by: Jeremi Joslin --- .../esignet-authentication-over-breg.mdx | 43 +++++++++++++------ .../docs/explanation/openfn-adaptors.mdx | 25 ++++++++--- docs/site/src/content/docs/spec/rs-terms.mdx | 2 +- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/docs/site/src/content/docs/explanation/esignet-authentication-over-breg.mdx b/docs/site/src/content/docs/explanation/esignet-authentication-over-breg.mdx index 67dc993ca1..f667df2ced 100644 --- a/docs/site/src/content/docs/explanation/esignet-authentication-over-breg.mdx +++ b/docs/site/src/content/docs/explanation/esignet-authentication-over-breg.mdx @@ -1,7 +1,7 @@ --- title: Authenticate citizens with eSignet over Base Registry Engine (BReg) description: How the eSignet Base Registry Engine provider turns a governed population registry lookup into a verified OpenID Connect login, what it discloses, and where its boundaries lie. -status: draft +status: current owner: registry-docs source_repos: [registry-stack, esignet-relay-authenticator] last_reviewed: "2026-09-09" @@ -16,14 +16,14 @@ authentication service from the MOSIP project. The eSignet BReg provider is the the two. It lets eSignet resolve a verified identifier against your registry through one governed lookup, and release only the demographic claims each citizen approves. This page explains what the provider does, what your registry must grant it, and what it deliberately leaves out. It is the -adopter-facing account of version 0.3.0 of -[registrystack/esignet-relay-authenticator](https://github.com/registrystack/esignet-relay-authenticator). +adopter-facing account of +[registrystack/esignet-relay-authenticator v0.3.0](https://github.com/registrystack/esignet-relay-authenticator/releases/tag/v0.3.0). {/* TODO[evidence]: provider behaviour (challenge verifier interface, PSUT derivation, claim mapping, error codes, build outputs) lives in the separate registrystack/esignet-relay-authenticator repository and cannot be anchored from this one. Re-verify the statements below against that -repository's README, docs/esignet-configuration.md, and docs/breg-lookup-contract.md when it -changes. */} +repository's README, docs/esignet-configuration.md, and docs/breg-lookup-contract.md at the next +release tag. */} ## Who owns what @@ -139,15 +139,28 @@ a code change. The provider needs: `lookup` operation and only the fields the provider may ever project. Leave `get`, `list`, and all write operations out of it. The provider never lists or writes, so a profile that permits either is wider than the integration needs. +- **A required scope only the provider holds.** `accessProfile` is a request parameter, so the + profile's name isolates nothing on its own: any authenticated caller whose token satisfies the + profile's principal claim, purposes, and row boundaries can select it. Give the profile a + `requiredScopes` entry, for example `registry:esignet:lookup`. A profile with no required scope + raises `access.profile.no_required_scope` when you check the project. - **A field list that mirrors the profile.** The provider's `provisioned_fields` must match what the profile grants, and `account_check_fields` must be a non-empty subset of it that contains the selector field. Keep the account check to the identifier and a status field. - **A separate administrative client** for anyone who edits records. The eSignet client's key is lookup-only by construction, and it stays that way only if nobody reuses it for change requests. -On the Mint side, register the provider as a `private_key_jwt` client with an ES256 or RS256 key. -Mint issues tokens the BReg access profile trusts, and the provider caches each one for at most -five minutes. +{/* Evidence: crates/registry-breg/src/api/mod.rs, authorize_profile_claims: the selected profile +admits a request when every entry in required_scopes is present in the verified token, so an empty +set admits any caller that satisfies the principal claim and the row boundaries. +crates/registry-breg/src/access.rs raises access.profile.no_required_scope for that case. +crates/registry-mint/src/token.rs joins a client's registered scopes into the minted scope claim. */} + +On the Mint side, register the provider as a `private_key_jwt` client with an ES256 or RS256 key, +and put the profile's required scope in that client's `authorization.scopes` and nowhere else. The +provider's token request carries no scope of its own, so the registration is the only thing that +lets it select the profile. Mint issues tokens the BReg access profile trusts, and the provider +caches each one for at most five minutes. The pages on [BReg access profiles](../../configure/breg-access/), [Registry Mint](../../configure/mint/), and @@ -169,13 +182,15 @@ rules shape it: - **eSignet signs, the provider does not.** The provider returns plain claims and the subject. Signing, JWE encryption for relying parties that request it, and key rotation are eSignet's. -Registry Stack's own OIDC verifier already accounts for eSignet's token shape. Evidence Gateway and -BReg accept eSignet-issued ID Tokens and UserInfo JWTs that omit the `typ` header, which is how -eSignet emits them, without relaxing any other check. +An eSignet ID Token or UserInfo JWT authenticates a citizen to a relying party. It is not a registry +credential. BReg and Evidence Gateway verify access tokens against their configured issuer and +required token type, so forwarding a citizen's ID Token to either one is rejected. The only +credential that reaches the registry is the provider's own Mint access token. -{/* Evidence: crates/registry-platform-oidc/src/lib.rs, enforce_optional_typ: the shared OIDC -verifier accepts JWTs without a typ header because eSignet omits it on ID Token and UserInfo JWTs, -while still enforcing every other header and claim check. */} +{/* Evidence: crates/registry-breg/src/runtime_config.rs and crates/registry-evidence/src/auth.rs +both build TokenVerifierConfig::access_token_profile, whose allowed_typ list is required and fails +closed when empty (crates/registry-platform-oidc/src/lib.rs, access_token_profile). Neither runtime +calls verify_id_token or verify_userinfo_jwt. */} ## Failure behaviour diff --git a/docs/site/src/content/docs/explanation/openfn-adaptors.mdx b/docs/site/src/content/docs/explanation/openfn-adaptors.mdx index 6509b18e1f..6d38398101 100644 --- a/docs/site/src/content/docs/explanation/openfn-adaptors.mdx +++ b/docs/site/src/content/docs/explanation/openfn-adaptors.mdx @@ -12,7 +12,7 @@ standards_referenced: [govstack-digital-registries] You run [OpenFn](https://www.openfn.org/) Lightning to move data between government systems, and you want its workflows to read from and write to Evidence Gateway, Base Registry Engine (BReg), and -Registry Relay without hand-writing HTTP calls or storing raw records in workflow state. The +Registry Relay without hand-writing HTTP calls or minting tokens in job code. The [Registry Stack adaptors for OpenFn](https://github.com/registrystack/openfn-language-registry-stack) give you that: three adaptor packages that wrap the maintained clients, plus a self-hosted synthetic pilot that shows registration, reviewed correction, and event-driven downstream updates end to end. @@ -117,8 +117,12 @@ the same rules a careful direct client would: `ifMatch` value from the request's advertised actions, never the record ETag. The lifecycle helper refetches the contract and the record and refuses to act on a changed proposal. - **Drafts do not submit themselves.** Creating a change request produces a draft. Submitting, - approving, rejecting, requesting revision, revising, cancelling, and applying are separate - lifecycle operations, each subject to the actor and stage that the registry's metadata advertises. + approving, rejecting, requesting revision, revising, and cancelling are separate lifecycle + operations, each subject to the actor and stage that the registry's metadata advertises. Whether + application is also a separate step is the registry's decision: `application.mode: manual` waits + for an applier, `automatic` applies inside the submit or the final approval, and `planner` lets a + script return an `apply` disposition. Follow the actions the registry advertises after each call + rather than assuming a fixed sequence. Patch members are written as field operations such as replace on a named field. The native client builds and validates the HTTP patch document, so job authors never touch JSON Pointer paths. @@ -127,9 +131,18 @@ builds and validates the HTTP patch document, so job authors never touch JSON Po The Relay adaptor is for workflows already authorized to read registry rows, metadata, relationships, or aggregates directly. It offers single-record reads, filtered and bounded lists, -aggregate queries, dataset discovery, entity schemas, and evidence-offering discovery. Collection -reads require an explicit limit and at least one filter unless you opt out. Use it when the -workflow needs the data itself, and the Evidence adaptor when the workflow needs a trust decision. +aggregate queries, dataset discovery, and entity schemas. Collection reads require an explicit +limit and at least one filter unless you opt out. Use it when the workflow needs the data itself, +and the Evidence adaptor when the workflow needs a trust decision. + +The adaptor also carries an evidence-offering listing. Do not build a workflow on it: Relay V2 +claims no Evidence capability family, the V1 metadata route that served offerings is retired, and +evidence offerings are published by Registry Manifest instead. + +{/* Evidence: docs/site/src/content/docs/products/registry-relay/index.md states that Relay V2 does +not claim the Evidence family, and docs/site/src/lib/relay-v2-retirement-redirects.mjs lists +list_metadata_evidence_offerings among the V1 operation slugs retired by the V2 cutover. +crates/registry-manifest-core/src/lib.rs renders evidence offerings from the manifest. */} ## How operations fail diff --git a/docs/site/src/content/docs/spec/rs-terms.mdx b/docs/site/src/content/docs/spec/rs-terms.mdx index 6f09ffb700..0c298d4cbf 100644 --- a/docs/site/src/content/docs/spec/rs-terms.mdx +++ b/docs/site/src/content/docs/spec/rs-terms.mdx @@ -123,7 +123,7 @@ The registry stack comprises four formal products: Registry Platform, Registry R **OID4VCI**: OpenID for Verifiable Credential Issuance. The Evidence Gateway runtime implements it in no part, and OID4VCI in any part is a frozen Evidence Gateway Version 1 non-goal under both subject binding modes, so no serialization has a credential offer, pre-authorized code, authorization or token endpoint, `c_nonce`, credential endpoint, deferred issuance, or wallet grant. The separate `registry-evidence-oid4vci` front end speaks it, which is why it is a separate service. -**eSignet**: Open-source identity and authentication service (part of MOSIP). No current stack service integrates with it. Evidence Gateway authenticates callers against one configured OIDC issuer and key set named in its immutable bundle and names no specific identity product. Not a Registry Stack product term. +**eSignet**: Open-source identity and authentication service (part of MOSIP). No stack service calls it. Evidence Gateway authenticates callers against one configured OIDC issuer and key set named in its immutable bundle and names no specific identity product. The integration runs the other way: a separately maintained eSignet provider authenticates a citizen against a Base Registry Engine (BReg) registry through one governed lookup carrying a Registry Mint token, which makes eSignet an ordinary governed client of a registry. See [Authenticate citizens with eSignet](../../explanation/esignet-authentication-over-breg/). Not a Registry Stack product term. ## 3. Metadata and standards terms From 2ca8e403b1e83bb33d4cd51c2177377da2801f2d Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 9 Sep 2026 22:43:27 +0700 Subject: [PATCH 3/3] docs: cite tracked source for the Relay evidence family claim The anchor named the generated products/registry-relay page, which a clean checkout does not hold, so the Evidence anchors job failed on a tree the local check passed against a built site. Signed-off-by: Jeremi Joslin --- .../src/content/docs/explanation/openfn-adaptors.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/site/src/content/docs/explanation/openfn-adaptors.mdx b/docs/site/src/content/docs/explanation/openfn-adaptors.mdx index 6d38398101..5e188d193c 100644 --- a/docs/site/src/content/docs/explanation/openfn-adaptors.mdx +++ b/docs/site/src/content/docs/explanation/openfn-adaptors.mdx @@ -139,10 +139,11 @@ The adaptor also carries an evidence-offering listing. Do not build a workflow o claims no Evidence capability family, the V1 metadata route that served offerings is retired, and evidence offerings are published by Registry Manifest instead. -{/* Evidence: docs/site/src/content/docs/products/registry-relay/index.md states that Relay V2 does -not claim the Evidence family, and docs/site/src/lib/relay-v2-retirement-redirects.mjs lists -list_metadata_evidence_offerings among the V1 operation slugs retired by the V2 cutover. -crates/registry-manifest-core/src/lib.rs renders evidence offerings from the manifest. */} +{/* Evidence: crates/registry-relay-v2/src/artifacts.rs builds unsupported_families with evidence +among its entries, so every V2 capability inventory reports that family as unsupported. +docs/site/src/lib/relay-v2-retirement-redirects.mjs lists list_metadata_evidence_offerings among the +V1 operation slugs the V2 cutover retired. crates/registry-manifest-core/src/lib.rs holds the +EvidenceOfferingManifest that Registry Manifest compiles and renders. */} ## How operations fail