From a6a92e0ac0264960437b0a023c05ce44fc944ae5 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 6 Aug 2026 02:43:09 +0700 Subject: [PATCH 01/53] Migrate local lab to Registry Evidence Signed-off-by: Jeremi Joslin --- .env.example | 112 +- .gitignore | 2 + README.md | 352 ++--- compose.yaml | 1056 +++------------ config/evidence/local/.gitkeep | 1 + docker/registry-stack-runtime/Dockerfile | 37 + docs/changelog.md | 17 + docs/purposes.md | 113 +- evidence/Caddyfile | 66 + evidence/bundle/adapters/extract.rhai | 26 + evidence/bundle/adapters/prepare.rhai | 16 + .../bundle/derivations/cra-child-benefit.rhai | 16 + evidence/bundle/derivations/cra-deceased.rhai | 6 + evidence/bundle/derivations/cra-linked.rhai | 6 + evidence/bundle/derivations/livestock.rhai | 16 + evidence/bundle/derivations/not-enrolled.rhai | 6 + .../bundle/derivations/pension-active.rhai | 6 + .../bundle/derivations/population-active.rhai | 8 + .../bundle/derivations/poverty-priority.rhai | 6 + .../bundle/derivations/survivor-eligible.rhai | 6 + evidence/bundle/derivations/voucher.rhai | 19 + evidence/bundle/evidence.yaml | 542 ++++++++ .../bundle/fixtures/cra-child-benefit.yaml | 62 + evidence/bundle/fixtures/cra-citizen.yaml | 39 + evidence/bundle/fixtures/cra-pension.yaml | 39 + .../bundle/fixtures/mosd-child-benefit.yaml | 39 + evidence/bundle/fixtures/nagdi-livestock.yaml | 65 + evidence/bundle/fixtures/nagdi-voucher.yaml | 67 + .../bundle/fixtures/nia-child-benefit.yaml | 39 + evidence/bundle/fixtures/nia-citizen.yaml | 39 + evidence/bundle/fixtures/sipf-pension.yaml | 39 + evidence/bundle/fixtures/sipf-survivor.yaml | 39 + .../bundle/fixtures/sro-child-benefit.yaml | 39 + .../schemas/adapter-parameters.schema.yaml | 7 + .../cra-child-benefit-facts.schema.yaml | 8 + .../schemas/cra-citizen-facts.schema.yaml | 5 + .../schemas/cra-pension-facts.schema.yaml | 6 + .../mosd-child-benefit-facts.schema.yaml | 6 + .../schemas/nagdi-livestock-facts.schema.yaml | 10 + .../schemas/nagdi-voucher-facts.schema.yaml | 13 + .../schemas/nia-population-facts.schema.yaml | 7 + evidence/bundle/schemas/response.schema.yaml | 45 + .../schemas/sipf-pension-facts.schema.yaml | 6 + .../schemas/sipf-survivor-facts.schema.yaml | 6 + .../sro-child-benefit-facts.schema.yaml | 6 + evidence/mint.yaml | 30 + evidence/runtime.yaml | 22 + generator/output/checksums.sha256 | 12 +- generator/solmara_lab/generate.py | 30 +- generator/tests/test_generator.py | 6 + home/src/lib/components/EngineerDoor.svelte | 7 +- home/src/lib/components/PurposeLens.svelte | 2 +- home/src/lib/components/TrustStrip.svelte | 6 +- home/src/lib/curl.test.ts | 24 +- home/src/lib/runresult.test.ts | 6 +- home/src/lib/runresult.ts | 14 +- home/src/lib/server/data.test.ts | 12 +- home/src/lib/server/problemcodes.test.ts | 25 +- home/src/lib/server/problemcodes.ts | 20 +- home/src/lib/server/purposes.test.ts | 16 +- home/src/lib/server/purposes.ts | 12 +- home/src/lib/server/services.test.ts | 115 +- home/src/lib/server/services.ts | 116 +- home/src/lib/server/tokens.test.ts | 188 +-- home/src/lib/server/tokens.ts | 50 +- home/src/lib/server/urlmap.test.ts | 100 +- home/src/lib/server/urlmap.ts | 10 +- home/src/lib/types.ts | 15 +- home/src/routes/anatomy/+page.svelte | 4 +- home/src/routes/stories/[id]/+page.svelte | 2 +- justfile | 74 +- .../fixtures/001-schema.sql | 2 +- .../interior-population/fixtures/002-load.sql | 26 +- .../labour-pensions/fixtures/003-schema.sql | 10 +- .../labour-pensions/fixtures/004-load.sql | 27 +- .../labour-pensions/fixtures/pension_case.csv | 2 +- .../fixtures/sipf_survivor_link.csv | 1 + portal/src/lib/forms/descriptors.ts | 12 +- portal/src/lib/providers/live/index.test.ts | 834 ++---------- portal/src/lib/providers/live/index.ts | 916 +++---------- portal/src/lib/server/provider.test.ts | 7 +- projects/cra-civil/environments/hosted.yaml | 25 +- projects/cra-civil/environments/local.yaml | 30 +- .../fixtures/future-birth-date.yaml | 14 - .../civil-person-by-uin/fixtures/match.yaml | 13 - .../fixtures/no-match.yaml | 13 - projects/cra-civil/registry-stack.yaml | 82 +- .../mosd-programme/environments/hosted.yaml | 22 +- .../mosd-programme/environments/local.yaml | 28 +- .../enrollment-by-uin/fixtures/match.yaml | 9 - .../enrollment-by-uin/fixtures/no-match.yaml | 1 - projects/mosd-programme/registry-stack.yaml | 35 +- .../environments/hosted.yaml | 28 +- .../nagdi-agriculture/environments/local.yaml | 34 +- .../fixtures/match.yaml | 15 - .../fixtures/no-match.yaml | 4 - .../fixtures/ambiguous.yaml | 2 +- .../fixtures/match.yaml | 15 - .../fixtures/no-match.yaml | 4 - .../nagdi-agriculture/registry-stack.yaml | 137 +- .../nia-population/environments/hosted.yaml | 27 +- .../nia-population/environments/local.yaml | 32 +- .../fixtures/match.yaml | 11 - .../fixtures/no-match.yaml | 11 - projects/nia-population/registry-stack.yaml | 40 +- .../sipf-pensions/environments/hosted.yaml | 26 +- .../sipf-pensions/environments/local.yaml | 31 +- .../fixtures/ambiguous.yaml | 1 - .../fixtures/match.yaml | 9 - .../fixtures/no-match.yaml | 1 - .../fixtures/ambiguous.yaml | 1 - .../fixtures/match.yaml | 9 - .../fixtures/no-match.yaml | 1 - projects/sipf-pensions/registry-stack.yaml | 72 +- projects/sro-social/environments/hosted.yaml | 22 +- projects/sro-social/environments/local.yaml | 28 +- .../fixtures/match.yaml | 9 - .../fixtures/no-match.yaml | 1 - projects/sro-social/registry-stack.yaml | 35 +- .../hosted/cra-civil/notary/notary.yaml | 233 ---- .../cra-civil-consultation-civil.json | 1 + .../civil-person-by-uin/conformance.json | 1 + .../civil-person-by-uin/minimization.json | 0 .../negative-security.json | 0 .../civil-person-by-uin.json | 2 +- .../cra-civil-consultation-civil.json} | 2 +- .../relay.yaml} | 111 +- .../cra-child-benefit-civil.json | 1 - .../cra-citizen-record-civil.json | 1 - .../cra-pension-death-civil.json | 1 - .../civil-person-by-uin/conformance.json | 1 - .../cra-pension-death-civil.json | 1 - .../hosted/cra-civil/relay/relay.yaml | 74 +- .../hosted/mosd-programme/notary/notary.yaml | 76 -- ...sd-enrollment-consultation-enrollment.json | 1 + .../enrollment-by-uin/conformance.json | 1 + .../enrollment-by-uin/minimization.json | 0 .../enrollment-by-uin/negative-security.json | 0 .../integration-packs/enrollment-by-uin.json | 2 +- ...d-enrollment-consultation-enrollment.json} | 2 +- .../relay.yaml} | 91 +- .../child-benefit-enrollment.json | 1 - .../enrollment-by-uin/conformance.json | 1 - .../hosted/mosd-programme/relay/relay.yaml | 65 +- .../nagdi-agriculture/notary/notary.yaml | 412 ------ .../nagdi-farmer-consultation-farmer.json | 1 + .../nagdi-herd-consultation-herd.json | 1 + .../farmer-voucher-by-farmer/conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../farmer-voucher-by-farmer.json | 2 +- .../livestock-movement-by-farmer.json | 2 +- .../nagdi-farmer-consultation-farmer.json} | 2 +- .../nagdi-herd-consultation-herd.json} | 2 +- .../relay-consultation/relay.yaml | 351 +++++ .../livestock-herd.json | 1 - .../voucher-farmer.json | 1 - .../farmer-voucher-by-farmer/conformance.json | 1 - .../conformance.json | 1 - .../relay/relay-consultation.yaml | 234 ---- .../hosted/nagdi-agriculture/relay/relay.yaml | 159 ++- .../hosted/nia-population/notary/notary.yaml | 152 --- ...ia-population-consultation-population.json | 1 + .../population-person-by-uin/conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../population-person-by-uin.json | 2 +- ...a-population-consultation-population.json} | 2 +- .../relay.yaml} | 41 +- .../nia-child-benefit-population.json | 1 - .../nia-citizen-status-population.json | 1 - .../population-person-by-uin/conformance.json | 1 - .../hosted/nia-population/relay/relay.yaml | 8 +- .../hosted/sipf-pensions/notary/notary.yaml | 136 -- .../sipf-pension-consultation-pension.json | 1 + .../sipf-survivor-consultation-survivor.json | 1 + .../conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../pension-case-by-survivor/conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../pension-case-by-pensioner.json | 2 +- .../pension-case-by-survivor.json | 2 +- .../sipf-pension-consultation-pension.json} | 2 +- .../sipf-survivor-consultation-survivor.json} | 2 +- .../relay-consultation/relay.yaml | 263 ++++ .../sipf-pension-payment-review-pension.json | 1 - .../sipf-survivor-benefit-survivor.json | 1 - .../conformance.json | 1 - .../pension-case-by-survivor/conformance.json | 1 - .../relay/relay-consultation.yaml | 182 --- .../hosted/sipf-pensions/relay/relay.yaml | 124 +- .../hosted/sro-social/notary/notary.yaml | 77 -- .../sro-household-consultation-household.json | 1 + .../conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../child-benefit-household-by-uin.json | 2 +- ...sro-household-consultation-household.json} | 2 +- .../relay.yaml} | 87 +- .../child-benefit-household.json | 1 - .../conformance.json | 1 - .../hosted/sro-social/relay/relay.yaml | 65 +- .../local/cra-civil/notary/notary.yaml | 235 ---- .../cra-civil-consultation-civil.json | 1 + .../civil-person-by-uin/conformance.json | 1 + .../civil-person-by-uin/minimization.json | 0 .../negative-security.json | 0 .../civil-person-by-uin.json | 2 +- .../cra-civil-consultation-civil.json} | 2 +- .../relay.yaml} | 111 +- .../cra-child-benefit-civil.json | 1 - .../cra-citizen-record-civil.json | 1 - .../cra-pension-death-civil.json | 1 - .../civil-person-by-uin/conformance.json | 1 - .../cra-child-benefit-civil.json | 1 - .../cra-citizen-record-civil.json | 1 - .../cra-pension-death-civil.json | 1 - .../local/cra-civil/relay/relay.yaml | 74 +- .../local/mosd-programme/notary/notary.yaml | 78 -- ...sd-enrollment-consultation-enrollment.json | 1 + .../enrollment-by-uin/conformance.json | 1 + .../enrollment-by-uin/minimization.json | 0 .../enrollment-by-uin/negative-security.json | 0 .../integration-packs/enrollment-by-uin.json | 2 +- ...d-enrollment-consultation-enrollment.json} | 2 +- .../relay.yaml} | 91 +- .../child-benefit-enrollment.json | 1 - .../enrollment-by-uin/conformance.json | 1 - .../local/mosd-programme/relay/relay.yaml | 65 +- .../nagdi-agriculture/notary/notary.yaml | 414 ------ .../nagdi-farmer-consultation-farmer.json | 1 + .../nagdi-herd-consultation-herd.json | 1 + .../farmer-voucher-by-farmer/conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../farmer-voucher-by-farmer.json | 2 +- .../livestock-movement-by-farmer.json | 2 +- .../nagdi-farmer-consultation-farmer.json} | 2 +- .../nagdi-herd-consultation-herd.json} | 2 +- .../relay-consultation/relay.yaml | 351 +++++ .../livestock-herd.json | 1 - .../voucher-farmer.json | 1 - .../farmer-voucher-by-farmer/conformance.json | 1 - .../conformance.json | 1 - .../relay/relay-consultation.yaml | 234 ---- .../local/nagdi-agriculture/relay/relay.yaml | 159 ++- .../local/nia-population/notary/notary.yaml | 154 --- ...ia-population-consultation-population.json | 1 + .../population-person-by-uin/conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../population-person-by-uin.json | 2 +- ...a-population-consultation-population.json} | 2 +- .../relay.yaml} | 41 +- .../nia-child-benefit-population.json | 1 - .../nia-citizen-status-population.json | 1 - .../population-person-by-uin/conformance.json | 1 - .../nia-child-benefit-population.json | 1 - .../nia-citizen-status-population.json | 1 - .../local/nia-population/relay/relay.yaml | 8 +- .../local/sipf-pensions/notary/notary.yaml | 138 -- .../sipf-pension-consultation-pension.json | 1 + .../sipf-survivor-consultation-survivor.json | 1 + .../conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../pension-case-by-survivor/conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../pension-case-by-pensioner.json | 2 +- .../pension-case-by-survivor.json | 2 +- .../sipf-pension-consultation-pension.json} | 2 +- .../sipf-survivor-consultation-survivor.json} | 2 +- .../relay-consultation/relay.yaml | 263 ++++ .../sipf-pension-payment-review-pension.json | 1 - .../sipf-survivor-benefit-survivor.json | 1 - .../conformance.json | 1 - .../pension-case-by-survivor/conformance.json | 1 - .../relay/relay-consultation.yaml | 182 --- .../local/sipf-pensions/relay/relay.yaml | 124 +- .../local/sro-social/notary/notary.yaml | 79 -- .../sro-household-consultation-household.json | 1 + .../conformance.json | 1 + .../minimization.json | 0 .../negative-security.json | 0 .../child-benefit-household-by-uin.json | 2 +- ...sro-household-consultation-household.json} | 2 +- .../relay.yaml} | 87 +- .../child-benefit-household.json | 1 - .../conformance.json | 1 - .../local/sro-social/relay/relay.yaml | 65 +- scenario-runner/child_benefit_federator.py | 576 ++------ scenario-runner/test_server.py | 1163 +++-------------- scenarios/child_benefit.py | 200 +-- scenarios/citizen.py | 308 +---- scenarios/common.py | 501 ++++--- scenarios/farmer_voucher.py | 129 +- scenarios/pension_survivor.py | 398 +----- scenarios/service_config.py | 119 +- scripts/build-registry-stack-runtime.sh | 95 ++ scripts/build-relay-runtime.sh | 76 +- scripts/check-config-secrets.py | 3 +- scripts/check-evidence-runtime.py | 121 ++ scripts/check-image-pins.py | 32 +- scripts/gen-secrets.py | 286 ++-- scripts/init-registry-postgresql.sh | 99 ++ scripts/postgres-entrypoint.sh | 28 +- scripts/registry-projects.sh | 30 +- scripts/registryctl-build-output.py | 26 +- scripts/registryctl-pinned.sh | 59 +- scripts/registryctl-test-output.py | 46 +- scripts/relay_workload_identity_agent.py | 5 +- scripts/review.sh | 16 +- scripts/smoke-live.py | 255 +--- scripts/smoke-relay-sources.py | 48 +- scripts/smoke-story-previews.py | 26 +- scripts/smoke.sh | 5 +- scripts/test_gen_secrets.py | 74 ++ scripts/test_image_pins.py | 36 +- scripts/test_registryctl_build_output.py | 29 +- scripts/test_registryctl_test_output.py | 69 +- scripts/test_relay_workload_identity_agent.py | 1 + versions.env | 13 +- 331 files changed, 6828 insertions(+), 10777 deletions(-) create mode 100644 config/evidence/local/.gitkeep create mode 100644 docker/registry-stack-runtime/Dockerfile create mode 100644 evidence/Caddyfile create mode 100644 evidence/bundle/adapters/extract.rhai create mode 100644 evidence/bundle/adapters/prepare.rhai create mode 100644 evidence/bundle/derivations/cra-child-benefit.rhai create mode 100644 evidence/bundle/derivations/cra-deceased.rhai create mode 100644 evidence/bundle/derivations/cra-linked.rhai create mode 100644 evidence/bundle/derivations/livestock.rhai create mode 100644 evidence/bundle/derivations/not-enrolled.rhai create mode 100644 evidence/bundle/derivations/pension-active.rhai create mode 100644 evidence/bundle/derivations/population-active.rhai create mode 100644 evidence/bundle/derivations/poverty-priority.rhai create mode 100644 evidence/bundle/derivations/survivor-eligible.rhai create mode 100644 evidence/bundle/derivations/voucher.rhai create mode 100644 evidence/bundle/evidence.yaml create mode 100644 evidence/bundle/fixtures/cra-child-benefit.yaml create mode 100644 evidence/bundle/fixtures/cra-citizen.yaml create mode 100644 evidence/bundle/fixtures/cra-pension.yaml create mode 100644 evidence/bundle/fixtures/mosd-child-benefit.yaml create mode 100644 evidence/bundle/fixtures/nagdi-livestock.yaml create mode 100644 evidence/bundle/fixtures/nagdi-voucher.yaml create mode 100644 evidence/bundle/fixtures/nia-child-benefit.yaml create mode 100644 evidence/bundle/fixtures/nia-citizen.yaml create mode 100644 evidence/bundle/fixtures/sipf-pension.yaml create mode 100644 evidence/bundle/fixtures/sipf-survivor.yaml create mode 100644 evidence/bundle/fixtures/sro-child-benefit.yaml create mode 100644 evidence/bundle/schemas/adapter-parameters.schema.yaml create mode 100644 evidence/bundle/schemas/cra-child-benefit-facts.schema.yaml create mode 100644 evidence/bundle/schemas/cra-citizen-facts.schema.yaml create mode 100644 evidence/bundle/schemas/cra-pension-facts.schema.yaml create mode 100644 evidence/bundle/schemas/mosd-child-benefit-facts.schema.yaml create mode 100644 evidence/bundle/schemas/nagdi-livestock-facts.schema.yaml create mode 100644 evidence/bundle/schemas/nagdi-voucher-facts.schema.yaml create mode 100644 evidence/bundle/schemas/nia-population-facts.schema.yaml create mode 100644 evidence/bundle/schemas/response.schema.yaml create mode 100644 evidence/bundle/schemas/sipf-pension-facts.schema.yaml create mode 100644 evidence/bundle/schemas/sipf-survivor-facts.schema.yaml create mode 100644 evidence/bundle/schemas/sro-child-benefit-facts.schema.yaml create mode 100644 evidence/mint.yaml create mode 100644 evidence/runtime.yaml delete mode 100644 runtime/registry-projects/hosted/cra-civil/notary/notary.yaml create mode 100644 runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json create mode 100644 runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json rename runtime/registry-projects/hosted/cra-civil/{relay => relay-consultation}/artifacts/evidence/civil-person-by-uin/minimization.json (100%) rename runtime/registry-projects/hosted/cra-civil/{relay => relay-consultation}/artifacts/evidence/civil-person-by-uin/negative-security.json (100%) rename runtime/registry-projects/hosted/cra-civil/{relay => relay-consultation}/artifacts/integration-packs/civil-person-by-uin.json (93%) rename runtime/registry-projects/hosted/cra-civil/{relay/artifacts/private-bindings/cra-child-benefit-civil.json => relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json} (67%) rename runtime/registry-projects/hosted/cra-civil/{relay/relay-consultation.yaml => relay-consultation/relay.yaml} (51%) delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-child-benefit-civil.json delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-citizen-record-civil.json delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-pension-death-civil.json delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay/artifacts/evidence/civil-person-by-uin/conformance.json delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-pension-death-civil.json delete mode 100644 runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml create mode 100644 runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json create mode 100644 runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json rename runtime/registry-projects/hosted/mosd-programme/{relay => relay-consultation}/artifacts/evidence/enrollment-by-uin/minimization.json (100%) rename runtime/registry-projects/hosted/mosd-programme/{relay => relay-consultation}/artifacts/evidence/enrollment-by-uin/negative-security.json (100%) rename runtime/registry-projects/hosted/mosd-programme/{relay => relay-consultation}/artifacts/integration-packs/enrollment-by-uin.json (92%) rename runtime/registry-projects/{local/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json => hosted/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json} (62%) rename runtime/registry-projects/hosted/mosd-programme/{relay/relay-consultation.yaml => relay-consultation/relay.yaml} (52%) delete mode 100644 runtime/registry-projects/hosted/mosd-programme/relay/artifacts/consultation-contracts/child-benefit-enrollment.json delete mode 100644 runtime/registry-projects/hosted/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/conformance.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml create mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json create mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json create mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json rename runtime/registry-projects/hosted/nagdi-agriculture/{relay => relay-consultation}/artifacts/evidence/farmer-voucher-by-farmer/minimization.json (100%) rename runtime/registry-projects/hosted/nagdi-agriculture/{relay => relay-consultation}/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json (100%) create mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json rename runtime/registry-projects/hosted/nagdi-agriculture/{relay => relay-consultation}/artifacts/evidence/livestock-movement-by-farmer/minimization.json (100%) rename runtime/registry-projects/hosted/nagdi-agriculture/{relay => relay-consultation}/artifacts/evidence/livestock-movement-by-farmer/negative-security.json (100%) rename runtime/registry-projects/hosted/nagdi-agriculture/{relay => relay-consultation}/artifacts/integration-packs/farmer-voucher-by-farmer.json (95%) rename runtime/registry-projects/{local/nagdi-agriculture/relay => hosted/nagdi-agriculture/relay-consultation}/artifacts/integration-packs/livestock-movement-by-farmer.json (94%) rename runtime/registry-projects/hosted/nagdi-agriculture/{relay/artifacts/private-bindings/voucher-farmer.json => relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json} (71%) rename runtime/registry-projects/hosted/nagdi-agriculture/{relay/artifacts/private-bindings/livestock-herd.json => relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json} (71%) create mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/consultation-contracts/livestock-herd.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/consultation-contracts/voucher-farmer.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/conformance.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/conformance.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay/relay-consultation.yaml delete mode 100644 runtime/registry-projects/hosted/nia-population/notary/notary.yaml create mode 100644 runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json create mode 100644 runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json rename runtime/registry-projects/hosted/nia-population/{relay => relay-consultation}/artifacts/evidence/population-person-by-uin/minimization.json (100%) rename runtime/registry-projects/hosted/nia-population/{relay => relay-consultation}/artifacts/evidence/population-person-by-uin/negative-security.json (100%) rename runtime/registry-projects/{local/nia-population/relay => hosted/nia-population/relay-consultation}/artifacts/integration-packs/population-person-by-uin.json (93%) rename runtime/registry-projects/hosted/nia-population/{relay/artifacts/private-bindings/nia-child-benefit-population.json => relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json} (60%) rename runtime/registry-projects/hosted/nia-population/{relay/relay-consultation.yaml => relay-consultation/relay.yaml} (83%) delete mode 100644 runtime/registry-projects/hosted/nia-population/relay/artifacts/consultation-contracts/nia-child-benefit-population.json delete mode 100644 runtime/registry-projects/hosted/nia-population/relay/artifacts/consultation-contracts/nia-citizen-status-population.json delete mode 100644 runtime/registry-projects/hosted/nia-population/relay/artifacts/evidence/population-person-by-uin/conformance.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml create mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json create mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json create mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json rename runtime/registry-projects/hosted/sipf-pensions/{relay => relay-consultation}/artifacts/evidence/pension-case-by-pensioner/minimization.json (100%) rename runtime/registry-projects/hosted/sipf-pensions/{relay => relay-consultation}/artifacts/evidence/pension-case-by-pensioner/negative-security.json (100%) create mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json rename runtime/registry-projects/hosted/sipf-pensions/{relay => relay-consultation}/artifacts/evidence/pension-case-by-survivor/minimization.json (100%) rename runtime/registry-projects/hosted/sipf-pensions/{relay => relay-consultation}/artifacts/evidence/pension-case-by-survivor/negative-security.json (100%) rename runtime/registry-projects/hosted/sipf-pensions/{relay => relay-consultation}/artifacts/integration-packs/pension-case-by-pensioner.json (92%) rename runtime/registry-projects/hosted/sipf-pensions/{relay => relay-consultation}/artifacts/integration-packs/pension-case-by-survivor.json (92%) rename runtime/registry-projects/hosted/sipf-pensions/{relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json => relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json} (61%) rename runtime/registry-projects/{local/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json => hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json} (67%) create mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/conformance.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/conformance.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay/relay-consultation.yaml delete mode 100644 runtime/registry-projects/hosted/sro-social/notary/notary.yaml create mode 100644 runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json create mode 100644 runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json rename runtime/registry-projects/hosted/sro-social/{relay => relay-consultation}/artifacts/evidence/child-benefit-household-by-uin/minimization.json (100%) rename runtime/registry-projects/hosted/sro-social/{relay => relay-consultation}/artifacts/evidence/child-benefit-household-by-uin/negative-security.json (100%) rename runtime/registry-projects/hosted/sro-social/{relay => relay-consultation}/artifacts/integration-packs/child-benefit-household-by-uin.json (92%) rename runtime/registry-projects/{local/sro-social/relay/artifacts/private-bindings/child-benefit-household.json => hosted/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json} (63%) rename runtime/registry-projects/hosted/sro-social/{relay/relay-consultation.yaml => relay-consultation/relay.yaml} (53%) delete mode 100644 runtime/registry-projects/hosted/sro-social/relay/artifacts/consultation-contracts/child-benefit-household.json delete mode 100644 runtime/registry-projects/hosted/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/conformance.json delete mode 100644 runtime/registry-projects/local/cra-civil/notary/notary.yaml create mode 100644 runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json create mode 100644 runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json rename runtime/registry-projects/local/cra-civil/{relay => relay-consultation}/artifacts/evidence/civil-person-by-uin/minimization.json (100%) rename runtime/registry-projects/local/cra-civil/{relay => relay-consultation}/artifacts/evidence/civil-person-by-uin/negative-security.json (100%) rename runtime/registry-projects/local/cra-civil/{relay => relay-consultation}/artifacts/integration-packs/civil-person-by-uin.json (93%) rename runtime/registry-projects/{hosted/cra-civil/relay/artifacts/private-bindings/cra-citizen-record-civil.json => local/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json} (67%) rename runtime/registry-projects/local/cra-civil/{relay/relay-consultation.yaml => relay-consultation/relay.yaml} (51%) delete mode 100644 runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-child-benefit-civil.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-citizen-record-civil.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-pension-death-civil.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay/artifacts/evidence/civil-person-by-uin/conformance.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-child-benefit-civil.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-citizen-record-civil.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-pension-death-civil.json delete mode 100644 runtime/registry-projects/local/mosd-programme/notary/notary.yaml create mode 100644 runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json create mode 100644 runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json rename runtime/registry-projects/local/mosd-programme/{relay => relay-consultation}/artifacts/evidence/enrollment-by-uin/minimization.json (100%) rename runtime/registry-projects/local/mosd-programme/{relay => relay-consultation}/artifacts/evidence/enrollment-by-uin/negative-security.json (100%) rename runtime/registry-projects/local/mosd-programme/{relay => relay-consultation}/artifacts/integration-packs/enrollment-by-uin.json (92%) rename runtime/registry-projects/{hosted/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json => local/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json} (62%) rename runtime/registry-projects/local/mosd-programme/{relay/relay-consultation.yaml => relay-consultation/relay.yaml} (51%) delete mode 100644 runtime/registry-projects/local/mosd-programme/relay/artifacts/consultation-contracts/child-benefit-enrollment.json delete mode 100644 runtime/registry-projects/local/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/conformance.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml create mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json create mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json create mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json rename runtime/registry-projects/local/nagdi-agriculture/{relay => relay-consultation}/artifacts/evidence/farmer-voucher-by-farmer/minimization.json (100%) rename runtime/registry-projects/local/nagdi-agriculture/{relay => relay-consultation}/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json (100%) create mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json rename runtime/registry-projects/local/nagdi-agriculture/{relay => relay-consultation}/artifacts/evidence/livestock-movement-by-farmer/minimization.json (100%) rename runtime/registry-projects/local/nagdi-agriculture/{relay => relay-consultation}/artifacts/evidence/livestock-movement-by-farmer/negative-security.json (100%) rename runtime/registry-projects/local/nagdi-agriculture/{relay => relay-consultation}/artifacts/integration-packs/farmer-voucher-by-farmer.json (95%) rename runtime/registry-projects/{hosted/nagdi-agriculture/relay => local/nagdi-agriculture/relay-consultation}/artifacts/integration-packs/livestock-movement-by-farmer.json (94%) rename runtime/registry-projects/local/nagdi-agriculture/{relay/artifacts/private-bindings/voucher-farmer.json => relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json} (71%) rename runtime/registry-projects/local/nagdi-agriculture/{relay/artifacts/private-bindings/livestock-herd.json => relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json} (71%) create mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/consultation-contracts/livestock-herd.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/consultation-contracts/voucher-farmer.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/conformance.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/conformance.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay/relay-consultation.yaml delete mode 100644 runtime/registry-projects/local/nia-population/notary/notary.yaml create mode 100644 runtime/registry-projects/local/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json create mode 100644 runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json rename runtime/registry-projects/local/nia-population/{relay => relay-consultation}/artifacts/evidence/population-person-by-uin/minimization.json (100%) rename runtime/registry-projects/local/nia-population/{relay => relay-consultation}/artifacts/evidence/population-person-by-uin/negative-security.json (100%) rename runtime/registry-projects/{hosted/nia-population/relay => local/nia-population/relay-consultation}/artifacts/integration-packs/population-person-by-uin.json (93%) rename runtime/registry-projects/{hosted/nia-population/relay/artifacts/private-bindings/nia-citizen-status-population.json => local/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json} (60%) rename runtime/registry-projects/local/nia-population/{relay/relay-consultation.yaml => relay-consultation/relay.yaml} (82%) delete mode 100644 runtime/registry-projects/local/nia-population/relay/artifacts/consultation-contracts/nia-child-benefit-population.json delete mode 100644 runtime/registry-projects/local/nia-population/relay/artifacts/consultation-contracts/nia-citizen-status-population.json delete mode 100644 runtime/registry-projects/local/nia-population/relay/artifacts/evidence/population-person-by-uin/conformance.json delete mode 100644 runtime/registry-projects/local/nia-population/relay/artifacts/private-bindings/nia-child-benefit-population.json delete mode 100644 runtime/registry-projects/local/nia-population/relay/artifacts/private-bindings/nia-citizen-status-population.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/notary/notary.yaml create mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json create mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json create mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json rename runtime/registry-projects/local/sipf-pensions/{relay => relay-consultation}/artifacts/evidence/pension-case-by-pensioner/minimization.json (100%) rename runtime/registry-projects/local/sipf-pensions/{relay => relay-consultation}/artifacts/evidence/pension-case-by-pensioner/negative-security.json (100%) create mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json rename runtime/registry-projects/local/sipf-pensions/{relay => relay-consultation}/artifacts/evidence/pension-case-by-survivor/minimization.json (100%) rename runtime/registry-projects/local/sipf-pensions/{relay => relay-consultation}/artifacts/evidence/pension-case-by-survivor/negative-security.json (100%) rename runtime/registry-projects/local/sipf-pensions/{relay => relay-consultation}/artifacts/integration-packs/pension-case-by-pensioner.json (92%) rename runtime/registry-projects/local/sipf-pensions/{relay => relay-consultation}/artifacts/integration-packs/pension-case-by-survivor.json (92%) rename runtime/registry-projects/local/sipf-pensions/{relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json => relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json} (61%) rename runtime/registry-projects/{hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json => local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json} (67%) create mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/conformance.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/conformance.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay/relay-consultation.yaml delete mode 100644 runtime/registry-projects/local/sro-social/notary/notary.yaml create mode 100644 runtime/registry-projects/local/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json create mode 100644 runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json rename runtime/registry-projects/local/sro-social/{relay => relay-consultation}/artifacts/evidence/child-benefit-household-by-uin/minimization.json (100%) rename runtime/registry-projects/local/sro-social/{relay => relay-consultation}/artifacts/evidence/child-benefit-household-by-uin/negative-security.json (100%) rename runtime/registry-projects/local/sro-social/{relay => relay-consultation}/artifacts/integration-packs/child-benefit-household-by-uin.json (92%) rename runtime/registry-projects/{hosted/sro-social/relay/artifacts/private-bindings/child-benefit-household.json => local/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json} (63%) rename runtime/registry-projects/local/sro-social/{relay/relay-consultation.yaml => relay-consultation/relay.yaml} (52%) delete mode 100644 runtime/registry-projects/local/sro-social/relay/artifacts/consultation-contracts/child-benefit-household.json delete mode 100644 runtime/registry-projects/local/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/conformance.json create mode 100755 scripts/build-registry-stack-runtime.sh create mode 100755 scripts/check-evidence-runtime.py create mode 100755 scripts/init-registry-postgresql.sh create mode 100644 scripts/test_gen_secrets.py diff --git a/.env.example b/.env.example index 8ba82c1..2486e92 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,8 @@ -# Generate .env with `just gen-secrets`. Values here are placeholders only. +# Generate .env and the ignored Evidence/Mint/TLS files with `just gen-secrets`. +# Values here are placeholders only. COMPOSE_PROJECT_NAME=solmara-lab-replace-me -SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS=4102444800000 -SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS=2592000000 -REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD= -REGISTRY_ESIGNET_KYC_TOKEN_SECRET= -REGISTRY_ESIGNET_PSUT_SECRET= - SOLMARA_POSTGRES_USER=solmara_registry SOLMARA_POSTGRES_PASSWORD= SOLMARA_POSTGRES_DB=solmara_lab @@ -15,80 +10,14 @@ NIA_SOURCE_POSTGRES_READER_PASSWORD= SIPF_SOURCE_POSTGRES_READER_PASSWORD= SOLMARA_NIA_DATABASE_URL=postgres://solmara_source_nia_reader:replace-me@postgres:5432/solmara_lab?sslmode=require SOLMARA_SIPF_DATABASE_URL=postgres://solmara_source_sipf_reader:replace-me@postgres:5432/solmara_lab?sslmode=require -SOLMARA_ESIGNET_POSTGRES_PASSWORD= CRA_RELAY_AUDIT_HASH_SECRET= -CRA_RELAY_AUDIT_PSEUDONYM_EPOCH_1= -CRA_RELAY_POSTGRES_RUNTIME_PASSWORD= -CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD= -CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD= -CRA_NOTARY_AUDIT_HASH_SECRET= -CRA_NOTARY_POSTGRES_MIGRATOR_PASSWORD= -CRA_NOTARY_POSTGRES_RUNTIME_PASSWORD= NIA_RELAY_AUDIT_HASH_SECRET= -NIA_RELAY_AUDIT_PSEUDONYM_EPOCH_1= -NIA_RELAY_POSTGRES_RUNTIME_PASSWORD= -NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD= -NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD= -NIA_NOTARY_AUDIT_HASH_SECRET= -NIA_NOTARY_POSTGRES_MIGRATOR_PASSWORD= -NIA_NOTARY_POSTGRES_RUNTIME_PASSWORD= SRO_RELAY_AUDIT_HASH_SECRET= -SRO_RELAY_AUDIT_PSEUDONYM_EPOCH_1= -SRO_RELAY_POSTGRES_RUNTIME_PASSWORD= -SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD= -SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD= -SRO_NOTARY_AUDIT_HASH_SECRET= -SRO_NOTARY_POSTGRES_MIGRATOR_PASSWORD= -SRO_NOTARY_POSTGRES_RUNTIME_PASSWORD= PROGRAMME_RELAY_AUDIT_HASH_SECRET= -PROGRAMME_RELAY_AUDIT_PSEUDONYM_EPOCH_1= -PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD= -PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD= -PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD= -PROGRAMME_NOTARY_AUDIT_HASH_SECRET= -PROGRAMME_NOTARY_POSTGRES_MIGRATOR_PASSWORD= -PROGRAMME_NOTARY_POSTGRES_RUNTIME_PASSWORD= SIPF_RELAY_AUDIT_HASH_SECRET= -SIPF_RELAY_AUDIT_PSEUDONYM_EPOCH_1= -SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD= -SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD= -SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD= -SIPF_NOTARY_AUDIT_HASH_SECRET= -SIPF_NOTARY_POSTGRES_MIGRATOR_PASSWORD= -SIPF_NOTARY_POSTGRES_RUNTIME_PASSWORD= NAGDI_RELAY_AUDIT_HASH_SECRET= -NAGDI_RELAY_AUDIT_PSEUDONYM_EPOCH_1= -NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD= -NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD= -NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD= -NAGDI_NOTARY_AUDIT_HASH_SECRET= -NAGDI_NOTARY_POSTGRES_MIGRATOR_PASSWORD= -NAGDI_NOTARY_POSTGRES_RUNTIME_PASSWORD= - -CHILD_BENEFIT_FEDERATOR_TOKEN= -CRA_CHILD_BENEFIT_CLIENT_TOKEN= -CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH= -NIA_CHILD_BENEFIT_CLIENT_TOKEN= -NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH= -SRO_CHILD_BENEFIT_CLIENT_TOKEN= -SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH= -PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN= -PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH= -CRA_PENSION_CLIENT_TOKEN= -CRA_PENSION_CLIENT_TOKEN_HASH= -CRA_CITIZEN_CLIENT_TOKEN= -CRA_CITIZEN_CLIENT_TOKEN_HASH= -NIA_CITIZEN_CLIENT_TOKEN= -NIA_CITIZEN_CLIENT_TOKEN_HASH= -SIPF_PENSION_CLIENT_TOKEN= -SIPF_PENSION_CLIENT_TOKEN_HASH= -NAGDI_NOTARY_TOKEN= -NAGDI_CLIENT_TOKEN_HASH= -NIA_NOTARY_ISSUER_JWK= -SIPF_NOTARY_ISSUER_JWK= -NAGDI_NOTARY_ISSUER_JWK= CRA_RELAY_WORKLOAD_JWK= NIA_RELAY_WORKLOAD_JWK= NIA_ESIGNET_RELAY_WORKLOAD_JWK= @@ -96,8 +25,23 @@ SRO_RELAY_WORKLOAD_JWK= PROGRAMME_RELAY_WORKLOAD_JWK= SIPF_RELAY_WORKLOAD_JWK= NAGDI_RELAY_WORKLOAD_JWK= + +CHILD_BENEFIT_FEDERATOR_TOKEN= +CHILD_BENEFIT_FEDERATOR_URL=https://localhost:4341/child-benefit/ +SOLMARA_EVIDENCE_URL=https://localhost:4341 +SOLMARA_MINT_URL=https://localhost:4341 +SOLMARA_MINT_ASSERTION_AUDIENCE=https://mint.evidence.solmara.invalid/token +SOLMARA_EVIDENCE_CLIENT_ID=solmara-demo +SOLMARA_EVIDENCE_CLIENT_KEY=/absolute/path/to/solmara-lab/config/evidence/local/mint/client-private.jwk +SOLMARA_EVIDENCE_CA_BUNDLE=/absolute/path/to/solmara-lab/config/evidence/local/tls/ca.crt PORTAL_SESSION_SECRET= PORTAL_AUTH_PROVIDER=mock + +# Optional local eSignet profile. +SOLMARA_ESIGNET_POSTGRES_PASSWORD= +REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD= +REGISTRY_ESIGNET_KYC_TOKEN_SECRET= +REGISTRY_ESIGNET_PSUT_SECRET= PORTAL_ESIGNET_CLIENT_ID=solmara-portal PORTAL_ESIGNET_CLIENT_KEY_ID=solmara-portal-key-1 PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64= @@ -109,27 +53,5 @@ PORTAL_ESIGNET_USERINFO_ENDPOINT=http://esignet:8088/v1/esignet/oidc/userinfo PORTAL_ESIGNET_REDIRECT_URI=http://127.0.0.1:4300/auth/callback PORTAL_ESIGNET_SCOPE=openid profile PORTAL_ESIGNET_SUBJECT_CLAIM=individual_id - -CHILD_BENEFIT_FEDERATOR_URL=http://127.0.0.1:4321 -CRA_NOTARY_URL=http://127.0.0.1:4325 -NIA_NOTARY_URL=http://127.0.0.1:4326 -SRO_NOTARY_URL=http://127.0.0.1:4327 -PROGRAMME_NOTARY_URL=http://127.0.0.1:4328 -SIPF_NOTARY_URL=http://127.0.0.1:4322 -NAGDI_NOTARY_URL=http://127.0.0.1:4323 -PORTAL_URL=http://127.0.0.1:4300 -SCENARIO_RUNNER_URL=http://127.0.0.1:4302 -STATIC_METADATA_URL=http://127.0.0.1:4331 -SOLMARA_ESIGNET_PORT=4308 -SOLMARA_ESIGNET_UI_PORT=4309 SOLMARA_ESIGNET_PUBLIC_BASE_URL=http://127.0.0.1:4308 SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL=http://127.0.0.1:4309 - -# Hosted Walt holder wallet. -CONFIG_REPO_REF=main -WALT_DB_PASSWORD= -WALT_AUTH_ENCRYPTION_KEY= -WALT_AUTH_SIGN_KEY= -WALT_AUTH_TOKEN_KEY= -WALT_KTOR_SIGNING_KEY= -WALT_KTOR_VERIFICATION_KEY= diff --git a/.gitignore b/.gitignore index 264ab1e..8e0a2c6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ projects/*/.registry-stack/ *.log config/postgres/ssl/* !config/postgres/ssl/.gitkeep +config/evidence/local/* +!config/evidence/local/.gitkeep diff --git a/README.md b/README.md index 5ad6d1d..734c2b8 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ # Solmara Lab -Solmara Lab is a standalone Registry Stack adopter demo for the fictional -Republic of Solmara. It replaces the old monorepo lab with one coherent country -story, generated synthetic data, purpose-limited Relay APIs, Notary evidence -services, and a citizen portal wired to the real local stack. +Solmara Lab is a Registry Stack adopter demo for the fictional Republic of +Solmara. Its local runtime uses the current Records API Relay, Registry +Evidence, and Registry Mint architecture. Notary is no longer part of the +local topology. -The lab uses published Registry Stack images pinned by digest. A local -`registry-stack` checkout is useful for development, but it is not required for -normal generation, live smoke tests, portal e2e, or hosted deployment. +Relay, Evidence, and Mint are not yet released. This branch builds all three +images from the exact Registry Stack `main` commit recorded in `versions.env`. +The build refuses a sibling checkout whose local `main` or `origin/main` does +not match that commit. -## Quick Start +## Quick start -From this repository: +Prerequisites are Docker with Compose, Rust, `just`, `uv`, `pnpm`, and a +Registry Stack checkout at `../registry-stack`. ```bash just setup @@ -21,216 +23,130 @@ just portal-live-e2e just down ``` -`just up`, `just down`, and `just reset` use a checkout-specific Docker Compose -project name by default so two local clones do not share containers or volumes. -Use `just down` to stop services while keeping local data. Use `just reset` only -when you intend to delete this checkout's Compose volumes. - -`up-generated` is the single clean-checkout generate/start journey. It creates -the synthetic fixtures and local secrets, regenerates every authority's Relay -and Notary closure with the real `registryctl` version pinned in `versions.env`, -compares those closures with the committed runtime, and starts the topology. -If the exact tool version is not installed, the helper downloads the matching -release binary and verifies it against the release SHA-256 file. - -`registry-projects-runtime-check` can run the compiler comparison without -starting services. The project wrapper consumes Registryctl's versioned JSON -build report and validates its project-owned output root rather than depending -on Registryctl's private build-directory layout. Its fixture gate also -consumes the versioned test report and requires independently authored -request-to-consultation evidence for every reachable target. Integration-only -fixtures are not accepted as caller compatibility proof. -`contract-generation-proof` is a separate release gate for one bounded SRO -authority pair. It compiles a harmless successor, proves the blue pair works, -rejects a mixed Relay/Notary generation before Relay execution or source -dispatch, activates the complete successor, and proves it works. Its temporary -Compose project and volumes are removed when the check finishes. - -The first wave covers three journeys: - -- Birth to child benefit. -- Death to pension stop plus survivor benefit. -- Farmer climate-smart voucher and livestock movement control. - -## Repository Map - -- `docs/` contains the Solmara world bible, purpose catalogue, naming record, - and story docs. -- `generator/` owns deterministic truth tables, per-registry projections, and - generated fixture checks. -- `geo/` contains the hand-authored Solmara geometry source used by the - generator. -- `ministries/` contains authority-owned source fixtures, manifest fragments, - and crosswalks. -- `projects/` contains the six authority-owned Registry project sources. Each - project generates one Relay config and one Notary config under `runtime/`. -- `metadata/` assembles the multi-authority metadata publication. -- `portal/` contains the citizen portal and BFF. -- `scenarios/`, `requests/`, and `perf/` carry guided scenarios, API examples, - and k6 smoke coverage. -- `scripts/` contains root quality gates and orchestration helpers. - -## Development Commands +`just up-generated` performs the clean-checkout journey: + +1. Generate deterministic Solmara fixtures and ignored local secrets. +2. Compile and compare all six Registry projects with the pinned Registryctl. +3. Build Relay, Evidence, and Mint from the exact Registry Stack source commit. +4. Build the Solmara services and start the local Compose topology. + +The first source build can take several minutes. Subsequent builds reuse +Docker layers. `just down` keeps local volumes; `just reset` removes this +checkout's volumes. + +Local entry points: + +- Visitor Center: `http://127.0.0.1:4301` +- Citizen portal: `http://127.0.0.1:4300` +- Evidence and Mint TLS gateway: `https://localhost:4341` +- Relay Records APIs: ports `4311` through `4316` +- Child-benefit application collector: `https://localhost:4341/child-benefit/` + +The generated local CA is +`config/evidence/local/tls/ca.crt`. Host-side scripts use it through the +generated `SOLMARA_EVIDENCE_CA_BUNDLE` setting. +Evidence, Mint, client, and CA keys are preserved across later +`just gen-secrets` runs so retained audit volumes remain verifiable. If that +material becomes partial, reset the local volumes before regenerating it. + +## Local architecture + +The six authority projects compile current Records API Relay configurations. +Each running Relay exposes only its public Records API. A workload identity +agent writes a short-lived, `solmara-evidence`-scoped bearer token into the +private Evidence secret volume. + +Registry Mint authenticates the Solmara application client with +`private_key_jwt`. It issues short-lived access tokens for the +`solmara-evidence` audience. Registry Evidence validates those tokens, applies +one of eleven reviewed requirements, calls the necessary Relay Records API, +and returns a flattened signed JWS assertion containing only the approved +concept values. + +Application code can combine multiple independently signed Evidence responses +for a workflow decision. Evidence itself does not own child-benefit, pension, +farmer, or citizen application decisions. + +The current Registryctl compiler still emits both public and consultation +Relay lanes for authored projects. Both generated lanes are committed under +`runtime/registry-projects/` for deterministic compiler review. Only the public +Records API lane runs in the local Compose topology. + +## Source binding + +`versions.env` records: + +- `REGISTRY_STACK_SOURCE_REF=main` +- the exact 40-character `REGISTRY_STACK_SOURCE_COMMIT` +- the matching Registryctl version +- the three local source-image names +- digest-pinned third-party runtime images + +`scripts/build-registry-stack-runtime.sh` validates local and remote-tracking +`main`, creates a temporary detached worktree at the pinned commit, and builds: + +- `solmara-lab-registry-relay:source` +- `solmara-lab-registry-evidence:source` +- `solmara-lab-registry-mint:source` + +The images receive the exact source revision as an OCI label. `just up` and +`just up-esignet` always run this source validation and build step. + +## Verification + +```bash +just registry-projects-check # validate six projects in local and hosted authoring profiles +just registry-projects-test # run every Registry project fixture +just registry-projects-runtime-check # prove committed generated runtime has no drift +just evidence-check # Mint check plus 11 Evidence fixture files and 89 cases +just lint # repository, metadata, TypeScript, and image-reference checks +just test # active Python, portal, and Visitor Center tests +just compose # validate local and eSignet Compose profiles +just smoke # exercise a running Relay, Mint, Evidence, scenario, and portal stack +``` + +`just evidence-check` requires the exact `evidencectl` and `mint` versions +shown in `versions.env`, plus local material generated by `just gen-secrets`. +It stages an immutable temporary deployment, uses fixture-only Relay tokens, +and does not modify committed Evidence inputs. + +For eSignet-backed portal login: ```bash -just lint # static repo checks, including fiction lint -just test # generator, portal, and script tests when their projects exist -just compose # docker compose config validation -just smoke # story previews plus live Relay, Notary, and Compose portal checks -just smoke-live # live Notary checks only -just portal-compose-smoke # HTTP smoke against the Compose portal and live BFF -just portal-live-e2e # browser e2e against the running local stack -just hosted-smoke # public hosted health, endpoint, scenario, and portal checks -just up # local stack with mock portal login -just up-esignet # local stack with eSignet-backed portal login -just up-dev # explicit source-built Relay development stack -just up-esignet-dev # source-built Relay development stack with eSignet -just smoke-esignet # eSignet public discovery smoke -just down # stop the local Compose topology without deleting volumes -just reset # stop the local Compose topology and delete its volumes -just up-generated # clean-checkout generation, compiler comparison, and start -just registry-projects-runtime-check # regenerate and compare all authority runtime closures -just hosted-relay-bundles-check # verify hosted Relay signatures and config closure -just registry-projects-review # complete redacted acquisition and disclosure reports -just registry-projects-capabilities # value-free installed/used/missing capability inventory -just registry-projects-editor # version-matched VS Code and Zed schemas for all projects -just contract-generation-proof # release-only live SRO blue/mixed/successor proof -just release-pins # compare committed versions.env pins against a candidate or release tag -just review # normal security and release-readiness checks -just review-release # candidate review with published pin validation +just up-esignet +just smoke-esignet ``` -Normal startup pulls the immutable canonical Relay image pinned in -`versions.env`; it does not clone or compile Registry Stack. Governed attribute -release is part of the canonical Registry Stack v0.15.2 Relay build. The -`*-dev` recipes are the explicit source-build path. They verify the pinned -source commit and build the same default feature set into a separate local -image, leaving the standalone path unchanged. - -`just generate` rewrites generated fixtures. Review those diffs like any other -committed generated artifact. - -Each authority project commits Registryctl-generated schemas plus VS Code and -Zed mappings under its own project directory. Open that authority directory as -the editor workspace to get validation and completion for its project, -environment, integration, fixture, and entity YAML. Refresh all six with -`just registry-projects-editor` only after updating the pinned Registryctl -release. CI reruns the generator and fails on drift, so the editor contract -cannot silently move to a different Registry Stack version. - -## Image Pins - -`versions.env` is the root source for the published Registry Stack image -digests and the exact source ref and commit used for release binding and the -explicit Relay development build. The Registry Stack `v0.15.2` Relay and -Notary images are both consumed directly by digest. Solmara does not publish -or select a feature-specific Relay runtime. - -Use `just up` rather than invoking `docker compose up` directly so the -checkout-specific Compose project name and complete env-file set are applied. -Because the release publishes amd64 images, Compose defaults -`REGISTRY_STACK_PLATFORM` to `linux/amd64`; override it only when every -selected base image is available for another platform. - -Every authority exposes one public Relay and one Notary. A separate private -consultation Relay shares only the Notary network namespace and is never -published on the Relay endpoint. Relay consultation state and all Notary -correctness state are PostgreSQL-backed. `just gen-secrets` creates local -PostgreSQL TLS material and distinct runtime and migrator passwords for each -authority. See -[`docs/notary-postgresql-state.md`](docs/notary-postgresql-state.md) for the -database map, diagnosis, backup, recovery, and upgrade workflow. - -Local public and consultation Relay namespaces each have their own -loopback-only workload issuer. The consultation issuer writes the Notary token -to a private, authority-specific volume; duplicating the issuer process avoids -opening either Relay's loopback JWKS listener onto the shared Compose network. - -The `REGISTRY_RELAY_STATE_EPOCH=v015` pin starts a fresh Relay state plane for -the v0.15.2 cache-persistence cutover. Earlier hosted deployments persisted -PostgreSQL publication pointers but not the immutable snapshot files they -referenced, so they cannot safely reuse the `v013` databases after adopting -durable Relay cache volumes. Keep the old `v013` databases quiesced for -rollback. The PostgreSQL runbook describes the stopped-writer and rollback -boundary. - -## Hosted Deployment - -See [`docs/hosted-deployment.md`](docs/hosted-deployment.md) for the full -runbook. Coolify uses one hosted Compose file for the lab edge plus four -ministry-grouped authority applications: - -- `compose.coolify.yaml` for the Visitor Center, portal, scenario runner, - child-benefit evidence composition, and static metadata. -- `compose.coolify.interior.yaml` for the CRA and NIA Relay and Notary pairs - and their PostgreSQL databases. -- `compose.coolify.esignet.yaml` for eSignet, eSignet UI, and its backing - Postgres/Redis/seed services. -- `compose.coolify.social-development.yaml` for the SRO and Programme Relay - and Notary pairs and their PostgreSQL databases. -- `compose.coolify.labour-pensions.yaml` for the SIPF Relay and Notary pair and - its PostgreSQL databases. -- `compose.coolify.agriculture.yaml` for the NAgDI Relay and Notary pair and - its PostgreSQL databases. - -The hosted compose files remove host port bindings and avoid repo bind mounts -because Coolify does not seed bind-mount sources from the Git checkout. They do -not define custom Docker networks; cross-authority calls use the public -`*.solmara.registrystack.org` TLS endpoints. Authority compose files preserve -authority-owned PostgreSQL state, separate public and consultation Relay -caches, and workload credentials. Notary containers do not use Redis or a -writable state directory. - -Each hosted public Relay and private consultation Relay starts from its own -instance-bound signed Config Bundle and anti-rollback state. Only the -consultation bundle contains the private consultation artifacts. The wrapper -contains public trust anchors and signed closures only; the offline signing key -is not committed. A sequence-zero baseline is copied only when the matching -Relay state volume is empty, allowing first boot while keeping later bundle -sequence rollback protection durable in that volume. - -Hosted workload agents keep Relay bearer credentials short-lived and confined -to per-consumer volumes. The separately served workload JWKS contains public -keys only; private workload JWKs remain Coolify secrets. - -Run `just registry-projects-sync` after editing an authority project, then -`just registry-projects-runtime-check` to verify the local and hosted Relay and -Notary closures are deterministic. - -Run `just hosted-smoke` after each hosted deploy from a trusted shell with the -demo tokens available in `.env` or the process environment. It checks public -routes, Relay source endpoints, Notary scenario evaluations, published-token -refusals, the Visitor Center scenario proxy, and the portal live BFF. Add -`SOLMARA_HOSTED_SMOKE_BROWSER=1` when you also want hosted Playwright coverage -for the Visitor Center and portal. - -The `release-candidate` workflow verifies the pinned Registry Stack source and -uses the canonical published Relay digest as the base for the hosted Relay -wrapper. A Solmara candidate does not recompile Registry Stack. The workflow -builds the Solmara-owned images and writes their digest refs to the workflow -summary for Coolify env vars: -`SOLMARA_RELAY_IMAGE`, `SOLMARA_NOTARY_IMAGE`, `SOLMARA_POSTGRES_IMAGE`, -`SOLMARA_STATIC_METADATA_IMAGE`, `SOLMARA_HOME_IMAGE`, -`SOLMARA_PORTAL_IMAGE`, `SOLMARA_SCENARIO_RUNNER_IMAGE`, -`SOLMARA_ESIGNET_RELAY_IMAGE`, `SOLMARA_ESIGNET_POSTGRES_IMAGE`, -`SOLMARA_ESIGNET_UI_IMAGE`, and `SOLMARA_ESIGNET_SEED_IMAGE`. - -Its manually supplied Registry Stack tag is required and must resolve to the -same Relay and Notary digests committed in `versions.env`. Run the same -candidate-only gate locally with `just review-release `; -the normal contributor and CI gate remains `just review`. - -For local eSignet testing, run `just up-esignet` instead of `just up`, then -sign in through the portal with Elena's fixture UIN `2300018263` and static -OTP `111111`. This sign-in is the end-to-end check of the NIA -`solmara-nia-userinfo` attribute-release profile and its rotating eSignet -workload identity. Run `just smoke-esignet` for the public discovery checks. - -Set `UMAMI_WEBSITE_ID` in the hosted environment to enable analytics for the -Visitor Center through the Registry Stack Umami instance. - -## Privacy Rules - -Solmara data is synthetic. Do not use real people, real email domains, real -addresses, or real administrative geography. Use `@mail.solmara.example` for -emails and keep all story domains under `gov.solmara.example`. +Use Elena's synthetic UIN `2300018263` and static OTP `111111`. + +## Repository map + +- `projects/` contains six authority-owned Registry project sources. +- `runtime/registry-projects/` contains deterministic Registryctl output. +- `evidence/` contains the Evidence bundle, Mint config, TLS gateway config, + requirements, derivations, schemas, and fixtures. +- `ministries/` contains authority-owned synthetic source fixtures. +- `generator/` creates deterministic country data and projections. +- `scenarios/` and `scenario-runner/` implement the guided Evidence journeys. +- `portal/` contains the citizen portal and live scenario-runner adapter. +- `home/` contains the Visitor Center. +- `scripts/` contains source-build, generation, verification, and smoke helpers. + +## Hosted deployment status + +Hosted and Coolify Compose files still describe the previous released Notary +topology. They are intentionally outside this local migration and are not +supported by this branch. Do not deploy them with the generated runtime in +`runtime/registry-projects/`. + +A hosted migration should follow once Relay, Evidence, and Mint have published +artifacts and an explicit deployment design is agreed. Until then, the active +commands and verification targets in this README cover only the locally built +source topology. + +## Privacy + +All Solmara data is synthetic. Do not use real people, real email domains, +real addresses, or real administrative geography. Use +`@mail.solmara.example` for email addresses and keep story domains under +`gov.solmara.example`. diff --git a/compose.yaml b/compose.yaml index 535bcda..6a6021f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,45 +2,15 @@ x-relay-environment: &relay-environment RUST_LOG: ${RUST_LOG:-info} REGISTRY_RELAY_BIND: 0.0.0.0:8080 -x-notary-environment: ¬ary-environment - RUST_LOG: ${RUST_LOG:-info} - REGISTRY_NOTARY_BIND: 0.0.0.0:8081 - REGISTRY_NOTARY_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - x-relay-common: &relay-common - image: ${REGISTRY_RELAY_IMAGE:-ghcr.io/registrystack/registry-relay@sha256:71082176f00107e420ffe79471f0bd3375c3dd1ae1c3c8c3a24100c602fa82b7} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} + image: ${REGISTRY_RELAY_IMAGE:?versions.env must name the locally built Registry Stack main Relay image} restart: unless-stopped environment: *relay-environment healthcheck: test: ["CMD", "/usr/local/bin/registry-relay", "healthcheck"] - interval: 30s - timeout: 10s - retries: 3 - -x-notary-common: ¬ary-common - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:1cce41247a18f70cb7ff4276bdeb5a16c1e641a9ed7951be8f612daaf35ab87b} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - user: "65534:65534" - restart: unless-stopped - environment: *notary-environment - healthcheck: - test: - [ - "CMD", - "/usr/local/bin/registry-notary", - "healthcheck", - "--url", - "http://127.0.0.1:8081/ready", - ] - interval: 30s - timeout: 10s - retries: 3 - -x-relay-bootstrap-common: &relay-bootstrap-common - image: ${REGISTRY_RELAY_IMAGE:-ghcr.io/registrystack/registry-relay@sha256:71082176f00107e420ffe79471f0bd3375c3dd1ae1c3c8c3a24100c602fa82b7} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" + interval: 10s + timeout: 5s + retries: 12 x-workload-agent-common: &workload-agent-common build: @@ -51,9 +21,18 @@ x-workload-agent-common: &workload-agent-common restart: unless-stopped healthcheck: test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] - interval: 10s + interval: 5s timeout: 5s - retries: 6 + retries: 12 + +x-evidence-client-environment: &evidence-client-environment + SOLMARA_EVIDENCE_URL: https://evidence.solmara.invalid + SOLMARA_MINT_URL: https://mint.evidence.solmara.invalid + SOLMARA_MINT_ASSERTION_AUDIENCE: https://mint.evidence.solmara.invalid/token + SOLMARA_EVIDENCE_CLIENT_ID: solmara-demo + SOLMARA_EVIDENCE_CLIENT_KEY: /run/secrets/solmara-evidence-client.jwk + SOLMARA_EVIDENCE_CA_BUNDLE: /etc/solmara-evidence/tls/ca.crt + SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt services: postgres: @@ -67,46 +46,13 @@ services: - ssl_cert_file=/var/lib/postgresql/server-ssl/server.crt - -c - ssl_key_file=/var/lib/postgresql/server-ssl/server.key - environment: &postgres-environment + environment: POSTGRES_USER: ${SOLMARA_POSTGRES_USER:-solmara_registry} POSTGRES_PASSWORD: ${SOLMARA_POSTGRES_PASSWORD} POSTGRES_DB: ${SOLMARA_POSTGRES_DB:-solmara_lab} + SOLMARA_SOURCE_READER_DATABASES: nia sipf NIA_SOURCE_POSTGRES_READER_PASSWORD: ${NIA_SOURCE_POSTGRES_READER_PASSWORD} SIPF_SOURCE_POSTGRES_READER_PASSWORD: ${SIPF_SOURCE_POSTGRES_READER_PASSWORD} - REGISTRY_RELAY_STATE_EPOCH: ${REGISTRY_RELAY_STATE_EPOCH:-v015} - SOLMARA_RELAY_DATABASES: cra nia sro programme sipf nagdi - SOLMARA_NOTARY_DATABASES: cra nia sro programme sipf nagdi - SOLMARA_SOURCE_READER_DATABASES: nia sipf - CRA_RELAY_POSTGRES_RUNTIME_PASSWORD: ${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD} - CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - CRA_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${CRA_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - CRA_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${CRA_NOTARY_POSTGRES_RUNTIME_PASSWORD} - NIA_RELAY_POSTGRES_RUNTIME_PASSWORD: ${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD} - NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - NIA_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${NIA_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - NIA_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${NIA_NOTARY_POSTGRES_RUNTIME_PASSWORD} - SRO_RELAY_POSTGRES_RUNTIME_PASSWORD: ${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD} - SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - SRO_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${SRO_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - SRO_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${SRO_NOTARY_POSTGRES_RUNTIME_PASSWORD} - PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD: ${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD} - PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - PROGRAMME_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${PROGRAMME_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - PROGRAMME_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${PROGRAMME_NOTARY_POSTGRES_RUNTIME_PASSWORD} - SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD: ${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD} - SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - SIPF_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${SIPF_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - SIPF_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${SIPF_NOTARY_POSTGRES_RUNTIME_PASSWORD} - NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD: ${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD} - NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - NAGDI_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${NAGDI_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - NAGDI_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${NAGDI_NOTARY_POSTGRES_RUNTIME_PASSWORD} ports: - "${SOLMARA_POSTGRES_PORT:-54329}:5432" volumes: @@ -119,30 +65,15 @@ services: - ./ministries/labour-pensions/fixtures/003-schema.sql:/docker-entrypoint-initdb.d/003-schema.sql:ro - ./ministries/labour-pensions/fixtures/004-load.sql:/docker-entrypoint-initdb.d/004-load.sql:ro - ./ministries/labour-pensions/fixtures/pension_case.csv:/docker-entrypoint-initdb.d/pension_case.csv:ro - - ./scripts/init-notary-postgresql.sh:/docker-entrypoint-initdb.d/010-init-notary-postgresql.sh:ro + - ./scripts/init-registry-postgresql.sh:/usr/local/bin/solmara-provision-postgresql.sh:ro - ./scripts/postgres-entrypoint.sh:/usr/local/bin/solmara-postgres-entrypoint.sh:ro - ./config/postgres/ssl:/run/solmara-postgres-ssl:ro healthcheck: - test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + test: ["CMD-SHELL", "test -f /tmp/solmara-postgres-provisioned && pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 10 - registry-postgresql-bootstrap: - image: ${POSTGRES_IMAGE:-postgres:16-alpine} - restart: "no" - entrypoint: ["/bin/sh", "/docker-entrypoint-initdb.d/010-init-notary-postgresql.sh"] - environment: - <<: *postgres-environment - PGHOST: postgres - PGPASSWORD: ${SOLMARA_POSTGRES_PASSWORD} - PGSSLMODE: require - volumes: - - ./scripts/init-notary-postgresql.sh:/docker-entrypoint-initdb.d/010-init-notary-postgresql.sh:ro - depends_on: - postgres: - condition: service_healthy - static-metadata: image: ${STATIC_METADATA_IMAGE:-python:3.12-alpine} command: ["python", "-m", "http.server", "8080", "--bind", "0.0.0.0", "--directory", "/srv/static"] @@ -151,221 +82,13 @@ services: volumes: - ./metadata/public:/srv/static:ro - scenario-runner: - build: - context: . - dockerfile: scenario-runner/Dockerfile - image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:-solmara-lab-scenario-runner:local} - environment: - SCENARIO_RUNNER_HOST: 0.0.0.0 - SCENARIO_RUNNER_PORT: 8080 - SOLMARA_LAB_MODE: local - CHILD_BENEFIT_FEDERATOR_URL: http://child-benefit-federator:8080 - CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN} - CRA_NOTARY_URL: http://cra-notary:8081 - CRA_PENSION_CLIENT_TOKEN: ${CRA_PENSION_CLIENT_TOKEN} - CRA_CITIZEN_CLIENT_TOKEN: ${CRA_CITIZEN_CLIENT_TOKEN} - SIPF_NOTARY_URL: http://sipf-notary:8081 - SIPF_PENSION_CLIENT_TOKEN: ${SIPF_PENSION_CLIENT_TOKEN} - NIA_NOTARY_URL: http://nia-notary:8081 - NIA_CITIZEN_CLIENT_TOKEN: ${NIA_CITIZEN_CLIENT_TOKEN} - NAGDI_NOTARY_URL: http://nagdi-notary:8081 - NAGDI_NOTARY_TOKEN: ${NAGDI_NOTARY_TOKEN} - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3)\""] - interval: 30s - timeout: 10s - retries: 3 - depends_on: - child-benefit-federator: - condition: service_healthy - cra-notary: - condition: service_healthy - sipf-notary: - condition: service_healthy - nagdi-notary: - condition: service_healthy - nia-notary: - condition: service_healthy - - home: - build: - context: . - dockerfile: home/Dockerfile - image: ${SOLMARA_HOME_IMAGE:-solmara-lab-home:local} - environment: - HOST: 0.0.0.0 - PORT: 4301 - SOLMARA_LAB_ROOT: /app - REPO_URL: ${REPO_URL:-https://github.com/registrystack/solmara-lab} - PORTAL_URL: http://127.0.0.1:${SOLMARA_PORTAL_PORT:-4300} - SCENARIO_RUNNER_URL: http://scenario-runner:8080 - STATIC_METADATA_URL: http://static-metadata:8080 - # Compose-internal probe URLs for the trust-strip status grid. The home - # container is on the same network, so it can probe every service directly. - PORTAL_PROBE_URL: http://portal:4000 - CRA_CIVIL_RELAY_URL: http://cra-civil-relay:8080 - NIA_POPULATION_RELAY_URL: http://nia-population-relay:8080 - SRO_SOCIAL_RELAY_URL: http://sro-social-relay:8080 - PROGRAMME_MIS_RELAY_URL: http://programme-mis-relay:8080 - SIPF_PENSIONS_RELAY_URL: http://sipf-pensions-relay:8080 - NAGDI_AGRICULTURE_RELAY_URL: http://nagdi-agriculture-relay:8080 - CHILD_BENEFIT_FEDERATOR_URL: http://child-benefit-federator:8080 - CRA_NOTARY_URL: http://cra-notary:8081 - NIA_NOTARY_URL: http://nia-notary:8081 - SRO_NOTARY_URL: http://sro-notary:8081 - PROGRAMME_NOTARY_URL: http://programme-notary:8081 - SIPF_NOTARY_URL: http://sipf-notary:8081 - NAGDI_NOTARY_URL: http://nagdi-notary:8081 - # Deliberate server-side allowlist of synthetic demo tokens the engineer - # door publishes. Only names listed here are ever rendered; the values are - # the same synthetic notary tokens the scenarios use. Never add a token - # that is not meant to be public. - HOME_PUBLISHED_TOKENS: >- - {"child-benefit-federator":"${CHILD_BENEFIT_FEDERATOR_TOKEN}", - "cra-pension-client":"${CRA_PENSION_CLIENT_TOKEN}", - "cra-citizen-client":"${CRA_CITIZEN_CLIENT_TOKEN}", - "nia-citizen-client":"${NIA_CITIZEN_CLIENT_TOKEN}", - "sipf-pension-client":"${SIPF_PENSION_CLIENT_TOKEN}", - "nagdi-notary":"${NAGDI_NOTARY_TOKEN}"} - ports: - - "${SOLMARA_HOME_PORT:-4301}:4301" - volumes: - # Read-only mounts so the trust strip can read live smoke evidence written - # by `just smoke` on the host and the generated data-seed manifest. The - # image bakes a snapshot of the seed; these keep it current and expose the - # smoke artifacts that the image cannot contain. - - ./output/smoke:/app/output/smoke:ro - - ./generator/output:/app/generator/output:ro - healthcheck: - test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:4301/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] - interval: 30s - timeout: 10s - retries: 3 - depends_on: - static-metadata: - condition: service_started - scenario-runner: - condition: service_healthy - - cra-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay-consultation.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${CRA_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${CRA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - volumes: - - ./runtime/registry-projects/local/cra-civil/relay:/etc/registry-relay:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - - nia-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay-consultation.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NIA_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NIA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} - volumes: - - ./runtime/registry-projects/local/nia-population/relay:/etc/registry-relay:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - - sro-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay-consultation.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SRO_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SRO_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - volumes: - - ./runtime/registry-projects/local/sro-social/relay:/etc/registry-relay:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - - programme-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay-consultation.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${PROGRAMME_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - volumes: - - ./runtime/registry-projects/local/mosd-programme/relay:/etc/registry-relay:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - - sipf-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay-consultation.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SIPF_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SIPF_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} - volumes: - - ./runtime/registry-projects/local/sipf-pensions/relay:/etc/registry-relay:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - - nagdi-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay-consultation.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NAGDI_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NAGDI_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - volumes: - - ./runtime/registry-projects/local/nagdi-agriculture/relay:/etc/registry-relay:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - cra-civil-relay: <<: *relay-common command: ["--config", "/etc/registry-relay/relay.yaml"] environment: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${CRA_RELAY_AUDIT_HASH_SECRET} - ports: - - "${SOLMARA_CRA_RELAY_PORT:-4311}:8080" + ports: ["${SOLMARA_CRA_RELAY_PORT:-4311}:8080"] volumes: - ./runtime/registry-projects/local/cra-civil/relay:/etc/registry-relay:ro - ./ministries/interior-civil:/var/lib/solmara/ministries/interior-civil:ro @@ -379,12 +102,13 @@ services: REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NIA_RELAY_AUDIT_HASH_SECRET} SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - ports: - - "${SOLMARA_NIA_RELAY_PORT:-4312}:8080" + ports: ["${SOLMARA_NIA_RELAY_PORT:-4312}:8080"] volumes: - ./runtime/registry-projects/local/nia-population/relay:/etc/registry-relay:ro - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - nia-relay-cache:/var/lib/registry-relay/cache + depends_on: + postgres: { condition: service_healthy } sro-social-relay: <<: *relay-common @@ -392,8 +116,7 @@ services: environment: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SRO_RELAY_AUDIT_HASH_SECRET} - ports: - - "${SOLMARA_SRO_RELAY_PORT:-4313}:8080" + ports: ["${SOLMARA_SRO_RELAY_PORT:-4313}:8080"] volumes: - ./runtime/registry-projects/local/sro-social/relay:/etc/registry-relay:ro - ./ministries/social-development:/var/lib/solmara/ministries/social-development:ro @@ -405,8 +128,7 @@ services: environment: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} - ports: - - "${SOLMARA_PROGRAMME_RELAY_PORT:-4314}:8080" + ports: ["${SOLMARA_PROGRAMME_RELAY_PORT:-4314}:8080"] volumes: - ./runtime/registry-projects/local/mosd-programme/relay:/etc/registry-relay:ro - ./ministries/social-development:/var/lib/solmara/ministries/social-development:ro @@ -420,12 +142,13 @@ services: REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SIPF_RELAY_AUDIT_HASH_SECRET} SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - ports: - - "${SOLMARA_SIPF_RELAY_PORT:-4315}:8080" + ports: ["${SOLMARA_SIPF_RELAY_PORT:-4315}:8080"] volumes: - ./runtime/registry-projects/local/sipf-pensions/relay:/etc/registry-relay:ro - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - sipf-relay-cache:/var/lib/registry-relay/cache + depends_on: + postgres: { condition: service_healthy } nagdi-agriculture-relay: <<: *relay-common @@ -433,152 +156,20 @@ services: environment: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NAGDI_RELAY_AUDIT_HASH_SECRET} - ports: - - "${SOLMARA_NAGDI_RELAY_PORT:-4316}:8080" + ports: ["${SOLMARA_NAGDI_RELAY_PORT:-4316}:8080"] volumes: - ./runtime/registry-projects/local/nagdi-agriculture/relay:/etc/registry-relay:ro - ./ministries/agriculture-nagdi:/var/lib/solmara/ministries/agriculture-nagdi:ro - nagdi-relay-cache:/var/lib/registry-relay/cache - cra-civil-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay-consultation.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${CRA_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${CRA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - ports: - - "${SOLMARA_CRA_NOTARY_PORT:-4325}:8081" - networks: - default: - aliases: [cra-notary] - volumes: - - ./runtime/registry-projects/local/cra-civil/relay:/etc/registry-relay:ro - - ./ministries/interior-civil:/var/lib/solmara/ministries/interior-civil:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - cra-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - cra-relay-state-bootstrap: - condition: service_completed_successfully - - nia-population-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay-consultation.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NIA_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NIA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} - DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - ports: - - "${SOLMARA_NIA_NOTARY_PORT:-4326}:8081" - networks: - default: - aliases: [nia-notary] - volumes: - - ./runtime/registry-projects/local/nia-population/relay:/etc/registry-relay:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - nia-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - nia-relay-state-bootstrap: - condition: service_completed_successfully - - sro-social-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay-consultation.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SRO_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SRO_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - ports: - - "${SOLMARA_SRO_NOTARY_PORT:-4327}:8081" - networks: - default: - aliases: [sro-notary] - volumes: - - ./runtime/registry-projects/local/sro-social/relay:/etc/registry-relay:ro - - ./ministries/social-development:/var/lib/solmara/ministries/social-development:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - sro-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - sro-relay-state-bootstrap: - condition: service_completed_successfully - - programme-mis-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay-consultation.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${PROGRAMME_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - ports: - - "${SOLMARA_PROGRAMME_NOTARY_PORT:-4328}:8081" - networks: - default: - aliases: [programme-notary] - volumes: - - ./runtime/registry-projects/local/mosd-programme/relay:/etc/registry-relay:ro - - ./ministries/social-development:/var/lib/solmara/ministries/social-development:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - programme-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - programme-relay-state-bootstrap: - condition: service_completed_successfully - - sipf-pensions-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay-consultation.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SIPF_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SIPF_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} - DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - ports: - - "${SOLMARA_SIPF_NOTARY_PORT:-4322}:8081" - networks: - default: - aliases: [sipf-notary] - volumes: - - ./runtime/registry-projects/local/sipf-pensions/relay:/etc/registry-relay:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - sipf-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - sipf-relay-state-bootstrap: - condition: service_completed_successfully - - nagdi-agriculture-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay-consultation.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NAGDI_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NAGDI_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - ports: - - "${SOLMARA_NAGDI_NOTARY_PORT:-4323}:8081" - networks: - default: - aliases: [nagdi-notary] + evidence-secrets-init: + image: ${VOLUME_INIT_IMAGE:?versions.env must pin the volume init image} + restart: "no" + command: ["sh", "-ec", "cp /seed/audit-hmac-key /seed/subject-binding-hmac-key /seed/signing-ed25519-private-jwk /target/; chown -R 65532:65532 /target /audit; chmod 700 /target /audit; chmod 600 /target/*"] volumes: - - ./runtime/registry-projects/local/nagdi-agriculture/relay:/etc/registry-relay:ro - - ./ministries/agriculture-nagdi:/var/lib/solmara/ministries/agriculture-nagdi:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - nagdi-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - nagdi-relay-state-bootstrap: - condition: service_completed_successfully + - ./config/evidence/local/evidence:/seed:ro + - evidence-secrets:/target + - evidence-audit:/audit cra-workload-agent: <<: *workload-agent-common @@ -586,13 +177,12 @@ services: environment: WORKLOAD_ISSUER: http://127.0.0.1:8090 WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"cra-notary","subject":"cra-notary","scopes":["registry:consult:cra-child-benefit","registry:consult:cra-citizen-record","registry:consult:cra-pension-death"],"token_file":"/run/secrets/cra-notary-relay-token","private_jwk_env":"CRA_RELAY_WORKLOAD_JWK"}] + [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["civil_people:rows"],"token_file":"/run/evidence-secrets/cra-relay-token","private_jwk_env":"CRA_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}] CRA_RELAY_WORKLOAD_JWK: ${CRA_RELAY_WORKLOAD_JWK} - volumes: - - cra-workload-token:/run/secrets + volumes: [evidence-secrets:/run/evidence-secrets] depends_on: - cra-civil-relay: - condition: service_started + evidence-secrets-init: { condition: service_completed_successfully } + cra-civil-relay: { condition: service_started } nia-workload-agent: <<: *workload-agent-common @@ -600,16 +190,16 @@ services: environment: WORKLOAD_ISSUER: http://127.0.0.1:8090 WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"nia-notary","subject":"nia-notary","scopes":["registry:consult:nia-child-benefit","registry:consult:nia-citizen-status"],"token_file":"/run/secrets/nia-notary-relay-token","private_jwk_env":"NIA_RELAY_WORKLOAD_JWK"}, + [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["population:rows"],"token_file":"/run/evidence-secrets/nia-relay-token","private_jwk_env":"NIA_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}, {"audience":"registry-relay","azp":"solmara-esignet","subject":"solmara-esignet","scopes":["population:identity_release"],"token_file":"/run/esignet-secrets/solmara-esignet-relay-token","private_jwk_env":"NIA_ESIGNET_RELAY_WORKLOAD_JWK","token_uid":1001,"token_gid":1001}] NIA_RELAY_WORKLOAD_JWK: ${NIA_RELAY_WORKLOAD_JWK} NIA_ESIGNET_RELAY_WORKLOAD_JWK: ${NIA_ESIGNET_RELAY_WORKLOAD_JWK} volumes: - - nia-workload-token:/run/secrets + - evidence-secrets:/run/evidence-secrets - nia-esignet-workload-token:/run/esignet-secrets depends_on: - nia-population-relay: - condition: service_started + evidence-secrets-init: { condition: service_completed_successfully } + nia-population-relay: { condition: service_started } sro-workload-agent: <<: *workload-agent-common @@ -617,13 +207,12 @@ services: environment: WORKLOAD_ISSUER: http://127.0.0.1:8090 WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"sro-notary","subject":"sro-notary","scopes":["registry:consult:child-benefit"],"token_file":"/run/secrets/sro-notary-relay-token","private_jwk_env":"SRO_RELAY_WORKLOAD_JWK"}] + [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["sro_child:rows"],"token_file":"/run/evidence-secrets/sro-relay-token","private_jwk_env":"SRO_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}] SRO_RELAY_WORKLOAD_JWK: ${SRO_RELAY_WORKLOAD_JWK} - volumes: - - sro-workload-token:/run/secrets + volumes: [evidence-secrets:/run/evidence-secrets] depends_on: - sro-social-relay: - condition: service_started + evidence-secrets-init: { condition: service_completed_successfully } + sro-social-relay: { condition: service_started } programme-workload-agent: <<: *workload-agent-common @@ -631,13 +220,12 @@ services: environment: WORKLOAD_ISSUER: http://127.0.0.1:8090 WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"programme-notary","subject":"programme-notary","scopes":["registry:consult:child-benefit"],"token_file":"/run/secrets/programme-notary-relay-token","private_jwk_env":"PROGRAMME_RELAY_WORKLOAD_JWK"}] + [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["mosd_enroll:rows"],"token_file":"/run/evidence-secrets/programme-relay-token","private_jwk_env":"PROGRAMME_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}] PROGRAMME_RELAY_WORKLOAD_JWK: ${PROGRAMME_RELAY_WORKLOAD_JWK} - volumes: - - programme-workload-token:/run/secrets + volumes: [evidence-secrets:/run/evidence-secrets] depends_on: - programme-mis-relay: - condition: service_started + evidence-secrets-init: { condition: service_completed_successfully } + programme-mis-relay: { condition: service_started } sipf-workload-agent: <<: *workload-agent-common @@ -645,13 +233,12 @@ services: environment: WORKLOAD_ISSUER: http://127.0.0.1:8090 WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"sipf-notary","subject":"sipf-notary","scopes":["registry:consult:sipf-pension-payment-review","registry:consult:sipf-survivor-benefit"],"token_file":"/run/secrets/sipf-notary-relay-token","private_jwk_env":"SIPF_RELAY_WORKLOAD_JWK"}] + [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["pension:rows","survivor:rows"],"token_file":"/run/evidence-secrets/sipf-relay-token","private_jwk_env":"SIPF_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}] SIPF_RELAY_WORKLOAD_JWK: ${SIPF_RELAY_WORKLOAD_JWK} - volumes: - - sipf-workload-token:/run/secrets + volumes: [evidence-secrets:/run/evidence-secrets] depends_on: - sipf-pensions-relay: - condition: service_started + evidence-secrets-init: { condition: service_completed_successfully } + sipf-pensions-relay: { condition: service_started } nagdi-workload-agent: <<: *workload-agent-common @@ -659,257 +246,82 @@ services: environment: WORKLOAD_ISSUER: http://127.0.0.1:8090 WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"nagdi-notary","subject":"nagdi-notary","scopes":["registry:consult:livestock","registry:consult:voucher"],"token_file":"/run/secrets/nagdi-notary-relay-token","private_jwk_env":"NAGDI_RELAY_WORKLOAD_JWK"}] + [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["nagdi_farmer:rows","nagdi_herd:rows"],"token_file":"/run/evidence-secrets/nagdi-relay-token","private_jwk_env":"NAGDI_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}] NAGDI_RELAY_WORKLOAD_JWK: ${NAGDI_RELAY_WORKLOAD_JWK} - volumes: - - nagdi-workload-token:/run/secrets - depends_on: - nagdi-agriculture-relay: - condition: service_started - - cra-consultation-workload-agent: - <<: *workload-agent-common - network_mode: service:cra-civil-relay-consultation - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"cra-notary","subject":"cra-notary","scopes":["registry:consult:cra-child-benefit","registry:consult:cra-citizen-record","registry:consult:cra-pension-death"],"token_file":"/run/secrets/cra-notary-relay-token","private_jwk_env":"CRA_RELAY_WORKLOAD_JWK"}] - CRA_RELAY_WORKLOAD_JWK: ${CRA_RELAY_WORKLOAD_JWK} - volumes: - - cra-consultation-workload-token:/run/secrets - depends_on: - cra-civil-relay-consultation: - condition: service_started - - nia-consultation-workload-agent: - <<: *workload-agent-common - network_mode: service:nia-population-relay-consultation - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"nia-notary","subject":"nia-notary","scopes":["registry:consult:nia-child-benefit","registry:consult:nia-citizen-status"],"token_file":"/run/secrets/nia-notary-relay-token","private_jwk_env":"NIA_RELAY_WORKLOAD_JWK"}] - NIA_RELAY_WORKLOAD_JWK: ${NIA_RELAY_WORKLOAD_JWK} - volumes: - - nia-consultation-workload-token:/run/secrets - depends_on: - nia-population-relay-consultation: - condition: service_started - - sro-consultation-workload-agent: - <<: *workload-agent-common - network_mode: service:sro-social-relay-consultation - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"sro-notary","subject":"sro-notary","scopes":["registry:consult:child-benefit"],"token_file":"/run/secrets/sro-notary-relay-token","private_jwk_env":"SRO_RELAY_WORKLOAD_JWK"}] - SRO_RELAY_WORKLOAD_JWK: ${SRO_RELAY_WORKLOAD_JWK} - volumes: - - sro-consultation-workload-token:/run/secrets - depends_on: - sro-social-relay-consultation: - condition: service_started - - programme-consultation-workload-agent: - <<: *workload-agent-common - network_mode: service:programme-mis-relay-consultation - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"programme-notary","subject":"programme-notary","scopes":["registry:consult:child-benefit"],"token_file":"/run/secrets/programme-notary-relay-token","private_jwk_env":"PROGRAMME_RELAY_WORKLOAD_JWK"}] - PROGRAMME_RELAY_WORKLOAD_JWK: ${PROGRAMME_RELAY_WORKLOAD_JWK} - volumes: - - programme-consultation-workload-token:/run/secrets - depends_on: - programme-mis-relay-consultation: - condition: service_started - - sipf-consultation-workload-agent: - <<: *workload-agent-common - network_mode: service:sipf-pensions-relay-consultation - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"sipf-notary","subject":"sipf-notary","scopes":["registry:consult:sipf-pension-payment-review","registry:consult:sipf-survivor-benefit"],"token_file":"/run/secrets/sipf-notary-relay-token","private_jwk_env":"SIPF_RELAY_WORKLOAD_JWK"}] - SIPF_RELAY_WORKLOAD_JWK: ${SIPF_RELAY_WORKLOAD_JWK} - volumes: - - sipf-consultation-workload-token:/run/secrets - depends_on: - sipf-pensions-relay-consultation: - condition: service_started - - nagdi-consultation-workload-agent: - <<: *workload-agent-common - network_mode: service:nagdi-agriculture-relay-consultation - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"nagdi-notary","subject":"nagdi-notary","scopes":["registry:consult:livestock","registry:consult:voucher"],"token_file":"/run/secrets/nagdi-notary-relay-token","private_jwk_env":"NAGDI_RELAY_WORKLOAD_JWK"}] - NAGDI_RELAY_WORKLOAD_JWK: ${NAGDI_RELAY_WORKLOAD_JWK} - volumes: - - nagdi-consultation-workload-token:/run/secrets - depends_on: - nagdi-agriculture-relay-consultation: - condition: service_started - - cra-notary: - <<: *notary-common - command: ["--config", "/etc/registry-notary/notary.yaml"] - network_mode: service:cra-civil-relay-consultation - environment: &cra-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${CRA_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_cra_runtime:${CRA_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_cra?sslmode=require" - CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH: ${CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH} - CRA_PENSION_CLIENT_TOKEN_HASH: ${CRA_PENSION_CLIENT_TOKEN_HASH} - CRA_CITIZEN_CLIENT_TOKEN_HASH: ${CRA_CITIZEN_CLIENT_TOKEN_HASH} - volumes: - - ./runtime/registry-projects/local/cra-civil/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - cra-consultation-workload-token:/run/secrets:ro - depends_on: - cra-civil-relay-consultation: - condition: service_healthy - cra-consultation-workload-agent: - condition: service_healthy - cra-notary-state-install: - condition: service_completed_successfully - - cra-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:1cce41247a18f70cb7ff4276bdeb5a16c1e641a9ed7951be8f612daaf35ab87b} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_cra_owner", "--runtime-role", "solmara_notary_cra_runtime"] - environment: - <<: *cra-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_cra_migrator:${CRA_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_cra?sslmode=require" - volumes: - - ./runtime/registry-projects/local/cra-civil/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - cra-consultation-workload-token:/run/secrets:ro - depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - cra-consultation-workload-agent: - condition: service_healthy - - nia-notary: - <<: *notary-common - command: ["--config", "/etc/registry-notary/notary.yaml"] - network_mode: service:nia-population-relay-consultation - environment: &nia-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${NIA_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_nia_runtime:${NIA_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_nia?sslmode=require" - NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH: ${NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH} - NIA_CITIZEN_CLIENT_TOKEN_HASH: ${NIA_CITIZEN_CLIENT_TOKEN_HASH} - NIA_NOTARY_ISSUER_JWK: ${NIA_NOTARY_ISSUER_JWK} - volumes: - - ./runtime/registry-projects/local/nia-population/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - nia-consultation-workload-token:/run/secrets:ro + volumes: [evidence-secrets:/run/evidence-secrets] depends_on: - nia-population-relay-consultation: - condition: service_healthy - nia-consultation-workload-agent: - condition: service_healthy - nia-notary-state-install: - condition: service_completed_successfully + evidence-secrets-init: { condition: service_completed_successfully } + nagdi-agriculture-relay: { condition: service_started } - nia-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:1cce41247a18f70cb7ff4276bdeb5a16c1e641a9ed7951be8f612daaf35ab87b} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} + mint-secrets-init: + image: ${VOLUME_INIT_IMAGE:?versions.env must pin the volume init image} restart: "no" - command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_nia_owner", "--runtime-role", "solmara_notary_nia_runtime"] - environment: - <<: *nia-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_nia_migrator:${NIA_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_nia?sslmode=require" - volumes: - - ./runtime/registry-projects/local/nia-population/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - nia-consultation-workload-token:/run/secrets:ro - depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - nia-consultation-workload-agent: - condition: service_healthy - - sro-notary: - <<: *notary-common - command: ["--config", "/etc/registry-notary/notary.yaml"] - network_mode: service:sro-social-relay-consultation - environment: &sro-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${SRO_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_sro_runtime:${SRO_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_sro?sslmode=require" - SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH: ${SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH} + command: ["sh", "-ec", "cp /seed/signing.jwk /seed/audit-hmac-key /target/; chown -R 65532:65532 /target /audit; chmod 700 /target /audit; chmod 600 /target/*"] volumes: - - ./runtime/registry-projects/local/sro-social/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - sro-consultation-workload-token:/run/secrets:ro - depends_on: - sro-social-relay-consultation: - condition: service_healthy - sro-consultation-workload-agent: - condition: service_healthy - sro-notary-state-install: - condition: service_completed_successfully + - ./config/evidence/local/mint:/seed:ro + - mint-secrets:/target + - mint-audit:/audit - sro-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:1cce41247a18f70cb7ff4276bdeb5a16c1e641a9ed7951be8f612daaf35ab87b} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_sro_owner", "--runtime-role", "solmara_notary_sro_runtime"] - environment: - <<: *sro-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_sro_migrator:${SRO_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_sro?sslmode=require" + mint: + image: ${SOLMARA_MINT_IMAGE:?versions.env must name the locally built Registry Stack main Mint image} + restart: unless-stopped + command: ["serve", "--config", "/etc/registry-mint/config.yaml"] volumes: - - ./runtime/registry-projects/local/sro-social/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - sro-consultation-workload-token:/run/secrets:ro + - ./evidence/mint.yaml:/etc/registry-mint/config.yaml:ro + - ./config/evidence/local/mint/clients:/etc/registry-mint/clients:ro + - mint-secrets:/run/secrets/registry-mint:ro + - mint-audit:/var/lib/registry-mint/audit + networks: [evidence] depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - sro-consultation-workload-agent: - condition: service_healthy + mint-secrets-init: { condition: service_completed_successfully } - programme-notary: - <<: *notary-common - command: ["--config", "/etc/registry-notary/notary.yaml"] - network_mode: service:programme-mis-relay-consultation - environment: &programme-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${PROGRAMME_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_programme_runtime:${PROGRAMME_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_programme?sslmode=require" - PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH: ${PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH} - volumes: - - ./runtime/registry-projects/local/mosd-programme/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - programme-consultation-workload-token:/run/secrets:ro - depends_on: - programme-mis-relay-consultation: - condition: service_healthy - programme-consultation-workload-agent: - condition: service_healthy - programme-notary-state-install: - condition: service_completed_successfully - - programme-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:1cce41247a18f70cb7ff4276bdeb5a16c1e641a9ed7951be8f612daaf35ab87b} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_programme_owner", "--runtime-role", "solmara_notary_programme_runtime"] - environment: - <<: *programme-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_programme_migrator:${PROGRAMME_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_programme?sslmode=require" + evidence-gateway: + image: ${EVIDENCE_GATEWAY_IMAGE:?versions.env must pin the Evidence TLS gateway image} + user: "0:0" + restart: unless-stopped + ports: + - "${SOLMARA_EVIDENCE_GATEWAY_PORT:-4341}:443" volumes: - - ./runtime/registry-projects/local/mosd-programme/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - programme-consultation-workload-token:/run/secrets:ro + - ./evidence/Caddyfile:/etc/caddy/Caddyfile:ro + - ./config/evidence/local/tls:/etc/solmara-evidence/tls:ro + - caddy-data:/data + - caddy-config:/config + networks: + default: {} + evidence: + aliases: + - mint.evidence.solmara.invalid + - evidence.solmara.invalid + - cra-relay.evidence.solmara.invalid + - nia-relay.evidence.solmara.invalid + - sro-relay.evidence.solmara.invalid + - programme-relay.evidence.solmara.invalid + - sipf-relay.evidence.solmara.invalid + - nagdi-relay.evidence.solmara.invalid + + evidence: + image: ${SOLMARA_EVIDENCE_IMAGE:?versions.env must name the locally built Registry Stack main Evidence image} + restart: unless-stopped + command: ["--runtime", "/etc/registry-evidence/runtime.yaml", "serve"] + environment: + SSL_CERT_FILE: /etc/registry-evidence/tls/lab-ca.crt + volumes: + - ./evidence/runtime.yaml:/etc/registry-evidence/runtime.yaml:ro + - ./evidence/bundle:/etc/registry-evidence/bundle:ro + - ./config/evidence/local/tls/ca.crt:/etc/registry-evidence/tls/lab-ca.crt:ro + - evidence-secrets:/run/secrets/registry-evidence:ro + - evidence-audit:/var/lib/registry-evidence/audit + networks: + evidence: + ipv4_address: 172.29.0.10 depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - programme-consultation-workload-agent: - condition: service_healthy + evidence-gateway: { condition: service_started } + cra-workload-agent: { condition: service_healthy } + nia-workload-agent: { condition: service_healthy } + sro-workload-agent: { condition: service_healthy } + programme-workload-agent: { condition: service_healthy } + sipf-workload-agent: { condition: service_healthy } + nagdi-workload-agent: { condition: service_healthy } child-benefit-federator: build: @@ -918,119 +330,54 @@ services: image: ${SOLMARA_CHILD_BENEFIT_FEDERATOR_IMAGE:-solmara-lab-scenario-runner:local} command: ["python", "scenario-runner/child_benefit_federator.py"] environment: + <<: *evidence-client-environment CHILD_BENEFIT_FEDERATOR_HOST: 0.0.0.0 CHILD_BENEFIT_FEDERATOR_PORT: 8080 CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN} - CRA_NOTARY_URL: http://cra-notary:8081 - CRA_CHILD_BENEFIT_CLIENT_TOKEN: ${CRA_CHILD_BENEFIT_CLIENT_TOKEN} - NIA_NOTARY_URL: http://nia-notary:8081 - NIA_CHILD_BENEFIT_CLIENT_TOKEN: ${NIA_CHILD_BENEFIT_CLIENT_TOKEN} - SRO_NOTARY_URL: http://sro-notary:8081 - SRO_CHILD_BENEFIT_CLIENT_TOKEN: ${SRO_CHILD_BENEFIT_CLIENT_TOKEN} - PROGRAMME_NOTARY_URL: http://programme-notary:8081 - PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN: ${PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN} - ports: - - "${SOLMARA_CHILD_BENEFIT_FEDERATOR_PORT:-4321}:8080" + ports: ["${SOLMARA_CHILD_BENEFIT_FEDERATOR_PORT:-4321}:8080"] + volumes: + - ./config/evidence/local/mint/client-private.jwk:/run/secrets/solmara-evidence-client.jwk:ro + - ./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro + networks: [default, evidence] healthcheck: test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3)\""] - interval: 30s - timeout: 10s - retries: 3 - depends_on: - cra-notary: - condition: service_healthy - nia-notary: - condition: service_healthy - sro-notary: - condition: service_healthy - programme-notary: - condition: service_healthy - - sipf-notary: - <<: *notary-common - command: ["--config", "/etc/registry-notary/notary.yaml"] - network_mode: service:sipf-pensions-relay-consultation - environment: &sipf-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${SIPF_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_sipf_runtime:${SIPF_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_sipf?sslmode=require" - SIPF_PENSION_CLIENT_TOKEN_HASH: ${SIPF_PENSION_CLIENT_TOKEN_HASH} - SIPF_NOTARY_ISSUER_JWK: ${SIPF_NOTARY_ISSUER_JWK} - volumes: - - ./runtime/registry-projects/local/sipf-pensions/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - sipf-consultation-workload-token:/run/secrets:ro - depends_on: - sipf-pensions-relay-consultation: - condition: service_healthy - sipf-consultation-workload-agent: - condition: service_healthy - sipf-notary-state-install: - condition: service_completed_successfully - - sipf-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:1cce41247a18f70cb7ff4276bdeb5a16c1e641a9ed7951be8f612daaf35ab87b} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_sipf_owner", "--runtime-role", "solmara_notary_sipf_runtime"] - environment: - <<: *sipf-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_sipf_migrator:${SIPF_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_sipf?sslmode=require" - volumes: - - ./runtime/registry-projects/local/sipf-pensions/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - sipf-consultation-workload-token:/run/secrets:ro - depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - sipf-consultation-workload-agent: - condition: service_healthy - - nagdi-notary: - <<: *notary-common - command: ["--config", "/etc/registry-notary/notary.yaml"] - network_mode: service:nagdi-agriculture-relay-consultation - environment: &nagdi-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${NAGDI_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_nagdi_runtime:${NAGDI_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_nagdi?sslmode=require" - NAGDI_CLIENT_TOKEN_HASH: ${NAGDI_CLIENT_TOKEN_HASH} - NAGDI_NOTARY_ISSUER_JWK: ${NAGDI_NOTARY_ISSUER_JWK} - volumes: - - ./runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - nagdi-consultation-workload-token:/run/secrets:ro + interval: 10s + timeout: 5s + retries: 12 depends_on: - nagdi-agriculture-relay-consultation: - condition: service_healthy - nagdi-consultation-workload-agent: - condition: service_healthy - nagdi-notary-state-install: - condition: service_completed_successfully + evidence: { condition: service_started } + mint: { condition: service_started } - nagdi-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:1cce41247a18f70cb7ff4276bdeb5a16c1e641a9ed7951be8f612daaf35ab87b} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_nagdi_owner", "--runtime-role", "solmara_notary_nagdi_runtime"] + scenario-runner: + build: + context: . + dockerfile: scenario-runner/Dockerfile + image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:-solmara-lab-scenario-runner:local} environment: - <<: *nagdi-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_nagdi_migrator:${NAGDI_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_nagdi?sslmode=require" + <<: *evidence-client-environment + SCENARIO_RUNNER_HOST: 0.0.0.0 + SCENARIO_RUNNER_PORT: 8080 + SOLMARA_LAB_MODE: local + CHILD_BENEFIT_FEDERATOR_URL: http://child-benefit-federator:8080 + CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN} volumes: - - ./runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - nagdi-consultation-workload-token:/run/secrets:ro + - ./config/evidence/local/mint/client-private.jwk:/run/secrets/solmara-evidence-client.jwk:ro + - ./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro + networks: [default, evidence] + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3)\""] + interval: 10s + timeout: 5s + retries: 12 depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - nagdi-consultation-workload-agent: - condition: service_healthy + child-benefit-federator: { condition: service_healthy } portal: build: context: ./portal image: ${SOLMARA_PORTAL_IMAGE:-solmara-lab-portal:local} environment: + <<: *evidence-client-environment HOST: 0.0.0.0 PORT: 4000 PORTAL_PROVIDER: live @@ -1038,60 +385,75 @@ services: PORTAL_SESSION_SECRET: ${PORTAL_SESSION_SECRET} CHILD_BENEFIT_FEDERATOR_URL: http://child-benefit-federator:8080 CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN} - CRA_NOTARY_URL: http://cra-notary:8081 - CRA_PENSION_CLIENT_TOKEN: ${CRA_PENSION_CLIENT_TOKEN} - CRA_CITIZEN_CLIENT_TOKEN: ${CRA_CITIZEN_CLIENT_TOKEN} - NIA_NOTARY_URL: http://nia-notary:8081 - NIA_CITIZEN_CLIENT_TOKEN: ${NIA_CITIZEN_CLIENT_TOKEN} - SIPF_NOTARY_URL: http://sipf-notary:8081 - SIPF_PENSION_CLIENT_TOKEN: ${SIPF_PENSION_CLIENT_TOKEN} - NAGDI_NOTARY_URL: http://nagdi-notary:8081 - NAGDI_NOTARY_TOKEN: ${NAGDI_NOTARY_TOKEN} - ports: - - "${SOLMARA_PORTAL_PORT:-4300}:4000" + SCENARIO_RUNNER_URL: http://scenario-runner:8080 + ports: ["${SOLMARA_PORTAL_PORT:-4300}:4000"] + volumes: + - ./config/evidence/local/mint/client-private.jwk:/run/secrets/solmara-evidence-client.jwk:ro + - ./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro healthcheck: test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:4000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] interval: 30s timeout: 10s retries: 3 depends_on: - child-benefit-federator: - condition: service_healthy - cra-notary: - condition: service_healthy - sipf-notary: - condition: service_healthy - nagdi-notary: - condition: service_healthy - nia-notary: - condition: service_healthy - scenario-runner: - condition: service_healthy + scenario-runner: { condition: service_healthy } + + home: + build: + context: . + dockerfile: home/Dockerfile + image: ${SOLMARA_HOME_IMAGE:-solmara-lab-home:local} + environment: + HOST: 0.0.0.0 + PORT: 4301 + SOLMARA_LAB_ROOT: /app + REPO_URL: ${REPO_URL:-https://github.com/registrystack/solmara-lab} + PORTAL_URL: http://127.0.0.1:${SOLMARA_PORTAL_PORT:-4300} + SCENARIO_RUNNER_URL: http://scenario-runner:8080 + STATIC_METADATA_URL: http://static-metadata:8080 + PORTAL_PROBE_URL: http://portal:4000 + CRA_CIVIL_RELAY_URL: http://cra-civil-relay:8080 + NIA_POPULATION_RELAY_URL: http://nia-population-relay:8080 + SRO_SOCIAL_RELAY_URL: http://sro-social-relay:8080 + PROGRAMME_MIS_RELAY_URL: http://programme-mis-relay:8080 + SIPF_PENSIONS_RELAY_URL: http://sipf-pensions-relay:8080 + NAGDI_AGRICULTURE_RELAY_URL: http://nagdi-agriculture-relay:8080 + EVIDENCE_URL: http://evidence:8080 + MINT_URL: http://mint:8081 + CHILD_BENEFIT_FEDERATOR_URL: http://child-benefit-federator:8080 + HOME_PUBLISHED_TOKENS: "{}" + networks: [default, evidence] + ports: ["${SOLMARA_HOME_PORT:-4301}:4301"] + volumes: + - ./output/smoke:/app/output/smoke:ro + - ./generator/output:/app/generator/output:ro + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:4301/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] + interval: 30s + timeout: 10s + retries: 3 + depends_on: + static-metadata: { condition: service_started } + scenario-runner: { condition: service_healthy } volumes: postgres-data: cra-relay-cache: - cra-relay-consultation-cache: nia-relay-cache: - nia-relay-consultation-cache: sro-relay-cache: - sro-relay-consultation-cache: programme-relay-cache: - programme-relay-consultation-cache: sipf-relay-cache: - sipf-relay-consultation-cache: nagdi-relay-cache: - nagdi-relay-consultation-cache: - cra-workload-token: - cra-consultation-workload-token: - nia-workload-token: - nia-consultation-workload-token: + evidence-secrets: + evidence-audit: + mint-secrets: + mint-audit: nia-esignet-workload-token: - sro-workload-token: - sro-consultation-workload-token: - programme-workload-token: - programme-consultation-workload-token: - sipf-workload-token: - sipf-consultation-workload-token: - nagdi-workload-token: - nagdi-consultation-workload-token: + caddy-data: + caddy-config: + +networks: + evidence: + ipam: + config: + - subnet: 172.29.0.0/24 diff --git a/config/evidence/local/.gitkeep b/config/evidence/local/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/config/evidence/local/.gitkeep @@ -0,0 +1 @@ + diff --git a/docker/registry-stack-runtime/Dockerfile b/docker/registry-stack-runtime/Dockerfile new file mode 100644 index 0000000..1dd8606 --- /dev/null +++ b/docker/registry-stack-runtime/Dockerfile @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1.7 + +FROM rust:1.95-trixie@sha256:f49565f188ee00bc2a18dd418183f2c5f23ef7d6e691890517ed341a598f67c3 AS relay-builder +WORKDIR /workspace + +COPY . . + +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/workspace/target \ + cargo build --release --locked --package registry-relay \ + && install -D -m 0755 target/release/registry-relay /runtime/usr/local/bin/registry-relay \ + && install -D -m 0755 target/release/registry-relay-rhai-worker /runtime/usr/local/bin/registry-relay-rhai-worker \ + && install -D -m 0644 LICENSE /runtime/licenses/registry-relay/LICENSE \ + && mkdir -p \ + /runtime/etc/registry-relay \ + /runtime/var/lib/registry/audit \ + /runtime/var/lib/registry/state \ + /runtime/var/lib/registry-relay/cache \ + /runtime/var/lib/registry-relay/data \ + /runtime/var/log/registry-relay \ + && chown -R 65532:65532 \ + /runtime/etc/registry-relay \ + /runtime/var/lib/registry \ + /runtime/var/lib/registry-relay \ + /runtime/var/log/registry-relay + +FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS relay + +COPY --from=relay-builder /runtime/ / + +WORKDIR /var/lib/registry-relay +ENV REGISTRY_RELAY_CONFIG=/etc/registry-relay/config.yaml +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD ["/usr/local/bin/registry-relay", "healthcheck"] +ENTRYPOINT ["/usr/local/bin/registry-relay"] +CMD ["--config", "/etc/registry-relay/config.yaml"] diff --git a/docs/changelog.md b/docs/changelog.md index 877d4b5..4c42051 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,23 @@ A small dated log of what changed in the visitor center and the lab topology. Newest entry first. +## 2026-08-06 Registry Stack main Evidence migration + +The local lab now builds Relay, Registry Evidence, and Registry Mint from the +exact Registry Stack `main` commit pinned in `versions.env`. Six authority +Relays expose current Records APIs to one Evidence service through scoped, +short-lived workload credentials. Mint uses `private_key_jwt` client +authentication, and the application journeys consume flattened signed JWS +Evidence assertions instead of authority Notary evaluations. + +All six Registry projects and their committed runtime closures are regenerated +with Registryctl 0.17.0. Eleven Evidence requirements cover child benefit, +pension and survivor workflows, farmer and livestock controls, and citizen +self-service. The active local quality gates validate the paired Mint config, +the Evidence bundle, and 89 Evidence fixture cases. Hosted and Coolify files +remain on the earlier released Notary topology and are explicitly unsupported +on this source-only branch. + ## 2026-07-29 Registry Stack v0.15.2 adoption The lab now consumes the canonical Registry Stack Relay image directly. The diff --git a/docs/purposes.md b/docs/purposes.md index 7e27d8a..9fc21d1 100644 --- a/docs/purposes.md +++ b/docs/purposes.md @@ -1,77 +1,70 @@ # Solmara Purpose Catalogue -Status: normative for Solmara Lab wave 1. +Status: normative for the local Registry Evidence deployment. -All Solmara purpose IRIs are minted under -`https://id.registrystack.org/solmara/purpose/...`. Manifests advertise these -IRIs, notaries enforce these IRIs, and smoke tests assert these IRIs and stable -problem codes. Smoke tests must not assert denial message text. +Registry Evidence requests use the closed purpose codes below. The reviewed +source adapters translate those grants into fixed `Data-Purpose` IRIs when they +call each authority's Records API. A caller cannot supply or override those +downstream headers. ## Wave 1 Purposes -| Purpose IRI | Advertised by | Enforced by | Story | Denial problem codes | +| Purpose code | Advertised by | Enforced by | Story | Denial problem codes | |---|---|---|---|---| -| `https://id.registrystack.org/solmara/purpose/child-benefit-review` | Civil Registration Authority, National Identity Agency, Social Registry Office, MoSD programme MIS | CRA, NIA, SRO, and Programme authority Notaries | Birth to child benefit | `pdp.purpose_not_permitted` | -| `https://id.registrystack.org/solmara/purpose/pension-payment-review` | Civil Registration Authority, Social Insurance and Pensions Fund | CRA and SIPF authority Notaries | Death to pension stop | `pdp.purpose_not_permitted` | -| `https://id.registrystack.org/solmara/purpose/survivor-benefit-determination` | Social Insurance and Pensions Fund | SIPF authority Notary | Survivor benefit | `pdp.purpose_not_permitted` | -| `https://id.registrystack.org/solmara/purpose/voucher-eligibility-review` | National Agricultural Data Institute | NAgDI authority Notary | Farmer climate-smart voucher | `pdp.purpose_not_permitted` | -| `https://id.registrystack.org/solmara/purpose/livestock-movement-control` | National Agricultural Data Institute | NAgDI authority Notary | Livestock movement permit companion | `pdp.purpose_not_permitted` | -| `https://id.registrystack.org/solmara/purpose/citizen-self-service` | Civil Registration Authority and National Identity Agency | CRA and NIA authority Notaries | Citizen portal | `pdp.purpose_not_permitted` | - -The two NAgDI purpose identifiers are canonical for wave 1 docs. WP4 must still -confirm that the ported NAgDI claim configs use these exact identifiers. +| `child-benefit-review` | Civil Registration Authority, National Identity Agency, Social Registry Office, MoSD programme MIS | Registry Evidence grants and four reviewed requirements | Birth to child benefit | `not_authorized` | +| `pension-payment-review` | Civil Registration Authority, Social Insurance and Pensions Fund | Registry Evidence grants and CRA/SIPF requirements | Death to pension stop | `not_authorized` | +| `survivor-benefit-determination` | Social Insurance and Pensions Fund | Registry Evidence grant and SIPF survivor requirement | Survivor benefit | `not_authorized` | +| `voucher-eligibility-review` | National Agricultural Data Institute | Registry Evidence grant and NAgDI voucher requirement | Farmer climate-smart voucher | `not_authorized` | +| `livestock-movement-control` | National Agricultural Data Institute | Registry Evidence grant and NAgDI livestock requirement | Livestock movement permit companion | `not_authorized` | +| `citizen-self-service` | Civil Registration Authority and National Identity Agency | Registry Evidence grants and CRA/NIA citizen requirements | Citizen portal | `not_authorized` | ## Purpose Rules `child-benefit-review` permits evidence needed to determine whether a child may -be enrolled in child support: registered birth, age under 5, child life status, -household eligibility band, and duplicate enrollment status. It does not permit -raw poverty scores, complete household profiles, or unrelated civil events. - -`pension-payment-review` permits the SIPF to determine whether an active pension -payment should continue, be held, or stop. It permits the fact of death and the -death registration number where needed. It does not permit cause of death or -medical details. - -`survivor-benefit-determination` permits the SIPF to determine whether a linked -spouse or dependent qualifies for survivor benefits. It permits spouse linkage, -marriage status, death fact, and pension membership predicates. It does not -permit cause of death, complete contribution history, or unrelated benefits. - -`voucher-eligibility-review` permits NAgDI to evaluate farmer registration, -data-use authorization, eligible crop or holding records, district risk band, -and prior voucher status. It does not permit unrelated livestock movements or -raw market-sizing tables. - -`livestock-movement-control` permits NAgDI to evaluate owner, animal, premises, -vaccination, quarantine, and movement predicates for a livestock permit. It does -not permit farmer voucher budget, unrelated crop records, or household poverty -data. - -`citizen-self-service` permits the citizen portal to request preview evidence -for the selected persona and to show proof traces for consented service -journeys. It does not permit bulk reads, administrative-only fields, or evidence -for a different selected persona. - -## Credential And Offering Names - -| Story | Evidence offering | Credential `vct` | +be enrolled in child support: registered birth, age under five, active +population record, household priority band, and duplicate enrollment status. +It does not permit raw poverty scores, complete household profiles, or unrelated +civil events. + +`pension-payment-review` permits an application to combine the fact of death +with an active-payment value. It does not permit cause of death, medical detail, +payment amount, or payment history. + +`survivor-benefit-determination` permits the reviewed survivor eligibility +value. It does not permit a complete marriage record, contribution history, or +unrelated benefits. + +`voucher-eligibility-review` permits farmer registration, data-use +authorization, and voucher eligibility values. It does not permit workbook rows, +unrelated livestock movements, or raw market-sizing data. + +`livestock-movement-control` permits registered-herd, quarantine, and movement +eligibility values. It does not permit voucher budgets, crop records, or +household poverty data. + +`citizen-self-service` permits separate CRA linkage and NIA active-population +values for the selected persona. It does not permit bulk reads, +administrative-only fields, or evidence for another selected persona. + +## Evidence Outputs + +| Story | Registry Evidence requirements | Output | |---|---|---| -| Birth to child benefit | Four authority predicate responses composed by the child-benefit orchestration service | No credential issued | -| Death to pension stop | `cra-pension-death` and `sipf-pension-payment-review` | No credential issued | -| Survivor benefit | `sipf-survivor-benefit` | `https://id.registrystack.org/solmara/vct/survivor-benefit-status` | -| Farmer climate-smart voucher | NAgDI `voucher` | `https://id.registrystack.org/solmara/vct/climate-smart-voucher-eligibility` | -| Livestock movement permit | NAgDI `livestock` | `https://id.registrystack.org/solmara/vct/livestock-movement-permit` | +| Birth to child benefit | CRA, NIA, SRO, and MoSD child-benefit requirements | Four flattened signed JWS assertions collected by the application | +| Death to pension stop | CRA death and SIPF active-payment requirements | Two flattened signed JWS assertions and an application-owned stop decision | +| Survivor benefit | SIPF survivor requirement | One flattened signed JWS assertion | +| Farmer climate-smart voucher | NAgDI voucher requirement | One flattened signed JWS assertion | +| Livestock movement permit | NAgDI livestock requirement | One flattened signed JWS assertion | +| Citizen self-service | CRA and NIA citizen requirements | Two flattened signed JWS assertions | ## Denial Assertions -Purpose-denial smoke tests assert: +Purpose-denial tests assert: -1. The denied request used a purpose IRI outside the permitted catalogue or a - permitted purpose against a prohibited field. -2. The response problem code is `pdp.purpose_not_permitted`. -3. The response does not include the prohibited source field. +1. The request names a purpose not granted for the selected requirement. +2. Registry Evidence returns HTTP 403 and `not_authorized`. +3. No source row or prohibited field appears in the response. -The child-benefit orchestration smoke verifies that four independently governed -authority responses can be composed without creating a seventh Notary or a -shared correctness-state owner. +The child-benefit collector proves that one application can collect four +independently governed signed assertions without owning a shared eligibility +rule or copying authority rows. diff --git a/evidence/Caddyfile b/evidence/Caddyfile new file mode 100644 index 0000000..1c6476f --- /dev/null +++ b/evidence/Caddyfile @@ -0,0 +1,66 @@ +{ + auto_https off +} + +(solmara_tls) { + tls /etc/solmara-evidence/tls/gateway.crt /etc/solmara-evidence/tls/gateway.key +} + +https://mint.evidence.solmara.invalid { + import solmara_tls + reverse_proxy mint:8081 +} + +https://evidence.solmara.invalid { + import solmara_tls + reverse_proxy evidence:8080 +} + +https://localhost { + import solmara_tls + handle_path /child-benefit/* { + reverse_proxy child-benefit-federator:8080 + } + handle /token* { + reverse_proxy mint:8081 + } + handle /.well-known/oauth-authorization-server { + reverse_proxy mint:8081 + } + handle /.well-known/jwks.json { + reverse_proxy mint:8081 + } + handle { + reverse_proxy evidence:8080 + } +} + +https://cra-relay.evidence.solmara.invalid { + import solmara_tls + reverse_proxy cra-civil-relay:8080 +} + +https://nia-relay.evidence.solmara.invalid { + import solmara_tls + reverse_proxy nia-population-relay:8080 +} + +https://sro-relay.evidence.solmara.invalid { + import solmara_tls + reverse_proxy sro-social-relay:8080 +} + +https://programme-relay.evidence.solmara.invalid { + import solmara_tls + reverse_proxy programme-mis-relay:8080 +} + +https://sipf-relay.evidence.solmara.invalid { + import solmara_tls + reverse_proxy sipf-pensions-relay:8080 +} + +https://nagdi-relay.evidence.solmara.invalid { + import solmara_tls + reverse_proxy nagdi-agriculture-relay:8080 +} diff --git a/evidence/bundle/adapters/extract.rhai b/evidence/bundle/adapters/extract.rhai new file mode 100644 index 0000000..1cdca2a --- /dev/null +++ b/evidence/bundle/adapters/extract.rhai @@ -0,0 +1,26 @@ +fn extract(source_response, parameters) { + let records = source_response["data"]; + let has_more = source_response["pagination"]["has_more"]; + if records.len == 0 { + if has_more { throw("source_protocol_error"); } + return #{outcome: "no_match"}; + } + if records.len > 1 || has_more { + return #{outcome: "ambiguous"}; + } + let record = records[0]; + // Relay preserves a nullable birth-registration number. Minimize that + // identifier into the only fact the derivation needs before validation. + if record.contains("birth_brn") { + return #{ + outcome: "match", + facts: #{ + uin: record["uin"], + birth_date: record["birth_date"], + birth_registered: !is_missing(record["birth_brn"]), + deceased: record["deceased"] + } + }; + } + #{outcome: "match", facts: record} +} diff --git a/evidence/bundle/adapters/prepare.rhai b/evidence/bundle/adapters/prepare.rhai new file mode 100644 index 0000000..6c6642d --- /dev/null +++ b/evidence/bundle/adapters/prepare.rhai @@ -0,0 +1,16 @@ +fn prepare(selectors, parameters) { + let selector_field = parameters["selectorField"]; + let subject = selectors["subject"]; + let selector_value = subject["values"]["uin"]; + if selector_field == "farmer_id" { + selector_value = subject["values"]["farmer_id"]; + } + #{ + query: [ + #{name: selector_field, value: selector_value}, + #{name: "fields", value: parameters["providerFields"]}, + #{name: "limit", value: parameters["resultLimit"]} + ], + body: () + } +} diff --git a/evidence/bundle/derivations/cra-child-benefit.rhai b/evidence/bundle/derivations/cra-child-benefit.rhai new file mode 100644 index 0000000..3b014df --- /dev/null +++ b/evidence/bundle/derivations/cra-child-benefit.rhai @@ -0,0 +1,16 @@ +fn derive(facts, selectors, evaluation_context) { + if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { + throw("derivation_input_error"); + } + let birth_date = parse_date(required(facts["birth_date"], "required_fact_missing")); + let fifth_birthday = add_calendar_years(birth_date, 5); + let as_of_date = parse_date(evaluation_context["parameters"]["as_of_date"]); + let registered = required(facts["birth_registered"], "required_fact_missing"); + let under_five = !required(facts["deceased"], "required_fact_missing") && + compare_dates(as_of_date, birth_date) >= 0 && + compare_dates(as_of_date, fifth_birthday) < 0; + [ + #{concept_id: "https://id.registrystack.org/solmara/concept/birth-is-registered", value: registered}, + #{concept_id: "https://id.registrystack.org/solmara/concept/child-age-under-5", value: under_five} + ] +} diff --git a/evidence/bundle/derivations/cra-deceased.rhai b/evidence/bundle/derivations/cra-deceased.rhai new file mode 100644 index 0000000..8f00bfe --- /dev/null +++ b/evidence/bundle/derivations/cra-deceased.rhai @@ -0,0 +1,6 @@ +fn derive(facts, selectors, evaluation_context) { + if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { + throw("derivation_input_error"); + } + [#{concept_id: "https://id.registrystack.org/solmara/concept/person-is-deceased", value: required(facts["deceased"], "required_fact_missing")}] +} diff --git a/evidence/bundle/derivations/cra-linked.rhai b/evidence/bundle/derivations/cra-linked.rhai new file mode 100644 index 0000000..5aba6fd --- /dev/null +++ b/evidence/bundle/derivations/cra-linked.rhai @@ -0,0 +1,6 @@ +fn derive(facts, selectors, evaluation_context) { + if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { + throw("derivation_input_error"); + } + [#{concept_id: "https://id.registrystack.org/solmara/concept/civil-record-linked", value: true}] +} diff --git a/evidence/bundle/derivations/livestock.rhai b/evidence/bundle/derivations/livestock.rhai new file mode 100644 index 0000000..ce583dc --- /dev/null +++ b/evidence/bundle/derivations/livestock.rhai @@ -0,0 +1,16 @@ +fn derive(facts, selectors, evaluation_context) { + if required(facts["farmer_id"], "required_fact_missing") != selectors["subject"]["values"]["farmer_id"] { + throw("derivation_input_error"); + } + let registered = required(facts["registered_herd"], "required_fact_missing"); + let not_quarantined = required(facts["origin_district_not_quarantined_for_species"], "required_fact_missing"); + let eligible = registered && not_quarantined && + required(facts["herd_vaccination_current"], "required_fact_missing") && + required(facts["destination_district_open"], "required_fact_missing") && + required(facts["no_conflicting_open_movement_permit"], "required_fact_missing"); + [ + #{concept_id: "https://id.registrystack.org/solmara/concept/registered-herd", value: registered}, + #{concept_id: "https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species", value: not_quarantined}, + #{concept_id: "https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit", value: eligible} + ] +} diff --git a/evidence/bundle/derivations/not-enrolled.rhai b/evidence/bundle/derivations/not-enrolled.rhai new file mode 100644 index 0000000..5f05927 --- /dev/null +++ b/evidence/bundle/derivations/not-enrolled.rhai @@ -0,0 +1,6 @@ +fn derive(facts, selectors, evaluation_context) { + if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { + throw("derivation_input_error"); + } + [#{concept_id: "https://id.registrystack.org/solmara/concept/not-already-enrolled", value: !required(facts["duplicate_flag"], "required_fact_missing")}] +} diff --git a/evidence/bundle/derivations/pension-active.rhai b/evidence/bundle/derivations/pension-active.rhai new file mode 100644 index 0000000..9cadc68 --- /dev/null +++ b/evidence/bundle/derivations/pension-active.rhai @@ -0,0 +1,6 @@ +fn derive(facts, selectors, evaluation_context) { + if required(facts["pensioner_uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { + throw("derivation_input_error"); + } + [#{concept_id: "https://id.registrystack.org/solmara/concept/pension-payment-active", value: required(facts["payment_status"], "required_fact_missing") == "active"}] +} diff --git a/evidence/bundle/derivations/population-active.rhai b/evidence/bundle/derivations/population-active.rhai new file mode 100644 index 0000000..16a8203 --- /dev/null +++ b/evidence/bundle/derivations/population-active.rhai @@ -0,0 +1,8 @@ +fn derive(facts, selectors, evaluation_context) { + if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { + throw("derivation_input_error"); + } + let active = required(facts["identity_status"], "required_fact_missing") == "active" && + required(facts["alive"], "required_fact_missing"); + [#{concept_id: evaluation_context["parameters"]["concept_id"], value: active}] +} diff --git a/evidence/bundle/derivations/poverty-priority.rhai b/evidence/bundle/derivations/poverty-priority.rhai new file mode 100644 index 0000000..aa9efcd --- /dev/null +++ b/evidence/bundle/derivations/poverty-priority.rhai @@ -0,0 +1,6 @@ +fn derive(facts, selectors, evaluation_context) { + if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { + throw("derivation_input_error"); + } + [#{concept_id: "https://id.registrystack.org/solmara/concept/household-below-poverty-threshold", value: required(facts["poverty_band"], "required_fact_missing") == "priority"}] +} diff --git a/evidence/bundle/derivations/survivor-eligible.rhai b/evidence/bundle/derivations/survivor-eligible.rhai new file mode 100644 index 0000000..c117977 --- /dev/null +++ b/evidence/bundle/derivations/survivor-eligible.rhai @@ -0,0 +1,6 @@ +fn derive(facts, selectors, evaluation_context) { + if required(facts["spouse_uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { + throw("derivation_input_error"); + } + [#{concept_id: "https://id.registrystack.org/solmara/concept/survivor-is-eligible", value: required(facts["survivor_eligible"], "required_fact_missing")}] +} diff --git a/evidence/bundle/derivations/voucher.rhai b/evidence/bundle/derivations/voucher.rhai new file mode 100644 index 0000000..d4d60de --- /dev/null +++ b/evidence/bundle/derivations/voucher.rhai @@ -0,0 +1,19 @@ +fn derive(facts, selectors, evaluation_context) { + if required(facts["farmer_id"], "required_fact_missing") != selectors["subject"]["values"]["farmer_id"] { + throw("derivation_input_error"); + } + let registered = required(facts["farmer_registered"], "required_fact_missing"); + let authorized = required(facts["data_use_authorized"], "required_fact_missing"); + let eligible = registered && authorized && + required(facts["active_smallholder_farmer"], "required_fact_missing") && + required(facts["active_farm_parcel"], "required_fact_missing") && + required(facts["crop_declared_for_season"], "required_fact_missing") && + required(facts["district_climate_risk_active"], "required_fact_missing") && + required(facts["voucher_entitlement_current"], "required_fact_missing") && + required(facts["voucher_not_redeemed"], "required_fact_missing"); + [ + #{concept_id: "https://id.registrystack.org/solmara/concept/farmer-registered", value: registered}, + #{concept_id: "https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose", value: authorized}, + #{concept_id: "https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher", value: eligible} + ] +} diff --git a/evidence/bundle/evidence.yaml b/evidence/bundle/evidence.yaml new file mode 100644 index 0000000..1a3447c --- /dev/null +++ b/evidence/bundle/evidence.yaml @@ -0,0 +1,542 @@ +version: 1 +assuranceProfile: production +service: { providerId: https://id.registrystack.org/solmara/evidence/national, trustDomain: https://id.registrystack.org/solmara/trust-domain/lab } +issuer: { id: https://id.registrystack.org/solmara/issuer/national-evidence-service } +authentication: + kind: oidc-access-token + issuer: https://mint.evidence.solmara.invalid + audiences: [solmara-evidence] + tokenTypes: [at+jwt] + algorithms: [EdDSA] + jwksUri: https://mint.evidence.solmara.invalid/.well-known/jwks.json + principalClaim: sub + requesterTagsClaim: evidence_tags + evidenceAudienceClaim: evidence_audience + grantIdClaim: evidence_grant_id + grantAuthorityClaim: evidence_authority +audit: { format: keyed-jsonl, hashSecretRef: secret:file/audit-hmac-key, hashKeyVersion: 1, failClosed: true } +subjectBinding: { secretRef: secret:file/subject-binding-hmac-key, keyVersion: 1 } +# One demo principal drives every guided workflow, so its burst covers a full +# end-to-end lab tour while retaining per-principal and selector-failure bounds. +rateLimits: { requestsPerPrincipalPerMinute: 600, burstPerPrincipal: 100, failedSelectorAttemptsPerPrincipalAuthorityPerMinute: 50 } +signing: + format: flattened-jws-json + algorithm: EdDSA + activeKeyId: solmara-evidence-signing-key-1 + activeKeyRef: secret:file/signing-ed25519-private-jwk + retiredPublicJwkFiles: [] + jwksPath: /.well-known/evidence/jwks.json + maximumAssertionValiditySeconds: 86400 + verifierClockSkewSeconds: 30 +responseFormats: [signed-jws] + +selectorProfiles: + solmara-uin-v1: + maximumAggregateBytes: 16 + fields: + uin: { type: string, minimumBytes: 10, maximumBytes: 10 } + farmer-reference-v1: + maximumAggregateBytes: 32 + fields: + farmer_id: { type: string, minimumBytes: 7, maximumBytes: 16 } + +sources: + cra-child-benefit: + transport: http-json + baseUrl: https://cra-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/cra-relay-token } + request: + method: GET + path: /v1/datasets/civil_people/entities/civil_people/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review } + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: uin, providerFields: "uin,birth_date,birth_brn,deceased", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/uin, /data/*/birth_date, /data/*/birth_brn, /data/*/deceased, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/cra-child-benefit-facts.schema.yaml + cra-pension: + transport: http-json + baseUrl: https://cra-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/cra-relay-token } + request: + method: GET + path: /v1/datasets/civil_people/entities/civil_people/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/pension-payment-review } + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: uin, providerFields: "uin,deceased", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/uin, /data/*/deceased, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/cra-pension-facts.schema.yaml + cra-citizen: + transport: http-json + baseUrl: https://cra-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/cra-relay-token } + request: + method: GET + path: /v1/datasets/civil_people/entities/civil_people/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/citizen-self-service } + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: uin, providerFields: "uin", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/uin, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/cra-citizen-facts.schema.yaml + nia-child-benefit: + transport: http-json + baseUrl: https://nia-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/nia-relay-token } + request: + method: GET + path: /v1/datasets/population/entities/population/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review } + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: uin, providerFields: "uin,identity_status,alive", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/uin, /data/*/identity_status, /data/*/alive, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/nia-population-facts.schema.yaml + nia-citizen: + transport: http-json + baseUrl: https://nia-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/nia-relay-token } + request: + method: GET + path: /v1/datasets/population/entities/population/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/citizen-self-service } + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: uin, providerFields: "uin,identity_status,alive", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/uin, /data/*/identity_status, /data/*/alive, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/nia-population-facts.schema.yaml + sro-child-benefit: + transport: http-json + baseUrl: https://sro-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/sro-relay-token } + request: + method: GET + path: /v1/datasets/sro_child/entities/sro_child/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review } + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: uin, providerFields: "uin,poverty_band", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/uin, /data/*/poverty_band, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/sro-child-benefit-facts.schema.yaml + mosd-child-benefit: + transport: http-json + baseUrl: https://programme-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/programme-relay-token } + request: + method: GET + path: /v1/datasets/mosd_enroll/entities/mosd_enroll/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review } + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: uin, providerFields: "uin,duplicate_flag", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/uin, /data/*/duplicate_flag, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/mosd-child-benefit-facts.schema.yaml + sipf-pension: + transport: http-json + baseUrl: https://sipf-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/sipf-relay-token } + request: + method: GET + path: /v1/datasets/pension/entities/pension/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/pension-payment-review } + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: pensioner_uin, providerFields: "pensioner_uin,payment_status", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/pensioner_uin, /data/*/payment_status, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/sipf-pension-facts.schema.yaml + sipf-survivor: + transport: http-json + baseUrl: https://sipf-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/sipf-relay-token } + request: + method: GET + path: /v1/datasets/survivor/entities/survivor/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination } + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: spouse_uin, providerFields: "spouse_uin,survivor_eligible", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/spouse_uin, /data/*/survivor_eligible, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/sipf-survivor-facts.schema.yaml + nagdi-voucher: + transport: http-json + baseUrl: https://nagdi-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/nagdi-relay-token } + request: + method: GET + path: /v1/datasets/nagdi_farmer/entities/nagdi_farmer/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review } + selectorInputs: [{ role: subject, alternatives: [{ profile: farmer-reference-v1, fields: [farmer_id] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: farmer_id, providerFields: "farmer_id,farmer_registered,data_use_authorized,active_smallholder_farmer,active_farm_parcel,crop_declared_for_season,district_climate_risk_active,voucher_entitlement_current,voucher_not_redeemed", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/farmer_id, /data/*/farmer_registered, /data/*/data_use_authorized, /data/*/active_smallholder_farmer, /data/*/active_farm_parcel, /data/*/crop_declared_for_season, /data/*/district_climate_risk_active, /data/*/voucher_entitlement_current, /data/*/voucher_not_redeemed, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/nagdi-voucher-facts.schema.yaml + nagdi-livestock: + transport: http-json + baseUrl: https://nagdi-relay.evidence.solmara.invalid + tlsTrustProfile: solmara-lab + posture: field-projected + authentication: { kind: static-bearer, tokenRef: secret:file/nagdi-relay-token } + request: + method: GET + path: /v1/datasets/nagdi_herd/entities/nagdi_herd/records + fixedHeaders: + - { name: Accept, value: application/json } + - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/livestock-movement-control } + selectorInputs: [{ role: subject, alternatives: [{ profile: farmer-reference-v1, fields: [farmer_id] }] }] + prepareScript: adapters/prepare.rhai + adapterParameters: { selectorField: farmer_id, providerFields: "farmer_id,registered_herd,herd_vaccination_current,origin_district_not_quarantined_for_species,destination_district_open,no_conflicting_open_movement_permit", resultLimit: "2" } + adapterParametersSchema: schemas/adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } + projection: [/data/*/farmer_id, /data/*/registered_herd, /data/*/herd_vaccination_current, /data/*/origin_district_not_quarantined_for_species, /data/*/destination_district_open, /data/*/no_conflicting_open_movement_permit, /pagination/has_more] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/extract.rhai + factSchema: schemas/nagdi-livestock-facts.schema.yaml + +authorityProfiles: + solmara-demo-v1: + kind: statutory + requesterTags: [solmara-demo] + grants: + - requirement: https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1 + purpose: child-benefit-review + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] + - requirement: https://id.registrystack.org/solmara/requirement/cra-pension-death/v1 + purpose: pension-payment-review + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] + - requirement: https://id.registrystack.org/solmara/requirement/cra-citizen-record/v1 + purpose: citizen-self-service + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] + - requirement: https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1 + purpose: child-benefit-review + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] + - requirement: https://id.registrystack.org/solmara/requirement/nia-citizen-status/v1 + purpose: citizen-self-service + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] + - requirement: https://id.registrystack.org/solmara/requirement/sro-child-benefit/v1 + purpose: child-benefit-review + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] + - requirement: https://id.registrystack.org/solmara/requirement/mosd-child-benefit/v1 + purpose: child-benefit-review + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] + - requirement: https://id.registrystack.org/solmara/requirement/sipf-pension-payment/v1 + purpose: pension-payment-review + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] + - requirement: https://id.registrystack.org/solmara/requirement/sipf-survivor-benefit/v1 + purpose: survivor-benefit-determination + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] + - requirement: https://id.registrystack.org/solmara/requirement/nagdi-voucher/v1 + purpose: voucher-eligibility-review + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: farmer-reference-v1, valueOrigin: request }] + - requirement: https://id.registrystack.org/solmara/requirement/nagdi-livestock/v1 + purpose: livestock-movement-control + audienceFrom: authenticated-requester + responseFormats: [signed-jws] + subjects: [{ role: subject, selectorProfile: farmer-reference-v1, valueOrigin: request }] + +requirements: + - id: https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1 + kind: information-requirement + source: cra-child-benefit + purposes: [child-benefit-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/cra-child-benefit/v1 + observationTimezone: Asia/Bangkok + validitySeconds: 86400 + derivation: + script: derivations/cra-child-benefit.rhai + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + parameters: { as_of_date: "2026-07-14" } + concepts: + - { id: https://id.registrystack.org/solmara/concept/birth-is-registered, form: boolean, required: true, constraints: {} } + - { id: https://id.registrystack.org/solmara/concept/child-age-under-5, form: boolean, required: true, constraints: {} } + fixtures: fixtures/cra-child-benefit.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/cra-child-benefit] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/cra-pension-death/v1 + kind: information-requirement + source: cra-pension + purposes: [pension-payment-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/social-insurance/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/cra-death-status/v1 + validitySeconds: 86400 + derivation: { script: derivations/cra-deceased.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/person-is-deceased, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/cra-pension.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/cra-pension] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/cra-citizen-record/v1 + kind: information-requirement + source: cra-citizen + purposes: [citizen-self-service] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/citizen-self-service/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/civil-record-linked/v1 + validitySeconds: 86400 + derivation: { script: derivations/cra-linked.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/civil-record-linked, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/cra-citizen.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/cra-citizen] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1 + kind: information-requirement + source: nia-child-benefit + purposes: [child-benefit-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/population-active/v1 + validitySeconds: 86400 + derivation: + script: derivations/population-active.rhai + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + parameters: { concept_id: https://id.registrystack.org/solmara/concept/population-record-active } + concepts: [{ id: https://id.registrystack.org/solmara/concept/population-record-active, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/nia-child-benefit.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nia-child-benefit] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/nia-citizen-status/v1 + kind: information-requirement + source: nia-citizen + purposes: [citizen-self-service] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/citizen-self-service/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/citizen-population-active/v1 + validitySeconds: 86400 + derivation: + script: derivations/population-active.rhai + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + parameters: { concept_id: https://id.registrystack.org/solmara/concept/citizen-population-record-active } + concepts: [{ id: https://id.registrystack.org/solmara/concept/citizen-population-record-active, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/nia-citizen.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nia-citizen] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/sro-child-benefit/v1 + kind: information-requirement + source: sro-child-benefit + purposes: [child-benefit-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/poverty-priority/v1 + validitySeconds: 86400 + derivation: { script: derivations/poverty-priority.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/household-below-poverty-threshold, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/sro-child-benefit.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/sro-child-benefit] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/mosd-child-benefit/v1 + kind: information-requirement + source: mosd-child-benefit + purposes: [child-benefit-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/not-enrolled/v1 + validitySeconds: 86400 + derivation: { script: derivations/not-enrolled.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/not-already-enrolled, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/mosd-child-benefit.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/mosd-child-benefit] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/sipf-pension-payment/v1 + kind: information-requirement + source: sipf-pension + purposes: [pension-payment-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/social-insurance/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/pension-payment-active/v1 + validitySeconds: 86400 + derivation: { script: derivations/pension-active.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/pension-payment-active, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/sipf-pension.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/sipf-pension] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/sipf-survivor-benefit/v1 + kind: information-requirement + source: sipf-survivor + purposes: [survivor-benefit-determination] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/social-insurance/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/survivor-benefit/v1 + validitySeconds: 86400 + derivation: { script: derivations/survivor-eligible.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/survivor-is-eligible, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/sipf-survivor.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/sipf-survivor] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/nagdi-voucher/v1 + kind: information-requirement + source: nagdi-voucher + purposes: [voucher-eligibility-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [farmer-reference-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/agriculture/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/climate-smart-voucher/v1 + validitySeconds: 600 + derivation: { script: derivations/voucher.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: farmer-reference-v1, fields: [farmer_id] }] }], parameters: {} } + concepts: + - { id: https://id.registrystack.org/solmara/concept/farmer-registered, form: boolean, required: true, constraints: {} } + - { id: https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose, form: boolean, required: true, constraints: {} } + - { id: https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher, form: boolean, required: true, constraints: {} } + fixtures: fixtures/nagdi-voucher.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nagdi-voucher] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/nagdi-livestock/v1 + kind: information-requirement + source: nagdi-livestock + purposes: [livestock-movement-control] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [farmer-reference-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/agriculture/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/livestock-movement/v1 + validitySeconds: 600 + derivation: { script: derivations/livestock.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: farmer-reference-v1, fields: [farmer_id] }] }], parameters: {} } + concepts: + - { id: https://id.registrystack.org/solmara/concept/registered-herd, form: boolean, required: true, constraints: {} } + - { id: https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species, form: boolean, required: true, constraints: {} } + - { id: https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit, form: boolean, required: true, constraints: {} } + fixtures: fixtures/nagdi-livestock.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nagdi-livestock] } + existenceDisclosure: collapse-unresolved diff --git a/evidence/bundle/fixtures/cra-child-benefit.yaml b/evidence/bundle/fixtures/cra-child-benefit.yaml new file mode 100644 index 0000000..662956f --- /dev/null +++ b/evidence/bundle/fixtures/cra-child-benefit.yaml @@ -0,0 +1,62 @@ +fixture: registry.evidence.reference.solmara.cra-child-benefit/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000001" } } } + derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000001" } } } + expectedRequestParts: + query: [{ name: uin, value: "1000000001" }, { name: fields, value: "uin,birth_date,birth_brn,deceased" }, { name: limit, value: "2" }] + body: null + expectedTransport: + path: /v1/datasets/civil_people/entities/civil_people/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review }] +cases: + - id: positive + response: { data: [{ uin: "1000000001", birth_date: "2022-08-01", birth_brn: BRN-0001, deceased: false }], pagination: { has_more: false } } + expected: + lookup: match + derivationRuns: true + signed: true + values: + https://id.registrystack.org/solmara/concept/birth-is-registered: true + https://id.registrystack.org/solmara/concept/child-age-under-5: true + - id: negative-subject-mismatch + response: { data: [{ uin: "9999999999", birth_date: "2022-08-01", birth_brn: BRN-0001, deceased: false }], pagination: { has_more: false } } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-fifth-birthday + response: { data: [{ uin: "1000000001", birth_date: "2021-07-14", birth_brn: BRN-0001, deceased: false }], pagination: { has_more: false } } + expected: + lookup: match + derivationRuns: true + signed: true + values: + https://id.registrystack.org/solmara/concept/birth-is-registered: true + https://id.registrystack.org/solmara/concept/child-age-under-5: false + - id: unregistered-birth + response: { data: [{ uin: "1000000001", birth_date: "2022-08-01", birth_brn: null, deceased: false }], pagination: { has_more: false } } + expected: + lookup: match + derivationRuns: true + signed: true + values: + https://id.registrystack.org/solmara/concept/birth-is-registered: false + https://id.registrystack.org/solmara/concept/child-age-under-5: true + - id: missing-fact + response: { data: [{ uin: "1000000001", birth_date: "2022-08-01", deceased: false }], pagination: { has_more: false } } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: { data: [{ uin: "1000000001", birth_date: "2022-08-01", birth_brn: BRN-0001, deceased: false }, { uin: "1000000001", birth_date: "2021-05-01", birth_brn: BRN-0002, deceased: false }], pagination: { has_more: true } } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/birth-is-registered, https://id.registrystack.org/solmara/concept/child-age-under-5] + evidenceExcludes: [uin, birth_date, birth_brn, deceased, "1000000001", BRN-0001] + diagnosticsExclude: ["1000000001", BRN-0001, "2022-08-01"] diff --git a/evidence/bundle/fixtures/cra-citizen.yaml b/evidence/bundle/fixtures/cra-citizen.yaml new file mode 100644 index 0000000..df11851 --- /dev/null +++ b/evidence/bundle/fixtures/cra-citizen.yaml @@ -0,0 +1,39 @@ +fixture: registry.evidence.reference.solmara.cra-citizen/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000003" } } } + derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000003" } } } + expectedRequestParts: { query: [{ name: uin, value: "1000000003" }, { name: fields, value: uin }, { name: limit, value: "2" }], body: null } + expectedTransport: + path: /v1/datasets/civil_people/entities/civil_people/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/citizen-self-service }] +cases: + - id: positive + response: { data: [{ uin: "1000000003" }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: negative-subject-mismatch + response: { data: [{ uin: "9999999999" }], pagination: { has_more: false } } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-linked-record + response: { data: [{ uin: "1000000003" }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: missing-fact + response: { data: [{}], pagination: { has_more: false } } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: { data: [{ uin: "1000000003" }, { uin: "1000000003" }], pagination: { has_more: true } } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/civil-record-linked] + evidenceExcludes: [uin, "1000000003"] + diagnosticsExclude: ["1000000003"] diff --git a/evidence/bundle/fixtures/cra-pension.yaml b/evidence/bundle/fixtures/cra-pension.yaml new file mode 100644 index 0000000..7368156 --- /dev/null +++ b/evidence/bundle/fixtures/cra-pension.yaml @@ -0,0 +1,39 @@ +fixture: registry.evidence.reference.solmara.cra-pension/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000002" } } } + derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000002" } } } + expectedRequestParts: { query: [{ name: uin, value: "1000000002" }, { name: fields, value: "uin,deceased" }, { name: limit, value: "2" }], body: null } + expectedTransport: + path: /v1/datasets/civil_people/entities/civil_people/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/pension-payment-review }] +cases: + - id: positive + response: { data: [{ uin: "1000000002", deceased: true }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: negative-subject-mismatch + response: { data: [{ uin: "9999999999", deceased: true }], pagination: { has_more: false } } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-living + response: { data: [{ uin: "1000000002", deceased: false }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: missing-fact + response: { data: [{ uin: "1000000002" }], pagination: { has_more: false } } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: { data: [{ uin: "1000000002", deceased: true }, { uin: "1000000002", deceased: false }], pagination: { has_more: true } } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/person-is-deceased] + evidenceExcludes: [uin, deceased, "1000000002"] + diagnosticsExclude: ["1000000002"] diff --git a/evidence/bundle/fixtures/mosd-child-benefit.yaml b/evidence/bundle/fixtures/mosd-child-benefit.yaml new file mode 100644 index 0000000..963ee7a --- /dev/null +++ b/evidence/bundle/fixtures/mosd-child-benefit.yaml @@ -0,0 +1,39 @@ +fixture: registry.evidence.reference.solmara.mosd-child-benefit/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000007" } } } + derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000007" } } } + expectedRequestParts: { query: [{ name: uin, value: "1000000007" }, { name: fields, value: "uin,duplicate_flag" }, { name: limit, value: "2" }], body: null } + expectedTransport: + path: /v1/datasets/mosd_enroll/entities/mosd_enroll/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review }] +cases: + - id: positive + response: { data: [{ uin: "1000000007", duplicate_flag: false }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: negative-subject-mismatch + response: { data: [{ uin: "9999999999", duplicate_flag: false }], pagination: { has_more: false } } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-already-enrolled + response: { data: [{ uin: "1000000007", duplicate_flag: true }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: missing-fact + response: { data: [{ uin: "1000000007" }], pagination: { has_more: false } } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: { data: [{ uin: "1000000007", duplicate_flag: false }, { uin: "1000000007", duplicate_flag: true }], pagination: { has_more: true } } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/not-already-enrolled] + evidenceExcludes: [uin, duplicate_flag, "1000000007"] + diagnosticsExclude: ["1000000007"] diff --git a/evidence/bundle/fixtures/nagdi-livestock.yaml b/evidence/bundle/fixtures/nagdi-livestock.yaml new file mode 100644 index 0000000..e6fd573 --- /dev/null +++ b/evidence/bundle/fixtures/nagdi-livestock.yaml @@ -0,0 +1,65 @@ +fixture: registry.evidence.reference.solmara.nagdi-livestock/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: farmer-reference-v1, values: { farmer_id: FR-000002 } } } + derivationSelectorInputs: { subject: { profile: farmer-reference-v1, values: { farmer_id: FR-000002 } } } + expectedRequestParts: { query: [{ name: farmer_id, value: FR-000002 }, { name: fields, value: "farmer_id,registered_herd,herd_vaccination_current,origin_district_not_quarantined_for_species,destination_district_open,no_conflicting_open_movement_permit" }, { name: limit, value: "2" }], body: null } + expectedTransport: + path: /v1/datasets/nagdi_herd/entities/nagdi_herd/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/livestock-movement-control }] +cases: + - id: positive + response: + data: [{ farmer_id: FR-000002, registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: true, destination_district_open: true, no_conflicting_open_movement_permit: true }] + pagination: { has_more: false } + expected: + lookup: match + derivationRuns: true + signed: true + values: + https://id.registrystack.org/solmara/concept/registered-herd: true + https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species: true + https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit: true + - id: negative-subject-mismatch + response: + data: [{ farmer_id: FR-999999, registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: true, destination_district_open: true, no_conflicting_open_movement_permit: true }] + pagination: { has_more: false } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-quarantined + response: + data: [{ farmer_id: FR-000002, registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: false, destination_district_open: true, no_conflicting_open_movement_permit: true }] + pagination: { has_more: false } + expected: + lookup: match + derivationRuns: true + signed: true + values: + https://id.registrystack.org/solmara/concept/registered-herd: true + https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species: false + https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit: false + - id: missing-fact + response: + data: [{ farmer_id: FR-000002, registered_herd: true, herd_vaccination_current: true, destination_district_open: true, no_conflicting_open_movement_permit: true }] + pagination: { has_more: false } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: + data: + - { farmer_id: FR-000002, registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: true, destination_district_open: true, no_conflicting_open_movement_permit: true } + - { farmer_id: FR-000002, registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: false, destination_district_open: true, no_conflicting_open_movement_permit: true } + pagination: { has_more: true } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/registered-herd, https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species, https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit] + evidenceExcludes: [farmer_id, registered_herd, origin_district_not_quarantined_for_species, FR-000002] + diagnosticsExclude: [FR-000002] diff --git a/evidence/bundle/fixtures/nagdi-voucher.yaml b/evidence/bundle/fixtures/nagdi-voucher.yaml new file mode 100644 index 0000000..7d22ad0 --- /dev/null +++ b/evidence/bundle/fixtures/nagdi-voucher.yaml @@ -0,0 +1,67 @@ +fixture: registry.evidence.reference.solmara.nagdi-voucher/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: farmer-reference-v1, values: { farmer_id: FR-000001 } } } + derivationSelectorInputs: { subject: { profile: farmer-reference-v1, values: { farmer_id: FR-000001 } } } + expectedRequestParts: + query: [{ name: farmer_id, value: FR-000001 }, { name: fields, value: "farmer_id,farmer_registered,data_use_authorized,active_smallholder_farmer,active_farm_parcel,crop_declared_for_season,district_climate_risk_active,voucher_entitlement_current,voucher_not_redeemed" }, { name: limit, value: "2" }] + body: null + expectedTransport: + path: /v1/datasets/nagdi_farmer/entities/nagdi_farmer/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review }] +cases: + - id: positive + response: + data: [{ farmer_id: FR-000001, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: true }] + pagination: { has_more: false } + expected: + lookup: match + derivationRuns: true + signed: true + values: + https://id.registrystack.org/solmara/concept/farmer-registered: true + https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose: true + https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher: true + - id: negative-subject-mismatch + response: + data: [{ farmer_id: FR-999999, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: true }] + pagination: { has_more: false } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-redeemed + response: + data: [{ farmer_id: FR-000001, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: false }] + pagination: { has_more: false } + expected: + lookup: match + derivationRuns: true + signed: true + values: + https://id.registrystack.org/solmara/concept/farmer-registered: true + https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose: true + https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher: false + - id: missing-fact + response: + data: [{ farmer_id: FR-000001, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true }] + pagination: { has_more: false } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: + data: + - { farmer_id: FR-000001, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: true } + - { farmer_id: FR-000001, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: false } + pagination: { has_more: true } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/farmer-registered, https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose, https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher] + evidenceExcludes: [farmer_id, farmer_registered, data_use_authorized, FR-000001] + diagnosticsExclude: [FR-000001] diff --git a/evidence/bundle/fixtures/nia-child-benefit.yaml b/evidence/bundle/fixtures/nia-child-benefit.yaml new file mode 100644 index 0000000..1ed5d4b --- /dev/null +++ b/evidence/bundle/fixtures/nia-child-benefit.yaml @@ -0,0 +1,39 @@ +fixture: registry.evidence.reference.solmara.nia-child-benefit/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000004" } } } + derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000004" } } } + expectedRequestParts: { query: [{ name: uin, value: "1000000004" }, { name: fields, value: "uin,identity_status,alive" }, { name: limit, value: "2" }], body: null } + expectedTransport: + path: /v1/datasets/population/entities/population/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review }] +cases: + - id: positive + response: { data: [{ uin: "1000000004", identity_status: active, alive: true }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: negative-subject-mismatch + response: { data: [{ uin: "9999999999", identity_status: active, alive: true }], pagination: { has_more: false } } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-inactive + response: { data: [{ uin: "1000000004", identity_status: inactive, alive: true }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: missing-fact + response: { data: [{ uin: "1000000004", identity_status: active }], pagination: { has_more: false } } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: { data: [{ uin: "1000000004", identity_status: active, alive: true }, { uin: "1000000004", identity_status: inactive, alive: true }], pagination: { has_more: true } } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/population-record-active] + evidenceExcludes: [uin, identity_status, alive, "1000000004"] + diagnosticsExclude: ["1000000004"] diff --git a/evidence/bundle/fixtures/nia-citizen.yaml b/evidence/bundle/fixtures/nia-citizen.yaml new file mode 100644 index 0000000..c3203aa --- /dev/null +++ b/evidence/bundle/fixtures/nia-citizen.yaml @@ -0,0 +1,39 @@ +fixture: registry.evidence.reference.solmara.nia-citizen/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000005" } } } + derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000005" } } } + expectedRequestParts: { query: [{ name: uin, value: "1000000005" }, { name: fields, value: "uin,identity_status,alive" }, { name: limit, value: "2" }], body: null } + expectedTransport: + path: /v1/datasets/population/entities/population/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/citizen-self-service }] +cases: + - id: positive + response: { data: [{ uin: "1000000005", identity_status: active, alive: true }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: negative-subject-mismatch + response: { data: [{ uin: "9999999999", identity_status: active, alive: true }], pagination: { has_more: false } } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-deceased + response: { data: [{ uin: "1000000005", identity_status: active, alive: false }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: missing-fact + response: { data: [{ uin: "1000000005", alive: true }], pagination: { has_more: false } } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: { data: [{ uin: "1000000005", identity_status: active, alive: true }, { uin: "1000000005", identity_status: inactive, alive: true }], pagination: { has_more: true } } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/citizen-population-record-active] + evidenceExcludes: [uin, identity_status, alive, "1000000005"] + diagnosticsExclude: ["1000000005"] diff --git a/evidence/bundle/fixtures/sipf-pension.yaml b/evidence/bundle/fixtures/sipf-pension.yaml new file mode 100644 index 0000000..00b410e --- /dev/null +++ b/evidence/bundle/fixtures/sipf-pension.yaml @@ -0,0 +1,39 @@ +fixture: registry.evidence.reference.solmara.sipf-pension/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000008" } } } + derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000008" } } } + expectedRequestParts: { query: [{ name: pensioner_uin, value: "1000000008" }, { name: fields, value: "pensioner_uin,payment_status" }, { name: limit, value: "2" }], body: null } + expectedTransport: + path: /v1/datasets/pension/entities/pension/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/pension-payment-review }] +cases: + - id: positive + response: { data: [{ pensioner_uin: "1000000008", payment_status: active }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: negative-subject-mismatch + response: { data: [{ pensioner_uin: "9999999999", payment_status: active }], pagination: { has_more: false } } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-suspended + response: { data: [{ pensioner_uin: "1000000008", payment_status: suspended }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: missing-fact + response: { data: [{ pensioner_uin: "1000000008" }], pagination: { has_more: false } } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: { data: [{ pensioner_uin: "1000000008", payment_status: active }, { pensioner_uin: "1000000008", payment_status: suspended }], pagination: { has_more: true } } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/pension-payment-active] + evidenceExcludes: [pensioner_uin, payment_status, "1000000008"] + diagnosticsExclude: ["1000000008"] diff --git a/evidence/bundle/fixtures/sipf-survivor.yaml b/evidence/bundle/fixtures/sipf-survivor.yaml new file mode 100644 index 0000000..97b1f9c --- /dev/null +++ b/evidence/bundle/fixtures/sipf-survivor.yaml @@ -0,0 +1,39 @@ +fixture: registry.evidence.reference.solmara.sipf-survivor/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000009" } } } + derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000009" } } } + expectedRequestParts: { query: [{ name: spouse_uin, value: "1000000009" }, { name: fields, value: "spouse_uin,survivor_eligible" }, { name: limit, value: "2" }], body: null } + expectedTransport: + path: /v1/datasets/survivor/entities/survivor/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination }] +cases: + - id: positive + response: { data: [{ spouse_uin: "1000000009", survivor_eligible: true }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: negative-subject-mismatch + response: { data: [{ spouse_uin: "9999999999", survivor_eligible: true }], pagination: { has_more: false } } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-ineligible + response: { data: [{ spouse_uin: "1000000009", survivor_eligible: false }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: missing-fact + response: { data: [{ spouse_uin: "1000000009" }], pagination: { has_more: false } } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: { data: [{ spouse_uin: "1000000009", survivor_eligible: true }, { spouse_uin: "1000000009", survivor_eligible: false }], pagination: { has_more: true } } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/survivor-is-eligible] + evidenceExcludes: [spouse_uin, survivor_eligible, "1000000009"] + diagnosticsExclude: ["1000000009"] diff --git a/evidence/bundle/fixtures/sro-child-benefit.yaml b/evidence/bundle/fixtures/sro-child-benefit.yaml new file mode 100644 index 0000000..a4cb64a --- /dev/null +++ b/evidence/bundle/fixtures/sro-child-benefit.yaml @@ -0,0 +1,39 @@ +fixture: registry.evidence.reference.solmara.sro-child-benefit/v1 +synthetic_only: true +common: + observed_at: "2026-07-14T00:00:00Z" + selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000006" } } } + derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000006" } } } + expectedRequestParts: { query: [{ name: uin, value: "1000000006" }, { name: fields, value: "uin,poverty_band" }, { name: limit, value: "2" }], body: null } + expectedTransport: + path: /v1/datasets/sro_child/entities/sro_child/records + fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review }] +cases: + - id: positive + response: { data: [{ uin: "1000000006", poverty_band: priority }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: negative-subject-mismatch + response: { data: [{ uin: "9999999999", poverty_band: priority }], pagination: { has_more: false } } + expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } + - id: boundary-non-priority + response: { data: [{ uin: "1000000006", poverty_band: standard }], pagination: { has_more: false } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: missing-fact + response: { data: [{ uin: "1000000006" }], pagination: { has_more: false } } + expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: no-match + response: { data: [], pagination: { has_more: false } } + expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: ambiguous + response: { data: [{ uin: "1000000006", poverty_band: priority }, { uin: "1000000006", poverty_band: standard }], pagination: { has_more: true } } + expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: dependency_unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: + evidenceContains: [https://id.registrystack.org/solmara/concept/household-below-poverty-threshold] + evidenceExcludes: [uin, poverty_band, "1000000006"] + diagnosticsExclude: ["1000000006"] diff --git a/evidence/bundle/schemas/adapter-parameters.schema.yaml b/evidence/bundle/schemas/adapter-parameters.schema.yaml new file mode 100644 index 0000000..1adeb08 --- /dev/null +++ b/evidence/bundle/schemas/adapter-parameters.schema.yaml @@ -0,0 +1,7 @@ +type: object +additionalProperties: false +required: [selectorField, providerFields, resultLimit] +properties: + selectorField: { type: string, enum: [uin, pensioner_uin, spouse_uin, farmer_id] } + providerFields: { type: string, minLength: 1, maxLength: 512 } + resultLimit: { const: "2" } diff --git a/evidence/bundle/schemas/cra-child-benefit-facts.schema.yaml b/evidence/bundle/schemas/cra-child-benefit-facts.schema.yaml new file mode 100644 index 0000000..87091dc --- /dev/null +++ b/evidence/bundle/schemas/cra-child-benefit-facts.schema.yaml @@ -0,0 +1,8 @@ +type: object +additionalProperties: false +required: [uin, birth_date, birth_registered, deceased] +properties: + uin: { type: string, minLength: 10, maxLength: 10 } + birth_date: { type: string, format: date, maxLength: 10 } + birth_registered: { type: boolean } + deceased: { type: boolean } diff --git a/evidence/bundle/schemas/cra-citizen-facts.schema.yaml b/evidence/bundle/schemas/cra-citizen-facts.schema.yaml new file mode 100644 index 0000000..73c2808 --- /dev/null +++ b/evidence/bundle/schemas/cra-citizen-facts.schema.yaml @@ -0,0 +1,5 @@ +type: object +additionalProperties: false +required: [uin] +properties: + uin: { type: string, minLength: 10, maxLength: 10 } diff --git a/evidence/bundle/schemas/cra-pension-facts.schema.yaml b/evidence/bundle/schemas/cra-pension-facts.schema.yaml new file mode 100644 index 0000000..3dfc2b3 --- /dev/null +++ b/evidence/bundle/schemas/cra-pension-facts.schema.yaml @@ -0,0 +1,6 @@ +type: object +additionalProperties: false +required: [uin, deceased] +properties: + uin: { type: string, minLength: 10, maxLength: 10 } + deceased: { type: boolean } diff --git a/evidence/bundle/schemas/mosd-child-benefit-facts.schema.yaml b/evidence/bundle/schemas/mosd-child-benefit-facts.schema.yaml new file mode 100644 index 0000000..cbcbc37 --- /dev/null +++ b/evidence/bundle/schemas/mosd-child-benefit-facts.schema.yaml @@ -0,0 +1,6 @@ +type: object +additionalProperties: false +required: [uin, duplicate_flag] +properties: + uin: { type: string, minLength: 10, maxLength: 10 } + duplicate_flag: { type: boolean } diff --git a/evidence/bundle/schemas/nagdi-livestock-facts.schema.yaml b/evidence/bundle/schemas/nagdi-livestock-facts.schema.yaml new file mode 100644 index 0000000..141fc31 --- /dev/null +++ b/evidence/bundle/schemas/nagdi-livestock-facts.schema.yaml @@ -0,0 +1,10 @@ +type: object +additionalProperties: false +required: [farmer_id, registered_herd, herd_vaccination_current, origin_district_not_quarantined_for_species, destination_district_open, no_conflicting_open_movement_permit] +properties: + farmer_id: { type: string, minLength: 7, maxLength: 16 } + registered_herd: { type: boolean } + herd_vaccination_current: { type: boolean } + origin_district_not_quarantined_for_species: { type: boolean } + destination_district_open: { type: boolean } + no_conflicting_open_movement_permit: { type: boolean } diff --git a/evidence/bundle/schemas/nagdi-voucher-facts.schema.yaml b/evidence/bundle/schemas/nagdi-voucher-facts.schema.yaml new file mode 100644 index 0000000..1678c09 --- /dev/null +++ b/evidence/bundle/schemas/nagdi-voucher-facts.schema.yaml @@ -0,0 +1,13 @@ +type: object +additionalProperties: false +required: [farmer_id, farmer_registered, data_use_authorized, active_smallholder_farmer, active_farm_parcel, crop_declared_for_season, district_climate_risk_active, voucher_entitlement_current, voucher_not_redeemed] +properties: + farmer_id: { type: string, minLength: 7, maxLength: 16 } + farmer_registered: { type: boolean } + data_use_authorized: { type: boolean } + active_smallholder_farmer: { type: boolean } + active_farm_parcel: { type: boolean } + crop_declared_for_season: { type: boolean } + district_climate_risk_active: { type: boolean } + voucher_entitlement_current: { type: boolean } + voucher_not_redeemed: { type: boolean } diff --git a/evidence/bundle/schemas/nia-population-facts.schema.yaml b/evidence/bundle/schemas/nia-population-facts.schema.yaml new file mode 100644 index 0000000..5df515c --- /dev/null +++ b/evidence/bundle/schemas/nia-population-facts.schema.yaml @@ -0,0 +1,7 @@ +type: object +additionalProperties: false +required: [uin, identity_status, alive] +properties: + uin: { type: string, minLength: 10, maxLength: 10 } + identity_status: { type: string, minLength: 1, maxLength: 32 } + alive: { type: boolean } diff --git a/evidence/bundle/schemas/response.schema.yaml b/evidence/bundle/schemas/response.schema.yaml new file mode 100644 index 0000000..c2fcecf --- /dev/null +++ b/evidence/bundle/schemas/response.schema.yaml @@ -0,0 +1,45 @@ +type: object +additionalProperties: false +required: [data, pagination] +properties: + data: + type: array + minItems: 0 + maxItems: 2 + items: + type: object + additionalProperties: false + required: [] + properties: + uin: { type: string, minLength: 10, maxLength: 10 } + pensioner_uin: { type: string, minLength: 10, maxLength: 10 } + spouse_uin: { type: string, minLength: 10, maxLength: 10 } + farmer_id: { type: string, minLength: 7, maxLength: 16 } + birth_date: { type: string, format: date, maxLength: 10 } + birth_brn: { type: [string, "null"], maxLength: 32 } + deceased: { type: boolean } + identity_status: { type: string, maxLength: 32 } + alive: { type: boolean } + poverty_band: { type: string, maxLength: 16 } + duplicate_flag: { type: boolean } + payment_status: { type: string, maxLength: 32 } + survivor_eligible: { type: boolean } + farmer_registered: { type: boolean } + data_use_authorized: { type: boolean } + active_smallholder_farmer: { type: boolean } + active_farm_parcel: { type: boolean } + crop_declared_for_season: { type: boolean } + district_climate_risk_active: { type: boolean } + voucher_entitlement_current: { type: boolean } + voucher_not_redeemed: { type: boolean } + registered_herd: { type: boolean } + herd_vaccination_current: { type: boolean } + origin_district_not_quarantined_for_species: { type: boolean } + destination_district_open: { type: boolean } + no_conflicting_open_movement_permit: { type: boolean } + pagination: + type: object + additionalProperties: false + required: [has_more] + properties: + has_more: { type: boolean } diff --git a/evidence/bundle/schemas/sipf-pension-facts.schema.yaml b/evidence/bundle/schemas/sipf-pension-facts.schema.yaml new file mode 100644 index 0000000..0c7b7a8 --- /dev/null +++ b/evidence/bundle/schemas/sipf-pension-facts.schema.yaml @@ -0,0 +1,6 @@ +type: object +additionalProperties: false +required: [pensioner_uin, payment_status] +properties: + pensioner_uin: { type: string, minLength: 10, maxLength: 10 } + payment_status: { type: string, minLength: 1, maxLength: 32 } diff --git a/evidence/bundle/schemas/sipf-survivor-facts.schema.yaml b/evidence/bundle/schemas/sipf-survivor-facts.schema.yaml new file mode 100644 index 0000000..45ba70b --- /dev/null +++ b/evidence/bundle/schemas/sipf-survivor-facts.schema.yaml @@ -0,0 +1,6 @@ +type: object +additionalProperties: false +required: [spouse_uin, survivor_eligible] +properties: + spouse_uin: { type: string, minLength: 10, maxLength: 10 } + survivor_eligible: { type: boolean } diff --git a/evidence/bundle/schemas/sro-child-benefit-facts.schema.yaml b/evidence/bundle/schemas/sro-child-benefit-facts.schema.yaml new file mode 100644 index 0000000..f3dd348 --- /dev/null +++ b/evidence/bundle/schemas/sro-child-benefit-facts.schema.yaml @@ -0,0 +1,6 @@ +type: object +additionalProperties: false +required: [uin, poverty_band] +properties: + uin: { type: string, minLength: 10, maxLength: 10 } + poverty_band: { type: string, minLength: 1, maxLength: 16 } diff --git a/evidence/mint.yaml b/evidence/mint.yaml new file mode 100644 index 0000000..6aac74d --- /dev/null +++ b/evidence/mint.yaml @@ -0,0 +1,30 @@ +version: 1 +issuer: https://mint.evidence.solmara.invalid +listener: + address: 0.0.0.0 + port: 8081 +signing: + algorithm: EdDSA + activeKeyId: solmara-mint-signing-key-1 + activeKeyFile: /run/secrets/registry-mint/signing.jwk + retiredPublicJwkFiles: [] +audit: + path: /var/lib/registry-mint/audit/mint.jsonl + maximumFileBytes: 1073741824 + hashKeyFile: /run/secrets/registry-mint/audit-hmac-key + hashKeyVersion: 1 +accessTokens: + audiences: [solmara-evidence] + lifetimeSeconds: 300 + claims: + principal: sub + requesterTags: evidence_tags + evidenceAudience: evidence_audience + grantId: evidence_grant_id + grantAuthority: evidence_authority +clientAssertion: + audience: https://mint.evidence.solmara.invalid/token + maximumLifetimeSeconds: 300 + algorithms: [EdDSA] +clients: + directory: /etc/registry-mint/clients diff --git a/evidence/runtime.yaml b/evidence/runtime.yaml new file mode 100644 index 0000000..91c7d96 --- /dev/null +++ b/evidence/runtime.yaml @@ -0,0 +1,22 @@ +version: 1 +bundleDirectory: /etc/registry-evidence/bundle +listener: + bindHost: 172.29.0.10 + port: 8080 + tlsTermination: operator-controlled-upstream + trustProxyIdentityHeaders: false + maximumRequestBytes: 65536 + maximumConcurrentRequests: 64 + requestTimeoutMilliseconds: 10000 + shutdownGraceMilliseconds: 30000 +secretProviders: + file: + root: /run/secrets/registry-evidence +auditStorage: + path: /var/lib/registry-evidence/audit/evidence.jsonl + maximumFileBytes: 1073741824 +outboundTls: + systemRoots: true + trustProfiles: + solmara-lab: + caBundleFile: /etc/registry-evidence/tls/lab-ca.crt diff --git a/generator/output/checksums.sha256 b/generator/output/checksums.sha256 index 6980158..09b6369 100644 --- a/generator/output/checksums.sha256 +++ b/generator/output/checksums.sha256 @@ -65,22 +65,22 @@ d9e59a69c8168bd3374b375fb3e9897ded4acd74b600309d446e507be2543265 ministries/int 815396c62cb0d8b93d2c928aeb91c9b398318267dd5ff1288180de970d064f8d ministries/interior-civil/fixtures/relationship.csv 5880193db8814db61d0ea548c6fd575fc4a1a80f2bcfb268fb09ddbb85c5702f ministries/interior-civil/manifest/fragment.yaml 8c03c745c9db517d1899a2172f27f7b081e18ed5707ff13697cf21ec03c8c550 ministries/interior-population/crosswalks/person.publicschema.yaml -9b638a0bc384f49e0f03bd786e96104b2ee3ed200462e10586fb400e131cc545 ministries/interior-population/fixtures/001-schema.sql -249b804280da2f2673355b58574e751ec530463947f7e036d9292a1cb6f94c04 ministries/interior-population/fixtures/002-load.sql +47c48c0796ad863ce048eb95b3e736487aa4a38db0a8c642c63a783886e20215 ministries/interior-population/fixtures/001-schema.sql +8dcc7a0e6501f0cbf1c594486a273bcde05911aa6cbe20dcfda99d83b25c79da ministries/interior-population/fixtures/002-load.sql 9c8ba5f60b8de794a2683ed681007965d2cae464e856c50b26e6ec49ed6a0fce ministries/interior-population/fixtures/consent_directive.csv 7f7f5a9a43fda8afc647bbe9bb67b64ee6f4fc1f8d52ba4656ad86a589746109 ministries/interior-population/fixtures/identity_document.csv e27ca0df1c1de91eb71673de25e5f02f48a14d7ac4551daa1bd39e8c887d551b ministries/interior-population/fixtures/population_person.csv 1bb558153f13b9c9ef4b730a0eebff7445e8d967b0945b224128e1d6f6389808 ministries/interior-population/manifest/fragment.yaml 6fe91b758a235881a2951069ad3663878982da0b91996fd8aafe0201e92e8094 ministries/labour-pensions/crosswalks/pension-case.publicschema.yaml -4990226a52462d100b3b3224c680f29a8d6b0a893d44ecf3ccc70162abd56e29 ministries/labour-pensions/fixtures/003-schema.sql -e3fe1d0faccede590348f6c2fb1e04bb08599d6c68f20da803acccc03393937b ministries/labour-pensions/fixtures/004-load.sql -8ff8cefbe323cab01331bac9cc08274ba3df20e6b426a3cfc5a37927bfec103b ministries/labour-pensions/fixtures/pension_case.csv +069c71335871825c38280901a5c64dc41ccce91d11c2c62c919a88899dcde24e ministries/labour-pensions/fixtures/003-schema.sql +b1ff968f1fbdf632bcd57207cd1d6ae724a12ed8967fb17f6b015ed73941c060 ministries/labour-pensions/fixtures/004-load.sql +bec871a255a240ba4077b914be691b1eb5a76e9f4c776e1ddca8737a09f0bd3a ministries/labour-pensions/fixtures/pension_case.csv 52bd856ce77c8747bab41e0de875846bef7e98a4c23eca1bf2616a388f258193 ministries/labour-pensions/fixtures/sipf_contribution_account.csv 98744875ad4080a8d53064a2850b19e299331595aef58ed7bf055bf069cf1187 ministries/labour-pensions/fixtures/sipf_contribution_period.csv 51a3f783312190393a4a8f299a38b3187965a03bc46302dda5e6a78b8915d3e0 ministries/labour-pensions/fixtures/sipf_payment_instruction.csv 47d32a1528cb5bc04c9d97317493e0b91870e5f9799dedc54e7cb4c9730f0a60 ministries/labour-pensions/fixtures/sipf_pension_award.csv d8c446c918e802acc44b813754f87a5f38b93678e669def4d03f2da939026ead ministries/labour-pensions/fixtures/sipf_proof_of_life_check.csv -5185ec453a3e21572a5e3838ac7a2dff0a0be1cf5209ec2dd997ec2ef5a35e3b ministries/labour-pensions/fixtures/sipf_survivor_link.csv +bbc8ffe85cd6c941889eab4f70f5f0e908e57ccd994f6132f3f723aaf855e549 ministries/labour-pensions/fixtures/sipf_survivor_link.csv dbb3bddd608356edd4b2e6dd9a201c535107068b097e58482835863618d16620 ministries/labour-pensions/manifest/fragment.yaml e76e4e8546d47d6b52957c8466056ec113304676804ca717a6b4dd7c6c99b879 ministries/social-development/crosswalks/enrollment.publicschema.yaml 9f844f6668958a8d072d921b7b41ed1b360214058ba97e5f29e602f50a00bf86 ministries/social-development/crosswalks/household.publicschema.yaml diff --git a/generator/solmara_lab/generate.py b/generator/solmara_lab/generate.py index 6b4cf28..46ad418 100644 --- a/generator/solmara_lab/generate.py +++ b/generator/solmara_lab/generate.py @@ -467,6 +467,7 @@ def build_rows(root: Path) -> dict[str, list[dict[str, object]]]: instructions.append(add_meta({"instruction_id": f"SIPF-PI-{idx:06d}", "award_no": award_no, "beneficiary_uin": uins[key], "pay_period_month": "2026-07-01", "amount": "240.00", "currency": "XTS", "payment_status": "released" if key == "deceased_pensioner" else "scheduled", "hold_reason": "none", "released_at": "2026-07-01T10:00:00Z" if key == "deceased_pensioner" else ""}, "SIPF-CORE")) pol.append(add_meta({"check_id": f"SIPF-POL-{idx:06d}", "award_no": award_no, "beneficiary_uin": uins[key], "method": "civil_registry_crosscheck", "result": "deceased_found" if key == "deceased_pensioner" else "confirmed_alive", "checked_date": "2026-06-01", "next_due_date": "2026-06-01" if key == "survivor_waits" else "2026-12-01"}, "SIPF-CORE")) survivor.append(add_meta({"survivor_link_id": "SIPF-SL-000001", "deceased_uin": uins["deceased_pensioner"], "survivor_uin": uins["survivor_spouse"], "relationship": "spouse", "proof_mrn": "MRN-1970-0401-00001", "survivor_award_no": "", "link_status": "verified"}, "SIPF-CORE")) + survivor.append(add_meta({"survivor_link_id": "SIPF-SL-000002", "deceased_uin": uins["survivor_waits"], "survivor_uin": uins["divorced_head"], "relationship": "former_spouse", "proof_mrn": "MRN-2010-0302-00002", "survivor_award_no": "", "link_status": "dissolved"}, "SIPF-CORE")) nagdi = build_nagdi(uins, by_uin, household_ids) return { "population_person": [add_meta(p, "NIA-SOLMARAID") for p in people], @@ -749,8 +750,33 @@ def generate(root: Path) -> None: write_csv(root / "ministries/interior-civil/fixtures" / f"{table}.csv", rows[table]) for table in ["population_person", "identity_document", "consent_directive"]: write_csv(root / "ministries/interior-population/fixtures" / f"{table}.csv", rows[table]) - write_text(root / "ministries/interior-population/fixtures/001-schema.sql", "create table population_person (uin text primary key, person_id text, legacy_nid text, given_name text, family_name text, birth_date date, sex text, district_code text, address_area text, settlement_type text, identity_status text, pending_merge_with_uin text, match_basis text, alive boolean, birth_brn text, updated_at timestamptz, observed_at timestamptz, source_system text);\n") - write_text(root / "ministries/interior-population/fixtures/002-load.sql", "copy population_person from '/docker-entrypoint-initdb.d/population_person.csv' with (format csv, header true);\n") + write_text(root / "ministries/interior-population/fixtures/001-schema.sql", "create table if not exists population_person (uin text primary key, person_id text, legacy_nid text, given_name text, family_name text, birth_date date, sex text, district_code text, address_area text, settlement_type text, identity_status text, pending_merge_with_uin text, match_basis text, alive boolean, birth_brn text, updated_at timestamptz, observed_at timestamptz, source_system text);\n") + write_text(root / "ministries/interior-population/fixtures/002-load.sql", """create temporary table population_person_fixture + (like population_person including defaults); + +copy population_person_fixture from '/docker-entrypoint-initdb.d/population_person.csv' with (format csv, header true); + +insert into population_person +select * from population_person_fixture +on conflict (uin) do update set + person_id = excluded.person_id, + legacy_nid = excluded.legacy_nid, + given_name = excluded.given_name, + family_name = excluded.family_name, + birth_date = excluded.birth_date, + sex = excluded.sex, + district_code = excluded.district_code, + address_area = excluded.address_area, + settlement_type = excluded.settlement_type, + identity_status = excluded.identity_status, + pending_merge_with_uin = excluded.pending_merge_with_uin, + match_basis = excluded.match_basis, + alive = excluded.alive, + birth_brn = excluded.birth_brn, + updated_at = excluded.updated_at, + observed_at = excluded.observed_at, + source_system = excluded.source_system; +""") for table in ["household", "household_member", "socio_economic_profile", "scoring_event", "program", "eligibility_decision", "enrollment", "entitlement", "payment_event", "grievance", "social_registry_household", "child_benefit_household", "programme_mis_enrollment"]: write_csv(root / "ministries/social-development/fixtures" / f"{table}.csv", rows[table]) for table in ["sipf_contribution_account", "sipf_contribution_period", "sipf_pension_award", "sipf_payment_instruction", "sipf_proof_of_life_check", "sipf_survivor_link", "pension_case"]: diff --git a/generator/tests/test_generator.py b/generator/tests/test_generator.py index 04b811c..eabda09 100644 --- a/generator/tests/test_generator.py +++ b/generator/tests/test_generator.py @@ -261,6 +261,12 @@ def test_relay_projection_edges(self) -> None: } self.assertEqual(pensions["2300109568"]["payment_status"], "active") self.assertEqual(pensions["2300109568"]["survivor_eligible"], "true") + survivor_cases = { + row["spouse_uin"]: row + for row in pensions.values() + if row["spouse_uin"] + } + self.assertEqual(survivor_cases["2300146081"]["survivor_eligible"], "false") vouchers = { row["farmer_id"]: row diff --git a/home/src/lib/components/EngineerDoor.svelte b/home/src/lib/components/EngineerDoor.svelte index f106175..7391d0c 100644 --- a/home/src/lib/components/EngineerDoor.svelte +++ b/home/src/lib/components/EngineerDoor.svelte @@ -32,9 +32,10 @@ Product docs and OpenAPI -

Pinned images

-

Relay {versions.REGISTRY_RELAY_IMAGE ?? 'missing'}

-

Notary {versions.REGISTRY_NOTARY_IMAGE ?? 'missing'}

+

Pinned source builds

+

Registry Stack main {versions.REGISTRY_STACK_SOURCE_COMMIT ?? 'missing'}

+

Evidence {versions.SOLMARA_EVIDENCE_IMAGE ?? 'missing'}

+

Mint {versions.SOLMARA_MINT_IMAGE ?? 'missing'}

diff --git a/home/src/lib/components/PurposeLens.svelte b/home/src/lib/components/PurposeLens.svelte index 56200de..210e357 100644 --- a/home/src/lib/components/PurposeLens.svelte +++ b/home/src/lib/components/PurposeLens.svelte @@ -52,7 +52,7 @@ $: disclosed = claimResults(firstResult).filter((claim) => claim.satisfied !== false); $: firstRunSucceeded = hasExpectedSuccessfulClaims(firstResult, expectedEvidenceIds); $: flipCode = explicitProblemCode(flipResult); - $: flipDenied = isExpectedProblemDenial(flipResult, 'pdp.purpose_not_permitted'); + $: flipDenied = isExpectedProblemDenial(flipResult, 'not_authorized'); $: flipSucceeded = hasExpectedSuccessfulClaims(flipResult, expectedEvidenceIds); $: journeyStatus = running ? 'Checking the four government offices.' diff --git a/home/src/lib/components/TrustStrip.svelte b/home/src/lib/components/TrustStrip.svelte index b723e1e..a8d427a 100644 --- a/home/src/lib/components/TrustStrip.svelte +++ b/home/src/lib/components/TrustStrip.svelte @@ -44,9 +44,9 @@
-

Pinned release

-

Relay {shortDigest(versions.REGISTRY_RELAY_IMAGE)}

-

Notary {shortDigest(versions.REGISTRY_NOTARY_IMAGE)}

+

Pinned source

+

Registry Stack main {versions.REGISTRY_STACK_SOURCE_COMMIT?.slice(0, 12) ?? 'missing'}

+

Evidence {shortDigest(versions.SOLMARA_EVIDENCE_IMAGE)}

Smoke evidence

diff --git a/home/src/lib/curl.test.ts b/home/src/lib/curl.test.ts index 28355e2..a3a4c3c 100644 --- a/home/src/lib/curl.test.ts +++ b/home/src/lib/curl.test.ts @@ -5,32 +5,32 @@ import { runnableRequestSources, toCurl } from './curl'; describe('runnableRequestSources', () => { const multiPreview: RequestSource = { method: 'MULTI', - url: 'solmara://authority-notaries', - headers: { 'Data-Purpose': 'citizen-self-service' } + url: 'solmara://registry-evidence', + purpose: 'citizen-self-service' }; - it('selects each executable authority call instead of the synthetic MULTI preview', () => { + it('selects each executable Evidence call instead of the synthetic MULTI preview', () => { const sources: RequestSource[] = [ { method: 'POST', - url: 'http://localhost:4325/v1/evaluations', - headers: { 'x-api-key': 'tok-cra-citizen' }, - body: { claims: ['civil-record-linked'] } + url: 'https://localhost:4341/v1/evidence', + headers: { Authorization: 'Bearer [runtime token hidden]' }, + body: { requirement: 'cra-citizen-record', purpose: 'citizen-self-service' } }, { method: 'POST', - url: 'http://localhost:4326/v1/evaluations', - headers: { 'x-api-key': 'tok-nia-citizen' }, - body: { claims: ['citizen-population-record-active'] } + url: 'https://localhost:4341/v1/evidence', + headers: { Authorization: 'Bearer [runtime token hidden]' }, + body: { requirement: 'nia-citizen-status', purpose: 'citizen-self-service' } } ]; const commands = runnableRequestSources(multiPreview, sources).map((source) => toCurl(source)); expect(commands).toHaveLength(2); - expect(commands[0]).toContain("curl -sS -X POST 'http://localhost:4325/v1/evaluations'"); - expect(commands[1]).toContain("curl -sS -X POST 'http://localhost:4326/v1/evaluations'"); - expect(commands.join('\n')).not.toContain('solmara://authority-notaries'); + expect(commands[0]).toContain("curl -sS -X POST 'https://localhost:4341/v1/evidence'"); + expect(commands[1]).toContain("curl -sS -X POST 'https://localhost:4341/v1/evidence'"); + expect(commands.join('\n')).not.toContain('solmara://registry-evidence'); }); it('uses the primary request when no underlying calls are present', () => { diff --git a/home/src/lib/runresult.test.ts b/home/src/lib/runresult.test.ts index f46c899..b7fe922 100644 --- a/home/src/lib/runresult.test.ts +++ b/home/src/lib/runresult.test.ts @@ -113,9 +113,9 @@ describe('isExpectedProblemDenial', () => { result({ friendlyStatus: 'done', httpStatus: 403, - body: { code: 'pdp.purpose_not_permitted' } + body: { code: 'not_authorized' } }), - 'pdp.purpose_not_permitted' + 'not_authorized' ) ).toBe(true); }); @@ -138,7 +138,7 @@ describe('isExpectedProblemDenial', () => { body: {} }) ]) { - expect(isExpectedProblemDenial(candidate, 'pdp.purpose_not_permitted')).toBe(false); + expect(isExpectedProblemDenial(candidate, 'not_authorized')).toBe(false); } }); }); diff --git a/home/src/lib/runresult.ts b/home/src/lib/runresult.ts index 432a5ac..3923af8 100644 --- a/home/src/lib/runresult.ts +++ b/home/src/lib/runresult.ts @@ -64,7 +64,7 @@ export function explicitProblemCode(result: StepRunResult | null | undefined): s } /** - * Extract the stable problem code from a denial response. Notary denials are + * Extract the stable problem code from an Evidence denial. Safe problems have * problem+json with a `code`; fall back to the trailing segment of a `type` * URI, then to a compatibility code for older error responses. */ @@ -73,7 +73,7 @@ export function problemCode(result: StepRunResult | null | undefined): string | const explicit = explicitProblemCode(result); if (explicit) return explicit; // Only surface a synthesized code when the response actually denied. - if (status !== null && status >= 400) return 'pdp.purpose_not_permitted'; + if (status !== null && status >= 400) return 'not_authorized'; return null; } @@ -90,10 +90,14 @@ export function isDenial(result: StepRunResult | null | undefined): boolean { return status !== null && status >= 400; } -/** The purpose IRI actually sent, read from the redacted request headers. */ +/** The Evidence purpose code actually sent. */ export function requestPurpose(result: StepRunResult | null | undefined): string | null { const headers = result?.request_source?.headers ?? {}; - return headers['Data-Purpose'] ?? headers['data-purpose'] ?? null; + const body = result?.request_source?.body; + const bodyPurpose = body && typeof body === 'object' && !Array.isArray(body) + ? (body as Record).purpose + : undefined; + return result?.request_source?.purpose ?? (typeof bodyPurpose === 'string' ? bodyPurpose : undefined) ?? headers['Data-Purpose'] ?? headers['data-purpose'] ?? null; } /** @@ -128,7 +132,7 @@ export function hopsFromResult(result: StepRunResult | null | undefined): string try { hops.push(`Question sent to ${new URL(url).host}`); } catch { - hops.push('Question sent to the Notary'); + hops.push('Question sent to Registry Evidence'); } } const purpose = requestPurpose(result); diff --git a/home/src/lib/server/data.test.ts b/home/src/lib/server/data.test.ts index b9b7bee..20f3fba 100644 --- a/home/src/lib/server/data.test.ts +++ b/home/src/lib/server/data.test.ts @@ -5,8 +5,8 @@ describe('home data adapters', () => { it('parses the purpose register from the normative docs page', async () => { const purposes = await readPurposes(); expect(purposes).toHaveLength(6); - expect(purposes[0].iri).toBe('https://id.registrystack.org/solmara/purpose/child-benefit-review'); - expect(purposes[0].denialCodes).toContain('pdp.purpose_not_permitted'); + expect(purposes[0].iri).toBe('child-benefit-review'); + expect(purposes[0].denialCodes).toContain('not_authorized'); }); it('derives anatomy services from compose', async () => { @@ -16,10 +16,12 @@ describe('home data adapters', () => { expect(services).toContain('static-metadata'); }); - it('reads pinned versions for the trust strip', async () => { + it('reads the exact source commit and local Evidence image names', async () => { const versions = await readVersions(); - expect(versions.REGISTRY_RELAY_IMAGE).toMatch(/@sha256:/); - expect(versions.REGISTRY_NOTARY_IMAGE).toMatch(/@sha256:/); + expect(versions.REGISTRY_STACK_SOURCE_REF).toBe('main'); + expect(versions.REGISTRY_STACK_SOURCE_COMMIT).toMatch(/^[0-9a-f]{40}$/); + expect(versions.SOLMARA_EVIDENCE_IMAGE).toContain(':source'); + expect(versions.SOLMARA_MINT_IMAGE).toContain(':source'); }); it('keeps compose-internal health probes out of visitor links', async () => { diff --git a/home/src/lib/server/problemcodes.test.ts b/home/src/lib/server/problemcodes.test.ts index 870028c..5932776 100644 --- a/home/src/lib/server/problemcodes.test.ts +++ b/home/src/lib/server/problemcodes.test.ts @@ -4,12 +4,12 @@ import type { Purpose, Scenario } from '$lib/types'; const purposes: Purpose[] = [ { - iri: 'https://id.registrystack.org/solmara/purpose/child-benefit-review', + iri: 'child-benefit-review', slug: 'child-benefit-review', advertisedBy: 'CRA', enforcedBy: 'child-benefit-federator', story: 'Birth to child benefit', - denialCodes: ['pdp.purpose_not_permitted'], + denialCodes: ['not_authorized'], plainLanguage: 'permits child benefit evidence' } ]; @@ -36,27 +36,26 @@ const scenarios: Scenario[] = [ describe('problem-code assembly', () => { const codes = assembleProblemCodes(purposes, scenarios); - it('includes every denial code from the catalogue plus the observed request.invalid', () => { + it('includes current Evidence authorization and malformed-request codes', () => { const ids = codes.map((code) => code.code); - expect(ids).toContain('pdp.purpose_not_permitted'); - expect(ids).toContain('request.invalid'); + expect(ids).toContain('not_authorized'); + expect(ids).toContain('malformed_request'); }); - it('uses the problem type URI observed in real notary responses', () => { - const pdp = codes.find((code) => code.code === 'pdp.purpose_not_permitted'); - expect(pdp?.typeUri).toBe('https://id.registrystack.org/problems/registry-notary/pdp/purpose_not_permitted'); + it('uses the current Registry Evidence problem type URI', () => { + const pdp = codes.find((code) => code.code === 'not_authorized'); + expect(pdp?.typeUri).toBe('https://registrystack.org/problems/evidence/not_authorized'); expect(pdp?.problemJson.status).toBe(403); - expect(pdp?.problemJson.code).toBe('pdp.purpose_not_permitted'); + expect(pdp?.problemJson.code).toBe('not_authorized'); }); - it('links purpose_not_permitted to the story step that demonstrates it', () => { - const pdp = codes.find((code) => code.code === 'pdp.purpose_not_permitted'); + it('links not_authorized to the story step that demonstrates it', () => { + const pdp = codes.find((code) => code.code === 'not_authorized'); expect(pdp?.demonstratedBy.map((link) => link.stepId)).toContain('purpose-denial'); expect(pdp?.purposeSlugs).toContain('child-benefit-review'); }); it('anchors each code by its stable code string', () => { - // Phase A deep-links to /problem-codes#pdp.purpose_not_permitted. - expect(codes.some((code) => code.code === 'pdp.purpose_not_permitted')).toBe(true); + expect(codes.some((code) => code.code === 'not_authorized')).toBe(true); }); }); diff --git a/home/src/lib/server/problemcodes.ts b/home/src/lib/server/problemcodes.ts index ace1b91..5f069e5 100644 --- a/home/src/lib/server/problemcodes.ts +++ b/home/src/lib/server/problemcodes.ts @@ -2,7 +2,7 @@ import type { ProblemCode, Purpose, Scenario, StoryStepLink } from '$lib/types'; /** * Static, maintained metadata for every stable problem code the lab can emit. - * The `typeUri` values are the problem type URIs actually observed in Notary + * The `typeUri` values are the problem type URIs emitted by Registry Evidence * responses. The set of codes is assembled from the purpose catalogue (which * lists each purpose's denial codes) plus these entries, so the page never * hand-maintains prose that can drift from the catalogue. Meanings are plain @@ -12,20 +12,20 @@ const CODE_META: Record< string, { title: string; status: number; typeUri: string; meaning: string; coverage?: string } > = { - 'pdp.purpose_not_permitted': { + not_authorized: { title: 'Purpose not permitted', status: 403, - typeUri: 'https://id.registrystack.org/problems/registry-notary/pdp/purpose_not_permitted', + typeUri: 'https://registrystack.org/problems/evidence/not_authorized', meaning: - 'The request named a purpose the authority does not allow for this evidence, or asked for a field outside that purpose. The Notary (the service that certifies evidence) refuses and discloses nothing. This is purpose limitation enforced at request time.' + 'The requester grant does not authorize this requirement, purpose, response format, or selector shape. Registry Evidence refuses before source access and discloses nothing.' }, - 'request.invalid': { + malformed_request: { title: 'Invalid evidence request', status: 400, - typeUri: 'https://id.registrystack.org/problems/registry-notary/request/invalid', + typeUri: 'https://registrystack.org/problems/evidence/malformed_request', meaning: - 'The request asked for something the Notary will not serve, such as a raw source row instead of a purpose-limited predicate. The Notary rejects the request rather than reach into the register. This is the clean refusal a skeptic gets when they try a raw row read.', - coverage: 'Asserted by the published-token smoke: a raw-row read attempt with a published demo token.' + 'The request does not match the closed Evidence request contract. Registry Evidence rejects it before evaluating a requirement.', + coverage: 'Covered by the Evidence bundle fixtures and current-main contract tests.' } }; @@ -63,12 +63,12 @@ export function assembleProblemCodes(purposes: Purpose[], scenarios: Scenario[]) const meta = CODE_META[code] ?? { title: code, status: 400, - typeUri: `https://id.registrystack.org/problems/registry-notary/${code.replace(/\./g, '/')}`, + typeUri: `https://registrystack.org/problems/evidence/${code.replace(/\./g, '_')}`, meaning: 'A stable problem code emitted by the stack. See the purpose catalogue for where it applies.' }; const typeUri = meta.typeUri; const purposeSlugs = purposes.filter((purpose) => purpose.denialCodes.includes(code)).map((purpose) => purpose.slug); - const demonstratedBy = code === 'pdp.purpose_not_permitted' ? denials : []; + const demonstratedBy = code === 'not_authorized' ? denials : []; return { code, typeUri, diff --git a/home/src/lib/server/purposes.test.ts b/home/src/lib/server/purposes.test.ts index 5dbb9da..1a4a874 100644 --- a/home/src/lib/server/purposes.test.ts +++ b/home/src/lib/server/purposes.test.ts @@ -8,8 +8,8 @@ const SAMPLE = `# Solmara Purpose Catalogue | Purpose IRI | Advertised by | Enforced by | Story | Denial problem codes | |---|---|---|---|---| -| \`https://id.registrystack.org/solmara/purpose/child-benefit-review\` | CRA, NIA | CRA and NIA authority Notaries | Birth to child benefit | \`pdp.purpose_not_permitted\` | -| \`https://id.registrystack.org/solmara/purpose/voucher-eligibility-review\` | NAgDI | \`nagdi-notary\` | Farmer climate-smart voucher | \`pdp.purpose_not_permitted\` | +| \`child-benefit-review\` | CRA, NIA | Registry Evidence | Birth to child benefit | \`not_authorized\` | +| \`voucher-eligibility-review\` | NAgDI | Registry Evidence | Farmer climate-smart voucher | \`not_authorized\` | ## Purpose Rules @@ -52,12 +52,12 @@ describe('purposes parser', () => { const purposes = parsePurposes(SAMPLE); expect(purposes).toHaveLength(2); const child = purposes[0]; - expect(child.iri).toBe('https://id.registrystack.org/solmara/purpose/child-benefit-review'); + expect(child.iri).toBe('child-benefit-review'); expect(child.slug).toBe('child-benefit-review'); expect(child.advertisedBy).toBe('CRA, NIA'); - expect(child.enforcedBy).toBe('CRA and NIA authority Notaries'); + expect(child.enforcedBy).toBe('Registry Evidence'); expect(child.story).toBe('Birth to child benefit'); - expect(child.denialCodes).toEqual(['pdp.purpose_not_permitted']); + expect(child.denialCodes).toEqual(['not_authorized']); expect(child.plainLanguage).toContain('permits evidence needed to determine whether a child'); expect(child.plainLanguage).toContain('does not permit raw poverty scores'); // The rule paragraph must not leak markdown backticks. @@ -81,11 +81,11 @@ describe('purposes parser', () => { it('links purposes to the story steps that send them', () => { const scenarios = [ scenario('birth-to-child-benefit', 'Birth to child benefit', [ - { id: 'positive', label: 'Evaluate eligible child', purpose: 'https://id.registrystack.org/solmara/purpose/child-benefit-review' }, - { id: 'purpose-denial', label: 'Purpose denial', purpose: 'https://id.registrystack.org/solmara/purpose/pension-payment-review' } + { id: 'positive', label: 'Evaluate eligible child', purpose: 'child-benefit-review' }, + { id: 'purpose-denial', label: 'Purpose denial', purpose: 'pension-payment-review' } ]) ]; - const links = storyLinksForPurpose('https://id.registrystack.org/solmara/purpose/child-benefit-review', scenarios); + const links = storyLinksForPurpose('child-benefit-review', scenarios); expect(links).toHaveLength(1); expect(links[0]).toMatchObject({ storyId: 'birth-to-child-benefit', stepId: 'positive' }); diff --git a/home/src/lib/server/purposes.ts b/home/src/lib/server/purposes.ts index e91ff79..2a1c4e6 100644 --- a/home/src/lib/server/purposes.ts +++ b/home/src/lib/server/purposes.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { runtime } from './runtime'; import type { Purpose, PurposeView, Scenario, StoryStepLink } from '$lib/types'; -const TABLE_ROW_PREFIX = '| `https://id.registrystack.org/solmara/purpose/'; +const TABLE_ROW_PREFIX = '| `'; const RULES_HEADING = '## Purpose Rules'; /** @@ -16,7 +16,7 @@ export function parsePurposes(raw: string): Purpose[] { const rules = parseRuleParagraphs(raw); return raw .split('\n') - .filter((line) => line.startsWith(TABLE_ROW_PREFIX)) + .filter((line) => line.startsWith(TABLE_ROW_PREFIX) && line.split('|').length === 7) .map((line) => { const cells = line.split('|').slice(1, -1).map((cell) => cell.trim()); const iri = stripTicks(cells[0]); @@ -61,15 +61,17 @@ export async function readPurposes(): Promise { /** * Return the guided-story steps that demonstrate a purpose, matched on the - * `Data-Purpose` header each step's request preview actually sends. This keeps + * Evidence `purpose` member each step's request preview actually sends. This keeps * the story cross-links driven by the scenario data rather than a hand table. */ export function storyLinksForPurpose(iri: string, scenarios: Scenario[]): StoryStepLink[] { const links: StoryStepLink[] = []; for (const scenario of scenarios) { for (const step of scenario.steps) { - const headers = step.request_preview?.headers ?? {}; - const sent = headers['Data-Purpose'] ?? headers['data-purpose']; + const preview = step.request_preview; + const headers = preview?.headers ?? {}; + const body = preview?.body && typeof preview.body === 'object' ? preview.body as Record : {}; + const sent = preview?.purpose ?? body.purpose ?? headers['Data-Purpose'] ?? headers['data-purpose']; if (sent === iri) { links.push({ storyId: scenario.id, storyTitle: scenario.title, stepId: step.id, stepLabel: step.label }); } diff --git a/home/src/lib/server/services.test.ts b/home/src/lib/server/services.test.ts index 19f856c..9cc625b 100644 --- a/home/src/lib/server/services.test.ts +++ b/home/src/lib/server/services.test.ts @@ -2,109 +2,54 @@ import { describe, expect, it } from 'vitest'; import { statusProbes, topologyGroups } from './services'; describe('status probe table', () => { - it('covers the whole topology: shared services and six authority pairs', () => { + it('covers six Records APIs, Evidence, Mint, and shared applications', () => { const probes = statusProbes('http://127.0.0.1:4300'); - const relays = probes.filter((probe) => probe.role === 'relay'); - const notaries = probes.filter((probe) => probe.role === 'notary'); - const shared = probes.filter((probe) => probe.role === 'shared'); - expect(relays).toHaveLength(6); - expect(notaries).toHaveLength(6); - expect(shared.length).toBeGreaterThanOrEqual(4); - expect(probes.length).toBeGreaterThanOrEqual(16); + expect(probes.filter((probe) => probe.role === 'relay')).toHaveLength(6); + expect(probes.filter((probe) => probe.role === 'evidence')).toHaveLength(2); + expect(probes.filter((probe) => probe.role === 'shared').length).toBeGreaterThanOrEqual(4); }); - it('probes the federator on health, notaries on claims, and relays on their gated root', () => { + it('uses public health probes for Evidence and Mint', () => { const probes = statusProbes('http://127.0.0.1:4300'); - const federator = probes.find((probe) => probe.id === 'child-benefit-federator'); - const notary = probes.find((probe) => probe.id === 'cra-notary'); - const relay = probes.find((probe) => probe.id === 'cra-civil-relay'); - expect(federator?.probeUrl).toContain('/health'); - expect(notary?.probeUrl).toContain('/v1/claims'); - expect(relay?.probeUrl?.endsWith('/')).toBe(true); - }); - - it('only marks the browsable services with an open link', () => { - const probes = statusProbes('http://portal.example'); - expect(probes.find((probe) => probe.id === 'metadata')?.href).toBe('/.well-known/api-catalog'); - expect(probes.find((probe) => probe.id === 'portal')?.href).toBe('http://portal.example'); - expect(probes.find((probe) => probe.id === 'home')?.href).toBe('/'); - expect(probes.find((probe) => probe.id === 'cra-civil-relay')?.href).toBeUndefined(); - }); - - it('reads probe origins from the env table, overriding the localhost defaults', () => { - const probes = statusProbes('http://127.0.0.1:4300', { - CHILD_BENEFIT_FEDERATOR_URL: 'http://child-benefit-federator:8080' - }); - expect(probes.find((probe) => probe.id === 'child-benefit-federator')?.probeUrl).toBe( - 'http://child-benefit-federator:8080/health' + expect(probes.find((probe) => probe.id === 'registry-evidence')?.probeUrl).toBe( + 'https://localhost:4341/health' ); - // A service with no override keeps its localhost default. - expect(probes.find((probe) => probe.id === 'sipf-notary')?.probeUrl).toBe('http://127.0.0.1:4322/v1/claims'); - }); - - it('maps every Compose authority Notary environment name to exactly one probe', () => { - const authorityOrigins = { - CRA_NOTARY_URL: 'http://cra-notary:8081', - NIA_NOTARY_URL: 'http://nia-notary:8081', - SRO_NOTARY_URL: 'http://sro-notary:8081', - PROGRAMME_NOTARY_URL: 'http://programme-notary:8081', - SIPF_NOTARY_URL: 'http://sipf-notary:8081', - NAGDI_NOTARY_URL: 'http://nagdi-notary:8081' - }; - const probes = statusProbes('http://127.0.0.1:4300', authorityOrigins).filter( - (probe) => probe.role === 'notary' + expect(probes.find((probe) => probe.id === 'registry-mint')?.probeUrl).toBe( + 'https://localhost:4341/health' ); + expect(probes.find((probe) => probe.id === 'cra-civil-relay')?.probeUrl?.endsWith('/')).toBe(true); + }); - expect(Object.fromEntries(probes.map((probe) => [probe.id, probe.probeUrl]))).toEqual({ - 'cra-notary': 'http://cra-notary:8081/v1/claims', - 'nia-notary': 'http://nia-notary:8081/v1/claims', - 'sro-notary': 'http://sro-notary:8081/v1/claims', - 'programme-notary': 'http://programme-notary:8081/v1/claims', - 'sipf-notary': 'http://sipf-notary:8081/v1/claims', - 'nagdi-notary': 'http://nagdi-notary:8081/v1/claims' + it('reads compose-internal Evidence and Mint origins from the environment', () => { + const probes = statusProbes('http://127.0.0.1:4300', { + EVIDENCE_URL: 'http://evidence:8080', + MINT_URL: 'http://mint:8081' }); + expect(probes.find((probe) => probe.id === 'registry-evidence')?.probeUrl).toBe('http://evidence:8080/health'); + expect(probes.find((probe) => probe.id === 'registry-mint')?.probeUrl).toBe('http://mint:8081/health'); }); - it('marks home as self so it is reported up without a network probe', () => { - const probes = statusProbes('http://127.0.0.1:4300'); - expect(probes.find((probe) => probe.id === 'home')?.self).toBe(true); + it('only marks browser-facing services with open links', () => { + const probes = statusProbes('http://portal.example'); + expect(probes.find((probe) => probe.id === 'metadata')?.href).toBe('/.well-known/api-catalog'); + expect(probes.find((probe) => probe.id === 'portal')?.href).toBe('http://portal.example'); + expect(probes.find((probe) => probe.id === 'home')?.href).toBe('/'); + expect(probes.find((probe) => probe.id === 'registry-evidence')?.href).toBeUndefined(); }); }); describe('topology groups', () => { const groups = topologyGroups('https://github.com/registrystack/solmara-lab'); - it('groups six authority-owned Relays, six authority-owned Notaries, and shared services', () => { - const keys = groups.map((group) => group.key); - expect(keys).toEqual(['relays', 'notaries', 'shared']); + it('groups authority Records APIs, Evidence and Mint, and shared services', () => { + expect(groups.map((group) => group.key)).toEqual(['relays', 'evidence', 'shared']); expect(groups[0].services).toHaveLength(6); - expect(groups[1].services).toHaveLength(6); - expect(groups[1].services.every((service) => Boolean(service.authority))).toBe(true); - }); - - it('links every authority to its authored project and generated Relay config', () => { - const cra = groups[0].services.find((service) => service.id === 'cra-civil-relay'); - expect(cra?.authority).toBe('Civil Registration Authority'); - const project = cra?.config.find((link) => link.label === 'Registry project'); - expect(project?.url).toBe( - 'https://github.com/registrystack/solmara-lab/blob/main/projects/cra-civil/registry-stack.yaml' - ); - const generated = cra?.config.find((link) => link.label === 'Generated Relay config'); - expect(generated?.url).toBe( - 'https://github.com/registrystack/solmara-lab/blob/main/runtime/registry-projects/local/cra-civil/relay/relay.yaml' - ); - const seed = cra?.config.find((link) => link.path === 'ministries/interior-civil'); - expect(seed?.url).toBe('https://github.com/registrystack/solmara-lab/tree/main/ministries/interior-civil'); + expect(groups[1].services.map((service) => service.id)).toEqual(['registry-evidence', 'registry-mint']); }); - it('links authority Notaries to their generated project closure', () => { - const nia = groups[1].services.find((service) => service.id === 'nia-notary'); - expect(nia?.config).toEqual([ - { - label: 'Generated Notary config', - path: 'runtime/registry-projects/local/nia-population/notary/notary.yaml', - url: 'https://github.com/registrystack/solmara-lab/blob/main/runtime/registry-projects/local/nia-population/notary/notary.yaml' - } - ]); + it('links Evidence to its runtime and reviewed bundle', () => { + const evidence = groups[1].services.find((service) => service.id === 'registry-evidence'); + expect(evidence?.config.map((link) => link.path)).toEqual(['evidence/runtime.yaml', 'evidence/bundle']); + expect(evidence?.config[1].url).toBe('https://github.com/registrystack/solmara-lab/tree/main/evidence/bundle'); }); }); diff --git a/home/src/lib/server/services.ts b/home/src/lib/server/services.ts index a4d4e71..c0dfaa0 100644 --- a/home/src/lib/server/services.ts +++ b/home/src/lib/server/services.ts @@ -7,12 +7,12 @@ import type { ConfigLink, TopologyGroup, TopologyService } from '$lib/types'; * port or hostname is scattered across components. Probe URLs default to the * published localhost ports and are overridden by the compose-internal URLs the * home container receives (see the `home` service env in `compose.yaml`), which - * is why relays and notaries are reachable for probing from inside the network. + * is why Relay, Evidence, and Mint are reachable for probing from the network. * * The internal probe origins mirror the same conventions as `urlmap.ts` and * `scenarios/service_config.py`. */ -export type ServiceRole = 'shared' | 'relay' | 'notary'; +export type ServiceRole = 'shared' | 'relay' | 'evidence'; type ServiceStatusSemantics = 'health' | 'auth-gated'; @@ -52,7 +52,7 @@ export const SERVICES: ServiceDef[] = [ id: 'scenario-runner', label: 'Scenario runner', role: 'shared', - blurb: 'Runs the guided stories and the Purpose Lens against the live notaries.', + blurb: 'Runs the guided stories and the Purpose Lens against Registry Evidence.', probeEnv: 'SCENARIO_RUNNER_URL', probeDefault: 'http://127.0.0.1:4302', probePath: '/health', @@ -193,82 +193,30 @@ export const SERVICES: ServiceDef[] = [ configPaths: [{ label: 'Federator service', path: 'scenario-runner/child_benefit_federator.py' }] }, { - id: 'cra-notary', - label: 'CRA notary', - role: 'notary', - authority: 'Civil Registration Authority', - purpose: 'child-benefit-review, pension-payment-review, citizen-self-service', - blurb: 'Answers civil-registration predicates for child benefit, pension review, and citizen services.', - probeEnv: 'CRA_NOTARY_URL', - probeDefault: 'http://127.0.0.1:4325', - probePath: '/v1/claims', - semantics: 'auth-gated', - configPaths: [{ label: 'Generated Notary config', path: 'runtime/registry-projects/local/cra-civil/notary/notary.yaml' }] - }, - { - id: 'nia-notary', - label: 'NIA notary', - role: 'notary', - authority: 'National Identity Agency', - purpose: 'child-benefit-review, citizen-self-service', - blurb: 'Answers active population-record predicates and owns the citizen population-status credential.', - probeEnv: 'NIA_NOTARY_URL', - probeDefault: 'http://127.0.0.1:4326', - probePath: '/v1/claims', - semantics: 'auth-gated', - configPaths: [{ label: 'Generated Notary config', path: 'runtime/registry-projects/local/nia-population/notary/notary.yaml' }] - }, - { - id: 'sro-notary', - label: 'SRO notary', - role: 'notary', - authority: 'Social Registry Office', - purpose: 'child-benefit-review', - blurb: 'Answers the household poverty-threshold predicate from the social registry.', - probeEnv: 'SRO_NOTARY_URL', - probeDefault: 'http://127.0.0.1:4327', - probePath: '/v1/claims', - semantics: 'auth-gated', - configPaths: [{ label: 'Generated Notary config', path: 'runtime/registry-projects/local/sro-social/notary/notary.yaml' }] - }, - { - id: 'programme-notary', - label: 'Programme MIS notary', - role: 'notary', - authority: 'Ministry of Social Development Programme MIS', - purpose: 'child-benefit-review', - blurb: 'Answers the duplicate-enrollment predicate from programme records.', - probeEnv: 'PROGRAMME_NOTARY_URL', - probeDefault: 'http://127.0.0.1:4328', - probePath: '/v1/claims', - semantics: 'auth-gated', - configPaths: [{ label: 'Generated Notary config', path: 'runtime/registry-projects/local/mosd-programme/notary/notary.yaml' }] - }, - { - id: 'sipf-notary', - label: 'SIPF notary', - role: 'notary', - authority: 'Social Insurance and Pensions Fund', - purpose: 'pension-payment-review, survivor-benefit-determination', - blurb: 'Answers pension-payment and survivor-benefit predicates and owns the survivor credential.', - probeEnv: 'SIPF_NOTARY_URL', - probeDefault: 'http://127.0.0.1:4322', - probePath: '/v1/claims', - semantics: 'auth-gated', - configPaths: [{ label: 'Generated Notary config', path: 'runtime/registry-projects/local/sipf-pensions/notary/notary.yaml' }] + id: 'registry-evidence', + label: 'Registry Evidence', + role: 'evidence', + purpose: 'all six reviewed Solmara purposes', + blurb: 'Evaluates eleven minimized requirements against authority Records APIs and returns signed evidence assertions.', + probeEnv: 'EVIDENCE_URL', + probeDefault: 'https://localhost:4341', + probePath: '/health', + semantics: 'health', + configPaths: [ + { label: 'Runtime config', path: 'evidence/runtime.yaml' }, + { label: 'Evidence bundle', path: 'evidence/bundle' } + ] }, { - id: 'nagdi-notary', - label: 'NAgDI notary', - role: 'notary', - authority: 'National Agricultural Data Institute', - purpose: 'voucher-eligibility-review, livestock-movement-control', - blurb: 'Evaluates farmer voucher and livestock movement claims.', - probeEnv: 'NAGDI_NOTARY_URL', - probeDefault: 'http://127.0.0.1:4323', - probePath: '/v1/claims', - semantics: 'auth-gated', - configPaths: [{ label: 'Generated Notary config', path: 'runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml' }] + id: 'registry-mint', + label: 'Registry Mint', + role: 'evidence', + blurb: 'Issues short-lived requester-bound access tokens for the local Evidence service using private_key_jwt.', + probeEnv: 'MINT_URL', + probeDefault: 'https://localhost:4341', + probePath: '/health', + semantics: 'health', + configPaths: [{ label: 'Mint config', path: 'evidence/mint.yaml' }] } ]; @@ -301,15 +249,15 @@ export function statusProbes(portalUrl: string, readEnv: Record service.role === 'shared'); const relays = SERVICES.filter((service) => service.role === 'relay'); - const notaries = SERVICES.filter((service) => service.role === 'notary'); + const evidence = SERVICES.filter((service) => service.role === 'evidence'); return [ { key: 'relays', @@ -319,11 +267,11 @@ export function topologyGroups(repoUrl: string): TopologyGroup[] { services: relays.map((service) => toTopologyService(service, repoUrl)) }, { - key: 'notaries', - title: 'Source-owned Notaries', + key: 'evidence', + title: 'Evidence and requester identity', blurb: - 'Each authority runs one Notary beside its Relay. Applications collect minimized, source-attributed predicates without moving raw rows or asking a Notary to compose the final programme decision.', - services: notaries.map((service) => toTopologyService(service, repoUrl)) + 'Evidence evaluates reviewed requirements against protected Records APIs and signs the minimized values. Mint supplies short-lived requester identity and grant tags. Neither service owns an application decision.', + services: evidence.map((service) => toTopologyService(service, repoUrl)) }, { key: 'shared', diff --git a/home/src/lib/server/tokens.test.ts b/home/src/lib/server/tokens.test.ts index 66cf3ab..a4ff4f0 100644 --- a/home/src/lib/server/tokens.test.ts +++ b/home/src/lib/server/tokens.test.ts @@ -1,205 +1,53 @@ import { describe, expect, it } from 'vitest'; import { buildCurlExamples, parsePublishedTokens, publishRequestTokens } from './tokens'; -describe('published-token allowlist', () => { - it('renders only the tokens named in the allowlist JSON', () => { - const tokens = parsePublishedTokens('{"child-benefit-federator":"tok-child","sipf-pension-client":"tok-pension"}'); - expect(tokens.map((token) => token.name)).toEqual(['child-benefit-federator', 'sipf-pension-client']); - expect(tokens.map((token) => token.token)).toEqual(['tok-child', 'tok-pension']); +describe('published local application token allowlist', () => { + it('renders only tokens explicitly named in the allowlist JSON', () => { + const tokens = parsePublishedTokens('{"child-benefit-federator":"tok-child","unknown":"tok-unknown"}'); + expect(tokens.map((token) => token.token)).toEqual(['tok-child', 'tok-unknown']); + expect(tokens[0].purpose).toBe('child-benefit-review'); }); - it('never surfaces a token that is not a value in the allowlist', () => { - // A container may hold many notary tokens; only those explicitly listed in - // HOME_PUBLISHED_TOKENS may ever reach page data. - const tokens = parsePublishedTokens('{"child-benefit-federator":"tok-published"}'); - const serialised = JSON.stringify(tokens); - expect(serialised).toContain('tok-published'); - expect(serialised).not.toContain('tok-secret-not-published'); - }); - - it('does not fall back to any other environment variable', () => { - // With no allowlist provided, no token is ever produced, even if individual - // *_NOTARY_TOKEN variables exist in the environment. + it('fails closed for absent or malformed allowlists', () => { expect(parsePublishedTokens(undefined)).toEqual([]); - expect(parsePublishedTokens('')).toEqual([]); - }); - - it('returns an empty list for a malformed allowlist rather than throwing', () => { expect(parsePublishedTokens('not json')).toEqual([]); expect(parsePublishedTokens('[]')).toEqual([]); - expect(parsePublishedTokens('{"empty":""}')).toEqual([]); }); - it('builds four curl examples including the skeptic wrong-purpose call', () => { - const tokens = parsePublishedTokens('{"child-benefit-federator":"tok-child"}'); - const examples = buildCurlExamples(tokens); + it('builds metadata and child collector curls without exposing Mint credentials', () => { + const examples = buildCurlExamples(parsePublishedTokens('{"child-benefit-federator":"tok-child"}')); expect(examples.map((example) => example.id)).toEqual([ 'metadata-get', 'claims-get', 'evaluate-post', 'wrong-purpose-post' ]); - const wrong = examples.find((example) => example.id === 'wrong-purpose-post'); - expect(wrong?.command).toContain('pension-payment-review'); - // The published token is inlined into the authenticated examples. - expect(examples.find((example) => example.id === 'claims-get')?.command).toContain('tok-child'); - // The unauthenticated metadata example never carries a token. - expect(examples.find((example) => example.id === 'metadata-get')?.command).not.toContain('tok-child'); + expect(examples.find((example) => example.id === 'evaluate-post')?.command).toContain('tok-child'); + expect(examples.find((example) => example.id === 'wrong-purpose-post')?.command).toContain('pension-payment-review'); + expect(JSON.stringify(examples)).not.toContain('client-private.jwk'); }); - it('uses an env-var placeholder in curls when no child token is published', () => { - const examples = buildCurlExamples([]); - expect(examples.find((example) => example.id === 'claims-get')?.command).toContain('$CHILD_BENEFIT_FEDERATOR_TOKEN'); - }); - - it('republishes only allowlisted lab tokens into story request sources', () => { + it('republishes only the exact child collector token into matching request sources', () => { const tokens = parsePublishedTokens('{"child-benefit-federator":"tok-child"}'); const result = publishRequestTokens( { request_source: { method: 'POST', url: 'http://localhost:4321/v1/evaluations', - headers: { - 'x-api-key': '[runtime token hidden]', - 'Data-Purpose': 'https://id.registrystack.org/solmara/purpose/child-benefit-review' - } + headers: { 'x-api-key': '[runtime token hidden]', 'Data-Purpose': 'child-benefit-review' } }, - credential_source: { - method: 'POST', - url: 'http://sipf-notary:8081/v1/credentials', - headers: { 'x-api-key': '[runtime token hidden]' } - } - }, - tokens - ); - - expect(result.request_source.headers['x-api-key']).toBe('tok-child'); - expect(result.credential_source.headers['x-api-key']).toBe('[runtime token hidden]'); - }); - - it('does not publish a token from purpose alone when the authority URL is unknown', () => { - const tokens = parsePublishedTokens('{"nagdi-notary":"tok-nagdi"}'); - const result = publishRequestTokens( - { - request_source: { - method: 'POST', - url: 'https://lab.example/evaluations', - headers: { - 'x-api-key': '[runtime token hidden]', - 'Data-Purpose': 'https://id.registrystack.org/solmara/purpose/livestock-movement-control' - } - } - }, - tokens - ); - - expect(result.request_source.headers['x-api-key']).toBe('[runtime token hidden]'); - }); - - it('does not guess between authority clients when purpose or URL binding is incomplete', () => { - const tokens = parsePublishedTokens( - '{"cra-pension-client":"tok-cra-pension","cra-citizen-client":"tok-cra-citizen","nia-citizen-client":"tok-nia-citizen","sipf-pension-client":"tok-sipf-pension"}' - ); - const result = publishRequestTokens( - { request_sources: [ { method: 'POST', - url: 'https://unmapped.example/v1/evaluations', - headers: { - 'x-api-key': '[runtime token hidden]', - 'Data-Purpose': 'https://id.registrystack.org/solmara/purpose/pension-payment-review' - } - }, - { - method: 'POST', - url: 'http://cra-notary:8081/v1/evaluations', - headers: { 'x-api-key': '[runtime token hidden]' } + url: 'https://localhost:4341/v1/evidence', + headers: { Authorization: 'Bearer [runtime token hidden]' }, + body: { purpose: 'child-benefit-review' } } ] }, tokens ); - - expect(result.request_sources[0].headers['x-api-key']).toBe('[runtime token hidden]'); - expect(result.request_sources[1].headers['x-api-key']).toBe('[runtime token hidden]'); - }); - - it('does not publish a purpose token to a different known authority endpoint', () => { - const tokens = parsePublishedTokens('{"child-benefit-federator":"tok-child"}'); - const result = publishRequestTokens( - { - request_source: { - method: 'POST', - url: 'http://cra-notary:8081/v1/evaluations', - headers: { - 'x-api-key': '[runtime token hidden]', - 'Data-Purpose': 'https://id.registrystack.org/solmara/purpose/child-benefit-review' - } - } - }, - tokens - ); - - expect(result.request_source.headers['x-api-key']).toBe('[runtime token hidden]'); - }); - - it('publishes authority client tokens by both Notary URL and purpose', () => { - const tokens = parsePublishedTokens( - JSON.stringify({ - 'cra-pension-client': 'tok-cra-pension', - 'cra-citizen-client': 'tok-cra-citizen', - 'nia-citizen-client': 'tok-nia-citizen', - 'sipf-pension-client': 'tok-sipf-pension' - }) - ); - const result = publishRequestTokens( - { - request_sources: [ - { - method: 'POST', - url: 'http://cra-notary:8081/v1/evaluations', - headers: { - 'x-api-key': '[runtime token hidden]', - 'Data-Purpose': 'https://id.registrystack.org/solmara/purpose/pension-payment-review' - } - }, - { - method: 'POST', - url: 'http://nia-notary:8081/v1/evaluations', - headers: { - 'x-api-key': '[runtime token hidden]', - 'Data-Purpose': 'https://id.registrystack.org/solmara/purpose/citizen-self-service' - } - } - ], - credential_source: { - method: 'POST', - url: 'http://sipf-notary:8081/v1/credentials', - headers: { - 'x-api-key': '[runtime token hidden]', - 'data-purpose': 'https://id.registrystack.org/solmara/purpose/survivor-benefit-determination' - } - }, - source_trace: [ - { - request_source: { - method: 'POST', - url: 'http://cra-notary:8081/v1/evaluations', - headers: { - 'x-api-key': '[runtime token hidden]', - 'Data-Purpose': 'https://id.registrystack.org/solmara/purpose/citizen-self-service' - } - } - } - ] - }, - tokens - ); - - expect(result.request_sources[0].headers['x-api-key']).toBe('tok-cra-pension'); - expect(result.request_sources[1].headers['x-api-key']).toBe('tok-nia-citizen'); - expect(result.credential_source.headers['x-api-key']).toBe('tok-sipf-pension'); - expect(result.source_trace[0].request_source.headers['x-api-key']).toBe('tok-cra-citizen'); + expect(result.request_source.headers['x-api-key']).toBe('tok-child'); + expect(result.request_sources[0].headers.Authorization).toBe('Bearer [runtime token hidden]'); }); }); diff --git a/home/src/lib/server/tokens.ts b/home/src/lib/server/tokens.ts index 1d695be..15f9af4 100644 --- a/home/src/lib/server/tokens.ts +++ b/home/src/lib/server/tokens.ts @@ -2,37 +2,13 @@ import { env } from '$env/dynamic/private'; import { buildPublicUrlMap, mapPublicUrl } from './urlmap'; import type { CurlExample, PublishedToken } from '$lib/types'; -const CHILD_PURPOSE = 'https://id.registrystack.org/solmara/purpose/child-benefit-review'; -const PENSION_PURPOSE = 'https://id.registrystack.org/solmara/purpose/pension-payment-review'; -const SURVIVOR_PURPOSE = 'https://id.registrystack.org/solmara/purpose/survivor-benefit-determination'; -const VOUCHER_PURPOSE = 'https://id.registrystack.org/solmara/purpose/voucher-eligibility-review'; -const LIVESTOCK_PURPOSE = 'https://id.registrystack.org/solmara/purpose/livestock-movement-control'; -const CITIZEN_PURPOSE = 'https://id.registrystack.org/solmara/purpose/citizen-self-service'; +const CHILD_PURPOSE = 'child-benefit-review'; +const PENSION_PURPOSE = 'pension-payment-review'; const TOKEN_BINDINGS: Record = { 'child-benefit-federator': { urlHints: ['child-benefit-federator', 'localhost:4321', '127.0.0.1:4321'], purposes: [CHILD_PURPOSE] - }, - 'cra-pension-client': { - urlHints: ['cra-notary', 'localhost:4325', '127.0.0.1:4325'], - purposes: [PENSION_PURPOSE] - }, - 'cra-citizen-client': { - urlHints: ['cra-notary', 'localhost:4325', '127.0.0.1:4325'], - purposes: [CITIZEN_PURPOSE] - }, - 'nia-citizen-client': { - urlHints: ['nia-notary', 'localhost:4326', '127.0.0.1:4326'], - purposes: [CITIZEN_PURPOSE] - }, - 'sipf-pension-client': { - urlHints: ['sipf-notary', 'localhost:4322', '127.0.0.1:4322'], - purposes: [PENSION_PURPOSE, SURVIVOR_PURPOSE] - }, - 'nagdi-notary': { - urlHints: ['nagdi-notary', 'localhost:4323', '127.0.0.1:4323'], - purposes: [VOUCHER_PURPOSE, LIVESTOCK_PURPOSE] } }; @@ -44,26 +20,6 @@ const TOKEN_NOTES: Record = { 'child-benefit-federator': { purpose: CHILD_PURPOSE, note: 'Scoped to the child benefit federator. Use it to collect source-owned predicates under child-benefit-review.' - }, - 'cra-pension-client': { - purpose: PENSION_PURPOSE, - note: 'Scoped to CRA civil evidence used during pension payment review.' - }, - 'cra-citizen-client': { - purpose: CITIZEN_PURPOSE, - note: 'Scoped to CRA civil evidence used by citizen self-service.' - }, - 'nia-citizen-client': { - purpose: CITIZEN_PURPOSE, - note: 'Scoped to NIA population evidence and credential issuance used by citizen self-service.' - }, - 'sipf-pension-client': { - purpose: PENSION_PURPOSE, - note: 'Scoped to SIPF evidence for pension payment and survivor benefit review.' - }, - 'nagdi-notary': { - purpose: VOUCHER_PURPOSE, - note: 'Scoped to the NAgDI notary for farmer voucher and livestock movement review.' } }; @@ -91,7 +47,7 @@ export function parsePublishedTokens(json: string | undefined = env.HOME_PUBLISH name, token: value, purpose: meta?.purpose, - note: meta?.note ?? 'Synthetic lab token, safe to publish. Scoped to one notary.' + note: meta?.note ?? 'Synthetic lab token, safe to publish. Scoped to one local application.' }); } return tokens; diff --git a/home/src/lib/server/urlmap.test.ts b/home/src/lib/server/urlmap.test.ts index 1286bbd..f24de8d 100644 --- a/home/src/lib/server/urlmap.test.ts +++ b/home/src/lib/server/urlmap.test.ts @@ -2,104 +2,54 @@ import { describe, expect, it } from 'vitest'; import { buildPublicUrlMap, mapPublicUrl, rewriteRequestUrls } from './urlmap'; describe('public URL map', () => { - it('rewrites compose-internal notary hostnames to host-reachable localhost ports', () => { + it('rewrites internal Evidence and Mint origins to the local TLS gateway', () => { const map = buildPublicUrlMap(); - expect(mapPublicUrl('http://child-benefit-federator:8080/v1/evaluations', map)).toBe( - 'http://localhost:4321/v1/evaluations' + expect(mapPublicUrl('https://evidence.solmara.invalid/v1/evidence', map)).toBe( + 'https://localhost:4341/v1/evidence' + ); + expect(mapPublicUrl('https://mint.evidence.solmara.invalid/token', map)).toBe( + 'https://localhost:4341/token' ); - expect( - [ - 'cra-notary:8081', - 'nia-notary:8081', - 'sro-notary:8081', - 'programme-notary:8081', - 'sipf-notary:8081', - 'nagdi-notary:8081' - ].map((host) => mapPublicUrl(`http://${host}/v1/claims`, map)) - ).toEqual([ - 'http://localhost:4325/v1/claims', - 'http://localhost:4326/v1/claims', - 'http://localhost:4327/v1/claims', - 'http://localhost:4328/v1/claims', - 'http://localhost:4322/v1/claims', - 'http://localhost:4323/v1/claims' - ]); }); - it('rewrites relay and metadata hostnames from the same table', () => { + it('rewrites Relay and application hostnames from the same table', () => { const map = buildPublicUrlMap(); expect(mapPublicUrl('http://cra-civil-relay:8080/', map)).toBe('http://localhost:4311/'); - expect(mapPublicUrl('http://static-metadata:8080/metadata/catalog.json', map)).toBe( - 'http://localhost:4331/metadata/catalog.json' + expect(mapPublicUrl('http://child-benefit-federator:8080/v1/evaluations', map)).toBe( + 'http://localhost:4321/v1/evaluations' ); }); - it('preserves path and query while swapping the origin', () => { + it('preserves path and query while swapping an Evidence origin', () => { const map = buildPublicUrlMap(); - expect(mapPublicUrl('http://cra-notary:8081/v1/evaluations?trace=1', map)).toBe( - 'http://localhost:4325/v1/evaluations?trace=1' + expect(mapPublicUrl('https://evidence.solmara.invalid/v1/evidence?trace=1', map)).toBe( + 'https://localhost:4341/v1/evidence?trace=1' ); }); - it('leaves already host-reachable URLs untouched', () => { + it('leaves already host-reachable and non-URL values untouched', () => { const map = buildPublicUrlMap(); expect(mapPublicUrl('http://localhost:4321/v1/claims', map)).toBe('http://localhost:4321/v1/claims'); - expect(mapPublicUrl('https://child-benefit-federator.solmara.registrystack.org/v1/claims', map)).toBe( - 'https://child-benefit-federator.solmara.registrystack.org/v1/claims' - ); - }); - - it('returns non-URL strings unchanged', () => { - const map = buildPublicUrlMap(); expect(mapPublicUrl('not a url', map)).toBe('not a url'); - expect(mapPublicUrl('', map)).toBe(''); }); - it('merges an env-provided JSON override over the defaults', () => { - const map = buildPublicUrlMap( - JSON.stringify({ - 'child-benefit-federator:8080': 'https://child-benefit-federator.solmara.registrystack.org' - }) - ); - expect(mapPublicUrl('http://child-benefit-federator:8080/v1/claims', map)).toBe( - 'https://child-benefit-federator.solmara.registrystack.org/v1/claims' - ); - // untouched defaults still apply - expect(mapPublicUrl('http://sipf-notary:8081/v1/claims', map)).toBe( - 'http://localhost:4322/v1/claims' - ); + it('merges an environment override over the defaults', () => { + const map = buildPublicUrlMap(JSON.stringify({ 'evidence.solmara.invalid': 'https://evidence.example' })); + expect(mapPublicUrl('https://evidence.solmara.invalid/v1/evidence', map)).toBe('https://evidence.example/v1/evidence'); + expect(mapPublicUrl('http://sipf-pensions-relay:8080/ready', map)).toBe('http://localhost:4315/ready'); }); - it('rewrites request_source and credential_source urls inside a run result', () => { + it('rewrites nested request sources without mutating the input', () => { const map = buildPublicUrlMap(); const result = { - request_source: { method: 'POST', url: 'http://child-benefit-federator:8080/v1/evaluations', headers: {} }, - credential_source: { method: 'POST', url: 'http://sipf-notary:8081/v1/credentials', headers: {} }, - request_sources: [{ method: 'POST', url: 'http://nia-notary:8081/v1/evaluations', headers: {} }], - source_trace: [ - { - request_source: { - method: 'POST', - url: 'http://cra-notary:8081/v1/evaluations', - headers: {} - } - }, - { - request_summary: { - method: 'POST', - url: 'http://sro-notary:8081/v1/evaluations' - } - } - ], - response_source: { status: 200 } + request_source: { method: 'POST', url: 'https://evidence.solmara.invalid/v1/evidence', headers: {} }, + request_sources: [{ method: 'POST', url: 'https://evidence.solmara.invalid/v1/evidence', headers: {} }], + source_trace: [{ request_source: { method: 'GET', url: 'http://cra-civil-relay:8080/records/civil_people' } }] }; const mapped = rewriteRequestUrls(result, map); - expect(mapped.request_source.url).toBe('http://localhost:4321/v1/evaluations'); - expect(mapped.credential_source.url).toBe('http://localhost:4322/v1/credentials'); - expect(mapped.request_sources[0].url).toBe('http://localhost:4326/v1/evaluations'); - expect(mapped.source_trace[0].request_source?.url).toBe('http://localhost:4325/v1/evaluations'); - expect(mapped.source_trace[1].request_summary?.url).toBe('http://localhost:4327/v1/evaluations'); - // does not mutate the original - expect(result.request_source.url).toBe('http://child-benefit-federator:8080/v1/evaluations'); + expect(mapped.request_source.url).toBe('https://localhost:4341/v1/evidence'); + expect(mapped.request_sources[0].url).toBe('https://localhost:4341/v1/evidence'); + expect(mapped.source_trace[0].request_source?.url).toBe('http://localhost:4311/records/civil_people'); + expect(result.request_source.url).toBe('https://evidence.solmara.invalid/v1/evidence'); }); }); diff --git a/home/src/lib/server/urlmap.ts b/home/src/lib/server/urlmap.ts index aa50027..6a3aee1 100644 --- a/home/src/lib/server/urlmap.ts +++ b/home/src/lib/server/urlmap.ts @@ -15,12 +15,10 @@ import { env } from '$env/dynamic/private'; */ const DEFAULT_LOCAL_MAP: Record = { 'child-benefit-federator:8080': 'http://localhost:4321', - 'cra-notary:8081': 'http://localhost:4325', - 'nia-notary:8081': 'http://localhost:4326', - 'sro-notary:8081': 'http://localhost:4327', - 'programme-notary:8081': 'http://localhost:4328', - 'sipf-notary:8081': 'http://localhost:4322', - 'nagdi-notary:8081': 'http://localhost:4323', + 'evidence.solmara.invalid': 'https://localhost:4341', + 'mint.evidence.solmara.invalid': 'https://localhost:4341', + 'evidence:8080': 'https://localhost:4341', + 'mint:8081': 'https://localhost:4341', 'cra-civil-relay:8080': 'http://localhost:4311', 'nia-population-relay:8080': 'http://localhost:4312', 'sro-social-relay:8080': 'http://localhost:4313', diff --git a/home/src/lib/types.ts b/home/src/lib/types.ts index 9e21db1..eefc413 100644 --- a/home/src/lib/types.ts +++ b/home/src/lib/types.ts @@ -49,7 +49,7 @@ export type ConfigLink = { export type TopologyService = { id: string; label: string; - role: 'shared' | 'relay' | 'notary'; + role: 'shared' | 'relay' | 'evidence'; authority?: string; purpose?: string; blurb: string; @@ -108,8 +108,15 @@ export type ScenarioStep = { request_preview?: { method: string; url: string; - headers: Record; + headers?: Record; + purpose?: string; body?: unknown; + requests?: Array<{ + method: string; + url: string; + headers?: Record; + body?: unknown; + }>; }; }; @@ -130,8 +137,10 @@ export type Scenario = { export type RequestSource = { method: string; url: string; - headers: Record; + headers?: Record; + purpose?: string; body?: unknown; + requests?: RequestSource[]; }; export type ResponseSource = { diff --git a/home/src/routes/anatomy/+page.svelte b/home/src/routes/anatomy/+page.svelte index 6c9c369..640308b 100644 --- a/home/src/routes/anatomy/+page.svelte +++ b/home/src/routes/anatomy/+page.svelte @@ -14,14 +14,14 @@ Anatomy · Solmara Lab - +
diff --git a/home/src/routes/stories/[id]/+page.svelte b/home/src/routes/stories/[id]/+page.svelte index 2aaaaed..b777637 100644 --- a/home/src/routes/stories/[id]/+page.svelte +++ b/home/src/routes/stories/[id]/+page.svelte @@ -325,7 +325,7 @@

Accountability

-

{applicationEvidence ? 'What the source trace recorded about this access' : 'What the Notary recorded about this access'}

+

{applicationEvidence ? 'What the source trace recorded about this access' : 'What Registry Evidence recorded about this access'}

{#if accountability} {@const first = claimResults(accountability)[0]?.raw ?? {}}
diff --git a/justfile b/justfile index 7167187..674c811 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,5 @@ -set dotenv-load := true -set positional-arguments := true +set dotenv-load +set positional-arguments compose_project_name := `python3 scripts/compose_project_name.py` @@ -44,9 +44,9 @@ registry-projects-editor: registry-projects-test: scripts/registry-projects.sh test -# Build private Relay and Notary inputs for every authority-owned project. +# Build private public and consultation Relay inputs for every authority project. registry-projects-build environment="local": - scripts/registry-projects.sh build {{environment}} + scripts/registry-projects.sh build {{ environment }} # Refresh the committed runtime closure from all authored authority projects. registry-projects-sync: @@ -56,18 +56,14 @@ registry-projects-sync: registry-projects-runtime-check: scripts/registry-projects.sh check-runtime -# Verify signed hosted Relay closures against their public trust anchors. -hosted-relay-bundles-check: - uv run --locked scripts/check-hosted-relay-bundles.py - -# Exercise one complete compiler-generated SRO blue-green release transition. -contract-generation-proof: - uv run --locked scripts/contract-generation-proof.py - # Generate only local secrets. gen-secrets: scripts/gen-secrets.py +# Check the paired Mint config and all authored Registry Evidence fixtures. +evidence-check: + scripts/check-evidence-runtime.py + # Publish the static metadata bundle served by static-metadata. metadata-publish: @if command -v registry-manifest-cli >/dev/null 2>&1; then registry-manifest-cli publish metadata/solmara-wave1.metadata.yaml --out metadata/public/metadata --site-root metadata/public; fi @@ -86,7 +82,6 @@ lint: scripts/check-fiction.sh scripts/check-image-pins.py scripts/check-config-secrets.py - just hosted-relay-bundles-check just metadata-publish-check just metadata-lint @if [ -f portal/package.json ]; then cd portal && pnpm check; fi @@ -98,49 +93,47 @@ test: uv run python3 -m unittest discover -s scenario-runner -p 'test_*.py' @if [ -f portal/package.json ]; then cd portal && pnpm test; fi @if [ -f home/package.json ]; then cd home && pnpm test; fi - uv run python3 -m unittest discover -s scripts -p 'test_*.py' + uv run python3 -m unittest scripts/test_gen_secrets.py scripts/test_image_pins.py scripts/test_registryctl_build_output.py scripts/test_registryctl_test_output.py scripts/test_relay_workload_identity_agent.py scripts/test_smoke_esignet.py scripts/test_smoke_nia_attribute_release.py scripts/test_smoke_portal_compose.py scripts/test_smoke_relay_sources.py # Validate Compose files without starting services. compose: @if [ ! -f .env ]; then echo ".env is missing; run 'just gen-secrets' first" >&2; exit 1; fi - @if [ -f compose.yaml ]; then COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose --env-file versions.env --env-file .env -f compose.yaml config >/dev/null; fi - @if [ -f compose.hosted.yaml ]; then COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose --env-file versions.env --env-file .env -f compose.yaml -f compose.hosted.yaml config >/dev/null; fi - @if [ -f compose.esignet.yaml ]; then COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose --env-file versions.env --env-file .env -f compose.yaml -f compose.esignet.yaml config >/dev/null; fi - scripts/check-coolify-compose.sh + @if [ -f compose.yaml ]; then COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml config >/dev/null; fi + @if [ -f compose.esignet.yaml ]; then COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml -f compose.esignet.yaml config >/dev/null; fi # Start the local topology. up: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml up -d --build + scripts/build-registry-stack-runtime.sh + @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml up -d --build -# Build Registry Relay from the pinned source for explicit local development. +# Compatibility alias: all local starts now build Relay, Evidence, and Mint from source. up-dev: - scripts/build-relay-runtime.sh - @set -a; . ./versions.env; set +a; env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_DEV_IMAGE" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml up -d --build + just up # Stop the local topology without removing local volumes. down: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml down + @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml down # Start the local topology with eSignet-backed portal login. up-esignet: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml up -d --build + scripts/build-registry-stack-runtime.sh + @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml up -d --build -# Build Registry Relay from the pinned source for explicit local eSignet development. +# Compatibility alias for the source-built eSignet topology. up-esignet-dev: - scripts/build-relay-runtime.sh - @set -a; . ./versions.env; set +a; env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_DEV_IMAGE" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml up -d --build + just up-esignet # Stop the local eSignet topology without removing local volumes. down-esignet: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml down + @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml down # Stop the local topology and remove this checkout's local volumes. reset: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml down -v + @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml down -v # Stop the local eSignet topology and remove this checkout's local eSignet volumes. reset-esignet: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml down -v + @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml down -v # Run story and authority-application smokes against the running local topology. smoke: @@ -150,15 +143,11 @@ smoke: smoke-live: uv run --locked scripts/smoke-live.py -# Prove all six Notary databases survive exact `just down` / `just up` recreation. -notary-state-restart-proof: - uv run --locked scripts/notary_state_restart.py - # Smoke eSignet discovery; portal login proves the NIA attribute-release path end to end. smoke-esignet *args: - uv run scripts/smoke-esignet.py {{args}} + uv run scripts/smoke-esignet.py {{ args }} -# Probe Relay source endpoints used by live Notary smoke. +# Probe the Relay Records APIs used by Registry Evidence. relay-source-smoke: scripts/smoke-relay-sources.py @@ -174,19 +163,6 @@ portal-live-e2e: home-live-e2e: @cd home && SOLMARA_HOME_E2E_MODE=live PLAYWRIGHT_BASE_URL="http://127.0.0.1:${SOLMARA_HOME_PORT:-4301}" pnpm e2e -# Run public hosted health, endpoint, scenario, and portal smoke checks. -hosted-smoke *args: - uv run scripts/smoke-hosted.py {{args}} - -# Verify committed Registry Stack release inputs match a candidate or release tag. -release-pins tag: - scripts/check-release-pins.py "$1" - # Run release-readiness and security-oriented checks. review: scripts/review.sh - -# Run release review against an explicit Registry Stack candidate or release tag. -review-release tag: - scripts/check-release-pins.py "$1" - scripts/review.sh diff --git a/ministries/interior-population/fixtures/001-schema.sql b/ministries/interior-population/fixtures/001-schema.sql index d21f367..0e518d5 100644 --- a/ministries/interior-population/fixtures/001-schema.sql +++ b/ministries/interior-population/fixtures/001-schema.sql @@ -1 +1 @@ -create table population_person (uin text primary key, person_id text, legacy_nid text, given_name text, family_name text, birth_date date, sex text, district_code text, address_area text, settlement_type text, identity_status text, pending_merge_with_uin text, match_basis text, alive boolean, birth_brn text, updated_at timestamptz, observed_at timestamptz, source_system text); +create table if not exists population_person (uin text primary key, person_id text, legacy_nid text, given_name text, family_name text, birth_date date, sex text, district_code text, address_area text, settlement_type text, identity_status text, pending_merge_with_uin text, match_basis text, alive boolean, birth_brn text, updated_at timestamptz, observed_at timestamptz, source_system text); diff --git a/ministries/interior-population/fixtures/002-load.sql b/ministries/interior-population/fixtures/002-load.sql index 3e72b86..384f13a 100644 --- a/ministries/interior-population/fixtures/002-load.sql +++ b/ministries/interior-population/fixtures/002-load.sql @@ -1 +1,25 @@ -copy population_person from '/docker-entrypoint-initdb.d/population_person.csv' with (format csv, header true); +create temporary table population_person_fixture + (like population_person including defaults); + +copy population_person_fixture from '/docker-entrypoint-initdb.d/population_person.csv' with (format csv, header true); + +insert into population_person +select * from population_person_fixture +on conflict (uin) do update set + person_id = excluded.person_id, + legacy_nid = excluded.legacy_nid, + given_name = excluded.given_name, + family_name = excluded.family_name, + birth_date = excluded.birth_date, + sex = excluded.sex, + district_code = excluded.district_code, + address_area = excluded.address_area, + settlement_type = excluded.settlement_type, + identity_status = excluded.identity_status, + pending_merge_with_uin = excluded.pending_merge_with_uin, + match_basis = excluded.match_basis, + alive = excluded.alive, + birth_brn = excluded.birth_brn, + updated_at = excluded.updated_at, + observed_at = excluded.observed_at, + source_system = excluded.source_system; diff --git a/ministries/labour-pensions/fixtures/003-schema.sql b/ministries/labour-pensions/fixtures/003-schema.sql index 9a2f55e..6290be8 100644 --- a/ministries/labour-pensions/fixtures/003-schema.sql +++ b/ministries/labour-pensions/fixtures/003-schema.sql @@ -1,4 +1,4 @@ -create table pension_case ( +create table if not exists pension_case ( pension_case_id text primary key, pensioner_uin text not null, spouse_uin text, @@ -12,15 +12,15 @@ create table pension_case ( source_system text not null ); -create index pension_case_pensioner_uin_idx on pension_case (pensioner_uin); -create index pension_case_spouse_uin_idx on pension_case (spouse_uin); +create index if not exists pension_case_pensioner_uin_idx on pension_case (pensioner_uin); +create index if not exists pension_case_spouse_uin_idx on pension_case (spouse_uin); -create table sipf_pension_payment ( +create table if not exists sipf_pension_payment ( pensioner_uin text primary key, payment_status text not null ); -create table sipf_survivor_benefit ( +create table if not exists sipf_survivor_benefit ( spouse_uin text primary key, survivor_eligible boolean not null ); diff --git a/ministries/labour-pensions/fixtures/004-load.sql b/ministries/labour-pensions/fixtures/004-load.sql index 5c291df..1bc3483 100644 --- a/ministries/labour-pensions/fixtures/004-load.sql +++ b/ministries/labour-pensions/fixtures/004-load.sql @@ -1,7 +1,28 @@ -copy pension_case from '/docker-entrypoint-initdb.d/pension_case.csv' with (format csv, header true); +create temporary table pension_case_fixture + (like pension_case including defaults); + +copy pension_case_fixture from '/docker-entrypoint-initdb.d/pension_case.csv' with (format csv, header true); + +insert into pension_case +select * from pension_case_fixture +on conflict (pension_case_id) do update set + pensioner_uin = excluded.pensioner_uin, + spouse_uin = excluded.spouse_uin, + marriage_mrn = excluded.marriage_mrn, + pension_status = excluded.pension_status, + payment_status = excluded.payment_status, + survivor_eligible = excluded.survivor_eligible, + last_payment_date = excluded.last_payment_date, + account_life_status = excluded.account_life_status, + observed_at = excluded.observed_at, + source_system = excluded.source_system; insert into sipf_pension_payment (pensioner_uin, payment_status) -select pensioner_uin, payment_status from pension_case; +select pensioner_uin, payment_status from pension_case_fixture +on conflict (pensioner_uin) do update set + payment_status = excluded.payment_status; insert into sipf_survivor_benefit (spouse_uin, survivor_eligible) -select spouse_uin, survivor_eligible from pension_case where spouse_uin is not null; +select spouse_uin, survivor_eligible from pension_case_fixture where spouse_uin is not null +on conflict (spouse_uin) do update set + survivor_eligible = excluded.survivor_eligible; diff --git a/ministries/labour-pensions/fixtures/pension_case.csv b/ministries/labour-pensions/fixtures/pension_case.csv index 0390c95..5c669ff 100644 --- a/ministries/labour-pensions/fixtures/pension_case.csv +++ b/ministries/labour-pensions/fixtures/pension_case.csv @@ -2,4 +2,4 @@ pension_case_id,pensioner_uin,spouse_uin,marriage_mrn,pension_status,payment_sta SIPF-AWD-000001,2300109568,2300118698,MRN-1970-0401-00001,in_payment,active,true,2026-07-01,deceased,2026-07-04T09:00:00Z,SIPF-RELAY-PROJECTION SIPF-AWD-000002,2300118698,,,in_payment,active,false,,alive,2026-07-04T09:00:00Z,SIPF-RELAY-PROJECTION SIPF-AWD-000003,2300127827,,,in_payment,active,false,,alive,2026-07-04T09:00:00Z,SIPF-RELAY-PROJECTION -SIPF-AWD-000004,2300136959,,,pending_proof_of_life,active,false,,alive,2026-07-04T09:00:00Z,SIPF-RELAY-PROJECTION +SIPF-AWD-000004,2300136959,2300146081,MRN-2010-0302-00002,pending_proof_of_life,active,false,,alive,2026-07-04T09:00:00Z,SIPF-RELAY-PROJECTION diff --git a/ministries/labour-pensions/fixtures/sipf_survivor_link.csv b/ministries/labour-pensions/fixtures/sipf_survivor_link.csv index da0004e..36591ed 100644 --- a/ministries/labour-pensions/fixtures/sipf_survivor_link.csv +++ b/ministries/labour-pensions/fixtures/sipf_survivor_link.csv @@ -1,2 +1,3 @@ survivor_link_id,deceased_uin,survivor_uin,relationship,proof_mrn,survivor_award_no,link_status,observed_at,source_system SIPF-SL-000001,2300109568,2300118698,spouse,MRN-1970-0401-00001,,verified,2026-07-04T09:00:00Z,SIPF-CORE +SIPF-SL-000002,2300136959,2300146081,former_spouse,MRN-2010-0302-00002,,dissolved,2026-07-04T09:00:00Z,SIPF-CORE diff --git a/portal/src/lib/forms/descriptors.ts b/portal/src/lib/forms/descriptors.ts index c513482..e6b18ad 100644 --- a/portal/src/lib/forms/descriptors.ts +++ b/portal/src/lib/forms/descriptors.ts @@ -1,6 +1,6 @@ // The three wave 1 ServiceForm descriptors. // -// Each Field maps a kind/claim/notary/purpose/disclose so the form page can drive +// Each Field maps a kind/claim/authority/purpose/disclose so the form page can drive // the EvidenceField renderer and the BFF can resolve a canned scenario by field // id. The field ids ARE the scenario lookup keys the MockEvidenceProvider expects // (see resolveScenarioKey), so they must match the keys in providers/mock/scenarios.ts. @@ -12,11 +12,11 @@ import type { Field, NotaryId, ServiceForm } from '$lib/types'; export const PURPOSES = { - childBenefitReview: 'https://id.registrystack.org/solmara/purpose/child-benefit-review', - pensionPaymentReview: 'https://id.registrystack.org/solmara/purpose/pension-payment-review', - survivorBenefitDetermination: 'https://id.registrystack.org/solmara/purpose/survivor-benefit-determination', - voucherEligibilityReview: 'https://id.registrystack.org/solmara/purpose/voucher-eligibility-review', - citizenSelfService: 'https://id.registrystack.org/solmara/purpose/citizen-self-service' + childBenefitReview: 'child-benefit-review', + pensionPaymentReview: 'pension-payment-review', + survivorBenefitDetermination: 'survivor-benefit-determination', + voucherEligibilityReview: 'voucher-eligibility-review', + citizenSelfService: 'citizen-self-service' } as const; // --------------------------------------------------------------------------- diff --git a/portal/src/lib/providers/live/index.test.ts b/portal/src/lib/providers/live/index.test.ts index fd49e3f..afe612d 100644 --- a/portal/src/lib/providers/live/index.test.ts +++ b/portal/src/lib/providers/live/index.test.ts @@ -1,793 +1,105 @@ import { describe, expect, it, vi } from 'vitest'; -import { LiveEvidenceProvider } from '.'; import { PURPOSES } from '$lib/forms/descriptors'; -import { CLAIM_RESULT_FORMAT } from '$lib/providers/mock/wire'; - -function jsonResponse(body: unknown, status = 200): Response { - return new Response(JSON.stringify(body), { - status, - headers: { 'Content-Type': 'application/json' } - }); -} - -function serviceIdFor(claimId: string): string { - if (claimId === 'citizen-population-record-active') return 'nia-notary'; - if (claimId === 'pension-payment-active' || claimId === 'survivor-is-eligible') { - return 'sipf-notary'; - } - if (claimId.startsWith('farmer-')) return 'nagdi-notary'; - return 'cra-notary'; -} - -function evaluationResult(claimId: string, satisfied: boolean): Record { - const evaluationId = `eval-${claimId}`; - const claimVersion = '1'; - return { - claim_id: claimId, - claim_version: claimVersion, - subject_type: 'person', - target_ref: { handle: 'rnref:v1:test-target' }, - value: satisfied, - satisfied, - disclosure: 'predicate', - evaluation_id: evaluationId, - format: CLAIM_RESULT_FORMAT, - issued_at: '2026-07-15T00:00:00Z', - expires_at: null, - provenance: { - schema_version: 'registry-notary-claim-provenance/v2', - generated_by: { - type: 'claim_evaluation', - service_id: serviceIdFor(claimId), - evaluation_id: evaluationId, - claim_id: claimId, - claim_version: claimVersion - }, - used: { relay_consultation_count: 1 }, - derived_from: [] - } - }; -} - -function evaluationResponse(claimId: string, satisfied: boolean): Response { - return jsonResponse({ - results: [evaluationResult(claimId, satisfied)] - }); -} - -function fetchInit(fetcher: ReturnType, callIndex: number): RequestInit { - const init: unknown = fetcher.mock.calls[callIndex]?.[1]; - if (typeof init !== 'object' || init === null) throw new Error('missing fetch init'); - // Vitest stores the RequestInit object as the second recorded fetch argument. - return init as RequestInit; -} - -describe('LiveEvidenceProvider', () => { - it('calls the CRA Notary directly with its pension-purpose token and server-selected subject', async () => { - const fetcher = vi.fn().mockResolvedValue(evaluationResponse('person-is-deceased', true)); - const provider = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_PENSION_CLIENT_TOKEN: 'cra-pension-token' - }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'person-is-alive', label: 'Alive?', kind: 'verify' }, - { subject: '2300018263', selectedSubject: '2300109568' } - ); - - expect(fetcher).toHaveBeenCalledTimes(1); - expect(fetcher.mock.calls[0][0]).toBe( - 'https://cra-notary.solmara.registrystack.org/v1/evaluations' - ); - const init = fetchInit(fetcher, 0); - expect(init.headers).toEqual({ - 'x-api-key': 'cra-pension-token', - Accept: CLAIM_RESULT_FORMAT, - 'Data-Purpose': PURPOSES.pensionPaymentReview, - 'Content-Type': 'application/json' - }); - expect(JSON.parse(String(init.body))).toEqual({ - claims: ['person-is-deceased'], - purpose: PURPOSES.pensionPaymentReview, - disclosure: 'predicate', - format: CLAIM_RESULT_FORMAT, - relationship: { type: 'self' }, - target: { - type: 'Person', - identifiers: [{ scheme: 'solmara_uin', value: '2300109568' }] - } - }); - expect(evaluation.result.display).toBe('Death registered: yes'); - expect(JSON.stringify(fetcher.mock.calls)).not.toContain('relay'); - }); - - it('fails closed when a successful authority response omits the predicate decision', async () => { - const fetcher = vi.fn().mockResolvedValue( - jsonResponse({ - results: [ - { - claim_id: 'person-is-deceased', - claim_version: '1', - disclosure: 'predicate', - evaluation_id: 'eval-malformed', - issued_at: '2026-07-15T00:00:00Z' +import { LiveEvidenceProvider } from './index'; + +const ctx = { subject: '2300018263', delegatedTarget: '2300010248' }; + +function envelope(results: Array<{ claim_id: string; satisfied: boolean; value?: unknown }>, extra: Record = {}) { + return new Response( + JSON.stringify({ + result: { + response_source: { + status: 200, + body: { + results: results.map((item) => ({ ...item, value: item.value ?? item.satisfied })), + signed_evidence: [{ protected: 'e30', payload: 'e30', signature: 'c2ln' }] } - ] - }) - ); - const provider = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_PENSION_CLIENT_TOKEN: 'cra-pension-token' - }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'person-is-alive', label: 'Alive?', kind: 'verify' }, - { subject: '2300109568' } - ); - - expect(evaluation.result.state).toBe('error'); - expect(evaluation.proof.status).toBe('error'); - expect(evaluation.result.asOf).toBeUndefined(); - }); - - for (const requiredField of [ - 'claim_version', - 'evaluation_id', - 'issued_at', - 'format', - 'provenance' - ]) { - it(`rejects a direct Notary result missing ${requiredField} without filling it locally`, async () => { - const result = evaluationResult('person-is-deceased', true); - delete result[requiredField]; - const fetcher = vi.fn().mockResolvedValue(jsonResponse({ results: [result] })); - const provider = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_PENSION_CLIENT_TOKEN: 'cra-pension-token' }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'person-is-alive', label: 'Alive?', kind: 'verify' }, - { subject: '2300109568' } - ); - - expect(evaluation.result.state).toBe('error'); - expect(evaluation.proof.status).toBe('error'); - expect(evaluation.raw.response.body).toEqual({ results: [] }); - expect(evaluation.result.asOf).toBeUndefined(); - }); - } - - it('rejects direct provenance that does not bind to the expected authority and result', async () => { - const result = evaluationResult('person-is-deceased', true); - // The fixture builder always installs this provenance object before the test mutates it. - const provenance = result.provenance as { - generated_by: { service_id: string; evaluation_id: string }; - }; - provenance.generated_by.service_id = 'nia-notary'; - provenance.generated_by.evaluation_id = 'eval-other'; - const fetcher = vi.fn().mockResolvedValue(jsonResponse({ results: [result] })); - const provider = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_PENSION_CLIENT_TOKEN: 'cra-pension-token' - }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'person-is-alive', label: 'Alive?', kind: 'verify' }, - { subject: '2300109568' } - ); - - expect(evaluation.result.state).toBe('error'); - expect(evaluation.proof.status).toBe('error'); - expect(evaluation.raw.response.body).toEqual({ results: [] }); - }); - - it('does not present an expired direct claim result as verified', async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-07-15T12:00:00Z')); - try { - const result = evaluationResult('person-is-deceased', true); - result.issued_at = '2026-07-10T00:00:00Z'; - result.expires_at = '2026-07-14T00:00:00Z'; - const provider = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_PENSION_CLIENT_TOKEN: 'cra-pension-token' - }, - vi.fn().mockResolvedValue(jsonResponse({ results: [result] })) - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'person-is-alive', label: 'Alive?', kind: 'verify' }, - { subject: '2300109568' } - ); - - expect(evaluation.result).toMatchObject({ - state: 'stale', - display: 'person-is-deceased: expired evidence, refresh required', - asOf: '2026-07-10T00:00:00Z' - }); - expect(evaluation.proof.status).toBe('error'); - } finally { - vi.useRealTimers(); - } - }); - - it('derives the pension-stop decision in the portal from independent CRA and SIPF claims', async () => { - const fetcher = vi - .fn() - .mockResolvedValueOnce(evaluationResponse('person-is-deceased', true)) - .mockResolvedValueOnce(evaluationResponse('pension-payment-active', true)); - const provider = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_PENSION_CLIENT_TOKEN: 'cra-pension-token', - SIPF_NOTARY_URL: 'https://sipf-notary.solmara.registrystack.org', - SIPF_PENSION_CLIENT_TOKEN: 'sipf-pension-token' - }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'disability-determination', label: 'Stop pension?', kind: 'verify' }, - { subject: '2300109568' } - ); - - expect(fetcher).toHaveBeenCalledTimes(2); - expect(fetcher.mock.calls.map((call) => call[0])).toEqual([ - 'https://cra-notary.solmara.registrystack.org/v1/evaluations', - 'https://sipf-notary.solmara.registrystack.org/v1/evaluations' - ]); - const requests = fetcher.mock.calls.map((_call, index) => - JSON.parse(String(fetchInit(fetcher, index).body)) - ); - expect(requests.map((request) => request.claims)).toEqual([ - ['person-is-deceased'], - ['pension-payment-active'] - ]); - expect(JSON.stringify(requests)).not.toContain('pension-payment-should-stop'); - expect(evaluation.result.display).toBe('Pension payment should stop: yes'); - expect(evaluation.result.authority).toBeUndefined(); - expect(evaluation.proof.authority).toBeUndefined(); - expect(evaluation.raw.request.method).toBe('MULTI'); - expect(evaluation.raw.response.body).toMatchObject({ - orchestration: { service_id: 'citizen-portal', decision: 'application_composed' }, - derived_decisions: { 'pension-payment-should-stop': true }, - source_trace: [ - { service_id: 'cra-notary', claims: ['person-is-deceased'] }, - { service_id: 'sipf-notary', claims: ['pension-payment-active'] } - ] - }); - }); - - it('uses the oldest source timestamp for a composed decision', async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-07-15T12:00:00Z')); - try { - const civil = evaluationResult('person-is-deceased', true); - civil.issued_at = '2026-07-14T00:00:00Z'; - civil.expires_at = '2026-08-14T00:00:00Z'; - const payment = evaluationResult('pension-payment-active', true); - payment.issued_at = '2026-07-10T00:00:00Z'; - payment.expires_at = '2026-08-10T00:00:00Z'; - const fetcher = vi - .fn() - .mockResolvedValueOnce(jsonResponse({ results: [civil] })) - .mockResolvedValueOnce(jsonResponse({ results: [payment] })); - const provider = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_PENSION_CLIENT_TOKEN: 'cra-pension-token', - SIPF_NOTARY_URL: 'https://sipf-notary.solmara.registrystack.org', - SIPF_PENSION_CLIENT_TOKEN: 'sipf-pension-token' - }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'disability-determination', label: 'Stop pension?', kind: 'verify' }, - { subject: '2300109568' } - ); - - expect(evaluation.result.asOf).toBe('2026-07-10T00:00:00Z'); - expect(evaluation.result.state).toBe('verified'); - } finally { - vi.useRealTimers(); - } - }); - - it('does not compose a positive decision when any source result is expired', async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-07-15T12:00:00Z')); - try { - const civil = evaluationResult('person-is-deceased', true); - civil.issued_at = '2026-07-10T00:00:00Z'; - civil.expires_at = '2026-07-14T00:00:00Z'; - const payment = evaluationResult('pension-payment-active', true); - payment.issued_at = '2026-07-14T00:00:00Z'; - payment.expires_at = '2026-08-14T00:00:00Z'; - const provider = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_PENSION_CLIENT_TOKEN: 'cra-pension-token', - SIPF_NOTARY_URL: 'https://sipf-notary.solmara.registrystack.org', - SIPF_PENSION_CLIENT_TOKEN: 'sipf-pension-token' - }, - vi - .fn() - .mockResolvedValueOnce(jsonResponse({ results: [civil] })) - .mockResolvedValueOnce(jsonResponse({ results: [payment] })) - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'disability-determination', label: 'Stop pension?', kind: 'verify' }, - { subject: '2300109568' } - ); - - expect(evaluation.result.state).toBe('stale'); - expect(evaluation.proof.status).toBe('error'); - expect(evaluation.raw.response.body).toMatchObject({ - derived_decisions: { 'pension-payment-should-stop': null } - }); - } finally { - vi.useRealTimers(); - } - }); - - it('shows the direct SIPF payment predicate without inventing a household conflict result', async () => { - const fetcher = vi.fn().mockResolvedValue(evaluationResponse('pension-payment-active', true)); - const provider = new LiveEvidenceProvider( - { - SIPF_NOTARY_URL: 'https://sipf-notary.solmara.registrystack.org', - SIPF_PENSION_CLIENT_TOKEN: 'sipf-pension-token' - }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'pension-payment-active', label: 'Pension payment active?', kind: 'verify' }, - { subject: '2300109568' } - ); - - expect(evaluation.result.display).toBe('Pension payment active: yes'); - expect(JSON.stringify(evaluation)).not.toMatch(/conflict|household-size/); - }); - - it('derives only a boolean survivor decision and never invents a support band', async () => { - const fetcher = vi - .fn() - .mockResolvedValueOnce(evaluationResponse('person-is-deceased', true)) - .mockResolvedValueOnce(evaluationResponse('pension-payment-active', true)) - .mockResolvedValueOnce(evaluationResponse('survivor-is-eligible', true)); - const provider = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_PENSION_CLIENT_TOKEN: 'cra-pension-token', - SIPF_NOTARY_URL: 'https://sipf-notary.solmara.registrystack.org', - SIPF_PENSION_CLIENT_TOKEN: 'sipf-pension-token' - }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'combined-support-eligibility', label: 'Eligibility decision', kind: 'decision' }, - { subject: '2300109568' } - ); - - expect(evaluation.result.display).toBe('Survivor benefit eligible: yes'); - expect(evaluation.raw.response.body).toMatchObject({ - derived_decisions: { 'survivor-benefit-eligible': true } - }); - expect(JSON.stringify(evaluation)).not.toMatch(/support.band|band B/i); - }); - - it('routes survivor evidence to SIPF under the survivor-benefit purpose', async () => { - const fetcher = vi.fn().mockResolvedValue(evaluationResponse('survivor-is-eligible', true)); - const provider = new LiveEvidenceProvider( - { - SIPF_NOTARY_URL: 'https://sipf-notary.solmara.registrystack.org', - SIPF_PENSION_CLIENT_TOKEN: 'sipf-pension-token' - }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'functioning-assessment', label: 'Survivor eligible?', kind: 'verify' }, - { subject: '2300118698' } - ); - - expect(fetcher.mock.calls[0][0]).toBe( - 'https://sipf-notary.solmara.registrystack.org/v1/evaluations' - ); - expect(fetchInit(fetcher, 0).headers).toMatchObject({ - 'x-api-key': 'sipf-pension-token', - 'Data-Purpose': PURPOSES.survivorBenefitDetermination - }); - expect(evaluation.proof.crypto).toMatchObject({ - issuerKey: 'Not applicable for claim-result evaluation', - credential: 'Claim results only; no credential issued by the portal' - }); - }); - - it('keeps CRA and NIA citizen evidence separate and names NIA as credential owner', async () => { - const fetcher = vi - .fn() - .mockResolvedValueOnce(evaluationResponse('civil-record-linked', true)) - .mockResolvedValueOnce(evaluationResponse('citizen-population-record-active', true)); - const provider = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_CITIZEN_CLIENT_TOKEN: 'cra-citizen-token', - NIA_NOTARY_URL: 'https://nia-notary.solmara.registrystack.org', - NIA_CITIZEN_CLIENT_TOKEN: 'nia-citizen-token' - }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'citizen-record-status', label: 'Citizen record status', kind: 'verify' }, - { subject: '2300018263' } - ); - - expect(fetcher.mock.calls.map((call) => call[0])).toEqual([ - 'https://cra-notary.solmara.registrystack.org/v1/evaluations', - 'https://nia-notary.solmara.registrystack.org/v1/evaluations' - ]); - expect(fetcher.mock.calls.map((_call, index) => fetchInit(fetcher, index).headers)).toEqual([ - { - 'x-api-key': 'cra-citizen-token', - Accept: CLAIM_RESULT_FORMAT, - 'Data-Purpose': PURPOSES.citizenSelfService, - 'Content-Type': 'application/json' - }, - { - 'x-api-key': 'nia-citizen-token', - Accept: CLAIM_RESULT_FORMAT, - 'Data-Purpose': PURPOSES.citizenSelfService, - 'Content-Type': 'application/json' + source_trace: [{ authority: 'Source authority', service_id: 'registry-evidence', status: 200 }], + ...extra } - ]); - expect(evaluation.result.display).toBe('Civil and population records active: yes'); - expect(evaluation.proof.crypto).toMatchObject({ - issuerKey: 'Not applicable for claim-result evaluation', - credential: 'Application decision only; no credential issued by the portal' - }); - expect(JSON.stringify(evaluation)).not.toMatch(/CSR-BIRTH|certificate_id|issued_on/); - }); - - it('denies delegated reads before configuration lookup or any upstream call', async () => { - const fetcher = vi.fn(); - const provider = new LiveEvidenceProvider({}, fetcher); - - const evaluation = await provider.evaluateDetailed( - { id: 'date-of-birth', label: 'Child age under 5', kind: 'verify' }, - { subject: '2300018263', delegatedTarget: '2300010248' } - ); - - expect(fetcher).not.toHaveBeenCalled(); - expect(evaluation.raw.request.url).toBe( - 'solmara://citizen-portal/blocked-before-authority-call' - ); - expect(evaluation.raw.response.status).toBe(403); - expect(evaluation.result.reasonCode).toBe('relationship_not_proven'); - }); - - it('uses the child application ordinary JSON evidence contract after the guardian gate', async () => { - const fetcher = vi.fn().mockResolvedValue( - jsonResponse({ - schema_version: 'solmara-child-benefit-evidence/v1', - evidence_set_id: 'cbe_live_1', - orchestration: { - service_id: 'child-benefit-federator', - decision: 'not_composed' - }, - purpose: PURPOSES.childBenefitReview, - target: { type: 'Person', identifier_schemes: ['solmara_uin'] }, - results: [ - { - claim_id: 'child-age-under-5', - claim_version: '1', - disclosure: 'predicate', - format: CLAIM_RESULT_FORMAT, - issued_at: '2026-07-15T00:00:00Z', - expires_at: '2026-08-14T00:00:00Z', - satisfied: true, - authority: 'Civil Registration Authority', - notary_service_id: 'cra-notary' - } - ], - source_trace: [ - { - authority: 'Civil Registration Authority', - service_id: 'cra-notary', - claims: ['child-age-under-5'] - } - ] - }) - ); - const provider = new LiveEvidenceProvider( - { - CHILD_BENEFIT_FEDERATOR_URL: - 'https://child-benefit-federator.solmara.registrystack.org', - CHILD_BENEFIT_FEDERATOR_TOKEN: 'child-token' - }, - fetcher - ); + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ); +} +describe('LiveEvidenceProvider', () => { + it('routes reviewed fields through the scenario runner with an Evidence purpose code', async () => { + const fetcher = vi.fn(async () => envelope([{ claim_id: 'farmer-registered', satisfied: true }])) as unknown as typeof fetch; + const provider = new LiveEvidenceProvider({ SCENARIO_RUNNER_URL: 'http://scenario-runner:8080' }, fetcher); const evaluation = await provider.evaluateDetailed( - { id: 'date-of-birth', label: 'Child age under 5', kind: 'verify' }, - { subject: '2300018263', delegatedTarget: '2300010248' }, - { guardianLinkVerified: true } + { id: 'registered-farmer', label: 'Registered farmer?', kind: 'verify' }, + ctx ); expect(fetcher).toHaveBeenCalledTimes(1); - expect(fetcher.mock.calls[0][0]).toBe( - 'https://child-benefit-federator.solmara.registrystack.org/v1/evaluations' - ); - const init = fetchInit(fetcher, 0); - expect(init.headers).toEqual({ - 'x-api-key': 'child-token', - Accept: 'application/json', - 'Data-Purpose': PURPOSES.childBenefitReview, - 'Content-Type': 'application/json' - }); - expect(JSON.parse(String(init.body))).toEqual({ - target: { - type: 'Person', - identifiers: [{ scheme: 'solmara_uin', value: '2300010248' }] - }, - claims: ['child-age-under-5'], - disclosure: 'predicate', - format: 'application/json', - variables: { as_of_date: '2026-01-15' } - }); - expect(evaluation.raw.response.body).toMatchObject({ - schema_version: 'solmara-child-benefit-evidence/v1', - evidence_set_id: 'cbe_live_1', - orchestration: { decision: 'not_composed' }, - source_trace: [{ service_id: 'cra-notary' }] - }); - expect(evaluation.proof.crypto).toMatchObject({ - algorithm: 'Ordinary JSON response; no application signature asserted', - credential: 'Minimized source-attributed predicate result', - auditId: 'evidence-set:cbe_live_1' - }); - expect(JSON.stringify(evaluation.proof.crypto)).not.toMatch(/federated|federation/); - expect(JSON.stringify(evaluation.proof.crypto)).not.toContain('SD-JWT'); + const [url, init] = vi.mocked(fetcher).mock.calls[0]; + expect(String(url)).toContain('/v1/scenarios/farmer-climate-smart-voucher/steps/positive/run'); + expect(JSON.parse(String(init?.body))).toEqual({ config: { purpose_override: PURPOSES.voucherEligibilityReview } }); + expect(evaluation.result.state).toBe('verified'); + expect(evaluation.proof.crypto.algorithm).toBe('Flattened JWS, EdDSA'); + expect(JSON.stringify(evaluation)).not.toContain('x-api-key'); }); - it('rejects a child predicate that lacks matching source attribution', async () => { - const fetcher = vi.fn().mockResolvedValue( - jsonResponse({ - schema_version: 'solmara-child-benefit-evidence/v1', - evidence_set_id: 'cbe_unattributed', - orchestration: { - service_id: 'child-benefit-federator', - decision: 'not_composed' - }, - purpose: PURPOSES.childBenefitReview, - target: { type: 'Person', identifier_schemes: ['solmara_uin'] }, - results: [ - { - claim_id: 'child-age-under-5', - claim_version: '1', - disclosure: 'predicate', - format: CLAIM_RESULT_FORMAT, - issued_at: '2026-07-15T00:00:00Z', - expires_at: null, - satisfied: true, - authority: 'Civil Registration Authority', - notary_service_id: 'cra-notary' - } - ], - source_trace: [] - }) - ); - const provider = new LiveEvidenceProvider( - { - CHILD_BENEFIT_FEDERATOR_URL: - 'https://child-benefit-federator.solmara.registrystack.org', - CHILD_BENEFIT_FEDERATOR_TOKEN: 'child-token' - }, - fetcher - ); - + it('blocks a dependent read until the relationship is proven', async () => { + const fetcher = vi.fn() as unknown as typeof fetch; + const provider = new LiveEvidenceProvider({ SCENARIO_RUNNER_URL: 'http://scenario-runner:8080' }, fetcher); const evaluation = await provider.evaluateDetailed( { id: 'date-of-birth', label: 'Child age under 5', kind: 'verify' }, - { subject: '2300018263', delegatedTarget: '2300010248' }, - { guardianLinkVerified: true } - ); - - expect(evaluation.result.state).toBe('error'); - expect(evaluation.proof.status).toBe('error'); - expect(evaluation.result.display).toBe('child-age-under-5: not returned'); - expect(evaluation.result.asOf).toBeUndefined(); - }); - - it('does not present expired child application evidence as verified', async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-07-15T12:00:00Z')); - try { - const fetcher = vi.fn().mockResolvedValue( - jsonResponse({ - schema_version: 'solmara-child-benefit-evidence/v1', - evidence_set_id: 'cbe_expired', - orchestration: { - service_id: 'child-benefit-federator', - decision: 'not_composed' - }, - purpose: PURPOSES.childBenefitReview, - target: { type: 'Person', identifier_schemes: ['solmara_uin'] }, - results: [ - { - claim_id: 'child-age-under-5', - claim_version: '1', - disclosure: 'predicate', - format: CLAIM_RESULT_FORMAT, - issued_at: '2026-07-10T00:00:00Z', - expires_at: '2026-07-14T00:00:00Z', - satisfied: true, - authority: 'Civil Registration Authority', - notary_service_id: 'cra-notary' - } - ], - source_trace: [ - { - authority: 'Civil Registration Authority', - service_id: 'cra-notary', - claims: ['child-age-under-5'] - } - ] - }) - ); - const provider = new LiveEvidenceProvider( - { - CHILD_BENEFIT_FEDERATOR_URL: - 'https://child-benefit-federator.solmara.registrystack.org', - CHILD_BENEFIT_FEDERATOR_TOKEN: 'child-token' - }, - fetcher - ); - - const evaluation = await provider.evaluateDetailed( - { id: 'date-of-birth', label: 'Child age under 5', kind: 'verify' }, - { subject: '2300018263', delegatedTarget: '2300010248' }, - { guardianLinkVerified: true } - ); - - expect(evaluation.result.state).toBe('stale'); - expect(evaluation.result.display).toBe('child-age-under-5: expired evidence, refresh required'); - expect(evaluation.proof.status).toBe('error'); - } finally { - vi.useRealTimers(); - } - }); - - it('rejects internally consistent child attribution from the wrong authority', async () => { - const fetcher = vi.fn().mockResolvedValue( - jsonResponse({ - schema_version: 'solmara-child-benefit-evidence/v1', - evidence_set_id: 'cbe_wrong_authority', - orchestration: { - service_id: 'child-benefit-federator', - decision: 'not_composed' - }, - purpose: PURPOSES.childBenefitReview, - target: { type: 'Person', identifier_schemes: ['solmara_uin'] }, - results: [ - { - claim_id: 'child-age-under-5', - claim_version: '1', - disclosure: 'predicate', - format: CLAIM_RESULT_FORMAT, - issued_at: '2026-07-15T00:00:00Z', - expires_at: null, - satisfied: true, - authority: 'National Identity Agency', - notary_service_id: 'nia-notary' - } - ], - source_trace: [ - { - authority: 'National Identity Agency', - service_id: 'nia-notary', - claims: ['child-age-under-5'] - } - ] - }) - ); - const provider = new LiveEvidenceProvider( - { - CHILD_BENEFIT_FEDERATOR_URL: - 'https://child-benefit-federator.solmara.registrystack.org', - CHILD_BENEFIT_FEDERATOR_TOKEN: 'child-token' - }, - fetcher + ctx ); + expect(fetcher).not.toHaveBeenCalled(); + expect(evaluation.proof.status).toBe('denied'); + expect(evaluation.raw.request.url).toBe('solmara://citizen-portal/blocked-before-evidence'); + }); + + it('selects one concept from a signed child-benefit assertion set', async () => { + const fetcher = vi.fn(async () => envelope([ + { claim_id: 'birth-is-registered', satisfied: true }, + { claim_id: 'child-age-under-5', satisfied: true }, + { claim_id: 'population-record-active', satisfied: true }, + { claim_id: 'household-below-poverty-threshold', satisfied: true }, + { claim_id: 'not-already-enrolled', satisfied: true } + ])) as unknown as typeof fetch; + const provider = new LiveEvidenceProvider({ SCENARIO_RUNNER_URL: 'http://scenario-runner:8080' }, fetcher); const evaluation = await provider.evaluateDetailed( { id: 'date-of-birth', label: 'Child age under 5', kind: 'verify' }, - { subject: '2300018263', delegatedTarget: '2300010248' }, + ctx, { guardianLinkVerified: true } ); - expect(evaluation.result.state).toBe('error'); - expect(evaluation.proof.status).toBe('error'); - expect(evaluation.result.display).toBe('child-age-under-5: not returned'); - expect(evaluation.result.asOf).toBeUndefined(); + expect(evaluation.result.state).toBe('verified'); + expect(evaluation.proof.answered).toContain('child-age-under-5 = true'); }); - it('returns the server-owned cross-person denial without an upstream call', async () => { - const fetcher = vi.fn(); - const provider = new LiveEvidenceProvider({}, fetcher); - + it('combines separate pension and survivor Evidence steps in the portal application', async () => { + const fetcher = vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes('/stop-payment/')) { + return envelope([ + { claim_id: 'person-is-deceased', satisfied: true }, + { claim_id: 'pension-payment-active', satisfied: true } + ]); + } + return envelope([{ claim_id: 'survivor-is-eligible', satisfied: true }]); + }) as unknown as typeof fetch; + const provider = new LiveEvidenceProvider({ SCENARIO_RUNNER_URL: 'http://scenario-runner:8080' }, fetcher); const evaluation = await provider.evaluateDetailed( - { id: 'denial', label: 'Cross-person denial', kind: 'verify' }, - { subject: '2300018263' }, - { scenarioKey: 'denial' } + { id: 'combined-support-eligibility', label: 'Eligibility', kind: 'decision' }, + ctx ); - expect(fetcher).not.toHaveBeenCalled(); - expect(evaluation.result.reasonCode).toBe('subject_mismatch'); + expect(fetcher).toHaveBeenCalledTimes(2); expect(evaluation.result.authority).toBeUndefined(); - expect(evaluation.proof.authority).toBeUndefined(); - expect(evaluation.proof.headline).toBe( - 'Portal denied the cross-person request before any authority call' - ); - expect(evaluation.proof.answered).toContain('before any authority call'); - expect(evaluation.raw.response.status).toBe(403); + expect(evaluation.result.state).toBe('verified'); + expect(evaluation.raw.request.method).toBe('MULTI'); }); - it('requires the authority endpoint and purpose-specific token only when evaluated', async () => { - const missingUrl = new LiveEvidenceProvider( - { CRA_PENSION_CLIENT_TOKEN: 'cra-pension-token' }, - vi.fn() - ); - await expect( - missingUrl.evaluateDetailed( - { id: 'person-is-alive', label: 'Alive?', kind: 'verify' }, - { subject: '2300018263' } - ) - ).rejects.toThrow('CRA_NOTARY_URL is required'); - - const missingToken = new LiveEvidenceProvider( - { CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org' }, - vi.fn() - ); - await expect( - missingToken.evaluateDetailed( - { id: 'person-is-alive', label: 'Alive?', kind: 'verify' }, - { subject: '2300018263' } - ) - ).rejects.toThrow('CRA_PENSION_CLIENT_TOKEN is required'); - - const partialFetcher = vi.fn(); - const incompleteCitizen = new LiveEvidenceProvider( - { - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_CITIZEN_CLIENT_TOKEN: 'cra-citizen-token', - NIA_NOTARY_URL: 'https://nia-notary.solmara.registrystack.org' - }, - partialFetcher - ); + it('requires only the scenario runner URL when a live call is made', async () => { + const provider = new LiveEvidenceProvider({}, vi.fn() as unknown as typeof fetch); await expect( - incompleteCitizen.evaluateDetailed( - { id: 'citizen-record-status', label: 'Citizen record status', kind: 'verify' }, - { subject: '2300018263' } - ) - ).rejects.toThrow('NIA_CITIZEN_CLIENT_TOKEN is required'); - expect(partialFetcher).not.toHaveBeenCalled(); + provider.evaluateDetailed({ id: 'registered-farmer', label: 'Registered farmer?', kind: 'verify' }, ctx) + ).rejects.toThrow('SCENARIO_RUNNER_URL is required'); }); }); diff --git a/portal/src/lib/providers/live/index.ts b/portal/src/lib/providers/live/index.ts index cecfc43..4b82df0 100644 --- a/portal/src/lib/providers/live/index.ts +++ b/portal/src/lib/providers/live/index.ts @@ -1,343 +1,158 @@ import type { EvaluateContext, DetailedEvidenceProvider } from '$lib/providers/EvidenceProvider'; import type { ClaimResult, Field, ProofTrace } from '$lib/types'; -import { - SCENARIOS, - PERSONA, - type EvaluateOptions, - type MockEvaluation -} from '$lib/providers/mock'; -import { AUTHORITY_LABEL, NOTARY_SERVICE_ID } from '$lib/providers/mock/scenarios'; -import { - authorityPlan, - isApplicationOwnedPlan, - type AuthorityClient, - type AuthorityPlan -} from '$lib/providers/authority-plan'; -import { - authorityLabel, - buildChildBenefitRequest, - buildEvaluationRequest, - buildRawRequest, - CLAIM_RESULT_FORMAT, - makeEvaluationId, - type ClaimResultView, - type RawApplicationRequest, - type RawApplicationResponse, - type RawChildBenefitResponse, - type RawEvaluateRequest, - type RawEvaluationResponse, - type RawProviderResponse -} from '$lib/providers/mock/wire'; +import { SCENARIOS, type EvaluateOptions, type MockEvaluation } from '$lib/providers/mock'; export type LiveProviderEnv = { - CHILD_BENEFIT_FEDERATOR_URL?: string; - CHILD_BENEFIT_FEDERATOR_TOKEN?: string; - CRA_NOTARY_URL?: string; - CRA_PENSION_CLIENT_TOKEN?: string; - CRA_CITIZEN_CLIENT_TOKEN?: string; - NIA_NOTARY_URL?: string; - NIA_CITIZEN_CLIENT_TOKEN?: string; - SIPF_NOTARY_URL?: string; - SIPF_PENSION_CLIENT_TOKEN?: string; - NAGDI_NOTARY_URL?: string; - NAGDI_NOTARY_TOKEN?: string; + SCENARIO_RUNNER_URL?: string; }; type Fetcher = typeof fetch; -type LiveClient = 'childBenefit' | AuthorityClient; -type LiveServiceRef = { url?: string; token?: string; urlEnv: string; tokenEnv: string }; -type DenialBody = { error: string; error_description: string }; -type AuthorityEvaluation = { - plan: AuthorityPlan; - url: string; - request: RawEvaluateRequest; - status: number; - response: RawEvaluationResponse; -}; -type ConfiguredService = { url: string; token: string }; -type EvaluationOutcome = { - hasResult: boolean; - satisfied: boolean | null; - value: unknown; - stale: boolean; - issuedAt?: string; - derivedDecisions?: Record; +type Dict = Record; +type RunnerResult = { + friendly?: { status?: string; title?: string; message?: string }; + request_source?: Dict; + request_sources?: Dict[]; + response_source?: { status?: number | null; body?: unknown; error?: string }; + source_trace?: Dict[]; + derived_decisions?: Record; }; +type RunnerCall = { scenarioId: string; stepId: string }; +/** + * The live portal delegates scenario execution to the server-side scenario + * runner. That runner owns Mint authentication and calls Registry Evidence; + * browser-controlled input can select only a reviewed portal field. + */ export class LiveEvidenceProvider implements DetailedEvidenceProvider { #seq = 0; - #services: Record; + #runnerUrl: string | undefined; #fetch: Fetcher; constructor(env: LiveProviderEnv, fetcher: Fetcher = fetch) { - this.#services = { - childBenefit: { - url: env.CHILD_BENEFIT_FEDERATOR_URL, - token: env.CHILD_BENEFIT_FEDERATOR_TOKEN, - urlEnv: 'CHILD_BENEFIT_FEDERATOR_URL', - tokenEnv: 'CHILD_BENEFIT_FEDERATOR_TOKEN' - }, - craPension: { - url: env.CRA_NOTARY_URL, - token: env.CRA_PENSION_CLIENT_TOKEN, - urlEnv: 'CRA_NOTARY_URL', - tokenEnv: 'CRA_PENSION_CLIENT_TOKEN' - }, - craCitizen: { - url: env.CRA_NOTARY_URL, - token: env.CRA_CITIZEN_CLIENT_TOKEN, - urlEnv: 'CRA_NOTARY_URL', - tokenEnv: 'CRA_CITIZEN_CLIENT_TOKEN' - }, - niaCitizen: { - url: env.NIA_NOTARY_URL, - token: env.NIA_CITIZEN_CLIENT_TOKEN, - urlEnv: 'NIA_NOTARY_URL', - tokenEnv: 'NIA_CITIZEN_CLIENT_TOKEN' - }, - sipfPension: { - url: env.SIPF_NOTARY_URL, - token: env.SIPF_PENSION_CLIENT_TOKEN, - urlEnv: 'SIPF_NOTARY_URL', - tokenEnv: 'SIPF_PENSION_CLIENT_TOKEN' - }, - nagdi: { - url: env.NAGDI_NOTARY_URL, - token: env.NAGDI_NOTARY_TOKEN, - urlEnv: 'NAGDI_NOTARY_URL', - tokenEnv: 'NAGDI_NOTARY_TOKEN' - } - }; + this.#runnerUrl = env.SCENARIO_RUNNER_URL; this.#fetch = fetcher; } async evaluate(field: Field, ctx: EvaluateContext, opts?: EvaluateOptions): Promise { - const evaluation = await this.evaluateDetailed(field, ctx, opts); - return evaluation.result; + return (await this.evaluateDetailed(field, ctx, opts)).result; } async evaluateDetailed( field: Field, - ctx: EvaluateContext, + _ctx: EvaluateContext, opts?: EvaluateOptions ): Promise { const scenarioKey = opts?.scenarioKey ?? field.id; const scenario = SCENARIOS[scenarioKey]; - if (!scenario) { - throw new Error(`LiveEvidenceProvider: no scenario mapping for field "${field.id}"`); - } + if (!scenario) throw new Error(`LiveEvidenceProvider: no scenario mapping for field "${field.id}"`); - if (scenarioKey === 'denial') { - return this.#denied(scenarioKey, scenario, ctx, 'subject_mismatch'); - } + if (scenarioKey === 'denial') return this.#blocked(field, scenario, 'subject_mismatch'); if (scenario.delegated && opts?.guardianLinkVerified !== true) { - return this.#denied(scenarioKey, scenario, ctx, 'relationship_not_proven'); + return this.#blocked(field, scenario, 'relationship_not_proven'); } + const calls = runnerCalls(scenarioKey, scenario.service); + const responses = await Promise.all(calls.map((call) => this.#run(call, scenario.purpose))); + const status = responses.find((response) => !isSuccess(response.response_source?.status))?.response_source?.status ?? 200; + const results = responses.flatMap((response) => responseResults(response)); + const outcome = outcomeFor(scenarioKey, scenario.claimId, results, responses); + const proofStatus = status === 403 ? 'denied' : !isSuccess(status) || !outcome.found ? 'error' : outcome.satisfied === false ? 'false' : 'ok'; const seq = ++this.#seq; - const subject = resolveSubject(scenarioKey, scenario, ctx); - if (scenario.service === 'childBenefit') { - return this.#evaluateChild(scenario, subject, seq); - } - - const plan = authorityPlan(scenarioKey, scenario); - const configured = plan.map((authority) => ({ - authority, - service: this.#serviceFor(authority.client) - })); - const evaluations = await Promise.all( - configured.map(({ authority, service }) => - this.#evaluateAuthority(authority, subject, service) - ) - ); - const normalizedResults = evaluations.flatMap((evaluation) => evaluation.response.results); - const outcome = evaluationOutcome(scenarioKey, scenario, normalizedResults); - const failed = evaluations.find( - (evaluation) => evaluation.status < 200 || evaluation.status >= 300 - ); - const responseStatus = failed?.status ?? 200; - const status = proofStatus(responseStatus, outcome.satisfied, outcome.hasResult); - const raw = liveRawTrace(evaluations, outcome.derivedDecisions); - const applicationOwned = isApplicationOwnedPlan(plan); - - return { - result: portalResult(scenario, status, outcome, seq), - raw, - proof: { - headline: scenario.headline, - answered: answeredByAuthorities(evaluations), - notDisclosed: scenario.notDisclosed, - status, - authority: applicationOwned ? undefined : scenario.notary, - crypto: liveCrypto(scenarioKey, evaluations) - }, - timing: { latencyMs: 0, staggerOrder: scenario.staggerOrder, slow: false } + const applicationOwned = scenario.applicationOwned === true; + const responseBody = { + results, + signed_evidence: responses.flatMap((response) => signedEvidence(response)), + source_trace: responses.flatMap((response) => response.source_trace ?? []), + ...(outcome.derivedDecisions ? { derived_decisions: outcome.derivedDecisions } : {}) }; - } - - async #evaluateChild( - scenario: (typeof SCENARIOS)[string], - subject: string, - seq: number - ): Promise { - const service = this.#serviceFor('childBenefit'); - const request = buildChildBenefitRequest(scenario, subject); - const url = joinedUrl(service.url, '/v1/evaluations'); - const response = await this.#fetchJson(url, { - method: 'POST', - headers: { - ...notaryHeaders(service.token, scenario.purpose, 'application/json'), - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }); - const childBody = - isChildBenefitResponse(response.body) && response.body.purpose === scenario.purpose - ? response.body - : undefined; - const claim = childBody?.results.find((result) => result.claim_id === scenario.claimId); - const attributed = - childBody !== undefined && claim !== undefined && hasChildAttribution(childBody, scenario); - const stale = attributed && isExpired(claim.expires_at); - const outcome: EvaluationOutcome = { - hasResult: attributed, - satisfied: attributed && !stale ? claim.satisfied : null, - value: attributed && !stale ? claim.satisfied : undefined, - stale, - issuedAt: attributed ? claim.issued_at : undefined + const requestBody = { + purpose: scenario.purpose, + scenario_steps: calls.map((call) => `${call.scenarioId}/${call.stepId}`), + composition: calls.length > 1 ? 'portal-application' : 'single-reviewed-step' }; - const status = proofStatus(response.status, outcome.satisfied, outcome.hasResult); - const evidenceSetId = childBody?.evidence_set_id; - const responseBody: RawProviderResponse = childBody ?? { results: [] }; + const display = proofStatus === 'ok' && outcome.satisfied === true + ? scenario.display + : `${scenario.claimId}: ${outcome.found ? String(outcome.value) : 'not returned'}`; return { - result: portalResult(scenario, status, outcome, seq), - raw: { - request: { method: 'POST', url, body: request }, - response: { status: response.status, body: responseBody } + result: { + state: proofStatus === 'ok' ? scenario.state : proofStatus === 'false' ? 'false' : 'error', + display, + ...(!applicationOwned ? { authority: scenario.notary } : {}), + traceId: `event ${seq}` }, + raw: { + request: { + method: calls.length > 1 ? 'MULTI' : 'POST', + url: `${requiredRunnerUrl(this.#runnerUrl)}/v1/scenarios`, + body: requestBody + }, + response: { status: typeof status === 'number' ? status : 503, body: responseBody } + } as unknown as MockEvaluation['raw'], proof: { headline: scenario.headline, - answered: childAnswered(attributed ? childBody : undefined, scenario.claimId), + answered: outcome.found + ? `Registry Evidence answered: ${scenario.claimId} = ${String(outcome.value)}` + : 'Registry Evidence returned no usable value for this field', notDisclosed: scenario.notDisclosed, - status, - authority: scenario.notary, - crypto: { - signedBy: `No application signature; ${authorityLabel(scenario)} source result was collected by child-benefit-federator`, - algorithm: 'Ordinary JSON response; no application signature asserted', - issuerKey: 'Not applicable for an application evidence set', - holderBound: 'Not credential-bound; the BFF selected the purpose and subject', - credential: 'Minimized source-attributed predicate result', - auditId: `evidence-set:${evidenceSetId ?? 'unavailable'}` - } + status: proofStatus, + authority: applicationOwned ? undefined : scenario.notary, + crypto: evidenceProof(responses) }, timing: { latencyMs: 0, staggerOrder: scenario.staggerOrder, slow: false } }; } - async #evaluateAuthority( - plan: AuthorityPlan, - subject: string, - service: ConfiguredService - ): Promise { - const request = buildEvaluationRequest( - plan.claimId, - subject, - plan.purpose, - plan.scheme ?? 'solmara_uin' - ); - const url = joinedUrl(service.url, '/v1/evaluations'); - const response = await this.#fetchJson(url, { + async #run(call: RunnerCall, purpose: string): Promise { + const url = `${requiredRunnerUrl(this.#runnerUrl)}/v1/scenarios/${call.scenarioId}/steps/${call.stepId}/run`; + const response = await this.#fetch(url, { method: 'POST', - headers: { - ...notaryHeaders(service.token, plan.purpose, CLAIM_RESULT_FORMAT), - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ config: { purpose_override: purpose } }) }); - return { - plan, - url, - request, - status: response.status, - response: parseEvaluationResponse(response.body, plan) - }; - } - - async #fetchJson( - url: string, - init: RequestInit - ): Promise<{ status: number; body: Record }> { - const response = await this.#fetch(url, init); - const body: unknown = await response.json().catch(() => ({})); - return { status: response.status, body: recordValue(body) ?? {} }; - } - - #serviceFor(client: LiveClient): { url: string; token: string } { - const ref = this.#services[client]; - return { - url: requiredUrl(ref.url, ref.urlEnv), - token: requiredValue(ref.token, ref.tokenEnv) - }; + const envelope = asDict(await response.json().catch(() => ({}))); + const result = asDict(envelope.result) as RunnerResult; + if (!response.ok && result.response_source === undefined) { + return { response_source: { status: response.status, body: { code: 'scenario_runner_unavailable' } } }; + } + return result; } - #denied( - scenarioKey: string, + #blocked( + field: Field, scenario: (typeof SCENARIOS)[string], - ctx: EvaluateContext, code: string ): MockEvaluation { const seq = ++this.#seq; - const evaluationId = makeEvaluationId(seq); - const subject = - scenarioKey === 'denial' - ? PERSONA.karim - : ctx.delegatedTarget ?? ctx.selectedSubject ?? ctx.subject; - const rawRequest = - scenario.service === 'childBenefit' - ? buildChildBenefitRequest(scenario, subject) - : buildRawRequest(scenario, subject, { - actorIdHash: hashActor(ctx.subject), - delegationRef: 'rnref:v1:REL-1001-MOTHER' - }); - const body: DenialBody = { - error: code, - error_description: scenario.denial?.message ?? 'requester is not authorized for this target' - }; + const body = { error: code, error_description: 'The portal stopped this request before source access.' }; return { result: { state: 'error', - display: - code === 'relationship_not_proven' - ? 'Denied: guardian link not proven, no dependent record was read' - : scenario.display, + display: code === 'relationship_not_proven' ? 'Denied: guardian link not proven' : scenario.display, reasonCode: code, traceId: `event ${seq}` }, raw: { request: { method: 'POST', - url: 'solmara://citizen-portal/blocked-before-authority-call', - body: rawRequest + url: 'solmara://citizen-portal/blocked-before-evidence', + body: { field: field.id, purpose: scenario.purpose, disclosure: scenario.disclosure } }, response: { status: 403, body } - }, + } as unknown as MockEvaluation['raw'], proof: { - headline: - code === 'relationship_not_proven' - ? `Portal denied the request before calling ${authorityLabel(scenario)}: the guardian link was not proven, so no dependent record was read` - : 'Portal denied the cross-person request before any authority call', - answered: `Portal authorization gate stopped the request before any authority call: 403 ${code}`, + headline: 'Portal authorization stopped the request before Registry Evidence was called', + answered: `Portal authorization gate returned 403 ${code}`, notDisclosed: scenario.notDisclosed, status: 'denied', authority: undefined, crypto: { - signedBy: 'Portal authorization gate; no authority Notary called', - algorithm: 'No signature; request stopped before source access', + signedBy: 'Portal authorization gate; Registry Evidence was not called', + algorithm: 'No evidence assertion was produced', issuerKey: 'Not applicable', - holderBound: 'Portal session actor and server-selected subject', - credential: 'No credential or evidence result returned', - auditId: `denial:${evaluationId}` + holderBound: 'Portal session and server-selected subject', + credential: 'No credential or evidence assertion returned', + auditId: `denial:event-${seq}` } }, timing: { latencyMs: 0, staggerOrder: scenario.staggerOrder, slow: false } @@ -345,515 +160,96 @@ export class LiveEvidenceProvider implements DetailedEvidenceProvider { } } -function evaluationOutcome( - scenarioKey: string, - scenario: (typeof SCENARIOS)[string], - results: ClaimResultView[] -): EvaluationOutcome { - if (scenarioKey === 'disability-determination') { - return derivedOutcome( - scenario.claimId, - results, - ['person-is-deceased', 'pension-payment-active'] - ); - } +function runnerCalls(scenarioKey: string, service: string): RunnerCall[] { + if (service === 'childBenefit') return [{ scenarioId: 'birth-to-child-benefit', stepId: 'positive' }]; + if (service === 'nagdi') return [{ scenarioId: 'farmer-climate-smart-voucher', stepId: 'positive' }]; + if (service === 'citizen') return [{ scenarioId: 'citizen-self-service', stepId: 'positive' }]; if (scenarioKey === 'combined-support-eligibility') { - return derivedOutcome( - scenario.claimId, - results, - ['person-is-deceased', 'pension-payment-active', 'survivor-is-eligible'] - ); + return [ + { scenarioId: 'death-to-pension-survivor', stepId: 'stop-payment' }, + { scenarioId: 'death-to-pension-survivor', stepId: 'survivor-benefit' } + ]; } - if (scenarioKey === 'citizen-record-status') { - return derivedOutcome( - 'citizen-self-service-ready', - results, - ['civil-record-linked', 'citizen-population-record-active'] - ); + if (scenarioKey === 'functioning-assessment') { + return [{ scenarioId: 'death-to-pension-survivor', stepId: 'survivor-benefit' }]; } - const claimId = - scenarioKey === 'person-is-alive' || scenarioKey === 'ambiguous' - ? 'person-is-deceased' - : scenarioKey === 'functioning-assessment' || - scenarioKey === 'stale' - ? 'survivor-is-eligible' - : scenario.claimId; - const result = results.find((candidate) => candidate.claim_id === claimId); - const stale = result !== undefined && isExpired(result.expires_at); - return { - hasResult: result !== undefined, - satisfied: stale ? null : (result?.satisfied ?? null), - value: stale ? undefined : result?.value, - stale, - issuedAt: result?.issued_at - }; -} - -function derivedOutcome( - decisionId: string, - results: ClaimResultView[], - claims: string[], - trueValue: unknown = true -): EvaluationOutcome { - const sourceResults = claims.map((claimId) => - results.find((result) => result.claim_id === claimId) - ); - const hasResult = sourceResults.every((result) => result !== undefined); - const stale = hasResult && sourceResults.some((result) => result && isExpired(result.expires_at)); - const satisfied = hasResult && !stale - ? sourceResults.every((result) => result?.satisfied === true) - : null; - return { - hasResult, - satisfied, - value: satisfied === true ? trueValue : satisfied, - stale, - issuedAt: oldestIssuedAt(sourceResults), - derivedDecisions: { [decisionId]: satisfied } - }; -} - -function oldestIssuedAt(results: Array): string | undefined { - return results - .flatMap((result) => (result ? [result.issued_at] : [])) - .sort((left, right) => Date.parse(left) - Date.parse(right))[0]; -} - -function isExpired(expiresAt: string | null, now = Date.now()): boolean { - return expiresAt !== null && Date.parse(expiresAt) <= now; -} - -function liveRawTrace( - evaluations: AuthorityEvaluation[], - derivedDecisions?: Record -): MockEvaluation['raw'] { - const first = evaluations[0]; - if (evaluations.length === 1 && first) { - return { - request: { method: 'POST', url: first.url, body: first.request }, - response: { status: first.status, body: first.response } - }; + if (scenarioKey === 'stale' || scenarioKey === 'ambiguous') { + return [{ scenarioId: 'death-to-pension-survivor', stepId: 'stale-control' }]; } - const purposes = [...new Set(evaluations.map((evaluation) => evaluation.plan.purpose))]; - const request: RawApplicationRequest = { - purpose: - purposes.length === 1 - ? (purposes[0] ?? '') - : 'application-composed-from-source-authorized-purposes', - disclosure: 'decision', - composition: 'application', - requests: evaluations.map((evaluation) => ({ - authority: evaluation.plan.authority, - service_id: evaluation.plan.serviceId, - body: evaluation.request - })) - }; - const response: RawApplicationResponse = { - schema_version: 'solmara-portal-evidence/v1', - orchestration: { - service_id: 'citizen-portal', - decision: 'application_composed' - }, - results: evaluations.flatMap((evaluation) => evaluation.response.results), - source_trace: evaluations.map((evaluation) => ({ - authority: evaluation.plan.authority, - service_id: evaluation.plan.serviceId, - status: evaluation.status, - claims: [evaluation.plan.claimId] - })), - ...(derivedDecisions ? { derived_decisions: derivedDecisions } : {}) - }; - const failed = evaluations.find( - (evaluation) => evaluation.status < 200 || evaluation.status >= 300 - ); - return { - request: { - method: 'MULTI', - url: 'solmara://citizen-portal/application-composition', - body: request - }, - response: { status: failed?.status ?? 200, body: response } - }; + return [{ scenarioId: 'death-to-pension-survivor', stepId: 'stop-payment' }]; } -function portalResult( - scenario: (typeof SCENARIOS)[string], - status: ProofTrace['status'], - outcome: EvaluationOutcome, - seq: number -): ClaimResult { - const applicationOwned = outcome.derivedDecisions !== undefined; - return { - state: - outcome.stale - ? 'stale' - : status === 'false' - ? 'false' - : status === 'denied' || status === 'error' - ? 'error' - : scenario.state, - display: displayResult(scenario, outcome.value, outcome.satisfied, status, outcome.stale), - ...(!applicationOwned ? { authority: scenario.notary } : {}), - ...(outcome.issuedAt ? { asOf: outcome.issuedAt } : {}), - traceId: `event ${seq}` - }; +function responseResults(response: RunnerResult): Dict[] { + const body = asDict(response.response_source?.body); + return Array.isArray(body.results) ? body.results.map(asDict).filter((item) => Object.keys(item).length > 0) : []; } -function liveCrypto( - _scenarioKey: string, - evaluations: AuthorityEvaluation[] -): NonNullable { - const evaluationIds = evaluations.flatMap((evaluation) => - evaluation.response.results.map((result) => result.evaluation_id) - ); - const auditId = - evaluationIds.length > 0 - ? evaluationIds.map((evaluationId) => `evaluation:${evaluationId}`).join('; ') - : 'No valid evaluation identifier returned'; - const answeringAuthorities = [ - ...new Set( - evaluations - .filter((evaluation) => evaluation.response.results.length > 0) - .map((evaluation) => evaluation.plan.authority) - ) - ]; - if (evaluations.length > 1) { - return { - signedBy: - answeringAuthorities.length > 0 - ? `No credential issued; ${answeringAuthorities.join(' and ')} returned separate claim evaluations` - : 'No valid claim result returned; the portal did not compose a decision', - algorithm: 'Independent Registry Notary claim-result responses', - issuerKey: 'Not applicable for claim-result evaluation', - holderBound: 'Not credential-bound; the BFF selected the purpose and subject', - credential: 'Application decision only; no credential issued by the portal', - auditId - }; - } - return { - signedBy: - answeringAuthorities.length === 1 - ? `No credential issued; ${answeringAuthorities[0]} returned a claim evaluation` - : 'No valid claim result returned', - algorithm: 'Source-owned Registry Notary claim-result responses', - issuerKey: 'Not applicable for claim-result evaluation', - holderBound: 'Not credential-bound; the BFF selected the purpose and subject', - credential: 'Claim results only; no credential issued by the portal', - auditId - }; +function signedEvidence(response: RunnerResult): unknown[] { + const body = asDict(response.response_source?.body); + const signed = body.signed_evidence; + if (Array.isArray(signed)) return signed; + return signed && typeof signed === 'object' ? [signed] : []; } -function resolveSubject( +function outcomeFor( scenarioKey: string, - scenario: (typeof SCENARIOS)[string], - ctx: EvaluateContext -): string { - if (scenarioKey === 'denial') return PERSONA.karim; - if (scenario.delegated || scenarioKey === 'caregiver-link') { - return requiredValue(ctx.delegatedTarget, 'delegatedTarget'); - } - return ctx.selectedSubject ?? ctx.subject; -} - -function requiredUrl(value: string | undefined, name: string): string { - const raw = requiredValue(value, name); - try { - return new URL(raw).toString().replace(/\/$/, ''); - } catch { - throw new Error(`${name} must be an absolute URL`); - } -} - -function requiredValue(value: string | undefined, name: string): string { - if (!value) throw new Error(`${name} is required for PORTAL_PROVIDER=live`); - return value; -} - -function joinedUrl(base: string, path: string): string { - return `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`; -} - -function notaryHeaders(token: string, purpose: string, accept: string): Record { - return { - 'x-api-key': token, - Accept: accept, - 'Data-Purpose': purpose - }; -} - -function parseEvaluationResponse( - body: Record, - expected: AuthorityPlan -): RawEvaluationResponse { - if (!Array.isArray(body.results) || body.results.length !== 1) return { results: [] }; - const result = parseClaimResult(body.results[0]); - if ( - !result || - result.claim_id !== expected.claimId || - result.provenance.generated_by.service_id !== expected.serviceId - ) { - return { results: [] }; - } - return { results: [result] }; -} - -function parseClaimResult(value: unknown): ClaimResultView | undefined { - const item = recordValue(value); - if (!item) return undefined; - const targetRef = parseTargetRef(item.target_ref); - const provenance = parseProvenance(item.provenance); - const expiresAt = item.expires_at; - if ( - !nonEmptyString(item.evaluation_id) || - !nonEmptyString(item.claim_id) || - !nonEmptyString(item.claim_version) || - !nonEmptyString(item.subject_type) || - !targetRef || - !('value' in item) || - (typeof item.satisfied !== 'boolean' && item.satisfied !== null) || - item.disclosure !== 'predicate' || - item.format !== CLAIM_RESULT_FORMAT || - !dateTimeString(item.issued_at) || - (expiresAt !== null && !dateTimeString(expiresAt)) || - !validLifetime(item.issued_at, expiresAt) || - !provenance - ) { - return undefined; - } - if ( - provenance.generated_by.evaluation_id !== item.evaluation_id || - provenance.generated_by.claim_id !== item.claim_id || - provenance.generated_by.claim_version !== item.claim_version - ) { - return undefined; - } - return { - evaluation_id: item.evaluation_id, - claim_id: item.claim_id, - claim_version: item.claim_version, - subject_type: item.subject_type, - target_ref: targetRef, - value: item.value, - satisfied: item.satisfied, - disclosure: item.disclosure, - format: item.format, - issued_at: item.issued_at, - expires_at: expiresAt, - provenance - }; -} - -function parseTargetRef(value: unknown): ClaimResultView['target_ref'] | undefined { - const target = recordValue(value); - if (!target || !nonEmptyString(target.handle)) return undefined; - if ( - ('identifier_schemes' in target && - (!Array.isArray(target.identifier_schemes) || - !target.identifier_schemes.every(nonEmptyString))) || - ('profile' in target && !nonEmptyString(target.profile)) || - ('type' in target && !nonEmptyString(target.type)) - ) { - return undefined; + claimId: string, + results: Dict[], + responses: RunnerResult[] +): { found: boolean; satisfied: boolean | null; value: unknown; derivedDecisions?: Record } { + if (scenarioKey === 'disability-determination') { + const value = responses[0]?.derived_decisions?.['pension-payment-should-stop']; + return { found: typeof value === 'boolean', satisfied: value ?? null, value, derivedDecisions: { 'pension-payment-should-stop': value ?? null } }; } - return { - handle: target.handle, - ...(Array.isArray(target.identifier_schemes) - ? { identifier_schemes: target.identifier_schemes } - : {}), - ...(typeof target.profile === 'string' ? { profile: target.profile } : {}), - ...(typeof target.type === 'string' ? { type: target.type } : {}) - }; -} - -function parseProvenance(value: unknown): ClaimResultView['provenance'] | undefined { - const provenance = recordValue(value); - const generatedBy = recordValue(provenance?.generated_by); - const used = recordValue(provenance?.used); - if ( - provenance?.schema_version !== 'registry-notary-claim-provenance/v2' || - !generatedBy || - generatedBy.type !== 'claim_evaluation' || - !nonEmptyString(generatedBy.service_id) || - !nonEmptyString(generatedBy.evaluation_id) || - !nonEmptyString(generatedBy.claim_id) || - !nonEmptyString(generatedBy.claim_version) || - !validOptionalString(generatedBy, 'policy_hash') || - !validOptionalString(generatedBy, 'policy_id') || - !validOptionalString(generatedBy, 'policy_version') || - !used || - !Number.isInteger(used.relay_consultation_count) || - Number(used.relay_consultation_count) < 0 || - !Array.isArray(provenance.derived_from) || - !provenance.derived_from.every((entry) => recordValue(entry) !== undefined) - ) { - return undefined; + if (scenarioKey === 'combined-support-eligibility') { + const required = ['person-is-deceased', 'pension-payment-active', 'survivor-is-eligible']; + const values = required.map((id) => results.find((item) => item.claim_id === id)?.satisfied); + const found = values.every((value) => typeof value === 'boolean'); + const value = found ? values.every((item) => item === true) : null; + return { found, satisfied: value, value, derivedDecisions: { [claimId]: value } }; } + if (scenarioKey === 'citizen-record-status') { + const required = ['civil-record-linked', 'citizen-population-record-active']; + const values = required.map((id) => results.find((item) => item.claim_id === id)?.satisfied); + const found = values.every((value) => typeof value === 'boolean'); + const value = found ? values.every((item) => item === true) : null; + return { found, satisfied: value, value, derivedDecisions: { 'citizen-self-service-ready': value } }; + } + const effectiveClaim = scenarioKey === 'person-is-alive' || scenarioKey === 'ambiguous' + ? 'person-is-deceased' + : scenarioKey === 'stale' + ? 'survivor-is-eligible' + : claimId; + const item = results.find((candidate) => candidate.claim_id === effectiveClaim); + const value = item?.value ?? item?.satisfied; + return { found: item !== undefined, satisfied: typeof item?.satisfied === 'boolean' ? item.satisfied : null, value }; +} + +function evidenceProof(responses: RunnerResult[]): NonNullable { + const assertions = responses.flatMap(signedEvidence); return { - schema_version: 'registry-notary-claim-provenance/v2', - generated_by: { - type: 'claim_evaluation', - service_id: generatedBy.service_id, - evaluation_id: generatedBy.evaluation_id, - claim_id: generatedBy.claim_id, - claim_version: generatedBy.claim_version, - ...(typeof generatedBy.policy_hash === 'string' - ? { policy_hash: generatedBy.policy_hash } - : {}), - ...(typeof generatedBy.policy_id === 'string' ? { policy_id: generatedBy.policy_id } : {}), - ...(typeof generatedBy.policy_version === 'string' - ? { policy_version: generatedBy.policy_version } - : {}) - }, - used: { relay_consultation_count: Number(used.relay_consultation_count) }, - derived_from: provenance.derived_from + signedBy: assertions.length ? 'Registry Evidence signed the returned assertion set' : 'No signed assertion was returned', + algorithm: assertions.length ? 'Flattened JWS, EdDSA' : 'Not available', + issuerKey: '/.well-known/evidence/jwks.json', + holderBound: 'Mint requester identity, purpose, requirement, nonce, and selector', + credential: 'Registry Evidence assertion, not an application credential', + auditId: `${assertions.length} signed assertion${assertions.length === 1 ? '' : 's'}` }; } -function isChildBenefitResponse(body: Record): body is RawChildBenefitResponse { - const orchestration = recordValue(body.orchestration); - const target = recordValue(body.target); - return ( - body.schema_version === 'solmara-child-benefit-evidence/v1' && - nonEmptyString(body.evidence_set_id) && - orchestration?.service_id === 'child-benefit-federator' && - orchestration.decision === 'not_composed' && - typeof body.purpose === 'string' && - target !== undefined && - !('identifiers' in target) && - Array.isArray(target.identifier_schemes) && - target.identifier_schemes.every((scheme) => typeof scheme === 'string') && - Array.isArray(body.results) && - body.results.every((item) => { - const result = recordValue(item); - return ( - result !== undefined && - nonEmptyString(result.claim_id) && - nonEmptyString(result.claim_version) && - typeof result.satisfied === 'boolean' && - result.disclosure === 'predicate' && - result.format === CLAIM_RESULT_FORMAT && - dateTimeString(result.issued_at) && - (dateTimeString(result.expires_at) || result.expires_at === null) && - validLifetime(result.issued_at, result.expires_at) && - nonEmptyString(result.authority) && - nonEmptyString(result.notary_service_id) - ); - }) && - Array.isArray(body.source_trace) - ); -} - -function hasChildAttribution( - body: RawChildBenefitResponse, - scenario: (typeof SCENARIOS)[string] -): boolean { - const claimId = scenario.claimId; - const expectedServiceId = NOTARY_SERVICE_ID[scenario.notary]; - const expectedAuthority = AUTHORITY_LABEL[scenario.notary]; - const result = body.results.find((candidate) => candidate.claim_id === claimId); - if (!result) return false; - if ( - result.notary_service_id !== expectedServiceId || - result.authority !== expectedAuthority - ) { - return false; - } - return body.source_trace.some((item) => { - const trace = recordValue(item); - return ( - trace?.service_id === expectedServiceId && - trace.authority === expectedAuthority && - Array.isArray(trace.claims) && - trace.claims.includes(claimId) - ); - }); -} - -function recordValue(value: unknown): Record | undefined { - if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined; - // TypeScript cannot infer the string-keyed record after the runtime object checks. - return value as Record; -} - -function nonEmptyString(value: unknown): value is string { - return typeof value === 'string' && value.trim().length > 0; -} - -function dateTimeString(value: unknown): value is string { - return ( - nonEmptyString(value) && - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && - !Number.isNaN(Date.parse(value)) - ); -} - -function validOptionalString(record: Record, key: string): boolean { - return !(key in record) || nonEmptyString(record[key]); -} - -function validLifetime(issuedAt: unknown, expiresAt: unknown): boolean { - if (!dateTimeString(issuedAt)) return false; - if (expiresAt === null) return true; - return dateTimeString(expiresAt) && Date.parse(expiresAt) >= Date.parse(issuedAt); -} - -function proofStatus( - status: number, - satisfied: unknown, - hasResult: boolean -): ProofTrace['status'] { - if (status === 403) return 'denied'; - if (status < 200 || status >= 300 || !hasResult) return 'error'; - if (satisfied === false) return 'false'; - return satisfied === true ? 'ok' : 'error'; -} - -function displayResult( - scenario: (typeof SCENARIOS)[string], - value: unknown, - satisfied: unknown, - status: ProofTrace['status'], - stale: boolean -): string { - if (stale) { - return scenario.state === 'stale' - ? scenario.display - : `${scenario.claimId}: expired evidence, refresh required`; - } - if (status === 'ok' && satisfied === true) return scenario.display; - return `${scenario.claimId}: ${displayValue(value, satisfied)}`; -} - -function displayValue(value: unknown, satisfied: unknown): string { - if (typeof satisfied === 'boolean') return String(satisfied); - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return String(value); +function requiredRunnerUrl(value: string | undefined): string { + if (!value) throw new Error('SCENARIO_RUNNER_URL is required for PORTAL_PROVIDER=live'); + try { + return new URL(value).toString().replace(/\/$/, ''); + } catch { + throw new Error('SCENARIO_RUNNER_URL must be an absolute URL'); } - if (value === null || value === undefined) return 'not returned'; - return JSON.stringify(value); -} - -function answeredByAuthorities(evaluations: AuthorityEvaluation[]): string { - return evaluations - .map((evaluation) => { - const result = evaluation.response.results.find( - (candidate) => candidate.claim_id === evaluation.plan.claimId - ); - return `${evaluation.plan.authority} answered: ${evaluation.plan.claimId} = ${displayValue(result?.value, result?.satisfied)}`; - }) - .join('; '); } -function childAnswered(body: RawChildBenefitResponse | undefined, claimId: string): string { - const result = body?.results.find((candidate) => candidate.claim_id === claimId); - if (!result) return `Child benefit application returned no valid ${claimId} result`; - return `${result.authority} answered: ${claimId} = ${result.satisfied}`; +function asDict(value: unknown): Dict { + return value && typeof value === 'object' && !Array.isArray(value) ? value as Dict : {}; } -function hashActor(subject: string): string { - let hash = 0; - for (let i = 0; i < subject.length; i += 1) { - hash = (hash * 31 + subject.charCodeAt(i)) & 0xffffffff; - } - return `hmac-sha256:${(hash >>> 0).toString(16).padStart(8, '0')}`; +function isSuccess(status: number | null | undefined): boolean { + return typeof status === 'number' && status >= 200 && status < 300; } diff --git a/portal/src/lib/server/provider.test.ts b/portal/src/lib/server/provider.test.ts index fa718b5..6da6bca 100644 --- a/portal/src/lib/server/provider.test.ts +++ b/portal/src/lib/server/provider.test.ts @@ -18,16 +18,13 @@ describe('getProvider', () => { it('constructs live mode from environment config', () => { Object.assign(env, { PORTAL_PROVIDER: 'live', - CRA_NOTARY_URL: 'https://cra-notary.solmara.registrystack.org', - CRA_CITIZEN_CLIENT_TOKEN: 'cra-citizen-token', - NIA_NOTARY_URL: 'https://nia-notary.solmara.registrystack.org', - NIA_CITIZEN_CLIENT_TOKEN: 'nia-citizen-token' + SCENARIO_RUNNER_URL: 'http://scenario-runner:8080' }); expect(getProvider()).not.toBeInstanceOf(MockEvidenceProvider); }); - it('does not require obsolete Relay or generic Notary configuration', () => { + it('constructs lazily without requiring credentials in the portal process', () => { Object.assign(env, { PORTAL_PROVIDER: 'live' }); diff --git a/projects/cra-civil/environments/hosted.yaml b/projects/cra-civil/environments/hosted.yaml index f5fdebd..788a555 100644 --- a/projects/cra-civil/environments/hosted.yaml +++ b/projects/cra-civil/environments/hosted.yaml @@ -12,33 +12,14 @@ entities: deceased: deceased source_revision: cra-civil-projection-v1 generation: 2026-07-14 -callers: - child-benefit-federator: - api_key_fingerprint: { secret: CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH } - scopes: ["evidence:cra:child-benefit:read"] - pension-review-desk: - api_key_fingerprint: { secret: CRA_PENSION_CLIENT_TOKEN_HASH } - scopes: ["evidence:cra:pension:read"] - citizen-portal: - api_key_fingerprint: { secret: CRA_CITIZEN_CLIENT_TOKEN_HASH } - scopes: ["evidence:cra:citizen:read"] relay: origin: https://cra-relay.solmara.registrystack.org issuer: https://workload-issuer.solmara.registrystack.org jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json audience: registry-relay - allowed_clients: [] -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: cra-notary - token_file: /run/secrets/cra-notary-relay-token -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt + allowed_clients: [solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } deployment: profile: hosted_lab relay: { service: cra-civil-relay } - notary: { service: cra-notary } diff --git a/projects/cra-civil/environments/local.yaml b/projects/cra-civil/environments/local.yaml index 70299bb..91d8096 100644 --- a/projects/cra-civil/environments/local.yaml +++ b/projects/cra-civil/environments/local.yaml @@ -12,38 +12,14 @@ entities: deceased: deceased source_revision: cra-civil-projection-v1 generation: 2026-07-14 -callers: - child-benefit-federator: - api_key_fingerprint: { secret: CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH } - scopes: ["evidence:cra:child-benefit:read"] - pension-review-desk: - api_key_fingerprint: { secret: CRA_PENSION_CLIENT_TOKEN_HASH } - scopes: ["evidence:cra:pension:read"] - citizen-portal: - api_key_fingerprint: { secret: CRA_CITIZEN_CLIENT_TOKEN_HASH } - scopes: ["evidence:cra:citizen:read"] relay: origin: http://127.0.0.1:8080 issuer: http://127.0.0.1:8090 jwks_url: http://127.0.0.1:8090/.well-known/jwks.json audience: registry-relay - allowed_clients: [] -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: cra-notary - token_file: /run/secrets/cra-notary-relay-token -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - -# Emulated local images require Registry Stack's bounded 1 GiB CEL worker ceiling. -# Hosted deployments intentionally retain the 128 MiB default. -notary_cel: - worker_memory_bytes: 1073741824 + allowed_clients: [solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } deployment: profile: local relay: { service: cra-civil-relay } - notary: { service: cra-notary } diff --git a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/future-birth-date.yaml b/projects/cra-civil/integrations/civil-person-by-uin/fixtures/future-birth-date.yaml index a114369..a3e6d95 100644 --- a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/future-birth-date.yaml +++ b/projects/cra-civil/integrations/civil-person-by-uin/fixtures/future-birth-date.yaml @@ -1,14 +1,5 @@ name: cra-civil-person-future-birth-date classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: solmara_uin, value: "2300010248" }] - variables: { as_of_date: 2026-07-14 } - claims: [birth-is-registered, child-age-under-5] - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review input: { uin: "2300010248" } variables: { as_of_date: 2026-07-14 } interactions: @@ -25,8 +16,3 @@ expect: birth_date: 2027-03-14 birth_brn: BRN-2027-0101-00001 deceased: false - claims: - birth-is-registered: true - child-age-under-5: false - person-is-deceased: false - civil-record-linked: true diff --git a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/match.yaml b/projects/cra-civil/integrations/civil-person-by-uin/fixtures/match.yaml index 9576003..3793df9 100644 --- a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/match.yaml +++ b/projects/cra-civil/integrations/civil-person-by-uin/fixtures/match.yaml @@ -1,13 +1,5 @@ name: cra-civil-person-match classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: solmara_uin, value: "2300010248" }] - claims: [civil-record-linked] - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/citizen-self-service input: { uin: "2300010248" } variables: { as_of_date: 2026-07-14 } interactions: @@ -24,8 +16,3 @@ expect: birth_date: 2022-03-14 birth_brn: BRN-2022-0101-00001 deceased: false - claims: - birth-is-registered: true - child-age-under-5: true - person-is-deceased: false - civil-record-linked: true diff --git a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/no-match.yaml b/projects/cra-civil/integrations/civil-person-by-uin/fixtures/no-match.yaml index 364dfc2..0870844 100644 --- a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/no-match.yaml +++ b/projects/cra-civil/integrations/civil-person-by-uin/fixtures/no-match.yaml @@ -1,13 +1,5 @@ name: cra-civil-person-no-match classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: solmara_uin, value: "9999999999" }] - claims: [person-is-deceased] - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review input: { uin: "9999999999" } variables: { as_of_date: 2026-07-14 } interactions: @@ -16,8 +8,3 @@ interactions: expect: outcome: no_match outputs: {} - claims: - birth-is-registered: false - child-age-under-5: false - person-is-deceased: false - civil-record-linked: false diff --git a/projects/cra-civil/registry-stack.yaml b/projects/cra-civil/registry-stack.yaml index db54e87..ee617d8 100644 --- a/projects/cra-civil/registry-stack.yaml +++ b/projects/cra-civil/registry-stack.yaml @@ -12,67 +12,39 @@ entities: file: entities/civil_people.yaml services: - cra-child-benefit: - kind: evidence + cra-civil-records: + kind: records_api + entity: civil_people + title: CRA civil records + description: Purpose-limited civil facts used by the Solmara Evidence service. + owner: Civil Registration Authority + sensitivity: personal + access_rights: restricted + update_frequency: continuous + api: + scopes: + metadata: civil_people:metadata + rows: civil_people:rows + aggregate: civil_people:aggregate + evidence_verification: civil_people:evidence_verification + purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + - https://id.registrystack.org/solmara/purpose/pension-payment-review + - https://id.registrystack.org/solmara/purpose/citizen-self-service + projection: [uin, birth_date, birth_brn, deceased] + pagination: { default_limit: 2, max_limit: 2 } + filters: + uin: [eq] + standards: { ogc_features: false, sp_dci: false } + + cra-civil-consultation: + kind: consultation_api version: 1 purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review legal_basis: child-benefit-act consent: not_required - access: - scopes: ["evidence:cra:child-benefit:read"] - variables: - as_of_date: - from: request.variables.as_of_date - type: date - consultations: - civil: - integration: civil-person-by-uin - input: - uin: request.target.identifiers.solmara_uin - claims: - birth-is-registered: - cel: "civil.matched && civil.birth_brn != null ? civil.birth_brn != '' : false" - disclosure: predicate - child-age-under-5: - cel: >- - civil.matched && civil.birth_date != null && civil.deceased == false - ? date.age_on(civil.birth_date, as_of_date) >= 0 - && date.age_on(civil.birth_date, as_of_date) < 5 - : false - disclosure: predicate - - cra-pension-death: - kind: evidence - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review - legal_basis: social-insurance-act - consent: not_required - access: - scopes: ["evidence:cra:pension:read"] - consultations: - civil: - integration: civil-person-by-uin - input: - uin: request.target.identifiers.solmara_uin - claims: - person-is-deceased: - cel: "civil.matched && civil.deceased == true" - disclosure: predicate - - cra-citizen-record: - kind: evidence - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/citizen-self-service - legal_basis: citizen-self-service - consent: not_required - access: - scopes: ["evidence:cra:citizen:read"] consultations: civil: integration: civil-person-by-uin input: uin: request.target.identifiers.solmara_uin - claims: - civil-record-linked: - cel: "civil.matched" - disclosure: predicate diff --git a/projects/mosd-programme/environments/hosted.yaml b/projects/mosd-programme/environments/hosted.yaml index 26fb468..f74db15 100644 --- a/projects/mosd-programme/environments/hosted.yaml +++ b/projects/mosd-programme/environments/hosted.yaml @@ -12,31 +12,15 @@ entities: source_revision: solmara-mosd-programme-projection-v1 generation: "2026-07-14" -callers: - child-benefit-federator: - api_key_fingerprint: { secret: PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH } - scopes: ["mosd_programme:evidence_verification"] - relay: origin: https://mosd-programme-relay.solmara.registrystack.org issuer: https://workload-issuer.solmara.registrystack.org jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json audience: registry-relay - allowed_clients: [programme-notary] - -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: programme-notary - token_file: /run/secrets/programme-notary-relay-token - -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt + allowed_clients: [solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt deployment: profile: hosted_lab relay: { service: programme-mis-relay } - notary: { service: programme-notary } diff --git a/projects/mosd-programme/environments/local.yaml b/projects/mosd-programme/environments/local.yaml index 4e3aca0..41ae0d7 100644 --- a/projects/mosd-programme/environments/local.yaml +++ b/projects/mosd-programme/environments/local.yaml @@ -12,37 +12,15 @@ entities: source_revision: solmara-mosd-programme-projection-v1 generation: "2026-07-14" -callers: - child-benefit-federator: - api_key_fingerprint: { secret: PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH } - scopes: ["mosd_programme:evidence_verification"] - relay: origin: http://127.0.0.1:8080 issuer: http://127.0.0.1:8090 jwks_url: http://127.0.0.1:8090/.well-known/jwks.json audience: registry-relay - allowed_clients: [programme-notary] - -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: programme-notary - token_file: /run/secrets/programme-notary-relay-token - -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - -# Emulated local images require Registry Stack's bounded 1 GiB CEL worker ceiling. -# Hosted deployments intentionally retain the 128 MiB default. -notary_cel: - worker_memory_bytes: 1073741824 + allowed_clients: [solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } deployment: profile: local relay: { service: programme-mis-relay } - notary: { service: programme-notary } diff --git a/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/match.yaml b/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/match.yaml index b063bc4..5230336 100644 --- a/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/match.yaml +++ b/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/match.yaml @@ -1,13 +1,5 @@ name: enrollment-match classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: solmara_uin, value: "2300010248" }] - claims: [not-already-enrolled] - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review input: { uin: "2300010248" } interactions: - expect: { method: GET, path: /snapshot } @@ -15,4 +7,3 @@ interactions: expect: outcome: match outputs: { duplicate_flag: false } - claims: { not-already-enrolled: true } diff --git a/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/no-match.yaml b/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/no-match.yaml index 990e5e0..20dd7ed 100644 --- a/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/no-match.yaml +++ b/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/no-match.yaml @@ -7,4 +7,3 @@ interactions: expect: outcome: no_match outputs: {} - claims: { not-already-enrolled: false } diff --git a/projects/mosd-programme/registry-stack.yaml b/projects/mosd-programme/registry-stack.yaml index 341ccb5..ba9075b 100644 --- a/projects/mosd-programme/registry-stack.yaml +++ b/projects/mosd-programme/registry-stack.yaml @@ -12,22 +12,37 @@ entities: file: entities/mosd_enroll.yaml services: - child-benefit: - kind: evidence + mosd-enrollment-records: + kind: records_api + entity: mosd_enroll + title: MOSD programme enrollment records + description: Purpose-limited enrollment facts used by the Solmara Evidence service. + owner: Ministry of Social Development + sensitivity: personal + access_rights: restricted + update_frequency: continuous + api: + scopes: + metadata: mosd_enroll:metadata + rows: mosd_enroll:rows + aggregate: mosd_enroll:aggregate + evidence_verification: mosd_enroll:evidence_verification + purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + projection: [uin, duplicate_flag] + pagination: { default_limit: 2, max_limit: 2 } + filters: + uin: [eq] + standards: { ogc_features: false, sp_dci: false } + + mosd-enrollment-consultation: + kind: consultation_api version: 1 purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review legal_basis: child-benefit-act consent: not_required - access: - scopes: ["mosd_programme:evidence_verification"] consultations: enrollment: integration: enrollment-by-uin input: uin: request.target.identifiers.solmara_uin - claims: - not-already-enrolled: - cel: enrollment.matched && enrollment.duplicate_flag == false - disclosure: - default: predicate - allowed: [predicate, redacted] diff --git a/projects/nagdi-agriculture/environments/hosted.yaml b/projects/nagdi-agriculture/environments/hosted.yaml index e7a65f1..c7a20c5 100644 --- a/projects/nagdi-agriculture/environments/hosted.yaml +++ b/projects/nagdi-agriculture/environments/hosted.yaml @@ -34,37 +34,15 @@ entities: source_revision: nagdi-livestock-movement-v1 generation: "2026-07-14" -issuance: - issuer: did:web:id.registrystack.org:solmara:authority:nagdi - signing_kid: did:web:id.registrystack.org:solmara:authority:nagdi#issuer-key-1 - signing_key: { secret: NAGDI_NOTARY_ISSUER_JWK } - generation: 1 - -callers: - nagdi-service: - api_key_fingerprint: { secret: NAGDI_CLIENT_TOKEN_HASH } - scopes: ["nagdi_agriculture:evidence_verification"] - relay: origin: https://nagdi-relay.solmara.registrystack.org issuer: https://workload-issuer.solmara.registrystack.org jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json audience: registry-relay - allowed_clients: [nagdi-notary] - -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: nagdi-notary - token_file: /run/secrets/nagdi-notary-relay-token - -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt + allowed_clients: [solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt deployment: profile: hosted_lab relay: { service: nagdi-agriculture-relay } - notary: { service: nagdi-notary } diff --git a/projects/nagdi-agriculture/environments/local.yaml b/projects/nagdi-agriculture/environments/local.yaml index 9ae1c1d..dc47f2a 100644 --- a/projects/nagdi-agriculture/environments/local.yaml +++ b/projects/nagdi-agriculture/environments/local.yaml @@ -34,43 +34,15 @@ entities: source_revision: nagdi-livestock-movement-v1 generation: "2026-07-14" -issuance: - issuer: did:web:id.registrystack.org:solmara:authority:nagdi - signing_kid: did:web:id.registrystack.org:solmara:authority:nagdi#issuer-key-1 - signing_key: { secret: NAGDI_NOTARY_ISSUER_JWK } - generation: 1 - -callers: - nagdi-service: - api_key_fingerprint: { secret: NAGDI_CLIENT_TOKEN_HASH } - scopes: ["nagdi_agriculture:evidence_verification"] - relay: origin: http://127.0.0.1:8080 issuer: http://127.0.0.1:8090 jwks_url: http://127.0.0.1:8090/.well-known/jwks.json audience: registry-relay - allowed_clients: [nagdi-notary] - -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: nagdi-notary - token_file: /run/secrets/nagdi-notary-relay-token - -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - -# Emulated local images require Registry Stack's bounded 1 GiB CEL worker ceiling. -# Hosted deployments intentionally retain the 128 MiB default. -notary_cel: - worker_memory_bytes: 1073741824 + allowed_clients: [solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } deployment: profile: local relay: { service: nagdi-agriculture-relay } - notary: { service: nagdi-notary } diff --git a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/match.yaml b/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/match.yaml index 4e1f809..0434561 100644 --- a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/match.yaml +++ b/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/match.yaml @@ -1,16 +1,5 @@ name: farmer-voucher-match classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: farmer_id, value: FR-1001 }] - claims: - - farmer-registered - - data-use-authorized-for-purpose - - eligible-for-climate-smart-input-voucher - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review input: { farmer_id: FR-1001 } interactions: - expect: { method: GET, path: /snapshot } @@ -36,7 +25,3 @@ expect: district_climate_risk_active: true voucher_entitlement_current: true voucher_not_redeemed: true - claims: - farmer-registered: true - data-use-authorized-for-purpose: true - eligible-for-climate-smart-input-voucher: true diff --git a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/no-match.yaml b/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/no-match.yaml index 9ba7e3b..2ae0097 100644 --- a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/no-match.yaml +++ b/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/no-match.yaml @@ -7,7 +7,3 @@ interactions: expect: outcome: no_match outputs: {} - claims: - farmer-registered: false - data-use-authorized-for-purpose: false - eligible-for-climate-smart-input-voucher: false diff --git a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/ambiguous.yaml b/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/ambiguous.yaml index bdf033c..56c9694 100644 --- a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/ambiguous.yaml +++ b/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/ambiguous.yaml @@ -16,4 +16,4 @@ interactions: origin_district_not_quarantined_for_species: false destination_district_open: false no_conflicting_open_movement_permit: false -expect: { outcome: ambiguous, outputs: {}, claims: {} } +expect: { outcome: ambiguous, outputs: {} } diff --git a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/match.yaml b/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/match.yaml index 18c87e1..e4d51b0 100644 --- a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/match.yaml +++ b/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/match.yaml @@ -1,16 +1,5 @@ name: livestock-movement-match classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: farmer_id, value: FR-1001 }] - claims: - - registered-herd - - origin-district-not-quarantined-for-species - - eligible-for-livestock-movement-permit - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control input: { farmer_id: FR-1001 } interactions: - expect: { method: GET, path: /snapshot } @@ -30,7 +19,3 @@ expect: origin_district_not_quarantined_for_species: true destination_district_open: true no_conflicting_open_movement_permit: true - claims: - registered-herd: true - origin-district-not-quarantined-for-species: true - eligible-for-livestock-movement-permit: true diff --git a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/no-match.yaml b/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/no-match.yaml index 10d7312..a55e732 100644 --- a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/no-match.yaml +++ b/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/no-match.yaml @@ -7,7 +7,3 @@ interactions: expect: outcome: no_match outputs: {} - claims: - registered-herd: false - origin-district-not-quarantined-for-species: false - eligible-for-livestock-movement-permit: false diff --git a/projects/nagdi-agriculture/registry-stack.yaml b/projects/nagdi-agriculture/registry-stack.yaml index 0333d7b..545691a 100644 --- a/projects/nagdi-agriculture/registry-stack.yaml +++ b/projects/nagdi-agriculture/registry-stack.yaml @@ -16,93 +16,88 @@ entities: file: entities/nagdi_herd.yaml services: - voucher: - kind: evidence + nagdi-farmer-records: + kind: records_api + entity: nagdi_farmer + title: NAgDI farmer records + description: Purpose-limited farmer facts used by the Solmara Evidence service. + owner: National Agriculture Data and Digital Innovation Agency + sensitivity: personal + access_rights: restricted + update_frequency: continuous + api: + scopes: + metadata: nagdi_farmer:metadata + rows: nagdi_farmer:rows + aggregate: nagdi_farmer:aggregate + evidence_verification: nagdi_farmer:evidence_verification + purposes: + - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review + projection: + - farmer_id + - farmer_registered + - data_use_authorized + - active_smallholder_farmer + - active_farm_parcel + - crop_declared_for_season + - district_climate_risk_active + - voucher_entitlement_current + - voucher_not_redeemed + pagination: { default_limit: 2, max_limit: 2 } + filters: + farmer_id: [eq] + standards: { ogc_features: false, sp_dci: false } + + nagdi-farmer-consultation: + kind: consultation_api version: 1 purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review legal_basis: agricultural-data-act consent: not_required - access: - scopes: ["nagdi_agriculture:evidence_verification"] consultations: farmer: integration: farmer-voucher-by-farmer input: farmer_id: request.target.identifiers.farmer_id - claims: - farmer-registered: - cel: farmer.matched && farmer.farmer_registered - disclosure: - default: predicate - allowed: [predicate, redacted] - data-use-authorized-for-purpose: - cel: farmer.matched && farmer.data_use_authorized - disclosure: - default: predicate - allowed: [predicate, redacted] - eligible-for-climate-smart-input-voucher: - cel: >- - farmer.matched && farmer.farmer_registered - && farmer.data_use_authorized - && farmer.active_smallholder_farmer - && farmer.active_farm_parcel - && farmer.crop_declared_for_season - && farmer.district_climate_risk_active - && farmer.voucher_entitlement_current - && farmer.voucher_not_redeemed - disclosure: - default: predicate - allowed: [predicate, redacted] - credential_profiles: - climate_smart_voucher_sd_jwt: - format: dc+sd-jwt - type: https://id.registrystack.org/solmara/vct/climate-smart-voucher-eligibility - validity: 10m - claims: - - farmer-registered - - data-use-authorized-for-purpose - - eligible-for-climate-smart-input-voucher - livestock: - kind: evidence + nagdi-herd-consultation: + kind: consultation_api version: 1 purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control legal_basis: agricultural-data-act consent: not_required - access: - scopes: ["nagdi_agriculture:evidence_verification"] consultations: herd: integration: livestock-movement-by-farmer input: farmer_id: request.target.identifiers.farmer_id - claims: - registered-herd: - cel: herd.matched && herd.registered_herd - disclosure: - default: predicate - allowed: [predicate, redacted] - origin-district-not-quarantined-for-species: - cel: herd.matched && herd.origin_district_not_quarantined_for_species - disclosure: - default: predicate - allowed: [predicate, redacted] - eligible-for-livestock-movement-permit: - cel: >- - herd.matched && herd.registered_herd - && herd.origin_district_not_quarantined_for_species - && herd.herd_vaccination_current - && herd.destination_district_open - && herd.no_conflicting_open_movement_permit - disclosure: - default: predicate - allowed: [predicate, redacted] - credential_profiles: - livestock_movement_sd_jwt: - format: dc+sd-jwt - type: https://id.registrystack.org/solmara/vct/livestock-movement-permit - validity: 10m - claims: - - registered-herd - - origin-district-not-quarantined-for-species - - eligible-for-livestock-movement-permit + + nagdi-herd-records: + kind: records_api + entity: nagdi_herd + title: NAgDI herd records + description: Purpose-limited herd facts used by the Solmara Evidence service. + owner: National Agriculture Data and Digital Innovation Agency + sensitivity: personal + access_rights: restricted + update_frequency: continuous + api: + scopes: + metadata: nagdi_herd:metadata + rows: nagdi_herd:rows + aggregate: nagdi_herd:aggregate + evidence_verification: nagdi_herd:evidence_verification + purposes: + - https://id.registrystack.org/solmara/purpose/livestock-movement-control + projection: + - herd_id + - farmer_id + - registered_herd + - herd_vaccination_current + - origin_district_not_quarantined_for_species + - destination_district_open + - no_conflicting_open_movement_permit + pagination: { default_limit: 2, max_limit: 2 } + filters: + farmer_id: [eq] + standards: { ogc_features: false, sp_dci: false } diff --git a/projects/nia-population/environments/hosted.yaml b/projects/nia-population/environments/hosted.yaml index d542939..f7ad455 100644 --- a/projects/nia-population/environments/hosted.yaml +++ b/projects/nia-population/environments/hosted.yaml @@ -17,35 +17,14 @@ entities: alive: alive source_revision: nia-population-person-v1 generation: 2026-07-14 -issuance: - issuer: did:web:id.registrystack.org:solmara:authority:nia - signing_kid: did:web:id.registrystack.org:solmara:authority:nia#issuer-key-1 - signing_key: { secret: NIA_NOTARY_ISSUER_JWK } - generation: 1 -callers: - child-benefit-federator: - api_key_fingerprint: { secret: NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH } - scopes: ["evidence:nia:child-benefit:read"] - citizen-portal: - api_key_fingerprint: { secret: NIA_CITIZEN_CLIENT_TOKEN_HASH } - scopes: ["evidence:nia:citizen:read"] relay: origin: https://nia-relay.solmara.registrystack.org issuer: https://workload-issuer.solmara.registrystack.org jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json audience: registry-relay - allowed_clients: [solmara-esignet] -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: nia-notary - token_file: /run/secrets/nia-notary-relay-token -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt + allowed_clients: [solmara-esignet, solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } deployment: profile: hosted_lab relay: { service: nia-population-relay } - notary: { service: nia-notary } diff --git a/projects/nia-population/environments/local.yaml b/projects/nia-population/environments/local.yaml index 6df2457..0742a5b 100644 --- a/projects/nia-population/environments/local.yaml +++ b/projects/nia-population/environments/local.yaml @@ -17,40 +17,14 @@ entities: alive: alive source_revision: nia-population-person-v1 generation: 2026-07-14 -issuance: - issuer: did:web:id.registrystack.org:solmara:authority:nia - signing_kid: did:web:id.registrystack.org:solmara:authority:nia#issuer-key-1 - signing_key: { secret: NIA_NOTARY_ISSUER_JWK } - generation: 1 -callers: - child-benefit-federator: - api_key_fingerprint: { secret: NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH } - scopes: ["evidence:nia:child-benefit:read"] - citizen-portal: - api_key_fingerprint: { secret: NIA_CITIZEN_CLIENT_TOKEN_HASH } - scopes: ["evidence:nia:citizen:read"] relay: origin: http://127.0.0.1:8080 issuer: http://127.0.0.1:8090 jwks_url: http://127.0.0.1:8090/.well-known/jwks.json audience: registry-relay - allowed_clients: [solmara-esignet] -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: nia-notary - token_file: /run/secrets/nia-notary-relay-token -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - -# Emulated local images require Registry Stack's bounded 1 GiB CEL worker ceiling. -# Hosted deployments intentionally retain the 128 MiB default. -notary_cel: - worker_memory_bytes: 1073741824 + allowed_clients: [solmara-esignet, solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } deployment: profile: local relay: { service: nia-population-relay } - notary: { service: nia-notary } diff --git a/projects/nia-population/integrations/population-person-by-uin/fixtures/match.yaml b/projects/nia-population/integrations/population-person-by-uin/fixtures/match.yaml index 11438f3..6a3a4e6 100644 --- a/projects/nia-population/integrations/population-person-by-uin/fixtures/match.yaml +++ b/projects/nia-population/integrations/population-person-by-uin/fixtures/match.yaml @@ -1,13 +1,5 @@ name: nia-population-person-match classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: solmara_uin, value: "2300010248" }] - claims: [population-record-active] - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review input: { uin: "2300010248" } interactions: - expect: { method: GET, path: /snapshot } @@ -17,6 +9,3 @@ interactions: expect: outcome: match outputs: { identity_status: active, alive: true } - claims: - population-record-active: true - citizen-population-record-active: true diff --git a/projects/nia-population/integrations/population-person-by-uin/fixtures/no-match.yaml b/projects/nia-population/integrations/population-person-by-uin/fixtures/no-match.yaml index 4bf76c4..08d453c 100644 --- a/projects/nia-population/integrations/population-person-by-uin/fixtures/no-match.yaml +++ b/projects/nia-population/integrations/population-person-by-uin/fixtures/no-match.yaml @@ -1,13 +1,5 @@ name: nia-population-person-no-match classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: solmara_uin, value: "9999999999" }] - claims: [citizen-population-record-active] - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/citizen-self-service input: { uin: "9999999999" } interactions: - expect: { method: GET, path: /snapshot } @@ -15,6 +7,3 @@ interactions: expect: outcome: no_match outputs: {} - claims: - population-record-active: false - citizen-population-record-active: false diff --git a/projects/nia-population/registry-stack.yaml b/projects/nia-population/registry-stack.yaml index 60e0cd3..ea110a8 100644 --- a/projects/nia-population/registry-stack.yaml +++ b/projects/nia-population/registry-stack.yaml @@ -28,6 +28,8 @@ services: evidence_verification: population:evidence_verification purposes: - https://id.registrystack.org/solmara/purpose/esignet-identity-verification + - https://id.registrystack.org/solmara/purpose/child-benefit-review + - https://id.registrystack.org/solmara/purpose/citizen-self-service projection: - uin - legacy_nid @@ -82,48 +84,14 @@ services: sensitivity: personal standards: { ogc_features: false, sp_dci: false } - nia-child-benefit: - kind: evidence + nia-population-consultation: + kind: consultation_api version: 1 purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review legal_basis: child-benefit-act consent: not_required - access: - scopes: ["evidence:nia:child-benefit:read"] consultations: population: integration: population-person-by-uin input: uin: request.target.identifiers.solmara_uin - claims: - population-record-active: - cel: >- - population.matched && population.identity_status == 'active' - && population.alive == true - disclosure: predicate - - nia-citizen-status: - kind: evidence - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/citizen-self-service - legal_basis: citizen-self-service - consent: not_required - access: - scopes: ["evidence:nia:citizen:read"] - consultations: - population: - integration: population-person-by-uin - input: - uin: request.target.identifiers.solmara_uin - claims: - citizen-population-record-active: - cel: >- - population.matched && population.identity_status == 'active' - && population.alive == true - disclosure: predicate - credential_profiles: - citizen-population-status: - format: dc+sd-jwt - type: https://id.registrystack.org/solmara/vct/citizen-population-status - validity: 10m - claims: [citizen-population-record-active] diff --git a/projects/sipf-pensions/environments/hosted.yaml b/projects/sipf-pensions/environments/hosted.yaml index 9977b87..ae9a145 100644 --- a/projects/sipf-pensions/environments/hosted.yaml +++ b/projects/sipf-pensions/environments/hosted.yaml @@ -22,34 +22,14 @@ entities: survivor_eligible: survivor_eligible source_revision: sipf-survivor-benefit-projection-v1 generation: 2026-07-14 -issuance: - issuer: did:web:id.registrystack.org:solmara:authority:sipf - signing_kid: did:web:id.registrystack.org:solmara:authority:sipf#issuer-key-1 - signing_key: { secret: SIPF_NOTARY_ISSUER_JWK } - generation: 1 -callers: - pension-review-desk: - api_key_fingerprint: { secret: SIPF_PENSION_CLIENT_TOKEN_HASH } - scopes: - - evidence:sipf:pension-payment:read - - evidence:sipf:survivor-benefit:read relay: origin: https://sipf-relay.solmara.registrystack.org issuer: https://workload-issuer.solmara.registrystack.org jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json audience: registry-relay - allowed_clients: [] -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: sipf-notary - token_file: /run/secrets/sipf-notary-relay-token -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt + allowed_clients: [solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } deployment: profile: hosted_lab relay: { service: sipf-pensions-relay } - notary: { service: sipf-notary } diff --git a/projects/sipf-pensions/environments/local.yaml b/projects/sipf-pensions/environments/local.yaml index ccebf5f..7064b3c 100644 --- a/projects/sipf-pensions/environments/local.yaml +++ b/projects/sipf-pensions/environments/local.yaml @@ -22,39 +22,14 @@ entities: survivor_eligible: survivor_eligible source_revision: sipf-survivor-benefit-projection-v1 generation: 2026-07-14 -issuance: - issuer: did:web:id.registrystack.org:solmara:authority:sipf - signing_kid: did:web:id.registrystack.org:solmara:authority:sipf#issuer-key-1 - signing_key: { secret: SIPF_NOTARY_ISSUER_JWK } - generation: 1 -callers: - pension-review-desk: - api_key_fingerprint: { secret: SIPF_PENSION_CLIENT_TOKEN_HASH } - scopes: - - evidence:sipf:pension-payment:read - - evidence:sipf:survivor-benefit:read relay: origin: http://127.0.0.1:8080 issuer: http://127.0.0.1:8090 jwks_url: http://127.0.0.1:8090/.well-known/jwks.json audience: registry-relay - allowed_clients: [] -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: sipf-notary - token_file: /run/secrets/sipf-notary-relay-token -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - -# Emulated local images require Registry Stack's bounded 1 GiB CEL worker ceiling. -# Hosted deployments intentionally retain the 128 MiB default. -notary_cel: - worker_memory_bytes: 1073741824 + allowed_clients: [solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } deployment: profile: local relay: { service: sipf-pensions-relay } - notary: { service: sipf-notary } diff --git a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/ambiguous.yaml b/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/ambiguous.yaml index 7ae1bef..78d5378 100644 --- a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/ambiguous.yaml +++ b/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/ambiguous.yaml @@ -11,4 +11,3 @@ interactions: expect: outcome: ambiguous outputs: {} - claims: {} diff --git a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/match.yaml b/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/match.yaml index 9c7695a..06f7c9f 100644 --- a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/match.yaml +++ b/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/match.yaml @@ -1,13 +1,5 @@ name: sipf-pension-case-match classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: solmara_uin, value: "2300109568" }] - claims: [pension-payment-active] - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review input: { uin: "2300109568" } interactions: - expect: { method: GET, path: /snapshot } @@ -15,4 +7,3 @@ interactions: expect: outcome: match outputs: { payment_status: active } - claims: { pension-payment-active: true } diff --git a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/no-match.yaml b/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/no-match.yaml index 8dc5416..792dcad 100644 --- a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/no-match.yaml +++ b/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/no-match.yaml @@ -7,4 +7,3 @@ interactions: expect: outcome: no_match outputs: {} - claims: { pension-payment-active: false } diff --git a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/ambiguous.yaml b/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/ambiguous.yaml index b364d2b..34858fd 100644 --- a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/ambiguous.yaml +++ b/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/ambiguous.yaml @@ -11,4 +11,3 @@ interactions: expect: outcome: ambiguous outputs: {} - claims: {} diff --git a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/match.yaml b/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/match.yaml index 1bdd88b..093ad3d 100644 --- a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/match.yaml +++ b/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/match.yaml @@ -1,13 +1,5 @@ name: sipf-survivor-case-match classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: solmara_uin, value: "2300118698" }] - claims: [survivor-is-eligible] - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination input: { uin: "2300118698" } interactions: - expect: { method: GET, path: /snapshot } @@ -15,4 +7,3 @@ interactions: expect: outcome: match outputs: { survivor_eligible: true } - claims: { survivor-is-eligible: true } diff --git a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/no-match.yaml b/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/no-match.yaml index ae04744..c07f8e7 100644 --- a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/no-match.yaml +++ b/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/no-match.yaml @@ -7,4 +7,3 @@ interactions: expect: outcome: no_match outputs: {} - claims: { survivor-is-eligible: false } diff --git a/projects/sipf-pensions/registry-stack.yaml b/projects/sipf-pensions/registry-stack.yaml index ed64b25..31bdcb7 100644 --- a/projects/sipf-pensions/registry-stack.yaml +++ b/projects/sipf-pensions/registry-stack.yaml @@ -16,44 +16,72 @@ entities: file: entities/survivor.yaml services: - sipf-pension-payment-review: - kind: evidence + sipf-pension-records: + kind: records_api + entity: pension + title: SIPF pension payment records + description: Purpose-limited pension facts used by the Solmara Evidence service. + owner: Social Insurance and Pension Fund + sensitivity: personal + access_rights: restricted + update_frequency: continuous + api: + scopes: + metadata: pension:metadata + rows: pension:rows + aggregate: pension:aggregate + evidence_verification: pension:evidence_verification + purposes: + - https://id.registrystack.org/solmara/purpose/pension-payment-review + projection: [pensioner_uin, payment_status] + pagination: { default_limit: 2, max_limit: 2 } + filters: + pensioner_uin: [eq] + standards: { ogc_features: false, sp_dci: false } + + sipf-pension-consultation: + kind: consultation_api version: 1 purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review legal_basis: social-insurance-act consent: not_required - access: - scopes: ["evidence:sipf:pension-payment:read"] consultations: pension: integration: pension-case-by-pensioner input: uin: request.target.identifiers.solmara_uin - claims: - pension-payment-active: - cel: "pension.matched && pension.payment_status == 'active'" - disclosure: predicate - sipf-survivor-benefit: - kind: evidence + sipf-survivor-consultation: + kind: consultation_api version: 1 purpose: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination legal_basis: social-insurance-act consent: not_required - access: - scopes: ["evidence:sipf:survivor-benefit:read"] consultations: survivor: integration: pension-case-by-survivor input: uin: request.target.identifiers.solmara_uin - claims: - survivor-is-eligible: - cel: "survivor.matched && survivor.survivor_eligible == true" - disclosure: predicate - credential_profiles: - survivor-benefit-status: - format: dc+sd-jwt - type: https://id.registrystack.org/solmara/vct/survivor-benefit-status - validity: 10m - claims: [survivor-is-eligible] + + sipf-survivor-records: + kind: records_api + entity: survivor + title: SIPF survivor benefit records + description: Purpose-limited survivor facts used by the Solmara Evidence service. + owner: Social Insurance and Pension Fund + sensitivity: personal + access_rights: restricted + update_frequency: continuous + api: + scopes: + metadata: survivor:metadata + rows: survivor:rows + aggregate: survivor:aggregate + evidence_verification: survivor:evidence_verification + purposes: + - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination + projection: [spouse_uin, survivor_eligible] + pagination: { default_limit: 2, max_limit: 2 } + filters: + spouse_uin: [eq] + standards: { ogc_features: false, sp_dci: false } diff --git a/projects/sro-social/environments/hosted.yaml b/projects/sro-social/environments/hosted.yaml index b2888ea..c01d136 100644 --- a/projects/sro-social/environments/hosted.yaml +++ b/projects/sro-social/environments/hosted.yaml @@ -12,31 +12,15 @@ entities: source_revision: solmara-sro-child-benefit-projection-v1 generation: "2026-07-14" -callers: - child-benefit-federator: - api_key_fingerprint: { secret: SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH } - scopes: ["sro_social:evidence_verification"] - relay: origin: https://sro-relay.solmara.registrystack.org issuer: https://workload-issuer.solmara.registrystack.org jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json audience: registry-relay - allowed_clients: [sro-notary] - -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: sro-notary - token_file: /run/secrets/sro-notary-relay-token - -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt + allowed_clients: [solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt deployment: profile: hosted_lab relay: { service: sro-social-relay } - notary: { service: sro-notary } diff --git a/projects/sro-social/environments/local.yaml b/projects/sro-social/environments/local.yaml index 2e494bd..5b03e74 100644 --- a/projects/sro-social/environments/local.yaml +++ b/projects/sro-social/environments/local.yaml @@ -12,37 +12,15 @@ entities: source_revision: solmara-sro-child-benefit-projection-v1 generation: "2026-07-14" -callers: - child-benefit-federator: - api_key_fingerprint: { secret: SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH } - scopes: ["sro_social:evidence_verification"] - relay: origin: http://127.0.0.1:8080 issuer: http://127.0.0.1:8090 jwks_url: http://127.0.0.1:8090/.well-known/jwks.json audience: registry-relay - allowed_clients: [sro-notary] - -notary_relay: - base_url: http://127.0.0.1:8080 - workload_client_id: sro-notary - token_file: /run/secrets/sro-notary-relay-token - -relay_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - -notary_state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - -# Emulated local images require Registry Stack's bounded 1 GiB CEL worker ceiling. -# Hosted deployments intentionally retain the 128 MiB default. -notary_cel: - worker_memory_bytes: 1073741824 + allowed_clients: [solmara-evidence] + consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } +relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } deployment: profile: local relay: { service: sro-social-relay } - notary: { service: sro-notary } diff --git a/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/match.yaml b/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/match.yaml index ad3f15f..e4b943a 100644 --- a/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/match.yaml +++ b/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/match.yaml @@ -1,13 +1,5 @@ name: child-benefit-household-match classification: synthetic -request: - target: - type: Person - identifiers: [{ scheme: solmara_uin, value: "2300010248" }] - claims: [household-below-poverty-threshold] - disclosure: predicate - format: application/vnd.registry-notary.claim-result+json - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review input: { uin: "2300010248" } interactions: - expect: { method: GET, path: /snapshot } @@ -15,4 +7,3 @@ interactions: expect: outcome: match outputs: { poverty_band: priority } - claims: { household-below-poverty-threshold: true } diff --git a/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/no-match.yaml b/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/no-match.yaml index 6092d49..b1fa7ed 100644 --- a/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/no-match.yaml +++ b/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/no-match.yaml @@ -7,4 +7,3 @@ interactions: expect: outcome: no_match outputs: {} - claims: { household-below-poverty-threshold: false } diff --git a/projects/sro-social/registry-stack.yaml b/projects/sro-social/registry-stack.yaml index 7e7974b..eddf1cc 100644 --- a/projects/sro-social/registry-stack.yaml +++ b/projects/sro-social/registry-stack.yaml @@ -12,22 +12,37 @@ entities: file: entities/sro_child.yaml services: - child-benefit: - kind: evidence + sro-household-records: + kind: records_api + entity: sro_child + title: SRO household records + description: Purpose-limited household facts used by the Solmara Evidence service. + owner: Social Registry Office + sensitivity: personal + access_rights: restricted + update_frequency: continuous + api: + scopes: + metadata: sro_child:metadata + rows: sro_child:rows + aggregate: sro_child:aggregate + evidence_verification: sro_child:evidence_verification + purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + projection: [uin, poverty_band] + pagination: { default_limit: 2, max_limit: 2 } + filters: + uin: [eq] + standards: { ogc_features: false, sp_dci: false } + + sro-household-consultation: + kind: consultation_api version: 1 purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review legal_basis: child-benefit-act consent: not_required - access: - scopes: ["sro_social:evidence_verification"] consultations: household: integration: child-benefit-household-by-uin input: uin: request.target.identifiers.solmara_uin - claims: - household-below-poverty-threshold: - cel: 'household.matched && household.poverty_band == "priority"' - disclosure: - default: predicate - allowed: [predicate, redacted] diff --git a/runtime/registry-projects/hosted/cra-civil/notary/notary.yaml b/runtime/registry-projects/hosted/cra-civil/notary/notary.yaml deleted file mode 100644 index f5453e2..0000000 --- a/runtime/registry-projects/hosted/cra-civil/notary/notary.yaml +++ /dev/null @@ -1,233 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH - provider: env - id: child-benefit-federator - scopes: - - evidence:cra:child-benefit:read - - fingerprint: - name: CRA_CITIZEN_CLIENT_TOKEN_HASH - provider: env - id: citizen-portal - scopes: - - evidence:cra:citizen:read - - fingerprint: - name: CRA_PENSION_CLIENT_TOKEN_HASH - provider: env - id: pension-review-desk - scopes: - - evidence:cra:pension:read -deployment: - profile: hosted_lab -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - - https://id.registrystack.org/solmara/purpose/pension-payment-review - claims: - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - civil: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - birth_brn: - max_bytes: 128 - nullable: true - type: string - birth_date: - nullable: false - type: date - deceased: - nullable: false - type: boolean - profile: - contract_hash: sha256:35acae8dfe5b6cc1c18f9aedbc188c93ded49a46553b7ab21451eb772e26918e - id: solmara-cra-civil.cra-child-benefit.civil - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: birth-is-registered - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - required_scopes: - - evidence:cra:child-benefit:read - rule: - bindings: {} - expression: 'civil.matched && civil.birth_brn != null ? civil.birth_brn != '''' : false' - type: cel - subject_type: person - title: birth is registered - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - civil: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - birth_brn: - max_bytes: 128 - nullable: true - type: string - birth_date: - nullable: false - type: date - deceased: - nullable: false - type: boolean - profile: - contract_hash: sha256:35acae8dfe5b6cc1c18f9aedbc188c93ded49a46553b7ab21451eb772e26918e - id: solmara-cra-civil.cra-child-benefit.civil - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: child-age-under-5 - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - required_scopes: - - evidence:cra:child-benefit:read - rule: - bindings: {} - expression: |- - civil.matched && civil.birth_date != null && civil.deceased == false - ? date.age_on(civil.birth_date, as_of_date) >= 0 - && date.age_on(civil.birth_date, as_of_date) < 5 - : false - type: cel - subject_type: person - title: child age under 5 - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - civil: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - birth_brn: - max_bytes: 128 - nullable: true - type: string - birth_date: - nullable: false - type: date - deceased: - nullable: false - type: boolean - profile: - contract_hash: sha256:c101d11d7fe66efe8400330851d65e4e105be62eae6ae9325eb43a83daae8aed - id: solmara-cra-civil.cra-citizen-record.civil - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: civil-record-linked - purpose: https://id.registrystack.org/solmara/purpose/citizen-self-service - required_scopes: - - evidence:cra:citizen:read - rule: - bindings: {} - expression: civil.matched - type: cel - subject_type: person - title: civil record linked - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - civil: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - birth_brn: - max_bytes: 128 - nullable: true - type: string - birth_date: - nullable: false - type: date - deceased: - nullable: false - type: boolean - profile: - contract_hash: sha256:80486f20e013dd4a99af89a22efe3c83638f37f3f79ea7c912a8d5b237db2924 - id: solmara-cra-civil.cra-pension-death.civil - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: person-is-deceased - purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review - required_scopes: - - evidence:cra:pension:read - rule: - bindings: {} - expression: civil.matched && civil.deceased == true - type: cel - subject_type: person - title: person is deceased - value: - nullable: false - type: boolean - version: '1' - credential_profiles: {} - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/cra-notary-relay-token - workload_client_id: cra-notary - service_id: cra-notary - variables: - as_of_date: - from: request.variables.as_of_date - type: date -instance: - environment: hosted - id: cra-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json new file mode 100644 index 0000000..1ec4b1f --- /dev/null +++ b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json @@ -0,0 +1 @@ +{"id":"solmara-cra-civil.cra-civil-consultation.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:aa167d27cfe661509fb5b8e159dcf410ea4057cf223d8adfc33bcdefaf3e2818","id":"relay.cra-civil-consultation.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:cra-civil-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json new file mode 100644 index 0000000..bfcbb9d --- /dev/null +++ b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-future-birth-date","variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-match","variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"cra-civil-person-no-match","variables":{"as_of_date":"2026-07-14"}}],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/evidence/civil-person-by-uin/minimization.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json similarity index 100% rename from runtime/registry-projects/hosted/cra-civil/relay/artifacts/evidence/civil-person-by-uin/minimization.json rename to runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json diff --git a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/evidence/civil-person-by-uin/negative-security.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json similarity index 100% rename from runtime/registry-projects/hosted/cra-civil/relay/artifacts/evidence/civil-person-by-uin/negative-security.json rename to runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json diff --git a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/integration-packs/civil-person-by-uin.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json similarity index 93% rename from runtime/registry-projects/hosted/cra-civil/relay/artifacts/integration-packs/civil-person-by-uin.json rename to runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json index 5533854..e80b1e7 100644 --- a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/integration-packs/civil-person-by-uin.json +++ b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json @@ -1 +1 @@ -{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:1943d411187b226b981cddc2cd0442e73953d29cb5a047bbb38c729d78883afd"],"minimization":["sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263"],"negative_security":["sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-cra-civil-person-by-uin","output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:4348d96de834e21384de4e5d3f7e19ff10e299a7148c8d1df0739ee1d50199a3"],"minimization":["sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263"],"negative_security":["sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-cra-civil-person-by-uin","output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-child-benefit-civil.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json similarity index 67% rename from runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-child-benefit-civil.json rename to runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json index 329227a..cb199e7 100644 --- a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-child-benefit-civil.json +++ b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-child-benefit.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:487bef77452bf86b610743eadc53f87429ef9ec66c06b9ee8d41e8a0e5ef6210","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-civil-consultation.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay/relay-consultation.yaml b/runtime/registry-projects/hosted/cra-civil/relay-consultation/relay.yaml similarity index 51% rename from runtime/registry-projects/hosted/cra-civil/relay/relay-consultation.yaml rename to runtime/registry-projects/hosted/cra-civil/relay-consultation/relay.yaml index e82fd3d..ce77c39 100644 --- a/runtime/registry-projects/hosted/cra-civil/relay/relay-consultation.yaml +++ b/runtime/registry-projects/hosted/cra-civil/relay-consultation/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: false allowed_clients: - - cra-notary + - solmara-consultation audiences: - registry-relay issuer: https://workload-issuer.solmara.registrystack.org @@ -18,39 +19,27 @@ catalog: consultation: artifacts: evidence: - - class: conformance - path: artifacts/evidence/civil-person-by-uin/conformance.json - sha256: sha256:1943d411187b226b981cddc2cd0442e73953d29cb5a047bbb38c729d78883afd - class: negative_security path: artifacts/evidence/civil-person-by-uin/negative-security.json sha256: sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f + - class: conformance + path: artifacts/evidence/civil-person-by-uin/conformance.json + sha256: sha256:4348d96de834e21384de4e5d3f7e19ff10e299a7148c8d1df0739ee1d50199a3 - class: minimization path: artifacts/evidence/civil-person-by-uin/minimization.json sha256: sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263 integration_packs: - - hash: sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499 + - hash: sha256:487bef77452bf86b610743eadc53f87429ef9ec66c06b9ee8d41e8a0e5ef6210 path: artifacts/integration-packs/civil-person-by-uin.json - sha256: sha256:ad85a4bbcbd9c164264afeb98c11c549cc0c54997b39572261282211ab9104b2 + sha256: sha256:975ab5565da70354b1eccb3f2f5da123e69d4983c45bcf5955cf55df6d4123cf private_bindings: - - hash: sha256:1fb0b8c2b28da70d6f22675209ced135f4855fcfdc2fd38a32aadaa0c70be0dd - path: artifacts/private-bindings/cra-child-benefit-civil.json - sha256: sha256:087d5ddfc5bbc43620f9bf499cda7343ff0b762427451f30fd3541bb7d230e22 - - hash: sha256:918b533b198e4e45e368db0b3d8737728700f4b6ee428e10a93a1a06460649b1 - path: artifacts/private-bindings/cra-citizen-record-civil.json - sha256: sha256:d564828b5a94041f3bb718c16b40a0a53ea68a94ed917bf96e5ed04f0ec8d086 - - hash: sha256:fbac0434f957c897098a1577a20ae8763258b1a1e2385a577129258dbbe86579 - path: artifacts/private-bindings/cra-pension-death-civil.json - sha256: sha256:f4eea1f1f2df7755c729d0a645778ca9805e15e3875962595c0038cc46142f51 + - hash: sha256:41e6b05846b019807b19590010bd812c5477d74228a55636b725548d21673bc4 + path: artifacts/private-bindings/cra-civil-consultation-civil.json + sha256: sha256:f5e2bd11c9c512a718bb0f82d8ba0d554b16f6da5ddf1d5f896bd536711824c5 public_contracts: - - hash: sha256:35acae8dfe5b6cc1c18f9aedbc188c93ded49a46553b7ab21451eb772e26918e - path: artifacts/consultation-contracts/cra-child-benefit-civil.json - sha256: sha256:65bfb5000e186b4e461c392454e01c76149e821551152ec79a22f28b1e15f6b1 - - hash: sha256:c101d11d7fe66efe8400330851d65e4e105be62eae6ae9325eb43a83daae8aed - path: artifacts/consultation-contracts/cra-citizen-record-civil.json - sha256: sha256:ffb8a94778c5d1015c14ecc03fd06d7f2b2d5926aca0654cc80887d930bf74d2 - - hash: sha256:80486f20e013dd4a99af89a22efe3c83638f37f3f79ea7c912a8d5b237db2924 - path: artifacts/consultation-contracts/cra-pension-death-civil.json - sha256: sha256:2fbbfadc074822821239ff13ead71115d7d64c6378e39c4d03a870eb3b6655ea + - hash: sha256:7d81ecba1b399a2caa3f8b08f27b93626f2c8b2d51ec2305874a19af34cc852b + path: artifacts/consultation-contracts/cra-civil-consultation-civil.json + sha256: sha256:28effc0ce34a4c498ff7605cde0c59c59ecd199877bbd02fb73c49be31b73f48 rhai_scripts: [] audit_pseudonym_materials: - key_id: epoch-1 @@ -60,8 +49,8 @@ consultation: authorized_workload: audience: registry-relay client_claim_selector: azp - client_value: cra-notary - principal_id: cra-notary + client_value: solmara-consultation + principal_id: solmara-consultation source_credentials: [] state_plane: audit_pseudonym_keyring_lock_key: 5222333210205388 @@ -78,21 +67,69 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized civil_people entity - entities: [] + description: Purpose-limited civil facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: civil_people:aggregate + evidence_verification_scope: civil_people:evidence_verification + metadata_scope: civil_people:metadata + read_scope: civil_people:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + - https://id.registrystack.org/solmara/purpose/pension-payment-review + - https://id.registrystack.org/solmara/purpose/citizen-self-service + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited civil facts used by the Solmara Evidence service. + fields: + - from: birth_brn + name: birth_brn + sensitive: true + - from: birth_date + name: birth_date + sensitive: true + - from: deceased + name: deceased + sensitive: true + - from: uin + name: uin + sensitive: true + name: civil_people + relationships: [] + spatial: null + table: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 + title: CRA civil records id: civil_people - owner: solmara-cra-civil + owner: Civil Registration Authority sensitivity: personal tables: - access: aggregate_scope: civil_people:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: civil_people:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 materialization: snapshot primary_key: uin @@ -126,13 +163,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv type: file - title: civil_people - update_frequency: as_needed + title: CRA civil records + update_frequency: continuous deployment: profile: hosted_lab instance: environment: hosted id: cra-civil-relay-consultation server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-child-benefit-civil.json b/runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-child-benefit-civil.json deleted file mode 100644 index aad610b..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-child-benefit-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-child-benefit.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:9ec50673e36783484c8e35137bd65396a871ecc12c2e8fdd2cc68b1f0cab108b","id":"relay.cra-child-benefit.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:cra-child-benefit","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-citizen-record-civil.json b/runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-citizen-record-civil.json deleted file mode 100644 index f0ef1ff..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-citizen-record-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-citizen-record.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"citizen-self-service","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:ddf7a76ffe3dabacabdc1df18588bb87826a00a061572ad49c5052baf073f576","id":"relay.cra-citizen-record.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/citizen-self-service"],"required_scope":"registry:consult:cra-citizen-record","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-pension-death-civil.json b/runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-pension-death-civil.json deleted file mode 100644 index 2f64031..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/consultation-contracts/cra-pension-death-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-pension-death.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:dd59f03e034f68b35706ae6193bdd580081f2b596c6b92673fc7a66961e34fc1","id":"relay.cra-pension-death.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:cra-pension-death","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/evidence/civil-person-by-uin/conformance.json b/runtime/registry-projects/hosted/cra-civil/relay/artifacts/evidence/civil-person-by-uin/conformance.json deleted file mode 100644 index 9e550c6..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/evidence/civil-person-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"birth-is-registered":true,"child-age-under-5":false,"civil-record-linked":true,"person-is-deceased":false},"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-future-birth-date","request":{"claims":[{"id":"birth-is-registered"},{"id":"child-age-under-5"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"},"variables":{"as_of_date":"2026-07-14"}},"variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"claims":{"birth-is-registered":true,"child-age-under-5":true,"civil-record-linked":true,"person-is-deceased":false},"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-match","request":{"claims":[{"id":"civil-record-linked"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/citizen-self-service","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"claims":{"birth-is-registered":false,"child-age-under-5":false,"civil-record-linked":false,"person-is-deceased":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"cra-civil-person-no-match","request":{"claims":[{"id":"person-is-deceased"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/pension-payment-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"9999999999"}],"type":"Person"}},"variables":{"as_of_date":"2026-07-14"}}],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-pension-death-civil.json b/runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-pension-death-civil.json deleted file mode 100644 index 5b5ec3b..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-pension-death-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-pension-death.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay/relay.yaml b/runtime/registry-projects/hosted/cra-civil/relay/relay.yaml index 66bd9da..24f67a0 100644 --- a/runtime/registry-projects/hosted/cra-civil/relay/relay.yaml +++ b/runtime/registry-projects/hosted/cra-civil/relay/relay.yaml @@ -1,11 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: false - allowed_clients: [] + allowed_clients: + - solmara-evidence audiences: - registry-relay issuer: https://workload-issuer.solmara.registrystack.org @@ -23,21 +25,69 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized civil_people entity - entities: [] + description: Purpose-limited civil facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: civil_people:aggregate + evidence_verification_scope: civil_people:evidence_verification + metadata_scope: civil_people:metadata + read_scope: civil_people:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + - https://id.registrystack.org/solmara/purpose/pension-payment-review + - https://id.registrystack.org/solmara/purpose/citizen-self-service + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited civil facts used by the Solmara Evidence service. + fields: + - from: birth_brn + name: birth_brn + sensitive: true + - from: birth_date + name: birth_date + sensitive: true + - from: deceased + name: deceased + sensitive: true + - from: uin + name: uin + sensitive: true + name: civil_people + relationships: [] + spatial: null + table: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 + title: CRA civil records id: civil_people - owner: solmara-cra-civil + owner: Civil Registration Authority sensitivity: personal tables: - access: aggregate_scope: civil_people:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: civil_people:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 materialization: snapshot primary_key: uin @@ -71,13 +121,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv type: file - title: civil_people - update_frequency: as_needed + title: CRA civil records + update_frequency: continuous deployment: profile: hosted_lab instance: environment: hosted id: cra-civil-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml b/runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml deleted file mode 100644 index 6dd83e0..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml +++ /dev/null @@ -1,76 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH - provider: env - id: child-benefit-federator - scopes: - - mosd_programme:evidence_verification -deployment: - profile: hosted_lab -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - claims: - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - enrollment: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - duplicate_flag: - nullable: false - type: boolean - profile: - contract_hash: sha256:9da47bc7171991c51e098b44cb0667595b344d5f4c2b994a67f0c3b401d4b5b3 - id: solmara-mosd-programme.child-benefit.enrollment - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: not-already-enrolled - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - required_scopes: - - mosd_programme:evidence_verification - rule: - bindings: {} - expression: enrollment.matched && enrollment.duplicate_flag == false - type: cel - subject_type: person - title: not already enrolled - value: - nullable: false - type: boolean - version: '1' - credential_profiles: {} - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/programme-notary-relay-token - workload_client_id: programme-notary - service_id: programme-notary - variables: {} -instance: - environment: hosted - id: programme-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json new file mode 100644 index 0000000..c43521d --- /dev/null +++ b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json @@ -0,0 +1 @@ +{"id":"solmara-mosd-programme.mosd-enrollment-consultation.enrollment","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:e51568815beddf2e304dcc482daf67f7493e1cd23ff68847d22eb62c22cb6ba3","id":"relay.mosd-enrollment-consultation.enrollment","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:mosd-enrollment-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-mosd-programme.enrollment-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["duplicate_flag"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json new file mode 100644 index 0000000..e91e1d3 --- /dev/null +++ b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"duplicate_flag":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"duplicate_flag":false},"headers":{},"status":200}}}],"name":"enrollment-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"enrollment-no-match","variables":{}}],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/minimization.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json similarity index 100% rename from runtime/registry-projects/hosted/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/minimization.json rename to runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json diff --git a/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/negative-security.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json similarity index 100% rename from runtime/registry-projects/hosted/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/negative-security.json rename to runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json diff --git a/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/integration-packs/enrollment-by-uin.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json similarity index 92% rename from runtime/registry-projects/hosted/mosd-programme/relay/artifacts/integration-packs/enrollment-by-uin.json rename to runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json index 729f6a0..096dbe6 100644 --- a/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/integration-packs/enrollment-by-uin.json +++ b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json @@ -1 +1 @@ -{"id":"solmara-mosd-programme.enrollment-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:f7df3b836234ef5893833546f66bb989f0b004ca1d32c006f72a13ebb3a4c2d8"],"minimization":["sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83"],"negative_security":["sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"enrollment-by-uin","output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-mosd-programme.enrollment-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:304b2cd16ce7d97127bfb0be3b60f7c25b2624ed25fbe0219bac09c067e6a1bb"],"minimization":["sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83"],"negative_security":["sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"enrollment-by-uin","output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json similarity index 62% rename from runtime/registry-projects/local/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json rename to runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json index 078b94e..a4bc8da 100644 --- a/runtime/registry-projects/local/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json +++ b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:cffdf1b631e81fee923267d17c17f3001685b1b6681ddb8b93040b0c2691a6e8","id":"solmara-mosd-programme.enrollment-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"duplicate_flag":"duplicate_flag"}},"table_provider":"mosd_enroll__materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf"},"profile":{"id":"solmara-mosd-programme.child-benefit.enrollment","version":"1"},"registry_instance":"solmara-mosd-programme","source_instance":"materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf","tenant":"solmara-mosd-programme"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:d82492e2f92fca8ce1d83208d892e2a18468ef6db6afa66d467df002336a6c92","id":"solmara-mosd-programme.enrollment-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"duplicate_flag":"duplicate_flag"}},"table_provider":"mosd_enroll__materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf"},"profile":{"id":"solmara-mosd-programme.mosd-enrollment-consultation.enrollment","version":"1"},"registry_instance":"solmara-mosd-programme","source_instance":"materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf","tenant":"solmara-mosd-programme"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/mosd-programme/relay/relay-consultation.yaml b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/relay.yaml similarity index 52% rename from runtime/registry-projects/hosted/mosd-programme/relay/relay-consultation.yaml rename to runtime/registry-projects/hosted/mosd-programme/relay-consultation/relay.yaml index 63d65e5..421266c 100644 --- a/runtime/registry-projects/hosted/mosd-programme/relay/relay-consultation.yaml +++ b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: false allowed_clients: - - programme-notary + - solmara-consultation audiences: - registry-relay issuer: https://workload-issuer.solmara.registrystack.org @@ -21,24 +22,24 @@ consultation: - class: minimization path: artifacts/evidence/enrollment-by-uin/minimization.json sha256: sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83 + - class: conformance + path: artifacts/evidence/enrollment-by-uin/conformance.json + sha256: sha256:304b2cd16ce7d97127bfb0be3b60f7c25b2624ed25fbe0219bac09c067e6a1bb - class: negative_security path: artifacts/evidence/enrollment-by-uin/negative-security.json sha256: sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282 - - class: conformance - path: artifacts/evidence/enrollment-by-uin/conformance.json - sha256: sha256:f7df3b836234ef5893833546f66bb989f0b004ca1d32c006f72a13ebb3a4c2d8 integration_packs: - - hash: sha256:cffdf1b631e81fee923267d17c17f3001685b1b6681ddb8b93040b0c2691a6e8 + - hash: sha256:d82492e2f92fca8ce1d83208d892e2a18468ef6db6afa66d467df002336a6c92 path: artifacts/integration-packs/enrollment-by-uin.json - sha256: sha256:40ac75a61c3346b282e3abadd16654b634ebe0ac60c77a7b4c2ac98cf5f3af50 + sha256: sha256:3b4de6e7946d91a2af1e6f0cdbc3397e7c382e960ae829ec0ddbb030f8192c5d private_bindings: - - hash: sha256:ae3674a401a41daa3325d73cd22363dba2d4c5145ae22cdc64587a6e0a293654 - path: artifacts/private-bindings/child-benefit-enrollment.json - sha256: sha256:fd8fc27359128d39dbed4cd6842d7db21f3b5eef21359f0b6bec93c0f6822f18 + - hash: sha256:2d91cd9c15c0badbb565bd16a2be9f46ac77d0ad0c827a0eea895e20edf3a3a2 + path: artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json + sha256: sha256:f27344b5e2dec067577087b23dfd4cc031e5680f4871dc028222be875f81865a public_contracts: - - hash: sha256:9da47bc7171991c51e098b44cb0667595b344d5f4c2b994a67f0c3b401d4b5b3 - path: artifacts/consultation-contracts/child-benefit-enrollment.json - sha256: sha256:253ee934a9c9bbf725e233f90c195d057893eeea14bc9bc12330c0957245ff10 + - hash: sha256:58e60de7fdae8b94a0cdf130f80c1daffb2622773aac4e60d3714f6547a7b2a5 + path: artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json + sha256: sha256:50bee564d51a566df7dcd634476a2bad80920ebfb0d88b535db85081c8558cab rhai_scripts: [] audit_pseudonym_materials: - key_id: epoch-1 @@ -48,8 +49,8 @@ consultation: authorized_workload: audience: registry-relay client_claim_selector: azp - client_value: programme-notary - principal_id: programme-notary + client_value: solmara-consultation + principal_id: solmara-consultation source_credentials: [] state_plane: audit_pseudonym_keyring_lock_key: 9069287079765840165 @@ -66,21 +67,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized mosd_enroll entity - entities: [] + description: Purpose-limited enrollment facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: mosd_enroll:aggregate + evidence_verification_scope: mosd_enroll:evidence_verification + metadata_scope: mosd_enroll:metadata + read_scope: mosd_enroll:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited enrollment facts used by the Solmara Evidence service. + fields: + - from: duplicate_flag + name: duplicate_flag + sensitive: true + - from: uin + name: uin + sensitive: true + name: mosd_enroll + relationships: [] + spatial: null + table: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf + title: MOSD programme enrollment records id: mosd_enroll - owner: solmara-mosd-programme + owner: Ministry of Social Development sensitivity: personal tables: - access: aggregate_scope: mosd_enroll:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: mosd_enroll:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf materialization: snapshot primary_key: uin @@ -106,13 +147,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv type: file - title: mosd_enroll - update_frequency: as_needed + title: MOSD programme enrollment records + update_frequency: continuous deployment: profile: hosted_lab instance: environment: hosted id: programme-mis-relay-consultation server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/consultation-contracts/child-benefit-enrollment.json b/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/consultation-contracts/child-benefit-enrollment.json deleted file mode 100644 index bc41921..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/consultation-contracts/child-benefit-enrollment.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-mosd-programme.child-benefit.enrollment","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:50a4f9fd40526b837aa90b02d27e13e98f66d9de1362eb06fb2465afbae31c53","id":"relay.child-benefit.enrollment","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:child-benefit","workload":"programme-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-mosd-programme.enrollment-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["duplicate_flag"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/conformance.json b/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/conformance.json deleted file mode 100644 index 6910195..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"not-already-enrolled":true},"error":null,"outcome":"match","outputs":{"duplicate_flag":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"duplicate_flag":false},"headers":{},"status":200}}}],"name":"enrollment-match","request":{"claims":[{"id":"not-already-enrolled"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"not-already-enrolled":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"enrollment-no-match","request":null,"variables":{}}],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/mosd-programme/relay/relay.yaml b/runtime/registry-projects/hosted/mosd-programme/relay/relay.yaml index 915fc47..2cc24c5 100644 --- a/runtime/registry-projects/hosted/mosd-programme/relay/relay.yaml +++ b/runtime/registry-projects/hosted/mosd-programme/relay/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: false allowed_clients: - - programme-notary + - solmara-evidence audiences: - registry-relay issuer: https://workload-issuer.solmara.registrystack.org @@ -24,21 +25,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized mosd_enroll entity - entities: [] + description: Purpose-limited enrollment facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: mosd_enroll:aggregate + evidence_verification_scope: mosd_enroll:evidence_verification + metadata_scope: mosd_enroll:metadata + read_scope: mosd_enroll:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited enrollment facts used by the Solmara Evidence service. + fields: + - from: duplicate_flag + name: duplicate_flag + sensitive: true + - from: uin + name: uin + sensitive: true + name: mosd_enroll + relationships: [] + spatial: null + table: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf + title: MOSD programme enrollment records id: mosd_enroll - owner: solmara-mosd-programme + owner: Ministry of Social Development sensitivity: personal tables: - access: aggregate_scope: mosd_enroll:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: mosd_enroll:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf materialization: snapshot primary_key: uin @@ -64,13 +105,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv type: file - title: mosd_enroll - update_frequency: as_needed + title: MOSD programme enrollment records + update_frequency: continuous deployment: profile: hosted_lab instance: environment: hosted id: programme-mis-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml b/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml deleted file mode 100644 index 00b03fb..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml +++ /dev/null @@ -1,412 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: NAGDI_CLIENT_TOKEN_HASH - provider: env - id: nagdi-service - scopes: - - nagdi_agriculture:evidence_verification -deployment: - profile: hosted_lab -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/livestock-movement-control - - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - claims: - - credential_profiles: - - livestock.livestock_movement_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - herd: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - destination_district_open: - nullable: false - type: boolean - herd_vaccination_current: - nullable: false - type: boolean - no_conflicting_open_movement_permit: - nullable: false - type: boolean - origin_district_not_quarantined_for_species: - nullable: false - type: boolean - registered_herd: - nullable: false - type: boolean - profile: - contract_hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 - id: solmara-nagdi-agriculture.livestock.herd - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: eligible-for-livestock-movement-permit - purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: |- - herd.matched && herd.registered_herd - && herd.origin_district_not_quarantined_for_species - && herd.herd_vaccination_current - && herd.destination_district_open - && herd.no_conflicting_open_movement_permit - type: cel - subject_type: person - title: eligible for livestock movement permit - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - livestock.livestock_movement_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - herd: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - destination_district_open: - nullable: false - type: boolean - herd_vaccination_current: - nullable: false - type: boolean - no_conflicting_open_movement_permit: - nullable: false - type: boolean - origin_district_not_quarantined_for_species: - nullable: false - type: boolean - registered_herd: - nullable: false - type: boolean - profile: - contract_hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 - id: solmara-nagdi-agriculture.livestock.herd - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: origin-district-not-quarantined-for-species - purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: herd.matched && herd.origin_district_not_quarantined_for_species - type: cel - subject_type: person - title: origin district not quarantined for species - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - livestock.livestock_movement_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - herd: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - destination_district_open: - nullable: false - type: boolean - herd_vaccination_current: - nullable: false - type: boolean - no_conflicting_open_movement_permit: - nullable: false - type: boolean - origin_district_not_quarantined_for_species: - nullable: false - type: boolean - registered_herd: - nullable: false - type: boolean - profile: - contract_hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 - id: solmara-nagdi-agriculture.livestock.herd - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: registered-herd - purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: herd.matched && herd.registered_herd - type: cel - subject_type: person - title: registered herd - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - voucher.climate_smart_voucher_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - farmer: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - active_farm_parcel: - nullable: false - type: boolean - active_smallholder_farmer: - nullable: false - type: boolean - crop_declared_for_season: - nullable: false - type: boolean - data_use_authorized: - nullable: false - type: boolean - district_climate_risk_active: - nullable: false - type: boolean - farmer_registered: - nullable: false - type: boolean - voucher_entitlement_current: - nullable: false - type: boolean - voucher_not_redeemed: - nullable: false - type: boolean - profile: - contract_hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c - id: solmara-nagdi-agriculture.voucher.farmer - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: data-use-authorized-for-purpose - purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: farmer.matched && farmer.data_use_authorized - type: cel - subject_type: person - title: data use authorized for purpose - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - voucher.climate_smart_voucher_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - farmer: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - active_farm_parcel: - nullable: false - type: boolean - active_smallholder_farmer: - nullable: false - type: boolean - crop_declared_for_season: - nullable: false - type: boolean - data_use_authorized: - nullable: false - type: boolean - district_climate_risk_active: - nullable: false - type: boolean - farmer_registered: - nullable: false - type: boolean - voucher_entitlement_current: - nullable: false - type: boolean - voucher_not_redeemed: - nullable: false - type: boolean - profile: - contract_hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c - id: solmara-nagdi-agriculture.voucher.farmer - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: eligible-for-climate-smart-input-voucher - purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: |- - farmer.matched && farmer.farmer_registered - && farmer.data_use_authorized - && farmer.active_smallholder_farmer - && farmer.active_farm_parcel - && farmer.crop_declared_for_season - && farmer.district_climate_risk_active - && farmer.voucher_entitlement_current - && farmer.voucher_not_redeemed - type: cel - subject_type: person - title: eligible for climate smart input voucher - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - voucher.climate_smart_voucher_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - farmer: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - active_farm_parcel: - nullable: false - type: boolean - active_smallholder_farmer: - nullable: false - type: boolean - crop_declared_for_season: - nullable: false - type: boolean - data_use_authorized: - nullable: false - type: boolean - district_climate_risk_active: - nullable: false - type: boolean - farmer_registered: - nullable: false - type: boolean - voucher_entitlement_current: - nullable: false - type: boolean - voucher_not_redeemed: - nullable: false - type: boolean - profile: - contract_hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c - id: solmara-nagdi-agriculture.voucher.farmer - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: farmer-registered - purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: farmer.matched && farmer.farmer_registered - type: cel - subject_type: person - title: farmer registered - value: - nullable: false - type: boolean - version: '1' - credential_profiles: - livestock.livestock_movement_sd_jwt: - allowed_claims: - - registered-herd - - origin-district-not-quarantined-for-species - - eligible-for-livestock-movement-permit - disclosure: - allowed: - - value - - predicate - - redacted - format: application/dc+sd-jwt - issuer: did:web:id.registrystack.org:solmara:authority:nagdi - signing_key: project-issuer - validity_seconds: 600 - vct: https://id.registrystack.org/solmara/vct/livestock-movement-permit - voucher.climate_smart_voucher_sd_jwt: - allowed_claims: - - farmer-registered - - data-use-authorized-for-purpose - - eligible-for-climate-smart-input-voucher - disclosure: - allowed: - - value - - predicate - - redacted - format: application/dc+sd-jwt - issuer: did:web:id.registrystack.org:solmara:authority:nagdi - signing_key: project-issuer - validity_seconds: 600 - vct: https://id.registrystack.org/solmara/vct/climate-smart-voucher-eligibility - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/nagdi-notary-relay-token - workload_client_id: nagdi-notary - service_id: nagdi-notary - signing_keys: - project-issuer: - alg: EdDSA - kid: did:web:id.registrystack.org:solmara:authority:nagdi#issuer-key-1 - private_jwk_env: NAGDI_NOTARY_ISSUER_JWK - provider: local_jwk_env - status: active - variables: {} -instance: - environment: hosted - id: nagdi-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json new file mode 100644 index 0000000..8378cbe --- /dev/null +++ b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json @@ -0,0 +1 @@ +{"id":"solmara-nagdi-agriculture.nagdi-farmer-consultation.farmer","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:20dc22dcff83f4f14e0402937e8def43c5f3255241783c35cdc6a6a7afbe3005","id":"relay.nagdi-farmer-consultation.farmer","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"],"required_scope":"registry:consult:nagdi-farmer-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["active_farm_parcel","active_smallholder_farmer","crop_declared_for_season","data_use_authorized","district_climate_risk_active","farmer_registered","voucher_entitlement_current","voucher_not_redeemed"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json new file mode 100644 index 0000000..45d152e --- /dev/null +++ b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json @@ -0,0 +1 @@ +{"id":"solmara-nagdi-agriculture.nagdi-herd-consultation.herd","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:7ed0ed7964797fe81e08fcda71fe90b6fcd5846611c3422bb8ce5f6d0b4fe970","id":"relay.nagdi-herd-consultation.herd","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/livestock-movement-control"],"required_scope":"registry:consult:nagdi-herd-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["destination_district_open","herd_vaccination_current","no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species","registered_herd"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json new file mode 100644 index 0000000..78620c0 --- /dev/null +++ b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true},"headers":{},"status":200}}}],"name":"farmer-voucher-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"farmer-voucher-no-match","variables":{}}],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/minimization.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json similarity index 100% rename from runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/minimization.json rename to runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json similarity index 100% rename from runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json rename to runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json new file mode 100644 index 0000000..a7cd064 --- /dev/null +++ b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"farmer_id":"FR-8888"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},{"destination_district_open":false,"herd_vaccination_current":false,"no_conflicting_open_movement_permit":false,"origin_district_not_quarantined_for_species":false,"registered_herd":false}],"headers":{},"status":200}}}],"name":"livestock-movement-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},"headers":{},"status":200}}}],"name":"livestock-movement-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"livestock-movement-no-match","variables":{}}],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/minimization.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json similarity index 100% rename from runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/minimization.json rename to runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/negative-security.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json similarity index 100% rename from runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/negative-security.json rename to runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/integration-packs/farmer-voucher-by-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json similarity index 95% rename from runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/integration-packs/farmer-voucher-by-farmer.json rename to runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json index ff96512..13be5da 100644 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/integration-packs/farmer-voucher-by-farmer.json +++ b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json @@ -1 +1 @@ -{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:6d1e0b039f6ed71b79f382260701582ef255c1b377e7d01ee942b1b9c2c53e52"],"minimization":["sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b"],"negative_security":["sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"farmer-voucher-by-farmer","output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:074f63c5eeddd3398a925e20c327d947da9b40239da0a03a13eae7efbf3ca48a"],"minimization":["sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b"],"negative_security":["sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"farmer-voucher-by-farmer","output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/integration-packs/livestock-movement-by-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json similarity index 94% rename from runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/integration-packs/livestock-movement-by-farmer.json rename to runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json index 043c50a..1733ff4 100644 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/integration-packs/livestock-movement-by-farmer.json +++ b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json @@ -1 +1 @@ -{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:10bdc960cb62d9030cee7895a45ce77a6df1ebe133f7beac2d6feb05aaf18910"],"minimization":["sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5"],"negative_security":["sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"livestock-movement-by-farmer","output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:f66511a1e088e0366836719d5011126c22599f4f4d570a24a1d7ecdb3a00dc15"],"minimization":["sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5"],"negative_security":["sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"livestock-movement-by-farmer","output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/private-bindings/voucher-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json similarity index 71% rename from runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/private-bindings/voucher-farmer.json rename to runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json index eddcecb..55b939f 100644 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/private-bindings/voucher-farmer.json +++ b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:eef622a1e97ac245493f2348cecad9b3b9245b7b0ce30d9db7ed602cc80827cd","id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"active_farm_parcel":"active_farm_parcel","active_smallholder_farmer":"active_smallholder_farmer","crop_declared_for_season":"crop_declared_for_season","data_use_authorized":"data_use_authorized","district_climate_risk_active":"district_climate_risk_active","farmer_registered":"farmer_registered","voucher_entitlement_current":"voucher_entitlement_current","voucher_not_redeemed":"voucher_not_redeemed"}},"table_provider":"nagdi_farmer__materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7"},"profile":{"id":"solmara-nagdi-agriculture.voucher.farmer","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:96e6941af8766505df657156db4249cc0504ec262992d8f802f3bbbd1e879e24","id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"active_farm_parcel":"active_farm_parcel","active_smallholder_farmer":"active_smallholder_farmer","crop_declared_for_season":"crop_declared_for_season","data_use_authorized":"data_use_authorized","district_climate_risk_active":"district_climate_risk_active","farmer_registered":"farmer_registered","voucher_entitlement_current":"voucher_entitlement_current","voucher_not_redeemed":"voucher_not_redeemed"}},"table_provider":"nagdi_farmer__materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7"},"profile":{"id":"solmara-nagdi-agriculture.nagdi-farmer-consultation.farmer","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/private-bindings/livestock-herd.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json similarity index 71% rename from runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/private-bindings/livestock-herd.json rename to runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json index 7db8adf..8c76bd0 100644 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/private-bindings/livestock-herd.json +++ b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:9d1daa45060e9f5c073be4a42f175c398907d7f00c650554aeddeedb9bd94092","id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"destination_district_open":"destination_district_open","herd_vaccination_current":"herd_vaccination_current","no_conflicting_open_movement_permit":"no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species":"origin_district_not_quarantined_for_species","registered_herd":"registered_herd"}},"table_provider":"nagdi_herd__materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b"},"profile":{"id":"solmara-nagdi-agriculture.livestock.herd","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:1c7445affde1963d641ab519a4bc7d0ea83e3c718b055e7f9bd868f707530a4a","id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"destination_district_open":"destination_district_open","herd_vaccination_current":"herd_vaccination_current","no_conflicting_open_movement_permit":"no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species":"origin_district_not_quarantined_for_species","registered_herd":"registered_herd"}},"table_provider":"nagdi_herd__materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b"},"profile":{"id":"solmara-nagdi-agriculture.nagdi-herd-consultation.herd","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/relay.yaml b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/relay.yaml new file mode 100644 index 0000000..5a284b2 --- /dev/null +++ b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/relay.yaml @@ -0,0 +1,351 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + path: /var/lib/registry/audit/audit.jsonl + sink: file +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - solmara-consultation + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://nagdi-relay.solmara.registrystack.org + publisher: solmara-nagdi-agriculture + title: solmara-nagdi-agriculture governed Registry Relay +consultation: + artifacts: + evidence: + - class: conformance + path: artifacts/evidence/farmer-voucher-by-farmer/conformance.json + sha256: sha256:074f63c5eeddd3398a925e20c327d947da9b40239da0a03a13eae7efbf3ca48a + - class: minimization + path: artifacts/evidence/livestock-movement-by-farmer/minimization.json + sha256: sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5 + - class: negative_security + path: artifacts/evidence/farmer-voucher-by-farmer/negative-security.json + sha256: sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d + - class: negative_security + path: artifacts/evidence/livestock-movement-by-farmer/negative-security.json + sha256: sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce + - class: conformance + path: artifacts/evidence/livestock-movement-by-farmer/conformance.json + sha256: sha256:f66511a1e088e0366836719d5011126c22599f4f4d570a24a1d7ecdb3a00dc15 + - class: minimization + path: artifacts/evidence/farmer-voucher-by-farmer/minimization.json + sha256: sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b + integration_packs: + - hash: sha256:96e6941af8766505df657156db4249cc0504ec262992d8f802f3bbbd1e879e24 + path: artifacts/integration-packs/farmer-voucher-by-farmer.json + sha256: sha256:4ae71cf8b62ea65e9e8e6cd505288532c0b815f4c2c1b32bf678f43d1e03f73a + - hash: sha256:1c7445affde1963d641ab519a4bc7d0ea83e3c718b055e7f9bd868f707530a4a + path: artifacts/integration-packs/livestock-movement-by-farmer.json + sha256: sha256:1edb2e9b8c975e08cbd73f13b86aa73a261f6b68168272ebc9609c49f202b088 + private_bindings: + - hash: sha256:0b1df60778d3ee3367038e1c9a1e5a7cb1f757efa1dc1381431358263fe1a304 + path: artifacts/private-bindings/nagdi-farmer-consultation-farmer.json + sha256: sha256:6b87ded9da5ac89da7d3d87e3a7f01fa71ebb2fa94c39579bd3b7f7acfebb06c + - hash: sha256:b58bf7470fc6339907c00cc4f6fd36c50f8330b16e92802f562bc49663c6d2f9 + path: artifacts/private-bindings/nagdi-herd-consultation-herd.json + sha256: sha256:ad14fa29847e7bf583e8626be3879af2eab79199b6ba08b3ed139494c6b72855 + public_contracts: + - hash: sha256:4f7b86b90a4165ad82d0168d99b367f143743f753dbc027ac497c9f0e1c1eb43 + path: artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json + sha256: sha256:25f7a255324189ea717a24f40ac3c325c7167aebc256db01b1c847a2b1acdbcf + - hash: sha256:1fe461df217f93da31fbe20bcd6ad38ca752e091ca3fe20388153c3365d1b409 + path: artifacts/consultation-contracts/nagdi-herd-consultation-herd.json + sha256: sha256:21e8a0ed388b1b497d4ee2229ddba6f7acbc0b316b3541a8572a478c38c67393 + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: solmara-consultation + principal_id: solmara-consultation + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 4554467022379962639 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 7450858971948805253 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Purpose-limited farmer facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: nagdi_farmer:aggregate + evidence_verification_scope: nagdi_farmer:evidence_verification + metadata_scope: nagdi_farmer:metadata + read_scope: nagdi_farmer:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: farmer_id + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited farmer facts used by the Solmara Evidence service. + fields: + - from: active_farm_parcel + name: active_farm_parcel + sensitive: true + - from: active_smallholder_farmer + name: active_smallholder_farmer + sensitive: true + - from: crop_declared_for_season + name: crop_declared_for_season + sensitive: true + - from: data_use_authorized + name: data_use_authorized + sensitive: true + - from: district_climate_risk_active + name: district_climate_risk_active + sensitive: true + - from: farmer_id + name: farmer_id + sensitive: true + - from: farmer_registered + name: farmer_registered + sensitive: true + - from: voucher_entitlement_current + name: voucher_entitlement_current + sensitive: true + - from: voucher_not_redeemed + name: voucher_not_redeemed + sensitive: true + name: nagdi_farmer + relationships: [] + spatial: null + table: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 + title: NAgDI farmer records + id: nagdi_farmer + owner: National Agriculture Data and Digital Innovation Agency + sensitivity: personal + tables: + - access: + aggregate_scope: nagdi_farmer:aggregate + metadata_scope: nagdi_farmer:metadata + aggregates: [] + api: + allowed_filters: [] + default_limit: 2 + max_limit: 2 + require_purpose_header: true + id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 + materialization: snapshot + primary_key: farmer_id + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: active_farm_parcel + nullable: false + sensitive: true + type: boolean + - name: active_smallholder_farmer + nullable: false + sensitive: true + type: boolean + - name: crop_declared_for_season + nullable: false + sensitive: true + type: boolean + - name: data_use_authorized + nullable: false + sensitive: true + type: boolean + - name: district_climate_risk_active + nullable: false + sensitive: true + type: boolean + - name: farmer_id + nullable: false + sensitive: true + type: string + - name: farmer_registered + nullable: false + sensitive: true + type: boolean + - name: voucher_entitlement_current + nullable: false + sensitive: true + type: boolean + - name: voucher_not_redeemed + nullable: false + sensitive: true + type: boolean + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv + type: file + title: NAgDI farmer records + update_frequency: continuous +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Purpose-limited herd facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: nagdi_herd:aggregate + evidence_verification_scope: nagdi_herd:evidence_verification + metadata_scope: nagdi_herd:metadata + read_scope: nagdi_herd:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: farmer_id + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/livestock-movement-control + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited herd facts used by the Solmara Evidence service. + fields: + - from: destination_district_open + name: destination_district_open + sensitive: true + - from: farmer_id + name: farmer_id + sensitive: true + - from: herd_id + name: herd_id + sensitive: true + - from: herd_vaccination_current + name: herd_vaccination_current + sensitive: true + - from: no_conflicting_open_movement_permit + name: no_conflicting_open_movement_permit + sensitive: true + - from: origin_district_not_quarantined_for_species + name: origin_district_not_quarantined_for_species + sensitive: true + - from: registered_herd + name: registered_herd + sensitive: true + name: nagdi_herd + relationships: [] + spatial: null + table: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b + title: NAgDI herd records + id: nagdi_herd + owner: National Agriculture Data and Digital Innovation Agency + sensitivity: personal + tables: + - access: + aggregate_scope: nagdi_herd:aggregate + metadata_scope: nagdi_herd:metadata + aggregates: [] + api: + allowed_filters: [] + default_limit: 2 + max_limit: 2 + require_purpose_header: true + id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b + materialization: snapshot + primary_key: herd_id + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: destination_district_open + nullable: false + sensitive: true + type: boolean + - name: farmer_id + nullable: false + sensitive: true + type: string + - name: herd_id + nullable: false + sensitive: true + type: string + - name: herd_vaccination_current + nullable: false + sensitive: true + type: boolean + - name: no_conflicting_open_movement_permit + nullable: false + sensitive: true + type: boolean + - name: origin_district_not_quarantined_for_species + nullable: false + sensitive: true + type: boolean + - name: registered_herd + nullable: false + sensitive: true + type: boolean + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv + type: file + title: NAgDI herd records + update_frequency: continuous +deployment: + profile: hosted_lab +instance: + environment: hosted + id: nagdi-agriculture-relay-consultation +server: + bind: 0.0.0.0:8080 +standards: {} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/consultation-contracts/livestock-herd.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/consultation-contracts/livestock-herd.json deleted file mode 100644 index adf39dd..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/consultation-contracts/livestock-herd.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.livestock.herd","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:05746b9128c73f3a079b88d4a6a5c392ea4d317755aa8e0771e0c83325a16b28","id":"relay.livestock.herd","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/livestock-movement-control"],"required_scope":"registry:consult:livestock","workload":"nagdi-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["destination_district_open","herd_vaccination_current","no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species","registered_herd"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/consultation-contracts/voucher-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/consultation-contracts/voucher-farmer.json deleted file mode 100644 index 3f414ad..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/consultation-contracts/voucher-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.voucher.farmer","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:6c9b13e328828747f1d32991be3ab0681112c283dae9f614a7187c8ca0d72a01","id":"relay.voucher.farmer","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"],"required_scope":"registry:consult:voucher","workload":"nagdi-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["active_farm_parcel","active_smallholder_farmer","crop_declared_for_season","data_use_authorized","district_climate_risk_active","farmer_registered","voucher_entitlement_current","voucher_not_redeemed"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/conformance.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/conformance.json deleted file mode 100644 index 4743085..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"data-use-authorized-for-purpose":true,"eligible-for-climate-smart-input-voucher":true,"farmer-registered":true},"error":null,"outcome":"match","outputs":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true},"headers":{},"status":200}}}],"name":"farmer-voucher-match","request":{"claims":[{"id":"farmer-registered"},{"id":"data-use-authorized-for-purpose"},{"id":"eligible-for-climate-smart-input-voucher"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/voucher-eligibility-review","target":{"identifiers":[{"scheme":"farmer_id","value":"FR-1001"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"data-use-authorized-for-purpose":false,"eligible-for-climate-smart-input-voucher":false,"farmer-registered":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"farmer-voucher-no-match","request":null,"variables":{}}],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/conformance.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/conformance.json deleted file mode 100644 index 25a9c51..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"farmer_id":"FR-8888"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},{"destination_district_open":false,"herd_vaccination_current":false,"no_conflicting_open_movement_permit":false,"origin_district_not_quarantined_for_species":false,"registered_herd":false}],"headers":{},"status":200}}}],"name":"livestock-movement-ambiguous","request":null,"variables":{}},{"classification":"synthetic","expect":{"claims":{"eligible-for-livestock-movement-permit":true,"origin-district-not-quarantined-for-species":true,"registered-herd":true},"error":null,"outcome":"match","outputs":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},"headers":{},"status":200}}}],"name":"livestock-movement-match","request":{"claims":[{"id":"registered-herd"},{"id":"origin-district-not-quarantined-for-species"},{"id":"eligible-for-livestock-movement-permit"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/livestock-movement-control","target":{"identifiers":[{"scheme":"farmer_id","value":"FR-1001"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"eligible-for-livestock-movement-permit":false,"origin-district-not-quarantined-for-species":false,"registered-herd":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"livestock-movement-no-match","request":null,"variables":{}}],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay-consultation.yaml b/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay-consultation.yaml deleted file mode 100644 index ab91080..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay-consultation.yaml +++ /dev/null @@ -1,234 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - nagdi-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nagdi-relay.solmara.registrystack.org - publisher: solmara-nagdi-agriculture - title: solmara-nagdi-agriculture governed Registry Relay -consultation: - artifacts: - evidence: - - class: conformance - path: artifacts/evidence/livestock-movement-by-farmer/conformance.json - sha256: sha256:10bdc960cb62d9030cee7895a45ce77a6df1ebe133f7beac2d6feb05aaf18910 - - class: minimization - path: artifacts/evidence/livestock-movement-by-farmer/minimization.json - sha256: sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5 - - class: conformance - path: artifacts/evidence/farmer-voucher-by-farmer/conformance.json - sha256: sha256:6d1e0b039f6ed71b79f382260701582ef255c1b377e7d01ee942b1b9c2c53e52 - - class: negative_security - path: artifacts/evidence/farmer-voucher-by-farmer/negative-security.json - sha256: sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d - - class: negative_security - path: artifacts/evidence/livestock-movement-by-farmer/negative-security.json - sha256: sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce - - class: minimization - path: artifacts/evidence/farmer-voucher-by-farmer/minimization.json - sha256: sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b - integration_packs: - - hash: sha256:eef622a1e97ac245493f2348cecad9b3b9245b7b0ce30d9db7ed602cc80827cd - path: artifacts/integration-packs/farmer-voucher-by-farmer.json - sha256: sha256:ed6f77b36df0d70ad9ffea72fdfedc21cc0a3621db3cdf4ea70d18f8960db12c - - hash: sha256:9d1daa45060e9f5c073be4a42f175c398907d7f00c650554aeddeedb9bd94092 - path: artifacts/integration-packs/livestock-movement-by-farmer.json - sha256: sha256:7af1198e4ba477344621f4361613c5ba1d0d30bf1a2bc94e94b7a7c5d8e8ca16 - private_bindings: - - hash: sha256:f319d52859657a0a1acaabba01034558012eeeb11e36e406d9def07a72814416 - path: artifacts/private-bindings/livestock-herd.json - sha256: sha256:33efab2ec578e7baf3dc70c7a6c139eb76a5cd80f4a2c753fc474b795477d306 - - hash: sha256:04ea387eadcc2279e7c791241c46b77f41dcbb02bd15919a3e246370f3443714 - path: artifacts/private-bindings/voucher-farmer.json - sha256: sha256:75a3c0379a33f5c7029cf91ebb87a42c661a5e8ddd93c5fb7edf37f0f5f51d9b - public_contracts: - - hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 - path: artifacts/consultation-contracts/livestock-herd.json - sha256: sha256:a1f4045c36462881aec560fcf103c1f78252bf0e2acf81ddd6b75ce21ccfd84c - - hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c - path: artifacts/consultation-contracts/voucher-farmer.json - sha256: sha256:8ea73ba2779ef33a85cc5f34988fe0d14f13f1892baa185d0d5c67e76dcbe1ad - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: nagdi-notary - principal_id: nagdi-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4554467022379962639 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 7450858971948805253 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_farmer entity - entities: [] - id: nagdi_farmer - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_farmer:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - materialization: snapshot - primary_key: farmer_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: active_farm_parcel - nullable: false - sensitive: true - type: boolean - - name: active_smallholder_farmer - nullable: false - sensitive: true - type: boolean - - name: crop_declared_for_season - nullable: false - sensitive: true - type: boolean - - name: data_use_authorized - nullable: false - sensitive: true - type: boolean - - name: district_climate_risk_active - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: farmer_registered - nullable: false - sensitive: true - type: boolean - - name: voucher_entitlement_current - nullable: false - sensitive: true - type: boolean - - name: voucher_not_redeemed - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - type: file - title: nagdi_farmer - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_herd entity - entities: [] - id: nagdi_herd - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_herd:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - materialization: snapshot - primary_key: herd_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: destination_district_open - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: herd_id - nullable: false - sensitive: true - type: string - - name: herd_vaccination_current - nullable: false - sensitive: true - type: boolean - - name: no_conflicting_open_movement_permit - nullable: false - sensitive: true - type: boolean - - name: origin_district_not_quarantined_for_species - nullable: false - sensitive: true - type: boolean - - name: registered_herd - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - type: file - title: nagdi_herd - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nagdi-agriculture-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml b/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml index d8d88d5..5262a3a 100644 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml +++ b/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: false allowed_clients: - - nagdi-notary + - solmara-evidence audiences: - registry-relay issuer: https://workload-issuer.solmara.registrystack.org @@ -24,21 +25,82 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized nagdi_farmer entity - entities: [] + description: Purpose-limited farmer facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: nagdi_farmer:aggregate + evidence_verification_scope: nagdi_farmer:evidence_verification + metadata_scope: nagdi_farmer:metadata + read_scope: nagdi_farmer:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: farmer_id + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited farmer facts used by the Solmara Evidence service. + fields: + - from: active_farm_parcel + name: active_farm_parcel + sensitive: true + - from: active_smallholder_farmer + name: active_smallholder_farmer + sensitive: true + - from: crop_declared_for_season + name: crop_declared_for_season + sensitive: true + - from: data_use_authorized + name: data_use_authorized + sensitive: true + - from: district_climate_risk_active + name: district_climate_risk_active + sensitive: true + - from: farmer_id + name: farmer_id + sensitive: true + - from: farmer_registered + name: farmer_registered + sensitive: true + - from: voucher_entitlement_current + name: voucher_entitlement_current + sensitive: true + - from: voucher_not_redeemed + name: voucher_not_redeemed + sensitive: true + name: nagdi_farmer + relationships: [] + spatial: null + table: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 + title: NAgDI farmer records id: nagdi_farmer - owner: solmara-nagdi-agriculture + owner: National Agriculture Data and Digital Innovation Agency sensitivity: personal tables: - access: aggregate_scope: nagdi_farmer:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: nagdi_farmer:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 materialization: snapshot primary_key: farmer_id @@ -92,8 +154,8 @@ datasets: quote: null path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv type: file - title: nagdi_farmer - update_frequency: as_needed + title: NAgDI farmer records + update_frequency: continuous - access_rights: restricted aggregates: [] conforms_to: [] @@ -102,21 +164,76 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized nagdi_herd entity - entities: [] + description: Purpose-limited herd facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: nagdi_herd:aggregate + evidence_verification_scope: nagdi_herd:evidence_verification + metadata_scope: nagdi_herd:metadata + read_scope: nagdi_herd:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: farmer_id + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/livestock-movement-control + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited herd facts used by the Solmara Evidence service. + fields: + - from: destination_district_open + name: destination_district_open + sensitive: true + - from: farmer_id + name: farmer_id + sensitive: true + - from: herd_id + name: herd_id + sensitive: true + - from: herd_vaccination_current + name: herd_vaccination_current + sensitive: true + - from: no_conflicting_open_movement_permit + name: no_conflicting_open_movement_permit + sensitive: true + - from: origin_district_not_quarantined_for_species + name: origin_district_not_quarantined_for_species + sensitive: true + - from: registered_herd + name: registered_herd + sensitive: true + name: nagdi_herd + relationships: [] + spatial: null + table: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b + title: NAgDI herd records id: nagdi_herd - owner: solmara-nagdi-agriculture + owner: National Agriculture Data and Digital Innovation Agency sensitivity: personal tables: - access: aggregate_scope: nagdi_herd:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: nagdi_herd:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b materialization: snapshot primary_key: herd_id @@ -162,13 +279,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv type: file - title: nagdi_herd - update_frequency: as_needed + title: NAgDI herd records + update_frequency: continuous deployment: profile: hosted_lab instance: environment: hosted id: nagdi-agriculture-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/hosted/nia-population/notary/notary.yaml b/runtime/registry-projects/hosted/nia-population/notary/notary.yaml deleted file mode 100644 index e99568f..0000000 --- a/runtime/registry-projects/hosted/nia-population/notary/notary.yaml +++ /dev/null @@ -1,152 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH - provider: env - id: child-benefit-federator - scopes: - - evidence:nia:child-benefit:read - - fingerprint: - name: NIA_CITIZEN_CLIENT_TOKEN_HASH - provider: env - id: citizen-portal - scopes: - - evidence:nia:citizen:read -deployment: - profile: hosted_lab -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - claims: - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - population: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - alive: - nullable: false - type: boolean - identity_status: - max_bytes: 128 - nullable: false - type: string - profile: - contract_hash: sha256:2b34d7466e4f45126055cdb3ec18026a6f0cf3051a863ad951eedfe6ed729958 - id: solmara-nia-population.nia-child-benefit.population - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: population-record-active - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - required_scopes: - - evidence:nia:child-benefit:read - rule: - bindings: {} - expression: |- - population.matched && population.identity_status == 'active' - && population.alive == true - type: cel - subject_type: person - title: population record active - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - nia-citizen-status.citizen-population-status - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - population: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - alive: - nullable: false - type: boolean - identity_status: - max_bytes: 128 - nullable: false - type: string - profile: - contract_hash: sha256:9433a86942bdfbb1add5e45e2c3462eb224a2e8097be0267a52eeaaecfde0c69 - id: solmara-nia-population.nia-citizen-status.population - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: citizen-population-record-active - purpose: https://id.registrystack.org/solmara/purpose/citizen-self-service - required_scopes: - - evidence:nia:citizen:read - rule: - bindings: {} - expression: |- - population.matched && population.identity_status == 'active' - && population.alive == true - type: cel - subject_type: person - title: citizen population record active - value: - nullable: false - type: boolean - version: '1' - credential_profiles: - nia-citizen-status.citizen-population-status: - allowed_claims: - - citizen-population-record-active - disclosure: - allowed: - - value - - predicate - - redacted - format: application/dc+sd-jwt - issuer: did:web:id.registrystack.org:solmara:authority:nia - signing_key: project-issuer - validity_seconds: 600 - vct: https://id.registrystack.org/solmara/vct/citizen-population-status - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/nia-notary-relay-token - workload_client_id: nia-notary - service_id: nia-notary - signing_keys: - project-issuer: - alg: EdDSA - kid: did:web:id.registrystack.org:solmara:authority:nia#issuer-key-1 - private_jwk_env: NIA_NOTARY_ISSUER_JWK - provider: local_jwk_env - status: active - variables: {} -instance: - environment: hosted - id: nia-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json new file mode 100644 index 0000000..6614b16 --- /dev/null +++ b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json @@ -0,0 +1 @@ +{"id":"solmara-nia-population.nia-population-consultation.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:c962257eac077820b96cd886c1319dd1e741ef4b4bd0f81efbe6cfb8e9105fb2","id":"relay.nia-population-consultation.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:nia-population-consultation","workload":"solmara-esignet"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json new file mode 100644 index 0000000..bf29091 --- /dev/null +++ b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"alive":true,"identity_status":"active"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"alive":true,"identity_status":"active"},"headers":{},"status":200}}}],"name":"nia-population-person-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"nia-population-person-no-match","variables":{}}],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nia-population/relay/artifacts/evidence/population-person-by-uin/minimization.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json similarity index 100% rename from runtime/registry-projects/hosted/nia-population/relay/artifacts/evidence/population-person-by-uin/minimization.json rename to runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json diff --git a/runtime/registry-projects/hosted/nia-population/relay/artifacts/evidence/population-person-by-uin/negative-security.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json similarity index 100% rename from runtime/registry-projects/hosted/nia-population/relay/artifacts/evidence/population-person-by-uin/negative-security.json rename to runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json diff --git a/runtime/registry-projects/local/nia-population/relay/artifacts/integration-packs/population-person-by-uin.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json similarity index 93% rename from runtime/registry-projects/local/nia-population/relay/artifacts/integration-packs/population-person-by-uin.json rename to runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json index 7430026..035dab4 100644 --- a/runtime/registry-projects/local/nia-population/relay/artifacts/integration-packs/population-person-by-uin.json +++ b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json @@ -1 +1 @@ -{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:bae5ef90128eb60a3ef4a3eeea3bea815e73bf6ccfdd521a48a3043446bd5fbd"],"minimization":["sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407"],"negative_security":["sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-nia-population-person-by-uin","output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":300000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:4f346c55aef3cfc11d5e9db10ca32efd54eac449f16ad2e16a76fbc5bd97f171"],"minimization":["sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407"],"negative_security":["sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-nia-population-person-by-uin","output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":300000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nia-population/relay/artifacts/private-bindings/nia-child-benefit-population.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json similarity index 60% rename from runtime/registry-projects/hosted/nia-population/relay/artifacts/private-bindings/nia-child-benefit-population.json rename to runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json index 9d14494..4369a15 100644 --- a/runtime/registry-projects/hosted/nia-population/relay/artifacts/private-bindings/nia-child-benefit-population.json +++ b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:72b42442af5dffa0ad96ef2bb6266fd422ae1f16299b3fc82dbd3bf3fd9b58e9","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d"},"profile":{"id":"solmara-nia-population.nia-child-benefit.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d","tenant":"solmara-nia-population"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:e4c64f00a246783ba2afc3e47bb775d0d5f5c2794c72ea622f8e02bf96792c8d","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d"},"profile":{"id":"solmara-nia-population.nia-population-consultation.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d","tenant":"solmara-nia-population"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nia-population/relay/relay-consultation.yaml b/runtime/registry-projects/hosted/nia-population/relay-consultation/relay.yaml similarity index 83% rename from runtime/registry-projects/hosted/nia-population/relay/relay-consultation.yaml rename to runtime/registry-projects/hosted/nia-population/relay-consultation/relay.yaml index 8b7c257..6d68f2c 100644 --- a/runtime/registry-projects/hosted/nia-population/relay/relay-consultation.yaml +++ b/runtime/registry-projects/hosted/nia-population/relay-consultation/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: false allowed_clients: - - nia-notary + - solmara-consultation audiences: - registry-relay issuer: https://workload-issuer.solmara.registrystack.org @@ -18,33 +19,27 @@ catalog: consultation: artifacts: evidence: + - class: conformance + path: artifacts/evidence/population-person-by-uin/conformance.json + sha256: sha256:4f346c55aef3cfc11d5e9db10ca32efd54eac449f16ad2e16a76fbc5bd97f171 - class: minimization path: artifacts/evidence/population-person-by-uin/minimization.json sha256: sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407 - - class: conformance - path: artifacts/evidence/population-person-by-uin/conformance.json - sha256: sha256:bae5ef90128eb60a3ef4a3eeea3bea815e73bf6ccfdd521a48a3043446bd5fbd - class: negative_security path: artifacts/evidence/population-person-by-uin/negative-security.json sha256: sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab integration_packs: - - hash: sha256:72b42442af5dffa0ad96ef2bb6266fd422ae1f16299b3fc82dbd3bf3fd9b58e9 + - hash: sha256:e4c64f00a246783ba2afc3e47bb775d0d5f5c2794c72ea622f8e02bf96792c8d path: artifacts/integration-packs/population-person-by-uin.json - sha256: sha256:83e0e332c81c331e2329535fdf1983be1427359a0e1bdf16986b8bc4a2f24b3e + sha256: sha256:aabbcc69617b62fbb2ad9d0cecdaa1d27f8020147d5fa8f9e9605bbf237a8f05 private_bindings: - - hash: sha256:fa33b620ee699baa8efb7f233dc46884826b4e4b64940a0fa27cca5b1871bb40 - path: artifacts/private-bindings/nia-child-benefit-population.json - sha256: sha256:41dd7eba725d043adbdfbfcc7a95178364970c5b8f76525fa75db87c31d228b6 - - hash: sha256:286382e48365cf203f3b0b5712bb6c8f94d18d33657078dc2522e42afb76aa8f - path: artifacts/private-bindings/nia-citizen-status-population.json - sha256: sha256:89e52d120d4c5ab0fc451e74e2fe0bb11966e7e1e4ab59505713d78f567ae8b7 + - hash: sha256:aa9e728f3b414efc56026b5cf4129ea1299869e20d93159929ed3f2593aa493e + path: artifacts/private-bindings/nia-population-consultation-population.json + sha256: sha256:be7924bf0dfd91d9cf734f6d72866bc686e35fcbfdf4e0732a7519cad96ad59c public_contracts: - - hash: sha256:2b34d7466e4f45126055cdb3ec18026a6f0cf3051a863ad951eedfe6ed729958 - path: artifacts/consultation-contracts/nia-child-benefit-population.json - sha256: sha256:43e56a4eaff600b7fbb8ca382f6d00bee299dcd9e528f50de89143ad9f3502f3 - - hash: sha256:9433a86942bdfbb1add5e45e2c3462eb224a2e8097be0267a52eeaaecfde0c69 - path: artifacts/consultation-contracts/nia-citizen-status-population.json - sha256: sha256:3ecdd494bdec2c8b591c24385a3c808d49a9b9562f9809e1a7dd24dcc66693a1 + - hash: sha256:eb210bd4c1c1110a465fb00e4e3da3d91e8b2b4320243750a6d92729a7bb6add + path: artifacts/consultation-contracts/nia-population-consultation-population.json + sha256: sha256:92937e0c5bf6ce83e92a40b492551a59798d2f3600592eb307cce2393675865c rhai_scripts: [] audit_pseudonym_materials: - key_id: epoch-1 @@ -54,8 +49,8 @@ consultation: authorized_workload: audience: registry-relay client_claim_selector: azp - client_value: nia-notary - principal_id: nia-notary + client_value: solmara-consultation + principal_id: solmara-consultation source_credentials: [] state_plane: audit_pseudonym_keyring_lock_key: 1016450306001272100 @@ -95,6 +90,8 @@ datasets: permitted_jurisdictions: [] permitted_purposes: - https://id.registrystack.org/solmara/purpose/esignet-identity-verification + - https://id.registrystack.org/solmara/purpose/child-benefit-review + - https://id.registrystack.org/solmara/purpose/citizen-self-service redaction_fields: [] require_consent: false require_legal_basis: false @@ -247,5 +244,5 @@ instance: environment: hosted id: nia-population-relay-consultation server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/hosted/nia-population/relay/artifacts/consultation-contracts/nia-child-benefit-population.json b/runtime/registry-projects/hosted/nia-population/relay/artifacts/consultation-contracts/nia-child-benefit-population.json deleted file mode 100644 index 90ad104..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay/artifacts/consultation-contracts/nia-child-benefit-population.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.nia-child-benefit.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:7af974a445cf4ebaec93647f7d6aa80732d70829150702ab0891c06f91850359","id":"relay.nia-child-benefit.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:nia-child-benefit","workload":"nia-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nia-population/relay/artifacts/consultation-contracts/nia-citizen-status-population.json b/runtime/registry-projects/hosted/nia-population/relay/artifacts/consultation-contracts/nia-citizen-status-population.json deleted file mode 100644 index 08977c4..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay/artifacts/consultation-contracts/nia-citizen-status-population.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.nia-citizen-status.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"citizen-self-service","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:0f648848f4934f37c2dbce9e58040e2ce7b43d39a5d1c8d0391cdd4aa07b9834","id":"relay.nia-citizen-status.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/citizen-self-service"],"required_scope":"registry:consult:nia-citizen-status","workload":"nia-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nia-population/relay/artifacts/evidence/population-person-by-uin/conformance.json b/runtime/registry-projects/hosted/nia-population/relay/artifacts/evidence/population-person-by-uin/conformance.json deleted file mode 100644 index 752267e..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay/artifacts/evidence/population-person-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"citizen-population-record-active":true,"population-record-active":true},"error":null,"outcome":"match","outputs":{"alive":true,"identity_status":"active"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"alive":true,"identity_status":"active"},"headers":{},"status":200}}}],"name":"nia-population-person-match","request":{"claims":[{"id":"population-record-active"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"citizen-population-record-active":false,"population-record-active":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"nia-population-person-no-match","request":{"claims":[{"id":"citizen-population-record-active"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/citizen-self-service","target":{"identifiers":[{"scheme":"solmara_uin","value":"9999999999"}],"type":"Person"}},"variables":{}}],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nia-population/relay/relay.yaml b/runtime/registry-projects/hosted/nia-population/relay/relay.yaml index 49e1a9b..560b5bb 100644 --- a/runtime/registry-projects/hosted/nia-population/relay/relay.yaml +++ b/runtime/registry-projects/hosted/nia-population/relay/relay.yaml @@ -1,12 +1,14 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: false allowed_clients: - solmara-esignet + - solmara-evidence audiences: - registry-relay issuer: https://workload-issuer.solmara.registrystack.org @@ -47,6 +49,8 @@ datasets: permitted_jurisdictions: [] permitted_purposes: - https://id.registrystack.org/solmara/purpose/esignet-identity-verification + - https://id.registrystack.org/solmara/purpose/child-benefit-review + - https://id.registrystack.org/solmara/purpose/citizen-self-service redaction_fields: [] require_consent: false require_legal_basis: false @@ -199,5 +203,5 @@ instance: environment: hosted id: nia-population-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml b/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml deleted file mode 100644 index 8750911..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml +++ /dev/null @@ -1,136 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: SIPF_PENSION_CLIENT_TOKEN_HASH - provider: env - id: pension-review-desk - scopes: - - evidence:sipf:pension-payment:read - - evidence:sipf:survivor-benefit:read -deployment: - profile: hosted_lab -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/pension-payment-review - - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - claims: - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - pension: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - payment_status: - max_bytes: 128 - nullable: false - type: string - profile: - contract_hash: sha256:04deb5f47932e163e61b8f7ce9c4d4adff54a7558128edeb367518962abde1bd - id: solmara-sipf-pensions.sipf-pension-payment-review.pension - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: pension-payment-active - purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review - required_scopes: - - evidence:sipf:pension-payment:read - rule: - bindings: {} - expression: pension.matched && pension.payment_status == 'active' - type: cel - subject_type: person - title: pension payment active - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - sipf-survivor-benefit.survivor-benefit-status - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - survivor: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - survivor_eligible: - nullable: false - type: boolean - profile: - contract_hash: sha256:de1c2295d58905dc34046288edf36263fcccf99cf684deaa885ab76f472ed6d6 - id: solmara-sipf-pensions.sipf-survivor-benefit.survivor - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: survivor-is-eligible - purpose: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - required_scopes: - - evidence:sipf:survivor-benefit:read - rule: - bindings: {} - expression: survivor.matched && survivor.survivor_eligible == true - type: cel - subject_type: person - title: survivor is eligible - value: - nullable: false - type: boolean - version: '1' - credential_profiles: - sipf-survivor-benefit.survivor-benefit-status: - allowed_claims: - - survivor-is-eligible - disclosure: - allowed: - - value - - predicate - - redacted - format: application/dc+sd-jwt - issuer: did:web:id.registrystack.org:solmara:authority:sipf - signing_key: project-issuer - validity_seconds: 600 - vct: https://id.registrystack.org/solmara/vct/survivor-benefit-status - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/sipf-notary-relay-token - workload_client_id: sipf-notary - service_id: sipf-notary - signing_keys: - project-issuer: - alg: EdDSA - kid: did:web:id.registrystack.org:solmara:authority:sipf#issuer-key-1 - private_jwk_env: SIPF_NOTARY_ISSUER_JWK - provider: local_jwk_env - status: active - variables: {} -instance: - environment: hosted - id: sipf-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json new file mode 100644 index 0000000..5b5d627 --- /dev/null +++ b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json @@ -0,0 +1 @@ +{"id":"solmara-sipf-pensions.sipf-pension-consultation.pension","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:39b4002812d2babf57a59302f2fc9680ac05e9439e57e74bc02b8d0dbcea3f0a","id":"relay.sipf-pension-consultation.pension","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:sipf-pension-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["payment_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json new file mode 100644 index 0000000..8e4fcd1 --- /dev/null +++ b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json @@ -0,0 +1 @@ +{"id":"solmara-sipf-pensions.sipf-survivor-consultation.survivor","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:99c1d7d89c1d4abe175bd0693a123c881dff1775da7e78099a5158396de11d58","id":"relay.sipf-survivor-consultation.survivor","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"],"required_scope":"registry:consult:sipf-survivor-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["survivor_eligible"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json new file mode 100644 index 0000000..d7d17b1 --- /dev/null +++ b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"payment_status":"active"},{"payment_status":"suspended"}],"headers":{},"status":200}}}],"name":"sipf-pension-case-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"payment_status":"active"}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"payment_status":"active"},"headers":{},"status":200}}}],"name":"sipf-pension-case-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-pension-case-no-match","variables":{}}],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/minimization.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json similarity index 100% rename from runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/minimization.json rename to runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/negative-security.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json similarity index 100% rename from runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/negative-security.json rename to runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json new file mode 100644 index 0000000..53ca6c1 --- /dev/null +++ b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"survivor_eligible":true},{"survivor_eligible":false}],"headers":{},"status":200}}}],"name":"sipf-survivor-case-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"survivor_eligible":true}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"survivor_eligible":true},"headers":{},"status":200}}}],"name":"sipf-survivor-case-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-survivor-case-no-match","variables":{}}],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/minimization.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json similarity index 100% rename from runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/minimization.json rename to runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/negative-security.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json similarity index 100% rename from runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/negative-security.json rename to runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-pensioner.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json similarity index 92% rename from runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-pensioner.json rename to runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json index 7fac5ec..ded8a18 100644 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-pensioner.json +++ b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json @@ -1 +1 @@ -{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:34d1119e31d3a2e22571b9532541d057874aebebbde363ed80a137bce5a6772e"],"minimization":["sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660"],"negative_security":["sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-pensioner","output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:baac4ff492615cf355beaa403abaad465a23d3552fd6ed3682dce4898e829370"],"minimization":["sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660"],"negative_security":["sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-pensioner","output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-survivor.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json similarity index 92% rename from runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-survivor.json rename to runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json index a0e0dda..2cd8a30 100644 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-survivor.json +++ b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json @@ -1 +1 @@ -{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:a1f2073fc39da5b7a4c52bbd6d8f7a414e719b5c9dcf293509afac4d27f39059"],"minimization":["sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f"],"negative_security":["sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-survivor","output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:7b0462b4a2d4592cd1aeb8cee70fb54b66a5d9cd7c0aae0d07ee4c160f56d84a"],"minimization":["sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f"],"negative_security":["sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-survivor","output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json similarity index 61% rename from runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json rename to runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json index 2b3b78b..94672b6 100644 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json +++ b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:ebcce823eb11489afd04cce13a550317ae7f9b9ef6c503bd68490d26085ab73e","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"pensioner_uin","physical_type":"utf8"}},"projection":{"payment_status":"payment_status"}},"table_provider":"pension__materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21"},"profile":{"id":"solmara-sipf-pensions.sipf-pension-payment-review.pension","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21","tenant":"solmara-sipf-pensions"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:6fe615f0be4b84d28497b7c6111c5426094af8199955977b881e9ebc7c34daa7","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"pensioner_uin","physical_type":"utf8"}},"projection":{"payment_status":"payment_status"}},"table_provider":"pension__materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21"},"profile":{"id":"solmara-sipf-pensions.sipf-pension-consultation.pension","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json similarity index 67% rename from runtime/registry-projects/local/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json rename to runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json index 8485dcd..b3c1c47 100644 --- a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json +++ b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:f4416975dcbe4c9eaea878107ea9e97a10b1fd9e02484b1d5b498b5c805d7235","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"spouse_uin","physical_type":"utf8"}},"projection":{"survivor_eligible":"survivor_eligible"}},"table_provider":"survivor__materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539"},"profile":{"id":"solmara-sipf-pensions.sipf-survivor-benefit.survivor","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539","tenant":"solmara-sipf-pensions"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:0aabcdecb0f1cc221e8d4949cdc613c1ecec9c396f0f85b22f24ca8810c92106","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"spouse_uin","physical_type":"utf8"}},"projection":{"survivor_eligible":"survivor_eligible"}},"table_provider":"survivor__materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539"},"profile":{"id":"solmara-sipf-pensions.sipf-survivor-consultation.survivor","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/relay.yaml b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/relay.yaml new file mode 100644 index 0000000..3903746 --- /dev/null +++ b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/relay.yaml @@ -0,0 +1,263 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + path: /var/lib/registry/audit/audit.jsonl + sink: file +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - solmara-consultation + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://sipf-relay.solmara.registrystack.org + publisher: solmara-sipf-pensions + title: solmara-sipf-pensions governed Registry Relay +consultation: + artifacts: + evidence: + - class: minimization + path: artifacts/evidence/pension-case-by-survivor/minimization.json + sha256: sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f + - class: negative_security + path: artifacts/evidence/pension-case-by-survivor/negative-security.json + sha256: sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7 + - class: minimization + path: artifacts/evidence/pension-case-by-pensioner/minimization.json + sha256: sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660 + - class: conformance + path: artifacts/evidence/pension-case-by-survivor/conformance.json + sha256: sha256:7b0462b4a2d4592cd1aeb8cee70fb54b66a5d9cd7c0aae0d07ee4c160f56d84a + - class: conformance + path: artifacts/evidence/pension-case-by-pensioner/conformance.json + sha256: sha256:baac4ff492615cf355beaa403abaad465a23d3552fd6ed3682dce4898e829370 + - class: negative_security + path: artifacts/evidence/pension-case-by-pensioner/negative-security.json + sha256: sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d + integration_packs: + - hash: sha256:6fe615f0be4b84d28497b7c6111c5426094af8199955977b881e9ebc7c34daa7 + path: artifacts/integration-packs/pension-case-by-pensioner.json + sha256: sha256:fc850bad2f6cec2f1b514c3005b92588c62c3f95cefc9d8419061d27cecd649a + - hash: sha256:0aabcdecb0f1cc221e8d4949cdc613c1ecec9c396f0f85b22f24ca8810c92106 + path: artifacts/integration-packs/pension-case-by-survivor.json + sha256: sha256:fac45aff1350bdfc5ec7f7e1741e02c02f74d5ac22505fa65d5b769a8a0697f8 + private_bindings: + - hash: sha256:4ed2016d95b43505d541bf2c91fa0ea952879457e5fce8c66fcd66a178ce57b4 + path: artifacts/private-bindings/sipf-pension-consultation-pension.json + sha256: sha256:872343722e004f2c420535877d15f50477726533c999bcd41d3529513654903d + - hash: sha256:4ff43cbb6fe5281a24a498a1cdb7bbd552ea3868c6aee27872df41fc11f37e2b + path: artifacts/private-bindings/sipf-survivor-consultation-survivor.json + sha256: sha256:f05668bc4724b8acabeadcf95eac837405da292196d1fb8d0e84d5f871bf683c + public_contracts: + - hash: sha256:67a23c52de56b492a69784ec0a57bde7f6772f5a8cbe3f7e450d5bf86a52fd13 + path: artifacts/consultation-contracts/sipf-pension-consultation-pension.json + sha256: sha256:46bdc7b1bf6fb6a2b83c5697bdc690831025b8da1c190ead0471769707c1f30a + - hash: sha256:0b57c5799c75c6a6717f35b045d76189a436aa124157faa070318a2f8a405253 + path: artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json + sha256: sha256:c0b833e6687c0af54df01acf34eda3e39eaef0ff42de2119bd5773e22e547b54 + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: solmara-consultation + principal_id: solmara-consultation + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 4444991830243562918 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 6117019989203477116 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Purpose-limited pension facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: pension:aggregate + evidence_verification_scope: pension:evidence_verification + metadata_scope: pension:metadata + read_scope: pension:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: pensioner_uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/pension-payment-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited pension facts used by the Solmara Evidence service. + fields: + - from: payment_status + name: payment_status + sensitive: true + - from: pensioner_uin + name: pensioner_uin + sensitive: true + name: pension + relationships: [] + spatial: null + table: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 + title: SIPF pension payment records + id: pension + owner: Social Insurance and Pension Fund + sensitivity: personal + tables: + - access: + aggregate_scope: pension:aggregate + metadata_scope: pension:metadata + aggregates: [] + api: + allowed_filters: [] + default_limit: 2 + max_limit: 2 + require_purpose_header: true + id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 + materialization: snapshot + primary_key: pensioner_uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: payment_status + nullable: false + sensitive: true + type: string + - name: pensioner_uin + nullable: false + sensitive: true + type: string + strict: true + source: + connection_env: SOLMARA_SIPF_DATABASE_URL + table: + name: sipf_pension_payment + schema: public + type: postgres + title: SIPF pension payment records + update_frequency: continuous +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Purpose-limited survivor facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: survivor:aggregate + evidence_verification_scope: survivor:evidence_verification + metadata_scope: survivor:metadata + read_scope: survivor:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: spouse_uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited survivor facts used by the Solmara Evidence service. + fields: + - from: spouse_uin + name: spouse_uin + sensitive: true + - from: survivor_eligible + name: survivor_eligible + sensitive: true + name: survivor + relationships: [] + spatial: null + table: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 + title: SIPF survivor benefit records + id: survivor + owner: Social Insurance and Pension Fund + sensitivity: personal + tables: + - access: + aggregate_scope: survivor:aggregate + metadata_scope: survivor:metadata + aggregates: [] + api: + allowed_filters: [] + default_limit: 2 + max_limit: 2 + require_purpose_header: true + id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 + materialization: snapshot + primary_key: spouse_uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: spouse_uin + nullable: false + sensitive: true + type: string + - name: survivor_eligible + nullable: false + sensitive: true + type: boolean + strict: true + source: + connection_env: SOLMARA_SIPF_DATABASE_URL + table: + name: sipf_survivor_benefit + schema: public + type: postgres + title: SIPF survivor benefit records + update_frequency: continuous +deployment: + profile: hosted_lab +instance: + environment: hosted + id: sipf-pensions-relay-consultation +server: + bind: 0.0.0.0:8080 +standards: {} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json deleted file mode 100644 index ad9440e..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.sipf-pension-payment-review.pension","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:96e56b4b170376860f623777977f7c72a19cd960854a48f568e1e92ca33f85e7","id":"relay.sipf-pension-payment-review.pension","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:sipf-pension-payment-review","workload":"sipf-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["payment_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json deleted file mode 100644 index a4fd9c0..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.sipf-survivor-benefit.survivor","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:ec6b5310716606dac48159abec1e512757fb9a18bff8a6e0ac634db932b2696b","id":"relay.sipf-survivor-benefit.survivor","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"],"required_scope":"registry:consult:sipf-survivor-benefit","workload":"sipf-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["survivor_eligible"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/conformance.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/conformance.json deleted file mode 100644 index 8babb7e..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"payment_status":"active"},{"payment_status":"suspended"}],"headers":{},"status":200}}}],"name":"sipf-pension-case-ambiguous","request":null,"variables":{}},{"classification":"synthetic","expect":{"claims":{"pension-payment-active":true},"error":null,"outcome":"match","outputs":{"payment_status":"active"}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"payment_status":"active"},"headers":{},"status":200}}}],"name":"sipf-pension-case-match","request":{"claims":[{"id":"pension-payment-active"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/pension-payment-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300109568"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"pension-payment-active":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-pension-case-no-match","request":null,"variables":{}}],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/conformance.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/conformance.json deleted file mode 100644 index e33e4f9..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"survivor_eligible":true},{"survivor_eligible":false}],"headers":{},"status":200}}}],"name":"sipf-survivor-case-ambiguous","request":null,"variables":{}},{"classification":"synthetic","expect":{"claims":{"survivor-is-eligible":true},"error":null,"outcome":"match","outputs":{"survivor_eligible":true}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"survivor_eligible":true},"headers":{},"status":200}}}],"name":"sipf-survivor-case-match","request":{"claims":[{"id":"survivor-is-eligible"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/survivor-benefit-determination","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300118698"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"survivor-is-eligible":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-survivor-case-no-match","request":null,"variables":{}}],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/relay-consultation.yaml b/runtime/registry-projects/hosted/sipf-pensions/relay/relay-consultation.yaml deleted file mode 100644 index 280b116..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/relay-consultation.yaml +++ /dev/null @@ -1,182 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - sipf-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sipf-relay.solmara.registrystack.org - publisher: solmara-sipf-pensions - title: solmara-sipf-pensions governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/pension-case-by-survivor/minimization.json - sha256: sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f - - class: conformance - path: artifacts/evidence/pension-case-by-pensioner/conformance.json - sha256: sha256:34d1119e31d3a2e22571b9532541d057874aebebbde363ed80a137bce5a6772e - - class: negative_security - path: artifacts/evidence/pension-case-by-survivor/negative-security.json - sha256: sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7 - - class: minimization - path: artifacts/evidence/pension-case-by-pensioner/minimization.json - sha256: sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660 - - class: conformance - path: artifacts/evidence/pension-case-by-survivor/conformance.json - sha256: sha256:a1f2073fc39da5b7a4c52bbd6d8f7a414e719b5c9dcf293509afac4d27f39059 - - class: negative_security - path: artifacts/evidence/pension-case-by-pensioner/negative-security.json - sha256: sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d - integration_packs: - - hash: sha256:ebcce823eb11489afd04cce13a550317ae7f9b9ef6c503bd68490d26085ab73e - path: artifacts/integration-packs/pension-case-by-pensioner.json - sha256: sha256:6252d74d83d4074d4eee0ae60cb2d1aac357c1a0a56664ab92120933ad0e77b3 - - hash: sha256:f4416975dcbe4c9eaea878107ea9e97a10b1fd9e02484b1d5b498b5c805d7235 - path: artifacts/integration-packs/pension-case-by-survivor.json - sha256: sha256:ca28e59689fa5e842f1435cd8c7bf922117ff2afc7bf3af08788781fcf458168 - private_bindings: - - hash: sha256:928e8a72b68fcdf9fe3ae4381cc71918d78a5c8f8bd138be09df42528a6fdcd3 - path: artifacts/private-bindings/sipf-pension-payment-review-pension.json - sha256: sha256:9499ffd554e9d00acdf62509e3dad500834050c4abff6ce0b1cc299fa35b7828 - - hash: sha256:5b46b40895d8fc69247e473ff117e119a08453121f5ecc9215e1297961ae701d - path: artifacts/private-bindings/sipf-survivor-benefit-survivor.json - sha256: sha256:37a68f42bf4870f69eb4faf6c820049c868951b49edbbe99483e61ae2cbde463 - public_contracts: - - hash: sha256:04deb5f47932e163e61b8f7ce9c4d4adff54a7558128edeb367518962abde1bd - path: artifacts/consultation-contracts/sipf-pension-payment-review-pension.json - sha256: sha256:75211b8be4be68778d4a0aa044fa46778b507017028dfa1b43c9330f9700a167 - - hash: sha256:de1c2295d58905dc34046288edf36263fcccf99cf684deaa885ab76f472ed6d6 - path: artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json - sha256: sha256:6bebf5122b4248dbc3eaefc5e3f7de1705796e30a1b8011b8c0fc3adf2c255f9 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: sipf-notary - principal_id: sipf-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4444991830243562918 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 6117019989203477116 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized pension entity - entities: [] - id: pension - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: pension:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - materialization: snapshot - primary_key: pensioner_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: payment_status - nullable: false - sensitive: true - type: string - - name: pensioner_uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_pension_payment - schema: public - type: postgres - title: pension - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized survivor entity - entities: [] - id: survivor - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: survivor:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - materialization: snapshot - primary_key: spouse_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: spouse_uin - nullable: false - sensitive: true - type: string - - name: survivor_eligible - nullable: false - sensitive: true - type: boolean - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_survivor_benefit - schema: public - type: postgres - title: survivor - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sipf-pensions-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/relay.yaml b/runtime/registry-projects/hosted/sipf-pensions/relay/relay.yaml index 5a2c71f..a9bf6fd 100644 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/relay.yaml +++ b/runtime/registry-projects/hosted/sipf-pensions/relay/relay.yaml @@ -1,11 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: false - allowed_clients: [] + allowed_clients: + - solmara-evidence audiences: - registry-relay issuer: https://workload-issuer.solmara.registrystack.org @@ -23,21 +25,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized pension entity - entities: [] + description: Purpose-limited pension facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: pension:aggregate + evidence_verification_scope: pension:evidence_verification + metadata_scope: pension:metadata + read_scope: pension:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: pensioner_uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/pension-payment-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited pension facts used by the Solmara Evidence service. + fields: + - from: payment_status + name: payment_status + sensitive: true + - from: pensioner_uin + name: pensioner_uin + sensitive: true + name: pension + relationships: [] + spatial: null + table: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 + title: SIPF pension payment records id: pension - owner: solmara-sipf-pensions + owner: Social Insurance and Pension Fund sensitivity: personal tables: - access: aggregate_scope: pension:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: pension:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 materialization: snapshot primary_key: pensioner_uin @@ -61,8 +103,8 @@ datasets: name: sipf_pension_payment schema: public type: postgres - title: pension - update_frequency: as_needed + title: SIPF pension payment records + update_frequency: continuous - access_rights: restricted aggregates: [] conforms_to: [] @@ -71,21 +113,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized survivor entity - entities: [] + description: Purpose-limited survivor facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: survivor:aggregate + evidence_verification_scope: survivor:evidence_verification + metadata_scope: survivor:metadata + read_scope: survivor:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: spouse_uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited survivor facts used by the Solmara Evidence service. + fields: + - from: spouse_uin + name: spouse_uin + sensitive: true + - from: survivor_eligible + name: survivor_eligible + sensitive: true + name: survivor + relationships: [] + spatial: null + table: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 + title: SIPF survivor benefit records id: survivor - owner: solmara-sipf-pensions + owner: Social Insurance and Pension Fund sensitivity: personal tables: - access: aggregate_scope: survivor:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: survivor:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 materialization: snapshot primary_key: spouse_uin @@ -109,13 +191,13 @@ datasets: name: sipf_survivor_benefit schema: public type: postgres - title: survivor - update_frequency: as_needed + title: SIPF survivor benefit records + update_frequency: continuous deployment: profile: hosted_lab instance: environment: hosted id: sipf-pensions-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/hosted/sro-social/notary/notary.yaml b/runtime/registry-projects/hosted/sro-social/notary/notary.yaml deleted file mode 100644 index 06d03f4..0000000 --- a/runtime/registry-projects/hosted/sro-social/notary/notary.yaml +++ /dev/null @@ -1,77 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH - provider: env - id: child-benefit-federator - scopes: - - sro_social:evidence_verification -deployment: - profile: hosted_lab -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - claims: - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - household: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - poverty_band: - max_bytes: 64 - nullable: false - type: string - profile: - contract_hash: sha256:7c6800ab1eea861c2e2500ba3e93bb9ddcb6d1212d7701756098155faaee4697 - id: solmara-sro-social.child-benefit.household - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: household-below-poverty-threshold - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - required_scopes: - - sro_social:evidence_verification - rule: - bindings: {} - expression: household.matched && household.poverty_band == "priority" - type: cel - subject_type: person - title: household below poverty threshold - value: - nullable: false - type: boolean - version: '1' - credential_profiles: {} - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/sro-notary-relay-token - workload_client_id: sro-notary - service_id: sro-notary - variables: {} -instance: - environment: hosted - id: sro-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json new file mode 100644 index 0000000..769fe52 --- /dev/null +++ b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json @@ -0,0 +1 @@ +{"id":"solmara-sro-social.sro-household-consultation.household","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:f6d2a35ee331166669b6ae0eecd3ad084de203e4da00827a9368a9fb70f5f180","id":"relay.sro-household-consultation.household","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:sro-household-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sro-social.child-benefit-household-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["poverty_band"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":2678400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json new file mode 100644 index 0000000..7b8ebae --- /dev/null +++ b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"poverty_band":"priority"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"poverty_band":"priority"},"headers":{},"status":200}}}],"name":"child-benefit-household-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"child-benefit-household-no-match","variables":{}}],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/minimization.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json similarity index 100% rename from runtime/registry-projects/hosted/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/minimization.json rename to runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json diff --git a/runtime/registry-projects/hosted/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/negative-security.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json similarity index 100% rename from runtime/registry-projects/hosted/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/negative-security.json rename to runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json diff --git a/runtime/registry-projects/hosted/sro-social/relay/artifacts/integration-packs/child-benefit-household-by-uin.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json similarity index 92% rename from runtime/registry-projects/hosted/sro-social/relay/artifacts/integration-packs/child-benefit-household-by-uin.json rename to runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json index 7133664..257709e 100644 --- a/runtime/registry-projects/hosted/sro-social/relay/artifacts/integration-packs/child-benefit-household-by-uin.json +++ b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json @@ -1 +1 @@ -{"id":"solmara-sro-social.child-benefit-household-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:d85f20f78036bb44a72ae05846565b2d7a705f92bb5836a75285c71ef63da9ce"],"minimization":["sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0"],"negative_security":["sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"child-benefit-household-by-uin","output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":2678400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-sro-social.child-benefit-household-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:a4d045756396f4c69a9607b88163f42efcb7baf55f46d7049b1555e850ed0070"],"minimization":["sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0"],"negative_security":["sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"child-benefit-household-by-uin","output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":2678400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sro-social/relay/artifacts/private-bindings/child-benefit-household.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json similarity index 63% rename from runtime/registry-projects/local/sro-social/relay/artifacts/private-bindings/child-benefit-household.json rename to runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json index e46d4f8..f598671 100644 --- a/runtime/registry-projects/local/sro-social/relay/artifacts/private-bindings/child-benefit-household.json +++ b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:7f32b8cf5cee550693f4c1a99d5859d251bbb54c6840c56a4c7ec1cf63c6462e","id":"solmara-sro-social.child-benefit-household-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"poverty_band":"poverty_band"}},"table_provider":"sro_child__materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d"},"profile":{"id":"solmara-sro-social.child-benefit.household","version":"1"},"registry_instance":"solmara-sro-social","source_instance":"materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d","tenant":"solmara-sro-social"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:47547a81285fa1f655d534d521062e47f86bff6c2a74cb9b3637fd51f2943441","id":"solmara-sro-social.child-benefit-household-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"poverty_band":"poverty_band"}},"table_provider":"sro_child__materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d"},"profile":{"id":"solmara-sro-social.sro-household-consultation.household","version":"1"},"registry_instance":"solmara-sro-social","source_instance":"materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d","tenant":"solmara-sro-social"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sro-social/relay/relay-consultation.yaml b/runtime/registry-projects/hosted/sro-social/relay-consultation/relay.yaml similarity index 53% rename from runtime/registry-projects/hosted/sro-social/relay/relay-consultation.yaml rename to runtime/registry-projects/hosted/sro-social/relay-consultation/relay.yaml index dcacafe..30c10dc 100644 --- a/runtime/registry-projects/hosted/sro-social/relay/relay-consultation.yaml +++ b/runtime/registry-projects/hosted/sro-social/relay-consultation/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: false allowed_clients: - - sro-notary + - solmara-consultation audiences: - registry-relay issuer: https://workload-issuer.solmara.registrystack.org @@ -26,19 +27,19 @@ consultation: sha256: sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0 - class: conformance path: artifacts/evidence/child-benefit-household-by-uin/conformance.json - sha256: sha256:d85f20f78036bb44a72ae05846565b2d7a705f92bb5836a75285c71ef63da9ce + sha256: sha256:a4d045756396f4c69a9607b88163f42efcb7baf55f46d7049b1555e850ed0070 integration_packs: - - hash: sha256:7f32b8cf5cee550693f4c1a99d5859d251bbb54c6840c56a4c7ec1cf63c6462e + - hash: sha256:47547a81285fa1f655d534d521062e47f86bff6c2a74cb9b3637fd51f2943441 path: artifacts/integration-packs/child-benefit-household-by-uin.json - sha256: sha256:bbf3bfb05e1f6020bdc14457db9549ad084ad421b5c59715bdb51d5ea67dc4d3 + sha256: sha256:b1cbeacef1a39cb0d4cb03a4adf1061b91532e04acf92abf1e06f60c0b84f1f0 private_bindings: - - hash: sha256:4dd783c0c8bcf6b6fee3aefd511c719dab911053adf3f39874cc99deded3bcfb - path: artifacts/private-bindings/child-benefit-household.json - sha256: sha256:9515fa0b01838a1f07f25b2b86b75843e8ef888438b82989f5ab7d657e2865ba + - hash: sha256:77a0bae801d725304c75e307f26f53753fb50f9cd72fd9f9bc501da37bfd1064 + path: artifacts/private-bindings/sro-household-consultation-household.json + sha256: sha256:2440ac8ae4c483d106e13af61a8b072509d2452b13a3a36f192b97421ece0bf1 public_contracts: - - hash: sha256:7c6800ab1eea861c2e2500ba3e93bb9ddcb6d1212d7701756098155faaee4697 - path: artifacts/consultation-contracts/child-benefit-household.json - sha256: sha256:77470bdaa56f3bf30beb1734e0aa0fd676068b09f45d576ce32cae4a51920efb + - hash: sha256:2009a7b48ceb64aaf24a028af795a3e2fd399fad618db5fd6b1b65cbb8bb281e + path: artifacts/consultation-contracts/sro-household-consultation-household.json + sha256: sha256:662eb6af3e5c5308eedb27ba57575399d2377dbf65d2dd9041520ee21bd8088b rhai_scripts: [] audit_pseudonym_materials: - key_id: epoch-1 @@ -48,8 +49,8 @@ consultation: authorized_workload: audience: registry-relay client_claim_selector: azp - client_value: sro-notary - principal_id: sro-notary + client_value: solmara-consultation + principal_id: solmara-consultation source_credentials: [] state_plane: audit_pseudonym_keyring_lock_key: 368637434756268187 @@ -66,21 +67,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized sro_child entity - entities: [] + description: Purpose-limited household facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: sro_child:aggregate + evidence_verification_scope: sro_child:evidence_verification + metadata_scope: sro_child:metadata + read_scope: sro_child:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited household facts used by the Solmara Evidence service. + fields: + - from: poverty_band + name: poverty_band + sensitive: true + - from: uin + name: uin + sensitive: true + name: sro_child + relationships: [] + spatial: null + table: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d + title: SRO household records id: sro_child - owner: solmara-sro-social + owner: Social Registry Office sensitivity: personal tables: - access: aggregate_scope: sro_child:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: sro_child:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d materialization: snapshot primary_key: uin @@ -106,13 +147,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv type: file - title: sro_child - update_frequency: as_needed + title: SRO household records + update_frequency: continuous deployment: profile: hosted_lab instance: environment: hosted id: sro-social-relay-consultation server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/hosted/sro-social/relay/artifacts/consultation-contracts/child-benefit-household.json b/runtime/registry-projects/hosted/sro-social/relay/artifacts/consultation-contracts/child-benefit-household.json deleted file mode 100644 index e8bbb22..0000000 --- a/runtime/registry-projects/hosted/sro-social/relay/artifacts/consultation-contracts/child-benefit-household.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sro-social.child-benefit.household","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:1f2bc3ace6f53ef9e6aa82974582fdd31113142f164f5afaf8328dafe39d92d3","id":"relay.child-benefit.household","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:child-benefit","workload":"sro-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sro-social.child-benefit-household-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["poverty_band"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":2678400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/conformance.json b/runtime/registry-projects/hosted/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/conformance.json deleted file mode 100644 index 6a52524..0000000 --- a/runtime/registry-projects/hosted/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"household-below-poverty-threshold":true},"error":null,"outcome":"match","outputs":{"poverty_band":"priority"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"poverty_band":"priority"},"headers":{},"status":200}}}],"name":"child-benefit-household-match","request":{"claims":[{"id":"household-below-poverty-threshold"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"household-below-poverty-threshold":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"child-benefit-household-no-match","request":null,"variables":{}}],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sro-social/relay/relay.yaml b/runtime/registry-projects/hosted/sro-social/relay/relay.yaml index fe45cb9..dee4b56 100644 --- a/runtime/registry-projects/hosted/sro-social/relay/relay.yaml +++ b/runtime/registry-projects/hosted/sro-social/relay/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: false allowed_clients: - - sro-notary + - solmara-evidence audiences: - registry-relay issuer: https://workload-issuer.solmara.registrystack.org @@ -24,21 +25,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized sro_child entity - entities: [] + description: Purpose-limited household facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: sro_child:aggregate + evidence_verification_scope: sro_child:evidence_verification + metadata_scope: sro_child:metadata + read_scope: sro_child:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited household facts used by the Solmara Evidence service. + fields: + - from: poverty_band + name: poverty_band + sensitive: true + - from: uin + name: uin + sensitive: true + name: sro_child + relationships: [] + spatial: null + table: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d + title: SRO household records id: sro_child - owner: solmara-sro-social + owner: Social Registry Office sensitivity: personal tables: - access: aggregate_scope: sro_child:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: sro_child:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d materialization: snapshot primary_key: uin @@ -64,13 +105,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv type: file - title: sro_child - update_frequency: as_needed + title: SRO household records + update_frequency: continuous deployment: profile: hosted_lab instance: environment: hosted id: sro-social-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/local/cra-civil/notary/notary.yaml b/runtime/registry-projects/local/cra-civil/notary/notary.yaml deleted file mode 100644 index 8f6c6f8..0000000 --- a/runtime/registry-projects/local/cra-civil/notary/notary.yaml +++ /dev/null @@ -1,235 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH - provider: env - id: child-benefit-federator - scopes: - - evidence:cra:child-benefit:read - - fingerprint: - name: CRA_CITIZEN_CLIENT_TOKEN_HASH - provider: env - id: citizen-portal - scopes: - - evidence:cra:citizen:read - - fingerprint: - name: CRA_PENSION_CLIENT_TOKEN_HASH - provider: env - id: pension-review-desk - scopes: - - evidence:cra:pension:read -cel: - worker_memory_bytes: 1073741824 -deployment: - profile: local -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - - https://id.registrystack.org/solmara/purpose/pension-payment-review - claims: - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - civil: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - birth_brn: - max_bytes: 128 - nullable: true - type: string - birth_date: - nullable: false - type: date - deceased: - nullable: false - type: boolean - profile: - contract_hash: sha256:35acae8dfe5b6cc1c18f9aedbc188c93ded49a46553b7ab21451eb772e26918e - id: solmara-cra-civil.cra-child-benefit.civil - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: birth-is-registered - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - required_scopes: - - evidence:cra:child-benefit:read - rule: - bindings: {} - expression: 'civil.matched && civil.birth_brn != null ? civil.birth_brn != '''' : false' - type: cel - subject_type: person - title: birth is registered - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - civil: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - birth_brn: - max_bytes: 128 - nullable: true - type: string - birth_date: - nullable: false - type: date - deceased: - nullable: false - type: boolean - profile: - contract_hash: sha256:35acae8dfe5b6cc1c18f9aedbc188c93ded49a46553b7ab21451eb772e26918e - id: solmara-cra-civil.cra-child-benefit.civil - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: child-age-under-5 - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - required_scopes: - - evidence:cra:child-benefit:read - rule: - bindings: {} - expression: |- - civil.matched && civil.birth_date != null && civil.deceased == false - ? date.age_on(civil.birth_date, as_of_date) >= 0 - && date.age_on(civil.birth_date, as_of_date) < 5 - : false - type: cel - subject_type: person - title: child age under 5 - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - civil: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - birth_brn: - max_bytes: 128 - nullable: true - type: string - birth_date: - nullable: false - type: date - deceased: - nullable: false - type: boolean - profile: - contract_hash: sha256:c101d11d7fe66efe8400330851d65e4e105be62eae6ae9325eb43a83daae8aed - id: solmara-cra-civil.cra-citizen-record.civil - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: civil-record-linked - purpose: https://id.registrystack.org/solmara/purpose/citizen-self-service - required_scopes: - - evidence:cra:citizen:read - rule: - bindings: {} - expression: civil.matched - type: cel - subject_type: person - title: civil record linked - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - civil: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - birth_brn: - max_bytes: 128 - nullable: true - type: string - birth_date: - nullable: false - type: date - deceased: - nullable: false - type: boolean - profile: - contract_hash: sha256:80486f20e013dd4a99af89a22efe3c83638f37f3f79ea7c912a8d5b237db2924 - id: solmara-cra-civil.cra-pension-death.civil - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: person-is-deceased - purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review - required_scopes: - - evidence:cra:pension:read - rule: - bindings: {} - expression: civil.matched && civil.deceased == true - type: cel - subject_type: person - title: person is deceased - value: - nullable: false - type: boolean - version: '1' - credential_profiles: {} - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/cra-notary-relay-token - workload_client_id: cra-notary - service_id: cra-notary - variables: - as_of_date: - from: request.variables.as_of_date - type: date -instance: - environment: local - id: cra-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json new file mode 100644 index 0000000..1ec4b1f --- /dev/null +++ b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json @@ -0,0 +1 @@ +{"id":"solmara-cra-civil.cra-civil-consultation.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:aa167d27cfe661509fb5b8e159dcf410ea4057cf223d8adfc33bcdefaf3e2818","id":"relay.cra-civil-consultation.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:cra-civil-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json new file mode 100644 index 0000000..bfcbb9d --- /dev/null +++ b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-future-birth-date","variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-match","variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"cra-civil-person-no-match","variables":{"as_of_date":"2026-07-14"}}],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/cra-civil/relay/artifacts/evidence/civil-person-by-uin/minimization.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json similarity index 100% rename from runtime/registry-projects/local/cra-civil/relay/artifacts/evidence/civil-person-by-uin/minimization.json rename to runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json diff --git a/runtime/registry-projects/local/cra-civil/relay/artifacts/evidence/civil-person-by-uin/negative-security.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json similarity index 100% rename from runtime/registry-projects/local/cra-civil/relay/artifacts/evidence/civil-person-by-uin/negative-security.json rename to runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json diff --git a/runtime/registry-projects/local/cra-civil/relay/artifacts/integration-packs/civil-person-by-uin.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json similarity index 93% rename from runtime/registry-projects/local/cra-civil/relay/artifacts/integration-packs/civil-person-by-uin.json rename to runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json index 5533854..e80b1e7 100644 --- a/runtime/registry-projects/local/cra-civil/relay/artifacts/integration-packs/civil-person-by-uin.json +++ b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json @@ -1 +1 @@ -{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:1943d411187b226b981cddc2cd0442e73953d29cb5a047bbb38c729d78883afd"],"minimization":["sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263"],"negative_security":["sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-cra-civil-person-by-uin","output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:4348d96de834e21384de4e5d3f7e19ff10e299a7148c8d1df0739ee1d50199a3"],"minimization":["sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263"],"negative_security":["sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-cra-civil-person-by-uin","output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-citizen-record-civil.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json similarity index 67% rename from runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-citizen-record-civil.json rename to runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json index ea8fb47..cb199e7 100644 --- a/runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-citizen-record-civil.json +++ b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-citizen-record.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:487bef77452bf86b610743eadc53f87429ef9ec66c06b9ee8d41e8a0e5ef6210","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-civil-consultation.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay/relay-consultation.yaml b/runtime/registry-projects/local/cra-civil/relay-consultation/relay.yaml similarity index 51% rename from runtime/registry-projects/local/cra-civil/relay/relay-consultation.yaml rename to runtime/registry-projects/local/cra-civil/relay-consultation/relay.yaml index 797c8b3..da0511f 100644 --- a/runtime/registry-projects/local/cra-civil/relay/relay-consultation.yaml +++ b/runtime/registry-projects/local/cra-civil/relay-consultation/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: true allowed_clients: - - cra-notary + - solmara-consultation audiences: - registry-relay issuer: http://127.0.0.1:8090 @@ -18,39 +19,27 @@ catalog: consultation: artifacts: evidence: - - class: conformance - path: artifacts/evidence/civil-person-by-uin/conformance.json - sha256: sha256:1943d411187b226b981cddc2cd0442e73953d29cb5a047bbb38c729d78883afd - class: negative_security path: artifacts/evidence/civil-person-by-uin/negative-security.json sha256: sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f + - class: conformance + path: artifacts/evidence/civil-person-by-uin/conformance.json + sha256: sha256:4348d96de834e21384de4e5d3f7e19ff10e299a7148c8d1df0739ee1d50199a3 - class: minimization path: artifacts/evidence/civil-person-by-uin/minimization.json sha256: sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263 integration_packs: - - hash: sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499 + - hash: sha256:487bef77452bf86b610743eadc53f87429ef9ec66c06b9ee8d41e8a0e5ef6210 path: artifacts/integration-packs/civil-person-by-uin.json - sha256: sha256:ad85a4bbcbd9c164264afeb98c11c549cc0c54997b39572261282211ab9104b2 + sha256: sha256:975ab5565da70354b1eccb3f2f5da123e69d4983c45bcf5955cf55df6d4123cf private_bindings: - - hash: sha256:1fb0b8c2b28da70d6f22675209ced135f4855fcfdc2fd38a32aadaa0c70be0dd - path: artifacts/private-bindings/cra-child-benefit-civil.json - sha256: sha256:087d5ddfc5bbc43620f9bf499cda7343ff0b762427451f30fd3541bb7d230e22 - - hash: sha256:918b533b198e4e45e368db0b3d8737728700f4b6ee428e10a93a1a06460649b1 - path: artifacts/private-bindings/cra-citizen-record-civil.json - sha256: sha256:d564828b5a94041f3bb718c16b40a0a53ea68a94ed917bf96e5ed04f0ec8d086 - - hash: sha256:fbac0434f957c897098a1577a20ae8763258b1a1e2385a577129258dbbe86579 - path: artifacts/private-bindings/cra-pension-death-civil.json - sha256: sha256:f4eea1f1f2df7755c729d0a645778ca9805e15e3875962595c0038cc46142f51 + - hash: sha256:41e6b05846b019807b19590010bd812c5477d74228a55636b725548d21673bc4 + path: artifacts/private-bindings/cra-civil-consultation-civil.json + sha256: sha256:f5e2bd11c9c512a718bb0f82d8ba0d554b16f6da5ddf1d5f896bd536711824c5 public_contracts: - - hash: sha256:35acae8dfe5b6cc1c18f9aedbc188c93ded49a46553b7ab21451eb772e26918e - path: artifacts/consultation-contracts/cra-child-benefit-civil.json - sha256: sha256:65bfb5000e186b4e461c392454e01c76149e821551152ec79a22f28b1e15f6b1 - - hash: sha256:c101d11d7fe66efe8400330851d65e4e105be62eae6ae9325eb43a83daae8aed - path: artifacts/consultation-contracts/cra-citizen-record-civil.json - sha256: sha256:ffb8a94778c5d1015c14ecc03fd06d7f2b2d5926aca0654cc80887d930bf74d2 - - hash: sha256:80486f20e013dd4a99af89a22efe3c83638f37f3f79ea7c912a8d5b237db2924 - path: artifacts/consultation-contracts/cra-pension-death-civil.json - sha256: sha256:2fbbfadc074822821239ff13ead71115d7d64c6378e39c4d03a870eb3b6655ea + - hash: sha256:7d81ecba1b399a2caa3f8b08f27b93626f2c8b2d51ec2305874a19af34cc852b + path: artifacts/consultation-contracts/cra-civil-consultation-civil.json + sha256: sha256:28effc0ce34a4c498ff7605cde0c59c59ecd199877bbd02fb73c49be31b73f48 rhai_scripts: [] audit_pseudonym_materials: - key_id: epoch-1 @@ -60,8 +49,8 @@ consultation: authorized_workload: audience: registry-relay client_claim_selector: azp - client_value: cra-notary - principal_id: cra-notary + client_value: solmara-consultation + principal_id: solmara-consultation source_credentials: [] state_plane: audit_pseudonym_keyring_lock_key: 5222333210205388 @@ -78,21 +67,69 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized civil_people entity - entities: [] + description: Purpose-limited civil facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: civil_people:aggregate + evidence_verification_scope: civil_people:evidence_verification + metadata_scope: civil_people:metadata + read_scope: civil_people:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + - https://id.registrystack.org/solmara/purpose/pension-payment-review + - https://id.registrystack.org/solmara/purpose/citizen-self-service + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited civil facts used by the Solmara Evidence service. + fields: + - from: birth_brn + name: birth_brn + sensitive: true + - from: birth_date + name: birth_date + sensitive: true + - from: deceased + name: deceased + sensitive: true + - from: uin + name: uin + sensitive: true + name: civil_people + relationships: [] + spatial: null + table: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 + title: CRA civil records id: civil_people - owner: solmara-cra-civil + owner: Civil Registration Authority sensitivity: personal tables: - access: aggregate_scope: civil_people:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: civil_people:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 materialization: snapshot primary_key: uin @@ -126,13 +163,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv type: file - title: civil_people - update_frequency: as_needed + title: CRA civil records + update_frequency: continuous deployment: profile: local instance: environment: local id: cra-civil-relay-consultation server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-child-benefit-civil.json b/runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-child-benefit-civil.json deleted file mode 100644 index aad610b..0000000 --- a/runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-child-benefit-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-child-benefit.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:9ec50673e36783484c8e35137bd65396a871ecc12c2e8fdd2cc68b1f0cab108b","id":"relay.cra-child-benefit.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:cra-child-benefit","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-citizen-record-civil.json b/runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-citizen-record-civil.json deleted file mode 100644 index f0ef1ff..0000000 --- a/runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-citizen-record-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-citizen-record.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"citizen-self-service","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:ddf7a76ffe3dabacabdc1df18588bb87826a00a061572ad49c5052baf073f576","id":"relay.cra-citizen-record.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/citizen-self-service"],"required_scope":"registry:consult:cra-citizen-record","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-pension-death-civil.json b/runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-pension-death-civil.json deleted file mode 100644 index 2f64031..0000000 --- a/runtime/registry-projects/local/cra-civil/relay/artifacts/consultation-contracts/cra-pension-death-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-pension-death.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:dd59f03e034f68b35706ae6193bdd580081f2b596c6b92673fc7a66961e34fc1","id":"relay.cra-pension-death.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:cra-pension-death","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay/artifacts/evidence/civil-person-by-uin/conformance.json b/runtime/registry-projects/local/cra-civil/relay/artifacts/evidence/civil-person-by-uin/conformance.json deleted file mode 100644 index 9e550c6..0000000 --- a/runtime/registry-projects/local/cra-civil/relay/artifacts/evidence/civil-person-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"birth-is-registered":true,"child-age-under-5":false,"civil-record-linked":true,"person-is-deceased":false},"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-future-birth-date","request":{"claims":[{"id":"birth-is-registered"},{"id":"child-age-under-5"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"},"variables":{"as_of_date":"2026-07-14"}},"variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"claims":{"birth-is-registered":true,"child-age-under-5":true,"civil-record-linked":true,"person-is-deceased":false},"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-match","request":{"claims":[{"id":"civil-record-linked"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/citizen-self-service","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"claims":{"birth-is-registered":false,"child-age-under-5":false,"civil-record-linked":false,"person-is-deceased":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"cra-civil-person-no-match","request":{"claims":[{"id":"person-is-deceased"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/pension-payment-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"9999999999"}],"type":"Person"}},"variables":{"as_of_date":"2026-07-14"}}],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-child-benefit-civil.json b/runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-child-benefit-civil.json deleted file mode 100644 index 329227a..0000000 --- a/runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-child-benefit-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-child-benefit.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-citizen-record-civil.json b/runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-citizen-record-civil.json deleted file mode 100644 index ea8fb47..0000000 --- a/runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-citizen-record-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-citizen-record.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-pension-death-civil.json b/runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-pension-death-civil.json deleted file mode 100644 index 5b5ec3b..0000000 --- a/runtime/registry-projects/local/cra-civil/relay/artifacts/private-bindings/cra-pension-death-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-pension-death.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay/relay.yaml b/runtime/registry-projects/local/cra-civil/relay/relay.yaml index 53cef5a..63ac593 100644 --- a/runtime/registry-projects/local/cra-civil/relay/relay.yaml +++ b/runtime/registry-projects/local/cra-civil/relay/relay.yaml @@ -1,11 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: true - allowed_clients: [] + allowed_clients: + - solmara-evidence audiences: - registry-relay issuer: http://127.0.0.1:8090 @@ -23,21 +25,69 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized civil_people entity - entities: [] + description: Purpose-limited civil facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: civil_people:aggregate + evidence_verification_scope: civil_people:evidence_verification + metadata_scope: civil_people:metadata + read_scope: civil_people:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + - https://id.registrystack.org/solmara/purpose/pension-payment-review + - https://id.registrystack.org/solmara/purpose/citizen-self-service + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited civil facts used by the Solmara Evidence service. + fields: + - from: birth_brn + name: birth_brn + sensitive: true + - from: birth_date + name: birth_date + sensitive: true + - from: deceased + name: deceased + sensitive: true + - from: uin + name: uin + sensitive: true + name: civil_people + relationships: [] + spatial: null + table: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 + title: CRA civil records id: civil_people - owner: solmara-cra-civil + owner: Civil Registration Authority sensitivity: personal tables: - access: aggregate_scope: civil_people:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: civil_people:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 materialization: snapshot primary_key: uin @@ -71,13 +121,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv type: file - title: civil_people - update_frequency: as_needed + title: CRA civil records + update_frequency: continuous deployment: profile: local instance: environment: local id: cra-civil-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/local/mosd-programme/notary/notary.yaml b/runtime/registry-projects/local/mosd-programme/notary/notary.yaml deleted file mode 100644 index 133d85d..0000000 --- a/runtime/registry-projects/local/mosd-programme/notary/notary.yaml +++ /dev/null @@ -1,78 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH - provider: env - id: child-benefit-federator - scopes: - - mosd_programme:evidence_verification -cel: - worker_memory_bytes: 1073741824 -deployment: - profile: local -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - claims: - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - enrollment: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - duplicate_flag: - nullable: false - type: boolean - profile: - contract_hash: sha256:9da47bc7171991c51e098b44cb0667595b344d5f4c2b994a67f0c3b401d4b5b3 - id: solmara-mosd-programme.child-benefit.enrollment - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: not-already-enrolled - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - required_scopes: - - mosd_programme:evidence_verification - rule: - bindings: {} - expression: enrollment.matched && enrollment.duplicate_flag == false - type: cel - subject_type: person - title: not already enrolled - value: - nullable: false - type: boolean - version: '1' - credential_profiles: {} - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/programme-notary-relay-token - workload_client_id: programme-notary - service_id: programme-notary - variables: {} -instance: - environment: local - id: programme-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json new file mode 100644 index 0000000..c43521d --- /dev/null +++ b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json @@ -0,0 +1 @@ +{"id":"solmara-mosd-programme.mosd-enrollment-consultation.enrollment","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:e51568815beddf2e304dcc482daf67f7493e1cd23ff68847d22eb62c22cb6ba3","id":"relay.mosd-enrollment-consultation.enrollment","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:mosd-enrollment-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-mosd-programme.enrollment-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["duplicate_flag"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json new file mode 100644 index 0000000..e91e1d3 --- /dev/null +++ b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"duplicate_flag":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"duplicate_flag":false},"headers":{},"status":200}}}],"name":"enrollment-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"enrollment-no-match","variables":{}}],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/minimization.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json similarity index 100% rename from runtime/registry-projects/local/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/minimization.json rename to runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json diff --git a/runtime/registry-projects/local/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/negative-security.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json similarity index 100% rename from runtime/registry-projects/local/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/negative-security.json rename to runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json diff --git a/runtime/registry-projects/local/mosd-programme/relay/artifacts/integration-packs/enrollment-by-uin.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json similarity index 92% rename from runtime/registry-projects/local/mosd-programme/relay/artifacts/integration-packs/enrollment-by-uin.json rename to runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json index 729f6a0..096dbe6 100644 --- a/runtime/registry-projects/local/mosd-programme/relay/artifacts/integration-packs/enrollment-by-uin.json +++ b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json @@ -1 +1 @@ -{"id":"solmara-mosd-programme.enrollment-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:f7df3b836234ef5893833546f66bb989f0b004ca1d32c006f72a13ebb3a4c2d8"],"minimization":["sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83"],"negative_security":["sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"enrollment-by-uin","output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-mosd-programme.enrollment-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:304b2cd16ce7d97127bfb0be3b60f7c25b2624ed25fbe0219bac09c067e6a1bb"],"minimization":["sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83"],"negative_security":["sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"enrollment-by-uin","output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json similarity index 62% rename from runtime/registry-projects/hosted/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json rename to runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json index 078b94e..a4bc8da 100644 --- a/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json +++ b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:cffdf1b631e81fee923267d17c17f3001685b1b6681ddb8b93040b0c2691a6e8","id":"solmara-mosd-programme.enrollment-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"duplicate_flag":"duplicate_flag"}},"table_provider":"mosd_enroll__materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf"},"profile":{"id":"solmara-mosd-programme.child-benefit.enrollment","version":"1"},"registry_instance":"solmara-mosd-programme","source_instance":"materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf","tenant":"solmara-mosd-programme"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:d82492e2f92fca8ce1d83208d892e2a18468ef6db6afa66d467df002336a6c92","id":"solmara-mosd-programme.enrollment-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"duplicate_flag":"duplicate_flag"}},"table_provider":"mosd_enroll__materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf"},"profile":{"id":"solmara-mosd-programme.mosd-enrollment-consultation.enrollment","version":"1"},"registry_instance":"solmara-mosd-programme","source_instance":"materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf","tenant":"solmara-mosd-programme"} \ No newline at end of file diff --git a/runtime/registry-projects/local/mosd-programme/relay/relay-consultation.yaml b/runtime/registry-projects/local/mosd-programme/relay-consultation/relay.yaml similarity index 51% rename from runtime/registry-projects/local/mosd-programme/relay/relay-consultation.yaml rename to runtime/registry-projects/local/mosd-programme/relay-consultation/relay.yaml index 263cc9e..61152d8 100644 --- a/runtime/registry-projects/local/mosd-programme/relay/relay-consultation.yaml +++ b/runtime/registry-projects/local/mosd-programme/relay-consultation/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: true allowed_clients: - - programme-notary + - solmara-consultation audiences: - registry-relay issuer: http://127.0.0.1:8090 @@ -21,24 +22,24 @@ consultation: - class: minimization path: artifacts/evidence/enrollment-by-uin/minimization.json sha256: sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83 + - class: conformance + path: artifacts/evidence/enrollment-by-uin/conformance.json + sha256: sha256:304b2cd16ce7d97127bfb0be3b60f7c25b2624ed25fbe0219bac09c067e6a1bb - class: negative_security path: artifacts/evidence/enrollment-by-uin/negative-security.json sha256: sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282 - - class: conformance - path: artifacts/evidence/enrollment-by-uin/conformance.json - sha256: sha256:f7df3b836234ef5893833546f66bb989f0b004ca1d32c006f72a13ebb3a4c2d8 integration_packs: - - hash: sha256:cffdf1b631e81fee923267d17c17f3001685b1b6681ddb8b93040b0c2691a6e8 + - hash: sha256:d82492e2f92fca8ce1d83208d892e2a18468ef6db6afa66d467df002336a6c92 path: artifacts/integration-packs/enrollment-by-uin.json - sha256: sha256:40ac75a61c3346b282e3abadd16654b634ebe0ac60c77a7b4c2ac98cf5f3af50 + sha256: sha256:3b4de6e7946d91a2af1e6f0cdbc3397e7c382e960ae829ec0ddbb030f8192c5d private_bindings: - - hash: sha256:ae3674a401a41daa3325d73cd22363dba2d4c5145ae22cdc64587a6e0a293654 - path: artifacts/private-bindings/child-benefit-enrollment.json - sha256: sha256:fd8fc27359128d39dbed4cd6842d7db21f3b5eef21359f0b6bec93c0f6822f18 + - hash: sha256:2d91cd9c15c0badbb565bd16a2be9f46ac77d0ad0c827a0eea895e20edf3a3a2 + path: artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json + sha256: sha256:f27344b5e2dec067577087b23dfd4cc031e5680f4871dc028222be875f81865a public_contracts: - - hash: sha256:9da47bc7171991c51e098b44cb0667595b344d5f4c2b994a67f0c3b401d4b5b3 - path: artifacts/consultation-contracts/child-benefit-enrollment.json - sha256: sha256:253ee934a9c9bbf725e233f90c195d057893eeea14bc9bc12330c0957245ff10 + - hash: sha256:58e60de7fdae8b94a0cdf130f80c1daffb2622773aac4e60d3714f6547a7b2a5 + path: artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json + sha256: sha256:50bee564d51a566df7dcd634476a2bad80920ebfb0d88b535db85081c8558cab rhai_scripts: [] audit_pseudonym_materials: - key_id: epoch-1 @@ -48,8 +49,8 @@ consultation: authorized_workload: audience: registry-relay client_claim_selector: azp - client_value: programme-notary - principal_id: programme-notary + client_value: solmara-consultation + principal_id: solmara-consultation source_credentials: [] state_plane: audit_pseudonym_keyring_lock_key: 9069287079765840165 @@ -66,21 +67,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized mosd_enroll entity - entities: [] + description: Purpose-limited enrollment facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: mosd_enroll:aggregate + evidence_verification_scope: mosd_enroll:evidence_verification + metadata_scope: mosd_enroll:metadata + read_scope: mosd_enroll:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited enrollment facts used by the Solmara Evidence service. + fields: + - from: duplicate_flag + name: duplicate_flag + sensitive: true + - from: uin + name: uin + sensitive: true + name: mosd_enroll + relationships: [] + spatial: null + table: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf + title: MOSD programme enrollment records id: mosd_enroll - owner: solmara-mosd-programme + owner: Ministry of Social Development sensitivity: personal tables: - access: aggregate_scope: mosd_enroll:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: mosd_enroll:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf materialization: snapshot primary_key: uin @@ -106,13 +147,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv type: file - title: mosd_enroll - update_frequency: as_needed + title: MOSD programme enrollment records + update_frequency: continuous deployment: profile: local instance: environment: local id: programme-mis-relay-consultation server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/local/mosd-programme/relay/artifacts/consultation-contracts/child-benefit-enrollment.json b/runtime/registry-projects/local/mosd-programme/relay/artifacts/consultation-contracts/child-benefit-enrollment.json deleted file mode 100644 index bc41921..0000000 --- a/runtime/registry-projects/local/mosd-programme/relay/artifacts/consultation-contracts/child-benefit-enrollment.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-mosd-programme.child-benefit.enrollment","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:50a4f9fd40526b837aa90b02d27e13e98f66d9de1362eb06fb2465afbae31c53","id":"relay.child-benefit.enrollment","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:child-benefit","workload":"programme-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-mosd-programme.enrollment-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["duplicate_flag"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/conformance.json b/runtime/registry-projects/local/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/conformance.json deleted file mode 100644 index 6910195..0000000 --- a/runtime/registry-projects/local/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"not-already-enrolled":true},"error":null,"outcome":"match","outputs":{"duplicate_flag":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"duplicate_flag":false},"headers":{},"status":200}}}],"name":"enrollment-match","request":{"claims":[{"id":"not-already-enrolled"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"not-already-enrolled":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"enrollment-no-match","request":null,"variables":{}}],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/mosd-programme/relay/relay.yaml b/runtime/registry-projects/local/mosd-programme/relay/relay.yaml index bb3f817..0c130a5 100644 --- a/runtime/registry-projects/local/mosd-programme/relay/relay.yaml +++ b/runtime/registry-projects/local/mosd-programme/relay/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: true allowed_clients: - - programme-notary + - solmara-evidence audiences: - registry-relay issuer: http://127.0.0.1:8090 @@ -24,21 +25,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized mosd_enroll entity - entities: [] + description: Purpose-limited enrollment facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: mosd_enroll:aggregate + evidence_verification_scope: mosd_enroll:evidence_verification + metadata_scope: mosd_enroll:metadata + read_scope: mosd_enroll:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited enrollment facts used by the Solmara Evidence service. + fields: + - from: duplicate_flag + name: duplicate_flag + sensitive: true + - from: uin + name: uin + sensitive: true + name: mosd_enroll + relationships: [] + spatial: null + table: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf + title: MOSD programme enrollment records id: mosd_enroll - owner: solmara-mosd-programme + owner: Ministry of Social Development sensitivity: personal tables: - access: aggregate_scope: mosd_enroll:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: mosd_enroll:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf materialization: snapshot primary_key: uin @@ -64,13 +105,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv type: file - title: mosd_enroll - update_frequency: as_needed + title: MOSD programme enrollment records + update_frequency: continuous deployment: profile: local instance: environment: local id: programme-mis-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml b/runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml deleted file mode 100644 index d4f45e9..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml +++ /dev/null @@ -1,414 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: NAGDI_CLIENT_TOKEN_HASH - provider: env - id: nagdi-service - scopes: - - nagdi_agriculture:evidence_verification -cel: - worker_memory_bytes: 1073741824 -deployment: - profile: local -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/livestock-movement-control - - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - claims: - - credential_profiles: - - livestock.livestock_movement_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - herd: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - destination_district_open: - nullable: false - type: boolean - herd_vaccination_current: - nullable: false - type: boolean - no_conflicting_open_movement_permit: - nullable: false - type: boolean - origin_district_not_quarantined_for_species: - nullable: false - type: boolean - registered_herd: - nullable: false - type: boolean - profile: - contract_hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 - id: solmara-nagdi-agriculture.livestock.herd - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: eligible-for-livestock-movement-permit - purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: |- - herd.matched && herd.registered_herd - && herd.origin_district_not_quarantined_for_species - && herd.herd_vaccination_current - && herd.destination_district_open - && herd.no_conflicting_open_movement_permit - type: cel - subject_type: person - title: eligible for livestock movement permit - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - livestock.livestock_movement_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - herd: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - destination_district_open: - nullable: false - type: boolean - herd_vaccination_current: - nullable: false - type: boolean - no_conflicting_open_movement_permit: - nullable: false - type: boolean - origin_district_not_quarantined_for_species: - nullable: false - type: boolean - registered_herd: - nullable: false - type: boolean - profile: - contract_hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 - id: solmara-nagdi-agriculture.livestock.herd - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: origin-district-not-quarantined-for-species - purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: herd.matched && herd.origin_district_not_quarantined_for_species - type: cel - subject_type: person - title: origin district not quarantined for species - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - livestock.livestock_movement_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - herd: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - destination_district_open: - nullable: false - type: boolean - herd_vaccination_current: - nullable: false - type: boolean - no_conflicting_open_movement_permit: - nullable: false - type: boolean - origin_district_not_quarantined_for_species: - nullable: false - type: boolean - registered_herd: - nullable: false - type: boolean - profile: - contract_hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 - id: solmara-nagdi-agriculture.livestock.herd - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: registered-herd - purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: herd.matched && herd.registered_herd - type: cel - subject_type: person - title: registered herd - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - voucher.climate_smart_voucher_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - farmer: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - active_farm_parcel: - nullable: false - type: boolean - active_smallholder_farmer: - nullable: false - type: boolean - crop_declared_for_season: - nullable: false - type: boolean - data_use_authorized: - nullable: false - type: boolean - district_climate_risk_active: - nullable: false - type: boolean - farmer_registered: - nullable: false - type: boolean - voucher_entitlement_current: - nullable: false - type: boolean - voucher_not_redeemed: - nullable: false - type: boolean - profile: - contract_hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c - id: solmara-nagdi-agriculture.voucher.farmer - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: data-use-authorized-for-purpose - purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: farmer.matched && farmer.data_use_authorized - type: cel - subject_type: person - title: data use authorized for purpose - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - voucher.climate_smart_voucher_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - farmer: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - active_farm_parcel: - nullable: false - type: boolean - active_smallholder_farmer: - nullable: false - type: boolean - crop_declared_for_season: - nullable: false - type: boolean - data_use_authorized: - nullable: false - type: boolean - district_climate_risk_active: - nullable: false - type: boolean - farmer_registered: - nullable: false - type: boolean - voucher_entitlement_current: - nullable: false - type: boolean - voucher_not_redeemed: - nullable: false - type: boolean - profile: - contract_hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c - id: solmara-nagdi-agriculture.voucher.farmer - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: eligible-for-climate-smart-input-voucher - purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: |- - farmer.matched && farmer.farmer_registered - && farmer.data_use_authorized - && farmer.active_smallholder_farmer - && farmer.active_farm_parcel - && farmer.crop_declared_for_season - && farmer.district_climate_risk_active - && farmer.voucher_entitlement_current - && farmer.voucher_not_redeemed - type: cel - subject_type: person - title: eligible for climate smart input voucher - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - voucher.climate_smart_voucher_sd_jwt - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - farmer: - inputs: - farmer_id: request.target.identifiers.farmer_id - outputs: - active_farm_parcel: - nullable: false - type: boolean - active_smallholder_farmer: - nullable: false - type: boolean - crop_declared_for_season: - nullable: false - type: boolean - data_use_authorized: - nullable: false - type: boolean - district_climate_risk_active: - nullable: false - type: boolean - farmer_registered: - nullable: false - type: boolean - voucher_entitlement_current: - nullable: false - type: boolean - voucher_not_redeemed: - nullable: false - type: boolean - profile: - contract_hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c - id: solmara-nagdi-agriculture.voucher.farmer - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: farmer-registered - purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - required_scopes: - - nagdi_agriculture:evidence_verification - rule: - bindings: {} - expression: farmer.matched && farmer.farmer_registered - type: cel - subject_type: person - title: farmer registered - value: - nullable: false - type: boolean - version: '1' - credential_profiles: - livestock.livestock_movement_sd_jwt: - allowed_claims: - - registered-herd - - origin-district-not-quarantined-for-species - - eligible-for-livestock-movement-permit - disclosure: - allowed: - - value - - predicate - - redacted - format: application/dc+sd-jwt - issuer: did:web:id.registrystack.org:solmara:authority:nagdi - signing_key: project-issuer - validity_seconds: 600 - vct: https://id.registrystack.org/solmara/vct/livestock-movement-permit - voucher.climate_smart_voucher_sd_jwt: - allowed_claims: - - farmer-registered - - data-use-authorized-for-purpose - - eligible-for-climate-smart-input-voucher - disclosure: - allowed: - - value - - predicate - - redacted - format: application/dc+sd-jwt - issuer: did:web:id.registrystack.org:solmara:authority:nagdi - signing_key: project-issuer - validity_seconds: 600 - vct: https://id.registrystack.org/solmara/vct/climate-smart-voucher-eligibility - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/nagdi-notary-relay-token - workload_client_id: nagdi-notary - service_id: nagdi-notary - signing_keys: - project-issuer: - alg: EdDSA - kid: did:web:id.registrystack.org:solmara:authority:nagdi#issuer-key-1 - private_jwk_env: NAGDI_NOTARY_ISSUER_JWK - provider: local_jwk_env - status: active - variables: {} -instance: - environment: local - id: nagdi-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json new file mode 100644 index 0000000..8378cbe --- /dev/null +++ b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json @@ -0,0 +1 @@ +{"id":"solmara-nagdi-agriculture.nagdi-farmer-consultation.farmer","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:20dc22dcff83f4f14e0402937e8def43c5f3255241783c35cdc6a6a7afbe3005","id":"relay.nagdi-farmer-consultation.farmer","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"],"required_scope":"registry:consult:nagdi-farmer-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["active_farm_parcel","active_smallholder_farmer","crop_declared_for_season","data_use_authorized","district_climate_risk_active","farmer_registered","voucher_entitlement_current","voucher_not_redeemed"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json new file mode 100644 index 0000000..45d152e --- /dev/null +++ b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json @@ -0,0 +1 @@ +{"id":"solmara-nagdi-agriculture.nagdi-herd-consultation.herd","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:7ed0ed7964797fe81e08fcda71fe90b6fcd5846611c3422bb8ce5f6d0b4fe970","id":"relay.nagdi-herd-consultation.herd","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/livestock-movement-control"],"required_scope":"registry:consult:nagdi-herd-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["destination_district_open","herd_vaccination_current","no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species","registered_herd"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json new file mode 100644 index 0000000..78620c0 --- /dev/null +++ b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true},"headers":{},"status":200}}}],"name":"farmer-voucher-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"farmer-voucher-no-match","variables":{}}],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/minimization.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json similarity index 100% rename from runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/minimization.json rename to runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json similarity index 100% rename from runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json rename to runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json new file mode 100644 index 0000000..a7cd064 --- /dev/null +++ b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"farmer_id":"FR-8888"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},{"destination_district_open":false,"herd_vaccination_current":false,"no_conflicting_open_movement_permit":false,"origin_district_not_quarantined_for_species":false,"registered_herd":false}],"headers":{},"status":200}}}],"name":"livestock-movement-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},"headers":{},"status":200}}}],"name":"livestock-movement-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"livestock-movement-no-match","variables":{}}],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/minimization.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json similarity index 100% rename from runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/minimization.json rename to runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/negative-security.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json similarity index 100% rename from runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/negative-security.json rename to runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/integration-packs/farmer-voucher-by-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json similarity index 95% rename from runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/integration-packs/farmer-voucher-by-farmer.json rename to runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json index ff96512..13be5da 100644 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/integration-packs/farmer-voucher-by-farmer.json +++ b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json @@ -1 +1 @@ -{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:6d1e0b039f6ed71b79f382260701582ef255c1b377e7d01ee942b1b9c2c53e52"],"minimization":["sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b"],"negative_security":["sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"farmer-voucher-by-farmer","output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:074f63c5eeddd3398a925e20c327d947da9b40239da0a03a13eae7efbf3ca48a"],"minimization":["sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b"],"negative_security":["sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"farmer-voucher-by-farmer","output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/integration-packs/livestock-movement-by-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json similarity index 94% rename from runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/integration-packs/livestock-movement-by-farmer.json rename to runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json index 043c50a..1733ff4 100644 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/integration-packs/livestock-movement-by-farmer.json +++ b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json @@ -1 +1 @@ -{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:10bdc960cb62d9030cee7895a45ce77a6df1ebe133f7beac2d6feb05aaf18910"],"minimization":["sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5"],"negative_security":["sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"livestock-movement-by-farmer","output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:f66511a1e088e0366836719d5011126c22599f4f4d570a24a1d7ecdb3a00dc15"],"minimization":["sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5"],"negative_security":["sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"livestock-movement-by-farmer","output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/private-bindings/voucher-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json similarity index 71% rename from runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/private-bindings/voucher-farmer.json rename to runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json index eddcecb..55b939f 100644 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/private-bindings/voucher-farmer.json +++ b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:eef622a1e97ac245493f2348cecad9b3b9245b7b0ce30d9db7ed602cc80827cd","id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"active_farm_parcel":"active_farm_parcel","active_smallholder_farmer":"active_smallholder_farmer","crop_declared_for_season":"crop_declared_for_season","data_use_authorized":"data_use_authorized","district_climate_risk_active":"district_climate_risk_active","farmer_registered":"farmer_registered","voucher_entitlement_current":"voucher_entitlement_current","voucher_not_redeemed":"voucher_not_redeemed"}},"table_provider":"nagdi_farmer__materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7"},"profile":{"id":"solmara-nagdi-agriculture.voucher.farmer","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:96e6941af8766505df657156db4249cc0504ec262992d8f802f3bbbd1e879e24","id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"active_farm_parcel":"active_farm_parcel","active_smallholder_farmer":"active_smallholder_farmer","crop_declared_for_season":"crop_declared_for_season","data_use_authorized":"data_use_authorized","district_climate_risk_active":"district_climate_risk_active","farmer_registered":"farmer_registered","voucher_entitlement_current":"voucher_entitlement_current","voucher_not_redeemed":"voucher_not_redeemed"}},"table_provider":"nagdi_farmer__materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7"},"profile":{"id":"solmara-nagdi-agriculture.nagdi-farmer-consultation.farmer","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/private-bindings/livestock-herd.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json similarity index 71% rename from runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/private-bindings/livestock-herd.json rename to runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json index 7db8adf..8c76bd0 100644 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/private-bindings/livestock-herd.json +++ b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:9d1daa45060e9f5c073be4a42f175c398907d7f00c650554aeddeedb9bd94092","id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"destination_district_open":"destination_district_open","herd_vaccination_current":"herd_vaccination_current","no_conflicting_open_movement_permit":"no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species":"origin_district_not_quarantined_for_species","registered_herd":"registered_herd"}},"table_provider":"nagdi_herd__materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b"},"profile":{"id":"solmara-nagdi-agriculture.livestock.herd","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:1c7445affde1963d641ab519a4bc7d0ea83e3c718b055e7f9bd868f707530a4a","id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"destination_district_open":"destination_district_open","herd_vaccination_current":"herd_vaccination_current","no_conflicting_open_movement_permit":"no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species":"origin_district_not_quarantined_for_species","registered_herd":"registered_herd"}},"table_provider":"nagdi_herd__materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b"},"profile":{"id":"solmara-nagdi-agriculture.nagdi-herd-consultation.herd","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/relay.yaml b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/relay.yaml new file mode 100644 index 0000000..520e854 --- /dev/null +++ b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/relay.yaml @@ -0,0 +1,351 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + path: /var/lib/registry/audit/audit.jsonl + sink: file +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: true + allowed_clients: + - solmara-consultation + audiences: + - registry-relay + issuer: http://127.0.0.1:8090 + jwks_url: http://127.0.0.1:8090/.well-known/jwks.json +catalog: + base_url: http://127.0.0.1:8080 + publisher: solmara-nagdi-agriculture + title: solmara-nagdi-agriculture governed Registry Relay +consultation: + artifacts: + evidence: + - class: conformance + path: artifacts/evidence/farmer-voucher-by-farmer/conformance.json + sha256: sha256:074f63c5eeddd3398a925e20c327d947da9b40239da0a03a13eae7efbf3ca48a + - class: minimization + path: artifacts/evidence/livestock-movement-by-farmer/minimization.json + sha256: sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5 + - class: negative_security + path: artifacts/evidence/farmer-voucher-by-farmer/negative-security.json + sha256: sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d + - class: negative_security + path: artifacts/evidence/livestock-movement-by-farmer/negative-security.json + sha256: sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce + - class: conformance + path: artifacts/evidence/livestock-movement-by-farmer/conformance.json + sha256: sha256:f66511a1e088e0366836719d5011126c22599f4f4d570a24a1d7ecdb3a00dc15 + - class: minimization + path: artifacts/evidence/farmer-voucher-by-farmer/minimization.json + sha256: sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b + integration_packs: + - hash: sha256:96e6941af8766505df657156db4249cc0504ec262992d8f802f3bbbd1e879e24 + path: artifacts/integration-packs/farmer-voucher-by-farmer.json + sha256: sha256:4ae71cf8b62ea65e9e8e6cd505288532c0b815f4c2c1b32bf678f43d1e03f73a + - hash: sha256:1c7445affde1963d641ab519a4bc7d0ea83e3c718b055e7f9bd868f707530a4a + path: artifacts/integration-packs/livestock-movement-by-farmer.json + sha256: sha256:1edb2e9b8c975e08cbd73f13b86aa73a261f6b68168272ebc9609c49f202b088 + private_bindings: + - hash: sha256:0b1df60778d3ee3367038e1c9a1e5a7cb1f757efa1dc1381431358263fe1a304 + path: artifacts/private-bindings/nagdi-farmer-consultation-farmer.json + sha256: sha256:6b87ded9da5ac89da7d3d87e3a7f01fa71ebb2fa94c39579bd3b7f7acfebb06c + - hash: sha256:b58bf7470fc6339907c00cc4f6fd36c50f8330b16e92802f562bc49663c6d2f9 + path: artifacts/private-bindings/nagdi-herd-consultation-herd.json + sha256: sha256:ad14fa29847e7bf583e8626be3879af2eab79199b6ba08b3ed139494c6b72855 + public_contracts: + - hash: sha256:4f7b86b90a4165ad82d0168d99b367f143743f753dbc027ac497c9f0e1c1eb43 + path: artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json + sha256: sha256:25f7a255324189ea717a24f40ac3c325c7167aebc256db01b1c847a2b1acdbcf + - hash: sha256:1fe461df217f93da31fbe20bcd6ad38ca752e091ca3fe20388153c3365d1b409 + path: artifacts/consultation-contracts/nagdi-herd-consultation-herd.json + sha256: sha256:21e8a0ed388b1b497d4ee2229ddba6f7acbc0b316b3541a8572a478c38c67393 + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: solmara-consultation + principal_id: solmara-consultation + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 4554467022379962639 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 7450858971948805253 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Purpose-limited farmer facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: nagdi_farmer:aggregate + evidence_verification_scope: nagdi_farmer:evidence_verification + metadata_scope: nagdi_farmer:metadata + read_scope: nagdi_farmer:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: farmer_id + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited farmer facts used by the Solmara Evidence service. + fields: + - from: active_farm_parcel + name: active_farm_parcel + sensitive: true + - from: active_smallholder_farmer + name: active_smallholder_farmer + sensitive: true + - from: crop_declared_for_season + name: crop_declared_for_season + sensitive: true + - from: data_use_authorized + name: data_use_authorized + sensitive: true + - from: district_climate_risk_active + name: district_climate_risk_active + sensitive: true + - from: farmer_id + name: farmer_id + sensitive: true + - from: farmer_registered + name: farmer_registered + sensitive: true + - from: voucher_entitlement_current + name: voucher_entitlement_current + sensitive: true + - from: voucher_not_redeemed + name: voucher_not_redeemed + sensitive: true + name: nagdi_farmer + relationships: [] + spatial: null + table: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 + title: NAgDI farmer records + id: nagdi_farmer + owner: National Agriculture Data and Digital Innovation Agency + sensitivity: personal + tables: + - access: + aggregate_scope: nagdi_farmer:aggregate + metadata_scope: nagdi_farmer:metadata + aggregates: [] + api: + allowed_filters: [] + default_limit: 2 + max_limit: 2 + require_purpose_header: true + id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 + materialization: snapshot + primary_key: farmer_id + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: active_farm_parcel + nullable: false + sensitive: true + type: boolean + - name: active_smallholder_farmer + nullable: false + sensitive: true + type: boolean + - name: crop_declared_for_season + nullable: false + sensitive: true + type: boolean + - name: data_use_authorized + nullable: false + sensitive: true + type: boolean + - name: district_climate_risk_active + nullable: false + sensitive: true + type: boolean + - name: farmer_id + nullable: false + sensitive: true + type: string + - name: farmer_registered + nullable: false + sensitive: true + type: boolean + - name: voucher_entitlement_current + nullable: false + sensitive: true + type: boolean + - name: voucher_not_redeemed + nullable: false + sensitive: true + type: boolean + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv + type: file + title: NAgDI farmer records + update_frequency: continuous +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Purpose-limited herd facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: nagdi_herd:aggregate + evidence_verification_scope: nagdi_herd:evidence_verification + metadata_scope: nagdi_herd:metadata + read_scope: nagdi_herd:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: farmer_id + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/livestock-movement-control + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited herd facts used by the Solmara Evidence service. + fields: + - from: destination_district_open + name: destination_district_open + sensitive: true + - from: farmer_id + name: farmer_id + sensitive: true + - from: herd_id + name: herd_id + sensitive: true + - from: herd_vaccination_current + name: herd_vaccination_current + sensitive: true + - from: no_conflicting_open_movement_permit + name: no_conflicting_open_movement_permit + sensitive: true + - from: origin_district_not_quarantined_for_species + name: origin_district_not_quarantined_for_species + sensitive: true + - from: registered_herd + name: registered_herd + sensitive: true + name: nagdi_herd + relationships: [] + spatial: null + table: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b + title: NAgDI herd records + id: nagdi_herd + owner: National Agriculture Data and Digital Innovation Agency + sensitivity: personal + tables: + - access: + aggregate_scope: nagdi_herd:aggregate + metadata_scope: nagdi_herd:metadata + aggregates: [] + api: + allowed_filters: [] + default_limit: 2 + max_limit: 2 + require_purpose_header: true + id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b + materialization: snapshot + primary_key: herd_id + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: destination_district_open + nullable: false + sensitive: true + type: boolean + - name: farmer_id + nullable: false + sensitive: true + type: string + - name: herd_id + nullable: false + sensitive: true + type: string + - name: herd_vaccination_current + nullable: false + sensitive: true + type: boolean + - name: no_conflicting_open_movement_permit + nullable: false + sensitive: true + type: boolean + - name: origin_district_not_quarantined_for_species + nullable: false + sensitive: true + type: boolean + - name: registered_herd + nullable: false + sensitive: true + type: boolean + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv + type: file + title: NAgDI herd records + update_frequency: continuous +deployment: + profile: local +instance: + environment: local + id: nagdi-agriculture-relay-consultation +server: + bind: 0.0.0.0:8080 +standards: {} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/consultation-contracts/livestock-herd.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/consultation-contracts/livestock-herd.json deleted file mode 100644 index adf39dd..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/consultation-contracts/livestock-herd.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.livestock.herd","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:05746b9128c73f3a079b88d4a6a5c392ea4d317755aa8e0771e0c83325a16b28","id":"relay.livestock.herd","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/livestock-movement-control"],"required_scope":"registry:consult:livestock","workload":"nagdi-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["destination_district_open","herd_vaccination_current","no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species","registered_herd"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/consultation-contracts/voucher-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/consultation-contracts/voucher-farmer.json deleted file mode 100644 index 3f414ad..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/consultation-contracts/voucher-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.voucher.farmer","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:6c9b13e328828747f1d32991be3ab0681112c283dae9f614a7187c8ca0d72a01","id":"relay.voucher.farmer","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"],"required_scope":"registry:consult:voucher","workload":"nagdi-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["active_farm_parcel","active_smallholder_farmer","crop_declared_for_season","data_use_authorized","district_climate_risk_active","farmer_registered","voucher_entitlement_current","voucher_not_redeemed"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/conformance.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/conformance.json deleted file mode 100644 index 4743085..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"data-use-authorized-for-purpose":true,"eligible-for-climate-smart-input-voucher":true,"farmer-registered":true},"error":null,"outcome":"match","outputs":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true},"headers":{},"status":200}}}],"name":"farmer-voucher-match","request":{"claims":[{"id":"farmer-registered"},{"id":"data-use-authorized-for-purpose"},{"id":"eligible-for-climate-smart-input-voucher"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/voucher-eligibility-review","target":{"identifiers":[{"scheme":"farmer_id","value":"FR-1001"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"data-use-authorized-for-purpose":false,"eligible-for-climate-smart-input-voucher":false,"farmer-registered":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"farmer-voucher-no-match","request":null,"variables":{}}],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/conformance.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/conformance.json deleted file mode 100644 index 25a9c51..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"farmer_id":"FR-8888"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},{"destination_district_open":false,"herd_vaccination_current":false,"no_conflicting_open_movement_permit":false,"origin_district_not_quarantined_for_species":false,"registered_herd":false}],"headers":{},"status":200}}}],"name":"livestock-movement-ambiguous","request":null,"variables":{}},{"classification":"synthetic","expect":{"claims":{"eligible-for-livestock-movement-permit":true,"origin-district-not-quarantined-for-species":true,"registered-herd":true},"error":null,"outcome":"match","outputs":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},"headers":{},"status":200}}}],"name":"livestock-movement-match","request":{"claims":[{"id":"registered-herd"},{"id":"origin-district-not-quarantined-for-species"},{"id":"eligible-for-livestock-movement-permit"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/livestock-movement-control","target":{"identifiers":[{"scheme":"farmer_id","value":"FR-1001"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"eligible-for-livestock-movement-permit":false,"origin-district-not-quarantined-for-species":false,"registered-herd":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"livestock-movement-no-match","request":null,"variables":{}}],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/relay-consultation.yaml b/runtime/registry-projects/local/nagdi-agriculture/relay/relay-consultation.yaml deleted file mode 100644 index 1ec71f5..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/relay-consultation.yaml +++ /dev/null @@ -1,234 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - nagdi-notary - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-nagdi-agriculture - title: solmara-nagdi-agriculture governed Registry Relay -consultation: - artifacts: - evidence: - - class: conformance - path: artifacts/evidence/livestock-movement-by-farmer/conformance.json - sha256: sha256:10bdc960cb62d9030cee7895a45ce77a6df1ebe133f7beac2d6feb05aaf18910 - - class: minimization - path: artifacts/evidence/livestock-movement-by-farmer/minimization.json - sha256: sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5 - - class: conformance - path: artifacts/evidence/farmer-voucher-by-farmer/conformance.json - sha256: sha256:6d1e0b039f6ed71b79f382260701582ef255c1b377e7d01ee942b1b9c2c53e52 - - class: negative_security - path: artifacts/evidence/farmer-voucher-by-farmer/negative-security.json - sha256: sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d - - class: negative_security - path: artifacts/evidence/livestock-movement-by-farmer/negative-security.json - sha256: sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce - - class: minimization - path: artifacts/evidence/farmer-voucher-by-farmer/minimization.json - sha256: sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b - integration_packs: - - hash: sha256:eef622a1e97ac245493f2348cecad9b3b9245b7b0ce30d9db7ed602cc80827cd - path: artifacts/integration-packs/farmer-voucher-by-farmer.json - sha256: sha256:ed6f77b36df0d70ad9ffea72fdfedc21cc0a3621db3cdf4ea70d18f8960db12c - - hash: sha256:9d1daa45060e9f5c073be4a42f175c398907d7f00c650554aeddeedb9bd94092 - path: artifacts/integration-packs/livestock-movement-by-farmer.json - sha256: sha256:7af1198e4ba477344621f4361613c5ba1d0d30bf1a2bc94e94b7a7c5d8e8ca16 - private_bindings: - - hash: sha256:f319d52859657a0a1acaabba01034558012eeeb11e36e406d9def07a72814416 - path: artifacts/private-bindings/livestock-herd.json - sha256: sha256:33efab2ec578e7baf3dc70c7a6c139eb76a5cd80f4a2c753fc474b795477d306 - - hash: sha256:04ea387eadcc2279e7c791241c46b77f41dcbb02bd15919a3e246370f3443714 - path: artifacts/private-bindings/voucher-farmer.json - sha256: sha256:75a3c0379a33f5c7029cf91ebb87a42c661a5e8ddd93c5fb7edf37f0f5f51d9b - public_contracts: - - hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 - path: artifacts/consultation-contracts/livestock-herd.json - sha256: sha256:a1f4045c36462881aec560fcf103c1f78252bf0e2acf81ddd6b75ce21ccfd84c - - hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c - path: artifacts/consultation-contracts/voucher-farmer.json - sha256: sha256:8ea73ba2779ef33a85cc5f34988fe0d14f13f1892baa185d0d5c67e76dcbe1ad - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: nagdi-notary - principal_id: nagdi-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4554467022379962639 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 7450858971948805253 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_farmer entity - entities: [] - id: nagdi_farmer - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_farmer:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - materialization: snapshot - primary_key: farmer_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: active_farm_parcel - nullable: false - sensitive: true - type: boolean - - name: active_smallholder_farmer - nullable: false - sensitive: true - type: boolean - - name: crop_declared_for_season - nullable: false - sensitive: true - type: boolean - - name: data_use_authorized - nullable: false - sensitive: true - type: boolean - - name: district_climate_risk_active - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: farmer_registered - nullable: false - sensitive: true - type: boolean - - name: voucher_entitlement_current - nullable: false - sensitive: true - type: boolean - - name: voucher_not_redeemed - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - type: file - title: nagdi_farmer - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_herd entity - entities: [] - id: nagdi_herd - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_herd:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - materialization: snapshot - primary_key: herd_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: destination_district_open - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: herd_id - nullable: false - sensitive: true - type: string - - name: herd_vaccination_current - nullable: false - sensitive: true - type: boolean - - name: no_conflicting_open_movement_permit - nullable: false - sensitive: true - type: boolean - - name: origin_district_not_quarantined_for_species - nullable: false - sensitive: true - type: boolean - - name: registered_herd - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - type: file - title: nagdi_herd - update_frequency: as_needed -deployment: - profile: local -instance: - environment: local - id: nagdi-agriculture-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/relay.yaml b/runtime/registry-projects/local/nagdi-agriculture/relay/relay.yaml index e58cf8f..439b7ba 100644 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/relay.yaml +++ b/runtime/registry-projects/local/nagdi-agriculture/relay/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: true allowed_clients: - - nagdi-notary + - solmara-evidence audiences: - registry-relay issuer: http://127.0.0.1:8090 @@ -24,21 +25,82 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized nagdi_farmer entity - entities: [] + description: Purpose-limited farmer facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: nagdi_farmer:aggregate + evidence_verification_scope: nagdi_farmer:evidence_verification + metadata_scope: nagdi_farmer:metadata + read_scope: nagdi_farmer:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: farmer_id + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited farmer facts used by the Solmara Evidence service. + fields: + - from: active_farm_parcel + name: active_farm_parcel + sensitive: true + - from: active_smallholder_farmer + name: active_smallholder_farmer + sensitive: true + - from: crop_declared_for_season + name: crop_declared_for_season + sensitive: true + - from: data_use_authorized + name: data_use_authorized + sensitive: true + - from: district_climate_risk_active + name: district_climate_risk_active + sensitive: true + - from: farmer_id + name: farmer_id + sensitive: true + - from: farmer_registered + name: farmer_registered + sensitive: true + - from: voucher_entitlement_current + name: voucher_entitlement_current + sensitive: true + - from: voucher_not_redeemed + name: voucher_not_redeemed + sensitive: true + name: nagdi_farmer + relationships: [] + spatial: null + table: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 + title: NAgDI farmer records id: nagdi_farmer - owner: solmara-nagdi-agriculture + owner: National Agriculture Data and Digital Innovation Agency sensitivity: personal tables: - access: aggregate_scope: nagdi_farmer:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: nagdi_farmer:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 materialization: snapshot primary_key: farmer_id @@ -92,8 +154,8 @@ datasets: quote: null path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv type: file - title: nagdi_farmer - update_frequency: as_needed + title: NAgDI farmer records + update_frequency: continuous - access_rights: restricted aggregates: [] conforms_to: [] @@ -102,21 +164,76 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized nagdi_herd entity - entities: [] + description: Purpose-limited herd facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: nagdi_herd:aggregate + evidence_verification_scope: nagdi_herd:evidence_verification + metadata_scope: nagdi_herd:metadata + read_scope: nagdi_herd:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: farmer_id + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/livestock-movement-control + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited herd facts used by the Solmara Evidence service. + fields: + - from: destination_district_open + name: destination_district_open + sensitive: true + - from: farmer_id + name: farmer_id + sensitive: true + - from: herd_id + name: herd_id + sensitive: true + - from: herd_vaccination_current + name: herd_vaccination_current + sensitive: true + - from: no_conflicting_open_movement_permit + name: no_conflicting_open_movement_permit + sensitive: true + - from: origin_district_not_quarantined_for_species + name: origin_district_not_quarantined_for_species + sensitive: true + - from: registered_herd + name: registered_herd + sensitive: true + name: nagdi_herd + relationships: [] + spatial: null + table: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b + title: NAgDI herd records id: nagdi_herd - owner: solmara-nagdi-agriculture + owner: National Agriculture Data and Digital Innovation Agency sensitivity: personal tables: - access: aggregate_scope: nagdi_herd:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: nagdi_herd:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b materialization: snapshot primary_key: herd_id @@ -162,13 +279,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv type: file - title: nagdi_herd - update_frequency: as_needed + title: NAgDI herd records + update_frequency: continuous deployment: profile: local instance: environment: local id: nagdi-agriculture-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/local/nia-population/notary/notary.yaml b/runtime/registry-projects/local/nia-population/notary/notary.yaml deleted file mode 100644 index 0bfc2da..0000000 --- a/runtime/registry-projects/local/nia-population/notary/notary.yaml +++ /dev/null @@ -1,154 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH - provider: env - id: child-benefit-federator - scopes: - - evidence:nia:child-benefit:read - - fingerprint: - name: NIA_CITIZEN_CLIENT_TOKEN_HASH - provider: env - id: citizen-portal - scopes: - - evidence:nia:citizen:read -cel: - worker_memory_bytes: 1073741824 -deployment: - profile: local -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - claims: - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - population: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - alive: - nullable: false - type: boolean - identity_status: - max_bytes: 128 - nullable: false - type: string - profile: - contract_hash: sha256:2b34d7466e4f45126055cdb3ec18026a6f0cf3051a863ad951eedfe6ed729958 - id: solmara-nia-population.nia-child-benefit.population - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: population-record-active - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - required_scopes: - - evidence:nia:child-benefit:read - rule: - bindings: {} - expression: |- - population.matched && population.identity_status == 'active' - && population.alive == true - type: cel - subject_type: person - title: population record active - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - nia-citizen-status.citizen-population-status - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - population: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - alive: - nullable: false - type: boolean - identity_status: - max_bytes: 128 - nullable: false - type: string - profile: - contract_hash: sha256:9433a86942bdfbb1add5e45e2c3462eb224a2e8097be0267a52eeaaecfde0c69 - id: solmara-nia-population.nia-citizen-status.population - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: citizen-population-record-active - purpose: https://id.registrystack.org/solmara/purpose/citizen-self-service - required_scopes: - - evidence:nia:citizen:read - rule: - bindings: {} - expression: |- - population.matched && population.identity_status == 'active' - && population.alive == true - type: cel - subject_type: person - title: citizen population record active - value: - nullable: false - type: boolean - version: '1' - credential_profiles: - nia-citizen-status.citizen-population-status: - allowed_claims: - - citizen-population-record-active - disclosure: - allowed: - - value - - predicate - - redacted - format: application/dc+sd-jwt - issuer: did:web:id.registrystack.org:solmara:authority:nia - signing_key: project-issuer - validity_seconds: 600 - vct: https://id.registrystack.org/solmara/vct/citizen-population-status - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/nia-notary-relay-token - workload_client_id: nia-notary - service_id: nia-notary - signing_keys: - project-issuer: - alg: EdDSA - kid: did:web:id.registrystack.org:solmara:authority:nia#issuer-key-1 - private_jwk_env: NIA_NOTARY_ISSUER_JWK - provider: local_jwk_env - status: active - variables: {} -instance: - environment: local - id: nia-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json new file mode 100644 index 0000000..6614b16 --- /dev/null +++ b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json @@ -0,0 +1 @@ +{"id":"solmara-nia-population.nia-population-consultation.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:c962257eac077820b96cd886c1319dd1e741ef4b4bd0f81efbe6cfb8e9105fb2","id":"relay.nia-population-consultation.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:nia-population-consultation","workload":"solmara-esignet"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json new file mode 100644 index 0000000..bf29091 --- /dev/null +++ b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"alive":true,"identity_status":"active"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"alive":true,"identity_status":"active"},"headers":{},"status":200}}}],"name":"nia-population-person-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"nia-population-person-no-match","variables":{}}],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nia-population/relay/artifacts/evidence/population-person-by-uin/minimization.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json similarity index 100% rename from runtime/registry-projects/local/nia-population/relay/artifacts/evidence/population-person-by-uin/minimization.json rename to runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json diff --git a/runtime/registry-projects/local/nia-population/relay/artifacts/evidence/population-person-by-uin/negative-security.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json similarity index 100% rename from runtime/registry-projects/local/nia-population/relay/artifacts/evidence/population-person-by-uin/negative-security.json rename to runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json diff --git a/runtime/registry-projects/hosted/nia-population/relay/artifacts/integration-packs/population-person-by-uin.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json similarity index 93% rename from runtime/registry-projects/hosted/nia-population/relay/artifacts/integration-packs/population-person-by-uin.json rename to runtime/registry-projects/local/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json index 7430026..035dab4 100644 --- a/runtime/registry-projects/hosted/nia-population/relay/artifacts/integration-packs/population-person-by-uin.json +++ b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json @@ -1 +1 @@ -{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:bae5ef90128eb60a3ef4a3eeea3bea815e73bf6ccfdd521a48a3043446bd5fbd"],"minimization":["sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407"],"negative_security":["sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-nia-population-person-by-uin","output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":300000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:4f346c55aef3cfc11d5e9db10ca32efd54eac449f16ad2e16a76fbc5bd97f171"],"minimization":["sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407"],"negative_security":["sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-nia-population-person-by-uin","output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":300000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nia-population/relay/artifacts/private-bindings/nia-citizen-status-population.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json similarity index 60% rename from runtime/registry-projects/hosted/nia-population/relay/artifacts/private-bindings/nia-citizen-status-population.json rename to runtime/registry-projects/local/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json index 735159f..4369a15 100644 --- a/runtime/registry-projects/hosted/nia-population/relay/artifacts/private-bindings/nia-citizen-status-population.json +++ b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:72b42442af5dffa0ad96ef2bb6266fd422ae1f16299b3fc82dbd3bf3fd9b58e9","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d"},"profile":{"id":"solmara-nia-population.nia-citizen-status.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d","tenant":"solmara-nia-population"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:e4c64f00a246783ba2afc3e47bb775d0d5f5c2794c72ea622f8e02bf96792c8d","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d"},"profile":{"id":"solmara-nia-population.nia-population-consultation.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d","tenant":"solmara-nia-population"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nia-population/relay/relay-consultation.yaml b/runtime/registry-projects/local/nia-population/relay-consultation/relay.yaml similarity index 82% rename from runtime/registry-projects/local/nia-population/relay/relay-consultation.yaml rename to runtime/registry-projects/local/nia-population/relay-consultation/relay.yaml index da20425..8a705b0 100644 --- a/runtime/registry-projects/local/nia-population/relay/relay-consultation.yaml +++ b/runtime/registry-projects/local/nia-population/relay-consultation/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: true allowed_clients: - - nia-notary + - solmara-consultation audiences: - registry-relay issuer: http://127.0.0.1:8090 @@ -18,33 +19,27 @@ catalog: consultation: artifacts: evidence: + - class: conformance + path: artifacts/evidence/population-person-by-uin/conformance.json + sha256: sha256:4f346c55aef3cfc11d5e9db10ca32efd54eac449f16ad2e16a76fbc5bd97f171 - class: minimization path: artifacts/evidence/population-person-by-uin/minimization.json sha256: sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407 - - class: conformance - path: artifacts/evidence/population-person-by-uin/conformance.json - sha256: sha256:bae5ef90128eb60a3ef4a3eeea3bea815e73bf6ccfdd521a48a3043446bd5fbd - class: negative_security path: artifacts/evidence/population-person-by-uin/negative-security.json sha256: sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab integration_packs: - - hash: sha256:72b42442af5dffa0ad96ef2bb6266fd422ae1f16299b3fc82dbd3bf3fd9b58e9 + - hash: sha256:e4c64f00a246783ba2afc3e47bb775d0d5f5c2794c72ea622f8e02bf96792c8d path: artifacts/integration-packs/population-person-by-uin.json - sha256: sha256:83e0e332c81c331e2329535fdf1983be1427359a0e1bdf16986b8bc4a2f24b3e + sha256: sha256:aabbcc69617b62fbb2ad9d0cecdaa1d27f8020147d5fa8f9e9605bbf237a8f05 private_bindings: - - hash: sha256:fa33b620ee699baa8efb7f233dc46884826b4e4b64940a0fa27cca5b1871bb40 - path: artifacts/private-bindings/nia-child-benefit-population.json - sha256: sha256:41dd7eba725d043adbdfbfcc7a95178364970c5b8f76525fa75db87c31d228b6 - - hash: sha256:286382e48365cf203f3b0b5712bb6c8f94d18d33657078dc2522e42afb76aa8f - path: artifacts/private-bindings/nia-citizen-status-population.json - sha256: sha256:89e52d120d4c5ab0fc451e74e2fe0bb11966e7e1e4ab59505713d78f567ae8b7 + - hash: sha256:aa9e728f3b414efc56026b5cf4129ea1299869e20d93159929ed3f2593aa493e + path: artifacts/private-bindings/nia-population-consultation-population.json + sha256: sha256:be7924bf0dfd91d9cf734f6d72866bc686e35fcbfdf4e0732a7519cad96ad59c public_contracts: - - hash: sha256:2b34d7466e4f45126055cdb3ec18026a6f0cf3051a863ad951eedfe6ed729958 - path: artifacts/consultation-contracts/nia-child-benefit-population.json - sha256: sha256:43e56a4eaff600b7fbb8ca382f6d00bee299dcd9e528f50de89143ad9f3502f3 - - hash: sha256:9433a86942bdfbb1add5e45e2c3462eb224a2e8097be0267a52eeaaecfde0c69 - path: artifacts/consultation-contracts/nia-citizen-status-population.json - sha256: sha256:3ecdd494bdec2c8b591c24385a3c808d49a9b9562f9809e1a7dd24dcc66693a1 + - hash: sha256:eb210bd4c1c1110a465fb00e4e3da3d91e8b2b4320243750a6d92729a7bb6add + path: artifacts/consultation-contracts/nia-population-consultation-population.json + sha256: sha256:92937e0c5bf6ce83e92a40b492551a59798d2f3600592eb307cce2393675865c rhai_scripts: [] audit_pseudonym_materials: - key_id: epoch-1 @@ -54,8 +49,8 @@ consultation: authorized_workload: audience: registry-relay client_claim_selector: azp - client_value: nia-notary - principal_id: nia-notary + client_value: solmara-consultation + principal_id: solmara-consultation source_credentials: [] state_plane: audit_pseudonym_keyring_lock_key: 1016450306001272100 @@ -95,6 +90,8 @@ datasets: permitted_jurisdictions: [] permitted_purposes: - https://id.registrystack.org/solmara/purpose/esignet-identity-verification + - https://id.registrystack.org/solmara/purpose/child-benefit-review + - https://id.registrystack.org/solmara/purpose/citizen-self-service redaction_fields: [] require_consent: false require_legal_basis: false @@ -247,5 +244,5 @@ instance: environment: local id: nia-population-relay-consultation server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/local/nia-population/relay/artifacts/consultation-contracts/nia-child-benefit-population.json b/runtime/registry-projects/local/nia-population/relay/artifacts/consultation-contracts/nia-child-benefit-population.json deleted file mode 100644 index 90ad104..0000000 --- a/runtime/registry-projects/local/nia-population/relay/artifacts/consultation-contracts/nia-child-benefit-population.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.nia-child-benefit.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:7af974a445cf4ebaec93647f7d6aa80732d70829150702ab0891c06f91850359","id":"relay.nia-child-benefit.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:nia-child-benefit","workload":"nia-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nia-population/relay/artifacts/consultation-contracts/nia-citizen-status-population.json b/runtime/registry-projects/local/nia-population/relay/artifacts/consultation-contracts/nia-citizen-status-population.json deleted file mode 100644 index 08977c4..0000000 --- a/runtime/registry-projects/local/nia-population/relay/artifacts/consultation-contracts/nia-citizen-status-population.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.nia-citizen-status.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"citizen-self-service","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:0f648848f4934f37c2dbce9e58040e2ce7b43d39a5d1c8d0391cdd4aa07b9834","id":"relay.nia-citizen-status.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/citizen-self-service"],"required_scope":"registry:consult:nia-citizen-status","workload":"nia-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nia-population/relay/artifacts/evidence/population-person-by-uin/conformance.json b/runtime/registry-projects/local/nia-population/relay/artifacts/evidence/population-person-by-uin/conformance.json deleted file mode 100644 index 752267e..0000000 --- a/runtime/registry-projects/local/nia-population/relay/artifacts/evidence/population-person-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"citizen-population-record-active":true,"population-record-active":true},"error":null,"outcome":"match","outputs":{"alive":true,"identity_status":"active"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"alive":true,"identity_status":"active"},"headers":{},"status":200}}}],"name":"nia-population-person-match","request":{"claims":[{"id":"population-record-active"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"citizen-population-record-active":false,"population-record-active":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"nia-population-person-no-match","request":{"claims":[{"id":"citizen-population-record-active"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/citizen-self-service","target":{"identifiers":[{"scheme":"solmara_uin","value":"9999999999"}],"type":"Person"}},"variables":{}}],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nia-population/relay/artifacts/private-bindings/nia-child-benefit-population.json b/runtime/registry-projects/local/nia-population/relay/artifacts/private-bindings/nia-child-benefit-population.json deleted file mode 100644 index 9d14494..0000000 --- a/runtime/registry-projects/local/nia-population/relay/artifacts/private-bindings/nia-child-benefit-population.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:72b42442af5dffa0ad96ef2bb6266fd422ae1f16299b3fc82dbd3bf3fd9b58e9","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d"},"profile":{"id":"solmara-nia-population.nia-child-benefit.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d","tenant":"solmara-nia-population"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nia-population/relay/artifacts/private-bindings/nia-citizen-status-population.json b/runtime/registry-projects/local/nia-population/relay/artifacts/private-bindings/nia-citizen-status-population.json deleted file mode 100644 index 735159f..0000000 --- a/runtime/registry-projects/local/nia-population/relay/artifacts/private-bindings/nia-citizen-status-population.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:72b42442af5dffa0ad96ef2bb6266fd422ae1f16299b3fc82dbd3bf3fd9b58e9","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d"},"profile":{"id":"solmara-nia-population.nia-citizen-status.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d","tenant":"solmara-nia-population"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nia-population/relay/relay.yaml b/runtime/registry-projects/local/nia-population/relay/relay.yaml index 3ea4469..4c0df3e 100644 --- a/runtime/registry-projects/local/nia-population/relay/relay.yaml +++ b/runtime/registry-projects/local/nia-population/relay/relay.yaml @@ -1,12 +1,14 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: true allowed_clients: - solmara-esignet + - solmara-evidence audiences: - registry-relay issuer: http://127.0.0.1:8090 @@ -47,6 +49,8 @@ datasets: permitted_jurisdictions: [] permitted_purposes: - https://id.registrystack.org/solmara/purpose/esignet-identity-verification + - https://id.registrystack.org/solmara/purpose/child-benefit-review + - https://id.registrystack.org/solmara/purpose/citizen-self-service redaction_fields: [] require_consent: false require_legal_basis: false @@ -199,5 +203,5 @@ instance: environment: local id: nia-population-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/local/sipf-pensions/notary/notary.yaml b/runtime/registry-projects/local/sipf-pensions/notary/notary.yaml deleted file mode 100644 index a3d7b1f..0000000 --- a/runtime/registry-projects/local/sipf-pensions/notary/notary.yaml +++ /dev/null @@ -1,138 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: SIPF_PENSION_CLIENT_TOKEN_HASH - provider: env - id: pension-review-desk - scopes: - - evidence:sipf:pension-payment:read - - evidence:sipf:survivor-benefit:read -cel: - worker_memory_bytes: 1073741824 -deployment: - profile: local -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/pension-payment-review - - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - claims: - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - pension: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - payment_status: - max_bytes: 128 - nullable: false - type: string - profile: - contract_hash: sha256:04deb5f47932e163e61b8f7ce9c4d4adff54a7558128edeb367518962abde1bd - id: solmara-sipf-pensions.sipf-pension-payment-review.pension - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: pension-payment-active - purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review - required_scopes: - - evidence:sipf:pension-payment:read - rule: - bindings: {} - expression: pension.matched && pension.payment_status == 'active' - type: cel - subject_type: person - title: pension payment active - value: - nullable: false - type: boolean - version: '1' - - credential_profiles: - - sipf-survivor-benefit.survivor-benefit-status - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - survivor: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - survivor_eligible: - nullable: false - type: boolean - profile: - contract_hash: sha256:de1c2295d58905dc34046288edf36263fcccf99cf684deaa885ab76f472ed6d6 - id: solmara-sipf-pensions.sipf-survivor-benefit.survivor - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: survivor-is-eligible - purpose: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - required_scopes: - - evidence:sipf:survivor-benefit:read - rule: - bindings: {} - expression: survivor.matched && survivor.survivor_eligible == true - type: cel - subject_type: person - title: survivor is eligible - value: - nullable: false - type: boolean - version: '1' - credential_profiles: - sipf-survivor-benefit.survivor-benefit-status: - allowed_claims: - - survivor-is-eligible - disclosure: - allowed: - - value - - predicate - - redacted - format: application/dc+sd-jwt - issuer: did:web:id.registrystack.org:solmara:authority:sipf - signing_key: project-issuer - validity_seconds: 600 - vct: https://id.registrystack.org/solmara/vct/survivor-benefit-status - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/sipf-notary-relay-token - workload_client_id: sipf-notary - service_id: sipf-notary - signing_keys: - project-issuer: - alg: EdDSA - kid: did:web:id.registrystack.org:solmara:authority:sipf#issuer-key-1 - private_jwk_env: SIPF_NOTARY_ISSUER_JWK - provider: local_jwk_env - status: active - variables: {} -instance: - environment: local - id: sipf-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json new file mode 100644 index 0000000..5b5d627 --- /dev/null +++ b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json @@ -0,0 +1 @@ +{"id":"solmara-sipf-pensions.sipf-pension-consultation.pension","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:39b4002812d2babf57a59302f2fc9680ac05e9439e57e74bc02b8d0dbcea3f0a","id":"relay.sipf-pension-consultation.pension","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:sipf-pension-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["payment_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json new file mode 100644 index 0000000..8e4fcd1 --- /dev/null +++ b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json @@ -0,0 +1 @@ +{"id":"solmara-sipf-pensions.sipf-survivor-consultation.survivor","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:99c1d7d89c1d4abe175bd0693a123c881dff1775da7e78099a5158396de11d58","id":"relay.sipf-survivor-consultation.survivor","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"],"required_scope":"registry:consult:sipf-survivor-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["survivor_eligible"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json new file mode 100644 index 0000000..d7d17b1 --- /dev/null +++ b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"payment_status":"active"},{"payment_status":"suspended"}],"headers":{},"status":200}}}],"name":"sipf-pension-case-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"payment_status":"active"}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"payment_status":"active"},"headers":{},"status":200}}}],"name":"sipf-pension-case-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-pension-case-no-match","variables":{}}],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/minimization.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json similarity index 100% rename from runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/minimization.json rename to runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/negative-security.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json similarity index 100% rename from runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/negative-security.json rename to runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json new file mode 100644 index 0000000..53ca6c1 --- /dev/null +++ b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"survivor_eligible":true},{"survivor_eligible":false}],"headers":{},"status":200}}}],"name":"sipf-survivor-case-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"survivor_eligible":true}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"survivor_eligible":true},"headers":{},"status":200}}}],"name":"sipf-survivor-case-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-survivor-case-no-match","variables":{}}],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/minimization.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json similarity index 100% rename from runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/minimization.json rename to runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/negative-security.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json similarity index 100% rename from runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/negative-security.json rename to runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-pensioner.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json similarity index 92% rename from runtime/registry-projects/local/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-pensioner.json rename to runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json index 7fac5ec..ded8a18 100644 --- a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-pensioner.json +++ b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json @@ -1 +1 @@ -{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:34d1119e31d3a2e22571b9532541d057874aebebbde363ed80a137bce5a6772e"],"minimization":["sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660"],"negative_security":["sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-pensioner","output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:baac4ff492615cf355beaa403abaad465a23d3552fd6ed3682dce4898e829370"],"minimization":["sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660"],"negative_security":["sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-pensioner","output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-survivor.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json similarity index 92% rename from runtime/registry-projects/local/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-survivor.json rename to runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json index a0e0dda..2cd8a30 100644 --- a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-survivor.json +++ b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json @@ -1 +1 @@ -{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:a1f2073fc39da5b7a4c52bbd6d8f7a414e719b5c9dcf293509afac4d27f39059"],"minimization":["sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f"],"negative_security":["sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-survivor","output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:7b0462b4a2d4592cd1aeb8cee70fb54b66a5d9cd7c0aae0d07ee4c160f56d84a"],"minimization":["sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f"],"negative_security":["sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-survivor","output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json similarity index 61% rename from runtime/registry-projects/local/sipf-pensions/relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json rename to runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json index 2b3b78b..94672b6 100644 --- a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json +++ b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:ebcce823eb11489afd04cce13a550317ae7f9b9ef6c503bd68490d26085ab73e","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"pensioner_uin","physical_type":"utf8"}},"projection":{"payment_status":"payment_status"}},"table_provider":"pension__materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21"},"profile":{"id":"solmara-sipf-pensions.sipf-pension-payment-review.pension","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21","tenant":"solmara-sipf-pensions"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:6fe615f0be4b84d28497b7c6111c5426094af8199955977b881e9ebc7c34daa7","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"pensioner_uin","physical_type":"utf8"}},"projection":{"payment_status":"payment_status"}},"table_provider":"pension__materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21"},"profile":{"id":"solmara-sipf-pensions.sipf-pension-consultation.pension","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json similarity index 67% rename from runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json rename to runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json index 8485dcd..b3c1c47 100644 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json +++ b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:f4416975dcbe4c9eaea878107ea9e97a10b1fd9e02484b1d5b498b5c805d7235","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"spouse_uin","physical_type":"utf8"}},"projection":{"survivor_eligible":"survivor_eligible"}},"table_provider":"survivor__materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539"},"profile":{"id":"solmara-sipf-pensions.sipf-survivor-benefit.survivor","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539","tenant":"solmara-sipf-pensions"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:0aabcdecb0f1cc221e8d4949cdc613c1ecec9c396f0f85b22f24ca8810c92106","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"spouse_uin","physical_type":"utf8"}},"projection":{"survivor_eligible":"survivor_eligible"}},"table_provider":"survivor__materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539"},"profile":{"id":"solmara-sipf-pensions.sipf-survivor-consultation.survivor","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/relay.yaml b/runtime/registry-projects/local/sipf-pensions/relay-consultation/relay.yaml new file mode 100644 index 0000000..c3338b5 --- /dev/null +++ b/runtime/registry-projects/local/sipf-pensions/relay-consultation/relay.yaml @@ -0,0 +1,263 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + path: /var/lib/registry/audit/audit.jsonl + sink: file +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: true + allowed_clients: + - solmara-consultation + audiences: + - registry-relay + issuer: http://127.0.0.1:8090 + jwks_url: http://127.0.0.1:8090/.well-known/jwks.json +catalog: + base_url: http://127.0.0.1:8080 + publisher: solmara-sipf-pensions + title: solmara-sipf-pensions governed Registry Relay +consultation: + artifacts: + evidence: + - class: minimization + path: artifacts/evidence/pension-case-by-survivor/minimization.json + sha256: sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f + - class: negative_security + path: artifacts/evidence/pension-case-by-survivor/negative-security.json + sha256: sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7 + - class: minimization + path: artifacts/evidence/pension-case-by-pensioner/minimization.json + sha256: sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660 + - class: conformance + path: artifacts/evidence/pension-case-by-survivor/conformance.json + sha256: sha256:7b0462b4a2d4592cd1aeb8cee70fb54b66a5d9cd7c0aae0d07ee4c160f56d84a + - class: conformance + path: artifacts/evidence/pension-case-by-pensioner/conformance.json + sha256: sha256:baac4ff492615cf355beaa403abaad465a23d3552fd6ed3682dce4898e829370 + - class: negative_security + path: artifacts/evidence/pension-case-by-pensioner/negative-security.json + sha256: sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d + integration_packs: + - hash: sha256:6fe615f0be4b84d28497b7c6111c5426094af8199955977b881e9ebc7c34daa7 + path: artifacts/integration-packs/pension-case-by-pensioner.json + sha256: sha256:fc850bad2f6cec2f1b514c3005b92588c62c3f95cefc9d8419061d27cecd649a + - hash: sha256:0aabcdecb0f1cc221e8d4949cdc613c1ecec9c396f0f85b22f24ca8810c92106 + path: artifacts/integration-packs/pension-case-by-survivor.json + sha256: sha256:fac45aff1350bdfc5ec7f7e1741e02c02f74d5ac22505fa65d5b769a8a0697f8 + private_bindings: + - hash: sha256:4ed2016d95b43505d541bf2c91fa0ea952879457e5fce8c66fcd66a178ce57b4 + path: artifacts/private-bindings/sipf-pension-consultation-pension.json + sha256: sha256:872343722e004f2c420535877d15f50477726533c999bcd41d3529513654903d + - hash: sha256:4ff43cbb6fe5281a24a498a1cdb7bbd552ea3868c6aee27872df41fc11f37e2b + path: artifacts/private-bindings/sipf-survivor-consultation-survivor.json + sha256: sha256:f05668bc4724b8acabeadcf95eac837405da292196d1fb8d0e84d5f871bf683c + public_contracts: + - hash: sha256:67a23c52de56b492a69784ec0a57bde7f6772f5a8cbe3f7e450d5bf86a52fd13 + path: artifacts/consultation-contracts/sipf-pension-consultation-pension.json + sha256: sha256:46bdc7b1bf6fb6a2b83c5697bdc690831025b8da1c190ead0471769707c1f30a + - hash: sha256:0b57c5799c75c6a6717f35b045d76189a436aa124157faa070318a2f8a405253 + path: artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json + sha256: sha256:c0b833e6687c0af54df01acf34eda3e39eaef0ff42de2119bd5773e22e547b54 + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: solmara-consultation + principal_id: solmara-consultation + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 4444991830243562918 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 6117019989203477116 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Purpose-limited pension facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: pension:aggregate + evidence_verification_scope: pension:evidence_verification + metadata_scope: pension:metadata + read_scope: pension:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: pensioner_uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/pension-payment-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited pension facts used by the Solmara Evidence service. + fields: + - from: payment_status + name: payment_status + sensitive: true + - from: pensioner_uin + name: pensioner_uin + sensitive: true + name: pension + relationships: [] + spatial: null + table: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 + title: SIPF pension payment records + id: pension + owner: Social Insurance and Pension Fund + sensitivity: personal + tables: + - access: + aggregate_scope: pension:aggregate + metadata_scope: pension:metadata + aggregates: [] + api: + allowed_filters: [] + default_limit: 2 + max_limit: 2 + require_purpose_header: true + id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 + materialization: snapshot + primary_key: pensioner_uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: payment_status + nullable: false + sensitive: true + type: string + - name: pensioner_uin + nullable: false + sensitive: true + type: string + strict: true + source: + connection_env: SOLMARA_SIPF_DATABASE_URL + table: + name: sipf_pension_payment + schema: public + type: postgres + title: SIPF pension payment records + update_frequency: continuous +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Purpose-limited survivor facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: survivor:aggregate + evidence_verification_scope: survivor:evidence_verification + metadata_scope: survivor:metadata + read_scope: survivor:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: spouse_uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited survivor facts used by the Solmara Evidence service. + fields: + - from: spouse_uin + name: spouse_uin + sensitive: true + - from: survivor_eligible + name: survivor_eligible + sensitive: true + name: survivor + relationships: [] + spatial: null + table: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 + title: SIPF survivor benefit records + id: survivor + owner: Social Insurance and Pension Fund + sensitivity: personal + tables: + - access: + aggregate_scope: survivor:aggregate + metadata_scope: survivor:metadata + aggregates: [] + api: + allowed_filters: [] + default_limit: 2 + max_limit: 2 + require_purpose_header: true + id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 + materialization: snapshot + primary_key: spouse_uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: spouse_uin + nullable: false + sensitive: true + type: string + - name: survivor_eligible + nullable: false + sensitive: true + type: boolean + strict: true + source: + connection_env: SOLMARA_SIPF_DATABASE_URL + table: + name: sipf_survivor_benefit + schema: public + type: postgres + title: SIPF survivor benefit records + update_frequency: continuous +deployment: + profile: local +instance: + environment: local + id: sipf-pensions-relay-consultation +server: + bind: 0.0.0.0:8080 +standards: {} diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json deleted file mode 100644 index ad9440e..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.sipf-pension-payment-review.pension","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:96e56b4b170376860f623777977f7c72a19cd960854a48f568e1e92ca33f85e7","id":"relay.sipf-pension-payment-review.pension","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:sipf-pension-payment-review","workload":"sipf-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["payment_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json deleted file mode 100644 index a4fd9c0..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.sipf-survivor-benefit.survivor","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:ec6b5310716606dac48159abec1e512757fb9a18bff8a6e0ac634db932b2696b","id":"relay.sipf-survivor-benefit.survivor","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"],"required_scope":"registry:consult:sipf-survivor-benefit","workload":"sipf-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["survivor_eligible"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/conformance.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/conformance.json deleted file mode 100644 index 8babb7e..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"payment_status":"active"},{"payment_status":"suspended"}],"headers":{},"status":200}}}],"name":"sipf-pension-case-ambiguous","request":null,"variables":{}},{"classification":"synthetic","expect":{"claims":{"pension-payment-active":true},"error":null,"outcome":"match","outputs":{"payment_status":"active"}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"payment_status":"active"},"headers":{},"status":200}}}],"name":"sipf-pension-case-match","request":{"claims":[{"id":"pension-payment-active"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/pension-payment-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300109568"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"pension-payment-active":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-pension-case-no-match","request":null,"variables":{}}],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/conformance.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/conformance.json deleted file mode 100644 index e33e4f9..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"survivor_eligible":true},{"survivor_eligible":false}],"headers":{},"status":200}}}],"name":"sipf-survivor-case-ambiguous","request":null,"variables":{}},{"classification":"synthetic","expect":{"claims":{"survivor-is-eligible":true},"error":null,"outcome":"match","outputs":{"survivor_eligible":true}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"survivor_eligible":true},"headers":{},"status":200}}}],"name":"sipf-survivor-case-match","request":{"claims":[{"id":"survivor-is-eligible"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/survivor-benefit-determination","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300118698"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"survivor-is-eligible":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-survivor-case-no-match","request":null,"variables":{}}],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sipf-pensions/relay/relay-consultation.yaml b/runtime/registry-projects/local/sipf-pensions/relay/relay-consultation.yaml deleted file mode 100644 index a9cae68..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay/relay-consultation.yaml +++ /dev/null @@ -1,182 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - sipf-notary - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-sipf-pensions - title: solmara-sipf-pensions governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/pension-case-by-survivor/minimization.json - sha256: sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f - - class: conformance - path: artifacts/evidence/pension-case-by-pensioner/conformance.json - sha256: sha256:34d1119e31d3a2e22571b9532541d057874aebebbde363ed80a137bce5a6772e - - class: negative_security - path: artifacts/evidence/pension-case-by-survivor/negative-security.json - sha256: sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7 - - class: minimization - path: artifacts/evidence/pension-case-by-pensioner/minimization.json - sha256: sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660 - - class: conformance - path: artifacts/evidence/pension-case-by-survivor/conformance.json - sha256: sha256:a1f2073fc39da5b7a4c52bbd6d8f7a414e719b5c9dcf293509afac4d27f39059 - - class: negative_security - path: artifacts/evidence/pension-case-by-pensioner/negative-security.json - sha256: sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d - integration_packs: - - hash: sha256:ebcce823eb11489afd04cce13a550317ae7f9b9ef6c503bd68490d26085ab73e - path: artifacts/integration-packs/pension-case-by-pensioner.json - sha256: sha256:6252d74d83d4074d4eee0ae60cb2d1aac357c1a0a56664ab92120933ad0e77b3 - - hash: sha256:f4416975dcbe4c9eaea878107ea9e97a10b1fd9e02484b1d5b498b5c805d7235 - path: artifacts/integration-packs/pension-case-by-survivor.json - sha256: sha256:ca28e59689fa5e842f1435cd8c7bf922117ff2afc7bf3af08788781fcf458168 - private_bindings: - - hash: sha256:928e8a72b68fcdf9fe3ae4381cc71918d78a5c8f8bd138be09df42528a6fdcd3 - path: artifacts/private-bindings/sipf-pension-payment-review-pension.json - sha256: sha256:9499ffd554e9d00acdf62509e3dad500834050c4abff6ce0b1cc299fa35b7828 - - hash: sha256:5b46b40895d8fc69247e473ff117e119a08453121f5ecc9215e1297961ae701d - path: artifacts/private-bindings/sipf-survivor-benefit-survivor.json - sha256: sha256:37a68f42bf4870f69eb4faf6c820049c868951b49edbbe99483e61ae2cbde463 - public_contracts: - - hash: sha256:04deb5f47932e163e61b8f7ce9c4d4adff54a7558128edeb367518962abde1bd - path: artifacts/consultation-contracts/sipf-pension-payment-review-pension.json - sha256: sha256:75211b8be4be68778d4a0aa044fa46778b507017028dfa1b43c9330f9700a167 - - hash: sha256:de1c2295d58905dc34046288edf36263fcccf99cf684deaa885ab76f472ed6d6 - path: artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json - sha256: sha256:6bebf5122b4248dbc3eaefc5e3f7de1705796e30a1b8011b8c0fc3adf2c255f9 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: sipf-notary - principal_id: sipf-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4444991830243562918 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 6117019989203477116 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized pension entity - entities: [] - id: pension - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: pension:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - materialization: snapshot - primary_key: pensioner_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: payment_status - nullable: false - sensitive: true - type: string - - name: pensioner_uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_pension_payment - schema: public - type: postgres - title: pension - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized survivor entity - entities: [] - id: survivor - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: survivor:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - materialization: snapshot - primary_key: spouse_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: spouse_uin - nullable: false - sensitive: true - type: string - - name: survivor_eligible - nullable: false - sensitive: true - type: boolean - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_survivor_benefit - schema: public - type: postgres - title: survivor - update_frequency: as_needed -deployment: - profile: local -instance: - environment: local - id: sipf-pensions-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} diff --git a/runtime/registry-projects/local/sipf-pensions/relay/relay.yaml b/runtime/registry-projects/local/sipf-pensions/relay/relay.yaml index 0fc6e7f..b96a6cf 100644 --- a/runtime/registry-projects/local/sipf-pensions/relay/relay.yaml +++ b/runtime/registry-projects/local/sipf-pensions/relay/relay.yaml @@ -1,11 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: true - allowed_clients: [] + allowed_clients: + - solmara-evidence audiences: - registry-relay issuer: http://127.0.0.1:8090 @@ -23,21 +25,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized pension entity - entities: [] + description: Purpose-limited pension facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: pension:aggregate + evidence_verification_scope: pension:evidence_verification + metadata_scope: pension:metadata + read_scope: pension:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: pensioner_uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/pension-payment-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited pension facts used by the Solmara Evidence service. + fields: + - from: payment_status + name: payment_status + sensitive: true + - from: pensioner_uin + name: pensioner_uin + sensitive: true + name: pension + relationships: [] + spatial: null + table: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 + title: SIPF pension payment records id: pension - owner: solmara-sipf-pensions + owner: Social Insurance and Pension Fund sensitivity: personal tables: - access: aggregate_scope: pension:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: pension:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 materialization: snapshot primary_key: pensioner_uin @@ -61,8 +103,8 @@ datasets: name: sipf_pension_payment schema: public type: postgres - title: pension - update_frequency: as_needed + title: SIPF pension payment records + update_frequency: continuous - access_rights: restricted aggregates: [] conforms_to: [] @@ -71,21 +113,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized survivor entity - entities: [] + description: Purpose-limited survivor facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: survivor:aggregate + evidence_verification_scope: survivor:evidence_verification + metadata_scope: survivor:metadata + read_scope: survivor:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: spouse_uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited survivor facts used by the Solmara Evidence service. + fields: + - from: spouse_uin + name: spouse_uin + sensitive: true + - from: survivor_eligible + name: survivor_eligible + sensitive: true + name: survivor + relationships: [] + spatial: null + table: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 + title: SIPF survivor benefit records id: survivor - owner: solmara-sipf-pensions + owner: Social Insurance and Pension Fund sensitivity: personal tables: - access: aggregate_scope: survivor:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: survivor:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 materialization: snapshot primary_key: spouse_uin @@ -109,13 +191,13 @@ datasets: name: sipf_survivor_benefit schema: public type: postgres - title: survivor - update_frequency: as_needed + title: SIPF survivor benefit records + update_frequency: continuous deployment: profile: local instance: environment: local id: sipf-pensions-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/local/sro-social/notary/notary.yaml b/runtime/registry-projects/local/sro-social/notary/notary.yaml deleted file mode 100644 index 6723e8e..0000000 --- a/runtime/registry-projects/local/sro-social/notary/notary.yaml +++ /dev/null @@ -1,79 +0,0 @@ -audit: - hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET - sink: stdout -auth: - api_keys: - - fingerprint: - name: SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH - provider: env - id: child-benefit-federator - scopes: - - sro_social:evidence_verification -cel: - worker_memory_bytes: 1073741824 -deployment: - profile: local -evidence: - allowed_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - claims: - - credential_profiles: [] - disclosure: - allowed: - - predicate - - redacted - default: predicate - downgrade: deny - evidence_mode: - consultations: - household: - inputs: - uin: request.target.identifiers.solmara_uin - outputs: - poverty_band: - max_bytes: 64 - nullable: false - type: string - profile: - contract_hash: sha256:7c6800ab1eea861c2e2500ba3e93bb9ddcb6d1212d7701756098155faaee4697 - id: solmara-sro-social.child-benefit.household - type: registry_backed - formats: - - application/vnd.registry-notary.claim-result+json - id: household-below-poverty-threshold - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - required_scopes: - - sro_social:evidence_verification - rule: - bindings: {} - expression: household.matched && household.poverty_band == "priority" - type: cel - subject_type: person - title: household below poverty threshold - value: - nullable: false - type: boolean - version: '1' - credential_profiles: {} - enabled: true - max_credential_validity_seconds: 600 - relay: - allow_insecure_localhost: true - allowed_private_cidrs: [] - base_url: http://127.0.0.1:8080 - max_in_flight: 8 - token_file: /run/secrets/sro-notary-relay-token - workload_client_id: sro-notary - service_id: sro-notary - variables: {} -instance: - environment: local - id: sro-notary -server: - bind: 127.0.0.1:8081 - request_timeout: 30s -state: - postgresql: - root_certificate_path: /etc/solmara/postgres/root.crt - url_env: REGISTRY_NOTARY_POSTGRES_URL - storage: postgresql diff --git a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json new file mode 100644 index 0000000..769fe52 --- /dev/null +++ b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json @@ -0,0 +1 @@ +{"id":"solmara-sro-social.sro-household-consultation.household","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:f6d2a35ee331166669b6ae0eecd3ad084de203e4da00827a9368a9fb70f5f180","id":"relay.sro-household-consultation.household","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:sro-household-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sro-social.child-benefit-household-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["poverty_band"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":2678400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json new file mode 100644 index 0000000..7b8ebae --- /dev/null +++ b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"poverty_band":"priority"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"poverty_band":"priority"},"headers":{},"status":200}}}],"name":"child-benefit-household-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"child-benefit-household-no-match","variables":{}}],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/minimization.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json similarity index 100% rename from runtime/registry-projects/local/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/minimization.json rename to runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json diff --git a/runtime/registry-projects/local/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/negative-security.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json similarity index 100% rename from runtime/registry-projects/local/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/negative-security.json rename to runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json diff --git a/runtime/registry-projects/local/sro-social/relay/artifacts/integration-packs/child-benefit-household-by-uin.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json similarity index 92% rename from runtime/registry-projects/local/sro-social/relay/artifacts/integration-packs/child-benefit-household-by-uin.json rename to runtime/registry-projects/local/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json index 7133664..257709e 100644 --- a/runtime/registry-projects/local/sro-social/relay/artifacts/integration-packs/child-benefit-household-by-uin.json +++ b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json @@ -1 +1 @@ -{"id":"solmara-sro-social.child-benefit-household-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:d85f20f78036bb44a72ae05846565b2d7a705f92bb5836a75285c71ef63da9ce"],"minimization":["sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0"],"negative_security":["sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"child-benefit-household-by-uin","output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":2678400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file +{"id":"solmara-sro-social.child-benefit-household-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:a4d045756396f4c69a9607b88163f42efcb7baf55f46d7049b1555e850ed0070"],"minimization":["sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0"],"negative_security":["sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"child-benefit-household-by-uin","output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":2678400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sro-social/relay/artifacts/private-bindings/child-benefit-household.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json similarity index 63% rename from runtime/registry-projects/hosted/sro-social/relay/artifacts/private-bindings/child-benefit-household.json rename to runtime/registry-projects/local/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json index e46d4f8..f598671 100644 --- a/runtime/registry-projects/hosted/sro-social/relay/artifacts/private-bindings/child-benefit-household.json +++ b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json @@ -1 +1 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:7f32b8cf5cee550693f4c1a99d5859d251bbb54c6840c56a4c7ec1cf63c6462e","id":"solmara-sro-social.child-benefit-household-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"poverty_band":"poverty_band"}},"table_provider":"sro_child__materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d"},"profile":{"id":"solmara-sro-social.child-benefit.household","version":"1"},"registry_instance":"solmara-sro-social","source_instance":"materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d","tenant":"solmara-sro-social"} \ No newline at end of file +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:47547a81285fa1f655d534d521062e47f86bff6c2a74cb9b3637fd51f2943441","id":"solmara-sro-social.child-benefit-household-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"poverty_band":"poverty_band"}},"table_provider":"sro_child__materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d"},"profile":{"id":"solmara-sro-social.sro-household-consultation.household","version":"1"},"registry_instance":"solmara-sro-social","source_instance":"materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d","tenant":"solmara-sro-social"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sro-social/relay/relay-consultation.yaml b/runtime/registry-projects/local/sro-social/relay-consultation/relay.yaml similarity index 52% rename from runtime/registry-projects/local/sro-social/relay/relay-consultation.yaml rename to runtime/registry-projects/local/sro-social/relay-consultation/relay.yaml index 9c9289e..185e879 100644 --- a/runtime/registry-projects/local/sro-social/relay/relay-consultation.yaml +++ b/runtime/registry-projects/local/sro-social/relay-consultation/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: true allowed_clients: - - sro-notary + - solmara-consultation audiences: - registry-relay issuer: http://127.0.0.1:8090 @@ -26,19 +27,19 @@ consultation: sha256: sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0 - class: conformance path: artifacts/evidence/child-benefit-household-by-uin/conformance.json - sha256: sha256:d85f20f78036bb44a72ae05846565b2d7a705f92bb5836a75285c71ef63da9ce + sha256: sha256:a4d045756396f4c69a9607b88163f42efcb7baf55f46d7049b1555e850ed0070 integration_packs: - - hash: sha256:7f32b8cf5cee550693f4c1a99d5859d251bbb54c6840c56a4c7ec1cf63c6462e + - hash: sha256:47547a81285fa1f655d534d521062e47f86bff6c2a74cb9b3637fd51f2943441 path: artifacts/integration-packs/child-benefit-household-by-uin.json - sha256: sha256:bbf3bfb05e1f6020bdc14457db9549ad084ad421b5c59715bdb51d5ea67dc4d3 + sha256: sha256:b1cbeacef1a39cb0d4cb03a4adf1061b91532e04acf92abf1e06f60c0b84f1f0 private_bindings: - - hash: sha256:4dd783c0c8bcf6b6fee3aefd511c719dab911053adf3f39874cc99deded3bcfb - path: artifacts/private-bindings/child-benefit-household.json - sha256: sha256:9515fa0b01838a1f07f25b2b86b75843e8ef888438b82989f5ab7d657e2865ba + - hash: sha256:77a0bae801d725304c75e307f26f53753fb50f9cd72fd9f9bc501da37bfd1064 + path: artifacts/private-bindings/sro-household-consultation-household.json + sha256: sha256:2440ac8ae4c483d106e13af61a8b072509d2452b13a3a36f192b97421ece0bf1 public_contracts: - - hash: sha256:7c6800ab1eea861c2e2500ba3e93bb9ddcb6d1212d7701756098155faaee4697 - path: artifacts/consultation-contracts/child-benefit-household.json - sha256: sha256:77470bdaa56f3bf30beb1734e0aa0fd676068b09f45d576ce32cae4a51920efb + - hash: sha256:2009a7b48ceb64aaf24a028af795a3e2fd399fad618db5fd6b1b65cbb8bb281e + path: artifacts/consultation-contracts/sro-household-consultation-household.json + sha256: sha256:662eb6af3e5c5308eedb27ba57575399d2377dbf65d2dd9041520ee21bd8088b rhai_scripts: [] audit_pseudonym_materials: - key_id: epoch-1 @@ -48,8 +49,8 @@ consultation: authorized_workload: audience: registry-relay client_claim_selector: azp - client_value: sro-notary - principal_id: sro-notary + client_value: solmara-consultation + principal_id: solmara-consultation source_credentials: [] state_plane: audit_pseudonym_keyring_lock_key: 368637434756268187 @@ -66,21 +67,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized sro_child entity - entities: [] + description: Purpose-limited household facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: sro_child:aggregate + evidence_verification_scope: sro_child:evidence_verification + metadata_scope: sro_child:metadata + read_scope: sro_child:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited household facts used by the Solmara Evidence service. + fields: + - from: poverty_band + name: poverty_band + sensitive: true + - from: uin + name: uin + sensitive: true + name: sro_child + relationships: [] + spatial: null + table: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d + title: SRO household records id: sro_child - owner: solmara-sro-social + owner: Social Registry Office sensitivity: personal tables: - access: aggregate_scope: sro_child:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: sro_child:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d materialization: snapshot primary_key: uin @@ -106,13 +147,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv type: file - title: sro_child - update_frequency: as_needed + title: SRO household records + update_frequency: continuous deployment: profile: local instance: environment: local id: sro-social-relay-consultation server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/runtime/registry-projects/local/sro-social/relay/artifacts/consultation-contracts/child-benefit-household.json b/runtime/registry-projects/local/sro-social/relay/artifacts/consultation-contracts/child-benefit-household.json deleted file mode 100644 index e8bbb22..0000000 --- a/runtime/registry-projects/local/sro-social/relay/artifacts/consultation-contracts/child-benefit-household.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sro-social.child-benefit.household","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:1f2bc3ace6f53ef9e6aa82974582fdd31113142f164f5afaf8328dafe39d92d3","id":"relay.child-benefit.household","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:child-benefit","workload":"sro-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sro-social.child-benefit-household-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["poverty_band"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":2678400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/conformance.json b/runtime/registry-projects/local/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/conformance.json deleted file mode 100644 index 6a52524..0000000 --- a/runtime/registry-projects/local/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"household-below-poverty-threshold":true},"error":null,"outcome":"match","outputs":{"poverty_band":"priority"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"poverty_band":"priority"},"headers":{},"status":200}}}],"name":"child-benefit-household-match","request":{"claims":[{"id":"household-below-poverty-threshold"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"household-below-poverty-threshold":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"child-benefit-household-no-match","request":null,"variables":{}}],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sro-social/relay/relay.yaml b/runtime/registry-projects/local/sro-social/relay/relay.yaml index 989731e..9e2d8c0 100644 --- a/runtime/registry-projects/local/sro-social/relay/relay.yaml +++ b/runtime/registry-projects/local/sro-social/relay/relay.yaml @@ -1,12 +1,13 @@ audit: hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout + path: /var/lib/registry/audit/audit.jsonl + sink: file auth: mode: oidc oidc: allow_dev_insecure_fetch_urls: true allowed_clients: - - sro-notary + - solmara-evidence audiences: - registry-relay issuer: http://127.0.0.1:8090 @@ -24,21 +25,61 @@ datasets: refresh: interval: 1m mode: interval - description: Materialized sro_child entity - entities: [] + description: Purpose-limited household facts used by the Solmara Evidence service. + entities: + - access: + aggregate_scope: sro_child:aggregate + evidence_verification_scope: sro_child:evidence_verification + metadata_scope: sro_child:metadata + read_scope: sro_child:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: uin + ops: + - eq + default_limit: 2 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/child-benefit-review + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 2 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: [] + description: Purpose-limited household facts used by the Solmara Evidence service. + fields: + - from: poverty_band + name: poverty_band + sensitive: true + - from: uin + name: uin + sensitive: true + name: sro_child + relationships: [] + spatial: null + table: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d + title: SRO household records id: sro_child - owner: solmara-sro-social + owner: Social Registry Office sensitivity: personal tables: - access: aggregate_scope: sro_child:aggregate - metadata_scope: registry-internal:materialization + metadata_scope: sro_child:metadata aggregates: [] api: allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false + default_limit: 2 + max_limit: 2 + require_purpose_header: true id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d materialization: snapshot primary_key: uin @@ -64,13 +105,13 @@ datasets: quote: null path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv type: file - title: sro_child - update_frequency: as_needed + title: SRO household records + update_frequency: continuous deployment: profile: local instance: environment: local id: sro-social-relay server: - bind: 127.0.0.1:8080 + bind: 0.0.0.0:8080 standards: {} diff --git a/scenario-runner/child_benefit_federator.py b/scenario-runner/child_benefit_federator.py index 4535e85..f08548f 100644 --- a/scenario-runner/child_benefit_federator.py +++ b/scenario-runner/child_benefit_federator.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -"""Child-benefit evidence collector for the Solmara lab. +"""Application-level child-benefit Evidence collector. -This is an application service, not a Notary and not an eligibility engine. -It asks the four authority-owned Notaries for their minimized predicates over -the ordinary Registry Notary HTTP API, then returns a source-attributed -evidence set. The programme policy layer remains responsible for eligibility. +The collector owns no eligibility rule. It asks one Evidence deployment for +four separately governed requirements and returns their signed concept values +with source attribution. Registry rows remain behind each authority's Relay. """ from __future__ import annotations @@ -12,564 +11,151 @@ import hmac import json import os -import secrets -import time -from datetime import date from http import HTTPStatus from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Any -from urllib.parse import unquote, urlparse +from urllib.parse import urlparse -from scenarios.common import ( - CLAIM_RESULT_FORMAT, - PURPOSES, - StepHttpResult, - auth_headers, - evaluation_body, - http_json, -) -from scenarios.service_config import ( - authority_service_id, - service_token, - service_token_env, - service_url, -) +from scenarios.common import PURPOSES, evidence_body, evidence_headers, http_json, normalized_evidence_result +from scenarios.service_config import requirement_id, service_token, service_url -API_VERSION = "solmara-child-benefit-evidence/v1" +API_VERSION = "solmara-child-benefit-evidence/v2" FEDERATOR_SERVICE_ID = "child-benefit-federator" FEDERATOR_TOKEN_ENV = "CHILD_BENEFIT_FEDERATOR_TOKEN" CHILD_PURPOSE = PURPOSES["child_benefit"] -SUPPORTED_DISCLOSURES = {"predicate"} -SENSITIVE_RAW_CLAIMS = {"household-poverty-score", "household-profile"} MAX_REQUEST_BODY_BYTES = 64 * 1024 -ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" - - SOURCE_ROUTES: tuple[dict[str, Any], ...] = ( - { - "client_id": "cra-child-benefit", - "authority": "Civil Registration Authority", - "claims": ("birth-is-registered", "child-age-under-5"), - }, - { - "client_id": "nia-child-benefit", - "authority": "National Identity Agency", - "claims": ("population-record-active",), - }, - { - "client_id": "sro-child-benefit", - "authority": "Social Registry Office", - "claims": ("household-below-poverty-threshold",), - }, - { - "client_id": "programme-child-benefit", - "authority": "MoSD Programme MIS", - "claims": ("not-already-enrolled",), - }, + {"client_id": "cra-child-benefit", "authority": "Civil Registration Authority", "claims": ("birth-is-registered", "child-age-under-5")}, + {"client_id": "nia-child-benefit", "authority": "National Identity Agency", "claims": ("population-record-active",)}, + {"client_id": "sro-child-benefit", "authority": "Social Registry Office", "claims": ("household-below-poverty-threshold",)}, + {"client_id": "programme-child-benefit", "authority": "MoSD Programme MIS", "claims": ("not-already-enrolled",)}, ) -CLAIM_ROUTES = { - claim_id: route for route in SOURCE_ROUTES for claim_id in route["claims"] -} - - -class RequestBodyError(Exception): - def __init__(self, status: HTTPStatus, detail: str) -> None: - super().__init__(detail) - self.status = status - self.detail = detail - - -class AuthorityUpstreamError(Exception): - def __init__(self, route: dict[str, Any], status: int | None, code: str) -> None: - super().__init__(code) - self.route = route - self.status = status - self.code = code +CLAIM_ROUTES = {claim: route for route in SOURCE_ROUTES for claim in route["claims"]} class ChildBenefitFederatorHandler(BaseHTTPRequestHandler): - server_version = "SolmaraChildBenefitEvidence/1.0" + server_version = "SolmaraChildBenefitEvidence/2.0" def do_GET(self) -> None: - parts = path_parts(self.path) - if parts in (["health"], ["healthz"]): + path = urlparse(self.path).path + if path in {"/health", "/healthz"}: self.write_json({"status": "ok", "service": FEDERATOR_SERVICE_ID}) return - if parts == ["v1", "claims"]: + if path == "/v1/claims": if not self.require_token(): return - purpose = self.headers.get("Data-Purpose", "") - if purpose and purpose != CHILD_PURPOSE: - self.write_problem( - HTTPStatus.FORBIDDEN, - "pdp.purpose_not_permitted", - "Purpose not permitted", - "This service only collects evidence for child-benefit review.", - ) - return - self.write_json(claim_catalog()) + self.write_json({"schema_version": API_VERSION, "claims": [{"claim_id": claim, "authority": route["authority"]} for claim, route in CLAIM_ROUTES.items()]}) return - self.write_problem( - HTTPStatus.NOT_FOUND, "not_found", "Not found", "No such application route." - ) + self.write_problem(HTTPStatus.NOT_FOUND, "not_found", "No such application route.") def do_POST(self) -> None: - if path_parts(self.path) != ["v1", "evaluations"]: - self.write_problem( - HTTPStatus.NOT_FOUND, - "not_found", - "Not found", - "No such application route.", - ) + if urlparse(self.path).path != "/v1/evaluations": + self.write_problem(HTTPStatus.NOT_FOUND, "not_found", "No such application route.") return if not self.require_token(): return purpose = self.headers.get("Data-Purpose", "") if purpose != CHILD_PURPOSE: - self.write_problem( - HTTPStatus.FORBIDDEN, - "pdp.purpose_not_permitted", - "Purpose not permitted", - "This service only collects evidence for child-benefit review.", - ) + self.write_problem(HTTPStatus.FORBIDDEN, "purpose_not_permitted", "Only child-benefit-review is permitted.") return - - try: - body = self.read_body() - except RequestBodyError as error: - self.write_problem( - error.status, - "request.invalid", - "Invalid evidence request", - error.detail, - ) + body = self.read_body() + if body is None: return - - if body.get("disclosure", "predicate") not in SUPPORTED_DISCLOSURES: - self.write_problem( - HTTPStatus.FORBIDDEN, - "pdp.purpose_not_permitted", - "Disclosure not permitted", - "This purpose permits minimized predicates only.", - ) - return - requested = requested_claims(body) - if any(claim in SENSITIVE_RAW_CLAIMS for claim in requested): - self.write_problem( - HTTPStatus.FORBIDDEN, - "pdp.purpose_not_permitted", - "Disclosure not permitted", - "Raw source fields are not available through this purpose.", - ) - return - if body.get("format", "application/json") != "application/json": - self.write_problem( - HTTPStatus.BAD_REQUEST, - "request.unsupported_format", - "Unsupported response format", - "Omit format or set it to application/json.", - ) - return - if not accepts_media_type(self.headers.get("Accept", ""), "application/json"): - self.write_problem( - HTTPStatus.NOT_ACCEPTABLE, - "request.not_acceptable", - "Unsupported response representation", - "Set Accept to application/json.", - ) - return - subject = subject_id(body) - variables = request_variables(body, requested) - unknown = [claim for claim in requested if claim not in CLAIM_ROUTES] - if ( - not subject - or not requested - or unknown - or len(requested) != len(set(requested)) - or variables is None - ): - self.write_problem( - HTTPStatus.BAD_REQUEST, - "request.invalid", - "Invalid evidence request", - "The request must name unique supported child-benefit predicates and a solmara_uin target.", - ) + claims = requested_claims(body) + if not subject or not claims or len(claims) != len(set(claims)) or any(claim not in CLAIM_ROUTES for claim in claims): + self.write_problem(HTTPStatus.BAD_REQUEST, "invalid_request", "Name one UIN and unique supported claims.") return - - try: - evidence = collect_evidence( - subject, - requested, - purpose, - body.get("target"), - variables, - ) - except AuthorityUpstreamError as error: - self.write_problem( - HTTPStatus.BAD_GATEWAY, - "authority.upstream_failed", - "Authority evidence unavailable", - f"{error.route['authority']} did not return the requested minimized evidence.", - ) + token = service_token("cra-child-benefit") + if not token: + self.write_problem(HTTPStatus.SERVICE_UNAVAILABLE, "mint_unavailable", "No Evidence access token is available.") return - self.write_json(evidence) + results: list[dict[str, Any]] = [] + trace: list[dict[str, Any]] = [] + signed_evidence: list[Any] = [] + for route in SOURCE_ROUTES: + requested = [claim for claim in claims if claim in route["claims"]] + if not requested: + continue + url = service_url(route["client_id"]) + headers = evidence_headers(token) + request = evidence_body(subject, requirement_id(route["client_id"]), purpose) + response = normalized_evidence_result(http_json("POST", url, headers, request)) + if response.status is None or not 200 <= response.status < 300: + self.write_problem(HTTPStatus.BAD_GATEWAY, "evidence_unavailable", f"{route['authority']} evidence was unavailable.") + return + returned = {item["claim_id"]: item for item in response.body.get("results", [])} + if any(claim not in returned for claim in requested): + self.write_problem(HTTPStatus.BAD_GATEWAY, "evidence_incomplete", f"{route['authority']} omitted a requested concept.") + return + results.extend({**returned[claim], "authority": route["authority"]} for claim in requested) + signed_evidence.append(response.body.get("signed_evidence")) + trace.append({"authority": route["authority"], "service_id": "registry-evidence", "requirement": requirement_id(route["client_id"]), "status": response.status}) + self.write_json({"schema_version": API_VERSION, "orchestration": {"service_id": FEDERATOR_SERVICE_ID, "decision": "not_composed"}, "purpose": purpose, "target": {"type": "Person", "binding": "withheld"}, "results": results, "signed_evidence": signed_evidence, "source_trace": trace}) def require_token(self) -> bool: expected = os.environ.get(FEDERATOR_TOKEN_ENV, "") received = self.headers.get("x-api-key", "") if expected and hmac.compare_digest(received, expected): return True - self.write_problem( - HTTPStatus.UNAUTHORIZED, - "auth.missing_or_invalid", - "Authentication required", - f"Set the {FEDERATOR_TOKEN_ENV} synthetic lab token.", - ) + self.write_problem(HTTPStatus.UNAUTHORIZED, "authentication_required", "A valid local application token is required.") return False - def read_body(self) -> dict[str, Any]: - raw_length = self.headers.get("Content-Length") + def read_body(self) -> dict[str, Any] | None: try: - length = int(raw_length) if raw_length is not None else 0 - except ValueError as error: - raise RequestBodyError( - HTTPStatus.BAD_REQUEST, "Content-Length must be a decimal byte count." - ) from error - if length <= 0: - raise RequestBodyError( - HTTPStatus.BAD_REQUEST, "A non-empty JSON request body is required." - ) - if length > MAX_REQUEST_BODY_BYTES: - raise RequestBodyError( - HTTPStatus.REQUEST_ENTITY_TOO_LARGE, - f"The JSON request body must not exceed {MAX_REQUEST_BODY_BYTES} bytes.", - ) - raw = self.rfile.read(length) - if len(raw) != length: - raise RequestBodyError( - HTTPStatus.BAD_REQUEST, - "The JSON request body ended before Content-Length bytes arrived.", - ) + length = int(self.headers.get("Content-Length", "0")) + except ValueError: + length = 0 + if length <= 0 or length > MAX_REQUEST_BODY_BYTES: + self.write_problem(HTTPStatus.BAD_REQUEST, "invalid_request", "A bounded JSON object is required.") + return None try: - parsed = json.loads(raw.decode("utf-8")) - except (UnicodeDecodeError, json.JSONDecodeError) as error: - raise RequestBodyError( - HTTPStatus.BAD_REQUEST, "The request body must be a valid JSON object." - ) from error - if not isinstance(parsed, dict): - raise RequestBodyError( - HTTPStatus.BAD_REQUEST, "The request body must be a JSON object." - ) - return parsed - - def write_json( - self, - payload: dict[str, Any], - status: HTTPStatus = HTTPStatus.OK, - *, - content_type: str = "application/json", - ) -> None: - body = json.dumps(payload, indent=2, sort_keys=True).encode("utf-8") + value = json.loads(self.rfile.read(length)) + except (json.JSONDecodeError, UnicodeDecodeError): + value = None + if not isinstance(value, dict): + self.write_problem(HTTPStatus.BAD_REQUEST, "invalid_request", "A JSON object is required.") + return None + return value + + def write_json(self, payload: dict[str, Any], status: HTTPStatus = HTTPStatus.OK) -> None: + body = json.dumps(payload, separators=(",", ":")).encode() self.send_response(status) - self.send_header("Content-Type", f"{content_type}; charset=utf-8") + self.send_header("Content-Type", "application/json") self.send_header("Cache-Control", "no-store") - self.send_header("X-Content-Type-Options", "nosniff") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) - def write_problem( - self, status: HTTPStatus, code: str, title: str, detail: str - ) -> None: - self.write_json( - { - "type": f"https://id.registrystack.org/problems/solmara/{code.replace('.', '/')}", - "title": title, - "status": int(status), - "code": code, - "detail": detail, - }, - status, - content_type="application/problem+json", - ) + def write_problem(self, status: HTTPStatus, code: str, detail: str) -> None: + self.write_json({"type": f"https://id.registrystack.org/problems/solmara/{code}", "title": code.replace("_", " ").title(), "status": int(status), "code": code, "detail": detail}, status) def log_message(self, format: str, *args: Any) -> None: - if os.environ.get("CHILD_BENEFIT_FEDERATOR_ACCESS_LOG", "").lower() in { - "1", - "true", - "yes", - }: + if os.environ.get("CHILD_BENEFIT_FEDERATOR_ACCESS_LOG") == "1": super().log_message(format, *args) -def collect_evidence( - subject: str, - claims: list[str], - purpose: str, - target: Any, - variables: dict[str, str] | None = None, -) -> dict[str, Any]: - by_claim: dict[str, dict[str, Any]] = {} - source_trace: list[dict[str, Any]] = [] - for route in SOURCE_ROUTES: - route_claims = [claim for claim in claims if claim in route["claims"]] - if not route_claims: - continue - source = call_authority_notary( - route, - subject, - route_claims, - purpose, - variables or {}, - ) - by_claim.update({result["claim_id"]: result for result in source["results"]}) - source_trace.append(source["trace"]) - return { - "schema_version": API_VERSION, - "evidence_set_id": f"cbe_{ulid()}", - "orchestration": { - "service_id": FEDERATOR_SERVICE_ID, - "decision": "not_composed", - }, - "purpose": purpose, - "target": public_target(target), - "results": [by_claim[claim] for claim in claims], - "source_trace": source_trace, - } - - -def call_authority_notary( - route: dict[str, Any], - subject: str, - claims: list[str], - purpose: str, - variables: dict[str, str], -) -> dict[str, Any]: - client_id = route["client_id"] - token = service_token(client_id) - if not token: - raise AuthorityUpstreamError( - route, None, f"missing_{service_token_env(client_id).lower()}" - ) - url = service_url(client_id, "/v1/evaluations") - request_variables = None - if "child-age-under-5" in claims: - as_of_date = variables.get("as_of_date") - if not as_of_date: - raise AuthorityUpstreamError(route, None, "missing_as_of_date") - request_variables = {"as_of_date": as_of_date} - body = evaluation_body( - subject, - claims, - scheme="solmara_uin", - format=CLAIM_RESULT_FORMAT, - variables=request_variables, - ) - headers = auth_headers(token, purpose, CLAIM_RESULT_FORMAT) - response = http_json("POST", url, headers, body) - if response.status is None or not 200 <= response.status < 300: - raise AuthorityUpstreamError( - route, response.status, upstream_error_code(response) - ) - results = minimized_results(route, claims, response) - return { - "results": results, - "trace": { - "authority": route["authority"], - "service_id": authority_service_id(client_id), - "claims": claims, - "request_summary": { - "method": "POST", - "url": url, - "purpose": purpose, - "disclosure": "predicate", - "claims": claims, - }, - "response_summary": { - "status": response.status, - "headers": allowlisted_headers(response), - "results": results, - }, - }, - } - - -def minimized_results( - route: dict[str, Any], - requested: list[str], - response: StepHttpResult, -) -> list[dict[str, Any]]: - body = response.body if isinstance(response.body, dict) else {} - raw_results = body.get("results") - if not isinstance(raw_results, list): - raise AuthorityUpstreamError(route, response.status, "invalid_response_payload") - result_by_claim: dict[str, dict[str, Any]] = {} - for raw_result in raw_results: - if not isinstance(raw_result, dict): - raise AuthorityUpstreamError(route, response.status, "invalid_claim_result") - claim_id = raw_result.get("claim_id") - if not isinstance(claim_id, str) or claim_id in result_by_claim: - raise AuthorityUpstreamError(route, response.status, "invalid_claim_result") - result_by_claim[claim_id] = raw_result - if set(result_by_claim) != set(requested): - raise AuthorityUpstreamError(route, response.status, "unexpected_claim_results") - - minimized: list[dict[str, Any]] = [] - for claim_id in requested: - raw_result = result_by_claim[claim_id] - satisfied = raw_result.get("satisfied") - if ( - not isinstance(satisfied, bool) - or raw_result.get("disclosure") != "predicate" - ): - raise AuthorityUpstreamError(route, response.status, "invalid_claim_result") - minimized.append( - { - "claim_id": claim_id, - "claim_version": raw_result.get("claim_version"), - "satisfied": satisfied, - "disclosure": "predicate", - "format": CLAIM_RESULT_FORMAT, - "issued_at": raw_result.get("issued_at"), - "expires_at": raw_result.get("expires_at"), - "authority": route["authority"], - "notary_service_id": authority_service_id(route["client_id"]), - } - ) - return minimized - - -def upstream_error_code(response: StepHttpResult) -> str: - body = response.body if isinstance(response.body, dict) else {} - code = body.get("code") or body.get("error") or response.error - return str(code) if code else f"http_{response.status}" - - -def allowlisted_headers(response: StepHttpResult) -> dict[str, str]: - return { - key: value - for key, value in response.headers.items() - if key.lower() in {"content-type", "www-authenticate"} - } - - -def accepts_media_type(value: str, expected: str) -> bool: - for item in value.split(","): - media_type = item.split(";", 1)[0].strip().lower() - if media_type in {expected.lower(), "application/*", "*/*"}: - return True - return False - - def subject_id(body: dict[str, Any]) -> str: - target = body.get("target") if isinstance(body.get("target"), dict) else {} - identifiers = target.get("identifiers") if isinstance(target, dict) else [] + target = body.get("target") + identifiers = target.get("identifiers") if isinstance(target, dict) else None if not isinstance(identifiers, list): return "" for identifier in identifiers: - if not isinstance(identifier, dict): - continue - if identifier.get("scheme") == "solmara_uin" and isinstance( - identifier.get("value"), str - ): + if isinstance(identifier, dict) and identifier.get("scheme") == "solmara_uin" and isinstance(identifier.get("value"), str): return identifier["value"] return "" def requested_claims(body: dict[str, Any]) -> list[str]: - raw = body.get("claims") - if not isinstance(raw, list): - return [] - return [claim for claim in raw if isinstance(claim, str)] - - -def request_variables( - body: dict[str, Any], requested: list[str] -) -> dict[str, str] | None: - raw = body.get("variables", {}) - if not isinstance(raw, dict) or set(raw) - {"as_of_date"}: - return None - if "child-age-under-5" not in requested: - return {} - value = raw.get("as_of_date") - if not isinstance(value, str) or len(value) != 10: - return None - try: - parsed = date.fromisoformat(value) - except ValueError: - return None - if parsed.isoformat() != value: - return None - return {"as_of_date": value} - - -def public_target(target: Any) -> dict[str, Any]: - """Describe the target without echoing its raw identifier value.""" - if not isinstance(target, dict): - return {} - identifiers = target.get("identifiers") - schemes = [] - if isinstance(identifiers, list): - schemes = sorted( - { - identifier["scheme"] - for identifier in identifiers - if isinstance(identifier, dict) - and isinstance(identifier.get("scheme"), str) - } - ) - public: dict[str, Any] = {"identifier_schemes": schemes} - if isinstance(target.get("type"), str): - public["type"] = target["type"] - return public - - -def claim_catalog() -> dict[str, Any]: - claims = [ - { - "id": claim_id, - "version": "1", - "authority": route["authority"], - "notary_service_id": authority_service_id(route["client_id"]), - "disclosure": "predicate", - } - for claim_id, route in CLAIM_ROUTES.items() - ] - return { - "schema_version": API_VERSION, - "service_id": FEDERATOR_SERVICE_ID, - "response_media_type": "application/json", - "claims": claims, - "data": claims, - "composition": { - "eligible-for-child-benefit": "not_returned_by_orchestrator", - "owner": "child-benefit-programme-policy", - }, - } - - -def path_parts(path: str) -> list[str]: - parsed = urlparse(path) - return [unquote(part) for part in parsed.path.split("/") if part] - - -def ulid() -> str: - value = (int(time.time() * 1000) << 80) | secrets.randbits(80) - chars = [] - for _ in range(26): - chars.append(ULID_ALPHABET[value & 0x1F]) - value >>= 5 - return "".join(reversed(chars)) + claims = body.get("claims") + return [claim for claim in claims if isinstance(claim, str)] if isinstance(claims, list) else [] def main() -> int: - host = os.environ.get("CHILD_BENEFIT_FEDERATOR_HOST", "0.0.0.0") + host = os.environ.get("CHILD_BENEFIT_FEDERATOR_HOST", "127.0.0.1") port = int(os.environ.get("CHILD_BENEFIT_FEDERATOR_PORT", "8080")) - server = ThreadingHTTPServer((host, port), ChildBenefitFederatorHandler) - print(f"child-benefit-federator listening on http://{host}:{port}", flush=True) - server.serve_forever() + ThreadingHTTPServer((host, port), ChildBenefitFederatorHandler).serve_forever() return 0 diff --git a/scenario-runner/test_server.py b/scenario-runner/test_server.py index 46f1cbb..7d7250d 100644 --- a/scenario-runner/test_server.py +++ b/scenario-runner/test_server.py @@ -1,19 +1,18 @@ #!/usr/bin/env python3 -"""Contract tests for the scenario-runner API.""" +"""Contract tests for the Evidence-backed scenario runner and collector.""" from __future__ import annotations import base64 -import hashlib -import io import json import os -import subprocess import sys +import tempfile import threading import time import unittest import urllib.error +import urllib.parse import urllib.request from http.server import ThreadingHTTPServer from pathlib import Path @@ -23,35 +22,37 @@ sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scenario-runner")) -from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat # noqa: E402 import child_benefit_federator # noqa: E402 from server import ScenarioRunnerHandler # noqa: E402 -from scenarios import child_benefit, citizen, common, pension_survivor # noqa: E402 +from scenarios import child_benefit, citizen, common, farmer_voucher, pension_survivor # noqa: E402 from scenarios.common import StepHttpResult # noqa: E402 def b64url_decode(segment: str) -> bytes: - padded = segment + "=" * (-len(segment) % 4) - return base64.urlsafe_b64decode(padded) + return base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4)) + + +def signed_evidence(values: list[tuple[str, Any]]) -> dict[str, str]: + payload = { + "supportedValues": [ + {"providesValueFor": f"https://id.registrystack.org/solmara/concept/{name}", "value": value} + for name, value in values + ] + } + return { + "protected": common.b64url_nopad(b"{}"), + "payload": common.b64url_nopad(json.dumps(payload).encode()), + "signature": common.b64url_nopad(b"test-signature"), + } class ScenarioRunnerServerTest(unittest.TestCase): @classmethod def setUpClass(cls) -> None: os.environ["SCENARIO_RUNNER_HOST"] = "127.0.0.1" - for name in ( - "CHILD_BENEFIT_FEDERATOR_TOKEN", - "CRA_CHILD_BENEFIT_CLIENT_TOKEN", - "NIA_CHILD_BENEFIT_CLIENT_TOKEN", - "SRO_CHILD_BENEFIT_CLIENT_TOKEN", - "PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN", - "CRA_PENSION_CLIENT_TOKEN", - "SIPF_PENSION_CLIENT_TOKEN", - "NAGDI_NOTARY_TOKEN", - "CRA_CITIZEN_CLIENT_TOKEN", - "NIA_CITIZEN_CLIENT_TOKEN", - ): - os.environ.pop(name, None) + os.environ.pop("SOLMARA_EVIDENCE_ACCESS_TOKEN", None) + os.environ.pop("SOLMARA_EVIDENCE_CLIENT_KEY", None) + os.environ.pop("CHILD_BENEFIT_FEDERATOR_TOKEN", None) cls.server = ThreadingHTTPServer(("127.0.0.1", 0), ScenarioRunnerHandler) cls.port = cls.server.server_address[1] cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True) @@ -63,374 +64,51 @@ def tearDownClass(cls) -> None: cls.thread.join(timeout=5) cls.server.server_close() - def test_lists_scenarios_with_default(self) -> None: + def test_lists_all_local_evidence_scenarios(self) -> None: payload = self.fetch_json("/v1/scenarios") self.assertEqual(payload["default_scenario_id"], "birth-to-child-benefit") - self.assertGreaterEqual(len(payload["scenarios"]), 3) + self.assertEqual(len(payload["scenarios"]), 4) + self.assertTrue(all(item["availability"] == "local" for item in payload["scenarios"])) self.assertTrue(all(item["runnable"] for item in payload["scenarios"])) - def test_detail_contains_request_previews(self) -> None: - payload = self.fetch_json("/v1/scenarios/birth-to-child-benefit") - steps = payload["story"]["steps"] - self.assertTrue(steps) - self.assertIn("request_preview", steps[0]) - self.assertIn("Data-Purpose", steps[0]["request_preview"]["headers"]) - - def test_run_step_is_idempotent_when_runtime_token_missing(self) -> None: - first = self.post_json( - "/v1/scenarios/birth-to-child-benefit/steps/positive/run", {} - ) - second = self.post_json( - "/v1/scenarios/birth-to-child-benefit/steps/positive/run", {} - ) + def test_detail_previews_evidence_requirements(self) -> None: + payload = self.fetch_json("/v1/scenarios/citizen-self-service") + positive = next(step for step in payload["story"]["steps"] if step["id"] == "positive") + preview = positive["request_preview"] + self.assertEqual(preview["method"], "MULTI") + self.assertEqual(len(preview["requests"]), 2) + self.assertTrue(all(item["url"].endswith("/v1/evidence") for item in preview["requests"])) + requirements = {item["body"]["requirement"] for item in preview["requests"]} + self.assertEqual(requirements, {citizen.requirement_id(client) for client in citizen.CLIENTS}) + + def test_missing_mint_credentials_is_stable_and_safe(self) -> None: + first = self.post_json("/v1/scenarios/citizen-self-service/steps/positive/run", {}) + second = self.post_json("/v1/scenarios/citizen-self-service/steps/positive/run", {}) self.assertEqual(first["result"]["friendly"]["status"], "needs_attention") - self.assertEqual( - first["result"]["response_source"], second["result"]["response_source"] - ) - - def test_child_benefit_purpose_override_reaches_request_source(self) -> None: - purpose = "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" - payload = self.post_json( - "/v1/scenarios/birth-to-child-benefit/steps/positive/run", - {"config": {"purpose_override": purpose}}, - ) - self.assertEqual( - payload["result"]["request_source"]["headers"]["Data-Purpose"], purpose - ) - self.assertEqual( - payload["result"]["request_source"]["headers"]["Content-Type"], - "application/json", - ) - - def test_child_benefit_denial_step_ignores_override(self) -> None: - payload = self.post_json( - "/v1/scenarios/birth-to-child-benefit/steps/purpose-denial/run", - { - "config": { - "purpose_override": "https://id.registrystack.org/solmara/purpose/child-benefit-review" - } - }, - ) - self.assertEqual( - payload["result"]["request_source"]["headers"]["Data-Purpose"], - "https://id.registrystack.org/solmara/purpose/unsupported-demo-purpose", - ) - - def test_child_benefit_positive_calls_application_without_credential_composition( - self, - ) -> None: - calls: list[str] = [] - original_http_json = child_benefit.http_json - os.environ["CHILD_BENEFIT_FEDERATOR_TOKEN"] = "runtime-token" - - def fake_http_json( - method: str, - url: str, - headers: dict, - body: dict | None = None, - timeout: float = 8.0, - ) -> StepHttpResult: - calls.append(url) - return StepHttpResult( - 200, - { - "orchestration": { - "service_id": "child-benefit-federator", - "decision": "not_composed", - }, - "results": [ - {"claim_id": "birth-is-registered", "satisfied": True}, - {"claim_id": "population-record-active", "satisfied": True}, - ], - "source_trace": [ - { - "authority": "Civil Registration Authority", - "service_id": "cra-notary", - } - ], - }, - {"content-type": "application/json"}, - ) - - try: - child_benefit.http_json = fake_http_json - result = child_benefit.run_step({}, "positive") - finally: - child_benefit.http_json = original_http_json - os.environ.pop("CHILD_BENEFIT_FEDERATOR_TOKEN", None) - - self.assertTrue(all(call.endswith("/v1/evaluations") for call in calls)) - self.assertNotIn("credential", result) - self.assertEqual( - result["request_source"]["body"]["format"], - "application/json", - ) - self.assertEqual( - result["request_source"]["body"]["variables"], - {"as_of_date": common.CHILD_BENEFIT_AS_OF_DATE}, - ) - self.assertIn( - "population-record-active", result["request_source"]["body"]["claims"] - ) - self.assertEqual( - result["source_trace"][0]["authority"], "Civil Registration Authority" - ) + self.assertEqual(first["result"]["response_source"], second["result"]["response_source"]) + serialized = json.dumps(first) + self.assertIn("Bearer [runtime token hidden]", serialized) + self.assertNotIn("Bearer runtime-token", serialized) - def test_pension_survivor_purpose_override_reaches_request_source(self) -> None: - purpose = ( - "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" - ) - payload = self.post_json( - "/v1/scenarios/death-to-pension-survivor/steps/stop-payment/run", - {"config": {"purpose_override": purpose}}, - ) - self.assertEqual( - payload["result"]["request_source"]["headers"]["Data-Purpose"], purpose - ) - - def test_pension_stop_calls_cra_and_sipf_and_derives_the_application_decision( - self, - ) -> None: - calls: list[tuple[str, dict[str, Any]]] = [] - original_http_json = pension_survivor.http_json - for name in ("CRA_PENSION_CLIENT_TOKEN", "SIPF_PENSION_CLIENT_TOKEN"): - os.environ[name] = f"token-{name.lower()}" - - def fake_http_json( - method: str, - url: str, - headers: dict, - body: dict | None = None, - timeout: float = 8.0, - ) -> StepHttpResult: - assert body is not None - calls.append((url, body)) - claim_id = body["claims"][0] - return StepHttpResult( - 200, - { - "results": [ - { - "evaluation_id": f"eval-{claim_id}", - "claim_id": claim_id, - "satisfied": True, - "disclosure": "predicate", - } - ] - }, - {"content-type": common.CLAIM_RESULT_FORMAT}, - ) - - try: - pension_survivor.http_json = fake_http_json - result = pension_survivor.run_step({}, "stop-payment") - finally: - pension_survivor.http_json = original_http_json - for name in ("CRA_PENSION_CLIENT_TOKEN", "SIPF_PENSION_CLIENT_TOKEN"): - os.environ.pop(name, None) - - self.assertEqual( - [body["claims"] for _, body in calls], - [["person-is-deceased"], ["pension-payment-active"]], - ) - self.assertTrue(calls[0][0].endswith("/v1/evaluations")) - self.assertTrue(calls[1][0].endswith("/v1/evaluations")) - self.assertEqual( - [trace["service_id"] for trace in result["source_trace"]], - ["cra-notary", "sipf-notary"], - ) - self.assertIs(result["derived_decisions"]["pension-payment-should-stop"], True) - self.assertNotIn( - "pension-payment-should-stop", [body["claims"][0] for _, body in calls] - ) - - def test_survivor_credential_is_issued_by_sipf(self) -> None: - original_http_json = pension_survivor.http_json - original_credential_attempt = pension_survivor.credential_attempt - os.environ["SIPF_PENSION_CLIENT_TOKEN"] = "sipf-token" - captured: dict[str, Any] = {} - - def fake_http_json( - method: str, - url: str, - headers: dict, - body: dict | None = None, - timeout: float = 8.0, - ) -> StepHttpResult: - return StepHttpResult( - 200, - { - "results": [ - { - "evaluation_id": "eval-survivor", - "claim_id": "survivor-is-eligible", - "satisfied": True, - "disclosure": "predicate", - } - ] - }, - {"content-type": common.CLAIM_RESULT_FORMAT}, - ) - - def fake_credential_attempt( - url, token, purpose, evaluation_result, profile, claim_ids, service_id - ): - captured.update( - url=url, - token=token, - profile=profile, - claim_ids=claim_ids, - service_id=service_id, - ) - return {"credential": {"status": "issued"}} - - try: - pension_survivor.http_json = fake_http_json - pension_survivor.credential_attempt = fake_credential_attempt - result = pension_survivor.run_step({}, "survivor-benefit") - finally: - pension_survivor.http_json = original_http_json - pension_survivor.credential_attempt = original_credential_attempt - os.environ.pop("SIPF_PENSION_CLIENT_TOKEN", None) - - self.assertEqual(captured["service_id"], "sipf-notary") - self.assertEqual( - captured["profile"], "sipf-survivor-benefit.survivor-benefit-status" - ) - self.assertEqual(captured["claim_ids"], ["survivor-is-eligible"]) - self.assertTrue(captured["url"].endswith("/v1/credentials")) - self.assertEqual( - result["request_source"]["body"]["format"], - common.CLAIM_RESULT_FORMAT, - ) - self.assertEqual(result["credential"]["status"], "issued") - - def test_farmer_voucher_purpose_override_reaches_request_source(self) -> None: - purpose = "https://id.registrystack.org/solmara/purpose/citizen-self-service" + def test_purpose_overrides_reach_evidence_request_bodies(self) -> None: + purpose = "pension-payment-review" payload = self.post_json( "/v1/scenarios/farmer-climate-smart-voucher/steps/positive/run", {"config": {"purpose_override": purpose}}, ) - self.assertEqual( - payload["result"]["request_source"]["headers"]["Data-Purpose"], purpose - ) - self.assertEqual( - payload["result"]["request_source"]["body"]["format"], - common.CLAIM_RESULT_FORMAT, - ) - - def test_farmer_voucher_denial_step_ignores_override(self) -> None: - payload = self.post_json( - "/v1/scenarios/farmer-climate-smart-voucher/steps/purpose-denial/run", - { - "config": { - "purpose_override": "https://id.registrystack.org/solmara/purpose/livestock-movement-control" - } - }, - ) - self.assertEqual( - payload["result"]["request_source"]["headers"]["Data-Purpose"], - "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review", - ) + self.assertEqual(payload["result"]["request_source"]["body"]["purpose"], purpose) - def test_citizen_purpose_override_reaches_request_source(self) -> None: - purpose = "https://id.registrystack.org/solmara/purpose/pension-payment-review" - payload = self.post_json( - "/v1/scenarios/citizen-self-service/steps/positive/run", - {"config": {"purpose_override": purpose}}, - ) - self.assertEqual( - payload["result"]["request_source"]["headers"]["Data-Purpose"], purpose - ) - - def test_citizen_denial_step_ignores_override(self) -> None: - payload = self.post_json( + def test_denial_steps_ignore_purpose_overrides(self) -> None: + citizen_result = self.post_json( "/v1/scenarios/citizen-self-service/steps/purpose-denial/run", - { - "config": { - "purpose_override": "https://id.registrystack.org/solmara/purpose/citizen-self-service" - } - }, - ) - self.assertEqual( - payload["result"]["request_source"]["headers"]["Data-Purpose"], - "https://id.registrystack.org/solmara/purpose/unsupported-demo-purpose", - ) - - def test_citizen_calls_cra_and_nia_and_uses_nia_for_issuance(self) -> None: - calls: list[tuple[str, dict[str, Any]]] = [] - original_http_json = citizen.http_json - original_credential_attempt = citizen.credential_attempt - for name in ("CRA_CITIZEN_CLIENT_TOKEN", "NIA_CITIZEN_CLIENT_TOKEN"): - os.environ[name] = f"token-{name.lower()}" - captured: dict[str, Any] = {} - - def fake_http_json( - method: str, - url: str, - headers: dict, - body: dict | None = None, - timeout: float = 8.0, - ) -> StepHttpResult: - assert body is not None - calls.append((url, body)) - claim_id = body["claims"][0] - return StepHttpResult( - 200, - { - "results": [ - { - "evaluation_id": f"eval-{claim_id}", - "claim_id": claim_id, - "satisfied": True, - "disclosure": "predicate", - } - ] - }, - {"content-type": common.CLAIM_RESULT_FORMAT}, - ) - - def fake_credential_attempt( - url, token, purpose, evaluation_result, profile, claim_ids, service_id - ): - captured.update( - url=url, - token=token, - profile=profile, - claim_ids=claim_ids, - service_id=service_id, - ) - return {"credential": {"status": "issued"}} - - try: - citizen.http_json = fake_http_json - citizen.credential_attempt = fake_credential_attempt - result = citizen.run_step({}, "positive") - finally: - citizen.http_json = original_http_json - citizen.credential_attempt = original_credential_attempt - for name in ("CRA_CITIZEN_CLIENT_TOKEN", "NIA_CITIZEN_CLIENT_TOKEN"): - os.environ.pop(name, None) - - self.assertEqual( - [body["claims"] for _, body in calls], - [["civil-record-linked"], ["citizen-population-record-active"]], - ) - self.assertEqual( - [body["format"] for _, body in calls], - [common.CLAIM_RESULT_FORMAT, common.CLAIM_RESULT_FORMAT], - ) - self.assertEqual( - [trace["service_id"] for trace in result["source_trace"]], - ["cra-notary", "nia-notary"], - ) - self.assertEqual(captured["service_id"], "nia-notary") - self.assertEqual( - captured["profile"], "nia-citizen-status.citizen-population-status" - ) - self.assertEqual(captured["claim_ids"], ["citizen-population-record-active"]) - self.assertTrue(captured["url"].endswith("/v1/credentials")) - self.assertNotIn("citizen-self-service-summary", json.dumps(result)) + {"config": {"purpose_override": common.PURPOSES["citizen_self_service"]}}, + )["result"] + farmer_result = self.post_json( + "/v1/scenarios/farmer-climate-smart-voucher/steps/purpose-denial/run", + {"config": {"purpose_override": common.PURPOSES["livestock"]}}, + )["result"] + self.assertEqual(citizen_result["request_source"]["purpose"], "unsupported-demo-purpose") + self.assertEqual(farmer_result["request_source"]["body"]["purpose"], common.PURPOSES["voucher"]) def test_unknown_scenario_returns_404(self) -> None: with self.assertRaises(urllib.error.HTTPError) as raised: @@ -438,625 +116,200 @@ def test_unknown_scenario_returns_404(self) -> None: self.assertEqual(raised.exception.code, 404) raised.exception.close() - def fetch_json(self, path: str) -> dict: - with urllib.request.urlopen( - f"http://127.0.0.1:{self.port}{path}", timeout=5 - ) as response: - return json.loads(response.read().decode("utf-8")) + def fetch_json(self, path: str) -> dict[str, Any]: + with urllib.request.urlopen(f"http://127.0.0.1:{self.port}{path}", timeout=5) as response: + return json.loads(response.read()) - def post_json(self, path: str, body: dict) -> dict: + def post_json(self, path: str, body: dict[str, Any]) -> dict[str, Any]: request = urllib.request.Request( f"http://127.0.0.1:{self.port}{path}", - data=json.dumps(body).encode("utf-8"), + data=json.dumps(body).encode(), headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(request, timeout=5) as response: - return json.loads(response.read().decode("utf-8")) - - -class HolderProofTest(unittest.TestCase): - """Unit coverage for the did:jwk holder key-binding proof used at credential issuance.""" - - def test_holder_keypair_produces_a_valid_did_jwk(self) -> None: - keypair = common.holder_keypair() - self.assertTrue(keypair.holder_id.startswith("did:jwk:")) - jwk = json.loads(b64url_decode(keypair.holder_id.removeprefix("did:jwk:"))) - self.assertEqual(jwk["kty"], "OKP") - self.assertEqual(jwk["crv"], "Ed25519") - public_bytes = keypair.private_key.public_key().public_bytes( - Encoding.Raw, PublicFormat.Raw - ) - self.assertEqual(jwk["x"], common.b64url_nopad(public_bytes)) - - def test_holder_proof_header_and_payload_bindings(self) -> None: - keypair = common.holder_keypair() - claim_ids = ["birth-is-registered", "child-age-under-5"] - proof = common.holder_proof( - keypair, - audience="sipf-notary", - evaluation_id="eval-123", - credential_profile="sipf-survivor-benefit.survivor-benefit-status", - disclosure="predicate", - claim_ids=claim_ids, - ) - header_b64, payload_b64, signature_b64 = proof.split(".") - header = json.loads(b64url_decode(header_b64)) - payload = json.loads(b64url_decode(payload_b64)) - - self.assertEqual( - header, {"alg": "EdDSA", "typ": "kb+jwt", "kid": keypair.holder_id} - ) - self.assertEqual(payload["sub"], keypair.holder_id) - self.assertEqual(payload["aud"], "sipf-notary") - self.assertEqual(payload["evaluation_id"], "eval-123") - self.assertEqual( - payload["credential_profile"], - "sipf-survivor-benefit.survivor-benefit-status", - ) - self.assertEqual(payload["claims"], claim_ids) - self.assertEqual( - payload["disclosure"], - common.b64url_nopad(hashlib.sha256(b"predicate").digest()), - ) - self.assertLessEqual(payload["iat"], int(time.time())) - self.assertEqual(payload["exp"] - payload["iat"], 60) - self.assertTrue(payload["jti"]) + return json.loads(response.read()) - signing_input = f"{header_b64}.{payload_b64}".encode("ascii") - signature = b64url_decode(signature_b64) - # Raises cryptography.exceptions.InvalidSignature if the proof does not verify. - keypair.private_key.public_key().verify(signature, signing_input) - def test_holder_proof_jti_is_unique_per_call(self) -> None: - keypair = common.holder_keypair() - kwargs = { - "audience": "a", - "evaluation_id": "e", - "credential_profile": "p", - "disclosure": "predicate", - "claim_ids": ["x"], - } - first = common.holder_proof(keypair, **kwargs) - second = common.holder_proof(keypair, **kwargs) - first_jti = json.loads(b64url_decode(first.split(".")[1]))["jti"] - second_jti = json.loads(b64url_decode(second.split(".")[1]))["jti"] - self.assertNotEqual(first_jti, second_jti) +class MintAndEvidenceContractTest(unittest.TestCase): + def tearDown(self) -> None: + common._TOKEN_CACHE = ("", 0.0) + for name in ( + "SOLMARA_EVIDENCE_ACCESS_TOKEN", + "SOLMARA_MINT_URL", + "SOLMARA_MINT_ASSERTION_AUDIENCE", + "SOLMARA_EVIDENCE_CLIENT_ID", + "SOLMARA_EVIDENCE_CLIENT_KEY", + ): + os.environ.pop(name, None) - def test_credential_attempt_includes_holder_binding_and_matching_claims( - self, - ) -> None: - claim_ids = ["survivor-is-eligible"] - evaluation_result = StepHttpResult( - 200, {"results": [{"evaluation_id": "eval-999"}]}, {} - ) - captured: dict[str, Any] = {} + def test_evidence_body_uses_bounded_selector_and_unique_nonce(self) -> None: + first = common.evidence_body("2300018263", "requirement", "purpose") + second = common.evidence_body("2300018263", "requirement", "purpose") + self.assertEqual(first["subjects"][0]["selector"]["values"], {"uin": "2300018263"}) + self.assertEqual(len(first["requestNonce"]), 43) + self.assertNotEqual(first["requestNonce"], second["requestNonce"]) - def fake_http_json(method, url, headers, body=None, timeout=8.0): - ( - captured["method"], - captured["url"], - captured["headers"], - captured["body"], - ) = method, url, headers, body - return StepHttpResult(400, {"code": "credential.holder_proof_required"}, {}) + def test_signed_evidence_is_preserved_and_normalized(self) -> None: + signed = signed_evidence([("person-is-deceased", True), ("pension-payment-active", False)]) + result = common.normalized_evidence_result(StepHttpResult(200, signed, {"content-type": common.EVIDENCE_JWS_MEDIA_TYPE})) + self.assertEqual([entry["satisfied"] for entry in result.body["results"]], [True, False]) + self.assertEqual(result.body["signed_evidence"], signed) - original = common.http_json - common.http_json = fake_http_json + def test_private_key_jwt_and_mint_token_request(self) -> None: try: - result = common.credential_attempt( - "http://example.invalid/v1/credentials", - "runtime-token", - "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination", - evaluation_result, - "sipf-survivor-benefit.survivor-benefit-status", - claim_ids, - "sipf-notary", - ) - finally: - common.http_json = original - - body = result["credential_source"]["body"] - self.assertEqual(body["claims"], claim_ids) - self.assertEqual(body["holder"]["binding"], "did") - self.assertTrue(body["holder"]["id"].startswith("did:jwk:")) - proof_payload = json.loads(b64url_decode(body["holder"]["proof"].split(".")[1])) - self.assertEqual(proof_payload["aud"], "sipf-notary") - self.assertEqual(proof_payload["claims"], claim_ids) - - # request_source redaction is unaffected by the new holder object. - self.assertEqual( - result["credential_source"]["headers"]["x-api-key"], - "[runtime token hidden]", - ) - # And the outgoing body actually carried the same holder object (nothing lost in transit). - self.assertEqual(captured["body"]["holder"], body["holder"]) - + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption + except ModuleNotFoundError: + self.skipTest("cryptography is not installed") + + private_key = Ed25519PrivateKey.generate() + raw = private_key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption()) + jwk = {"kty": "OKP", "crv": "Ed25519", "kid": "scenario-client-1", "d": common.b64url_nopad(raw)} + captured: dict[str, Any] = {} -class ChildBenefitFederatorTest(unittest.TestCase): - TOKEN_ENVS = ( - "CRA_CHILD_BENEFIT_CLIENT_TOKEN", - "NIA_CHILD_BENEFIT_CLIENT_TOKEN", - "SRO_CHILD_BENEFIT_CLIENT_TOKEN", - "PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN", - ) + class Response: + def __enter__(self): + return self - def setUp(self) -> None: - for name in self.TOKEN_ENVS: - os.environ[name] = f"token-{name.lower()}" + def __exit__(self, *_args): + return None - def tearDown(self) -> None: - for name in self.TOKEN_ENVS: - os.environ.pop(name, None) - os.environ.pop(child_benefit_federator.FEDERATOR_TOKEN_ENV, None) - - def test_catalog_lists_authority_predicates_without_eligibility_composition( - self, - ) -> None: - catalog = child_benefit_federator.claim_catalog() - ids = {entry["id"] for entry in catalog["claims"]} - - self.assertEqual( - ids, - { - "birth-is-registered", - "child-age-under-5", - "population-record-active", - "household-below-poverty-threshold", - "not-already-enrolled", - }, - ) - self.assertNotIn("eligible-for-child-benefit", ids) - self.assertEqual( - catalog["response_media_type"], - "application/json", - ) - self.assertEqual( - catalog["composition"]["eligible-for-child-benefit"], - "not_returned_by_orchestrator", - ) - self.assertEqual(catalog["data"], catalog["claims"]) + def read(self): + return json.dumps({"access_token": "minted-token", "expires_in": 120}).encode() - def test_collection_groups_five_claims_into_four_ordinary_notary_calls( - self, - ) -> None: - calls: list[tuple[str, dict[str, str], dict[str, Any]]] = [] - original_http_json = child_benefit_federator.http_json + original = urllib.request.urlopen - def fake_http_json( - method: str, - url: str, - headers: dict[str, str], - body: dict[str, Any], - timeout: float = 8.0, - ) -> StepHttpResult: - calls.append((url, headers, body)) - return StepHttpResult( - 200, - { - "results": [ - { - "evaluation_id": f"internal-{claim_id}", - "claim_id": claim_id, - "claim_version": "1", - "satisfied": True, - "disclosure": "predicate", - "issued_at": "2026-07-15T00:00:00Z", - "source_row": {"private": "must-not-cross"}, - } - for claim_id in body["claims"] - ] - }, - {"content-type": common.CLAIM_RESULT_FORMAT}, - ) + def fake_urlopen(request, **kwargs): + captured["url"] = request.full_url + captured["form"] = urllib.parse.parse_qs(request.data.decode()) + captured["context"] = kwargs.get("context") + return Response() - try: - child_benefit_federator.http_json = fake_http_json - evidence = child_benefit_federator.collect_evidence( - "2300010248", - list(child_benefit.CLAIMS), - common.PURPOSES["child_benefit"], - { - "type": "Person", - "identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}], - }, - {"as_of_date": common.CHILD_BENEFIT_AS_OF_DATE}, + with tempfile.TemporaryDirectory() as directory: + key_path = Path(directory) / "client.jwk" + key_path.write_text(json.dumps(jwk)) + os.environ.update( + SOLMARA_MINT_URL="https://localhost:4341", + SOLMARA_MINT_ASSERTION_AUDIENCE="https://mint.evidence.solmara.invalid/token", + SOLMARA_EVIDENCE_CLIENT_ID="solmara-scenario-runner", + SOLMARA_EVIDENCE_CLIENT_KEY=str(key_path), ) - finally: - child_benefit_federator.http_json = original_http_json + urllib.request.urlopen = fake_urlopen + try: + token = common.evidence_access_token() + finally: + urllib.request.urlopen = original - self.assertEqual(len(calls), 4) - self.assertEqual( - calls[0][2]["claims"], ["birth-is-registered", "child-age-under-5"] - ) + self.assertEqual(token, "minted-token") + self.assertEqual(captured["url"], "https://localhost:4341/token") + assertion = captured["form"]["client_assertion"][0] + header_segment, claims_segment, signature_segment = assertion.split(".") + header = json.loads(b64url_decode(header_segment)) + claims = json.loads(b64url_decode(claims_segment)) + self.assertEqual(header, {"alg": "EdDSA", "typ": "JWT", "kid": "scenario-client-1"}) + self.assertEqual(claims["iss"], "solmara-scenario-runner") + self.assertEqual(claims["sub"], "solmara-scenario-runner") self.assertEqual( - calls[0][2]["variables"], - {"as_of_date": common.CHILD_BENEFIT_AS_OF_DATE}, + claims["aud"], "https://mint.evidence.solmara.invalid/token" ) - self.assertTrue(all("variables" not in body for _, _, body in calls[1:])) - self.assertEqual(calls[1][2]["claims"], ["population-record-active"]) - self.assertEqual(calls[2][2]["claims"], ["household-below-poverty-threshold"]) - self.assertEqual(calls[3][2]["claims"], ["not-already-enrolled"]) - self.assertTrue(all(url.endswith("/v1/evaluations") for url, _, _ in calls)) - self.assertTrue( - all( - headers["Accept"] == common.CLAIM_RESULT_FORMAT - for _, headers, _ in calls - ) - ) - self.assertEqual( - [result["claim_id"] for result in evidence["results"]], child_benefit.CLAIMS - ) - self.assertEqual( - [trace["service_id"] for trace in evidence["source_trace"]], - ["cra-notary", "nia-notary", "sro-notary", "programme-notary"], - ) - self.assertEqual(evidence["orchestration"]["decision"], "not_composed") - serialized = json.dumps(evidence, sort_keys=True) - self.assertNotIn("2300010248", serialized) - self.assertNotIn("internal-", serialized) - self.assertNotIn("source_row", serialized) - self.assertNotIn("must-not-cross", serialized) - self.assertNotIn("token-", serialized) + self.assertEqual(claims["exp"] - claims["iat"], 120) + private_key.public_key().verify(b64url_decode(signature_segment), f"{header_segment}.{claims_segment}".encode()) - def test_unavailable_authority_is_an_error_not_a_false_predicate(self) -> None: - original_http_json = child_benefit_federator.http_json - child_benefit_federator.http_json = lambda *_args, **_kwargs: StepHttpResult( - 503, - {"code": "upstream.unavailable"}, - {"content-type": "application/problem+json"}, - ) - try: - with self.assertRaises( - child_benefit_federator.AuthorityUpstreamError - ) as raised: - child_benefit_federator.collect_evidence( - "2300010248", - ["birth-is-registered"], - common.PURPOSES["child_benefit"], - {}, - {}, - ) - finally: - child_benefit_federator.http_json = original_http_json - - self.assertEqual(raised.exception.status, 503) - self.assertEqual(raised.exception.code, "upstream.unavailable") - - def test_authority_must_return_exactly_the_requested_predicate_set(self) -> None: - route = child_benefit_federator.CLAIM_ROUTES["birth-is-registered"] - response = StepHttpResult( - 200, - { - "results": [ - { - "claim_id": "population-record-active", - "satisfied": True, - "disclosure": "predicate", - } - ] - }, - {}, - ) - - with self.assertRaises( - child_benefit_federator.AuthorityUpstreamError - ) as raised: - child_benefit_federator.minimized_results( - route, ["birth-is-registered"], response - ) - - self.assertEqual(raised.exception.code, "unexpected_claim_results") - - def test_request_body_has_a_hard_size_limit(self) -> None: - handler = object.__new__(child_benefit_federator.ChildBenefitFederatorHandler) - handler.headers = { - "Content-Length": str(child_benefit_federator.MAX_REQUEST_BODY_BYTES + 1) - } - handler.rfile = io.BytesIO(b"") - with self.assertRaises(child_benefit_federator.RequestBodyError) as rejected: - handler.read_body() - - self.assertEqual(rejected.exception.status, 413) - - def test_duplicate_claims_are_rejected_before_authority_calls(self) -> None: - status, content_type, payload = self.post_application( - { - "target": { - "identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}] - }, - "claims": ["birth-is-registered", "birth-is-registered"], - "disclosure": "predicate", - "format": "application/json", - } - ) +class EvidenceScenarioTest(unittest.TestCase): + def setUp(self) -> None: + os.environ["SOLMARA_EVIDENCE_ACCESS_TOKEN"] = "runtime-token" - self.assertEqual(status, 400) - self.assertEqual(content_type, "application/problem+json") - self.assertEqual(payload["code"], "request.invalid") + def tearDown(self) -> None: + os.environ.pop("SOLMARA_EVIDENCE_ACCESS_TOKEN", None) - def test_raw_household_request_is_denied_without_echoing_the_claim(self) -> None: - status, content_type, payload = self.post_application( - { - "target": { - "identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}] - }, - "claims": ["household-poverty-score"], - "disclosure": "value", - "format": "application/json", - } - ) + def test_pension_combines_two_signed_requirements_in_the_application(self) -> None: + calls: list[dict[str, Any]] = [] + original = pension_survivor.http_json - self.assertEqual(status, 403) - self.assertEqual(content_type, "application/problem+json") - self.assertEqual(payload["code"], "pdp.purpose_not_permitted") - self.assertNotIn("household-poverty-score", json.dumps(payload)) + def fake_http_json(method, url, headers, body=None, timeout=8.0): + calls.append(body) + requirement = body["requirement"] + concept = "person-is-deceased" if "cra-pension-death" in requirement else "pension-payment-active" + return StepHttpResult(200, signed_evidence([(concept, True)]), {}) - def test_success_uses_the_child_benefit_evidence_media_type(self) -> None: - original_collect_evidence = child_benefit_federator.collect_evidence - child_benefit_federator.collect_evidence = lambda *_args: { - "schema_version": child_benefit_federator.API_VERSION, - "results": [], - "source_trace": [], - } + pension_survivor.http_json = fake_http_json try: - status, content_type, _payload = self.post_application( - { - "target": { - "identifiers": [ - {"scheme": "solmara_uin", "value": "2300010248"} - ] - }, - "claims": ["birth-is-registered"], - "disclosure": "predicate", - "format": "application/json", - } - ) + result = pension_survivor.run_step({}, "stop-payment") finally: - child_benefit_federator.collect_evidence = original_collect_evidence + pension_survivor.http_json = original + self.assertEqual(len(calls), 2) + self.assertTrue(result["derived_decisions"]["pension-payment-should-stop"]) + self.assertEqual({trace["service_id"] for trace in result["source_trace"]}, {"registry-evidence"}) + self.assertEqual(len(result["response_source"]["body"]["signed_evidence"]), 2) - self.assertEqual(status, 200) - self.assertEqual(content_type, "application/json") - - def test_obsolete_federation_route_is_absent(self) -> None: - token = "test-application-token" - os.environ[child_benefit_federator.FEDERATOR_TOKEN_ENV] = token - server = ThreadingHTTPServer( - ("127.0.0.1", 0), child_benefit_federator.ChildBenefitFederatorHandler - ) - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() - request = urllib.request.Request( - f"http://127.0.0.1:{server.server_address[1]}/federation/v1/evaluations", - data=b"{}", - headers={"x-api-key": token, "Content-Type": "application/json"}, - method="POST", - ) - try: - with self.assertRaises(urllib.error.HTTPError) as rejected: - urllib.request.urlopen(request, timeout=5) - payload = json.loads(rejected.exception.read().decode("utf-8")) - rejected.exception.close() - finally: - server.shutdown() - thread.join(timeout=5) - server.server_close() + def test_citizen_requests_cra_and_nia_requirements(self) -> None: + calls: list[str] = [] + original = citizen.http_json - self.assertEqual(rejected.exception.code, 404) - self.assertEqual(payload["code"], "not_found") + def fake_http_json(method, url, headers, body=None, timeout=8.0): + calls.append(body["requirement"]) + concept = "civil-record-linked" if "cra-citizen" in body["requirement"] else "citizen-population-record-active" + return StepHttpResult(200, signed_evidence([(concept, True)]), {}) - def post_application(self, body: dict[str, Any]) -> tuple[int, str, dict[str, Any]]: - token = "test-application-token" - os.environ[child_benefit_federator.FEDERATOR_TOKEN_ENV] = token - server = ThreadingHTTPServer( - ("127.0.0.1", 0), child_benefit_federator.ChildBenefitFederatorHandler - ) - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() - request = urllib.request.Request( - f"http://127.0.0.1:{server.server_address[1]}/v1/evaluations", - data=json.dumps(body).encode("utf-8"), - headers={ - "x-api-key": token, - "Data-Purpose": common.PURPOSES["child_benefit"], - "Content-Type": "application/json", - "Accept": "application/json", - }, - method="POST", - ) + citizen.http_json = fake_http_json try: - try: - with urllib.request.urlopen(request, timeout=5) as response: - return ( - response.status, - response.headers.get_content_type(), - json.loads(response.read().decode("utf-8")), - ) - except urllib.error.HTTPError as error: - return ( - error.code, - error.headers.get_content_type(), - json.loads(error.read().decode("utf-8")), - ) + result = citizen.run_step({}, "positive") finally: - server.shutdown() - thread.join(timeout=5) - server.server_close() - - -class StdlibOnlyImportTest(unittest.TestCase): - def test_scenarios_import_without_cryptography(self) -> None: - """Preview-only consumers (scripts/smoke-story-previews.py) run under the - system Python, so importing the scenario modules must not require the - cryptography package; only actually signing a holder proof may.""" - code = ( - "import builtins\n" - "real_import = builtins.__import__\n" - "def guard(name, *args, **kwargs):\n" - " if name.split('.')[0] == 'cryptography':\n" - " raise ModuleNotFoundError(name)\n" - " return real_import(name, *args, **kwargs)\n" - "builtins.__import__ = guard\n" - "import scenarios.common\n" - "import scenarios.child_benefit\n" - ) - result = subprocess.run( - [sys.executable, "-c", code], cwd=ROOT, capture_output=True, text=True - ) - self.assertEqual(result.returncode, 0, result.stderr) - - -class CredentialSummaryVctTest(unittest.TestCase): - VCT = ( - "https://id.registrystack.org/solmara/vct/child-benefit-enrollment-eligibility" - ) - - @staticmethod - def compact_sd_jwt(payload: dict[str, Any]) -> str: - def b64url(data: bytes) -> str: - return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") - - header = b64url(json.dumps({"alg": "EdDSA", "typ": "dc+sd-jwt"}).encode()) - body = b64url(json.dumps(payload).encode()) - return f"{header}.{body}.fake-signature~ZmFrZS1kaXNjbG9zdXJl~" - - def test_issued_summary_extracts_vct_from_sd_jwt_payload(self) -> None: - body = { - "credential": self.compact_sd_jwt( - {"vct": self.VCT, "iss": "did:web:child-benefit-notary"} - ), - "credential_profile": "child_benefit_eligibility_sd_jwt", - "disclosures": ["a", "b"], - } - summary = common.credential_summary( - "profile", "did:jwk:x", StepHttpResult(200, body, {}) - ) - self.assertEqual(summary["status"], "issued") - self.assertEqual(summary["vct"], self.VCT) - - def test_issued_summary_without_decodable_credential_has_no_vct(self) -> None: - body = {"credential": "not-a-jwt", "disclosures": []} - summary = common.credential_summary( - "profile", "did:jwk:x", StepHttpResult(200, body, {}) - ) - self.assertEqual(summary["status"], "issued") - self.assertIsNone(summary["vct"]) - - def test_issued_summary_with_undecodable_payload_has_no_vct(self) -> None: - body = { - "credential": "eyJhbGciOiJFZERTQSJ9.%%%not-base64%%%.sig", - "disclosures": [], - } - summary = common.credential_summary( - "profile", "did:jwk:x", StepHttpResult(200, body, {}) - ) - self.assertEqual(summary["status"], "issued") - self.assertIsNone(summary["vct"]) + citizen.http_json = original + self.assertEqual(calls, [citizen.requirement_id(client) for client in citizen.CLIENTS]) + self.assertEqual(len(result["source_trace"]), 2) -class FriendlyResultTest(unittest.TestCase): - COPY = { - "positive": { - "met": ( - "Yes. Mateo qualifies for review.", - "All four facts came back met.", - ), - }, - "poverty-control": { - "unmet": ( - "Rejected: the household is above the threshold.", - "The caseworker never sees the income.", - ), - }, - } - - def test_pdp_denial_reads_as_designed(self) -> None: - result = StepHttpResult( - 403, {"code": "pdp.purpose_not_permitted", "detail": "nope"}, {} - ) - friendly = common.friendly_result("positive", result, self.COPY) - self.assertEqual(friendly["status"], "done") - self.assertEqual(friendly["title"], "Refused, exactly as designed.") - - def test_all_claims_met_uses_step_copy(self) -> None: - body = { - "results": [ - {"claim_id": "a", "satisfied": True}, - {"claim_id": "b", "satisfied": True}, - ] - } - friendly = common.friendly_result( - "positive", StepHttpResult(200, body, {}), self.COPY - ) - self.assertEqual(friendly["status"], "done") - self.assertEqual(friendly["title"], "Yes. Mateo qualifies for review.") - self.assertIn({"label": "Claims met", "value": "2 of 2"}, friendly["facts"]) - - def test_unmet_claim_uses_unmet_copy_and_names_claim(self) -> None: - body = { - "results": [ - {"claim_id": "household-below-poverty-threshold", "satisfied": False} - ] - } - friendly = common.friendly_result( - "poverty-control", StepHttpResult(200, body, {}), self.COPY - ) - self.assertEqual(friendly["status"], "done") - self.assertEqual( - friendly["title"], "Rejected: the household is above the threshold." - ) - - def test_unmet_claim_without_copy_falls_back_to_generic_rejection(self) -> None: - body = {"results": [{"claim_id": "child-age-under-5", "satisfied": False}]} - friendly = common.friendly_result( - "deceased-control", StepHttpResult(200, body, {}), self.COPY - ) - self.assertEqual(friendly["status"], "done") - self.assertEqual(friendly["title"], "Rejected, exactly as designed.") - self.assertIn("child-age-under-5", friendly["message"]) - - def test_refused_copy_frames_intended_denial_as_done(self) -> None: - copy = { - "cause-of-death-denial": { - "refused": ( - "Refused: that question does not exist here.", - "No such claim is offered.", - ) - } - } - result = StepHttpResult( - 404, - { - "code": "claim.not_found", - "detail": "the requested claim is not available", - }, - {}, - ) - friendly = common.friendly_result("cause-of-death-denial", result, copy) - self.assertEqual(friendly["status"], "done") - self.assertEqual( - friendly["title"], "Refused: that question does not exist here." - ) +class ChildBenefitCollectorTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + os.environ[child_benefit_federator.FEDERATOR_TOKEN_ENV] = "collector-token" + cls.server = ThreadingHTTPServer(("127.0.0.1", 0), child_benefit_federator.ChildBenefitFederatorHandler) + cls.port = cls.server.server_address[1] + cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True) + cls.thread.start() - def test_non_pdp_error_without_refused_copy_needs_attention(self) -> None: - result = StepHttpResult( - 404, {"code": "claim.not_found", "detail": "missing"}, {} - ) - friendly = common.friendly_result("positive", result, self.COPY) - self.assertEqual(friendly["status"], "needs_attention") + @classmethod + def tearDownClass(cls) -> None: + cls.server.shutdown() + cls.thread.join(timeout=5) + cls.server.server_close() + os.environ.pop(child_benefit_federator.FEDERATOR_TOKEN_ENV, None) - def test_no_response_needs_attention(self) -> None: - friendly = common.friendly_result( - "positive", StepHttpResult(None, {}, {}, "URLError"), self.COPY - ) - self.assertEqual(friendly["status"], "needs_attention") + def request(self, path: str, *, token: str = "collector-token", body: dict[str, Any] | None = None): + headers = {"x-api-key": token} + data = None + method = "GET" + if body is not None: + headers.update({"Content-Type": "application/json", "Data-Purpose": common.PURPOSES["child_benefit"]}) + data = json.dumps(body).encode() + method = "POST" + return urllib.request.urlopen( + urllib.request.Request(f"http://127.0.0.1:{self.port}{path}", data=data, headers=headers, method=method), + timeout=5, + ) + + def test_catalog_lists_five_source_owned_concepts(self) -> None: + with self.request("/v1/claims") as response: + payload = json.loads(response.read()) + self.assertEqual({item["claim_id"] for item in payload["claims"]}, set(child_benefit.CLAIMS)) + self.assertNotIn("eligible-for-child-benefit", json.dumps(payload)) + + def test_requires_local_application_auth(self) -> None: + with self.assertRaises(urllib.error.HTTPError) as raised: + self.request("/v1/claims", token="wrong") + self.assertEqual(raised.exception.code, 401) + raised.exception.close() - def test_unexpected_error_status_needs_attention(self) -> None: - result = StepHttpResult(500, {"detail": "boom"}, {}) - friendly = common.friendly_result("positive", result, self.COPY) - self.assertEqual(friendly["status"], "needs_attention") - self.assertIn("boom", friendly["message"]) + def test_rejects_duplicate_or_unknown_concepts_before_evidence(self) -> None: + base = {"target": {"identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}]}} + for claims in (["birth-is-registered", "birth-is-registered"], ["not-a-concept"]): + with self.subTest(claims=claims), self.assertRaises(urllib.error.HTTPError) as raised: + self.request("/v1/evaluations", body={**base, "claims": claims}) + self.assertEqual(raised.exception.code, 400) + raised.exception.close() if __name__ == "__main__": diff --git a/scenarios/child_benefit.py b/scenarios/child_benefit.py index bea583a..a35b353 100644 --- a/scenarios/child_benefit.py +++ b/scenarios/child_benefit.py @@ -1,81 +1,30 @@ #!/usr/bin/env python3 -"""Birth to child benefit guided scenario.""" +"""Birth-to-child-benefit guided scenario through the application collector.""" from __future__ import annotations from typing import Any -from .common import ( - CHILD_BENEFIT_AS_OF_DATE, - PURPOSES, - auth_headers, - evaluation_body, - friendly_result, - http_json, - missing_runtime_token, - request_source, - source_response, - standard_error_result, -) +from .common import CHILD_BENEFIT_AS_OF_DATE, PURPOSES, friendly_result, http_json, missing_runtime_token, request_source, source_response, standard_error_result from .service_config import service_token, service_token_env, service_url SCENARIO_ID = "birth-to-child-benefit" -SERVICE_NAME = "Child Benefit Federator" +SERVICE_NAME = "Child Benefit Evidence Collector" SERVICE_ID = "child-benefit-federator" POSITIVE_SUBJECT = "2300010248" DECEASED_CONTROL = "2300091305" ABOVE_THRESHOLD_CONTROL = "2300036523" UNREGISTERED_CONTROL = "2300073046" DUPLICATE_CONTROL = "2300054788" -CLAIMS = [ - "birth-is-registered", - "population-record-active", - "child-age-under-5", - "household-below-poverty-threshold", - "not-already-enrolled", -] +CLAIMS = ["birth-is-registered", "population-record-active", "child-age-under-5", "household-below-poverty-threshold", "not-already-enrolled"] FRIENDLY = { - "discover": { - "met": ( - "The catalogue lists what may be asked.", - "Claim definitions only. No resident data has moved yet.", - ), - }, - "positive": { - "met": ( - "Mateo's source predicates are ready for review.", - "The application collected five source-owned facts. It did not make the benefit decision.", - ), - "unmet": ( - "Review cannot proceed on the facts returned.", - "One or more source-owned checks came back not met. The programme policy layer decides what happens next.", - ), - }, - "deceased-control": { - "unmet": ( - "Rejected, exactly as designed.", - "The civil predicate fails for the deceased control case. The application only returns that fact.", - ), - }, - "poverty-control": { - "unmet": ( - "Rejected: the household is above the threshold.", - "The social registry predicate came back not met. The caseworker never sees the household's actual income.", - ), - }, - "unregistered-control": { - "unmet": ( - "No birth predicate could be satisfied. Registration comes first.", - "The civil authority returns only the minimized predicate result, not a source row.", - ), - }, - "duplicate-control": { - "unmet": ( - "Rejected: already enrolled.", - "The programme MIS predicate came back not met, preventing a double payment.", - ), - }, + "positive": {"met": ("Mateo's signed source evidence is ready.", "Evidence evaluated five reviewed requirements without copying authority rows.")}, + "deceased-control": {"unmet": ("Rejected, exactly as designed.", "The civil evidence says the child is not active for this review.")}, + "poverty-control": {"unmet": ("Rejected: the household is above the threshold.", "Only the reviewed poverty predicate was disclosed.")}, + "unregistered-control": {"unmet": ("Registration comes first.", "No registered-birth evidence was asserted.")}, + "duplicate-control": {"unmet": ("Rejected: already enrolled.", "The programme evidence prevents a duplicate payment.")}, + "purpose-denial": {"refused": ("Refused, exactly as designed.", "The unsupported purpose matched no Evidence grant.")}, } @@ -84,74 +33,23 @@ def story() -> dict[str, Any]: "id": SCENARIO_ID, "title": "Birth to child benefit", "short_title": "Child benefit", - "proves": "Civil, population, social registry, and beneficiary evidence can be collected as source-owned predicates without copying source rows.", + "proves": "One application can collect separately signed CRA, NIA, SRO, and MoSD evidence without copying source rows.", "domain": "Social protection", - "availability": "hosted", - "intro": "A caseworker reviews child benefit eligibility from minimized Solmara evidence.", + "availability": "local", + "intro": "A caseworker reviews minimized Registry Evidence assertions.", "actor": "MoSD child benefit caseworker", "subject": {"name": "Mateo Santos", "identifier": POSITIVE_SUBJECT}, - "requester": { - "name": "Child benefit desk", - "purpose": PURPOSES["child_benefit"], - }, + "requester": {"name": "Child benefit desk", "purpose": PURPOSES["child_benefit"]}, "steps": [ - { - "id": "discover", - "label": "Discover predicates", - "prompt": "Read the child-benefit evidence catalogue.", - "button": "Discover", - "request_summary": "GET /v1/claims", - }, - { - "id": "positive", - "label": "Collect eligible child predicates", - "prompt": "Run the positive control.", - "button": "Evaluate", - "request_summary": "POST child-benefit evidence request for the positive UIN.", - }, - { - "id": "deceased-control", - "label": "Deceased control", - "prompt": "Confirm a deceased child is rejected.", - "button": "Evaluate", - "request_summary": "POST child-benefit claims for the deceased control UIN.", - }, - { - "id": "poverty-control", - "label": "Income threshold control", - "prompt": "Confirm an above-threshold household is rejected.", - "button": "Evaluate", - "request_summary": "POST child-benefit claims for the threshold control UIN.", - }, - { - "id": "unregistered-control", - "label": "Unregistered birth control", - "prompt": "Route an unregistered birth to registration first.", - "button": "Evaluate", - "request_summary": "POST child-benefit claims for the unregistered control UIN.", - }, - { - "id": "duplicate-control", - "label": "Duplicate enrollment control", - "prompt": "Reject an already-enrolled child.", - "button": "Evaluate", - "request_summary": "POST child-benefit claims for the duplicate control UIN.", - }, - { - "id": "purpose-denial", - "label": "Purpose denial", - "prompt": "Try the same request with an unsupported purpose.", - "button": "Try denial", - "request_summary": "POST with an unsupported Data-Purpose header.", - }, - ], - "receipt": [ - { - "label": "Evidence", - "value": "Source-owned predicates, no eligibility composition", - }, - {"label": "Raw rows copied", "value": "No"}, + {"id": "discover", "label": "Discover requirements", "prompt": "Read the Evidence definitions.", "button": "Discover", "request_summary": "GET /v1/evidence-definitions"}, + {"id": "positive", "label": "Collect eligible child evidence", "prompt": "Run the positive control.", "button": "Evaluate", "request_summary": "POST five Evidence requirements."}, + {"id": "deceased-control", "label": "Deceased control", "prompt": "Confirm a deceased child is rejected.", "button": "Evaluate", "request_summary": "POST child-benefit requirements."}, + {"id": "poverty-control", "label": "Income threshold control", "prompt": "Confirm an above-threshold household is rejected.", "button": "Evaluate", "request_summary": "POST child-benefit requirements."}, + {"id": "unregistered-control", "label": "Unregistered birth control", "prompt": "Route an unregistered birth to registration first.", "button": "Evaluate", "request_summary": "POST child-benefit requirements."}, + {"id": "duplicate-control", "label": "Duplicate enrollment control", "prompt": "Reject an already-enrolled child.", "button": "Evaluate", "request_summary": "POST child-benefit requirements."}, + {"id": "purpose-denial", "label": "Purpose denial", "prompt": "Try an unsupported purpose.", "button": "Try denial", "request_summary": "POST with an unsupported purpose."}, ], + "receipt": [{"label": "Evidence", "value": "Flattened signed JWS assertions"}, {"label": "Raw rows copied", "value": "No"}], } @@ -164,57 +62,23 @@ def run_step(config: dict[str, Any], step_id: str) -> dict[str, Any]: def _request(config: dict[str, Any], step_id: str, *, send: bool) -> dict[str, Any]: - url = service_url( - SERVICE_ID, "/v1/claims" if step_id == "discover" else "/v1/evaluations" - ) - subject = { - "positive": POSITIVE_SUBJECT, - "deceased-control": DECEASED_CONTROL, - "poverty-control": ABOVE_THRESHOLD_CONTROL, - "unregistered-control": UNREGISTERED_CONTROL, - "duplicate-control": DUPLICATE_CONTROL, - "purpose-denial": POSITIVE_SUBJECT, - }.get(step_id) - purpose = request_purpose(config, step_id) - token = service_token(SERVICE_ID) - headers = auth_headers(token, purpose, "application/json") - body = ( - None - if step_id == "discover" - else evaluation_body( - subject or "", - CLAIMS, - scheme="solmara_uin", - format="application/json", - variables={"as_of_date": CHILD_BENEFIT_AS_OF_DATE}, - ) - ) + subject = {"positive": POSITIVE_SUBJECT, "deceased-control": DECEASED_CONTROL, "poverty-control": ABOVE_THRESHOLD_CONTROL, "unregistered-control": UNREGISTERED_CONTROL, "duplicate-control": DUPLICATE_CONTROL, "purpose-denial": POSITIVE_SUBJECT}.get(step_id) if step_id != "discover" and not subject: return standard_error_result(step_id) - request = request_source( - "GET" if step_id == "discover" else "POST", url, headers, body - ) + token = service_token(SERVICE_ID) if send else "" + purpose = "unsupported-demo-purpose" if step_id == "purpose-denial" else str(config.get("purpose_override") or PURPOSES["child_benefit"]) + url = service_url(SERVICE_ID, "/v1/claims" if step_id == "discover" else "/v1/evaluations") + headers = {"x-api-key": token, "Accept": "application/json", "Data-Purpose": purpose} + body = None if step_id == "discover" else {"target": {"type": "Person", "identifiers": [{"scheme": "solmara_uin", "value": subject}]}, "claims": CLAIMS, "disclosure": "predicate", "format": "application/json", "variables": {"as_of_date": CHILD_BENEFIT_AS_OF_DATE}} + request = request_source("GET" if step_id == "discover" else "POST", url, headers, body) if not send: return {"request_source": request} if not token: - return missing_runtime_token( - step_id, SERVICE_NAME, service_token_env(SERVICE_ID), request - ) + return missing_runtime_token(step_id, SERVICE_NAME, service_token_env(SERVICE_ID), request) result = http_json("GET" if step_id == "discover" else "POST", url, headers, body) response_body = result.body if isinstance(result.body, dict) else {} - payload = { - "step_id": step_id, - "friendly": friendly_result(step_id, result, FRIENDLY), - "request_source": request, - "response_source": source_response(result), - "source_trace": response_body.get("source_trace", []), - } - return payload + return {"step_id": step_id, "friendly": friendly_result(step_id, result, FRIENDLY), "request_source": request, "response_source": source_response(result), "source_trace": response_body.get("source_trace", [])} def request_purpose(config: dict[str, Any], step_id: str) -> str: - if step_id == "purpose-denial": - return "https://id.registrystack.org/solmara/purpose/unsupported-demo-purpose" - if isinstance(config.get("purpose_override"), str): - return config["purpose_override"] - return PURPOSES["child_benefit"] + return "unsupported-demo-purpose" if step_id == "purpose-denial" else str(config.get("purpose_override") or PURPOSES["child_benefit"]) diff --git a/scenarios/citizen.py b/scenarios/citizen.py index 2ff2593..41ceb0c 100644 --- a/scenarios/citizen.py +++ b/scenarios/citizen.py @@ -1,111 +1,24 @@ #!/usr/bin/env python3 -"""Citizen self-service guided scenario.""" +"""Citizen self-service Evidence scenario.""" from __future__ import annotations from typing import Any -from .common import ( - CLAIM_RESULT_FORMAT, - PURPOSES, - StepHttpResult, - auth_headers, - credential_attempt, - evaluation_body, - friendly_result, - http_json, - missing_runtime_token, - request_source, - source_response, - standard_error_result, -) -from .service_config import ( - authority_service_id, - service_token, - service_token_env, - service_url, -) +from .common import PURPOSES, StepHttpResult, evidence_body, evidence_headers, friendly_result, http_json, missing_runtime_token, normalized_evidence_result, request_source, source_response, standard_error_result +from .service_config import requirement_id, service_token, service_token_env, service_url SCENARIO_ID = "citizen-self-service" -SERVICE_NAME = "Citizen self-service evidence" -CRA_CLIENT = "cra-citizen" -NIA_CLIENT = "nia-citizen" +SERVICE_NAME = "Registry Evidence" POSITIVE_SUBJECT = "2300018263" -CRA_CLAIMS = ["civil-record-linked"] -NIA_CLAIMS = ["citizen-population-record-active"] -CREDENTIAL_PROFILE = "nia-citizen-status.citizen-population-status" -CREDENTIAL_STEPS = {"positive"} -AUTHORITY_NAMES = { - CRA_CLIENT: "Civil Registration Authority", - NIA_CLIENT: "National Identity Agency", -} -FRIENDLY = { - "discover": { - "met": ( - "The catalogues list what may be asked.", - "Claim definitions only. No resident data has moved.", - ), - }, - "positive": { - "met": ( - "Elena's minimized status is ready.", - "CRA confirmed the civil link and NIA confirmed an active population record. NIA can issue the population-status credential.", - ), - "unmet": ( - "No status credential is available on the facts returned.", - "One or more self-service checks came back not met.", - ), - }, -} +CLIENTS = ("cra-citizen", "nia-citizen") +AUTHORITY_NAMES = {"cra-citizen": "Civil Registration Authority", "nia-citizen": "National Identity Agency"} +FRIENDLY = {"positive": {"met": ("Elena's signed status evidence is ready.", "CRA and NIA released separate reviewed concept values.")}, "purpose-denial": {"refused": ("Refused, exactly as designed.", "No grant permits that purpose.")}} def story() -> dict[str, Any]: - return { - "id": SCENARIO_ID, - "title": "Citizen self-service", - "short_title": "Citizen self-service", - "proves": "The portal can present separate CRA and NIA evidence while NIA remains the sole citizen-status credential issuer.", - "domain": "Citizen services", - "availability": "hosted", - "intro": "A signed-in citizen previews their own minimized status evidence.", - "actor": "Citizen Services Portal", - "subject": {"name": "Elena Dela Cruz", "identifier": POSITIVE_SUBJECT}, - "requester": { - "name": "Citizen portal BFF", - "purpose": PURPOSES["citizen_self_service"], - }, - "steps": [ - { - "id": "discover", - "label": "Discover citizen claims", - "prompt": "Read the CRA and NIA catalogues.", - "button": "Discover", - "request_summary": "GET /v1/claims from CRA and NIA.", - }, - { - "id": "positive", - "label": "Evaluate citizen status", - "prompt": "Evaluate the two source-owned predicates.", - "button": "Evaluate", - "request_summary": "POST CRA civil-link and NIA population-status claims.", - }, - { - "id": "purpose-denial", - "label": "Purpose denial", - "prompt": "Use an unsupported purpose.", - "button": "Try denial", - "request_summary": "POST the same claims with an unsupported Data-Purpose header.", - }, - ], - "receipt": [ - { - "label": "Credential", - "value": "NIA citizen population-status SD-JWT VC preview", - }, - {"label": "Raw rows copied", "value": "No"}, - ], - } + return {"id": SCENARIO_ID, "title": "Citizen self-service", "short_title": "Citizen self-service", "proves": "The portal can present separate CRA and NIA signed evidence through one Evidence service.", "domain": "Citizen services", "availability": "local", "intro": "A signed-in citizen previews minimized evidence.", "actor": "Citizen Services Portal", "subject": {"name": "Elena Dela Cruz", "identifier": POSITIVE_SUBJECT}, "requester": {"name": "Citizen portal BFF", "purpose": PURPOSES["citizen_self_service"]}, "steps": [{"id": "discover", "label": "Discover citizen requirements", "prompt": "Read the Evidence definitions.", "button": "Discover", "request_summary": "GET /v1/evidence-definitions"}, {"id": "positive", "label": "Evaluate citizen status", "prompt": "Evaluate CRA and NIA requirements.", "button": "Evaluate", "request_summary": "POST two Evidence requirements."}, {"id": "purpose-denial", "label": "Purpose denial", "prompt": "Use an unsupported purpose.", "button": "Try denial", "request_summary": "POST with an unsupported purpose."}], "receipt": [{"label": "Artifact", "value": "Two signed Evidence JWS assertions"}, {"label": "Raw rows copied", "value": "No"}]} def preview_step(config: dict[str, Any], step_id: str) -> dict[str, Any]: @@ -117,193 +30,42 @@ def run_step(config: dict[str, Any], step_id: str) -> dict[str, Any]: def _request(config: dict[str, Any], step_id: str, *, send: bool) -> dict[str, Any]: - subject = {"positive": POSITIVE_SUBJECT, "purpose-denial": POSITIVE_SUBJECT}.get( - step_id - ) - if step_id != "discover" and not subject: + if step_id not in {"discover", "positive", "purpose-denial"}: return standard_error_result(step_id) - purpose = request_purpose(config, step_id) - requests = authority_requests(step_id, subject, purpose) - preview = combined_request_preview(requests, purpose) + token = service_token("cra-citizen") if send else "" + purpose = "unsupported-demo-purpose" if step_id == "purpose-denial" else str(config.get("purpose_override") or PURPOSES["citizen_self_service"]) + requests = _requests(step_id, POSITIVE_SUBJECT, purpose, token) + preview = _preview(requests, purpose) if not send: return {"request_source": preview} + if not token: + return missing_runtime_token(step_id, SERVICE_NAME, service_token_env("cra-citizen"), preview) + responses = [(item, normalized_evidence_result(http_json(item["method"], item["url"], item["headers"], item["body"]))) for item in requests] + aggregate = _aggregate(responses) + return {"step_id": step_id, "friendly": friendly_result(step_id, aggregate, FRIENDLY), "request_source": preview, "request_sources": [item["source"] for item, _ in responses], "response_source": source_response(aggregate), "source_trace": [{"authority": AUTHORITY_NAMES[item["client_id"]], "service_id": "registry-evidence", "status": response.status} for item, response in responses]} - missing = next( - (request for request in requests if not service_token(request["client_id"])), - None, - ) - if missing: - result = missing_runtime_token( - step_id, - SERVICE_NAME, - service_token_env(missing["client_id"]), - preview, - ) - result["request_sources"] = [request["source"] for request in requests] - return result - responses: list[tuple[dict[str, Any], StepHttpResult]] = [] - for authority_request in requests: - result = http_json( - authority_request["method"], - authority_request["url"], - authority_request["headers"], - authority_request["body"], - ) - responses.append((authority_request, result)) - - aggregate = aggregate_response(responses) - payload: dict[str, Any] = { - "step_id": step_id, - "friendly": friendly_result(step_id, aggregate, FRIENDLY), - "request_source": preview, - "request_sources": [request["source"] for request, _ in responses], - "response_source": source_response(aggregate), - "source_trace": [ - authority_trace(request, response) for request, response in responses - ], - } - if step_id in CREDENTIAL_STEPS and aggregate.status == 200: - nia_result = next( - response - for request, response in responses - if request["client_id"] == NIA_CLIENT - ) - payload.update( - credential_attempt( - service_url(NIA_CLIENT, "/v1/credentials"), - service_token(NIA_CLIENT), - purpose, - nia_result, - CREDENTIAL_PROFILE, - NIA_CLAIMS, - authority_service_id(NIA_CLIENT), - ) - ) - return payload - - -def authority_requests( - step_id: str, subject: str | None, purpose: str -) -> list[dict[str, Any]]: +def _requests(step_id: str, subject: str, purpose: str, token: str) -> list[dict[str, Any]]: if step_id == "discover": - return [ - build_request(CRA_CLIENT, "GET", "/v1/claims", purpose), - build_request(NIA_CLIENT, "GET", "/v1/claims", purpose), - ] - if step_id in {"positive", "purpose-denial"}: - return [ - build_request( - CRA_CLIENT, "POST", "/v1/evaluations", purpose, subject, CRA_CLAIMS - ), - build_request( - NIA_CLIENT, - "POST", - "/v1/evaluations", - purpose, - subject, - NIA_CLAIMS, - ), - ] - return [] - - -def build_request( - client_id: str, - method: str, - path: str, - purpose: str, - subject: str | None = None, - claims: list[str] | None = None, - response_format: str = CLAIM_RESULT_FORMAT, -) -> dict[str, Any]: - token = service_token(client_id) - url = service_url(client_id, path) - headers = auth_headers( - token, purpose, response_format if method == "POST" else "application/json" - ) - body = ( - evaluation_body( - subject or "", claims or [], scheme="solmara_uin", format=response_format - ) - if method == "POST" - else None - ) - return { - "client_id": client_id, - "method": method, - "url": url, - "headers": headers, - "body": body, - "source": request_source(method, url, headers, body), - } + url = service_url("cra-citizen", "/v1/evidence-definitions") + headers = evidence_headers(token, discover=True) + return [{"client_id": "cra-citizen", "method": "GET", "url": url, "headers": headers, "body": None, "source": request_source("GET", url, headers)}] + items = [] + for client in CLIENTS: + url = service_url(client) + headers = evidence_headers(token) + body = evidence_body(subject, requirement_id(client), purpose) + items.append({"client_id": client, "method": "POST", "url": url, "headers": headers, "body": body, "source": request_source("POST", url, headers, body)}) + return items -def combined_request_preview( - requests: list[dict[str, Any]], purpose: str -) -> dict[str, Any]: - return { - "method": "MULTI", - "url": "solmara://authority-notaries", - "headers": {"Data-Purpose": purpose}, - "requests": [request["source"] for request in requests], - } +def _preview(requests: list[dict[str, Any]], purpose: str) -> dict[str, Any]: + return requests[0]["source"] if len(requests) == 1 else {"method": "MULTI", "url": "solmara://registry-evidence", "purpose": purpose, "requests": [item["source"] for item in requests]} -def aggregate_response( - responses: list[tuple[dict[str, Any], StepHttpResult]], -) -> StepHttpResult: - failed = next( - ( - response - for _, response in responses - if response.status is None or not 200 <= response.status < 300 - ), - None, - ) +def _aggregate(responses: list[tuple[dict[str, Any], StepHttpResult]]) -> StepHttpResult: + failed = next((response for _, response in responses if response.status is None or not 200 <= response.status < 300), None) if failed: return failed - results: list[dict[str, Any]] = [] - for request, response in responses: - body = response.body if isinstance(response.body, dict) else {} - raw_results = body.get("results") - if request["method"] == "POST" and not isinstance(raw_results, list): - return StepHttpResult( - 502, - { - "code": "authority.invalid_response", - "detail": "An authority response omitted claim results.", - }, - {}, - ) - for result in raw_results or []: - if isinstance(result, dict): - results.append( - { - **result, - "authority": AUTHORITY_NAMES[request["client_id"]], - "notary_service_id": authority_service_id(request["client_id"]), - } - ) - return StepHttpResult( - 200, {"results": results}, {"content-type": "application/json"} - ) - - -def authority_trace( - request: dict[str, Any], response: StepHttpResult -) -> dict[str, Any]: - return { - "authority": AUTHORITY_NAMES[request["client_id"]], - "service_id": authority_service_id(request["client_id"]), - "request_source": request["source"], - "response_source": source_response(response), - } - - -def request_purpose(config: dict[str, Any], step_id: str) -> str: - if step_id == "purpose-denial": - return "https://id.registrystack.org/solmara/purpose/unsupported-demo-purpose" - if isinstance(config.get("purpose_override"), str): - return config["purpose_override"] - return PURPOSES["citizen_self_service"] + results = [entry for _, response in responses for entry in response.body.get("results", [])] + return StepHttpResult(200, {"results": results, "signed_evidence": [response.body.get("signed_evidence") for _, response in responses]}, {"content-type": "application/json"}) diff --git a/scenarios/common.py b/scenarios/common.py index bc6ee46..775cb4f 100644 --- a/scenarios/common.py +++ b/scenarios/common.py @@ -1,39 +1,38 @@ #!/usr/bin/env python3 -"""Shared helpers for Solmara Lab guided scenarios.""" +"""Registry Evidence and Mint helpers for the guided Solmara scenarios.""" from __future__ import annotations import base64 -import hashlib import json import os +import secrets +import ssl +import threading import time import urllib.error +import urllib.parse import urllib.request import uuid from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from pathlib import Path +from typing import Any from urllib.parse import urljoin -if TYPE_CHECKING: - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - - -CLAIM_RESULT_FORMAT = "application/vnd.registry-notary.claim-result+json" +EVIDENCE_JWS_MEDIA_TYPE = "application/jose+json" CHILD_BENEFIT_AS_OF_DATE = "2026-07-14" -SD_JWT_VC_FORMAT = "application/dc+sd-jwt" -HOLDER_PROOF_TYP = "kb+jwt" -HOLDER_PROOF_ALG = "EdDSA" -HOLDER_PROOF_LIFETIME_SECONDS = 60 PURPOSES = { - "child_benefit": "https://id.registrystack.org/solmara/purpose/child-benefit-review", - "pension_payment": "https://id.registrystack.org/solmara/purpose/pension-payment-review", - "survivor_benefit": "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination", - "voucher": "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review", - "livestock": "https://id.registrystack.org/solmara/purpose/livestock-movement-control", - "citizen_self_service": "https://id.registrystack.org/solmara/purpose/citizen-self-service", + "child_benefit": "child-benefit-review", + "pension_payment": "pension-payment-review", + "survivor_benefit": "survivor-benefit-determination", + "voucher": "voucher-eligibility-review", + "livestock": "livestock-movement-control", + "citizen_self_service": "citizen-self-service", } +_TOKEN_LOCK = threading.Lock() +_TOKEN_CACHE: tuple[str, float] = ("", 0.0) + @dataclass class StepHttpResult: @@ -47,15 +46,25 @@ def joined_url(base: str, path: str) -> str: return urljoin(base.rstrip("/") + "/", path.lstrip("/")) -def env_url(env_name: str, default: str, path: str) -> str: - return joined_url(os.environ.get(env_name, default), path) +def b64url_nopad(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") + + +def b64url_decode(value: str) -> bytes: + return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) -def request_source(method: str, url: str, headers: dict[str, str], body: Any | None = None) -> dict[str, Any]: - source_headers = dict(headers) - if body is not None: - source_headers.setdefault("Content-Type", "application/json") - source: dict[str, Any] = {"method": method, "url": url, "headers": redact_headers(source_headers)} +def request_source( + method: str, + url: str, + headers: dict[str, str], + body: Any | None = None, +) -> dict[str, Any]: + source: dict[str, Any] = { + "method": method, + "url": url, + "headers": redact_headers(headers), + } if body is not None: source["body"] = body return source @@ -63,106 +72,26 @@ def request_source(method: str, url: str, headers: dict[str, str], body: Any | N def redact_headers(headers: dict[str, str]) -> dict[str, str]: redacted = dict(headers) - for key in list(redacted): + for key in redacted: if key.lower() == "authorization": - redacted[key] = "Bearer [runtime token hidden]" if redacted[key] else "Bearer [runtime token missing]" - if key.lower() == "x-api-key": - redacted[key] = "[runtime token hidden]" if redacted[key] else "[runtime token missing]" + redacted[key] = "Bearer [runtime token hidden]" + elif key.lower() == "x-api-key": + redacted[key] = "[runtime token hidden]" return redacted -def auth_headers(token: str, purpose: str, accept: str = "application/json") -> dict[str, str]: - return {"x-api-key": token, "Accept": accept, "Data-Purpose": purpose} - - -def evaluation_body( - subject: str, - claim_ids: list[str], - *, - scheme: str, - disclosure: str = "predicate", - format: str = CLAIM_RESULT_FORMAT, - variables: dict[str, Any] | None = None, -) -> dict[str, Any]: - body = { - "target": {"type": "Person", "identifiers": [{"scheme": scheme, "value": subject}]}, - "claims": claim_ids, - "disclosure": disclosure, - "format": format, - } - if variables is not None: - body["variables"] = variables - return body - - -def b64url_nopad(data: bytes) -> str: - return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") - +def tls_context() -> ssl.SSLContext | None: + ca_bundle = os.environ.get("SOLMARA_EVIDENCE_CA_BUNDLE") + return ssl.create_default_context(cafile=ca_bundle) if ca_bundle else None -def json_b64url(value: Any) -> str: - return b64url_nopad(json.dumps(value, separators=(",", ":")).encode("utf-8")) - -@dataclass -class HolderKeypair: - """An ephemeral did:jwk holder identity used to prove possession at credential issuance.""" - - holder_id: str - private_key: "Ed25519PrivateKey" - - -def holder_keypair() -> HolderKeypair: - """Generate a fresh Ed25519 keypair and derive its did:jwk holder id. - - The private key never leaves this process; only the public JWK is encoded - into the did:jwk identifier that gets sent to the notary. - """ - # Imported here, not at module level: preview-only consumers (the smoke - # story previews) run under the system Python without the cryptography - # package, and only real signing needs it. - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat - - private_key = Ed25519PrivateKey.generate() - public_bytes = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) - public_jwk = {"kty": "OKP", "crv": "Ed25519", "x": b64url_nopad(public_bytes)} - holder_id = f"did:jwk:{json_b64url(public_jwk)}" - return HolderKeypair(holder_id=holder_id, private_key=private_key) - - -def holder_proof( - keypair: HolderKeypair, - *, - audience: str, - evaluation_id: str, - credential_profile: str, - disclosure: str, - claim_ids: list[str], -) -> str: - """Sign a holder key-binding proof JWT for a credential issuance request. - - A fresh jti is minted on every call so repeated runs stay replay-safe. - """ - now = int(time.time()) - header = {"alg": HOLDER_PROOF_ALG, "typ": HOLDER_PROOF_TYP, "kid": keypair.holder_id} - disclosure_hash = b64url_nopad(hashlib.sha256(disclosure.encode("utf-8")).digest()) - payload = { - "sub": keypair.holder_id, - "aud": audience, - "iat": now, - "exp": now + HOLDER_PROOF_LIFETIME_SECONDS, - "jti": str(uuid.uuid4()), - "evaluation_id": evaluation_id, - "credential_profile": credential_profile, - "disclosure": disclosure_hash, - "claims": claim_ids, - } - signing_input = f"{json_b64url(header)}.{json_b64url(payload)}" - signature = keypair.private_key.sign(signing_input.encode("ascii")) - return f"{signing_input}.{b64url_nopad(signature)}" - - -def http_json(method: str, url: str, headers: dict[str, str], body: Any | None = None, timeout: float = 8.0) -> StepHttpResult: +def http_json( + method: str, + url: str, + headers: dict[str, str], + body: Any | None = None, + timeout: float = 8.0, +) -> StepHttpResult: data = None request_headers = dict(headers) if body is not None: @@ -170,12 +99,19 @@ def http_json(method: str, url: str, headers: dict[str, str], body: Any | None = request_headers.setdefault("Content-Type", "application/json") request = urllib.request.Request(url, headers=request_headers, data=data, method=method) try: - with urllib.request.urlopen(request, timeout=timeout) as response: - raw = response.read() - return StepHttpResult(response.status, parse_body(raw), {key.lower(): value for key, value in response.headers.items()}) + with urllib.request.urlopen(request, timeout=timeout, context=tls_context()) as response: + return StepHttpResult( + response.status, + parse_body(response.read()), + {key.lower(): value for key, value in response.headers.items()}, + ) except urllib.error.HTTPError as error: - return StepHttpResult(error.code, parse_body(error.read()), {key.lower(): value for key, value in error.headers.items()}) - except Exception as error: + return StepHttpResult( + error.code, + parse_body(error.read()), + {key.lower(): value for key, value in error.headers.items()}, + ) + except Exception as error: # the guided UI reports a value-free class only return StepHttpResult(None, {}, {}, error.__class__.__name__) @@ -191,202 +127,207 @@ def parse_body(raw: bytes) -> Any: def source_response(result: StepHttpResult) -> dict[str, Any]: return { "status": result.status, - "headers": {key: value for key, value in result.headers.items() if key in {"content-type", "www-authenticate"}}, + "headers": { + key: value + for key, value in result.headers.items() + if key in {"content-type", "www-authenticate"} + }, "body": result.body, "error": result.error, } -def credential_attempt( - url: str, - token: str, - purpose: str, - evaluation_result: StepHttpResult, - profile: str, - claim_ids: list[str], - service_id: str, - *, - disclosure: str = "predicate", -) -> dict[str, Any]: - evaluation_id = first_evaluation_id(evaluation_result.body) - if not evaluation_id: - return { - "credential": { - "status": "not_attempted", - "profile": profile, - "format": SD_JWT_VC_FORMAT, - "reason": "evaluation_id_missing", - "message": "The notary did not return an evaluation id for credential issuance.", - } - } - keypair = holder_keypair() - proof = holder_proof( - keypair, - audience=service_id, - evaluation_id=evaluation_id, - credential_profile=profile, - disclosure=disclosure, - claim_ids=claim_ids, - ) - body = { - "evaluation_id": evaluation_id, - "credential_profile": profile, - "format": SD_JWT_VC_FORMAT, - "claims": claim_ids, - "disclosure": disclosure, - "purpose": purpose, - "holder": {"binding": "did", "id": keypair.holder_id, "proof": proof}, +def _client_assertion(client_id: str, key_path: str, audience: str) -> str: + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + + jwk = json.loads(Path(key_path).read_text()) + now = int(time.time()) + header = {"alg": "EdDSA", "typ": "JWT", "kid": jwk["kid"]} + claims = { + "iss": client_id, + "sub": client_id, + "aud": audience, + "iat": now, + "exp": now + 120, + "jti": str(uuid.uuid4()), } - headers = auth_headers(token, purpose, "application/json") - request = request_source("POST", url, headers, body) - result = http_json("POST", url, headers, body) + signing_input = ".".join( + b64url_nopad(json.dumps(value, separators=(",", ":")).encode()) + for value in (header, claims) + ) + private_key = Ed25519PrivateKey.from_private_bytes(b64url_decode(jwk["d"])) + signature = private_key.sign(signing_input.encode("ascii")) + return f"{signing_input}.{b64url_nopad(signature)}" + + +def evidence_access_token() -> str: + """Obtain and briefly cache a Mint token using private_key_jwt.""" + static = os.environ.get("SOLMARA_EVIDENCE_ACCESS_TOKEN", "") + if static: + return static + mint_url = os.environ.get("SOLMARA_MINT_URL", "") + assertion_audience = os.environ.get("SOLMARA_MINT_ASSERTION_AUDIENCE", "") + client_id = os.environ.get("SOLMARA_EVIDENCE_CLIENT_ID", "") + key_path = os.environ.get("SOLMARA_EVIDENCE_CLIENT_KEY", "") + if not mint_url or not client_id or not key_path or not Path(key_path).is_file(): + return "" + global _TOKEN_CACHE + with _TOKEN_LOCK: + if _TOKEN_CACHE[0] and _TOKEN_CACHE[1] > time.time() + 10: + return _TOKEN_CACHE[0] + token_url = joined_url(mint_url, "/token") + assertion = _client_assertion( + client_id, key_path, assertion_audience or token_url + ) + form = urllib.parse.urlencode( + { + "grant_type": "client_credentials", + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion": assertion, + } + ).encode() + request = urllib.request.Request( + token_url, + data=form, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=8, context=tls_context()) as response: + payload = json.loads(response.read()) + except Exception: + return "" + token = payload.get("access_token", "") + lifetime = int(payload.get("expires_in", 300)) + if isinstance(token, str) and token: + _TOKEN_CACHE = (token, time.time() + lifetime) + return token + return "" + + +def evidence_headers(token: str, *, discover: bool = False) -> dict[str, str]: return { - "credential_source": request, - "credential_response_source": source_response(result), - "credential": credential_summary(profile, keypair.holder_id, result), + "Authorization": f"Bearer {token}", + "Accept": "application/json" if discover else EVIDENCE_JWS_MEDIA_TYPE, } -def first_evaluation_id(body: Any) -> str | None: - if not isinstance(body, dict): - return None - results = body.get("results") - if not isinstance(results, list): - return None - for result in results: - if isinstance(result, dict) and isinstance(result.get("evaluation_id"), str): - return result["evaluation_id"] - return None - +def evidence_body( + subject: str, + requirement: str, + purpose: str, + *, + selector_profile: str = "solmara-uin-v1", + selector_field: str = "uin", +) -> dict[str, Any]: + return { + "requestNonce": b64url_nopad(secrets.token_bytes(32)), + "requirement": requirement, + "purpose": purpose, + "subjects": [ + { + "role": "subject", + "selector": { + "profile": selector_profile, + "values": {selector_field: subject}, + }, + } + ], + } -def sd_jwt_vct(credential: Any) -> str | None: - """Read the `vct` claim from a compact SD-JWT. - The issuance response body carries no top-level vct; it only exists inside - the signed payload, so decode it here for display. Returns None when the - credential is absent or not decodable. - """ - if not isinstance(credential, str): - return None - segments = credential.split("~")[0].split(".") - if len(segments) != 3: +def decoded_evidence_payload(body: Any) -> dict[str, Any] | None: + if not isinstance(body, dict) or not isinstance(body.get("payload"), str): return None try: - payload = json.loads(base64.urlsafe_b64decode(segments[1] + "=" * (-len(segments[1]) % 4))) - except (ValueError, UnicodeDecodeError): + payload = json.loads(b64url_decode(body["payload"])) + except (ValueError, UnicodeDecodeError, json.JSONDecodeError): return None - vct = payload.get("vct") if isinstance(payload, dict) else None - return vct if isinstance(vct, str) else None - - -def credential_summary(profile: str, holder_id: str, result: StepHttpResult) -> dict[str, Any]: - body = result.body if isinstance(result.body, dict) else {} - if result.status and 200 <= result.status < 300: - credential = body.get("credential") - preview = f"{credential[:48]}..." if isinstance(credential, str) and len(credential) > 48 else credential - disclosures = body.get("disclosures") - return { - "status": "issued", - "profile": body.get("credential_profile", profile), - "format": body.get("format", SD_JWT_VC_FORMAT), - "vct": sd_jwt_vct(credential), - "issuer": body.get("issuer"), - "credential_id": body.get("credential_id"), - "expires_at": body.get("expires_at"), - "holder_id": holder_id, - "disclosures": len(disclosures) if isinstance(disclosures, list) else 0, - "compact_preview": preview, - } - reason = body.get("code") or body.get("error") or result.error or f"HTTP {result.status}" - message = body.get("detail") or body.get("message") or body.get("title") or "Credential issuance did not complete." - return { - "status": "not_issued", - "profile": profile, - "format": SD_JWT_VC_FORMAT, - "reason": reason, - "http_status": result.status, - "message": message, - } - + return payload if isinstance(payload, dict) else None + + +def normalized_evidence_result(result: StepHttpResult) -> StepHttpResult: + """Keep the signed JWS and add the old UI's small predicate summary.""" + if result.status is None or not 200 <= result.status < 300: + return result + payload = decoded_evidence_payload(result.body) + if payload is None: + return StepHttpResult( + 502, + {"code": "evidence.invalid_response", "detail": "Evidence returned an invalid signed assertion."}, + {}, + ) + supported = payload.get("supportedValues", []) + results = [] + for entry in supported if isinstance(supported, list) else []: + if not isinstance(entry, dict): + continue + concept = str(entry.get("providesValueFor", "")) + value = entry.get("value") + results.append( + { + "claim_id": concept.rsplit("/", 1)[-1], + "concept_id": concept, + "satisfied": value if isinstance(value, bool) else None, + "value": value, + } + ) + return StepHttpResult( + result.status, + {"results": results, "assertion": payload, "signed_evidence": result.body}, + result.headers, + result.error, + ) -def friendly_result(step_id: str, result: StepHttpResult, copy: dict[str, dict[str, tuple[str, str]]] | None = None) -> dict[str, Any]: - """Civilian-language summary of a step outcome. - `copy` maps step_id -> {"met": (title, message), "unmet": (title, message)} - for outcomes the story wants to narrate; everything else falls back to - honest generic copy. A 403 with a pdp.* code is the boundary working, so - it reads as success, never as an error. - """ +def friendly_result( + step_id: str, + result: StepHttpResult, + copy: dict[str, dict[str, tuple[str, str]]] | None = None, +) -> dict[str, Any]: copy = copy or {} body = result.body if isinstance(result.body, dict) else {} raw_results = body.get("results") - results = raw_results if isinstance(raw_results, list) else None - unmet = ( - [entry.get("claim_id") for entry in results if isinstance(entry, dict) and entry.get("satisfied") is False] - if results is not None - else [] - ) - facts: list[dict[str, Any]] = [ - {"label": "HTTP status", "value": result.status if result.status is not None else "No response"} - ] - if results is not None: - facts.append({"label": "Claims met", "value": f"{len(results) - len(unmet)} of {len(results)}"}) + results = raw_results if isinstance(raw_results, list) else [] + unmet = [item.get("claim_id") for item in results if item.get("satisfied") is False] + facts = [{"label": "HTTP status", "value": result.status or "No response"}] + if results: + facts.append({"label": "Evidence values", "value": f"{len(results) - len(unmet)} of {len(results)} true"}) if result.status is None: - return { - "title": "No response from the service.", - "message": "The request could not be sent. Check that the lab stack is running.", - "status": "needs_attention", - "facts": facts, - } - code = str(body.get("code", "")) + return {"title": "No response from the service.", "message": "Check that Mint and Evidence are running.", "status": "needs_attention", "facts": facts} if result.status >= 400 and "refused" in copy.get(step_id, {}): title, message = copy[step_id]["refused"] return {"title": title, "message": message, "status": "done", "facts": facts} - if result.status == 403 and code.startswith("pdp."): - return { - "title": "Refused, exactly as designed.", - "message": "That purpose does not permit this question. Nothing was disclosed, only a stable problem code.", - "status": "done", - "facts": facts, - } if 200 <= result.status < 300: - step_copy = copy.get(step_id, {}) - if unmet: - title, message = step_copy.get( - "unmet", - ( - "Rejected, exactly as designed.", - f"The check on {unmet[0]} came back not met, so the request stops there without exposing anything else.", - ), - ) - else: - title, message = step_copy.get( - "met", - ("Request completed.", "The response is minimized to claim results and denial codes."), - ) + key = "unmet" if unmet else "met" + default = ( + ("Evidence returned a false value.", "The signed response discloses only the reviewed concept value.") + if unmet + else ("Signed evidence returned.", "The source row stayed behind its authority's Records API.") + ) + title, message = copy.get(step_id, {}).get(key, default) return {"title": title, "message": message, "status": "done", "facts": facts} return { "title": "Request needs attention.", - "message": str(body.get("detail") or body.get("title") or "The service returned an unexpected status."), + "message": str(body.get("detail") or body.get("title") or body.get("code") or "Evidence refused the request."), "status": "needs_attention", "facts": facts, } -def missing_runtime_token(step_id: str, service: str, token_env: str, request: dict[str, Any]) -> dict[str, Any]: +def missing_runtime_token( + step_id: str, service: str, token_env: str, request: dict[str, Any] +) -> dict[str, Any]: return { "step_id": step_id, "friendly": { "title": f"{service} is not configured yet.", - "message": "The Solmara wave 1 flow is scaffolded. Set the service URL and token environment variables once the local stack endpoints are available.", + "message": "Set the local Mint URL and client key, then start the Evidence stack.", "status": "needs_attention", - "facts": [ - {"label": "Required token env", "value": token_env}, - {"label": "Runtime", "value": "Not called without a token"}, - ], + "facts": [{"label": "Required setting", "value": token_env}], }, "request_source": request, - "response_source": {"note": "No runtime token configured, so the request was not sent."}, + "response_source": {"note": "No Mint access token was available."}, } diff --git a/scenarios/farmer_voucher.py b/scenarios/farmer_voucher.py index a2db5d2..5382959 100644 --- a/scenarios/farmer_voucher.py +++ b/scenarios/farmer_voucher.py @@ -1,89 +1,30 @@ #!/usr/bin/env python3 -"""Farmer climate-smart voucher guided scenario.""" +"""Farmer voucher and livestock Evidence scenario.""" from __future__ import annotations from typing import Any -from .common import CLAIM_RESULT_FORMAT, PURPOSES, auth_headers, credential_attempt, evaluation_body, friendly_result, http_json, missing_runtime_token, request_source, source_response, standard_error_result -from .service_config import service_token, service_token_env, service_url +from .common import PURPOSES, evidence_body, evidence_headers, friendly_result, http_json, missing_runtime_token, normalized_evidence_result, request_source, source_response, standard_error_result +from .service_config import requirement_id, service_token, service_token_env, service_url SCENARIO_ID = "farmer-climate-smart-voucher" -SERVICE_NAME = "NAgDI Notary" -SERVICE_ID = "nagdi-notary" +SERVICE_NAME = "Registry Evidence" POSITIVE_FARMER = "FR-1001" AUTHORIZATION_CONTROL = "FR-1002" REDEEMED_CONTROL = "FR-1003" -CLAIMS = ["eligible-for-climate-smart-input-voucher"] -MOVEMENT_CLAIMS = ["eligible-for-livestock-movement-permit"] -VOUCHER_CREDENTIAL_PROFILE = "climate_smart_voucher_sd_jwt" -MOVEMENT_CREDENTIAL_PROFILE = "livestock_movement_sd_jwt" -CREDENTIAL_STEPS = {"positive", "movement-permit"} FRIENDLY = { - "discover": { - "met": ("The catalogue lists what may be asked.", "Claim definitions only. No workbook rows have moved."), - }, - "positive": { - "met": ( - "Yes. This farmer qualifies for the voucher.", - "The eligibility fact came back met. The workbook itself never left NAgDI.", - ), - "unmet": ( - "Not eligible on the facts returned.", - "The voucher eligibility check came back not met for this farmer.", - ), - }, - "authorization-control": { - "unmet": ( - "Rejected: no data-use authorization on file.", - "The eligibility check came back not met because Diego has not authorized his data to be used for this review, so no voucher is issued.", - ), - }, - "redeemed-control": { - "unmet": ( - "Rejected: already redeemed this season.", - "The eligibility check came back not met, preventing a double redemption.", - ), - }, - "movement-permit": { - "met": ( - "Yes. The movement permit can be issued.", - "The livestock movement fact came back met under its own purpose.", - ), - "unmet": ( - "No permit on the facts returned.", - "The movement-control check came back not met.", - ), - }, + "positive": {"met": ("Yes. This farmer qualifies for the voucher.", "The signed assertion carries reviewed concept values, not a workbook row.")}, + "authorization-control": {"unmet": ("Rejected: no data-use authorization on file.", "Evidence returned a signed false eligibility value.")}, + "redeemed-control": {"unmet": ("Rejected: already redeemed this season.", "The signed false value prevents double redemption.")}, + "movement-permit": {"met": ("Yes. The movement permit can be issued.", "The livestock requirement was evaluated under its own purpose.")}, + "purpose-denial": {"refused": ("Refused, exactly as designed.", "The voucher purpose cannot authorize the livestock requirement.")}, } def story() -> dict[str, Any]: - return { - "id": SCENARIO_ID, - "title": "Farmer climate-smart voucher", - "short_title": "Farmer voucher", - "proves": "NAgDI farmer and livestock workbooks can back governed voucher and movement-control APIs.", - "domain": "Agriculture", - "availability": "hosted", - "intro": "A supplier checks voucher eligibility without receiving farmer or livestock workbooks.", - "actor": "Voucher redemption desk", - "subject": {"name": "Amina Kone", "identifier": POSITIVE_FARMER}, - "requester": {"name": "NAgDI voucher desk", "purpose": PURPOSES["voucher"]}, - "steps": [ - {"id": "discover", "label": "Discover NAgDI claims", "prompt": "Read the NAgDI claim catalogue.", "button": "Discover", "request_summary": "GET /v1/claims"}, - {"id": "positive", "label": "Evaluate voucher eligibility", "prompt": "Run the positive farmer control.", "button": "Evaluate", "request_summary": "POST voucher claim for FR-1001."}, - {"id": "authorization-control", "label": "Missing authorization control", "prompt": "Reject a farmer who has not authorized this data use.", "button": "Evaluate", "request_summary": "POST voucher claim for FR-1002."}, - {"id": "redeemed-control", "label": "Already redeemed control", "prompt": "Reject an already-redeemed farmer.", "button": "Evaluate", "request_summary": "POST voucher claim for FR-1003."}, - {"id": "movement-permit", "label": "Livestock movement permit", "prompt": "Evaluate the companion movement-control claim.", "button": "Evaluate", "request_summary": "POST livestock movement-control claim."}, - {"id": "purpose-denial", "label": "Purpose denial", "prompt": "Use the wrong purpose for a movement-control request.", "button": "Try denial", "request_summary": "POST livestock claim with voucher purpose."}, - ], - "receipt": [ - {"label": "Credential", "value": "voucher eligibility VC preview"}, - {"label": "Workbook exported", "value": "No"}, - ], - } + return {"id": SCENARIO_ID, "title": "Farmer climate-smart voucher", "short_title": "Farmer voucher", "proves": "NAgDI workbooks can back governed Evidence requirements without workbook export.", "domain": "Agriculture", "availability": "local", "intro": "A supplier checks a minimized signed assertion.", "actor": "Voucher redemption desk", "subject": {"name": "Amina Kone", "identifier": POSITIVE_FARMER}, "requester": {"name": "NAgDI voucher desk", "purpose": PURPOSES["voucher"]}, "steps": [{"id": "discover", "label": "Discover NAgDI requirements", "prompt": "Read Evidence definitions.", "button": "Discover", "request_summary": "GET /v1/evidence-definitions"}, {"id": "positive", "label": "Evaluate voucher eligibility", "prompt": "Run the positive control.", "button": "Evaluate", "request_summary": "POST voucher Evidence requirement."}, {"id": "authorization-control", "label": "Missing authorization control", "prompt": "Reject missing data-use authorization.", "button": "Evaluate", "request_summary": "POST voucher Evidence requirement."}, {"id": "redeemed-control", "label": "Already redeemed control", "prompt": "Reject an already-redeemed farmer.", "button": "Evaluate", "request_summary": "POST voucher Evidence requirement."}, {"id": "movement-permit", "label": "Livestock movement permit", "prompt": "Evaluate movement control.", "button": "Evaluate", "request_summary": "POST livestock Evidence requirement."}, {"id": "purpose-denial", "label": "Purpose denial", "prompt": "Use the wrong purpose.", "button": "Try denial", "request_summary": "POST livestock requirement with voucher purpose."}], "receipt": [{"label": "Artifact", "value": "Signed Evidence JWS"}, {"label": "Workbook exported", "value": "No"}]} def preview_step(config: dict[str, Any], step_id: str) -> dict[str, Any]: @@ -95,45 +36,23 @@ def run_step(config: dict[str, Any], step_id: str) -> dict[str, Any]: def _request(config: dict[str, Any], step_id: str, *, send: bool) -> dict[str, Any]: - url = service_url(SERVICE_ID, "/v1/claims" if step_id == "discover" else "/v1/evaluations") - subject = { - "positive": POSITIVE_FARMER, - "authorization-control": AUTHORIZATION_CONTROL, - "redeemed-control": REDEEMED_CONTROL, - "movement-permit": POSITIVE_FARMER, - "purpose-denial": POSITIVE_FARMER, - }.get(step_id) - claims = MOVEMENT_CLAIMS if step_id in {"movement-permit", "purpose-denial"} else CLAIMS - purpose = request_purpose(config, step_id) - token = service_token(SERVICE_ID) - credential_profile = credential_profile_for_step(step_id) - headers = auth_headers(token, purpose, CLAIM_RESULT_FORMAT if step_id != "discover" else "application/json") - body = None if step_id == "discover" else evaluation_body(subject or "", claims, scheme="farmer_id") - if step_id != "discover" and not subject: + subjects = {"positive": POSITIVE_FARMER, "authorization-control": AUTHORIZATION_CONTROL, "redeemed-control": REDEEMED_CONTROL, "movement-permit": POSITIVE_FARMER, "purpose-denial": POSITIVE_FARMER} + if step_id != "discover" and step_id not in subjects: return standard_error_result(step_id) + client = "nagdi-livestock" if step_id in {"movement-permit", "purpose-denial"} else "nagdi-voucher" + token = service_token(client) if send else "" + purpose = request_purpose(config, step_id) + url = service_url(client, "/v1/evidence-definitions" if step_id == "discover" else "/v1/evidence") + headers = evidence_headers(token, discover=step_id == "discover") + body = None if step_id == "discover" else evidence_body(subjects[step_id], requirement_id(client), purpose, selector_profile="farmer-reference-v1", selector_field="farmer_id") request = request_source("GET" if step_id == "discover" else "POST", url, headers, body) if not send: return {"request_source": request} if not token: - return missing_runtime_token(step_id, SERVICE_NAME, service_token_env(SERVICE_ID), request) - result = http_json("GET" if step_id == "discover" else "POST", url, headers, body) - payload = { - "step_id": step_id, - "friendly": friendly_result(step_id, result, FRIENDLY), - "request_source": request, - "response_source": source_response(result), - } - if credential_profile and result.status and 200 <= result.status < 300: - payload.update(credential_attempt(service_url(SERVICE_ID, "/v1/credentials"), token, purpose, result, credential_profile, claims, SERVICE_ID)) - return payload - - -def credential_profile_for_step(step_id: str) -> str | None: - if step_id == "positive": - return VOUCHER_CREDENTIAL_PROFILE - if step_id == "movement-permit": - return MOVEMENT_CREDENTIAL_PROFILE - return None + return missing_runtime_token(step_id, SERVICE_NAME, service_token_env(client), request) + raw = http_json("GET" if step_id == "discover" else "POST", url, headers, body) + result = raw if step_id == "discover" else normalized_evidence_result(raw) + return {"step_id": step_id, "friendly": friendly_result(step_id, result, FRIENDLY), "request_source": request, "response_source": source_response(result)} def request_purpose(config: dict[str, Any], step_id: str) -> str: @@ -141,6 +60,4 @@ def request_purpose(config: dict[str, Any], step_id: str) -> str: return PURPOSES["voucher"] if isinstance(config.get("purpose_override"), str): return config["purpose_override"] - if step_id == "movement-permit": - return PURPOSES["livestock"] - return PURPOSES["voucher"] + return PURPOSES["livestock"] if step_id == "movement-permit" else PURPOSES["voucher"] diff --git a/scenarios/pension_survivor.py b/scenarios/pension_survivor.py index 7b8f9af..f309e2c 100644 --- a/scenarios/pension_survivor.py +++ b/scenarios/pension_survivor.py @@ -1,161 +1,32 @@ #!/usr/bin/env python3 -"""Death-to-pension-stop plus survivor-benefit guided scenario.""" +"""Death-to-pension-stop and survivor Evidence scenario.""" from __future__ import annotations from typing import Any -from .common import ( - CLAIM_RESULT_FORMAT, - PURPOSES, - StepHttpResult, - auth_headers, - credential_attempt, - evaluation_body, - friendly_result, - http_json, - missing_runtime_token, - request_source, - source_response, - standard_error_result, -) -from .service_config import ( - authority_service_id, - service_token, - service_token_env, - service_url, -) +from .common import PURPOSES, StepHttpResult, evidence_body, evidence_headers, friendly_result, http_json, missing_runtime_token, normalized_evidence_result, request_source, source_response, standard_error_result +from .service_config import requirement_id, service_token, service_token_env, service_url SCENARIO_ID = "death-to-pension-survivor" -SERVICE_NAME = "Pension evidence services" -CRA_CLIENT = "cra-pension" -SIPF_CLIENT = "sipf-pension" +SERVICE_NAME = "Registry Evidence" DECEASED_PENSIONER = "2300109568" SURVIVING_SPOUSE = "2300118698" STALE_CONTROL = "2300127827" DISSOLVED_MARRIAGE_CONTROL = "2300146081" -DEATH_CLAIMS = ["person-is-deceased"] -PAYMENT_CLAIMS = ["pension-payment-active"] -SURVIVOR_CLAIMS = ["survivor-is-eligible"] -CREDENTIAL_PROFILE = "sipf-survivor-benefit.survivor-benefit-status" -CREDENTIAL_STEPS = {"survivor-benefit"} -AUTHORITY_NAMES = { - CRA_CLIENT: "Civil Registration Authority", - SIPF_CLIENT: "Social Insurance and Pensions Fund", -} +AUTHORITY_NAMES = {"cra-pension": "Civil Registration Authority", "sipf-pension": "Social Insurance and Pensions Fund", "sipf-survivor": "Social Insurance and Pensions Fund"} FRIENDLY = { - "discover": { - "met": ( - "The catalogues list what may be asked.", - "Claim definitions only. No pension records have moved.", - ), - }, - "stop-payment": { - "met": ( - "The pension stops.", - "The application combined CRA's death predicate with SIPF's active-payment predicate. Neither authority received the other's source record.", - ), - "unmet": ( - "No stop today.", - "The application only derives a stop when death is registered and SIPF confirms an active payment.", - ), - }, - "survivor-benefit": { - "met": ( - "Yes. The surviving spouse can be offered the benefit.", - "SIPF returned only the survivor eligibility fact and can issue the corresponding credential.", - ), - "unmet": ( - "Not eligible on the facts returned.", - "The SIPF survivor eligibility check came back not met.", - ), - }, - "stale-control": { - "unmet": ( - "No death registered yet, so nothing changes.", - "CRA has no death record for this pensioner. The application does not derive a stop.", - ), - }, - "dissolved-control": { - "unmet": ( - "Rejected: the marriage was dissolved.", - "The SIPF survivor check came back not met because the marriage no longer stands.", - ), - }, - "cause-of-death-denial": { - "refused": ( - "Refused: that question does not exist here.", - "CRA offers no cause-of-death claim for this purpose, so the over-disclosing question cannot be asked.", - ), - }, + "stop-payment": {"met": ("The pension stops.", "The application combined two signed values while each authority kept its source row.")}, + "survivor-benefit": {"met": ("Yes. The surviving spouse can be offered the benefit.", "SIPF returned only the reviewed survivor concept.")}, + "stale-control": {"unmet": ("No death registered yet, so nothing changes.", "The application does not infer a death from an unresolved requirement.")}, + "dissolved-control": {"unmet": ("Rejected: the marriage was dissolved.", "SIPF returned a signed false survivor value.")}, + "cause-of-death-denial": {"refused": ("Refused: that requirement does not exist here.", "Cause of death cannot be requested through this Evidence bundle.")}, } def story() -> dict[str, Any]: - return { - "id": SCENARIO_ID, - "title": "Death to pension stop plus survivor benefit", - "short_title": "Pension stop and survivor benefit", - "proves": "CRA death evidence and SIPF payment evidence can drive an application decision without a cross-authority Notary.", - "domain": "Pensions", - "availability": "hosted", - "intro": "SIPF reviews a death registration without requesting cause of death.", - "actor": "SIPF payments reviewer", - "subject": {"name": "Rafael Nkomo", "identifier": DECEASED_PENSIONER}, - "requester": { - "name": "SIPF review desk", - "purpose": PURPOSES["pension_payment"], - }, - "steps": [ - { - "id": "discover", - "label": "Discover pension claims", - "prompt": "Read the CRA and SIPF catalogues.", - "button": "Discover", - "request_summary": "GET /v1/claims from CRA and SIPF.", - }, - { - "id": "stop-payment", - "label": "Stop pension payment", - "prompt": "Evaluate the deceased pensioner.", - "button": "Evaluate", - "request_summary": "POST the CRA death and SIPF active-payment claims.", - }, - { - "id": "survivor-benefit", - "label": "Preview survivor benefit", - "prompt": "Evaluate survivor eligibility.", - "button": "Evaluate", - "request_summary": "POST the SIPF survivor claim.", - }, - { - "id": "stale-control", - "label": "Death not yet registered", - "prompt": "Show stale-data reconciliation.", - "button": "Evaluate", - "request_summary": "POST the CRA and SIPF claims for the stale control UIN.", - }, - { - "id": "dissolved-control", - "label": "Dissolved marriage control", - "prompt": "Reject survivor eligibility when the marriage was dissolved.", - "button": "Evaluate", - "request_summary": "POST the SIPF survivor claim for the dissolved-marriage control UIN.", - }, - { - "id": "cause-of-death-denial", - "label": "Purpose denial", - "prompt": "Ask for cause of death and get denied.", - "button": "Try denial", - "request_summary": "POST an unavailable CRA cause-of-death claim.", - }, - ], - "receipt": [ - {"label": "Credential", "value": "SIPF survivor-benefit status VC preview"}, - {"label": "Cause of death disclosed", "value": "No"}, - ], - } + return {"id": SCENARIO_ID, "title": "Death to pension stop plus survivor benefit", "short_title": "Pension stop and survivor benefit", "proves": "CRA and SIPF signed evidence can drive an application decision without a cross-authority decision service.", "domain": "Pensions", "availability": "local", "intro": "SIPF reviews death evidence without requesting cause of death.", "actor": "SIPF payments reviewer", "subject": {"name": "Rafael Nkomo", "identifier": DECEASED_PENSIONER}, "requester": {"name": "SIPF review desk", "purpose": PURPOSES["pension_payment"]}, "steps": [{"id": "discover", "label": "Discover pension requirements", "prompt": "Read Evidence definitions.", "button": "Discover", "request_summary": "GET /v1/evidence-definitions"}, {"id": "stop-payment", "label": "Stop pension payment", "prompt": "Evaluate the deceased pensioner.", "button": "Evaluate", "request_summary": "POST CRA death and SIPF active-payment requirements."}, {"id": "survivor-benefit", "label": "Preview survivor benefit", "prompt": "Evaluate survivor eligibility.", "button": "Evaluate", "request_summary": "POST SIPF survivor requirement."}, {"id": "stale-control", "label": "Death not yet registered", "prompt": "Show stale-data reconciliation.", "button": "Evaluate", "request_summary": "POST CRA and SIPF requirements."}, {"id": "dissolved-control", "label": "Dissolved marriage control", "prompt": "Reject survivor eligibility.", "button": "Evaluate", "request_summary": "POST SIPF survivor requirement."}, {"id": "cause-of-death-denial", "label": "Requirement denial", "prompt": "Ask for cause of death and get denied.", "button": "Try denial", "request_summary": "POST an unconfigured requirement."}], "receipt": [{"label": "Artifact", "value": "Signed Evidence JWS"}, {"label": "Cause of death disclosed", "value": "No"}]} def preview_step(config: dict[str, Any], step_id: str) -> dict[str, Any]: @@ -167,234 +38,53 @@ def run_step(config: dict[str, Any], step_id: str) -> dict[str, Any]: def _request(config: dict[str, Any], step_id: str, *, send: bool) -> dict[str, Any]: - subject = { - "stop-payment": DECEASED_PENSIONER, - "survivor-benefit": SURVIVING_SPOUSE, - "stale-control": STALE_CONTROL, - "dissolved-control": DISSOLVED_MARRIAGE_CONTROL, - "cause-of-death-denial": DECEASED_PENSIONER, - }.get(step_id) - if step_id != "discover" and not subject: + subject = {"stop-payment": DECEASED_PENSIONER, "survivor-benefit": SURVIVING_SPOUSE, "stale-control": STALE_CONTROL, "dissolved-control": DISSOLVED_MARRIAGE_CONTROL, "cause-of-death-denial": DECEASED_PENSIONER}.get(step_id) + if step_id != "discover" and subject is None: return standard_error_result(step_id) + token = service_token("cra-pension") if send else "" purpose = request_purpose(config, step_id) - requests = authority_requests(step_id, subject, purpose) - preview = combined_request_preview(requests, purpose) + requests = _requests(step_id, subject or "", purpose, token) + preview = requests[0]["source"] if len(requests) == 1 else {"method": "MULTI", "url": "solmara://registry-evidence", "purpose": purpose, "requests": [item["source"] for item in requests]} if not send: return {"request_source": preview} - - missing = next( - (request for request in requests if not service_token(request["client_id"])), - None, - ) - if missing: - result = missing_runtime_token( - step_id, - SERVICE_NAME, - service_token_env(missing["client_id"]), - preview, - ) - result["request_sources"] = [request["source"] for request in requests] - return result - - responses: list[tuple[dict[str, Any], StepHttpResult]] = [] - for authority_request in requests: - result = http_json( - authority_request["method"], - authority_request["url"], - authority_request["headers"], - authority_request["body"], - ) - responses.append((authority_request, result)) - - aggregate = aggregate_response(responses) - payload: dict[str, Any] = { - "step_id": step_id, - "friendly": friendly_result(step_id, aggregate, FRIENDLY), - "request_source": preview, - "request_sources": [request["source"] for request, _ in responses], - "response_source": source_response(aggregate), - "source_trace": [ - authority_trace(request, response) for request, response in responses - ], - } + if not token: + return missing_runtime_token(step_id, SERVICE_NAME, service_token_env("cra-pension"), preview) + responses = [(item, normalized_evidence_result(http_json(item["method"], item["url"], item["headers"], item["body"]))) for item in requests] + aggregate = _aggregate(responses) + payload: dict[str, Any] = {"step_id": step_id, "friendly": friendly_result(step_id, aggregate, FRIENDLY), "request_source": preview, "request_sources": [item["source"] for item, _ in responses], "response_source": source_response(aggregate), "source_trace": [{"authority": AUTHORITY_NAMES.get(item["client_id"], "Registry Evidence"), "service_id": "registry-evidence", "status": response.status} for item, response in responses]} if step_id in {"stop-payment", "stale-control"} and aggregate.status == 200: - payload["derived_decisions"] = { - "pension-payment-should-stop": derived_stop_decision(aggregate.body), - "owner": "pension-review-application", - } - if step_id in CREDENTIAL_STEPS and aggregate.status == 200: - sipf_result = responses[0][1] - payload.update( - credential_attempt( - service_url(SIPF_CLIENT, "/v1/credentials"), - service_token(SIPF_CLIENT), - purpose, - sipf_result, - CREDENTIAL_PROFILE, - SURVIVOR_CLAIMS, - authority_service_id(SIPF_CLIENT), - ) - ) + values = {item.get("claim_id"): item.get("satisfied") for item in aggregate.body.get("results", [])} + payload["derived_decisions"] = {"pension-payment-should-stop": values.get("person-is-deceased") is True and values.get("pension-payment-active") is True, "owner": "pension-review-application"} return payload -def authority_requests( - step_id: str, subject: str | None, purpose: str -) -> list[dict[str, Any]]: +def _requests(step_id: str, subject: str, purpose: str, token: str) -> list[dict[str, Any]]: if step_id == "discover": - return [ - build_request(CRA_CLIENT, "GET", "/v1/claims", purpose), - build_request(SIPF_CLIENT, "GET", "/v1/claims", purpose), - ] - if step_id in {"stop-payment", "stale-control"}: - return [ - build_request( - CRA_CLIENT, "POST", "/v1/evaluations", purpose, subject, DEATH_CLAIMS - ), - build_request( - SIPF_CLIENT, "POST", "/v1/evaluations", purpose, subject, PAYMENT_CLAIMS - ), - ] - if step_id in {"survivor-benefit", "dissolved-control"}: - return [ - build_request( - SIPF_CLIENT, - "POST", - "/v1/evaluations", - purpose, - subject, - SURVIVOR_CLAIMS, - ) - ] + url = service_url("cra-pension", "/v1/evidence-definitions") + headers = evidence_headers(token, discover=True) + return [{"client_id": "cra-pension", "method": "GET", "url": url, "headers": headers, "body": None, "source": request_source("GET", url, headers)}] + clients = ["cra-pension", "sipf-pension"] if step_id in {"stop-payment", "stale-control"} else ["sipf-survivor"] if step_id == "cause-of-death-denial": - return [ - build_request( - CRA_CLIENT, - "POST", - "/v1/evaluations", - purpose, - subject, - ["cause-of-death"], - ) - ] - return [] - - -def build_request( - client_id: str, - method: str, - path: str, - purpose: str, - subject: str | None = None, - claims: list[str] | None = None, - response_format: str = CLAIM_RESULT_FORMAT, -) -> dict[str, Any]: - token = service_token(client_id) - url = service_url(client_id, path) - headers = auth_headers( - token, purpose, response_format if method == "POST" else "application/json" - ) - body = ( - evaluation_body( - subject or "", claims or [], scheme="solmara_uin", format=response_format - ) - if method == "POST" - else None - ) - return { - "client_id": client_id, - "method": method, - "url": url, - "headers": headers, - "body": body, - "source": request_source(method, url, headers, body), - } - - -def combined_request_preview( - requests: list[dict[str, Any]], purpose: str -) -> dict[str, Any]: - if len(requests) == 1: - return requests[0]["source"] - return { - "method": "MULTI", - "url": "solmara://authority-notaries", - "headers": {"Data-Purpose": purpose}, - "requests": [request["source"] for request in requests], - } - - -def aggregate_response( - responses: list[tuple[dict[str, Any], StepHttpResult]], -) -> StepHttpResult: - failed = next( - ( - response - for _, response in responses - if response.status is None or not 200 <= response.status < 300 - ), - None, - ) + clients = ["cra-pension"] + items = [] + for client in clients: + url = service_url(client) + headers = evidence_headers(token) + requirement = "https://id.registrystack.org/solmara/requirement/cra-cause-of-death/v1" if step_id == "cause-of-death-denial" else requirement_id(client) + body = evidence_body(subject, requirement, purpose) + items.append({"client_id": client, "method": "POST", "url": url, "headers": headers, "body": body, "source": request_source("POST", url, headers, body)}) + return items + + +def _aggregate(responses: list[tuple[dict[str, Any], StepHttpResult]]) -> StepHttpResult: + failed = next((response for _, response in responses if response.status is None or not 200 <= response.status < 300), None) if failed: return failed - results: list[dict[str, Any]] = [] - for request, response in responses: - body = response.body if isinstance(response.body, dict) else {} - raw_results = body.get("results") - if request["method"] == "POST" and not isinstance(raw_results, list): - return StepHttpResult( - 502, - { - "code": "authority.invalid_response", - "detail": "An authority response omitted claim results.", - }, - {}, - ) - for result in raw_results or []: - if isinstance(result, dict): - results.append( - { - **result, - "authority": AUTHORITY_NAMES[request["client_id"]], - "notary_service_id": authority_service_id(request["client_id"]), - } - ) - return StepHttpResult( - 200, {"results": results}, {"content-type": "application/json"} - ) - - -def derived_stop_decision(body: Any) -> bool | None: - if not isinstance(body, dict) or not isinstance(body.get("results"), list): - return None - satisfied = { - result.get("claim_id"): result.get("satisfied") - for result in body["results"] - if isinstance(result, dict) - } - death = satisfied.get("person-is-deceased") - active = satisfied.get("pension-payment-active") - return ( - death and active - if isinstance(death, bool) and isinstance(active, bool) - else None - ) - - -def authority_trace( - request: dict[str, Any], response: StepHttpResult -) -> dict[str, Any]: - return { - "authority": AUTHORITY_NAMES[request["client_id"]], - "service_id": authority_service_id(request["client_id"]), - "request_source": request["source"], - "response_source": source_response(response), - } + results = [entry for _, response in responses for entry in response.body.get("results", [])] + return StepHttpResult(200, {"results": results, "signed_evidence": [response.body.get("signed_evidence") for _, response in responses]}, {"content-type": "application/json"}) def request_purpose(config: dict[str, Any], step_id: str) -> str: if isinstance(config.get("purpose_override"), str): return config["purpose_override"] - if step_id in {"survivor-benefit", "dissolved-control"}: - return PURPOSES["survivor_benefit"] - return PURPOSES["pension_payment"] + return PURPOSES["survivor_benefit"] if step_id in {"survivor-benefit", "dissolved-control"} else PURPOSES["pension_payment"] diff --git a/scenarios/service_config.py b/scenarios/service_config.py index e656e59..574b363 100644 --- a/scenarios/service_config.py +++ b/scenarios/service_config.py @@ -1,96 +1,61 @@ #!/usr/bin/env python3 -"""Runtime endpoint lookup for guided Solmara scenarios.""" +"""One Evidence endpoint with requirement-specific policy identifiers.""" from __future__ import annotations import os -from urllib.parse import urljoin - -SERVICE_ENDPOINTS = { - "child-benefit-federator": { - "url_env": "CHILD_BENEFIT_FEDERATOR_URL", - "token_env": "CHILD_BENEFIT_FEDERATOR_TOKEN", - "default_url": "http://127.0.0.1:4321", - }, - "cra-child-benefit": { - "service_id": "cra-notary", - "url_env": "CRA_NOTARY_URL", - "token_env": "CRA_CHILD_BENEFIT_CLIENT_TOKEN", - "default_url": "http://127.0.0.1:4325", - }, - "nia-child-benefit": { - "service_id": "nia-notary", - "url_env": "NIA_NOTARY_URL", - "token_env": "NIA_CHILD_BENEFIT_CLIENT_TOKEN", - "default_url": "http://127.0.0.1:4326", - }, - "sro-child-benefit": { - "service_id": "sro-notary", - "url_env": "SRO_NOTARY_URL", - "token_env": "SRO_CHILD_BENEFIT_CLIENT_TOKEN", - "default_url": "http://127.0.0.1:4327", - }, - "programme-child-benefit": { - "service_id": "programme-notary", - "url_env": "PROGRAMME_NOTARY_URL", - "token_env": "PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN", - "default_url": "http://127.0.0.1:4328", - }, - "cra-pension": { - "service_id": "cra-notary", - "url_env": "CRA_NOTARY_URL", - "token_env": "CRA_PENSION_CLIENT_TOKEN", - "default_url": "http://127.0.0.1:4325", - }, - "sipf-pension": { - "service_id": "sipf-notary", - "url_env": "SIPF_NOTARY_URL", - "token_env": "SIPF_PENSION_CLIENT_TOKEN", - "default_url": "http://127.0.0.1:4322", - }, - "cra-citizen": { - "service_id": "cra-notary", - "url_env": "CRA_NOTARY_URL", - "token_env": "CRA_CITIZEN_CLIENT_TOKEN", - "default_url": "http://127.0.0.1:4325", - }, - "nia-citizen": { - "service_id": "nia-notary", - "url_env": "NIA_NOTARY_URL", - "token_env": "NIA_CITIZEN_CLIENT_TOKEN", - "default_url": "http://127.0.0.1:4326", - }, - "nagdi-notary": { - "service_id": "nagdi-notary", - "url_env": "NAGDI_NOTARY_URL", - "token_env": "NAGDI_NOTARY_TOKEN", - "default_url": "http://127.0.0.1:4323", - }, +from .common import evidence_access_token, joined_url + + +BASE = "https://id.registrystack.org/solmara/requirement" +REQUIREMENTS = { + "cra-child-benefit": f"{BASE}/cra-child-benefit/v1", + "nia-child-benefit": f"{BASE}/nia-child-benefit/v1", + "sro-child-benefit": f"{BASE}/sro-child-benefit/v1", + "programme-child-benefit": f"{BASE}/mosd-child-benefit/v1", + "cra-pension": f"{BASE}/cra-pension-death/v1", + "sipf-pension": f"{BASE}/sipf-pension-payment/v1", + "sipf-survivor": f"{BASE}/sipf-survivor-benefit/v1", + "cra-citizen": f"{BASE}/cra-citizen-record/v1", + "nia-citizen": f"{BASE}/nia-citizen-status/v1", + "nagdi-voucher": f"{BASE}/nagdi-voucher/v1", + "nagdi-livestock": f"{BASE}/nagdi-livestock/v1", } -def service_url(service_id: str, path: str) -> str: - entry = service_entry(service_id) - base_url = os.environ.get(entry["url_env"], entry["default_url"]) - return urljoin(base_url.rstrip("/") + "/", path.lstrip("/")) +def service_url(service_id: str, path: str = "/v1/evidence") -> str: + if service_id == "child-benefit-federator": + return joined_url( + os.environ.get("CHILD_BENEFIT_FEDERATOR_URL", "http://127.0.0.1:4321"), + path, + ) + return joined_url( + os.environ.get("SOLMARA_EVIDENCE_URL", "https://evidence.solmara.invalid"), + path, + ) def service_token(service_id: str) -> str: - return os.environ.get(service_token_env(service_id), "") + if service_id == "child-benefit-federator": + return os.environ.get("CHILD_BENEFIT_FEDERATOR_TOKEN", "") + return evidence_access_token() def service_token_env(service_id: str) -> str: - return service_entry(service_id)["token_env"] - - -def authority_service_id(service_id: str) -> str: - entry = service_entry(service_id) - return entry.get("service_id", service_id) + return ( + "CHILD_BENEFIT_FEDERATOR_TOKEN" + if service_id == "child-benefit-federator" + else "SOLMARA_EVIDENCE_CLIENT_KEY" + ) -def service_entry(service_id: str) -> dict[str, str]: +def requirement_id(service_id: str) -> str: try: - return SERVICE_ENDPOINTS[service_id] + return REQUIREMENTS[service_id] except KeyError as error: - raise ValueError(f"unknown service endpoint: {service_id}") from error + raise ValueError(f"unknown Evidence requirement: {service_id}") from error + + +def authority_service_id(service_id: str) -> str: + return "registry-evidence" diff --git a/scripts/build-registry-stack-runtime.sh b/scripts/build-registry-stack-runtime.sh new file mode 100755 index 0000000..4c031fb --- /dev/null +++ b/scripts/build-registry-stack-runtime.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) + +set -a +# shellcheck disable=SC1091 +. "$root/versions.env" +set +a + +source_ref=${REGISTRY_STACK_SOURCE_REF:?missing REGISTRY_STACK_SOURCE_REF} +source_commit=${REGISTRY_STACK_SOURCE_COMMIT:?missing REGISTRY_STACK_SOURCE_COMMIT} +relay_image=${REGISTRY_RELAY_IMAGE:?missing REGISTRY_RELAY_IMAGE} +evidence_image=${SOLMARA_EVIDENCE_IMAGE:?missing SOLMARA_EVIDENCE_IMAGE} +mint_image=${SOLMARA_MINT_IMAGE:?missing SOLMARA_MINT_IMAGE} +source_dir=${REGISTRY_STACK_SOURCE_DIR:-"$root/../registry-stack"} + +if [ "$source_ref" != "main" ]; then + echo "REGISTRY_STACK_SOURCE_REF must be main" >&2 + exit 1 +fi +case "$source_commit" in + *[!0-9a-f]*) + echo "REGISTRY_STACK_SOURCE_COMMIT must be exactly 40 lowercase hex characters" >&2 + exit 1 + ;; +esac +if [ "${#source_commit}" -ne 40 ]; then + echo "REGISTRY_STACK_SOURCE_COMMIT must be exactly 40 lowercase hex characters" >&2 + exit 1 +fi +if [ ! -d "$source_dir/.git" ]; then + echo "REGISTRY_STACK_SOURCE_DIR must name a Registry Stack checkout" >&2 + exit 1 +fi +if ! git -C "$source_dir" cat-file -e "$source_commit^{commit}"; then + echo "Registry Stack source commit is unavailable in $source_dir" >&2 + exit 1 +fi +resolved_ref=$(git -C "$source_dir" rev-parse "$source_ref^{commit}") +remote_main=$(git -C "$source_dir" rev-parse "refs/remotes/origin/$source_ref^{commit}" 2>/dev/null || true) +if [ "$resolved_ref" != "$source_commit" ] || [ "$remote_main" != "$source_commit" ]; then + echo "Registry Stack main does not match $source_commit; fetch origin/main and update versions.env intentionally" >&2 + exit 1 +fi + +all_current=true +for image in "$relay_image" "$evidence_image" "$mint_image"; do + revision=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$image" 2>/dev/null || true) + if [ "$revision" != "$source_commit" ]; then + all_current=false + fi +done +if [ "$all_current" = true ]; then + echo "Registry Stack runtime images already match $source_commit" + exit 0 +fi + +temporary=$(mktemp -d "${TMPDIR:-/tmp}/solmara-registry-stack.XXXXXX") +worktree="$temporary/source" +cleanup() { + git -C "$source_dir" worktree remove --force "$worktree" >/dev/null 2>&1 || true + rm -rf "$temporary" +} +trap cleanup EXIT HUP INT TERM +git -C "$source_dir" worktree add --detach "$worktree" "$source_commit" >/dev/null + +platform_args="" +if [ -n "${REGISTRY_STACK_PLATFORM:-}" ]; then + platform_args="--platform $REGISTRY_STACK_PLATFORM" +fi + +# shellcheck disable=SC2086 +docker buildx build --load $platform_args \ + --label "org.opencontainers.image.revision=$source_commit" \ + --label "org.opencontainers.image.ref.name=$source_ref" \ + --tag "$relay_image" \ + --file "$root/docker/registry-stack-runtime/Dockerfile" \ + --target relay \ + "$worktree" + +for target in evidence mint; do + case "$target" in + evidence) image=$evidence_image ;; + mint) image=$mint_image ;; + esac + # shellcheck disable=SC2086 + docker buildx build --load $platform_args \ + --label "org.opencontainers.image.revision=$source_commit" \ + --label "org.opencontainers.image.ref.name=$source_ref" \ + --tag "$image" \ + --file "$worktree/docker/Dockerfile" \ + --target "$target" \ + "$worktree" +done diff --git a/scripts/build-relay-runtime.sh b/scripts/build-relay-runtime.sh index 92b85f9..050cac9 100755 --- a/scripts/build-relay-runtime.sh +++ b/scripts/build-relay-runtime.sh @@ -2,78 +2,4 @@ set -eu root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) - -set -a -# shellcheck disable=SC1091 -. "$root/versions.env" -set +a - -source_ref=${REGISTRY_STACK_SOURCE_REF:?missing REGISTRY_STACK_SOURCE_REF} -source_commit=${REGISTRY_STACK_SOURCE_COMMIT:?missing REGISTRY_STACK_SOURCE_COMMIT} -runtime_image=${SOLMARA_RELAY_DEV_IMAGE:?missing SOLMARA_RELAY_DEV_IMAGE} -release_image=${REGISTRY_RELAY_IMAGE:?missing REGISTRY_RELAY_IMAGE} -platform=${REGISTRY_STACK_PLATFORM:-linux/amd64} - -case "$source_ref" in - v[0-9]*.[0-9]*.[0-9]*) ;; - *) - echo "REGISTRY_STACK_SOURCE_REF must be a stable vMAJOR.MINOR.PATCH tag" >&2 - exit 1 - ;; -esac -case "$source_commit" in - *[!0-9a-f]*) - echo "REGISTRY_STACK_SOURCE_COMMIT must be exactly 40 lowercase hex characters" >&2 - exit 1 - ;; -esac -if [ "${#source_commit}" -ne 40 ]; then - echo "REGISTRY_STACK_SOURCE_COMMIT must be exactly 40 lowercase hex characters" >&2 - exit 1 -fi - -image_revision=$( - docker image inspect \ - --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' \ - "$runtime_image" 2>/dev/null || true -) -image_base=$( - docker image inspect \ - --format '{{ index .Config.Labels "org.opencontainers.image.base.name" }}' \ - "$runtime_image" 2>/dev/null || true -) -if [ "$image_revision" = "$source_commit" ] \ - && [ "$image_base" = "$release_image" ]; then - echo "Solmara Relay development image already matches $source_ref" - exit 0 -fi - -if [ -n "${REGISTRY_STACK_SOURCE_DIR:-}" ]; then - source_dir=$REGISTRY_STACK_SOURCE_DIR -else - source_dir="$root/.cache/registry-stack/$source_commit" - if [ ! -d "$source_dir/.git" ]; then - mkdir -p "$(dirname -- "$source_dir")" - git clone --filter=blob:none --depth 1 --branch "$source_ref" \ - https://github.com/registrystack/registry-stack.git "$source_dir" - fi -fi - -actual_commit=$(git -C "$source_dir" rev-parse HEAD) -if [ "$actual_commit" != "$source_commit" ]; then - echo "Registry Stack source mismatch: expected $source_commit, found $actual_commit" >&2 - exit 1 -fi -if [ -n "$(git -C "$source_dir" status --porcelain)" ]; then - echo "Registry Stack source checkout must be clean: $source_dir" >&2 - exit 1 -fi - -docker buildx build \ - --load \ - --platform "$platform" \ - --build-arg "REGISTRY_RELAY_IMAGE=$release_image" \ - --build-arg "REGISTRY_STACK_SOURCE_COMMIT=$source_commit" \ - --tag "$runtime_image" \ - --file "$root/docker/relay-runtime/Dockerfile" \ - "$source_dir" +exec "$root/scripts/build-registry-stack-runtime.sh" diff --git a/scripts/check-config-secrets.py b/scripts/check-config-secrets.py index 1d9d819..3efa6de 100755 --- a/scripts/check-config-secrets.py +++ b/scripts/check-config-secrets.py @@ -13,6 +13,7 @@ "metadata", "projects", "runtime/registry-projects", + "evidence", "config/hosted-relay-bundles", "compose.yaml", "compose.hosted.yaml", @@ -25,8 +26,8 @@ "token_env:", "private_jwk_env:", "hash_secret_env:", + "secret:file/", "POSTGRES_PASSWORD:", - "REGISTRY_NOTARY_REPLAY_REDIS_URL:", ) WORKLOAD_TOKEN_VOLUME = re.compile( r"^\s*-\s*[a-z0-9-]+-workload-token:/run/(?:esignet-)?secrets(?::ro)?\s*$" diff --git a/scripts/check-evidence-runtime.py b/scripts/check-evidence-runtime.py new file mode 100755 index 0000000..d41a31c --- /dev/null +++ b/scripts/check-evidence-runtime.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Validate the authored Evidence bundle and paired Mint configuration.""" + +from __future__ import annotations + +import shutil +import stat +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +LOCAL = ROOT / "config/evidence/local" + + +def main() -> int: + version = versions()["REGISTRYCTL_VERSION"] + require_tool_version("evidencectl", version) + require_tool_version("mint", version) + required = ( + LOCAL / "evidence/audit-hmac-key", + LOCAL / "evidence/signing-ed25519-private-jwk", + LOCAL / "evidence/subject-binding-hmac-key", + LOCAL / "mint/audit-hmac-key", + LOCAL / "mint/signing.jwk", + LOCAL / "mint/clients/solmara-demo.yaml", + LOCAL / "tls/ca.crt", + LOCAL / "tls/gateway.crt", + ) + missing = [path for path in required if not path.is_file()] + if missing: + names = ", ".join(str(path.relative_to(ROOT)) for path in missing) + raise SystemExit(f"missing generated Evidence material: {names}; run `just gen-secrets`") + + with tempfile.TemporaryDirectory(prefix="solmara-evidence-check-") as directory: + stage = Path(directory) + evidence_project = stage / "evidence" + shutil.copytree(ROOT / "evidence", evidence_project) + make_immutable(evidence_project / "bundle") + audit = stage / "audit" + audit.mkdir(mode=0o700) + evidence_secrets = stage / "evidence-secrets" + evidence_secrets.mkdir(mode=0o700) + for name in ( + "audit-hmac-key", + "signing-ed25519-private-jwk", + "subject-binding-hmac-key", + ): + shutil.copy2(LOCAL / "evidence" / name, evidence_secrets / name) + for authority in ("cra", "nia", "sro", "programme", "sipf", "nagdi"): + token = evidence_secrets / f"{authority}-relay-token" + token.write_text("fixture-only-token") + token.chmod(0o600) + staged_ca = stage / "ca.crt" + shutil.copy2(LOCAL / "tls/ca.crt", staged_ca) + staged_ca.chmod(staged_ca.stat().st_mode & ~stat.S_IWUSR) + + runtime = (evidence_project / "runtime.yaml").read_text() + replacements = { + "/etc/registry-evidence/bundle": str(evidence_project / "bundle"), + "bindHost: 172.29.0.10": "bindHost: 127.0.0.1", + "/run/secrets/registry-evidence": str(evidence_secrets), + "/var/lib/registry-evidence/audit/evidence.jsonl": str(audit / "evidence.jsonl"), + "/etc/registry-evidence/tls/lab-ca.crt": str(staged_ca), + } + for old, new in replacements.items(): + runtime = runtime.replace(old, new) + staged_runtime = evidence_project / "runtime.yaml" + staged_runtime.write_text(runtime) + staged_runtime.chmod(staged_runtime.stat().st_mode & ~stat.S_IWUSR) + + mint_config = (ROOT / "evidence/mint.yaml").read_text() + mint_replacements = { + "/run/secrets/registry-mint/": f"{LOCAL / 'mint'}/", + "/var/lib/registry-mint/audit/mint.jsonl": str(audit / "mint.jsonl"), + "/etc/registry-mint/clients": str(LOCAL / "mint/clients"), + } + for old, new in mint_replacements.items(): + mint_config = mint_config.replace(old, new) + staged_mint = stage / "mint.yaml" + staged_mint.write_text(mint_config) + + subprocess.run(["mint", "check", "--config", str(staged_mint)], check=True) + subprocess.run( + ["evidencectl", "fixtures", "run", "--project", str(evidence_project)], + check=True, + ) + return 0 + + +def versions() -> dict[str, str]: + return { + key: value + for line in (ROOT / "versions.env").read_text().splitlines() + if line and not line.startswith("#") and "=" in line + for key, value in [line.split("=", 1)] + } + + +def require_tool_version(command: str, expected: str) -> None: + path = shutil.which(command) + if not path: + raise SystemExit(f"{command} is missing; install Registry Stack {expected} tools from the pinned source checkout") + output = subprocess.run([path, "--version"], check=True, capture_output=True, text=True).stdout.strip() + actual = output.rsplit(" ", 1)[-1] + if actual != expected: + raise SystemExit(f"{command} {actual} does not match pinned Registry Stack {expected}") + + +def make_immutable(root: Path) -> None: + for path in [root, *root.rglob("*")]: + mode = path.stat().st_mode + path.chmod(mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)) + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except subprocess.CalledProcessError as error: + raise SystemExit(error.returncode) from error diff --git a/scripts/check-image-pins.py b/scripts/check-image-pins.py index d781465..96e79cb 100755 --- a/scripts/check-image-pins.py +++ b/scripts/check-image-pins.py @@ -10,13 +10,12 @@ ROOT = Path(__file__).resolve().parents[1] LINE_RE = re.compile(r"^([A-Z0-9_]+)=([^#\s]+)$") PIN_RE = re.compile(r"^[^#\s]+@sha256:[0-9a-f]{64}$") -REGISTRY_STACK_IMAGE_KEYS = {"REGISTRY_RELAY_IMAGE", "REGISTRY_NOTARY_IMAGE"} -PINNED_IMAGE_KEYS = REGISTRY_STACK_IMAGE_KEYS | { - "VOLUME_INIT_IMAGE", +SOURCE_IMAGE_KEYS = { + "REGISTRY_RELAY_IMAGE", + "SOLMARA_EVIDENCE_IMAGE", + "SOLMARA_MINT_IMAGE", } -COMPOSE_FALLBACK_RE = re.compile( - r"\$\{(?PREGISTRY_(?:RELAY|NOTARY)_IMAGE):-(?P[^}]+)\}" -) +PINNED_IMAGE_KEYS = {"VOLUME_INIT_IMAGE", "EVIDENCE_GATEWAY_IMAGE"} def main() -> int: @@ -42,31 +41,24 @@ def main() -> int: if "@latest" in value or ":latest" in value: failures.append(f"versions.env:{line_no}: latest tags are not allowed") - for key in PINNED_IMAGE_KEYS: + for key in PINNED_IMAGE_KEYS | SOURCE_IMAGE_KEYS: if key not in values: failures.append(f"versions.env: {key} is required") - compose_files = [ROOT / "compose.yaml", ROOT / "compose.hosted.yaml"] - compose_files.extend(sorted(ROOT.glob("compose.coolify*.yaml"))) - fallback_counts = {key: 0 for key in REGISTRY_STACK_IMAGE_KEYS} + compose_files = [ROOT / "compose.yaml", ROOT / "compose.esignet.yaml"] + required_counts = {key: 0 for key in SOURCE_IMAGE_KEYS} for compose in compose_files: if not compose.exists(): continue text = compose.read_text() if "@latest" in text or ":latest" in text: failures.append(f"{compose.name}: latest tags are not allowed") - for fallback in COMPOSE_FALLBACK_RE.finditer(text): - key = fallback.group("key") - fallback_counts[key] += 1 - expected = values.get(key) - if expected and fallback.group("value") != expected: - failures.append( - f"{compose.name}: {key} fallback must match versions.env" - ) + for key in SOURCE_IMAGE_KEYS: + required_counts[key] += text.count(f"${{{key}:?") - for key, count in fallback_counts.items(): + for key, count in required_counts.items(): if count == 0: - failures.append(f"compose files: expected a {key} fallback") + failures.append(f"compose files: expected a required {key} reference") if failures: for failure in failures: diff --git a/scripts/gen-secrets.py b/scripts/gen-secrets.py index 67fde6a..2ec6a1b 100755 --- a/scripts/gen-secrets.py +++ b/scripts/gen-secrets.py @@ -5,7 +5,6 @@ import argparse import base64 -import hashlib import json import secrets import shlex @@ -16,21 +15,7 @@ ROOT = Path(__file__).resolve().parents[1] POSTGRES_SSL_DIR = ROOT / "config" / "postgres" / "ssl" - -RAW_HASH_PAIRS = [ - ("CRA_CHILD_BENEFIT_CLIENT_TOKEN", "CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH"), - ("CRA_PENSION_CLIENT_TOKEN", "CRA_PENSION_CLIENT_TOKEN_HASH"), - ("CRA_CITIZEN_CLIENT_TOKEN", "CRA_CITIZEN_CLIENT_TOKEN_HASH"), - ("NIA_CHILD_BENEFIT_CLIENT_TOKEN", "NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH"), - ("NIA_CITIZEN_CLIENT_TOKEN", "NIA_CITIZEN_CLIENT_TOKEN_HASH"), - ("SRO_CHILD_BENEFIT_CLIENT_TOKEN", "SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH"), - ( - "PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN", - "PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH", - ), - ("SIPF_PENSION_CLIENT_TOKEN", "SIPF_PENSION_CLIENT_TOKEN_HASH"), - ("NAGDI_NOTARY_TOKEN", "NAGDI_CLIENT_TOKEN_HASH"), -] +EVIDENCE_LOCAL_DIR = ROOT / "config" / "evidence" / "local" JWK_KIDS = { "CRA_RELAY_WORKLOAD_JWK": "solmara-cra-relay-workload-key-1", @@ -40,25 +25,12 @@ "PROGRAMME_RELAY_WORKLOAD_JWK": "solmara-programme-relay-workload-key-1", "SIPF_RELAY_WORKLOAD_JWK": "solmara-sipf-relay-workload-key-1", "NAGDI_RELAY_WORKLOAD_JWK": "solmara-nagdi-relay-workload-key-1", - "NIA_NOTARY_ISSUER_JWK": "did:web:id.registrystack.org:solmara:authority:nia#issuer-key-1", - "SIPF_NOTARY_ISSUER_JWK": "did:web:id.registrystack.org:solmara:authority:sipf#issuer-key-1", - "NAGDI_NOTARY_ISSUER_JWK": "did:web:id.registrystack.org:solmara:authority:nagdi#issuer-key-1", -} - -DIRECT_PROJECT_SECRET_NAMES = { - "SOLMARA_NIA_DATABASE_URL", - "SOLMARA_SIPF_DATABASE_URL", } - def raw_key() -> str: return secrets.token_urlsafe(32) -def fingerprint(value: str) -> str: - return "sha256:" + hashlib.sha256(value.encode("ascii")).hexdigest() - - def b64url(raw: bytes) -> str: return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") @@ -90,6 +62,192 @@ def local_ed25519_jwk(kid: str) -> str: return json.dumps(jwk, separators=(",", ":"), sort_keys=True) +def public_jwk(private_jwk: str) -> dict[str, str]: + jwk = json.loads(private_jwk) + return {key: jwk[key] for key in ("kty", "crv", "kid", "alg", "x")} + + +def write_private(path: Path, value: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(value.rstrip("\n") + "\n") + path.chmod(0o600) + + +def ensure_evidence_material() -> None: + evidence_dir = EVIDENCE_LOCAL_DIR / "evidence" + mint_dir = EVIDENCE_LOCAL_DIR / "mint" + tls_dir = EVIDENCE_LOCAL_DIR / "tls" + for directory in (evidence_dir, mint_dir / "clients", tls_dir): + directory.mkdir(parents=True, exist_ok=True) + + private_paths = ( + evidence_dir / "audit-hmac-key", + evidence_dir / "subject-binding-hmac-key", + evidence_dir / "signing-ed25519-private-jwk", + mint_dir / "signing.jwk", + mint_dir / "audit-hmac-key", + mint_dir / "client-private.jwk", + tls_dir / "ca.key", + tls_dir / "gateway.key", + ) + public_paths = ( + mint_dir / "clients" / "solmara-demo.yaml", + tls_dir / "ca.crt", + tls_dir / "gateway.crt", + ) + material_paths = (*private_paths, *public_paths) + present = tuple(path for path in material_paths if path.is_file()) + if len(present) == len(material_paths): + for path in private_paths: + path.chmod(0o600) + for path in public_paths: + path.chmod(0o644) + return + if present: + missing = ", ".join( + str(path.relative_to(EVIDENCE_LOCAL_DIR)) + for path in material_paths + if not path.is_file() + ) + raise SystemExit( + "incomplete local Evidence material: " + f"{missing}; run `just reset`, remove config/evidence/local/evidence, " + "config/evidence/local/mint, and config/evidence/local/tls, then rerun " + "`just gen-secrets`" + ) + + write_private(evidence_dir / "audit-hmac-key", raw_key()) + write_private(evidence_dir / "subject-binding-hmac-key", raw_key()) + write_private( + evidence_dir / "signing-ed25519-private-jwk", + local_ed25519_jwk("solmara-evidence-signing-key-1"), + ) + + mint_signing_jwk = local_ed25519_jwk("solmara-mint-signing-key-1") + client_jwk = local_ed25519_jwk("solmara-demo-client-key-1") + write_private(mint_dir / "signing.jwk", mint_signing_jwk) + write_private(mint_dir / "audit-hmac-key", raw_key()) + write_private(mint_dir / "client-private.jwk", client_jwk) + client = { + "clientId": "solmara-demo", + "principal": "https://id.registrystack.org/solmara/principal/demo-client", + "evidenceAudience": "https://id.registrystack.org/solmara/audience/demo-client", + "requesterTags": ["solmara-demo"], + "keys": [public_jwk(client_jwk)], + } + client_path = mint_dir / "clients" / "solmara-demo.yaml" + client_path.write_text(json.dumps(client, indent=2, sort_keys=True) + "\n") + client_path.chmod(0o644) + + ca_certificate = tls_dir / "ca.crt" + ca_private_key = tls_dir / "ca.key" + certificate = tls_dir / "gateway.crt" + private_key = tls_dir / "gateway.key" + certificate_request = tls_dir / "gateway.csr" + ca_serial = tls_dir / "ca.srl" + for path in ( + ca_certificate, + ca_private_key, + certificate, + private_key, + certificate_request, + ca_serial, + ): + path.unlink(missing_ok=True) + san_names = [ + "localhost", + "mint.evidence.solmara.invalid", + "evidence.solmara.invalid", + "cra-relay.evidence.solmara.invalid", + "nia-relay.evidence.solmara.invalid", + "sro-relay.evidence.solmara.invalid", + "programme-relay.evidence.solmara.invalid", + "sipf-relay.evidence.solmara.invalid", + "nagdi-relay.evidence.solmara.invalid", + ] + subprocess.run( + [ + "openssl", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-days", + "3650", + "-subj", + "/CN=Solmara Lab Evidence Development CA", + "-addext", + "basicConstraints=critical,CA:TRUE", + "-addext", + "keyUsage=critical,keyCertSign,cRLSign", + "-keyout", + str(ca_private_key), + "-out", + str(ca_certificate), + ], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + subprocess.run( + [ + "openssl", + "req", + "-new", + "-newkey", + "rsa:2048", + "-nodes", + "-subj", + "/CN=evidence.solmara.invalid", + "-addext", + "subjectAltName=" + ",".join(f"DNS:{name}" for name in san_names), + "-addext", + "basicConstraints=critical,CA:FALSE", + "-addext", + "keyUsage=critical,digitalSignature,keyEncipherment", + "-addext", + "extendedKeyUsage=serverAuth", + "-keyout", + str(private_key), + "-out", + str(certificate_request), + ], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + subprocess.run( + [ + "openssl", + "x509", + "-req", + "-in", + str(certificate_request), + "-CA", + str(ca_certificate), + "-CAkey", + str(ca_private_key), + "-CAcreateserial", + "-days", + "3650", + "-copy_extensions", + "copy", + "-out", + str(certificate), + ], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + certificate_request.unlink() + ca_serial.unlink(missing_ok=True) + ca_private_key.chmod(0o600) + private_key.chmod(0o600) + ca_certificate.chmod(0o644) + certificate.chmod(0o644) + + def local_rsa_private_key_b64() -> str: private_pem = subprocess.run( ["openssl", "genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048"], @@ -148,6 +306,7 @@ def main(argv: list[str] | None = None) -> int: parser.parse_args(argv) ensure_postgres_tls() + ensure_evidence_material() postgres_user = "solmara_registry" postgres_password = raw_key() postgres_db = "solmara_lab" @@ -156,55 +315,11 @@ def main(argv: list[str] | None = None) -> int: values: dict[str, str] = { "COMPOSE_PROJECT_NAME": compose_project_name(ROOT), "CRA_RELAY_AUDIT_HASH_SECRET": raw_key(), - "CRA_RELAY_AUDIT_PSEUDONYM_EPOCH_1": raw_key(), - "CRA_RELAY_POSTGRES_RUNTIME_PASSWORD": raw_key(), - "CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD": raw_key(), - "CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD": raw_key(), - "CRA_NOTARY_AUDIT_HASH_SECRET": raw_key(), - "CRA_NOTARY_POSTGRES_MIGRATOR_PASSWORD": raw_key(), - "CRA_NOTARY_POSTGRES_RUNTIME_PASSWORD": raw_key(), "NIA_RELAY_AUDIT_HASH_SECRET": raw_key(), - "NIA_RELAY_AUDIT_PSEUDONYM_EPOCH_1": raw_key(), - "NIA_RELAY_POSTGRES_RUNTIME_PASSWORD": raw_key(), - "NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD": raw_key(), - "NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD": raw_key(), - "NIA_NOTARY_AUDIT_HASH_SECRET": raw_key(), - "NIA_NOTARY_POSTGRES_MIGRATOR_PASSWORD": raw_key(), - "NIA_NOTARY_POSTGRES_RUNTIME_PASSWORD": raw_key(), "SRO_RELAY_AUDIT_HASH_SECRET": raw_key(), - "SRO_RELAY_AUDIT_PSEUDONYM_EPOCH_1": raw_key(), - "SRO_RELAY_POSTGRES_RUNTIME_PASSWORD": raw_key(), - "SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD": raw_key(), - "SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD": raw_key(), - "SRO_NOTARY_AUDIT_HASH_SECRET": raw_key(), - "SRO_NOTARY_POSTGRES_MIGRATOR_PASSWORD": raw_key(), - "SRO_NOTARY_POSTGRES_RUNTIME_PASSWORD": raw_key(), "PROGRAMME_RELAY_AUDIT_HASH_SECRET": raw_key(), - "PROGRAMME_RELAY_AUDIT_PSEUDONYM_EPOCH_1": raw_key(), - "PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD": raw_key(), - "PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD": raw_key(), - "PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD": raw_key(), - "PROGRAMME_NOTARY_AUDIT_HASH_SECRET": raw_key(), - "PROGRAMME_NOTARY_POSTGRES_MIGRATOR_PASSWORD": raw_key(), - "PROGRAMME_NOTARY_POSTGRES_RUNTIME_PASSWORD": raw_key(), "SIPF_RELAY_AUDIT_HASH_SECRET": raw_key(), - "SIPF_RELAY_AUDIT_PSEUDONYM_EPOCH_1": raw_key(), - "SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD": raw_key(), - "SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD": raw_key(), - "SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD": raw_key(), - "SIPF_NOTARY_AUDIT_HASH_SECRET": raw_key(), - "SIPF_NOTARY_POSTGRES_MIGRATOR_PASSWORD": raw_key(), - "SIPF_NOTARY_POSTGRES_RUNTIME_PASSWORD": raw_key(), "NAGDI_RELAY_AUDIT_HASH_SECRET": raw_key(), - "NAGDI_RELAY_AUDIT_PSEUDONYM_EPOCH_1": raw_key(), - "NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD": raw_key(), - "NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD": raw_key(), - "NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD": raw_key(), - "NAGDI_NOTARY_AUDIT_HASH_SECRET": raw_key(), - "NAGDI_NOTARY_POSTGRES_MIGRATOR_PASSWORD": raw_key(), - "NAGDI_NOTARY_POSTGRES_RUNTIME_PASSWORD": raw_key(), - "SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS": "4102444800000", - "SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS": "2592000000", "REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD": raw_key(), "REGISTRY_ESIGNET_KYC_TOKEN_SECRET": raw_key(), "REGISTRY_ESIGNET_PSUT_SECRET": raw_key(), @@ -232,20 +347,21 @@ def main(argv: list[str] | None = None) -> int: "SOLMARA_SIPF_DATABASE_URL": f"postgres://solmara_source_sipf_reader:{sipf_source_password}@postgres:5432/{postgres_db}?sslmode=require", "SOLMARA_ESIGNET_POSTGRES_PASSWORD": raw_key(), "CHILD_BENEFIT_FEDERATOR_TOKEN": raw_key(), - "CHILD_BENEFIT_FEDERATOR_URL": "http://127.0.0.1:4321", - "CRA_NOTARY_URL": "http://127.0.0.1:4325", - "NIA_NOTARY_URL": "http://127.0.0.1:4326", - "SRO_NOTARY_URL": "http://127.0.0.1:4327", - "PROGRAMME_NOTARY_URL": "http://127.0.0.1:4328", - "SIPF_NOTARY_URL": "http://127.0.0.1:4322", - "NAGDI_NOTARY_URL": "http://127.0.0.1:4323", + "CHILD_BENEFIT_FEDERATOR_URL": "https://localhost:4341/child-benefit/", + # Host-side scenario smokes use the gateway's localhost certificate SAN. + # Compose services override these with the internal gateway hostnames. + "SOLMARA_EVIDENCE_URL": "https://localhost:4341", + "SOLMARA_MINT_URL": "https://localhost:4341", + "SOLMARA_MINT_ASSERTION_AUDIENCE": "https://mint.evidence.solmara.invalid/token", + "SOLMARA_EVIDENCE_CLIENT_ID": "solmara-demo", + "SOLMARA_EVIDENCE_CLIENT_KEY": str( + ROOT / "config/evidence/local/mint/client-private.jwk" + ), + "SOLMARA_EVIDENCE_CA_BUNDLE": str( + ROOT / "config/evidence/local/tls/ca.crt" + ), } - for raw_name, hash_name in RAW_HASH_PAIRS: - raw = raw_key() - values[raw_name] = raw - values[hash_name] = fingerprint(raw) - for name, kid in JWK_KIDS.items(): values[name] = local_ed25519_jwk(kid) diff --git a/scripts/init-registry-postgresql.sh b/scripts/init-registry-postgresql.sh new file mode 100755 index 0000000..c8a0704 --- /dev/null +++ b/scripts/init-registry-postgresql.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env sh +set -eu + +# Provision least-privilege readers for the two PostgreSQL-backed Records API +# sources. Evidence never receives these credentials. Each Relay holds its own +# reader and releases only the reviewed field projection. +for fixture_script in \ + /docker-entrypoint-initdb.d/001-schema.sql \ + /docker-entrypoint-initdb.d/002-load.sql \ + /docker-entrypoint-initdb.d/003-schema.sql \ + /docker-entrypoint-initdb.d/004-load.sql; do + psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ + --file "$fixture_script" +done + +provision_source_reader() { + key=$1 + password=$2 + role="solmara_source_${key}_reader" + + psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ + --set=database="$POSTGRES_DB" \ + --set=role="$role" \ + --set=password="$password" <<'SQL' +SELECT format( + 'CREATE ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', + :'role', + :'password' +) +WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'role') +\gexec +SELECT format( + 'ALTER ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', + :'role', + :'password' +) +\gexec +SELECT format('REVOKE %I FROM %I', granted.rolname, member.rolname) +FROM pg_catalog.pg_auth_members AS membership +JOIN pg_catalog.pg_roles AS granted ON granted.oid = membership.roleid +JOIN pg_catalog.pg_roles AS member ON member.oid = membership.member +WHERE member.rolname = :'role' +\gexec +SELECT format('REVOKE ALL ON DATABASE %I FROM %I', :'database', :'role') +\gexec +SELECT format('GRANT CONNECT ON DATABASE %I TO %I', :'database', :'role') +\gexec +SQL + + psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ + --set=role="$role" <<'SQL' +REVOKE CREATE ON SCHEMA public FROM PUBLIC; +SELECT format('REVOKE ALL ON ALL TABLES IN SCHEMA public FROM %I', :'role') +\gexec +SELECT format('GRANT USAGE ON SCHEMA public TO %I', :'role') +\gexec +SQL + + case "$key" in + nia) + psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ + --set=role="$role" <<'SQL' +SELECT format('GRANT SELECT ON TABLE public.population_person TO %I', :'role') +\gexec +SQL + ;; + sipf) + psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ + --set=role="$role" <<'SQL' +SELECT format( + 'GRANT SELECT ON TABLE public.sipf_pension_payment, public.sipf_survivor_benefit TO %I', + :'role' +) +\gexec +SQL + ;; + *) + echo "Unsupported source reader" >&2 + exit 1 + ;; + esac +} + +for key in ${SOLMARA_SOURCE_READER_DATABASES:-}; do + case "$key" in + nia) + provision_source_reader "$key" \ + "${NIA_SOURCE_POSTGRES_READER_PASSWORD:?missing NIA source reader password}" + ;; + sipf) + provision_source_reader "$key" \ + "${SIPF_SOURCE_POSTGRES_READER_PASSWORD:?missing SIPF source reader password}" + ;; + *) + echo "Unsupported SOLMARA_SOURCE_READER_DATABASES entry" >&2 + exit 1 + ;; + esac +done diff --git a/scripts/postgres-entrypoint.sh b/scripts/postgres-entrypoint.sh index 7b55ea1..d29ccba 100755 --- a/scripts/postgres-entrypoint.sh +++ b/scripts/postgres-entrypoint.sh @@ -11,4 +11,30 @@ chown postgres:postgres "$ssl_dst/server.crt" "$ssl_dst/server.key" chmod 0644 "$ssl_dst/server.crt" chmod 0600 "$ssl_dst/server.key" -exec /usr/local/bin/docker-entrypoint.sh "$@" +ready_file=/tmp/solmara-postgres-provisioned +rm -f "$ready_file" + +/usr/local/bin/docker-entrypoint.sh "$@" & +postgres_pid=$! + +stop_postgres() { + kill -TERM "$postgres_pid" 2>/dev/null || true +} +trap stop_postgres INT TERM + +until pg_isready --quiet --host 127.0.0.1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB"; do + if ! kill -0 "$postgres_pid" 2>/dev/null; then + wait "$postgres_pid" + exit $? + fi + sleep 1 +done + +if ! /usr/local/bin/solmara-provision-postgresql.sh; then + stop_postgres + wait "$postgres_pid" || true + exit 1 +fi + +touch "$ready_file" +wait "$postgres_pid" diff --git a/scripts/registry-projects.sh b/scripts/registry-projects.sh index b92ed15..a545da0 100755 --- a/scripts/registry-projects.sh +++ b/scripts/registry-projects.sh @@ -20,14 +20,14 @@ if [ "$actual_version" != "registryctl $required_version" ]; then echo "set REGISTRYCTL_BIN to the matching release binary when it is not on PATH" >&2 exit 1 fi -for command in check test build capabilities; do +for command in check test build; do if ! "$REGISTRYCTL" "$command" --help >/dev/null 2>&1; then echo "registryctl $required_version with project-authoring check/test/build/capabilities is required" >&2 echo "set REGISTRYCTL_BIN to a compatible Registry Stack build" >&2 exit 1 fi done -if ! "$REGISTRYCTL" authoring editor --help >/dev/null 2>&1; then +if ! "$REGISTRYCTL" tooling editor --help >/dev/null 2>&1; then echo "registryctl $required_version with project-authoring editor setup is required" >&2 echo "set REGISTRYCTL_BIN to a compatible Registry Stack build" >&2 exit 1 @@ -89,9 +89,10 @@ inspect_capabilities() { for project in $projects; do for environment in local hosted; do echo "registryctl capabilities: $project ($environment)" - "$REGISTRYCTL" capabilities \ + "$REGISTRYCTL" check \ --project-dir "$ROOT/projects/$project" \ - --environment "$environment" + --environment "$environment" \ + --explain done done } @@ -99,7 +100,7 @@ inspect_capabilities() { sync_editor_support() { for project in $projects; do echo "registryctl authoring editor: $project" - "$REGISTRYCTL" authoring editor \ + "$REGISTRYCTL" tooling editor \ --project-dir "$ROOT/projects/$project" done } @@ -111,9 +112,9 @@ stage_runtime() { build_root=$(build_project_output "$project" "$environment") source="$build_root/private" target="$destination/$environment/$project" - mkdir -p "$target/relay" "$target/notary" - cp -R "$source/relay/config/." "$target/relay/" - cp "$source/notary/config/notary.yaml" "$target/notary/notary.yaml" + mkdir -p "$target/relay" "$target/relay-consultation" + cp -R "$source/relay-public/config/." "$target/relay/" + cp -R "$source/relay-consultation/config/." "$target/relay-consultation/" done done chmod -R u=rwX,go=rX "$destination" @@ -123,11 +124,14 @@ action=${1:-} case "$action" in test) for project in $projects; do - echo "registryctl test: $project" - "$REGISTRYCTL" test \ - --project-dir "$ROOT/projects/$project" \ - --format json | - python3 "$ROOT/scripts/registryctl-test-output.py" + for environment in local hosted; do + echo "registryctl test: $project ($environment)" + "$REGISTRYCTL" test \ + --project-dir "$ROOT/projects/$project" \ + --environment "$environment" \ + --format json | + python3 "$ROOT/scripts/registryctl-test-output.py" + done done ;; check) diff --git a/scripts/registryctl-build-output.py b/scripts/registryctl-build-output.py index 5c805ed..02da531 100755 --- a/scripts/registryctl-build-output.py +++ b/scripts/registryctl-build-output.py @@ -10,7 +10,8 @@ from typing import Any -REPORT_SCHEMA = "registryctl.project_command.v1" +REPORT_SCHEMA = "registryctl.reviewed_project_build_report.v1" +BUILD_SCHEMA = "registryctl.project_command.v1" MAX_REPORT_BYTES = 8 * 1024 * 1024 @@ -45,14 +46,17 @@ def parse_build_output( raise BuildReportError("the JSON report is not an object") if report.get("schema_version") != REPORT_SCHEMA: raise BuildReportError("the JSON report has an unsupported schema") - if report.get("status") != "built": + build = report.get("build") + if not isinstance(build, dict) or build.get("schema_version") != BUILD_SCHEMA: + raise BuildReportError("the JSON report has no supported build result") + if build.get("status") != "built": raise BuildReportError("registryctl did not report a completed build") - if report.get("environment") != environment: + if build.get("environment") != environment: raise BuildReportError("the JSON report has the wrong environment binding") - if not isinstance(report.get("project"), str) or not report["project"]: + if not isinstance(build.get("project"), str) or not build["project"]: raise BuildReportError("the JSON report has no project identity") - output_value = report.get("output") + output_value = build.get("output") if not isinstance(output_value, str) or not output_value: raise BuildReportError("the JSON report has no output root") output = Path(output_value) @@ -69,13 +73,15 @@ def parse_build_output( if not output_root.is_dir(): raise BuildReportError("the JSON report output root is not a directory") - relay_config = output_root / "private" / "relay" / "config" - notary_config = output_root / "private" / "notary" / "config" / "notary.yaml" - if not relay_config.is_dir() or not notary_config.is_file(): + relay_public = output_root / "private" / "relay-public" / "config" / "relay.yaml" + relay_consultation = ( + output_root / "private" / "relay-consultation" / "config" / "relay.yaml" + ) + if not relay_public.is_file() or not relay_consultation.is_file(): raise BuildReportError( - "the generated Relay or Notary configuration closure is incomplete" + "the generated Relay configuration closure is incomplete" ) - for required in (relay_config, notary_config): + for required in (relay_public, relay_consultation): try: required.resolve(strict=True).relative_to(output_root) except (FileNotFoundError, RuntimeError, ValueError) as error: diff --git a/scripts/registryctl-pinned.sh b/scripts/registryctl-pinned.sh index c813a48..39c661a 100755 --- a/scripts/registryctl-pinned.sh +++ b/scripts/registryctl-pinned.sh @@ -24,49 +24,24 @@ if [ -n "${REGISTRYCTL_BIN:-}" ]; then elif command -v registryctl >/dev/null 2>&1 && verify_version "$(command -v registryctl)"; then registryctl=$(command -v registryctl) else - case "$(uname -s):$(uname -m)" in - Linux:x86_64) platform=linux-amd64 ;; - Linux:aarch64 | Linux:arm64) platform=linux-arm64 ;; - Darwin:arm64) platform=macos-arm64 ;; - *) - echo "registryctl $version has no published binary for $(uname -s) $(uname -m)" >&2 - echo "set REGISTRYCTL_BIN to a verified compatible binary" >&2 - exit 1 - ;; - esac - - asset="registryctl-v${version}-${platform}" - tools="$ROOT/output/tools" - registryctl="$tools/$asset" + source_commit=$(sed -n 's/^REGISTRY_STACK_SOURCE_COMMIT=//p' "$VERSION_FILE") + source_dir=${REGISTRY_STACK_SOURCE_DIR:-"$ROOT/../registry-stack"} + if [ ! -d "$source_dir/.git" ]; then + echo "REGISTRY_STACK_SOURCE_DIR must name a Registry Stack checkout" >&2 + exit 1 + fi + actual_commit=$(git -C "$source_dir" rev-parse HEAD) + if [ "$actual_commit" != "$source_commit" ]; then + echo "Registry Stack source mismatch: expected $source_commit, found $actual_commit" >&2 + exit 1 + fi + registryctl="$source_dir/target/release/registryctl" + if ! verify_version "$registryctl"; then + cargo build --locked --release --manifest-path "$source_dir/Cargo.toml" -p registryctl + fi if ! verify_version "$registryctl"; then - mkdir -p "$tools" - temporary=$(mktemp -d "${TMPDIR:-/tmp}/solmara-registryctl.XXXXXX") - trap 'rm -rf "$temporary"' EXIT HUP INT TERM - base="https://github.com/registrystack/registry-stack/releases/download/v${version}" - curl --proto '=https' --tlsv1.2 --fail --location --silent --show-error \ - "$base/SHA256SUMS" -o "$temporary/SHA256SUMS" - curl --proto '=https' --tlsv1.2 --fail --location --silent --show-error \ - "$base/$asset" -o "$temporary/$asset" - expected=$(awk -v asset="$asset" '$2 == asset { print $1 }' "$temporary/SHA256SUMS") - if [ -z "$expected" ]; then - echo "SHA256SUMS does not cover $asset" >&2 - exit 1 - fi - if command -v sha256sum >/dev/null 2>&1; then - actual=$(sha256sum "$temporary/$asset" | awk '{print $1}') - else - actual=$(shasum -a 256 "$temporary/$asset" | awk '{print $1}') - fi - if [ "$actual" != "$expected" ]; then - echo "downloaded $asset failed its published SHA-256 check" >&2 - exit 1 - fi - chmod 0755 "$temporary/$asset" - mv "$temporary/$asset" "$registryctl" - if ! verify_version "$registryctl"; then - echo "downloaded $asset did not report registryctl $version" >&2 - exit 1 - fi + echo "the source-built registryctl did not report registryctl $version" >&2 + exit 1 fi fi diff --git a/scripts/registryctl-test-output.py b/scripts/registryctl-test-output.py index 4e15ddf..3f6db2f 100755 --- a/scripts/registryctl-test-output.py +++ b/scripts/registryctl-test-output.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Validate a registryctl test report and require governed request witnesses.""" +"""Validate a current-main registryctl fixture and coverage report.""" from __future__ import annotations @@ -9,7 +9,6 @@ REPORT_SCHEMA = "registryctl.project_command.v1" -REQUEST_BINDING_REQUIREMENT = "request_to_consultation_binding" MAX_REPORT_BYTES = 16 * 1024 * 1024 @@ -61,32 +60,31 @@ def validate_test_report(raw: bytes) -> tuple[str, int, int]: if not isinstance(targets, list) or not targets: raise TestReportError("the JSON report has no fixture coverage targets") for target in targets: - requirements = target.get("requirements") if isinstance(target, dict) else None - if not isinstance(requirements, list): - raise TestReportError("a fixture coverage target has no requirements") - bindings = [ - requirement - for requirement in requirements - if isinstance(requirement, dict) - and requirement.get("requirement") == REQUEST_BINDING_REQUIREMENT - ] - if len(bindings) != 1 or bindings[0].get("state") != "covered": - raise TestReportError( - "every fixture target must cover request-to-consultation binding" - ) - evidence = bindings[0].get("evidence") + if not isinstance(target, dict): + raise TestReportError("a fixture coverage target is not an object") + identity = target.get("identity") + compiled = target.get("compiled_contract") + inventory = target.get("fixture_inventory") if ( - not isinstance(evidence, list) - or not evidence + target.get("fixture_set_state") != "fixture_bearing" + or not isinstance(identity, dict) + or not isinstance(identity.get("integration"), str) + or not isinstance(identity.get("capability"), str) + or not isinstance(compiled, dict) + or compiled.get("kind") != "compiled_contract" + or not isinstance(compiled.get("digest"), str) + or not compiled["digest"].startswith("sha256:") + or not isinstance(inventory, list) + or not inventory or any( not isinstance(item, dict) - or item.get("kind") != "authored_fixture" - for item in evidence + or item.get("pass_state") != "passed" + or not isinstance(item.get("fixture_digest"), str) + or not item["fixture_digest"].startswith("sha256:") + for item in inventory ) ): - raise TestReportError( - "request-to-consultation binding requires authored fixture evidence" - ) + raise TestReportError("every coverage target must bind passing fixtures to a compiled contract") return project, len(fixtures), len(targets) @@ -99,7 +97,7 @@ def main() -> int: return 1 print( f"PASS: {project}: {fixture_count}/{fixture_count} fixtures passed; " - f"request witnesses cover {target_count}/{target_count} targets" + f"compiled fixture coverage includes {target_count}/{target_count} targets" ) return 0 diff --git a/scripts/relay_workload_identity_agent.py b/scripts/relay_workload_identity_agent.py index a6b7174..a87c5d6 100755 --- a/scripts/relay_workload_identity_agent.py +++ b/scripts/relay_workload_identity_agent.py @@ -540,7 +540,6 @@ def _atomic_write_token(path: Path, token: str, *, uid: int, gid: int) -> None: with os.fdopen(descriptor, "wb", closefd=True) as output: descriptor = -1 output.write(token.encode("ascii")) - output.write(b"\n") output.flush() os.fsync(output.fileno()) os.replace(temporary_path, path) @@ -592,7 +591,7 @@ def rotate(self) -> None: ) self._expires_at[index] = expires_at self._published_hashes[index] = hashlib.sha256( - token.encode("ascii") + b"\n" + token.encode("ascii") ).digest() def rotate_if_due(self) -> bool: @@ -614,7 +613,7 @@ def rotate_if_due(self) -> bool: ) self._expires_at[index] = expires_at self._published_hashes[index] = hashlib.sha256( - token.encode("ascii") + b"\n" + token.encode("ascii") ).digest() rotated = True return rotated diff --git a/scripts/review.sh b/scripts/review.sh index e666dcd..d74d646 100755 --- a/scripts/review.sh +++ b/scripts/review.sh @@ -11,6 +11,9 @@ fi "$root/scripts/check-fiction.sh" "$root/scripts/check-image-pins.py" "$root/scripts/check-config-secrets.py" +"$root/scripts/registry-projects.sh" check +"$root/scripts/registry-projects.sh" check-runtime +"$root/scripts/check-evidence-runtime.py" uv run --project "$root" "$root/scripts/publish-metadata.py" --check uv run --project "$root" "$root/scripts/metadata-lint.py" @@ -24,11 +27,10 @@ fi cat <<'CHECKLIST' Security checklist for reviewer: 1. Purpose-gated Relay entities require Data-Purpose where sensitive data is exposed. -2. Raw tokens appear only in generated .env, never committed configs. -3. Notary source connections use token_env and scoped Relay tokens. -4. Redactable fields are not disclosed through predicate channels. -5. Run `just smoke` against the live stack: its child-benefit application gate verifies authority-owned - predicate success, unsupported-purpose denial, non-composition, and rejection of raw-source results. -6. Audit hash secrets are environment-backed. -7. Fiction lint is green. +2. Evidence source credentials are short-lived Relay workload tokens held in a private runtime volume. +3. Mint authenticates the application with private_key_jwt and issues only the configured Evidence audience. +4. Evidence requirements disclose reviewed concept values as flattened signed JWS, never source rows. +5. Run `just smoke` against the live stack to exercise Mint, Evidence, Relay sources, scenarios, and the portal. +6. Audit and subject-binding secrets are generated locally and remain uncommitted. +7. The exact Registry Stack main commit in versions.env owns all three locally built runtime images. CHECKLIST diff --git a/scripts/smoke-live.py b/scripts/smoke-live.py index bfe01f4..5926cad 100755 --- a/scripts/smoke-live.py +++ b/scripts/smoke-live.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Smoke the running Solmara Lab Notary topology.""" +"""Smoke the running local Registry Evidence and Mint topology.""" from __future__ import annotations @@ -16,17 +16,7 @@ if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from scenarios.common import PURPOSES, auth_headers, http_json, joined_url # noqa: E402 - - -@dataclass(frozen=True) -class NotaryService: - name: str - url_env: str - default_url: str - token_env: str - purpose: str - claim_ids: tuple[str, ...] +from scenarios.common import http_json, joined_url # noqa: E402 @dataclass(frozen=True) @@ -38,64 +28,6 @@ class ScenarioCase: expected_values: dict[str, bool] -SERVICES = ( - NotaryService( - "CRA Notary", - "CRA_NOTARY_URL", - "http://127.0.0.1:4325", - "CRA_CHILD_BENEFIT_CLIENT_TOKEN", - PURPOSES["child_benefit"], - ("birth-is-registered", "child-age-under-5"), - ), - NotaryService( - "NIA Notary", - "NIA_NOTARY_URL", - "http://127.0.0.1:4326", - "NIA_CHILD_BENEFIT_CLIENT_TOKEN", - PURPOSES["child_benefit"], - ("population-record-active",), - ), - NotaryService( - "SRO Notary", - "SRO_NOTARY_URL", - "http://127.0.0.1:4327", - "SRO_CHILD_BENEFIT_CLIENT_TOKEN", - PURPOSES["child_benefit"], - ("household-below-poverty-threshold",), - ), - NotaryService( - "Programme Notary", - "PROGRAMME_NOTARY_URL", - "http://127.0.0.1:4328", - "PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN", - PURPOSES["child_benefit"], - ("not-already-enrolled",), - ), - NotaryService( - "SIPF Notary", - "SIPF_NOTARY_URL", - "http://127.0.0.1:4322", - "SIPF_PENSION_CLIENT_TOKEN", - PURPOSES["pension_payment"], - ("pension-payment-active", "survivor-is-eligible"), - ), - NotaryService( - "NAgDI Notary", - "NAGDI_NOTARY_URL", - "http://127.0.0.1:4323", - "NAGDI_NOTARY_TOKEN", - PURPOSES["voucher"], - ( - "farmer-registered", - "data-use-authorized-for-purpose", - "eligible-for-climate-smart-input-voucher", - "registered-herd", - "origin-district-not-quarantined-for-species", - "eligible-for-livestock-movement-permit", - ), - ), -) - SCENARIO_CASES = ( ScenarioCase( "child eligible", @@ -110,29 +42,11 @@ class ScenarioCase: "not-already-enrolled": True, }, ), - ScenarioCase( - "child deceased control", - "child_benefit", - "deceased-control", - 200, - {"child-age-under-5": False}, - ), - ScenarioCase( - "child poverty control", - "child_benefit", - "poverty-control", - 200, - {"household-below-poverty-threshold": False}, - ), + ScenarioCase("child deceased control", "child_benefit", "deceased-control", 200, {"child-age-under-5": False}), + ScenarioCase("child poverty control", "child_benefit", "poverty-control", 200, {"household-below-poverty-threshold": False}), ScenarioCase("child unregistered control", "child_benefit", "unregistered-control", 200, {"birth-is-registered": False}), - ScenarioCase( - "child duplicate enrollment control", - "child_benefit", - "duplicate-control", - 200, - {"not-already-enrolled": False}, - ), - ScenarioCase("child unsupported purpose denial", "child_benefit", "purpose-denial", range(400, 500), {}), + ScenarioCase("child duplicate control", "child_benefit", "duplicate-control", 200, {"not-already-enrolled": False}), + ScenarioCase("child purpose denial", "child_benefit", "purpose-denial", range(400, 500), {}), ScenarioCase( "pension stop payment", "pension_survivor", @@ -140,78 +54,29 @@ class ScenarioCase: 200, {"person-is-deceased": True, "pension-payment-active": True}, ), - ScenarioCase( - "pension stale death control", - "pension_survivor", - "stale-control", - 200, - {"person-is-deceased": False}, - ), - ScenarioCase( - "pension survivor benefit", - "pension_survivor", - "survivor-benefit", - 200, - {"survivor-is-eligible": True}, - ), - ScenarioCase( - "pension dissolved marriage control", - "pension_survivor", - "dissolved-control", - 200, - {"survivor-is-eligible": False}, - ), + ScenarioCase("pension stale death control", "pension_survivor", "stale-control", 200, {"person-is-deceased": False}), + ScenarioCase("pension survivor benefit", "pension_survivor", "survivor-benefit", 200, {"survivor-is-eligible": True}), + ScenarioCase("pension dissolved marriage control", "pension_survivor", "dissolved-control", 200, {"survivor-is-eligible": False}), ScenarioCase("pension over-disclosure denial", "pension_survivor", "cause-of-death-denial", range(400, 500), {}), - ScenarioCase( - "farmer voucher eligible", - "farmer_voucher", - "positive", - 200, - {"eligible-for-climate-smart-input-voucher": True}, - ), - ScenarioCase( - "farmer missing authorization control", - "farmer_voucher", - "authorization-control", - 200, - {"eligible-for-climate-smart-input-voucher": False}, - ), - ScenarioCase( - "farmer redeemed control", - "farmer_voucher", - "redeemed-control", - 200, - {"eligible-for-climate-smart-input-voucher": False}, - ), - ScenarioCase( - "livestock movement permit eligible", - "farmer_voucher", - "movement-permit", - 200, - {"eligible-for-livestock-movement-permit": True}, - ), + ScenarioCase("farmer voucher eligible", "farmer_voucher", "positive", 200, {"eligible-for-climate-smart-input-voucher": True}), + ScenarioCase("farmer authorization control", "farmer_voucher", "authorization-control", 200, {"eligible-for-climate-smart-input-voucher": False}), + ScenarioCase("farmer redeemed control", "farmer_voucher", "redeemed-control", 200, {"eligible-for-climate-smart-input-voucher": False}), + ScenarioCase("livestock movement eligible", "farmer_voucher", "movement-permit", 200, {"eligible-for-livestock-movement-permit": True}), ScenarioCase("livestock purpose denial", "farmer_voucher", "purpose-denial", range(400, 500), {}), ScenarioCase( - "citizen self-service summary", + "citizen self-service", "citizen", "positive", 200, - { - "citizen-population-record-active": True, - "civil-record-linked": True, - }, + {"citizen-population-record-active": True, "civil-record-linked": True}, ), - ScenarioCase("citizen unsupported purpose denial", "citizen", "purpose-denial", range(400, 500), {}), + ScenarioCase("citizen purpose denial", "citizen", "purpose-denial", range(400, 500), {}), ) def main() -> int: load_dotenv(ROOT / ".env") - failures: list[str] = [] - - for service in SERVICES: - failures.extend(check_service(service)) - + failures = check_runtime() for case in SCENARIO_CASES: failures.extend(check_case(case)) @@ -220,7 +85,7 @@ def main() -> int: print(f"smoke-live: {failure}", file=sys.stderr) return 1 - print(f"smoke-live: {len(SERVICES)} services and {len(SCENARIO_CASES)} scenario checks passed") + print(f"smoke-live: Mint, Evidence, and {len(SCENARIO_CASES)} scenario checks passed") return 0 @@ -235,38 +100,32 @@ def load_dotenv(path: Path) -> None: key = key.strip() if not key or key in os.environ: continue - if raw_value == "": - os.environ[key] = "" - continue parts = shlex.split(raw_value, posix=True) os.environ[key] = parts[0] if parts else "" -def check_service(service: NotaryService) -> list[str]: +def check_runtime() -> list[str]: + evidence_url = os.environ.get("SOLMARA_EVIDENCE_URL", "https://localhost:4341") failures: list[str] = [] - base_url = os.environ.get(service.url_env, service.default_url) - if not os.environ.get(service.token_env): - return [f"{service.name}: missing {service.token_env}; run `just generate` before live smoke"] - - readiness = wait_for_readiness(base_url, service.name) + readiness = wait_for_readiness(evidence_url) if readiness is not None: failures.append(readiness) - headers = auth_headers(os.environ[service.token_env], service.purpose, "application/json") - result = http_json("GET", joined_url(base_url, "/v1/claims"), headers, timeout=5.0) - if result.status != 200: - failures.append(f"{service.name}: GET /v1/claims returned {result.status}; body={compact_body(result.body)}") - return failures - - ids = catalog_claim_ids(result.body) - missing = sorted(set(service.claim_ids) - ids) - if missing: - failures.append(f"{service.name}: missing claims from catalogue: {', '.join(missing)}") + metadata = http_json( + "GET", + joined_url(evidence_url, "/.well-known/oauth-authorization-server"), + {"Accept": "application/json"}, + timeout=5.0, + ) + if metadata.status != 200: + failures.append(f"Mint metadata returned {metadata.status or metadata.error}; body={compact_body(metadata.body)}") + elif not isinstance(metadata.body, dict) or metadata.body.get("issuer") != "https://mint.evidence.solmara.invalid": + failures.append(f"Mint metadata exposed an unexpected issuer; body={compact_body(metadata.body)}") return failures -def wait_for_readiness(base_url: str, name: str) -> str | None: - url = joined_url(base_url, "/ready") +def wait_for_readiness(evidence_url: str) -> str | None: + url = joined_url(evidence_url, "/ready") deadline = time.monotonic() + float(os.environ.get("SOLMARA_SMOKE_READY_TIMEOUT_SECONDS", "90")) last_status: int | None = None last_error = "" @@ -278,7 +137,7 @@ def wait_for_readiness(base_url: str, name: str) -> str | None: return None time.sleep(1) detail = f"status {last_status}" if last_status is not None else last_error or "no response" - return f"{name}: /ready did not become ready at {url} ({detail})" + return f"Evidence /ready did not become ready at {url} ({detail})" def check_case(case: ScenarioCase) -> list[str]: @@ -287,26 +146,26 @@ def check_case(case: ScenarioCase) -> list[str]: response = result.get("response_source", {}) status = response.get("status") if not status_matches(status, case.expected_status): - return [f"{case.name}: expected HTTP {format_expected(case.expected_status)}, got {status}; body={compact_body(response.get('body'))}"] - + return [ + f"{case.name}: expected HTTP {format_expected(case.expected_status)}, got {status}; " + f"body={compact_body(response.get('body'))}" + ] if not case.expected_values: return [] values = claim_values(response.get("body", {})) - failures = [] - for claim_id, expected in case.expected_values.items(): - actual = values.get(claim_id) - if actual is not expected: - failures.append(f"{case.name}: expected {claim_id}={expected}, got {actual}; body={compact_body(response.get('body'))}") - return failures + return [ + f"{case.name}: expected {claim_id}={expected}, got {values.get(claim_id)}; " + f"body={compact_body(response.get('body'))}" + for claim_id, expected in case.expected_values.items() + if values.get(claim_id) is not expected + ] def status_matches(status: Any, expected: int | tuple[int, ...] | range) -> bool: if not isinstance(status, int): return False - if isinstance(expected, int): - return status == expected - return status in expected + return status == expected if isinstance(expected, int) else status in expected def format_expected(expected: int | tuple[int, ...] | range) -> str: @@ -317,31 +176,17 @@ def format_expected(expected: int | tuple[int, ...] | range) -> str: return str(expected) -def catalog_claim_ids(body: Any) -> set[str]: - if not isinstance(body, dict): - return set() - claims = body.get("data", []) - if not isinstance(claims, list): - return set() - return {claim.get("id") for claim in claims if isinstance(claim, dict) and isinstance(claim.get("id"), str)} - - def claim_values(body: Any) -> dict[str, bool | None]: if not isinstance(body, dict): return {} results = body.get("results", []) if not isinstance(results, list): return {} - values: dict[str, bool | None] = {} - for item in results: - if not isinstance(item, dict): - continue - claim_id = item.get("claim_id") - if not isinstance(claim_id, str): - continue - value = item.get("value") - values[claim_id] = value if isinstance(value, bool) else item.get("satisfied") - return values + return { + item["claim_id"]: item.get("value") if isinstance(item.get("value"), bool) else item.get("satisfied") + for item in results + if isinstance(item, dict) and isinstance(item.get("claim_id"), str) + } def compact_body(body: Any) -> str: diff --git a/scripts/smoke-relay-sources.py b/scripts/smoke-relay-sources.py index 114408c..b3e7fe8 100755 --- a/scripts/smoke-relay-sources.py +++ b/scripts/smoke-relay-sources.py @@ -1,12 +1,10 @@ #!/usr/bin/env python3 -"""Probe public Relay readiness and its unauthenticated request boundary. - -Authority Notaries reach their paired Relays with short-lived workload -identity tokens through the separate private consultation Relay processes. -This public-endpoint smoke deliberately has no token: it proves that all six -public Relays are ready and refuse unauthenticated requests with the stable -missing-credential problem. Notary live smokes separately exercise the private -consultation path. +"""Probe Relay readiness and the unauthenticated Records API boundary. + +Registry Evidence reaches these APIs with short-lived workload credentials. +This host-side probe deliberately has no token: it proves that all six source +Relays are ready and refuse unauthenticated record reads with the stable +missing-credential problem. """ from __future__ import annotations @@ -33,7 +31,8 @@ class RelayBoundary: name: str base_url_env: str default_base_url: str - consultation_profile: str + dataset: str + entity: str RELAYS = ( @@ -41,37 +40,43 @@ class RelayBoundary: "CRA Relay", "SOLMARA_CRA_RELAY_URL", "http://127.0.0.1:4311", - "solmara-cra-civil.cra-child-benefit.civil", + "civil_people", + "civil_people", ), RelayBoundary( "NIA Relay", "SOLMARA_NIA_RELAY_URL", "http://127.0.0.1:4312", - "solmara-nia-population.nia-child-benefit.population", + "population", + "population", ), RelayBoundary( "SRO Relay", "SOLMARA_SRO_RELAY_URL", "http://127.0.0.1:4313", - "solmara-sro-social.child-benefit.household", + "sro_child", + "sro_child", ), RelayBoundary( "Programme Relay", "SOLMARA_PROGRAMME_RELAY_URL", "http://127.0.0.1:4314", - "solmara-mosd-programme.child-benefit.enrollment", + "mosd_enroll", + "mosd_enroll", ), RelayBoundary( "SIPF Relay", "SOLMARA_SIPF_RELAY_URL", "http://127.0.0.1:4315", - "solmara-sipf-pensions.sipf-pension-payment-review.pension", + "pension", + "pension", ), RelayBoundary( "NAgDI Relay", "SOLMARA_NAGDI_RELAY_URL", "http://127.0.0.1:4316", - "solmara-nagdi-agriculture.voucher.farmer", + "nagdi_farmer", + "nagdi_farmer", ), ) @@ -148,13 +153,16 @@ def run_probe( "detail": status_detail(ready), } - profile = urllib.parse.quote(relay.consultation_profile, safe=".-_") - denial = http_get(joined_url(base_url, f"/v1/consultations/{profile}")) + dataset = urllib.parse.quote(relay.dataset, safe=".-_") + entity = urllib.parse.quote(relay.entity, safe=".-_") + denial = http_get( + joined_url(base_url, f"/v1/datasets/{dataset}/entities/{entity}/records") + ) denial_failure = validate_unauthenticated_denial(denial) if denial_failure: return { "name": relay.name, - "status": "consultation_boundary_failed", + "status": "records_boundary_failed", "detail": denial_failure, } @@ -163,8 +171,8 @@ def run_probe( "status": "ok", "liveness_status": health[0], "readiness_status": ready[0], - "unauthenticated_consultation_status": denial[0], - "unauthenticated_consultation_code": "auth.missing_credential", + "unauthenticated_records_status": denial[0], + "unauthenticated_records_code": "auth.missing_credential", } diff --git a/scripts/smoke-story-previews.py b/scripts/smoke-story-previews.py index bb7d230..118375f 100755 --- a/scripts/smoke-story-previews.py +++ b/scripts/smoke-story-previews.py @@ -23,21 +23,21 @@ def validate_request(module_name: str, step_id: str, request: dict[str, Any]) -> url = request.get("url") headers = request.get("headers") if method == "MULTI": - if url != "solmara://authority-notaries": + if url != "solmara://registry-evidence": raise ValueError( - f"{module_name}:{step_id}: expected authority collection URL, got {url!r}" + f"{module_name}:{step_id}: expected Evidence collection URL, got {url!r}" ) - if not isinstance(headers, dict) or "Data-Purpose" not in headers: - raise ValueError(f"{module_name}:{step_id}: missing Data-Purpose") + if not isinstance(request.get("purpose"), str): + raise ValueError(f"{module_name}:{step_id}: missing Evidence purpose") requests = request.get("requests") if not isinstance(requests, list) or not requests: raise ValueError( - f"{module_name}:{step_id}: authority collection has no requests" + f"{module_name}:{step_id}: Evidence collection has no requests" ) for index, authority_request in enumerate(requests): if not isinstance(authority_request, dict): raise ValueError( - f"{module_name}:{step_id}: authority request {index} is invalid" + f"{module_name}:{step_id}: Evidence request {index} is invalid" ) validate_request( module_name, @@ -47,10 +47,16 @@ def validate_request(module_name: str, step_id: str, request: dict[str, Any]) -> return if method not in {"GET", "POST"}: raise ValueError(f"{module_name}:{step_id}: invalid method {method!r}") - if not isinstance(url, str) or not url.startswith("http://127.0.0.1:"): - raise ValueError(f"{module_name}:{step_id}: expected local URL, got {url!r}") - if not isinstance(headers, dict) or "Data-Purpose" not in headers: - raise ValueError(f"{module_name}:{step_id}: missing Data-Purpose") + if not isinstance(url, str) or not ( + url.startswith("http://127.0.0.1:") + or url.startswith("https://localhost:") + or url.startswith("https://evidence.solmara.invalid/") + ): + raise ValueError(f"{module_name}:{step_id}: expected local Evidence URL, got {url!r}") + if not isinstance(headers, dict): + raise ValueError(f"{module_name}:{step_id}: missing headers") + if "Data-Purpose" not in headers and "Authorization" not in headers: + raise ValueError(f"{module_name}:{step_id}: missing application or Evidence authorization") for header in ("Authorization", "x-api-key"): if header in headers and "runtime token hidden" not in headers[header] and "runtime token missing" not in headers[header]: raise ValueError(f"{module_name}:{step_id}: {header} header was not redacted") diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 79a5f69..cee6868 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -24,11 +24,8 @@ if [ "${SOLMARA_SMOKE_LIVE:-1}" != "0" ]; then NIA_ESIGNET_RELAY_TOKEN="$nia_esignet_relay_token" \ "$root/scripts/smoke-nia-attribute-release.py" unset nia_esignet_relay_token - # The signing smokes need cryptography from the locked project environment. + # The Mint private_key_jwt smoke needs cryptography from the locked environment. uv run --locked --project "$root" "$root/scripts/smoke-live.py" - uv run --locked --project "$root" "$root/scripts/notary_state_restart.py" - uv run --locked --project "$root" "$root/scripts/smoke-child-benefit-application.py" - "$root/scripts/smoke-published-tokens.py" "$root/scripts/smoke-portal-compose.py" fi diff --git a/scripts/test_gen_secrets.py b/scripts/test_gen_secrets.py new file mode 100644 index 0000000..34fc868 --- /dev/null +++ b/scripts/test_gen_secrets.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import hashlib +import importlib.util +import sys +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_generator(): + compose_spec = importlib.util.spec_from_file_location( + "compose_project_name", ROOT / "scripts/compose_project_name.py" + ) + if compose_spec is None or compose_spec.loader is None: + raise RuntimeError("could not load compose_project_name.py") + compose_module = importlib.util.module_from_spec(compose_spec) + sys.modules["compose_project_name"] = compose_module + compose_spec.loader.exec_module(compose_module) + + spec = importlib.util.spec_from_file_location( + "solmara_gen_secrets", ROOT / "scripts/gen-secrets.py" + ) + if spec is None or spec.loader is None: + raise RuntimeError("could not load gen-secrets.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class EvidenceMaterialTest(unittest.TestCase): + def setUp(self) -> None: + self.generator = load_generator() + + def test_complete_material_is_preserved_across_regeneration(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.generator.EVIDENCE_LOCAL_DIR = root + self.generator.ensure_evidence_material() + first = { + path.relative_to(root): hashlib.sha256(path.read_bytes()).digest() + for path in root.rglob("*") + if path.is_file() + } + + self.generator.ensure_evidence_material() + second = { + path.relative_to(root): hashlib.sha256(path.read_bytes()).digest() + for path in root.rglob("*") + if path.is_file() + } + + self.assertEqual(first, second) + self.assertEqual(len(first), 11) + + def test_partial_material_fails_without_rotating_existing_key(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + existing = root / "evidence/audit-hmac-key" + existing.parent.mkdir(parents=True) + existing.write_text("existing-audit-key\n") + self.generator.EVIDENCE_LOCAL_DIR = root + + with self.assertRaisesRegex(SystemExit, "incomplete local Evidence material"): + self.generator.ensure_evidence_material() + + self.assertEqual(existing.read_text(), "existing-audit-key\n") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_image_pins.py b/scripts/test_image_pins.py index 4601752..0b4b94e 100644 --- a/scripts/test_image_pins.py +++ b/scripts/test_image_pins.py @@ -10,9 +10,11 @@ ROOT = Path(__file__).resolve().parents[1] -RELAY = "ghcr.io/registrystack/registry-relay@sha256:" + "1" * 64 -NOTARY = "ghcr.io/registrystack/registry-notary@sha256:" + "2" * 64 +RELAY = "solmara-lab-registry-relay:source" +EVIDENCE = "solmara-lab-registry-evidence:source" +MINT = "solmara-lab-registry-mint:source" VOLUME_INIT = "busybox@sha256:" + "4" * 64 +GATEWAY = "caddy@sha256:" + "5" * 64 def load_check_image_pins(): @@ -35,19 +37,23 @@ def setUp(self) -> None: self.module.ROOT = self.root (self.root / "versions.env").write_text( f"REGISTRY_RELAY_IMAGE={RELAY}\n" - f"REGISTRY_NOTARY_IMAGE={NOTARY}\n" - f"VOLUME_INIT_IMAGE={VOLUME_INIT}\n", + f"SOLMARA_EVIDENCE_IMAGE={EVIDENCE}\n" + f"SOLMARA_MINT_IMAGE={MINT}\n" + f"VOLUME_INIT_IMAGE={VOLUME_INIT}\n" + f"EVIDENCE_GATEWAY_IMAGE={GATEWAY}\n", encoding="utf-8", ) def tearDown(self) -> None: self.directory.cleanup() - def run_check(self, relay: str = RELAY, notary: str = NOTARY) -> tuple[int, str]: + def run_check(self, *, required: bool = True) -> tuple[int, str]: + operator = ":?required" if required else ":-fallback" (self.root / "compose.yaml").write_text( "services:\n" - f" relay:\n image: ${{REGISTRY_RELAY_IMAGE:-{relay}}}\n" - f" notary:\n image: ${{REGISTRY_NOTARY_IMAGE:-{notary}}}\n", + f" relay:\n image: ${{REGISTRY_RELAY_IMAGE{operator}}}\n" + f" evidence:\n image: ${{SOLMARA_EVIDENCE_IMAGE{operator}}}\n" + f" mint:\n image: ${{SOLMARA_MINT_IMAGE{operator}}}\n", encoding="utf-8", ) stderr = io.StringIO() @@ -55,22 +61,24 @@ def run_check(self, relay: str = RELAY, notary: str = NOTARY) -> tuple[int, str] result = self.module.main() return result, stderr.getvalue() - def test_matching_compose_fallbacks_pass(self) -> None: + def test_matching_source_image_references_pass(self) -> None: result, stderr = self.run_check() self.assertEqual(result, 0, stderr) - def test_relay_fallback_must_match_versions_env(self) -> None: - result, stderr = self.run_check(relay=RELAY[:-1] + "3") + def test_source_images_must_be_required(self) -> None: + result, stderr = self.run_check(required=False) self.assertEqual(result, 1) - self.assertIn("REGISTRY_RELAY_IMAGE fallback must match versions.env", stderr) + self.assertIn("expected a required REGISTRY_RELAY_IMAGE reference", stderr) - def test_notary_fallback_must_match_versions_env(self) -> None: - result, stderr = self.run_check(notary=NOTARY[:-1] + "3") + def test_gateway_must_be_digest_pinned(self) -> None: + versions = (self.root / "versions.env").read_text().replace(GATEWAY, "caddy:latest") + (self.root / "versions.env").write_text(versions) + result, stderr = self.run_check() self.assertEqual(result, 1) - self.assertIn("REGISTRY_NOTARY_IMAGE fallback must match versions.env", stderr) + self.assertIn("EVIDENCE_GATEWAY_IMAGE must use image@sha256", stderr) if __name__ == "__main__": diff --git a/scripts/test_registryctl_build_output.py b/scripts/test_registryctl_build_output.py index ab4663f..b2415d5 100644 --- a/scripts/test_registryctl_build_output.py +++ b/scripts/test_registryctl_build_output.py @@ -28,16 +28,14 @@ def setUp(self) -> None: encoding="utf-8", ) self.output = self.project / ".registry-stack" / "build" / "local" - relay = self.output / "private" / "relay" / "config" - relay.mkdir(parents=True) - (relay / "relay.yaml").write_text("instance: {}\n", encoding="utf-8") - notary = self.output / "private" / "notary" / "config" - notary.mkdir(parents=True) - (notary / "notary.yaml").write_text("instance: {}\n", encoding="utf-8") + for lane in ("relay-public", "relay-consultation"): + relay = self.output / "private" / lane / "config" + relay.mkdir(parents=True) + (relay / "relay.yaml").write_text("instance: {}\n", encoding="utf-8") def report(self, **overrides: object) -> bytes: - report: dict[str, object] = { - "schema_version": MODULE.REPORT_SCHEMA, + build: dict[str, object] = { + "schema_version": MODULE.BUILD_SCHEMA, "status": "built", "project": "example", "environment": "local", @@ -46,7 +44,12 @@ def report(self, **overrides: object) -> bytes: "baseline": "initial_without_baseline", "output": ".registry-stack/build/local", } - report.update(overrides) + build.update(overrides) + report: dict[str, object] = { + "schema_version": MODULE.REPORT_SCHEMA, + "affected_lanes": ["relay-public", "relay-consultation"], + "build": build, + } return json.dumps(report).encode("utf-8") def test_accepts_the_versioned_project_owned_build_root(self) -> None: @@ -108,7 +111,13 @@ def test_rejects_a_relative_output_root_that_escapes_the_project(self) -> None: ) def test_rejects_an_incomplete_product_closure(self) -> None: - (self.output / "private" / "notary" / "config" / "notary.yaml").unlink() + ( + self.output + / "private" + / "relay-consultation" + / "config" + / "relay.yaml" + ).unlink() with self.assertRaisesRegex( MODULE.BuildReportError, "configuration closure is incomplete", diff --git a/scripts/test_registryctl_test_output.py b/scripts/test_registryctl_test_output.py index a79c048..13171e3 100644 --- a/scripts/test_registryctl_test_output.py +++ b/scripts/test_registryctl_test_output.py @@ -26,18 +26,19 @@ def report(self, **overrides: object) -> bytes: "fixture_coverage": { "targets": [ { - "requirements": [ + "identity": {"integration": "example", "capability": "snapshot"}, + "fixture_set_state": "fixture_bearing", + "compiled_contract": { + "kind": "compiled_contract", + "digest": "sha256:compiled", + }, + "fixture_inventory": [ { - "state": "covered", - "requirement": MODULE.REQUEST_BINDING_REQUIREMENT, - "evidence": [ - { - "kind": "authored_fixture", - "id": "target/example/fixture/match", - } - ], + "fixture_id": "match", + "fixture_digest": "sha256:fixture", + "pass_state": "passed", } - ] + ], } ] }, @@ -45,56 +46,28 @@ def report(self, **overrides: object) -> bytes: report.update(overrides) return json.dumps(report).encode("utf-8") - def test_accepts_passing_authored_request_witnesses(self) -> None: + def test_accepts_passing_compiled_fixture_coverage(self) -> None: self.assertEqual( MODULE.validate_test_report(self.report()), ("example", 1, 1), ) - def test_rejects_mapping_derived_request_coverage(self) -> None: - coverage = { - "targets": [ - { - "requirements": [ - { - "state": "missing", - "requirement": MODULE.REQUEST_BINDING_REQUIREMENT, - "reason": "required_evidence_missing", - "evidence": [], - } - ] - } - ] - } + def test_rejects_a_target_without_a_compiled_contract(self) -> None: + coverage = {"targets": [{"identity": {"integration": "example", "capability": "snapshot"}, "fixture_set_state": "fixture_bearing", "fixture_inventory": []}]} with self.assertRaisesRegex( MODULE.TestReportError, - "every fixture target must cover", + "must bind passing fixtures", ): - MODULE.validate_test_report( - self.report(fixture_coverage=coverage) - ) + MODULE.validate_test_report(self.report(fixture_coverage=coverage)) - def test_rejects_non_authored_binding_evidence(self) -> None: - coverage = { - "targets": [ - { - "requirements": [ - { - "state": "covered", - "requirement": MODULE.REQUEST_BINDING_REQUIREMENT, - "evidence": [{"kind": "compiled_contract"}], - } - ] - } - ] - } + def test_rejects_a_non_passing_coverage_fixture(self) -> None: + report = json.loads(self.report()) + report["fixture_coverage"]["targets"][0]["fixture_inventory"][0]["pass_state"] = "failed" with self.assertRaisesRegex( MODULE.TestReportError, - "requires authored fixture evidence", + "must bind passing fixtures", ): - MODULE.validate_test_report( - self.report(fixture_coverage=coverage) - ) + MODULE.validate_test_report(json.dumps(report).encode()) def test_rejects_a_non_passing_fixture_without_echoing_values(self) -> None: with self.assertRaisesRegex( diff --git a/scripts/test_relay_workload_identity_agent.py b/scripts/test_relay_workload_identity_agent.py index 96167bc..51ec770 100644 --- a/scripts/test_relay_workload_identity_agent.py +++ b/scripts/test_relay_workload_identity_agent.py @@ -248,6 +248,7 @@ def inspect_then_replace(source: str, destination: Path) -> None: self.assertEqual( len(identity.token_file.read_text(encoding="ascii").split(".")), 3 ) + self.assertFalse(identity.token_file.read_bytes().endswith(b"\n")) self.assertTrue(state.ready()) def test_readiness_requires_every_current_regular_output(self) -> None: diff --git a/versions.env b/versions.env index 8f9f620..992405d 100644 --- a/versions.env +++ b/versions.env @@ -1,12 +1,13 @@ -REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/registry-relay@sha256:71082176f00107e420ffe79471f0bd3375c3dd1ae1c3c8c3a24100c602fa82b7 -REGISTRY_NOTARY_IMAGE=ghcr.io/registrystack/registry-notary@sha256:1cce41247a18f70cb7ff4276bdeb5a16c1e641a9ed7951be8f612daaf35ab87b -REGISTRYCTL_VERSION=0.15.2 -REGISTRY_STACK_SOURCE_REF=v0.15.2 -REGISTRY_STACK_SOURCE_COMMIT=835b824d862e4ffbc1b96307d9d717e41c8e268d +REGISTRY_RELAY_IMAGE=solmara-lab-registry-relay:source +REGISTRYCTL_VERSION=0.17.0 +REGISTRY_STACK_SOURCE_REF=main +REGISTRY_STACK_SOURCE_COMMIT=9a4365eb3693f485e538ca07e7f8655538ab2df6 SOLMARA_RELAY_DEV_IMAGE=solmara-lab-registry-relay:source +SOLMARA_EVIDENCE_IMAGE=solmara-lab-registry-evidence:source +SOLMARA_MINT_IMAGE=solmara-lab-registry-mint:source SOLMARA_RELAY_BUNDLE_SEQUENCE=2 -REGISTRY_RELAY_STATE_EPOCH=v015 POSTGRES_IMAGE=postgres:16-alpine STATIC_METADATA_IMAGE=python:3.12-alpine VOLUME_INIT_IMAGE=busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 +EVIDENCE_GATEWAY_IMAGE=docker.io/library/caddy@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d SOLMARA_PORTAL_IMAGE=solmara-lab-portal:local From 3bbe0f8a64f4dc6f42b2d30c613f3170a08968e8 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 12 Aug 2026 22:59:01 +0700 Subject: [PATCH 02/53] Reset Solmara around authority-owned Relay and Evidence Signed-off-by: Jeremi Joslin --- .dockerignore | 39 +- .env.example | 49 +- .github/workflows/ci.yml | 52 +- .github/workflows/release-candidate.yml | 428 ++-- .gitignore | 4 + README.md | 230 +- compose.coolify.agriculture.yaml | 279 +-- compose.coolify.esignet.yaml | 200 +- compose.coolify.interior.yaml | 448 +--- compose.coolify.labour-pensions.yaml | 285 +-- compose.coolify.provision.yaml | 299 +++ compose.coolify.social-development.yaml | 426 +--- compose.coolify.walt.yaml | 138 +- compose.coolify.yaml | 266 +-- compose.esignet.yaml | 179 +- compose.hosted.yaml | 402 +--- compose.yaml | 741 +++--- config/esignet/nginx.conf | 10 +- config/evidence/Caddyfile | 58 + .../cra-civil/antirollback-seed.json | 9 - .../cra-civil/bootstrap.yaml | 87 - .../cra-civil/bundle/config/relay.yaml | 87 - .../cra-civil/bundle/manifest.json | 17 - .../cra-civil/bundle/manifest.sig.json | 10 - .../consultation/antirollback-seed.json | 9 - .../cra-civil/consultation/bootstrap.yaml | 142 -- .../cra-child-benefit-civil.json | 1 - .../cra-citizen-record-civil.json | 1 - .../cra-pension-death-civil.json | 1 - .../civil-person-by-uin/conformance.json | 1 - .../civil-person-by-uin/minimization.json | 1 - .../negative-security.json | 1 - .../civil-person-by-uin.json | 1 - .../cra-child-benefit-civil.json | 1 - .../cra-citizen-record-civil.json | 1 - .../cra-pension-death-civil.json | 1 - .../consultation/bundle/config/relay.yaml | 142 -- .../consultation/bundle/manifest.json | 57 - .../consultation/bundle/manifest.sig.json | 10 - .../cra-civil/consultation/trust-anchor.json | 20 - .../cra-civil/trust-anchor.json | 20 - .../mosd-programme/antirollback-seed.json | 9 - .../mosd-programme/bootstrap.yaml | 80 - .../mosd-programme/bundle/config/relay.yaml | 80 - .../mosd-programme/bundle/manifest.json | 17 - .../mosd-programme/bundle/manifest.sig.json | 10 - .../consultation/antirollback-seed.json | 9 - .../consultation/bootstrap.yaml | 122 - .../child-benefit-enrollment.json | 1 - .../enrollment-by-uin/conformance.json | 1 - .../enrollment-by-uin/minimization.json | 1 - .../enrollment-by-uin/negative-security.json | 1 - .../integration-packs/enrollment-by-uin.json | 1 - .../child-benefit-enrollment.json | 1 - .../consultation/bundle/config/relay.yaml | 122 - .../consultation/bundle/manifest.json | 41 - .../consultation/bundle/manifest.sig.json | 10 - .../consultation/trust-anchor.json | 20 - .../mosd-programme/trust-anchor.json | 20 - .../nagdi-agriculture/antirollback-seed.json | 9 - .../nagdi-agriculture/bootstrap.yaml | 178 -- .../bundle/config/relay.yaml | 178 -- .../nagdi-agriculture/bundle/manifest.json | 17 - .../bundle/manifest.sig.json | 10 - .../consultation/antirollback-seed.json | 9 - .../consultation/bootstrap.yaml | 238 -- .../livestock-herd.json | 1 - .../voucher-farmer.json | 1 - .../farmer-voucher-by-farmer/conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../farmer-voucher-by-farmer.json | 1 - .../livestock-movement-by-farmer.json | 1 - .../private-bindings/livestock-herd.json | 1 - .../private-bindings/voucher-farmer.json | 1 - .../consultation/bundle/config/relay.yaml | 238 -- .../consultation/bundle/manifest.json | 65 - .../consultation/bundle/manifest.sig.json | 10 - .../consultation/trust-anchor.json | 20 - .../nagdi-agriculture/trust-anchor.json | 20 - .../nia-population/antirollback-seed.json | 9 - .../nia-population/bootstrap.yaml | 207 -- .../nia-population/bundle/config/relay.yaml | 207 -- .../nia-population/bundle/manifest.json | 17 - .../nia-population/bundle/manifest.sig.json | 10 - .../consultation/antirollback-seed.json | 9 - .../consultation/bootstrap.yaml | 255 --- .../nia-child-benefit-population.json | 1 - .../nia-citizen-status-population.json | 1 - .../population-person-by-uin/conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../population-person-by-uin.json | 1 - .../nia-child-benefit-population.json | 1 - .../nia-citizen-status-population.json | 1 - .../consultation/bundle/config/relay.yaml | 255 --- .../consultation/bundle/manifest.json | 49 - .../consultation/bundle/manifest.sig.json | 10 - .../consultation/trust-anchor.json | 20 - .../nia-population/trust-anchor.json | 20 - .../sipf-pensions/antirollback-seed.json | 9 - .../sipf-pensions/bootstrap.yaml | 125 - .../sipf-pensions/bundle/config/relay.yaml | 125 - .../sipf-pensions/bundle/manifest.json | 17 - .../sipf-pensions/bundle/manifest.sig.json | 10 - .../consultation/antirollback-seed.json | 9 - .../sipf-pensions/consultation/bootstrap.yaml | 186 -- .../sipf-pension-payment-review-pension.json | 1 - .../sipf-survivor-benefit-survivor.json | 1 - .../conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../pension-case-by-survivor/conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../pension-case-by-pensioner.json | 1 - .../pension-case-by-survivor.json | 1 - .../sipf-pension-payment-review-pension.json | 1 - .../sipf-survivor-benefit-survivor.json | 1 - .../consultation/bundle/config/relay.yaml | 186 -- .../consultation/bundle/manifest.json | 65 - .../consultation/bundle/manifest.sig.json | 10 - .../consultation/trust-anchor.json | 20 - .../sipf-pensions/trust-anchor.json | 20 - .../sro-social/antirollback-seed.json | 9 - .../sro-social/bootstrap.yaml | 80 - .../sro-social/bundle/config/relay.yaml | 80 - .../sro-social/bundle/manifest.json | 17 - .../sro-social/bundle/manifest.sig.json | 10 - .../consultation/antirollback-seed.json | 9 - .../sro-social/consultation/bootstrap.yaml | 122 - .../child-benefit-household.json | 1 - .../conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../child-benefit-household-by-uin.json | 1 - .../child-benefit-household.json | 1 - .../consultation/bundle/config/relay.yaml | 122 - .../consultation/bundle/manifest.json | 41 - .../consultation/bundle/manifest.sig.json | 10 - .../sro-social/consultation/trust-anchor.json | 20 - .../sro-social/trust-anchor.json | 20 - config/walt/Caddyfile | 4 +- config/walt/README.md | 8 +- config/walt/registration-defaults.conf | 6 +- docker/esignet-postgres/Dockerfile | 2 +- docker/esignet-relay/Dockerfile | 24 +- docker/esignet-ui/Dockerfile | 2 +- .../hosted-authority-provisioner/Dockerfile | 49 + docker/hosted-transit-signer/Dockerfile | 20 + docker/notary/Dockerfile | 10 - docker/postgres/Dockerfile | 11 - .../registry-stack-release-binary/Dockerfile | 42 + docker/registry-stack-runtime/Dockerfile | 40 +- docker/relay-runtime/Dockerfile | 26 - docker/relay/Dockerfile | 9 - docker/static-metadata/Dockerfile | 2 +- docs/changelog.md | 18 + docs/esignet.md | 24 + docs/hosted-deployment.md | 457 ++-- docs/notary-postgresql-state.md | 192 -- docs/purposes.md | 95 +- docs/solmara.md | 240 +- docs/source-recovery.md | 20 + docs/stories/birth-to-child-benefit.md | 120 +- docs/stories/death-to-pension-survivor.md | 106 +- docs/stories/farmer-climate-smart-voucher.md | 122 +- docs/wallet-oid4vci.md | 21 - evidence/Caddyfile | 66 - evidence/bundle/adapters/extract.rhai | 26 - evidence/bundle/adapters/prepare.rhai | 16 - .../bundle/derivations/cra-child-benefit.rhai | 16 - .../bundle/derivations/population-active.rhai | 8 - evidence/bundle/derivations/voucher.rhai | 19 - evidence/bundle/evidence.yaml | 542 ----- .../bundle/fixtures/cra-child-benefit.yaml | 62 - evidence/bundle/fixtures/cra-citizen.yaml | 39 - evidence/bundle/fixtures/cra-pension.yaml | 39 - .../bundle/fixtures/mosd-child-benefit.yaml | 39 - evidence/bundle/fixtures/nagdi-livestock.yaml | 65 - evidence/bundle/fixtures/nagdi-voucher.yaml | 67 - .../bundle/fixtures/nia-child-benefit.yaml | 39 - evidence/bundle/fixtures/nia-citizen.yaml | 39 - evidence/bundle/fixtures/sipf-pension.yaml | 39 - evidence/bundle/fixtures/sipf-survivor.yaml | 39 - .../bundle/fixtures/sro-child-benefit.yaml | 39 - .../schemas/adapter-parameters.schema.yaml | 7 - .../cra-child-benefit-facts.schema.yaml | 8 - .../schemas/cra-citizen-facts.schema.yaml | 5 - .../schemas/cra-pension-facts.schema.yaml | 6 - .../mosd-child-benefit-facts.schema.yaml | 6 - .../schemas/nagdi-livestock-facts.schema.yaml | 10 - .../schemas/nagdi-voucher-facts.schema.yaml | 13 - .../schemas/nia-population-facts.schema.yaml | 7 - evidence/bundle/schemas/response.schema.yaml | 45 - .../schemas/sipf-pension-facts.schema.yaml | 6 - .../schemas/sipf-survivor-facts.schema.yaml | 6 - .../sro-child-benefit-facts.schema.yaml | 6 - .../cra/bundle/adapters/birth-extract.rhai | 7 + .../cra/bundle/adapters/relay-extract.rhai | 5 + .../cra/bundle/adapters/relay-prepare.rhai | 5 + .../cra/bundle/derivations/child-benefit.rhai | 9 + .../cra/bundle/derivations/deceased.rhai} | 3 - .../cra/bundle/derivations/linked.rhai} | 4 +- evidence/cells/cra/bundle/evidence.yaml | 128 ++ .../cra/bundle/fixtures/child-benefit.yaml | 42 + .../cells/cra/bundle/fixtures/citizen.yaml | 34 + .../cells/cra/bundle/fixtures/pension.yaml | 34 + .../cra/bundle/queries/birth-evidence.sql | 4 + .../bundle/schemas/birth-facts.schema.yaml | 4 + .../bundle/schemas/birth-response.schema.yaml | 5 + .../bundle/schemas/deceased-facts.schema.yaml | 4 + .../schemas/deceased-response.schema.yaml | 4 + .../bundle/schemas/linked-facts.schema.yaml | 4 + .../schemas/linked-response.schema.yaml | 4 + .../relay-adapter-parameters.schema.yaml | 4 + evidence/cells/cra/runtime.yaml | 9 + .../bundle/adapters/relay-extract.rhai | 1 + .../bundle/adapters/relay-prepare.rhai | 5 + .../bundle/derivations/not-enrolled.rhai | 3 - .../cells/mosd-programme/bundle/evidence.yaml | 54 + .../bundle/fixtures/child-benefit.yaml | 34 + .../bundle/schemas/facts.schema.yaml | 4 + .../relay-adapter-parameters.schema.yaml | 4 + .../bundle/schemas/response.schema.yaml | 4 + evidence/cells/mosd-programme/runtime.yaml | 7 + .../nagdi/bundle/adapters/relay-extract.rhai | 7 + .../nagdi/bundle/adapters/relay-prepare.rhai | 5 + .../nagdi}/bundle/derivations/livestock.rhai | 8 +- .../nagdi/bundle/derivations/voucher.rhai | 10 + evidence/cells/nagdi/bundle/evidence.yaml | 92 + .../nagdi/bundle/fixtures/livestock.yaml | 34 + .../cells/nagdi/bundle/fixtures/voucher.yaml | 34 + .../schemas/livestock-facts.schema.yaml | 4 + .../schemas/livestock-response.schema.yaml | 4 + .../relay-adapter-parameters.schema.yaml | 4 + .../bundle/schemas/voucher-facts.schema.yaml | 4 + .../schemas/voucher-response.schema.yaml | 4 + evidence/cells/nagdi/runtime.yaml | 7 + .../nia/bundle/adapters/sqlite-extract.rhai | 6 + .../bundle/derivations/population-active.rhai | 4 + evidence/cells/nia/bundle/evidence.yaml | 67 + .../nia/bundle/fixtures/child-benefit.yaml | 42 + .../cells/nia/bundle/fixtures/citizen.yaml | 43 + .../bundle/queries/population-evidence.sql | 4 + .../schemas/population-facts.schema.yaml | 4 + .../schemas/population-response.schema.yaml | 5 + evidence/cells/nia/runtime.yaml | 9 + .../sipf/bundle/adapters/relay-extract.rhai | 5 + .../sipf/bundle/adapters/relay-prepare.rhai | 13 + .../bundle/derivations/pension-active.rhai | 3 - .../bundle/derivations/survivor-eligible.rhai | 3 - evidence/cells/sipf/bundle/evidence.yaml | 92 + .../cells/sipf/bundle/fixtures/pension.yaml | 34 + .../cells/sipf/bundle/fixtures/survivor.yaml | 34 + .../bundle/schemas/pension-facts.schema.yaml | 4 + .../schemas/pension-response.schema.yaml | 4 + .../relay-adapter-parameters.schema.yaml | 4 + .../bundle/schemas/survivor-facts.schema.yaml | 4 + .../schemas/survivor-response.schema.yaml | 4 + evidence/cells/sipf/runtime.yaml | 7 + .../sro/bundle/adapters/sqlite-extract.rhai | 6 + .../bundle/derivations/poverty-priority.rhai | 3 - evidence/cells/sro/bundle/evidence.yaml | 52 + .../sro/bundle/fixtures/child-benefit.yaml | 42 + .../sro/bundle/queries/poverty-evidence.sql | 4 + .../bundle/schemas/poverty-facts.schema.yaml | 4 + .../schemas/poverty-response.schema.yaml | 5 + evidence/cells/sro/runtime.yaml | 9 + evidence/mint.yaml | 32 +- evidence/runtime.yaml | 22 - evidence/scripts/build-cells.py | 159 ++ evidence/tests/test_cells.py | 218 ++ generator/solmara_lab/generate.py | 2 +- generator/solmara_lab/publisher.py | 984 ++++++++ generator/tests/test_publisher.py | 560 +++++ home/Dockerfile | 4 +- home/e2e/home.test.ts | 115 +- home/src/lib/components/EngineerDoor.svelte | 30 +- home/src/lib/components/NationPanel.svelte | 12 +- home/src/lib/components/ProofStrip.svelte | 8 +- home/src/lib/components/PurposeLens.svelte | 73 +- home/src/lib/components/SolmaraPreview.svelte | 8 +- home/src/lib/components/TrustStrip.svelte | 4 +- home/src/lib/curl.test.ts | 4 +- home/src/lib/curl.ts | 6 +- home/src/lib/runresult.ts | 10 +- home/src/lib/server/data.test.ts | 11 +- home/src/lib/server/data.ts | 74 +- home/src/lib/server/problemcodes.test.ts | 2 +- home/src/lib/server/problemcodes.ts | 13 +- home/src/lib/server/purposes.test.ts | 8 +- home/src/lib/server/purposes.ts | 16 +- home/src/lib/server/services.test.ts | 66 +- home/src/lib/server/services.ts | 407 ++-- home/src/lib/server/tokens.test.ts | 63 +- home/src/lib/server/tokens.ts | 244 +- home/src/lib/server/urlmap.test.ts | 55 +- home/src/lib/server/urlmap.ts | 21 +- home/src/lib/types.ts | 32 +- home/src/routes/+page.svelte | 2 +- home/src/routes/anatomy/+page.svelte | 6 +- .../[scenario]/steps/[step]/run/+server.ts | 132 +- home/src/routes/developers/+page.svelte | 3 +- home/src/routes/explorer/+page.svelte | 42 +- home/src/routes/stories/[id]/+page.svelte | 25 +- justfile | 197 +- metadata/public/metadata/catalog.json | 472 ++-- metadata/public/metadata/cpsv-ap | 406 ++-- metadata/public/metadata/cpsv-ap.jsonld | 406 ++-- metadata/public/metadata/dcat.jsonld | 16 +- .../public/metadata/evidence-offerings.json | 351 +-- .../cra-birth-registration-offering.json | 42 - .../cra-child-benefit-v1-offering.json | 42 + .../cra-citizen-record-v1-offering.json | 41 + ...son => cra-pension-death-v1-offering.json} | 23 +- .../mosd-child-benefit-v1-offering.json | 41 + ...ramme-beneficiary-enrollment-offering.json | 40 - ...i-agriculture-farmer-voucher-offering.json | 40 - ...riculture-livestock-movement-offering.json | 40 - .../nagdi-livestock-v1-offering.json | 43 + .../nagdi-voucher-v1-offering.json | 43 + .../nia-child-benefit-v1-offering.json | 41 + .../nia-citizen-status-v1-offering.json | 41 + ...population-population-status-offering.json | 40 - .../sipf-pension-payment-v1-offering.json | 41 + .../sipf-pensions-pension-case-offering.json | 41 - .../sipf-survivor-benefit-v1-offering.json | 41 + ...enefit.authority-predicate-collection.json | 45 - .../sro-child-benefit-v1-offering.json | 41 + ...sro-social-household-poverty-offering.json | 41 - metadata/public/metadata/index.json | 110 +- metadata/public/metadata/metadata.yaml | 579 ++--- .../public/metadata/ogc-records/items.json | 12 +- metadata/public/metadata/policies.jsonld | 111 +- ...a-child-benefit-v1-offering-policy.jsonld} | 6 +- ...a-citizen-record-v1-offering-policy.jsonld | 24 + ...a-pension-death-v1-offering-policy.jsonld} | 6 +- ...d-child-benefit-v1-offering-policy.jsonld} | 6 +- ...nagdi-livestock-v1-offering-policy.jsonld} | 6 +- ...> nagdi-voucher-v1-offering-policy.jsonld} | 6 +- ...a-child-benefit-v1-offering-policy.jsonld} | 6 +- ...a-citizen-status-v1-offering-policy.jsonld | 24 + ...pension-payment-v1-offering-policy.jsonld} | 9 +- ...survivor-benefit-v1-offering-policy.jsonld | 24 + ...thority-predicate-collection-policy.jsonld | 24 - ...o-child-benefit-v1-offering-policy.jsonld} | 6 +- metadata/solmara-wave1.metadata.yaml | 579 ++--- perf/README.md | 133 -- perf/k6/lib/common.js | 336 --- perf/k6/notary_relay_backed.js | 110 - portal/AGENTS.md | 2 +- portal/Dockerfile | 4 +- portal/e2e/portal.test.ts | 8 +- portal/src/lib/fields/EvidenceField.test.ts | 6 +- portal/src/lib/fields/authorities.ts | 65 +- portal/src/lib/fields/index.ts | 2 +- portal/src/lib/forms/DenialBeat.svelte | 8 +- portal/src/lib/forms/clientFeed.svelte.ts | 27 +- portal/src/lib/forms/clientFeed.test.ts | 7 +- portal/src/lib/forms/descriptors.test.ts | 4 +- portal/src/lib/forms/descriptors.ts | 30 +- portal/src/lib/forms/identity.ts | 15 +- portal/src/lib/proof/ProofInspector.svelte | 1002 +------- portal/src/lib/proof/ProofInspector.test.ts | 288 +-- portal/src/lib/proof/ProofTicker.svelte | 8 +- portal/src/lib/proof/ProofTicker.test.ts | 14 +- portal/src/lib/proof/canned-traces.ts | 190 +- portal/src/lib/providers/authority-plan.ts | 42 +- portal/src/lib/providers/feeds.svelte.test.ts | 11 +- portal/src/lib/providers/live/index.test.ts | 14 +- portal/src/lib/providers/live/index.ts | 145 +- portal/src/lib/providers/mock/index.test.ts | 459 ++-- portal/src/lib/providers/mock/index.ts | 225 +- .../src/lib/providers/mock/scenarios.test.ts | 29 +- portal/src/lib/providers/mock/scenarios.ts | 80 +- portal/src/lib/providers/mock/wire.ts | 479 ++-- portal/src/lib/rail/MinistryRail.svelte | 14 +- portal/src/lib/server/bff.ts | 48 +- portal/src/lib/server/redact.test.ts | 252 +- portal/src/lib/server/redact.ts | 238 +- portal/src/lib/types.ts | 40 +- portal/src/routes/+layout.svelte | 26 +- portal/src/routes/api/evaluate/+server.ts | 7 +- portal/src/routes/gallery/fields/+page.svelte | 8 +- portal/src/routes/gallery/proof/+page.svelte | 6 +- portal/src/routes/proof/stream/server.test.ts | 11 +- .../src/routes/services/[slug]/+page.svelte | 6 +- .../.registry-stack-editor/manifest.json | 37 - .../schemas/entity.schema.json | 131 -- .../schemas/environment.schema.json | 337 --- .../schemas/fixture.schema.json | 262 --- .../schemas/integration.schema.json | 514 ----- .../schemas/project.schema.json | 537 ----- projects/cra-civil/.vscode/extensions.json | 5 - projects/cra-civil/.vscode/settings.json | 9 - projects/cra-civil/.zed/settings.json | 17 - projects/cra-civil/entities/civil_people.yaml | 28 - projects/cra-civil/environments/hosted.yaml | 25 - projects/cra-civil/environments/local.yaml | 25 - .../fixtures/future-birth-date.yaml | 18 - .../civil-person-by-uin/fixtures/match.yaml | 18 - .../fixtures/no-match.yaml | 10 - .../civil-person-by-uin/integration.yaml | 28 - projects/cra-civil/registry-stack.yaml | 50 - .../.registry-stack-editor/manifest.json | 37 - .../schemas/entity.schema.json | 131 -- .../schemas/environment.schema.json | 337 --- .../schemas/fixture.schema.json | 262 --- .../schemas/integration.schema.json | 514 ----- .../schemas/project.schema.json | 537 ----- .../mosd-programme/.vscode/extensions.json | 5 - projects/mosd-programme/.vscode/settings.json | 9 - projects/mosd-programme/.zed/settings.json | 17 - .../mosd-programme/entities/mosd_enroll.yaml | 20 - .../mosd-programme/environments/hosted.yaml | 26 - .../mosd-programme/environments/local.yaml | 26 - .../enrollment-by-uin/fixtures/match.yaml | 9 - .../enrollment-by-uin/fixtures/no-match.yaml | 9 - .../enrollment-by-uin/integration.yaml | 28 - projects/mosd-programme/registry-stack.yaml | 48 - .../.registry-stack-editor/manifest.json | 37 - .../schemas/entity.schema.json | 131 -- .../schemas/environment.schema.json | 337 --- .../schemas/fixture.schema.json | 262 --- .../schemas/integration.schema.json | 514 ----- .../schemas/project.schema.json | 537 ----- .../nagdi-agriculture/.vscode/extensions.json | 5 - .../nagdi-agriculture/.vscode/settings.json | 9 - projects/nagdi-agriculture/.zed/settings.json | 17 - .../entities/nagdi_farmer.yaml | 36 - .../entities/nagdi_herd.yaml | 36 - .../environments/hosted.yaml | 48 - .../nagdi-agriculture/environments/local.yaml | 48 - .../fixtures/match.yaml | 27 - .../fixtures/no-match.yaml | 9 - .../farmer-voucher-by-farmer/integration.yaml | 36 - .../fixtures/ambiguous.yaml | 19 - .../fixtures/match.yaml | 21 - .../fixtures/no-match.yaml | 9 - .../integration.yaml | 30 - .../nagdi-agriculture/registry-stack.yaml | 103 - .../.registry-stack-editor/manifest.json | 37 - .../schemas/entity.schema.json | 131 -- .../schemas/environment.schema.json | 337 --- .../schemas/fixture.schema.json | 262 --- .../schemas/integration.schema.json | 514 ----- .../schemas/project.schema.json | 537 ----- .../nia-population/.vscode/extensions.json | 5 - projects/nia-population/.vscode/settings.json | 9 - projects/nia-population/.zed/settings.json | 17 - .../nia-population/entities/population.yaml | 48 - .../nia-population/environments/hosted.yaml | 30 - .../nia-population/environments/local.yaml | 30 - .../fixtures/match.yaml | 11 - .../fixtures/no-match.yaml | 9 - .../population-person-by-uin/integration.yaml | 28 - projects/nia-population/registry-stack.yaml | 97 - .../.registry-stack-editor/manifest.json | 37 - .../schemas/entity.schema.json | 131 -- .../schemas/environment.schema.json | 337 --- .../schemas/fixture.schema.json | 262 --- .../schemas/integration.schema.json | 514 ----- .../schemas/project.schema.json | 537 ----- .../sipf-pensions/.vscode/extensions.json | 5 - projects/sipf-pensions/.vscode/settings.json | 9 - projects/sipf-pensions/.zed/settings.json | 17 - projects/sipf-pensions/entities/pension.yaml | 22 - projects/sipf-pensions/entities/survivor.yaml | 21 - .../sipf-pensions/environments/hosted.yaml | 35 - .../sipf-pensions/environments/local.yaml | 35 - .../fixtures/ambiguous.yaml | 13 - .../fixtures/match.yaml | 9 - .../fixtures/no-match.yaml | 9 - .../integration.yaml | 25 - .../fixtures/ambiguous.yaml | 13 - .../fixtures/match.yaml | 9 - .../fixtures/no-match.yaml | 9 - .../pension-case-by-survivor/integration.yaml | 25 - projects/sipf-pensions/registry-stack.yaml | 87 - .../.registry-stack-editor/manifest.json | 37 - .../schemas/entity.schema.json | 131 -- .../schemas/environment.schema.json | 337 --- .../schemas/fixture.schema.json | 262 --- .../schemas/integration.schema.json | 514 ----- .../schemas/project.schema.json | 537 ----- projects/sro-social/.vscode/extensions.json | 5 - projects/sro-social/.vscode/settings.json | 9 - projects/sro-social/.zed/settings.json | 17 - projects/sro-social/entities/sro_child.yaml | 23 - projects/sro-social/environments/hosted.yaml | 26 - projects/sro-social/environments/local.yaml | 26 - .../fixtures/match.yaml | 9 - .../fixtures/no-match.yaml | 9 - .../integration.yaml | 28 - projects/sro-social/registry-stack.yaml | 48 - relays/cra/codelists/record-lifecycle.yaml | 4 + relays/cra/expected-http.yaml | 42 + relays/cra/fixture.sql | 25 + .../classification-review-rationale.md | 1 + .../cra/governance/classification-review.yaml | 9 + .../cra/governance/identifier-lifecycle.yaml | 2 + relays/cra/governance/legal-basis.yaml | 2 + relays/cra/governance/processing.dpv.yaml | 2 + relays/cra/registry.yaml | 86 + relays/cra/runtime.yaml | 15 + relays/mosd/codelists/record-lifecycle.yaml | 4 + relays/mosd/expected-http.yaml | 21 + relays/mosd/fixture.sql | 21 + .../classification-review-rationale.md | 1 + .../governance/classification-review.yaml | 9 + .../mosd/governance/identifier-lifecycle.yaml | 2 + relays/mosd/governance/legal-basis.yaml | 2 + relays/mosd/governance/processing.dpv.yaml | 2 + relays/mosd/registry.yaml | 50 + relays/mosd/runtime.yaml | 10 + relays/nagdi/codelists/record-lifecycle.yaml | 4 + relays/nagdi/expected-http.yaml | 33 + relays/nagdi/fixture.sql | 57 + .../classification-review-rationale.md | 1 + .../governance/classification-review.yaml | 9 + .../governance/identifier-lifecycle.yaml | 2 + relays/nagdi/governance/legal-basis.yaml | 2 + relays/nagdi/governance/processing.dpv.yaml | 2 + relays/nagdi/registry.yaml | 104 + relays/nagdi/runtime.yaml | 11 + relays/nia/codelists/identity-status.yaml | 4 + relays/nia/codelists/record-lifecycle.yaml | 4 + relays/nia/codelists/sex.yaml | 4 + relays/nia/expected-http.yaml | 21 + relays/nia/fixture.sql | 28 + .../classification-review-rationale.md | 1 + .../nia/governance/classification-review.yaml | 9 + .../nia/governance/identifier-lifecycle.yaml | 2 + relays/nia/governance/legal-basis.yaml | 2 + relays/nia/governance/processing.dpv.yaml | 2 + relays/nia/registry.yaml | 72 + relays/nia/runtime.yaml | 10 + relays/sipf/codelists/payment-status.yaml | 4 + relays/sipf/codelists/record-lifecycle.yaml | 4 + relays/sipf/expected-http.yaml | 32 + relays/sipf/fixture.sql | 36 + .../classification-review-rationale.md | 1 + .../governance/classification-review.yaml | 9 + .../sipf/governance/identifier-lifecycle.yaml | 2 + relays/sipf/governance/legal-basis.yaml | 2 + relays/sipf/governance/processing.dpv.yaml | 2 + relays/sipf/registry.yaml | 66 + relays/sipf/runtime.yaml | 11 + relays/test_relay_projects.py | 155 ++ .../01 - Collect source predicates.bru | 2 +- .../01 - Evaluate pension stop.bru | 33 +- .../02 - Read active pension payment.bru | 33 +- .../03 - Read survivor eligibility.bru | 33 +- .../01 - Voucher eligibility.bru | 31 +- .../02 - Livestock movement control.bru | 31 +- .../50 - Relay V2/01 - CRA death lookup.bru | 20 + .../50 - Relay V2/02 - NIA eSignet lookup.bru | 20 + .../03 - MoSD enrolment lookup.bru | 20 + .../04 - SIPF pension lookup.bru | 20 + .../05 - NAgDI voucher lookup.bru | 20 + .../06 - CRA citizen-link lookup.bru | 20 + .../07 - SIPF survivor lookup.bru | 20 + .../08 - NAgDI livestock lookup.bru | 20 + requests/registry-lab/README.md | 32 +- .../registry-lab/environments/Hosted Lab.bru | 31 +- .../environments/Local Compose.bru | 31 +- .../cra-civil-consultation-civil.json | 1 - .../civil-person-by-uin/conformance.json | 1 - .../civil-person-by-uin/minimization.json | 1 - .../negative-security.json | 1 - .../civil-person-by-uin.json | 1 - .../cra-civil-consultation-civil.json | 1 - .../cra-civil/relay-consultation/relay.yaml | 175 -- .../hosted/cra-civil/relay/relay.yaml | 133 -- ...sd-enrollment-consultation-enrollment.json | 1 - .../enrollment-by-uin/conformance.json | 1 - .../enrollment-by-uin/minimization.json | 1 - .../enrollment-by-uin/negative-security.json | 1 - .../integration-packs/enrollment-by-uin.json | 1 - ...sd-enrollment-consultation-enrollment.json | 1 - .../relay-consultation/relay.yaml | 159 -- .../hosted/mosd-programme/relay/relay.yaml | 117 - .../nagdi-farmer-consultation-farmer.json | 1 - .../nagdi-herd-consultation-herd.json | 1 - .../farmer-voucher-by-farmer/conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../farmer-voucher-by-farmer.json | 1 - .../livestock-movement-by-farmer.json | 1 - .../nagdi-farmer-consultation-farmer.json | 1 - .../nagdi-herd-consultation-herd.json | 1 - .../relay-consultation/relay.yaml | 351 --- .../hosted/nagdi-agriculture/relay/relay.yaml | 291 --- ...ia-population-consultation-population.json | 1 - .../population-person-by-uin/conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../population-person-by-uin.json | 1 - ...ia-population-consultation-population.json | 1 - .../relay-consultation/relay.yaml | 248 -- .../hosted/nia-population/relay/relay.yaml | 207 -- .../sipf-pension-consultation-pension.json | 1 - .../sipf-survivor-consultation-survivor.json | 1 - .../conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../pension-case-by-survivor/conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../pension-case-by-pensioner.json | 1 - .../pension-case-by-survivor.json | 1 - .../sipf-pension-consultation-pension.json | 1 - .../sipf-survivor-consultation-survivor.json | 1 - .../relay-consultation/relay.yaml | 263 --- .../hosted/sipf-pensions/relay/relay.yaml | 203 -- .../sro-household-consultation-household.json | 1 - .../conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../child-benefit-household-by-uin.json | 1 - .../sro-household-consultation-household.json | 1 - .../sro-social/relay-consultation/relay.yaml | 159 -- .../hosted/sro-social/relay/relay.yaml | 117 - .../cra-civil-consultation-civil.json | 1 - .../civil-person-by-uin/conformance.json | 1 - .../civil-person-by-uin/minimization.json | 1 - .../negative-security.json | 1 - .../civil-person-by-uin.json | 1 - .../cra-civil-consultation-civil.json | 1 - .../cra-civil/relay-consultation/relay.yaml | 175 -- .../local/cra-civil/relay/relay.yaml | 133 -- ...sd-enrollment-consultation-enrollment.json | 1 - .../enrollment-by-uin/conformance.json | 1 - .../enrollment-by-uin/minimization.json | 1 - .../enrollment-by-uin/negative-security.json | 1 - .../integration-packs/enrollment-by-uin.json | 1 - ...sd-enrollment-consultation-enrollment.json | 1 - .../relay-consultation/relay.yaml | 159 -- .../local/mosd-programme/relay/relay.yaml | 117 - .../nagdi-farmer-consultation-farmer.json | 1 - .../nagdi-herd-consultation-herd.json | 1 - .../farmer-voucher-by-farmer/conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../farmer-voucher-by-farmer.json | 1 - .../livestock-movement-by-farmer.json | 1 - .../nagdi-farmer-consultation-farmer.json | 1 - .../nagdi-herd-consultation-herd.json | 1 - .../relay-consultation/relay.yaml | 351 --- .../local/nagdi-agriculture/relay/relay.yaml | 291 --- ...ia-population-consultation-population.json | 1 - .../population-person-by-uin/conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../population-person-by-uin.json | 1 - ...ia-population-consultation-population.json | 1 - .../relay-consultation/relay.yaml | 248 -- .../local/nia-population/relay/relay.yaml | 207 -- .../sipf-pension-consultation-pension.json | 1 - .../sipf-survivor-consultation-survivor.json | 1 - .../conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../pension-case-by-survivor/conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../pension-case-by-pensioner.json | 1 - .../pension-case-by-survivor.json | 1 - .../sipf-pension-consultation-pension.json | 1 - .../sipf-survivor-consultation-survivor.json | 1 - .../relay-consultation/relay.yaml | 263 --- .../local/sipf-pensions/relay/relay.yaml | 203 -- .../sro-household-consultation-household.json | 1 - .../conformance.json | 1 - .../minimization.json | 1 - .../negative-security.json | 1 - .../child-benefit-household-by-uin.json | 1 - .../sro-household-consultation-household.json | 1 - .../sro-social/relay-consultation/relay.yaml | 159 -- .../local/sro-social/relay/relay.yaml | 117 - scenario-runner/Dockerfile | 5 +- scenario-runner/child_benefit_federator.py | 35 +- scenario-runner/test_server.py | 380 ++- scenarios/child_benefit.py | 28 +- scenarios/citizen.py | 30 +- scenarios/common.py | 462 +++- scenarios/farmer_voucher.py | 11 +- scenarios/pension_survivor.py | 29 +- scenarios/service_config.py | 107 +- scripts/build-registry-stack-runtime.sh | 106 +- scripts/build-relay-runtime.sh | 5 - scripts/check-config-secrets.py | 4 +- scripts/check-coolify-compose.sh | 84 +- scripts/check-evidence-cells.sh | 24 + scripts/check-evidence-runtime.py | 121 - scripts/check-hosted-compose.sh | 20 + scripts/check-hosted-relay-bundles.py | 230 -- scripts/check-image-pins.py | 17 +- scripts/check-local-transit-providers.py | 38 + scripts/check-registry-stack-release-pin.py | 95 + scripts/check-relay-projects.sh | 58 + scripts/check-release-pins.py | 173 -- scripts/check-runtime-topology.py | 57 + scripts/check-signer-public-keys.py | 45 + scripts/contract-generation-proof.py | 607 ----- scripts/gen-secrets.py | 517 ++--- scripts/generate-hosted-relay-bundles.py | 280 --- scripts/hosted-authority-rollout.py | 37 + scripts/hosted-image-manifest.py | 198 ++ scripts/hosted-runtime-assets.py | 485 ++++ scripts/hosted-transit-signer.py | 375 +++ scripts/init-notary-postgresql.sh | 360 --- scripts/init-registry-postgresql.sh | 99 - scripts/lifecycle_proof.py | 378 +++ scripts/live-lifecycle-proof.py | 839 +++++++ scripts/local-relay-source-publisher.py | 451 ++++ scripts/local-transit-proxy.py | 421 ++++ scripts/local-transit-signers.py | 150 ++ scripts/notary_state_restart.py | 560 ----- scripts/postgres-entrypoint.sh | 40 - scripts/prepare-authority-runtime.sh | 32 + scripts/project-runtime-secrets.py | 37 + scripts/provision-hosted-runtime.py | 800 +++++++ scripts/publish-metadata.py | 378 ++- scripts/publish-runtime-extracts.py | 228 ++ scripts/registry-projects.sh | 179 -- scripts/registryctl-build-output.py | 115 - scripts/registryctl-pinned.sh | 62 - scripts/registryctl-test-output.py | 106 - scripts/relay_workload_identity_agent.py | 754 ------ scripts/review.sh | 36 - scripts/seed-esignet.py | 7 +- scripts/smoke-child-benefit-application.py | 257 --- scripts/smoke-esignet-login.mjs | 67 + scripts/smoke-esignet.py | 9 + scripts/smoke-hosted-provisioner-image.py | 206 ++ scripts/smoke-hosted.py | 569 ----- scripts/smoke-live.py | 198 -- scripts/smoke-nia-attribute-release.py | 99 - scripts/smoke-programme-acceptance.py | 341 +++ scripts/smoke-published-tokens.py | 123 - scripts/smoke-relay-sources.py | 243 -- scripts/smoke-story-previews.py | 89 - scripts/smoke.sh | 62 +- scripts/start-esignet-relay.sh | 13 + scripts/test_contract_generation_proof.py | 316 --- scripts/test_gen_secrets.py | 157 +- scripts/test_hosted_authority_rollout.py | 27 + scripts/test_hosted_home_topology.py | 99 + scripts/test_hosted_image_manifest.py | 298 +++ scripts/test_hosted_provisioning_topology.py | 168 ++ scripts/test_hosted_relay_bundles.py | 174 -- scripts/test_hosted_runtime_assets.py | 225 ++ scripts/test_hosted_transit_signer.py | 364 +++ scripts/test_image_pins.py | 19 + scripts/test_lifecycle_proof.py | 171 ++ scripts/test_live_lifecycle_proof.py | 509 ++++ scripts/test_local_relay_source_publisher.py | 279 +++ scripts/test_local_transit_providers.py | 39 + scripts/test_local_transit_proxy.py | 325 +++ scripts/test_local_transit_signers.py | 37 + scripts/test_metadata_authority_contracts.py | 61 +- scripts/test_notary_state_restart.py | 486 ---- scripts/test_project_runtime_secrets.py | 41 + scripts/test_provision_hosted_runtime.py | 341 +++ scripts/test_publish_runtime_extracts.py | 280 +++ scripts/test_quality_scripts.py | 2039 ----------------- scripts/test_registry_stack_release_pin.py | 70 + scripts/test_registryctl_build_output.py | 133 -- scripts/test_registryctl_test_output.py | 91 - scripts/test_relay_workload_identity_agent.py | 591 ----- scripts/test_release_pins.py | 321 --- scripts/test_runtime_topology.py | 404 ++++ scripts/test_signer_public_keys.py | 40 + .../test_smoke_child_benefit_application.py | 160 -- scripts/test_smoke_esignet.py | 96 +- scripts/test_smoke_hosted.py | 287 --- .../test_smoke_hosted_provisioner_image.py | 62 + scripts/test_smoke_nia_attribute_release.py | 109 - scripts/test_smoke_programme_acceptance.py | 306 +++ scripts/test_smoke_relay_sources.py | 66 - versions.env | 47 +- 792 files changed, 22084 insertions(+), 44424 deletions(-) create mode 100644 compose.coolify.provision.yaml create mode 100644 config/evidence/Caddyfile delete mode 100644 config/hosted-relay-bundles/cra-civil/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/cra-civil/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/cra-civil/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/cra-civil/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-child-benefit-civil.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-citizen-record-civil.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-pension-death-civil.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/conformance.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/minimization.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/negative-security.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/integration-packs/civil-person-by-uin.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-child-benefit-civil.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-citizen-record-civil.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-pension-death-civil.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/cra-civil/consultation/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/cra-civil/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/consultation-contracts/child-benefit-enrollment.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/conformance.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/minimization.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/negative-security.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/integration-packs/enrollment-by-uin.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/private-bindings/child-benefit-enrollment.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/consultation/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/mosd-programme/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/consultation-contracts/livestock-herd.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/consultation-contracts/voucher-farmer.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/conformance.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/minimization.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/conformance.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/minimization.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/negative-security.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/integration-packs/farmer-voucher-by-farmer.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/integration-packs/livestock-movement-by-farmer.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/private-bindings/livestock-herd.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/private-bindings/voucher-farmer.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/consultation/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/nia-population/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/nia-population/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/nia-population/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/nia-population/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/nia-population/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/consultation-contracts/nia-child-benefit-population.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/consultation-contracts/nia-citizen-status-population.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/conformance.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/minimization.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/negative-security.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/integration-packs/population-person-by-uin.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/private-bindings/nia-child-benefit-population.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/private-bindings/nia-citizen-status-population.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/nia-population/consultation/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/nia-population/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/conformance.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/minimization.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/negative-security.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/conformance.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/minimization.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/negative-security.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/integration-packs/pension-case-by-pensioner.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/integration-packs/pension-case-by-survivor.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/private-bindings/sipf-pension-payment-review-pension.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/private-bindings/sipf-survivor-benefit-survivor.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/consultation/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/sipf-pensions/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/sro-social/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/sro-social/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/sro-social/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/sro-social/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/sro-social/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/antirollback-seed.json delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/bootstrap.yaml delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/consultation-contracts/child-benefit-household.json delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/conformance.json delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/minimization.json delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/negative-security.json delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/integration-packs/child-benefit-household-by-uin.json delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/private-bindings/child-benefit-household.json delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/bundle/config/relay.yaml delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/bundle/manifest.json delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/bundle/manifest.sig.json delete mode 100644 config/hosted-relay-bundles/sro-social/consultation/trust-anchor.json delete mode 100644 config/hosted-relay-bundles/sro-social/trust-anchor.json create mode 100644 docker/hosted-authority-provisioner/Dockerfile create mode 100644 docker/hosted-transit-signer/Dockerfile delete mode 100644 docker/notary/Dockerfile delete mode 100644 docker/postgres/Dockerfile create mode 100644 docker/registry-stack-release-binary/Dockerfile delete mode 100644 docker/relay-runtime/Dockerfile delete mode 100644 docker/relay/Dockerfile create mode 100644 docs/esignet.md delete mode 100644 docs/notary-postgresql-state.md create mode 100644 docs/source-recovery.md delete mode 100644 docs/wallet-oid4vci.md delete mode 100644 evidence/Caddyfile delete mode 100644 evidence/bundle/adapters/extract.rhai delete mode 100644 evidence/bundle/adapters/prepare.rhai delete mode 100644 evidence/bundle/derivations/cra-child-benefit.rhai delete mode 100644 evidence/bundle/derivations/population-active.rhai delete mode 100644 evidence/bundle/derivations/voucher.rhai delete mode 100644 evidence/bundle/evidence.yaml delete mode 100644 evidence/bundle/fixtures/cra-child-benefit.yaml delete mode 100644 evidence/bundle/fixtures/cra-citizen.yaml delete mode 100644 evidence/bundle/fixtures/cra-pension.yaml delete mode 100644 evidence/bundle/fixtures/mosd-child-benefit.yaml delete mode 100644 evidence/bundle/fixtures/nagdi-livestock.yaml delete mode 100644 evidence/bundle/fixtures/nagdi-voucher.yaml delete mode 100644 evidence/bundle/fixtures/nia-child-benefit.yaml delete mode 100644 evidence/bundle/fixtures/nia-citizen.yaml delete mode 100644 evidence/bundle/fixtures/sipf-pension.yaml delete mode 100644 evidence/bundle/fixtures/sipf-survivor.yaml delete mode 100644 evidence/bundle/fixtures/sro-child-benefit.yaml delete mode 100644 evidence/bundle/schemas/adapter-parameters.schema.yaml delete mode 100644 evidence/bundle/schemas/cra-child-benefit-facts.schema.yaml delete mode 100644 evidence/bundle/schemas/cra-citizen-facts.schema.yaml delete mode 100644 evidence/bundle/schemas/cra-pension-facts.schema.yaml delete mode 100644 evidence/bundle/schemas/mosd-child-benefit-facts.schema.yaml delete mode 100644 evidence/bundle/schemas/nagdi-livestock-facts.schema.yaml delete mode 100644 evidence/bundle/schemas/nagdi-voucher-facts.schema.yaml delete mode 100644 evidence/bundle/schemas/nia-population-facts.schema.yaml delete mode 100644 evidence/bundle/schemas/response.schema.yaml delete mode 100644 evidence/bundle/schemas/sipf-pension-facts.schema.yaml delete mode 100644 evidence/bundle/schemas/sipf-survivor-facts.schema.yaml delete mode 100644 evidence/bundle/schemas/sro-child-benefit-facts.schema.yaml create mode 100644 evidence/cells/cra/bundle/adapters/birth-extract.rhai create mode 100644 evidence/cells/cra/bundle/adapters/relay-extract.rhai create mode 100644 evidence/cells/cra/bundle/adapters/relay-prepare.rhai create mode 100644 evidence/cells/cra/bundle/derivations/child-benefit.rhai rename evidence/{bundle/derivations/cra-deceased.rhai => cells/cra/bundle/derivations/deceased.rhai} (57%) rename evidence/{bundle/derivations/cra-linked.rhai => cells/cra/bundle/derivations/linked.rhai} (51%) create mode 100644 evidence/cells/cra/bundle/evidence.yaml create mode 100644 evidence/cells/cra/bundle/fixtures/child-benefit.yaml create mode 100644 evidence/cells/cra/bundle/fixtures/citizen.yaml create mode 100644 evidence/cells/cra/bundle/fixtures/pension.yaml create mode 100644 evidence/cells/cra/bundle/queries/birth-evidence.sql create mode 100644 evidence/cells/cra/bundle/schemas/birth-facts.schema.yaml create mode 100644 evidence/cells/cra/bundle/schemas/birth-response.schema.yaml create mode 100644 evidence/cells/cra/bundle/schemas/deceased-facts.schema.yaml create mode 100644 evidence/cells/cra/bundle/schemas/deceased-response.schema.yaml create mode 100644 evidence/cells/cra/bundle/schemas/linked-facts.schema.yaml create mode 100644 evidence/cells/cra/bundle/schemas/linked-response.schema.yaml create mode 100644 evidence/cells/cra/bundle/schemas/relay-adapter-parameters.schema.yaml create mode 100644 evidence/cells/cra/runtime.yaml create mode 100644 evidence/cells/mosd-programme/bundle/adapters/relay-extract.rhai create mode 100644 evidence/cells/mosd-programme/bundle/adapters/relay-prepare.rhai rename evidence/{ => cells/mosd-programme}/bundle/derivations/not-enrolled.rhai (59%) create mode 100644 evidence/cells/mosd-programme/bundle/evidence.yaml create mode 100644 evidence/cells/mosd-programme/bundle/fixtures/child-benefit.yaml create mode 100644 evidence/cells/mosd-programme/bundle/schemas/facts.schema.yaml create mode 100644 evidence/cells/mosd-programme/bundle/schemas/relay-adapter-parameters.schema.yaml create mode 100644 evidence/cells/mosd-programme/bundle/schemas/response.schema.yaml create mode 100644 evidence/cells/mosd-programme/runtime.yaml create mode 100644 evidence/cells/nagdi/bundle/adapters/relay-extract.rhai create mode 100644 evidence/cells/nagdi/bundle/adapters/relay-prepare.rhai rename evidence/{ => cells/nagdi}/bundle/derivations/livestock.rhai (57%) create mode 100644 evidence/cells/nagdi/bundle/derivations/voucher.rhai create mode 100644 evidence/cells/nagdi/bundle/evidence.yaml create mode 100644 evidence/cells/nagdi/bundle/fixtures/livestock.yaml create mode 100644 evidence/cells/nagdi/bundle/fixtures/voucher.yaml create mode 100644 evidence/cells/nagdi/bundle/schemas/livestock-facts.schema.yaml create mode 100644 evidence/cells/nagdi/bundle/schemas/livestock-response.schema.yaml create mode 100644 evidence/cells/nagdi/bundle/schemas/relay-adapter-parameters.schema.yaml create mode 100644 evidence/cells/nagdi/bundle/schemas/voucher-facts.schema.yaml create mode 100644 evidence/cells/nagdi/bundle/schemas/voucher-response.schema.yaml create mode 100644 evidence/cells/nagdi/runtime.yaml create mode 100644 evidence/cells/nia/bundle/adapters/sqlite-extract.rhai create mode 100644 evidence/cells/nia/bundle/derivations/population-active.rhai create mode 100644 evidence/cells/nia/bundle/evidence.yaml create mode 100644 evidence/cells/nia/bundle/fixtures/child-benefit.yaml create mode 100644 evidence/cells/nia/bundle/fixtures/citizen.yaml create mode 100644 evidence/cells/nia/bundle/queries/population-evidence.sql create mode 100644 evidence/cells/nia/bundle/schemas/population-facts.schema.yaml create mode 100644 evidence/cells/nia/bundle/schemas/population-response.schema.yaml create mode 100644 evidence/cells/nia/runtime.yaml create mode 100644 evidence/cells/sipf/bundle/adapters/relay-extract.rhai create mode 100644 evidence/cells/sipf/bundle/adapters/relay-prepare.rhai rename evidence/{ => cells/sipf}/bundle/derivations/pension-active.rhai (58%) rename evidence/{ => cells/sipf}/bundle/derivations/survivor-eligible.rhai (58%) create mode 100644 evidence/cells/sipf/bundle/evidence.yaml create mode 100644 evidence/cells/sipf/bundle/fixtures/pension.yaml create mode 100644 evidence/cells/sipf/bundle/fixtures/survivor.yaml create mode 100644 evidence/cells/sipf/bundle/schemas/pension-facts.schema.yaml create mode 100644 evidence/cells/sipf/bundle/schemas/pension-response.schema.yaml create mode 100644 evidence/cells/sipf/bundle/schemas/relay-adapter-parameters.schema.yaml create mode 100644 evidence/cells/sipf/bundle/schemas/survivor-facts.schema.yaml create mode 100644 evidence/cells/sipf/bundle/schemas/survivor-response.schema.yaml create mode 100644 evidence/cells/sipf/runtime.yaml create mode 100644 evidence/cells/sro/bundle/adapters/sqlite-extract.rhai rename evidence/{ => cells/sro}/bundle/derivations/poverty-priority.rhai (61%) create mode 100644 evidence/cells/sro/bundle/evidence.yaml create mode 100644 evidence/cells/sro/bundle/fixtures/child-benefit.yaml create mode 100644 evidence/cells/sro/bundle/queries/poverty-evidence.sql create mode 100644 evidence/cells/sro/bundle/schemas/poverty-facts.schema.yaml create mode 100644 evidence/cells/sro/bundle/schemas/poverty-response.schema.yaml create mode 100644 evidence/cells/sro/runtime.yaml delete mode 100644 evidence/runtime.yaml create mode 100644 evidence/scripts/build-cells.py create mode 100644 evidence/tests/test_cells.py create mode 100644 generator/solmara_lab/publisher.py create mode 100644 generator/tests/test_publisher.py delete mode 100644 metadata/public/metadata/evidence-offerings/cra-birth-registration-offering.json create mode 100644 metadata/public/metadata/evidence-offerings/cra-child-benefit-v1-offering.json create mode 100644 metadata/public/metadata/evidence-offerings/cra-citizen-record-v1-offering.json rename metadata/public/metadata/evidence-offerings/{cra-death-registration-offering.json => cra-pension-death-v1-offering.json} (51%) create mode 100644 metadata/public/metadata/evidence-offerings/mosd-child-benefit-v1-offering.json delete mode 100644 metadata/public/metadata/evidence-offerings/mosd-programme-beneficiary-enrollment-offering.json delete mode 100644 metadata/public/metadata/evidence-offerings/nagdi-agriculture-farmer-voucher-offering.json delete mode 100644 metadata/public/metadata/evidence-offerings/nagdi-agriculture-livestock-movement-offering.json create mode 100644 metadata/public/metadata/evidence-offerings/nagdi-livestock-v1-offering.json create mode 100644 metadata/public/metadata/evidence-offerings/nagdi-voucher-v1-offering.json create mode 100644 metadata/public/metadata/evidence-offerings/nia-child-benefit-v1-offering.json create mode 100644 metadata/public/metadata/evidence-offerings/nia-citizen-status-v1-offering.json delete mode 100644 metadata/public/metadata/evidence-offerings/nia-population-population-status-offering.json create mode 100644 metadata/public/metadata/evidence-offerings/sipf-pension-payment-v1-offering.json delete mode 100644 metadata/public/metadata/evidence-offerings/sipf-pensions-pension-case-offering.json create mode 100644 metadata/public/metadata/evidence-offerings/sipf-survivor-benefit-v1-offering.json delete mode 100644 metadata/public/metadata/evidence-offerings/solmara.child-benefit.authority-predicate-collection.json create mode 100644 metadata/public/metadata/evidence-offerings/sro-child-benefit-v1-offering.json delete mode 100644 metadata/public/metadata/evidence-offerings/sro-social-household-poverty-offering.json rename metadata/public/metadata/policies/{cra-birth-registration-offering-policy.jsonld => cra-child-benefit-v1-offering-policy.jsonld} (73%) create mode 100644 metadata/public/metadata/policies/cra-citizen-record-v1-offering-policy.jsonld rename metadata/public/metadata/policies/{cra-death-registration-offering-policy.jsonld => cra-pension-death-v1-offering-policy.jsonld} (73%) rename metadata/public/metadata/policies/{sro-social-household-poverty-offering-policy.jsonld => mosd-child-benefit-v1-offering-policy.jsonld} (71%) rename metadata/public/metadata/policies/{nagdi-agriculture-livestock-movement-offering-policy.jsonld => nagdi-livestock-v1-offering-policy.jsonld} (69%) rename metadata/public/metadata/policies/{nagdi-agriculture-farmer-voucher-offering-policy.jsonld => nagdi-voucher-v1-offering-policy.jsonld} (70%) rename metadata/public/metadata/policies/{nia-population-population-status-offering-policy.jsonld => nia-child-benefit-v1-offering-policy.jsonld} (69%) create mode 100644 metadata/public/metadata/policies/nia-citizen-status-v1-offering-policy.jsonld rename metadata/public/metadata/policies/{sipf-pensions-pension-case-offering-policy.jsonld => sipf-pension-payment-v1-offering-policy.jsonld} (69%) create mode 100644 metadata/public/metadata/policies/sipf-survivor-benefit-v1-offering-policy.jsonld delete mode 100644 metadata/public/metadata/policies/solmara-child-benefit-authority-predicate-collection-policy.jsonld rename metadata/public/metadata/policies/{mosd-programme-beneficiary-enrollment-offering-policy.jsonld => sro-child-benefit-v1-offering-policy.jsonld} (68%) delete mode 100644 perf/README.md delete mode 100644 perf/k6/lib/common.js delete mode 100644 perf/k6/notary_relay_backed.js delete mode 100644 projects/cra-civil/.registry-stack-editor/manifest.json delete mode 100644 projects/cra-civil/.registry-stack-editor/schemas/entity.schema.json delete mode 100644 projects/cra-civil/.registry-stack-editor/schemas/environment.schema.json delete mode 100644 projects/cra-civil/.registry-stack-editor/schemas/fixture.schema.json delete mode 100644 projects/cra-civil/.registry-stack-editor/schemas/integration.schema.json delete mode 100644 projects/cra-civil/.registry-stack-editor/schemas/project.schema.json delete mode 100644 projects/cra-civil/.vscode/extensions.json delete mode 100644 projects/cra-civil/.vscode/settings.json delete mode 100644 projects/cra-civil/.zed/settings.json delete mode 100644 projects/cra-civil/entities/civil_people.yaml delete mode 100644 projects/cra-civil/environments/hosted.yaml delete mode 100644 projects/cra-civil/environments/local.yaml delete mode 100644 projects/cra-civil/integrations/civil-person-by-uin/fixtures/future-birth-date.yaml delete mode 100644 projects/cra-civil/integrations/civil-person-by-uin/fixtures/match.yaml delete mode 100644 projects/cra-civil/integrations/civil-person-by-uin/fixtures/no-match.yaml delete mode 100644 projects/cra-civil/integrations/civil-person-by-uin/integration.yaml delete mode 100644 projects/cra-civil/registry-stack.yaml delete mode 100644 projects/mosd-programme/.registry-stack-editor/manifest.json delete mode 100644 projects/mosd-programme/.registry-stack-editor/schemas/entity.schema.json delete mode 100644 projects/mosd-programme/.registry-stack-editor/schemas/environment.schema.json delete mode 100644 projects/mosd-programme/.registry-stack-editor/schemas/fixture.schema.json delete mode 100644 projects/mosd-programme/.registry-stack-editor/schemas/integration.schema.json delete mode 100644 projects/mosd-programme/.registry-stack-editor/schemas/project.schema.json delete mode 100644 projects/mosd-programme/.vscode/extensions.json delete mode 100644 projects/mosd-programme/.vscode/settings.json delete mode 100644 projects/mosd-programme/.zed/settings.json delete mode 100644 projects/mosd-programme/entities/mosd_enroll.yaml delete mode 100644 projects/mosd-programme/environments/hosted.yaml delete mode 100644 projects/mosd-programme/environments/local.yaml delete mode 100644 projects/mosd-programme/integrations/enrollment-by-uin/fixtures/match.yaml delete mode 100644 projects/mosd-programme/integrations/enrollment-by-uin/fixtures/no-match.yaml delete mode 100644 projects/mosd-programme/integrations/enrollment-by-uin/integration.yaml delete mode 100644 projects/mosd-programme/registry-stack.yaml delete mode 100644 projects/nagdi-agriculture/.registry-stack-editor/manifest.json delete mode 100644 projects/nagdi-agriculture/.registry-stack-editor/schemas/entity.schema.json delete mode 100644 projects/nagdi-agriculture/.registry-stack-editor/schemas/environment.schema.json delete mode 100644 projects/nagdi-agriculture/.registry-stack-editor/schemas/fixture.schema.json delete mode 100644 projects/nagdi-agriculture/.registry-stack-editor/schemas/integration.schema.json delete mode 100644 projects/nagdi-agriculture/.registry-stack-editor/schemas/project.schema.json delete mode 100644 projects/nagdi-agriculture/.vscode/extensions.json delete mode 100644 projects/nagdi-agriculture/.vscode/settings.json delete mode 100644 projects/nagdi-agriculture/.zed/settings.json delete mode 100644 projects/nagdi-agriculture/entities/nagdi_farmer.yaml delete mode 100644 projects/nagdi-agriculture/entities/nagdi_herd.yaml delete mode 100644 projects/nagdi-agriculture/environments/hosted.yaml delete mode 100644 projects/nagdi-agriculture/environments/local.yaml delete mode 100644 projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/match.yaml delete mode 100644 projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/no-match.yaml delete mode 100644 projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/integration.yaml delete mode 100644 projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/ambiguous.yaml delete mode 100644 projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/match.yaml delete mode 100644 projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/no-match.yaml delete mode 100644 projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/integration.yaml delete mode 100644 projects/nagdi-agriculture/registry-stack.yaml delete mode 100644 projects/nia-population/.registry-stack-editor/manifest.json delete mode 100644 projects/nia-population/.registry-stack-editor/schemas/entity.schema.json delete mode 100644 projects/nia-population/.registry-stack-editor/schemas/environment.schema.json delete mode 100644 projects/nia-population/.registry-stack-editor/schemas/fixture.schema.json delete mode 100644 projects/nia-population/.registry-stack-editor/schemas/integration.schema.json delete mode 100644 projects/nia-population/.registry-stack-editor/schemas/project.schema.json delete mode 100644 projects/nia-population/.vscode/extensions.json delete mode 100644 projects/nia-population/.vscode/settings.json delete mode 100644 projects/nia-population/.zed/settings.json delete mode 100644 projects/nia-population/entities/population.yaml delete mode 100644 projects/nia-population/environments/hosted.yaml delete mode 100644 projects/nia-population/environments/local.yaml delete mode 100644 projects/nia-population/integrations/population-person-by-uin/fixtures/match.yaml delete mode 100644 projects/nia-population/integrations/population-person-by-uin/fixtures/no-match.yaml delete mode 100644 projects/nia-population/integrations/population-person-by-uin/integration.yaml delete mode 100644 projects/nia-population/registry-stack.yaml delete mode 100644 projects/sipf-pensions/.registry-stack-editor/manifest.json delete mode 100644 projects/sipf-pensions/.registry-stack-editor/schemas/entity.schema.json delete mode 100644 projects/sipf-pensions/.registry-stack-editor/schemas/environment.schema.json delete mode 100644 projects/sipf-pensions/.registry-stack-editor/schemas/fixture.schema.json delete mode 100644 projects/sipf-pensions/.registry-stack-editor/schemas/integration.schema.json delete mode 100644 projects/sipf-pensions/.registry-stack-editor/schemas/project.schema.json delete mode 100644 projects/sipf-pensions/.vscode/extensions.json delete mode 100644 projects/sipf-pensions/.vscode/settings.json delete mode 100644 projects/sipf-pensions/.zed/settings.json delete mode 100644 projects/sipf-pensions/entities/pension.yaml delete mode 100644 projects/sipf-pensions/entities/survivor.yaml delete mode 100644 projects/sipf-pensions/environments/hosted.yaml delete mode 100644 projects/sipf-pensions/environments/local.yaml delete mode 100644 projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/ambiguous.yaml delete mode 100644 projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/match.yaml delete mode 100644 projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/no-match.yaml delete mode 100644 projects/sipf-pensions/integrations/pension-case-by-pensioner/integration.yaml delete mode 100644 projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/ambiguous.yaml delete mode 100644 projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/match.yaml delete mode 100644 projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/no-match.yaml delete mode 100644 projects/sipf-pensions/integrations/pension-case-by-survivor/integration.yaml delete mode 100644 projects/sipf-pensions/registry-stack.yaml delete mode 100644 projects/sro-social/.registry-stack-editor/manifest.json delete mode 100644 projects/sro-social/.registry-stack-editor/schemas/entity.schema.json delete mode 100644 projects/sro-social/.registry-stack-editor/schemas/environment.schema.json delete mode 100644 projects/sro-social/.registry-stack-editor/schemas/fixture.schema.json delete mode 100644 projects/sro-social/.registry-stack-editor/schemas/integration.schema.json delete mode 100644 projects/sro-social/.registry-stack-editor/schemas/project.schema.json delete mode 100644 projects/sro-social/.vscode/extensions.json delete mode 100644 projects/sro-social/.vscode/settings.json delete mode 100644 projects/sro-social/.zed/settings.json delete mode 100644 projects/sro-social/entities/sro_child.yaml delete mode 100644 projects/sro-social/environments/hosted.yaml delete mode 100644 projects/sro-social/environments/local.yaml delete mode 100644 projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/match.yaml delete mode 100644 projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/no-match.yaml delete mode 100644 projects/sro-social/integrations/child-benefit-household-by-uin/integration.yaml delete mode 100644 projects/sro-social/registry-stack.yaml create mode 100644 relays/cra/codelists/record-lifecycle.yaml create mode 100644 relays/cra/expected-http.yaml create mode 100644 relays/cra/fixture.sql create mode 100644 relays/cra/governance/classification-review-rationale.md create mode 100644 relays/cra/governance/classification-review.yaml create mode 100644 relays/cra/governance/identifier-lifecycle.yaml create mode 100644 relays/cra/governance/legal-basis.yaml create mode 100644 relays/cra/governance/processing.dpv.yaml create mode 100644 relays/cra/registry.yaml create mode 100644 relays/cra/runtime.yaml create mode 100644 relays/mosd/codelists/record-lifecycle.yaml create mode 100644 relays/mosd/expected-http.yaml create mode 100644 relays/mosd/fixture.sql create mode 100644 relays/mosd/governance/classification-review-rationale.md create mode 100644 relays/mosd/governance/classification-review.yaml create mode 100644 relays/mosd/governance/identifier-lifecycle.yaml create mode 100644 relays/mosd/governance/legal-basis.yaml create mode 100644 relays/mosd/governance/processing.dpv.yaml create mode 100644 relays/mosd/registry.yaml create mode 100644 relays/mosd/runtime.yaml create mode 100644 relays/nagdi/codelists/record-lifecycle.yaml create mode 100644 relays/nagdi/expected-http.yaml create mode 100644 relays/nagdi/fixture.sql create mode 100644 relays/nagdi/governance/classification-review-rationale.md create mode 100644 relays/nagdi/governance/classification-review.yaml create mode 100644 relays/nagdi/governance/identifier-lifecycle.yaml create mode 100644 relays/nagdi/governance/legal-basis.yaml create mode 100644 relays/nagdi/governance/processing.dpv.yaml create mode 100644 relays/nagdi/registry.yaml create mode 100644 relays/nagdi/runtime.yaml create mode 100644 relays/nia/codelists/identity-status.yaml create mode 100644 relays/nia/codelists/record-lifecycle.yaml create mode 100644 relays/nia/codelists/sex.yaml create mode 100644 relays/nia/expected-http.yaml create mode 100644 relays/nia/fixture.sql create mode 100644 relays/nia/governance/classification-review-rationale.md create mode 100644 relays/nia/governance/classification-review.yaml create mode 100644 relays/nia/governance/identifier-lifecycle.yaml create mode 100644 relays/nia/governance/legal-basis.yaml create mode 100644 relays/nia/governance/processing.dpv.yaml create mode 100644 relays/nia/registry.yaml create mode 100644 relays/nia/runtime.yaml create mode 100644 relays/sipf/codelists/payment-status.yaml create mode 100644 relays/sipf/codelists/record-lifecycle.yaml create mode 100644 relays/sipf/expected-http.yaml create mode 100644 relays/sipf/fixture.sql create mode 100644 relays/sipf/governance/classification-review-rationale.md create mode 100644 relays/sipf/governance/classification-review.yaml create mode 100644 relays/sipf/governance/identifier-lifecycle.yaml create mode 100644 relays/sipf/governance/legal-basis.yaml create mode 100644 relays/sipf/governance/processing.dpv.yaml create mode 100644 relays/sipf/registry.yaml create mode 100644 relays/sipf/runtime.yaml create mode 100644 relays/test_relay_projects.py create mode 100644 requests/registry-lab/50 - Relay V2/01 - CRA death lookup.bru create mode 100644 requests/registry-lab/50 - Relay V2/02 - NIA eSignet lookup.bru create mode 100644 requests/registry-lab/50 - Relay V2/03 - MoSD enrolment lookup.bru create mode 100644 requests/registry-lab/50 - Relay V2/04 - SIPF pension lookup.bru create mode 100644 requests/registry-lab/50 - Relay V2/05 - NAgDI voucher lookup.bru create mode 100644 requests/registry-lab/50 - Relay V2/06 - CRA citizen-link lookup.bru create mode 100644 requests/registry-lab/50 - Relay V2/07 - SIPF survivor lookup.bru create mode 100644 requests/registry-lab/50 - Relay V2/08 - NAgDI livestock lookup.bru delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/hosted/cra-civil/relay/relay.yaml delete mode 100644 runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json delete mode 100644 runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json delete mode 100644 runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json delete mode 100644 runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json delete mode 100644 runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json delete mode 100644 runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json delete mode 100644 runtime/registry-projects/hosted/mosd-programme/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/hosted/mosd-programme/relay/relay.yaml delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml delete mode 100644 runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json delete mode 100644 runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json delete mode 100644 runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json delete mode 100644 runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json delete mode 100644 runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json delete mode 100644 runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json delete mode 100644 runtime/registry-projects/hosted/nia-population/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/hosted/nia-population/relay/relay.yaml delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/hosted/sipf-pensions/relay/relay.yaml delete mode 100644 runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json delete mode 100644 runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json delete mode 100644 runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json delete mode 100644 runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json delete mode 100644 runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json delete mode 100644 runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json delete mode 100644 runtime/registry-projects/hosted/sro-social/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/hosted/sro-social/relay/relay.yaml delete mode 100644 runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json delete mode 100644 runtime/registry-projects/local/cra-civil/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/local/cra-civil/relay/relay.yaml delete mode 100644 runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json delete mode 100644 runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json delete mode 100644 runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json delete mode 100644 runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json delete mode 100644 runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json delete mode 100644 runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json delete mode 100644 runtime/registry-projects/local/mosd-programme/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/local/mosd-programme/relay/relay.yaml delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/local/nagdi-agriculture/relay/relay.yaml delete mode 100644 runtime/registry-projects/local/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json delete mode 100644 runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json delete mode 100644 runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json delete mode 100644 runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json delete mode 100644 runtime/registry-projects/local/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json delete mode 100644 runtime/registry-projects/local/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json delete mode 100644 runtime/registry-projects/local/nia-population/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/local/nia-population/relay/relay.yaml delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/local/sipf-pensions/relay/relay.yaml delete mode 100644 runtime/registry-projects/local/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json delete mode 100644 runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json delete mode 100644 runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json delete mode 100644 runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json delete mode 100644 runtime/registry-projects/local/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json delete mode 100644 runtime/registry-projects/local/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json delete mode 100644 runtime/registry-projects/local/sro-social/relay-consultation/relay.yaml delete mode 100644 runtime/registry-projects/local/sro-social/relay/relay.yaml delete mode 100755 scripts/build-relay-runtime.sh create mode 100755 scripts/check-evidence-cells.sh delete mode 100755 scripts/check-evidence-runtime.py create mode 100755 scripts/check-hosted-compose.sh delete mode 100755 scripts/check-hosted-relay-bundles.py create mode 100755 scripts/check-local-transit-providers.py create mode 100755 scripts/check-registry-stack-release-pin.py create mode 100755 scripts/check-relay-projects.sh delete mode 100755 scripts/check-release-pins.py create mode 100755 scripts/check-runtime-topology.py create mode 100755 scripts/check-signer-public-keys.py delete mode 100755 scripts/contract-generation-proof.py delete mode 100755 scripts/generate-hosted-relay-bundles.py create mode 100755 scripts/hosted-authority-rollout.py create mode 100755 scripts/hosted-image-manifest.py create mode 100755 scripts/hosted-runtime-assets.py create mode 100644 scripts/hosted-transit-signer.py delete mode 100755 scripts/init-notary-postgresql.sh delete mode 100755 scripts/init-registry-postgresql.sh create mode 100755 scripts/lifecycle_proof.py create mode 100755 scripts/live-lifecycle-proof.py create mode 100644 scripts/local-relay-source-publisher.py create mode 100755 scripts/local-transit-proxy.py create mode 100644 scripts/local-transit-signers.py delete mode 100644 scripts/notary_state_restart.py delete mode 100755 scripts/postgres-entrypoint.sh create mode 100755 scripts/prepare-authority-runtime.sh create mode 100644 scripts/project-runtime-secrets.py create mode 100644 scripts/provision-hosted-runtime.py create mode 100755 scripts/publish-runtime-extracts.py delete mode 100755 scripts/registry-projects.sh delete mode 100755 scripts/registryctl-build-output.py delete mode 100755 scripts/registryctl-pinned.sh delete mode 100755 scripts/registryctl-test-output.py delete mode 100755 scripts/relay_workload_identity_agent.py delete mode 100755 scripts/review.sh delete mode 100755 scripts/smoke-child-benefit-application.py create mode 100644 scripts/smoke-esignet-login.mjs create mode 100755 scripts/smoke-hosted-provisioner-image.py delete mode 100755 scripts/smoke-hosted.py delete mode 100755 scripts/smoke-live.py delete mode 100755 scripts/smoke-nia-attribute-release.py create mode 100755 scripts/smoke-programme-acceptance.py delete mode 100755 scripts/smoke-published-tokens.py delete mode 100755 scripts/smoke-relay-sources.py delete mode 100755 scripts/smoke-story-previews.py delete mode 100644 scripts/test_contract_generation_proof.py create mode 100644 scripts/test_hosted_authority_rollout.py create mode 100644 scripts/test_hosted_home_topology.py create mode 100644 scripts/test_hosted_image_manifest.py create mode 100644 scripts/test_hosted_provisioning_topology.py delete mode 100644 scripts/test_hosted_relay_bundles.py create mode 100644 scripts/test_hosted_runtime_assets.py create mode 100644 scripts/test_hosted_transit_signer.py create mode 100644 scripts/test_lifecycle_proof.py create mode 100644 scripts/test_live_lifecycle_proof.py create mode 100644 scripts/test_local_relay_source_publisher.py create mode 100644 scripts/test_local_transit_providers.py create mode 100644 scripts/test_local_transit_proxy.py create mode 100644 scripts/test_local_transit_signers.py delete mode 100644 scripts/test_notary_state_restart.py create mode 100644 scripts/test_project_runtime_secrets.py create mode 100644 scripts/test_provision_hosted_runtime.py create mode 100644 scripts/test_publish_runtime_extracts.py delete mode 100644 scripts/test_quality_scripts.py create mode 100644 scripts/test_registry_stack_release_pin.py delete mode 100644 scripts/test_registryctl_build_output.py delete mode 100644 scripts/test_registryctl_test_output.py delete mode 100644 scripts/test_relay_workload_identity_agent.py delete mode 100644 scripts/test_release_pins.py create mode 100644 scripts/test_runtime_topology.py create mode 100644 scripts/test_signer_public_keys.py delete mode 100644 scripts/test_smoke_child_benefit_application.py delete mode 100644 scripts/test_smoke_hosted.py create mode 100644 scripts/test_smoke_hosted_provisioner_image.py delete mode 100644 scripts/test_smoke_nia_attribute_release.py create mode 100644 scripts/test_smoke_programme_acceptance.py delete mode 100644 scripts/test_smoke_relay_sources.py diff --git a/.dockerignore b/.dockerignore index b717487..620c754 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,21 @@ .git .env .env.* +.cache +**/.cache +config/evidence/local +runtime +**/runtime +output +**/output +!generator/output +!generator/output/** +!output output/* -!output/smoke/ -!output/smoke/** +!output/smoke +output/smoke/* +!output/smoke/.gitkeep +relays/*/package node_modules **/node_modules .svelte-kit @@ -22,5 +34,28 @@ __pycache__ **/__pycache__ .pytest_cache **/.pytest_cache +.ruff_cache +**/.ruff_cache *.pyc *.log +**/*.log +**/*.jsonl +**/*.sqlite +**/*.sqlite-journal +**/*.sqlite-shm +**/*.sqlite-wal +**/secrets +**/secrets/** +**/audit +**/audit/** +**/audits +**/audits/** +**/extracts +**/extracts/** +**/*private*.jwk +**/*private*.jwk.json +**/signing.jwk +**/*.key +**/*.pem +**/*.p12 +**/*.pfx diff --git a/.env.example b/.env.example index 2486e92..7e12e99 100644 --- a/.env.example +++ b/.env.example @@ -3,55 +3,24 @@ COMPOSE_PROJECT_NAME=solmara-lab-replace-me -SOLMARA_POSTGRES_USER=solmara_registry -SOLMARA_POSTGRES_PASSWORD= -SOLMARA_POSTGRES_DB=solmara_lab -NIA_SOURCE_POSTGRES_READER_PASSWORD= -SIPF_SOURCE_POSTGRES_READER_PASSWORD= -SOLMARA_NIA_DATABASE_URL=postgres://solmara_source_nia_reader:replace-me@postgres:5432/solmara_lab?sslmode=require -SOLMARA_SIPF_DATABASE_URL=postgres://solmara_source_sipf_reader:replace-me@postgres:5432/solmara_lab?sslmode=require - -CRA_RELAY_AUDIT_HASH_SECRET= -NIA_RELAY_AUDIT_HASH_SECRET= -SRO_RELAY_AUDIT_HASH_SECRET= -PROGRAMME_RELAY_AUDIT_HASH_SECRET= -SIPF_RELAY_AUDIT_HASH_SECRET= -NAGDI_RELAY_AUDIT_HASH_SECRET= - -CRA_RELAY_WORKLOAD_JWK= -NIA_RELAY_WORKLOAD_JWK= -NIA_ESIGNET_RELAY_WORKLOAD_JWK= -SRO_RELAY_WORKLOAD_JWK= -PROGRAMME_RELAY_WORKLOAD_JWK= -SIPF_RELAY_WORKLOAD_JWK= -NAGDI_RELAY_WORKLOAD_JWK= - +CRA_RELAY_AUDIT_KEY= +NIA_RELAY_AUDIT_KEY= +MOSD_RELAY_AUDIT_KEY= +SIPF_RELAY_AUDIT_KEY= +SIPF_RELAY_CURSOR_KEY= +NAGDI_RELAY_AUDIT_KEY= +NAGDI_RELAY_CURSOR_KEY= CHILD_BENEFIT_FEDERATOR_TOKEN= -CHILD_BENEFIT_FEDERATOR_URL=https://localhost:4341/child-benefit/ -SOLMARA_EVIDENCE_URL=https://localhost:4341 -SOLMARA_MINT_URL=https://localhost:4341 -SOLMARA_MINT_ASSERTION_AUDIENCE=https://mint.evidence.solmara.invalid/token -SOLMARA_EVIDENCE_CLIENT_ID=solmara-demo -SOLMARA_EVIDENCE_CLIENT_KEY=/absolute/path/to/solmara-lab/config/evidence/local/mint/client-private.jwk -SOLMARA_EVIDENCE_CA_BUNDLE=/absolute/path/to/solmara-lab/config/evidence/local/tls/ca.crt +SOLMARA_EVIDENCE_CLIENT_KEY=/absolute/path/to/solmara-lab/config/evidence/local/cells/mint/clients/solmara-demo-client-key PORTAL_SESSION_SECRET= PORTAL_AUTH_PROVIDER=mock # Optional local eSignet profile. SOLMARA_ESIGNET_POSTGRES_PASSWORD= +NIA_ESIGNET_CLIENT_PRIVATE_JWK= REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD= REGISTRY_ESIGNET_KYC_TOKEN_SECRET= REGISTRY_ESIGNET_PSUT_SECRET= PORTAL_ESIGNET_CLIENT_ID=solmara-portal PORTAL_ESIGNET_CLIENT_KEY_ID=solmara-portal-key-1 PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64= -PORTAL_ESIGNET_ISSUER=http://127.0.0.1:4308 -PORTAL_ESIGNET_AUTHORIZATION_ENDPOINT=http://127.0.0.1:4309/authorize -PORTAL_ESIGNET_TOKEN_ENDPOINT=http://esignet:8088/v1/esignet/oauth/v2/token -PORTAL_ESIGNET_CLIENT_ASSERTION_AUDIENCE=http://127.0.0.1:4308/v1/esignet/oauth/v2/token -PORTAL_ESIGNET_USERINFO_ENDPOINT=http://esignet:8088/v1/esignet/oidc/userinfo -PORTAL_ESIGNET_REDIRECT_URI=http://127.0.0.1:4300/auth/callback -PORTAL_ESIGNET_SCOPE=openid profile -PORTAL_ESIGNET_SUBJECT_CLAIM=individual_id -SOLMARA_ESIGNET_PUBLIC_BASE_URL=http://127.0.0.1:4308 -SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL=http://127.0.0.1:4309 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc92583..7f15f2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,43 +7,39 @@ on: - main workflow_dispatch: +permissions: + contents: read + jobs: - checks: + authority-cell-checks: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - - name: Enable pnpm - run: corepack enable - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4.4.0 with: node-version: "22" cache: pnpm cache-dependency-path: portal/pnpm-lock.yaml - name: Install just - uses: extractions/setup-just@v2 - - name: Setup - run: just setup - - name: Publish metadata + run: | + sudo apt-get update + sudo apt-get install --yes just + - name: Install dependencies + run: | + corepack enable + just setup + - name: Generate ignored runtime material + run: just generate + - name: Publish checked-in metadata run: just metadata-publish - - name: Lint + - name: Lint authority-cell configuration run: just lint - - name: Test + - name: Test authority-cell application run: just test - - name: Verify compiler-generated authority projects - run: | - just registry-projects-editor - if [ -n "$(git status --short -- projects)" ]; then - git status --short -- projects - exit 1 - fi - just registry-projects-check - just registry-projects-test - just registry-projects-capabilities - just registry-projects-runtime-check - - name: Generate local secrets - run: just gen-secrets - - name: Compose + - name: Validate and package all Relay V2 projects + run: just relay-check + - name: Validate Compose topology run: just compose - - name: Review - run: just review diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index b26a8fc..a825cf9 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -3,15 +3,6 @@ name: release-candidate on: workflow_dispatch: inputs: - registry_relay_image: - description: Digest-pinned registry-relay image - required: true - registry_notary_image: - description: Digest-pinned registry-notary image - required: true - registry_stack_tag: - description: Registry Stack candidate or release tag matching the supplied image digests - required: true solmara_image_tag: description: Tag for Solmara-owned images. Defaults to the workflow commit SHA. required: false @@ -21,199 +12,337 @@ permissions: packages: write jobs: - smoke: + verify-and-publish: runs-on: ubuntu-latest env: - REGISTRY_RELAY_IMAGE: ${{ inputs.registry_relay_image }} - REGISTRY_NOTARY_IMAGE: ${{ inputs.registry_notary_image }} - REGISTRY_STACK_TAG: ${{ inputs.registry_stack_tag }} SOLMARA_IMAGE_REGISTRY: ghcr.io/registrystack SOLMARA_IMAGE_TAG: ${{ inputs.solmara_image_tag || github.sha }} - POSTGRES_IMAGE: postgres:16-alpine - ESIGNET_POSTGRES_IMAGE: postgres:16 - ESIGNET_BASE_IMAGE: mosipid/esignet-with-plugins:1.8.0 - ESIGNET_UI_IMAGE: mosipid/oidc-ui:1.8.0 - ESIGNET_RELAY_AUTHENTICATOR_REF: 2e7d0e452a778e048f2c259391397d1e1844318e - STATIC_METADATA_IMAGE: python:3.12-alpine steps: - - uses: actions/checkout@v4 - - name: Checkout Registry Stack source - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Read immutable Registry Stack source identity + id: registry-stack + run: | + set -euo pipefail + set -a + source versions.env + set +a + printf 'source_ref=%s\n' "$REGISTRY_STACK_SOURCE_REF" >> "$GITHUB_OUTPUT" + printf 'source_commit=%s\n' "$REGISTRY_STACK_SOURCE_COMMIT" >> "$GITHUB_OUTPUT" + for key in ESIGNET_BASE_IMAGE ESIGNET_POSTGRES_IMAGE ESIGNET_UI_IMAGE \ + ESIGNET_AUTHENTICATOR_JAR_URL ESIGNET_AUTHENTICATOR_JAR_SHA256 \ + REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256 \ + REGISTRY_STACK_RELEASE_MINT_ASSET_URL REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256 \ + REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256; do + value="$(printenv "$key")" + printf '%s=%s\n' "$key" "$value" >> "$GITHUB_ENV" + done + - name: Require published v0.20.1 Registry Stack and authenticator pins + run: scripts/check-registry-stack-release-pin.py --require-public + - name: Checkout the exact Registry Stack source + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: registrystack/registry-stack - ref: ${{ env.REGISTRY_STACK_TAG }} + ref: ${{ steps.registry-stack.outputs.source_commit }} path: vendor/registry-stack - - name: Verify Registry Stack source + persist-credentials: false + - name: Verify Registry Stack source and published runtime artifacts run: | set -euo pipefail set -a source versions.env set +a - test "$REGISTRY_STACK_SOURCE_REF" = "$REGISTRY_STACK_TAG" test "$(git -C vendor/registry-stack rev-parse HEAD)" = "$REGISTRY_STACK_SOURCE_COMMIT" - { - printf 'SOLMARA_RELAY_BUNDLE_SEQUENCE=%s\n' "$SOLMARA_RELAY_BUNDLE_SEQUENCE" - printf 'VOLUME_INIT_IMAGE=%s\n' "$VOLUME_INIT_IMAGE" - } >> "$GITHUB_ENV" - - name: Checkout eSignet relay authenticator - uses: actions/checkout@v4 - with: - repository: jeremi/esignet-relay-authenticator - ref: ${{ env.ESIGNET_RELAY_AUTHENTICATOR_REF }} - path: vendor/esignet-relay-authenticator - ssh-key: ${{ secrets.ESIGNET_RELAY_AUTHENTICATOR_SSH_KEY }} - - uses: astral-sh/setup-uv@v5 - - uses: actions/setup-node@v4 + relay_image="ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST}" + docker pull "$relay_image" + revision="$(docker image inspect "$relay_image" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}')" + test "$revision" = "$REGISTRY_STACK_SOURCE_COMMIT" + for component in evidence mint relayctl; do + url_variable="REGISTRY_STACK_RELEASE_${component^^}_ASSET_URL" + sha_variable="REGISTRY_STACK_RELEASE_${component^^}_ASSET_SHA256" + curl --fail --location --silent --show-error "${!url_variable}" --output "$component-v0.20.1" + printf '%s %s\n' "${!sha_variable}" "$component-v0.20.1" | sha256sum --check --status + rm -f "$component-v0.20.1" + done + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4.4.0 with: node-version: "22" - - uses: docker/setup-buildx-action@v3 - - name: Install tools + cache: pnpm + cache-dependency-path: portal/pnpm-lock.yaml + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v3.11.1 + - name: Install just run: | - corepack enable sudo apt-get update - sudo apt-get install -y just - - name: Setup - run: just setup - - name: Install Playwright browser + sudo apt-get install --yes just + - name: Install dependencies + run: | + corepack enable + just setup + - name: Install browser test dependencies run: | cd portal && pnpm exec playwright install --with-deps chromium cd ../home && pnpm exec playwright install chromium - - name: Generate fixtures and secrets + - name: Generate and validate authority-cell runtime + env: + REGISTRY_STACK_SOURCE_DIR: ${{ github.workspace }}/vendor/registry-stack run: | - just generate + just prepare just metadata-publish - - name: Validate - run: | just lint just test - just registry-projects-editor - if [ -n "$(git status --short -- projects)" ]; then - git status --short -- projects - exit 1 - fi - just registry-projects-check - just registry-projects-test - just registry-projects-capabilities - just registry-projects-runtime-check just compose - just review-release "$REGISTRY_STACK_TAG" - - name: Prove SRO contract generation transition - run: just contract-generation-proof - - name: Start stack - run: just up - - name: Smoke and PostgreSQL restart persistence - run: just smoke - - name: Portal live e2e - run: just portal-live-e2e - - name: Visitor center e2e - run: just home-live-e2e - - name: Login to GHCR - uses: docker/login-action@v3 + just relay-check + - name: Start and smoke authority cells + env: + REGISTRY_STACK_SOURCE_DIR: ${{ github.workspace }}/vendor/registry-stack + run: | + just up + just smoke + just evidence-check + just programme-acceptance + just lifecycle-proof + just portal-live-e2e + just home-live-e2e + - name: Record recoverable rollout phases + run: | + just rollout side-by-side + just rollout switch + just rollout disable + - name: Log in to GHCR + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push hosted Relay image - uses: docker/build-push-action@v6 + - name: Require pre-provisioned public Solmara packages + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + for package in \ + solmara-lab-evidence \ + solmara-lab-mint \ + solmara-lab-authority-provisioner \ + solmara-lab-transit-signer; do + visibility="$(gh api "/orgs/registrystack/packages/container/${package}" --jq .visibility)" + if [ "$visibility" != public ]; then + echo "$package must be pre-provisioned as a public package with Actions write access" >&2 + exit 1 + fi + done + - name: Build and push verified Evidence release binary + id: evidence + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . - file: docker/relay/Dockerfile + file: docker/registry-stack-release-binary/Dockerfile + target: evidence platforms: linux/amd64 push: true - tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay:${{ env.SOLMARA_IMAGE_TAG }} + tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-evidence:${{ env.SOLMARA_IMAGE_TAG }} + labels: | + org.opencontainers.image.revision=${{ steps.registry-stack.outputs.source_commit }} + org.opencontainers.image.version=0.20.1 + org.opencontainers.image.source=https://github.com/registrystack/registry-stack build-args: | - REGISTRY_RELAY_IMAGE=${{ env.REGISTRY_RELAY_IMAGE }} - VOLUME_INIT_IMAGE=${{ env.VOLUME_INIT_IMAGE }} - - name: Verify hosted Relay signed bundles - env: - SOLMARA_HOSTED_RELAY_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay:${{ env.SOLMARA_IMAGE_TAG }} + REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL=${{ env.REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL }} + REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256=${{ env.REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256 }} + - name: Build and push verified Mint release binary + id: mint + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + context: . + file: docker/registry-stack-release-binary/Dockerfile + target: mint + platforms: linux/amd64 + push: true + tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-mint:${{ env.SOLMARA_IMAGE_TAG }} + labels: | + org.opencontainers.image.revision=${{ steps.registry-stack.outputs.source_commit }} + org.opencontainers.image.version=0.20.1 + org.opencontainers.image.source=https://github.com/registrystack/registry-stack + build-args: | + REGISTRY_STACK_RELEASE_MINT_ASSET_URL=${{ env.REGISTRY_STACK_RELEASE_MINT_ASSET_URL }} + REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256=${{ env.REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256 }} + - name: Verify Solmara Evidence and Mint image source labels run: | set -euo pipefail - for project in cra-civil nia-population sro-social mosd-programme sipf-pensions nagdi-agriculture; do - for bundle in "$project" "$project/consultation"; do - docker run --rm --platform linux/amd64 \ - -e REGISTRY_RELAY_AUDIT_HASH_SECRET=0123456789abcdef0123456789abcdef \ - -e REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1=abcdef0123456789abcdef0123456789 \ - -e REGISTRY_RELAY_CONSULTATION_DATABASE_URL=postgresql://runtime:placeholder@postgres:5432/consultation?sslmode=require \ - -e SOLMARA_NIA_DATABASE_URL=postgresql://reader:placeholder@postgres:5432/source?sslmode=require \ - -e SOLMARA_SIPF_DATABASE_URL=postgresql://reader:placeholder@postgres:5432/source?sslmode=require \ - "$SOLMARA_HOSTED_RELAY_IMAGE" \ - config verify-bundle \ - --bundle-dir "/etc/solmara/hosted-relay-bundles/$bundle/bundle" \ - --anchor-path "/etc/solmara/hosted-relay-bundles/$bundle/trust-anchor.json" \ - --state-path "/etc/solmara/hosted-relay-bundles/$bundle/antirollback-seed.json" \ - | jq -e --argjson expected "$SOLMARA_RELAY_BUNDLE_SEQUENCE" \ - '.result == "verified" and .bundle_sequence == $expected' >/dev/null - done + for component in evidence mint; do + case "$component" in + evidence) digest='${{ steps.evidence.outputs.digest }}' ;; + mint) digest='${{ steps.mint.outputs.digest }}' ;; + esac + image="$SOLMARA_IMAGE_REGISTRY/solmara-lab-${component}@${digest}" + docker pull "$image" + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}')" = '${{ steps.registry-stack.outputs.source_commit }}' + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.version" }}')" = 0.20.1 + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.source" }}')" = https://github.com/registrystack/registry-stack done - - name: Build and push hosted Notary image - uses: docker/build-push-action@v6 + - name: Build and push authority provisioner + id: authority_provisioner + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . - file: docker/notary/Dockerfile + file: docker/hosted-authority-provisioner/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-notary:${{ env.SOLMARA_IMAGE_TAG }} + tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-authority-provisioner:${{ env.SOLMARA_IMAGE_TAG }} + labels: | + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.version=${{ env.SOLMARA_IMAGE_TAG }} + org.opencontainers.image.source=https://github.com/registrystack/solmara-lab + org.registrystack.release.version=0.20.1 + org.registrystack.release.revision=${{ steps.registry-stack.outputs.source_commit }} build-args: | - REGISTRY_NOTARY_IMAGE=${{ env.REGISTRY_NOTARY_IMAGE }} - - name: Build and push hosted Postgres image - uses: docker/build-push-action@v6 + REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=${{ env.REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL }} + REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256=${{ env.REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256 }} + - name: Build and push Transit signer + id: transit_signer + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . - file: docker/postgres/Dockerfile + file: docker/hosted-transit-signer/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-postgres:${{ env.SOLMARA_IMAGE_TAG }} - build-args: | - POSTGRES_IMAGE=${{ env.POSTGRES_IMAGE }} - - name: Build and push hosted static metadata image - uses: docker/build-push-action@v6 + tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-transit-signer:${{ env.SOLMARA_IMAGE_TAG }} + labels: | + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.version=${{ env.SOLMARA_IMAGE_TAG }} + org.opencontainers.image.source=https://github.com/registrystack/solmara-lab + org.registrystack.release.version=0.20.1 + org.registrystack.release.revision=${{ steps.registry-stack.outputs.source_commit }} + - name: Verify hosted authority image source labels + run: | + set -euo pipefail + for component in authority-provisioner transit-signer; do + case "$component" in + authority-provisioner) digest='${{ steps.authority_provisioner.outputs.digest }}' ;; + transit-signer) digest='${{ steps.transit_signer.outputs.digest }}' ;; + esac + image="$SOLMARA_IMAGE_REGISTRY/solmara-lab-${component}@${digest}" + docker pull "$image" + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}')" = '${{ github.sha }}' + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.source" }}')" = https://github.com/registrystack/solmara-lab + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.registrystack.release.version" }}')" = 0.20.1 + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.registrystack.release.revision" }}')" = '${{ steps.registry-stack.outputs.source_commit }}' + done + - name: Smoke hosted authority provisioner image + run: | + set -euo pipefail + image="$SOLMARA_IMAGE_REGISTRY/solmara-lab-authority-provisioner@${{ steps.authority_provisioner.outputs.digest }}" + state="$RUNNER_TEMP/solmara-authority-provisioner-state" + mkdir -p "$state/runtime" "$state/source" + for _ in 1 2; do + docker run --rm --network none --read-only --tmpfs /tmp \ + --mount "type=bind,source=$state/runtime,target=/provisioned/runtime" \ + --mount "type=bind,source=$state/source,target=/provisioned/source" \ + "$image" provision \ + --target cra-relay \ + --assets /opt/solmara-hosted-assets \ + --secrets /run/secrets/solmara-provisioning \ + --runtime-output /provisioned/runtime \ + --source-output /provisioned/source + done + python3 - "$state" <<'PY' + import sqlite3 + import sys + from pathlib import Path + root = Path(sys.argv[1]) + with sqlite3.connect(f"file:{root / 'source/cra.sqlite'}?mode=ro", uri=True) as connection: + assert connection.execute("PRAGMA quick_check").fetchall() == [("ok",)] + assert (root / "runtime/runtime.yaml").is_file() + assert any((root / "runtime/package").iterdir()) + PY + chmod u+w "$state/runtime/runtime.yaml" + printf '\n' >> "$state/runtime/runtime.yaml" + stdout="$RUNNER_TEMP/solmara-authority-provisioner.stdout" + stderr="$RUNNER_TEMP/solmara-authority-provisioner.stderr" + set +e + docker run --rm --network none --read-only --tmpfs /tmp \ + --mount "type=bind,source=$state/runtime,target=/provisioned/runtime" \ + --mount "type=bind,source=$state/source,target=/provisioned/source" \ + "$image" provision \ + --target cra-relay \ + --assets /opt/solmara-hosted-assets \ + --secrets /run/secrets/solmara-provisioning \ + --runtime-output /provisioned/runtime \ + --source-output /provisioned/source >"$stdout" 2>"$stderr" + status=$? + set -e + test "$status" -eq 1 + test ! -s "$stdout" + test "$(cat "$stderr")" = "hosted target provisioning failed" + uv run scripts/smoke-hosted-provisioner-image.py \ + --image "$image" \ + --state-root "$RUNNER_TEMP/solmara-authority-provisioner-authority-state" + - name: Smoke hosted Transit signer image + run: | + set -euo pipefail + image="$SOLMARA_IMAGE_REGISTRY/solmara-lab-transit-signer@${{ steps.transit_signer.outputs.digest }}" + docker run --rm --network none --read-only --entrypoint python "$image" -c 'import cryptography' + stdout="$RUNNER_TEMP/solmara-transit-signer.stdout" + stderr="$RUNNER_TEMP/solmara-transit-signer.stderr" + set +e + docker run --rm --network none --read-only "$image" >"$stdout" 2>"$stderr" + status=$? + set -e + test "$status" -eq 1 + test ! -s "$stdout" + test "$(cat "$stderr")" = "hosted Transit signer could not start" + - name: Build and push static metadata + id: static_metadata + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . file: docker/static-metadata/Dockerfile platforms: linux/amd64 push: true tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-static-metadata:${{ env.SOLMARA_IMAGE_TAG }} - build-args: | - STATIC_METADATA_IMAGE=${{ env.STATIC_METADATA_IMAGE }} - - name: Build and push scenario runner image - uses: docker/build-push-action@v6 + - name: Build and push scenario runner + id: scenario_runner + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . file: scenario-runner/Dockerfile platforms: linux/amd64 push: true tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-scenario-runner:${{ env.SOLMARA_IMAGE_TAG }} - - name: Build and push Visitor Center image - uses: docker/build-push-action@v6 + - name: Build and push Visitor Center + id: home + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . file: home/Dockerfile platforms: linux/amd64 push: true tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-home:${{ env.SOLMARA_IMAGE_TAG }} - - name: Build and push portal image - uses: docker/build-push-action@v6 + - name: Build and push programme portal + id: portal + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: ./portal file: ./portal/Dockerfile platforms: linux/amd64 push: true tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-portal:${{ env.SOLMARA_IMAGE_TAG }} - - name: Build and push eSignet relay image - uses: docker/build-push-action@v6 + - name: Build and push eSignet Relay V2 authenticator image + id: esignet_relay + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . file: docker/esignet-relay/Dockerfile - build-contexts: | - esignet_relay_authenticator_src=./vendor/esignet-relay-authenticator platforms: linux/amd64 push: true tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-esignet-relay:${{ env.SOLMARA_IMAGE_TAG }} build-args: | ESIGNET_BASE_IMAGE=${{ env.ESIGNET_BASE_IMAGE }} - - name: Build and push eSignet Postgres image - uses: docker/build-push-action@v6 + ESIGNET_AUTHENTICATOR_JAR_URL=${{ env.ESIGNET_AUTHENTICATOR_JAR_URL }} + ESIGNET_AUTHENTICATOR_JAR_SHA256=${{ env.ESIGNET_AUTHENTICATOR_JAR_SHA256 }} + - name: Build and push isolated eSignet database + id: esignet_postgres + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . file: docker/esignet-postgres/Dockerfile @@ -222,8 +351,9 @@ jobs: tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-esignet-postgres:${{ env.SOLMARA_IMAGE_TAG }} build-args: | POSTGRES_IMAGE=${{ env.ESIGNET_POSTGRES_IMAGE }} - - name: Build and push eSignet UI image - uses: docker/build-push-action@v6 + - name: Build and push eSignet UI + id: esignet_ui + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . file: docker/esignet-ui/Dockerfile @@ -234,27 +364,49 @@ jobs: ESIGNET_UI_IMAGE=${{ env.ESIGNET_UI_IMAGE }} ESIGNET_NGINX_CONF=config/esignet/nginx-hosted.conf - name: Build and push eSignet seed image - uses: docker/build-push-action@v6 + id: esignet_seed + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . file: docker/esignet-seed/Dockerfile platforms: linux/amd64 push: true tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-esignet-seed:${{ env.SOLMARA_IMAGE_TAG }} - - name: Report image digests + - name: Generate Coolify image manifest + env: + SOLMARA_EVIDENCE_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-evidence@${{ steps.evidence.outputs.digest }} + SOLMARA_MINT_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-mint@${{ steps.mint.outputs.digest }} + SOLMARA_AUTHORITY_PROVISIONER_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-authority-provisioner@${{ steps.authority_provisioner.outputs.digest }} + SOLMARA_TRANSIT_SIGNER_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-transit-signer@${{ steps.transit_signer.outputs.digest }} + SOLMARA_STATIC_METADATA_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-static-metadata@${{ steps.static_metadata.outputs.digest }} + SOLMARA_SCENARIO_RUNNER_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-scenario-runner@${{ steps.scenario_runner.outputs.digest }} + SOLMARA_HOME_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-home@${{ steps.home.outputs.digest }} + SOLMARA_PORTAL_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-portal@${{ steps.portal.outputs.digest }} + SOLMARA_ESIGNET_RELAY_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-esignet-relay@${{ steps.esignet_relay.outputs.digest }} + SOLMARA_ESIGNET_POSTGRES_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-esignet-postgres@${{ steps.esignet_postgres.outputs.digest }} + SOLMARA_ESIGNET_UI_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-esignet-ui@${{ steps.esignet_ui.outputs.digest }} + SOLMARA_ESIGNET_SEED_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-esignet-seed@${{ steps.esignet_seed.outputs.digest }} + run: | + set -euo pipefail + manifest="$RUNNER_TEMP/solmara-release/solmara-hosted-images.env" + python3 scripts/hosted-image-manifest.py write --output "$manifest" + python3 scripts/hosted-image-manifest.py validate --manifest "$manifest" + - name: Upload Coolify image manifest + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: solmara-hosted-images-${{ github.sha }} + path: ${{ runner.temp }}/solmara-release/solmara-hosted-images.env + if-no-files-found: error + retention-days: 30 + - name: Report immutable Solmara image digests run: | { echo "### Solmara image digests" echo - for image in solmara-lab-relay solmara-lab-notary solmara-lab-postgres solmara-lab-static-metadata solmara-lab-scenario-runner solmara-lab-home solmara-lab-portal solmara-lab-esignet-relay solmara-lab-esignet-postgres solmara-lab-esignet-ui solmara-lab-esignet-seed; do - ref="${SOLMARA_IMAGE_REGISTRY}/${image}:${SOLMARA_IMAGE_TAG}" - digest="$(docker buildx imagetools inspect "$ref" --format '{{.Manifest.Digest}}')" - echo "- ${SOLMARA_IMAGE_REGISTRY}/${image}@${digest}" - done - } | tee -a "$GITHUB_STEP_SUMMARY" - - name: Stop stack + while IFS='=' read -r key image; do + printf -- '- %s=%s\n' "$key" "$image" + done < "$RUNNER_TEMP/solmara-release/solmara-hosted-images.env" + } >> "$GITHUB_STEP_SUMMARY" + - name: Stop authority-cell stack if: always() - run: | - if command -v just >/dev/null 2>&1; then - just down - fi + run: just down || true diff --git a/.gitignore b/.gitignore index 8e0a2c6..b96da6a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ output/* !output/smoke/ output/smoke/* !output/smoke/.gitkeep +relays/*/package/ +relays/*/.package.*/ +runtime/evidence-cells/ +runtime/local-transit-signers/ static-metadata/* !static-metadata/.gitkeep node_modules/ diff --git a/README.md b/README.md index 734c2b8..6dabe37 100644 --- a/README.md +++ b/README.md @@ -1,152 +1,146 @@ # Solmara Lab -Solmara Lab is a Registry Stack adopter demo for the fictional Republic of -Solmara. Its local runtime uses the current Records API Relay, Registry -Evidence, and Registry Mint architecture. Notary is no longer part of the -local topology. +Solmara Lab is a fictional Registry Stack adopter lab for authority-owned, +independently signed evidence. The programme application composes assertions. +Registry Evidence does not make a cross-authority programme decision. -Relay, Evidence, and Mint are not yet released. This branch builds all three -images from the exact Registry Stack `main` commit recorded in `versions.env`. -The build refuses a sibling checkout whose local `main` or `origin/main` does -not match that commit. +The reset has two evidence cadences: + +- CRA birth, NIA population, and SRO poverty requirements read checkpointed, + immutable SQLite extracts. +- CRA civil links and death status, MoSD enrolment, SIPF pension and survivor + cases, and NAgDI agriculture requirements use named Relay V2 exact lookups. + +Six Evidence cells have distinct providers, issuers, signing keys, JWKS, audit +sinks, subject-binding secrets, and endpoints. Five Relays expose only the +named non-enumerating operations needed by the lab. NIA's Relay is reserved for +the optional eSignet UserInfo profile; NIA Evidence reads its own extract. + +## Release prerequisite + +Registry Stack v0.19.0 cannot run this topology. Its Mint cannot issue the +Relay-compatible scope and purpose claims required by Relay V2, and its +Evidence runtime cannot classify Relay's concealed `consultation.unresolved` +response as a data-free unresolved consultation. Registry Stack v0.20.1 is the +first immutable release containing both capabilities. Solmara pins its release +tag and source commit, the Relay OCI digest, and the Evidence, Mint, and +`relayctl` binary checksums in `versions.env`. + +The eSignet profile uses the separately released +`esignet-relay-authenticator` v0.2.0 JAR and its matching SHA-256 checksum. No +source-build, floating-tag, or v0.19 compatibility fallback is accepted. ## Quick start -Prerequisites are Docker with Compose, Rust, `just`, `uv`, `pnpm`, and a -Registry Stack checkout at `../registry-stack`. +Prerequisites are Docker with Compose, `just`, `uv`, and `pnpm`. ```bash just setup -just up-generated +just generate +just relay-check +just compose +just up +just evidence-check just smoke -just portal-live-e2e -just down +just programme-acceptance +just lifecycle-proof ``` -`just up-generated` performs the clean-checkout journey: - -1. Generate deterministic Solmara fixtures and ignored local secrets. -2. Compile and compare all six Registry projects with the pinned Registryctl. -3. Build Relay, Evidence, and Mint from the exact Registry Stack source commit. -4. Build the Solmara services and start the local Compose topology. +`just generate` publishes five mutable Relay databases and three versioned +immutable extracts below ignored `output/sqlite/`. It also creates ignored +runtime secrets and public projections. An active extract is never overwritten +in place. -The first source build can take several minutes. Subsequent builds reuse -Docker layers. `just down` keeps local volumes; `just reset` removes this -checkout's volumes. - -Local entry points: +Local entry points after startup are: - Visitor Center: `http://127.0.0.1:4301` -- Citizen portal: `http://127.0.0.1:4300` -- Evidence and Mint TLS gateway: `https://localhost:4341` -- Relay Records APIs: ports `4311` through `4316` -- Child-benefit application collector: `https://localhost:4341/child-benefit/` - -The generated local CA is -`config/evidence/local/tls/ca.crt`. Host-side scripts use it through the -generated `SOLMARA_EVIDENCE_CA_BUNDLE` setting. -Evidence, Mint, client, and CA keys are preserved across later -`just gen-secrets` runs so retained audit volumes remain verifiable. If that -material becomes partial, reset the local volumes before regenerating it. +- Programme portal: `http://127.0.0.1:4300` +- Authority Evidence gateway: `https://localhost:4341/evidence/{authority}` +- Static metadata: `http://127.0.0.1:4331` -## Local architecture +The gateway strips `/evidence/{authority}` before forwarding. Application code +uses six authority base URLs and never assumes a national Evidence host. -The six authority projects compile current Records API Relay configurations. -Each running Relay exposes only its public Records API. A workload identity -agent writes a short-lived, `solmara-evidence`-scoped bearer token into the -private Evidence secret volume. +## Architecture -Registry Mint authenticates the Solmara application client with -`private_key_jwt`. It issues short-lived access tokens for the -`solmara-evidence` audience. Registry Evidence validates those tokens, applies -one of eleven reviewed requirements, calls the necessary Relay Records API, -and returns a flattened signed JWS assertion containing only the approved -concept values. +| Authority | Evidence source | Relay V2 role | +|---|---|---| +| CRA | immutable birth extract; Relay for death and civil link | `civil-person/death-by-uin`, `civil-person/citizen-link-by-uin` | +| NIA | immutable population extract | `population-person/esignet-userinfo` for eSignet | +| SRO | immutable poverty extract | none | +| MoSD | Relay lookup | `beneficiary-enrolment/by-uin` | +| SIPF | Relay lookups | `pension-payment/by-pensioner-uin`, `survivor-case/by-spouse-uin` | +| NAgDI | Relay lookups | `farmer/voucher-by-farmer-id`, `livestock-herd/movement-by-farmer-id` | -Application code can combine multiple independently signed Evidence responses -for a workflow decision. Evidence itself does not own child-benefit, pension, -farmer, or citizen application decisions. +One lab Mint issues tokens for the common `solmara-runtime` audience. Every +operation still has a distinct client, fixed scope, canonical purpose claim, +access profile, and disclosure profile. This shared audience is a lab +convenience, not production tenancy guidance. -The current Registryctl compiler still emits both public and consultation -Relay lanes for authored projects. Both generated lanes are committed under -`runtime/registry-projects/` for deterministic compiler review. Only the public -Records API lane runs in the local Compose topology. +Direct extracts have a maximum age of 86,400 seconds and assertions valid for +at most 3,600 seconds. Relay-backed assertions are valid for at most 300 +seconds. Every response is a flattened ES256 JWS and is verified against the +JWKS of the authority endpoint that issued it. -## Source binding +## Publisher lifecycle -`versions.env` records: - -- `REGISTRY_STACK_SOURCE_REF=main` -- the exact 40-character `REGISTRY_STACK_SOURCE_COMMIT` -- the matching Registryctl version -- the three local source-image names -- digest-pinned third-party runtime images - -`scripts/build-registry-stack-runtime.sh` validates local and remote-tracking -`main`, creates a temporary detached worktree at the pinned commit, and builds: - -- `solmara-lab-registry-relay:source` -- `solmara-lab-registry-evidence:source` -- `solmara-lab-registry-mint:source` - -The images receive the exact source revision as an OCI label. `just up` and -`just up-esignet` always run this source validation and build step. - -## Verification +From `generator/`: ```bash -just registry-projects-check # validate six projects in local and hosted authoring profiles -just registry-projects-test # run every Registry project fixture -just registry-projects-runtime-check # prove committed generated runtime has no drift -just evidence-check # Mint check plus 11 Evidence fixture files and 89 cases -just lint # repository, metadata, TypeScript, and image-reference checks -just test # active Python, portal, and Visitor Center tests -just compose # validate local and eSignet Compose profiles -just smoke # exercise a running Relay, Mint, Evidence, scenario, and portal stack +uv run python -m solmara_lab.publisher --root .. publish-all +uv run python -m solmara_lab.publisher --root .. mutate-mosd \ + --uin 2300010248 --duplicate-flag true \ + --recorded-at 2026-08-12T12:00:00Z +uv run python -m solmara_lab.publisher --root .. publish-extract \ + --authority sro --published-at 2026-08-12T12:00:00Z \ + --extract-id sro-poverty-20260812T120000Z ``` -`just evidence-check` requires the exact `evidencectl` and `mint` versions -shown in `versions.env`, plus local material generated by `just gen-secrets`. -It stages an immutable temporary deployment, uses fixture-only Relay tokens, -and does not modify committed Evidence inputs. +The live MoSD mutation is visible on the next Relay-backed Evidence request +without a restart. A newly named SRO extract is not visible until only the SRO +Evidence cell is rebound and restarted. Invalid, stale, or overwritten extracts +fail closed. Run lifecycle smoke in isolated volumes or restore the deterministic +fixture state afterward. -For eSignet-backed portal login: +## Verification ```bash -just up-esignet -just smoke-esignet +just relay-check # relayctl check --production, generate, test, package +just evidence-check # six bundles, 11 requirements, source/error fixtures +just lint # metadata, redaction, portal and Visitor Center checks +just test # Python and web unit/integration suites +just compose # local, hosted, Coolify, and optional eSignet config +just smoke # local UI and Relay health +just programme-acceptance # programme stories and generic denial controls +just lifecycle-proof # live Relay and immutable-extract cadence +just portal-live-e2e +just home-live-e2e +just smoke-esignet # optional NIA Relay V2 login profile ``` -Use Elena's synthetic UIN `2300018263` and static OTP `111111`. +Generated SQLite databases, Relay packages, runtime bundles, keys, secrets, +tokens, and private audit records are not tracked. Public metadata and governed +Relay/Evidence authoring inputs are tracked. ## Repository map -- `projects/` contains six authority-owned Registry project sources. -- `runtime/registry-projects/` contains deterministic Registryctl output. -- `evidence/` contains the Evidence bundle, Mint config, TLS gateway config, - requirements, derivations, schemas, and fixtures. -- `ministries/` contains authority-owned synthetic source fixtures. -- `generator/` creates deterministic country data and projections. -- `scenarios/` and `scenario-runner/` implement the guided Evidence journeys. -- `portal/` contains the citizen portal and live scenario-runner adapter. -- `home/` contains the Visitor Center. -- `scripts/` contains source-build, generation, verification, and smoke helpers. - -## Hosted deployment status - -Hosted and Coolify Compose files still describe the previous released Notary -topology. They are intentionally outside this local migration and are not -supported by this branch. Do not deploy them with the generated runtime in -`runtime/registry-projects/`. - -A hosted migration should follow once Relay, Evidence, and Mint have published -artifacts and an explicit deployment design is agreed. Until then, the active -commands and verification targets in this README cover only the locally built -source topology. - -## Privacy - -All Solmara data is synthetic. Do not use real people, real email domains, -real addresses, or real administrative geography. Use -`@mail.solmara.example` for email addresses and keep story domains under -`gov.solmara.example`. +- `relays/` contains five authority-governed Relay V2 projects. +- `evidence/cells/` contains six authority Evidence bundle templates and + runtime bindings. +- `generator/solmara_lab/publisher.py` publishes deterministic SQLite sources. +- `scenarios/` and `scenario-runner/` route requirements to authority cells, + verify multiple JWKS, and compose application outcomes. +- `portal/` and `home/` display safe authority, issuer, and source-type labels. +- `metadata/` and `requests/registry-lab/` publish discovery and request + examples for the reset. + +## Hosted rollout and recovery + +Deploy the new V2 services and new volumes alongside the existing deployment. +Smoke the new endpoints before switching application and metadata routing. +After the switch, disable superseded services but retain their volumes. Deleting +old data is a separate, explicitly approved cleanup and is not part of this +reset. + +All Solmara data is synthetic. Never put real people, credentials, hosted +deployment evidence, private audit output, or private keys in this repository. diff --git a/compose.coolify.agriculture.yaml b/compose.coolify.agriculture.yaml index fad559a..677cf7d 100644 --- a/compose.coolify.agriculture.yaml +++ b/compose.coolify.agriculture.yaml @@ -1,235 +1,60 @@ -name: solmara-lab-agriculture +name: solmara-lab-agriculture-cells -x-relay-environment: &relay-environment - RUST_LOG: ${RUST_LOG:-info} - REGISTRY_RELAY_BIND: 0.0.0.0:8080 - -x-notary-environment: ¬ary-environment - RUST_LOG: ${RUST_LOG:-info} - REGISTRY_NOTARY_BIND: 0.0.0.0:8081 - REGISTRY_NOTARY_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - -x-relay-common: &relay-common - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: unless-stopped - environment: *relay-environment - healthcheck: - test: ["CMD", "/usr/local/bin/registry-relay", "healthcheck"] - interval: 30s - timeout: 10s - retries: 3 - -x-relay-bootstrap-common: &relay-bootstrap-common - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - -x-relay-config-state-init: &relay-config-state-init - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - user: "0:0" - restart: "no" - entrypoint: ["/bin/busybox", "sh", "-eu", "-c"] - -x-workload-agent-common: &workload-agent-common - image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} - command: ["python", "scripts/relay_workload_identity_agent.py"] +x-runtime: &runtime restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] - interval: 10s - timeout: 5s - retries: 6 - -x-notary-common: ¬ary-common - image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - user: "65534:65534" - restart: unless-stopped - environment: *notary-environment - healthcheck: - test: ["CMD", "/usr/local/bin/registry-notary", "healthcheck", "--url", "http://127.0.0.1:8081/ready"] - interval: 30s - timeout: 10s - retries: 3 + read_only: true + tmpfs: [/tmp] + networks: [runtime] + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + depends_on: + audit-permissions: {condition: service_completed_successfully} services: - postgres: - image: ${SOLMARA_POSTGRES_IMAGE:?set digest-pinned Solmara Postgres image} - entrypoint: ["/bin/sh", "/usr/local/bin/solmara-postgres-entrypoint.sh"] - command: - - postgres - - -c - - ssl=on - - -c - - ssl_cert_file=/var/lib/postgresql/server-ssl/server.crt - - -c - - ssl_key_file=/var/lib/postgresql/server-ssl/server.key - environment: &postgres-environment - POSTGRES_USER: ${SOLMARA_POSTGRES_USER:-solmara_registry} - POSTGRES_PASSWORD: ${SOLMARA_POSTGRES_PASSWORD} - POSTGRES_DB: ${SOLMARA_POSTGRES_DB:-solmara_lab} - SOLMARA_SOURCE_READER_DATABASES: "" - REGISTRY_RELAY_STATE_EPOCH: ${REGISTRY_RELAY_STATE_EPOCH:-v015} - SOLMARA_RELAY_DATABASES: nagdi - SOLMARA_NOTARY_DATABASES: nagdi - NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD: ${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD} - NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - NAGDI_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${NAGDI_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - NAGDI_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${NAGDI_NOTARY_POSTGRES_RUNTIME_PASSWORD} - volumes: - - postgres-data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] - interval: 5s - timeout: 5s - retries: 10 - - registry-postgresql-bootstrap: - image: ${SOLMARA_POSTGRES_IMAGE:?set digest-pinned Solmara Postgres image} - restart: "no" - entrypoint: ["/bin/sh", "/docker-entrypoint-initdb.d/010-init-notary-postgresql.sh"] - environment: - <<: *postgres-environment - PGHOST: postgres - PGPASSWORD: ${SOLMARA_POSTGRES_PASSWORD} - PGSSLMODE: require - depends_on: - postgres: - condition: service_healthy - - nagdi-workload-agent: - <<: *workload-agent-common - environment: - WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"nagdi-notary","subject":"nagdi-notary","scopes":["registry:consult:livestock","registry:consult:voucher"],"token_file":"/run/secrets/nagdi-notary-relay-token","private_jwk_env":"NAGDI_RELAY_WORKLOAD_JWK"}] - NAGDI_RELAY_WORKLOAD_JWK: ${NAGDI_RELAY_WORKLOAD_JWK} - volumes: - - nagdi-workload-token:/run/secrets - - nagdi-relay-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/nagdi-agriculture/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - nagdi-relay-cache:/var/lib/registry-relay/cache - - nagdi-relay-consultation-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/nagdi-agriculture/consultation/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - nagdi-relay-consultation-cache:/var/lib/registry-relay/cache - - nagdi-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/nagdi-agriculture/consultation/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NAGDI_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NAGDI_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - volumes: - - nagdi-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - nagdi-relay-consultation-config-state-init: - condition: service_completed_successfully - registry-postgresql-bootstrap: - condition: service_completed_successfully - - nagdi-agriculture-relay: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NAGDI_RELAY_AUDIT_HASH_SECRET} - expose: ["8080"] - volumes: - - nagdi-relay-cache:/var/lib/registry-relay/cache - depends_on: - nagdi-relay-config-state-init: - condition: service_completed_successfully - labels: - solmara.lab.host: nagdi-relay.solmara.registrystack.org - - nagdi-agriculture-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/nagdi-agriculture/consultation/bootstrap.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NAGDI_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NAGDI_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - expose: ["8081"] - volumes: - - nagdi-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - nagdi-relay-state-bootstrap: - condition: service_completed_successfully - nagdi-workload-agent: - condition: service_healthy - - nagdi-notary: - <<: *notary-common - command: ["--config", "/etc/solmara/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml"] - network_mode: service:nagdi-agriculture-relay-consultation - environment: &nagdi-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${NAGDI_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_nagdi_runtime:${NAGDI_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_nagdi?sslmode=require" - NAGDI_CLIENT_TOKEN_HASH: ${NAGDI_CLIENT_TOKEN_HASH} - NAGDI_NOTARY_ISSUER_JWK: ${NAGDI_NOTARY_ISSUER_JWK} - volumes: - - nagdi-workload-token:/run/secrets:ro - depends_on: - nagdi-agriculture-relay-consultation: - condition: service_healthy - nagdi-notary-state-install: - condition: service_completed_successfully - nagdi-workload-agent: - condition: service_healthy - labels: - solmara.lab.host: nagdi-notary.solmara.registrystack.org - - nagdi-notary-state-install: - image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} + audit-permissions: + image: ${SOLMARA_AUTHORITY_PROVISIONER_IMAGE:?set the digest-pinned Solmara authority provisioner image} + entrypoint: [python, /app/scripts/provision-hosted-runtime.py] + command: [init-audit, --destination, /audit/nagdi-relay, --destination, /audit/nagdi-evidence, --uid, "65532", --gid, "65532"] restart: "no" - command: ["--config", "/etc/solmara/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_nagdi_owner", "--runtime-role", "solmara_notary_nagdi_runtime"] - environment: - <<: *nagdi-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_nagdi_migrator:${NAGDI_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_nagdi?sslmode=require" - volumes: - - nagdi-workload-token:/run/secrets:ro + user: "0:0" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + cap_add: [CHOWN, FOWNER] + security_opt: [no-new-privileges:true] + volumes: [nagdi-relay-audit:/audit/nagdi-relay, nagdi-evidence-audit:/audit/nagdi-evidence] + + nagdi-relay: + <<: *runtime + image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + command: [serve, --runtime, /etc/relay/nagdi/runtime.yaml] + environment: {SOLMARA_RELAY_AUDIT_KEY: "${NAGDI_RELAY_AUDIT_KEY:?required}", SOLMARA_RELAY_CURSOR_KEY: "${NAGDI_RELAY_CURSOR_KEY:?required}"} + volumes: [nagdi-relay-runtime:/etc/relay/nagdi:ro, nagdi-relay-source:/var/lib/relay/source:ro, nagdi-relay-audit:/var/lib/relay/audit] + labels: {solmara.lab.host: nagdi-relay.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + + nagdi-evidence: + <<: *runtime + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + command: [--runtime, /etc/registry-evidence/nagdi/runtime.yaml, serve] + volumes: [nagdi-evidence-runtime:/etc/registry-evidence/nagdi:ro, nagdi-evidence-secrets:/run/secrets/registry-evidence/nagdi:ro, nagdi-evidence-transit:/run/registry-evidence/nagdi:ro, nagdi-evidence-audit:/var/lib/registry-evidence/nagdi/audit] + networks: {runtime: {ipv4_address: 172.29.1.26}} depends_on: - nagdi-workload-agent: - condition: service_healthy - registry-postgresql-bootstrap: - condition: service_completed_successfully + audit-permissions: {condition: service_completed_successfully} + nagdi-relay: {condition: service_started} + labels: {solmara.lab.host: nagdi-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} volumes: - postgres-data: - nagdi-relay-cache: - nagdi-relay-consultation-cache: - nagdi-workload-token: + nagdi-relay-runtime: {external: true, name: solmara-authority-cells-nagdi-relay-runtime} + nagdi-relay-source: {external: true, name: solmara-authority-cells-nagdi-relay-source} + nagdi-relay-audit: + nagdi-evidence-runtime: {external: true, name: solmara-authority-cells-nagdi-evidence-runtime} + nagdi-evidence-secrets: {external: true, name: solmara-authority-cells-nagdi-evidence-secrets} + nagdi-evidence-transit: {external: true, name: solmara-authority-cells-nagdi-evidence-transit} + nagdi-evidence-audit: + superseded-state: {external: true, name: "${SOLMARA_SUPERSEDED_AGRICULTURE_VOLUME:-solmara-lab-agriculture-state}"} + +networks: + runtime: + ipam: + config: [{subnet: 172.29.1.0/24}] diff --git a/compose.coolify.esignet.yaml b/compose.coolify.esignet.yaml index ac5a630..618d84a 100644 --- a/compose.coolify.esignet.yaml +++ b/compose.coolify.esignet.yaml @@ -1,52 +1,28 @@ -name: solmara-lab-esignet - services: esignet-database: - image: ${SOLMARA_ESIGNET_POSTGRES_IMAGE:?set digest-pinned Solmara eSignet Postgres image} - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD} - volumes: - - esignet-pgdata:/var/lib/postgresql/data + image: ${SOLMARA_ESIGNET_POSTGRES_IMAGE:?set digest-pinned eSignet database image} + environment: {POSTGRES_USER: esignet, POSTGRES_PASSWORD: "${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?required}"} + volumes: ["esignet-db-data:/var/lib/postgresql"] healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + test: ["CMD-SHELL", "pg_isready -U esignet -d esignet"] interval: 5s timeout: 5s retries: 30 esignet-redis: - image: ${ESIGNET_REDIS_IMAGE:-redis:6.0} + image: ${ESIGNET_REDIS_IMAGE:?versions.env must pin Redis} command: ["redis-server"] - restart: unless-stopped - volumes: - - esignet-redis-data:/data + volumes: ["esignet-redis-data:/data"] healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 30 - nia-esignet-workload-agent: - image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} - command: ["python", "scripts/relay_workload_identity_agent.py"] - restart: unless-stopped - environment: - WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"solmara-esignet","subject":"solmara-esignet","scopes":["population:identity_release"],"token_file":"/run/secrets/solmara-esignet-relay-token","private_jwk_env":"NIA_ESIGNET_RELAY_WORKLOAD_JWK","token_uid":1001,"token_gid":1001}] - NIA_ESIGNET_RELAY_WORKLOAD_JWK: ${NIA_ESIGNET_RELAY_WORKLOAD_JWK} - volumes: - - nia-esignet-workload-token:/run/secrets - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] - interval: 10s - timeout: 5s - retries: 6 - esignet: - image: ${SOLMARA_ESIGNET_RELAY_IMAGE:?set digest-pinned Solmara eSignet relay image} - user: root + image: ${SOLMARA_ESIGNET_RELAY_IMAGE:?set digest-pinned eSignet image} restart: unless-stopped + user: root environment: active_profile_env: default,local spring_config_label_env: "" @@ -64,140 +40,84 @@ services: SPRING_AUTOCONFIGURE_EXCLUDE: org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration SPRING_CACHE_TYPE: redis SPRING_DATA_REDIS_HOST: esignet-redis - SPRING_REDIS_PASSWORD: "" MOSIP_ESIGNET_HOST: esignet.solmara.registrystack.org MOSIP_ESIGNET_DISCOVERY_ISSUER_ID: https://esignet.solmara.registrystack.org MOSIP_ESIGNET_DISCOVERY_KEY_VALUES: "{'issuer':'https://esignet.solmara.registrystack.org','authorization_endpoint':'https://esignet-ui.solmara.registrystack.org/authorize','token_endpoint':'https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token','userinfo_endpoint':'https://esignet.solmara.registrystack.org/v1/esignet/oidc/userinfo','jwks_uri':'https://esignet.solmara.registrystack.org/v1/esignet/oauth/.well-known/jwks.json','token_endpoint_auth_methods_supported':{'private_key_jwt'},'token_endpoint_auth_signing_alg_values_supported':{'RS256','PS256','ES256'}}" MOSIP_ESIGNET_OAUTH_KEY_VALUES: "{'issuer':'https://esignet.solmara.registrystack.org','authorization_endpoint':'https://esignet-ui.solmara.registrystack.org/authorize','token_endpoint':'https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token','jwks_uri':'https://esignet.solmara.registrystack.org/v1/esignet/oauth/.well-known/jwks.json','token_endpoint_auth_methods_supported':{'private_key_jwt'},'token_endpoint_auth_signing_alg_values_supported':{'RS256','PS256','ES256'}}" MOSIP_ESIGNET_DATABASE_URL: jdbc:postgresql://esignet-database:5432/mosip_esignet?currentSchema=esignet - MOSIP_ESIGNET_DATABASE_USERNAME: postgres - MOSIP_ESIGNET_DATABASE_PASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD} + MOSIP_ESIGNET_DATABASE_USERNAME: esignet + MOSIP_ESIGNET_DATABASE_PASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?required} MOSIP_ESIGNET_INTEGRATION_SCAN_BASE_PACKAGE: io.mosip.esignet.mock.integration,io.registry.esignet.relay MOSIP_ESIGNET_INTEGRATION_AUTHENTICATOR: RelayAuthenticationService MOSIP_ESIGNET_INTEGRATION_KEY_BINDER: MockKeyBindingWrapperService - MOSIP_ESIGNET_OPENID_SCOPE_CLAIMS: "{'profile' : {'name','given_name','family_name','gender','birthdate','individual_id'}}" + MOSIP_ESIGNET_OPENID_SCOPE_CLAIMS: "{'profile' : {'given_name','family_name','gender','birthdate','individual_id'}}" REGISTRY_RELAY_BASE_URL: https://nia-relay.solmara.registrystack.org - REGISTRY_RELAY_ATTRIBUTE_RELEASE_PROFILE_ID: solmara-nia-userinfo - REGISTRY_RELAY_ATTRIBUTE_RELEASE_PROFILE_VERSION: v1 - REGISTRY_RELAY_ATTRIBUTE_RELEASE_PURPOSE: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - REGISTRY_RELAY_AUTH_BEARER_TOKEN_FILE: /run/secrets/solmara-esignet-relay-token - REGISTRY_RELAY_SUBJECT_ID_TYPE: national_id - REGISTRY_RELAY_DEFAULT_CLAIMS: individual_id,name,given_name,family_name,birthdate,gender - REGISTRY_ESIGNET_AUTH_SUPPORTED_FACTORS: OTP - REGISTRY_ESIGNET_AUTH_OTP_CHANNELS: email,phone + REGISTRY_RELAY_RESOURCE: population-person + REGISTRY_RELAY_LOOKUP: esignet-userinfo + REGISTRY_RELAY_ACCESS_PROFILE: esignet + REGISTRY_RELAY_DEFAULT_CLAIMS: individualId,givenName,familyName,birthdate,gender + SPRING_APPLICATION_JSON: '{"registry":{"esignet":{"claim-map":{"sub":"$$psut","individual_id":"individualId","given_name":"givenName","family_name":"familyName","birthdate":"birthdate","gender":"gender"}}}}' + REGISTRY_MINT_TOKEN_ENDPOINT: https://mint.solmara.registrystack.org/token + REGISTRY_MINT_CLIENT_ID: nia-esignet + REGISTRY_MINT_PRIVATE_JWK: ${NIA_ESIGNET_CLIENT_PRIVATE_JWK:?required} + REGISTRY_MINT_TOKEN_CACHE_MAX_SECONDS: "300" REGISTRY_ESIGNET_AUTH_OTP_STATIC_ENABLED: "true" - REGISTRY_ESIGNET_AUTH_OTP_STATIC_VALUE: "111111" - REGISTRY_ESIGNET_KYC_TOKEN_HMAC_SECRET: ${REGISTRY_ESIGNET_KYC_TOKEN_SECRET} - REGISTRY_ESIGNET_PSUT_HMAC_SECRET: ${REGISTRY_ESIGNET_PSUT_SECRET} - REGISTRY_ESIGNET_USER_INFO_ISSUER: https://esignet.solmara.registrystack.org + REGISTRY_ESIGNET_AUTH_OTP_STATIC_VALUE: ${ESIGNET_DEMO_OTP:?required} + REGISTRY_ESIGNET_AUTH_OTP_CHANNELS: EMAIL,PHONE + REGISTRY_ESIGNET_ACCOUNT_CHECK_CLAIMS: individualId + MOSIP_ESIGNET_AUTHENTICATOR_IDA_OTP_CHANNELS: EMAIL,PHONE + REGISTRY_ESIGNET_KYC_TOKEN_HMAC_SECRET: ${REGISTRY_ESIGNET_KYC_TOKEN_SECRET:?required} + REGISTRY_ESIGNET_PSUT_HMAC_SECRET: ${REGISTRY_ESIGNET_PSUT_SECRET:?required} REGISTRY_ESIGNET_KYC_SIGNING_KEYSTORE_PATH: /home/mosip/kyc-signing/kyc-signing.p12 REGISTRY_ESIGNET_KYC_SIGNING_KEYSTORE_TYPE: PKCS12 - REGISTRY_ESIGNET_KYC_SIGNING_KEYSTORE_PASSWORD: ${REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD} + REGISTRY_ESIGNET_KYC_SIGNING_KEYSTORE_PASSWORD: ${REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD:?required} REGISTRY_ESIGNET_KYC_SIGNING_KEY_ALIAS: esignet-relay-kyc - REGISTRY_ESIGNET_KYC_SIGNING_KEY_PASSWORD: ${REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD} + REGISTRY_ESIGNET_KYC_SIGNING_KEY_PASSWORD: ${REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD:?required} MOSIP_KERNEL_KEYMANAGER_HSM_CONFIG_PATH: /home/mosip/keystore/esignet_local.p12 - expose: - - "8088" - volumes: - - esignet-keystore:/home/mosip/keystore - - esignet-kyc-signing:/home/mosip/kyc-signing - - nia-esignet-workload-token:/run/secrets:ro - depends_on: - esignet-database: - condition: service_healthy - esignet-redis: - condition: service_healthy - nia-esignet-workload-agent: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-fsS", "http://esignet:8088/v1/esignet/oidc/.well-known/openid-configuration"] - interval: 10s - timeout: 5s - retries: 60 - start_period: 90s + volumes: ["esignet-keystore:/home/mosip/keystore", "esignet-kyc-signing:/home/mosip/kyc-signing"] + depends_on: {esignet-database: {condition: service_healthy}, esignet-redis: {condition: service_healthy}} + labels: {solmara.lab.host: esignet.solmara.registrystack.org} esignet-ui: - image: ${SOLMARA_ESIGNET_UI_IMAGE:?set digest-pinned Solmara eSignet UI image} - user: root - restart: unless-stopped - environment: - container_user: mosip - DEFAULT_WELLKNOWN: "%5B%7B%22name%22%3A%22OpenID%20Configuration%22%2C%22value%22%3A%22%2F.well-known%2Fopenid-configuration%22%7D%2C%7B%22name%22%3A%22Jwks%20Json%22%2C%22value%22%3A%22%2F.well-known%2Fjwks.json%22%7D%2C%7B%22name%22%3A%22Authorization%20Server%22%2C%22value%22%3A%22%2F.well-known%2Foauth-authorization-server%22%7D%5D" - SIGN_IN_WITH_ESIGNET_PLUGIN_URL: https://raw.githubusercontent.com/mosip/artifactory-ref-impl/977607990661e864c2adddb9d7de50ea6c651b8f/artifacts/src/mosip-plugins/sign-in-with-esignet/sign-in-with-esignet.zip - i18n_url_env: "" - theme_url_env: "" - images_url_env: "" - expose: - - "3000" - depends_on: - esignet: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://esignet-ui:3000/.well-known/openid-configuration"] - interval: 10s - timeout: 5s - retries: 30 - start_period: 30s - labels: - solmara.lab.host: esignet-ui.solmara.registrystack.org - - esignet-edge: - image: ${SOLMARA_ESIGNET_UI_IMAGE:?set digest-pinned Solmara eSignet UI image} - user: root - restart: unless-stopped - environment: - container_user: mosip - DEFAULT_WELLKNOWN: "%5B%7B%22name%22%3A%22OpenID%20Configuration%22%2C%22value%22%3A%22%2F.well-known%2Fopenid-configuration%22%7D%2C%7B%22name%22%3A%22Jwks%20Json%22%2C%22value%22%3A%22%2F.well-known%2Fjwks.json%22%7D%2C%7B%22name%22%3A%22Authorization%20Server%22%2C%22value%22%3A%22%2F.well-known%2Foauth-authorization-server%22%7D%5D" - SIGN_IN_WITH_ESIGNET_PLUGIN_URL: https://raw.githubusercontent.com/mosip/artifactory-ref-impl/977607990661e864c2adddb9d7de50ea6c651b8f/artifacts/src/mosip-plugins/sign-in-with-esignet/sign-in-with-esignet.zip - i18n_url_env: "" - theme_url_env: "" - images_url_env: "" - expose: - - "3000" - depends_on: - esignet: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://esignet-edge:3000/.well-known/openid-configuration"] - interval: 10s - timeout: 5s - retries: 30 - start_period: 30s - labels: - solmara.lab.host: esignet.solmara.registrystack.org + image: ${SOLMARA_ESIGNET_UI_IMAGE:?set digest-pinned eSignet UI image} + depends_on: {esignet: {condition: service_started}} + labels: {solmara.lab.host: esignet-ui.solmara.registrystack.org} esignet-seed: - image: ${SOLMARA_ESIGNET_SEED_IMAGE:?set digest-pinned Solmara eSignet seed image} + image: ${SOLMARA_ESIGNET_SEED_IMAGE:?set digest-pinned eSignet seed image} environment: PGHOST: esignet-database - PGPORT: "5432" - PGUSER: postgres - PGPASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD} + PGUSER: esignet + PGPASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?required} ESIGNET_CLIENT_ID: ${PORTAL_ESIGNET_CLIENT_ID:-solmara-portal} ESIGNET_CLIENT_KEY_ID: ${PORTAL_ESIGNET_CLIENT_KEY_ID:-solmara-portal-key-1} - ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64} - ESIGNET_CLIENT_REDIRECT_URIS_JSON: '["https://portal.solmara.registrystack.org/auth/callback"]' - ESIGNET_REQUIRE_HTTPS_REDIRECTS: "true" + ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?required} ESIGNET_REDIS_HOST: esignet-redis - ESIGNET_ACCOUNT_SOURCE_LABEL: NIA population Relay profile solmara-nia-userinfo - ESIGNET_DEMO_OTP: "111111" - ESIGNET_SEED_STAY_READY: "true" + ESIGNET_REQUIRE_HTTPS_REDIRECTS: "true" + ESIGNET_CLIENT_REDIRECT_URIS_JSON: '["https://portal.solmara.registrystack.org/auth/callback"]' entrypoint: ["seed-esignet.py"] - depends_on: - esignet-database: - condition: service_healthy - esignet: - condition: service_healthy - restart: unless-stopped - healthcheck: - test: ["CMD", "test", "-f", "/tmp/ready"] - interval: 10s - timeout: 5s - retries: 30 + depends_on: {esignet-database: {condition: service_healthy}, esignet: {condition: service_started}} + + # This file is an overlay on compose.coolify.yaml. It switches the main + # Portal from mock login to the hosted eSignet client seeded above. + portal: + environment: + PORTAL_AUTH_PROVIDER: esignet + PORTAL_SECURE_COOKIES: "true" + PORTAL_ESIGNET_CLIENT_ID: ${PORTAL_ESIGNET_CLIENT_ID:-solmara-portal} + PORTAL_ESIGNET_CLIENT_KEY_ID: ${PORTAL_ESIGNET_CLIENT_KEY_ID:-solmara-portal-key-1} + PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?required} + PORTAL_ESIGNET_ISSUER: https://esignet.solmara.registrystack.org + PORTAL_ESIGNET_AUTHORIZATION_ENDPOINT: https://esignet-ui.solmara.registrystack.org/authorize + PORTAL_ESIGNET_TOKEN_ENDPOINT: https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token + PORTAL_ESIGNET_CLIENT_ASSERTION_AUDIENCE: https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token + PORTAL_ESIGNET_USERINFO_ENDPOINT: https://esignet.solmara.registrystack.org/v1/esignet/oidc/userinfo + PORTAL_ESIGNET_REDIRECT_URI: https://portal.solmara.registrystack.org/auth/callback + PORTAL_ESIGNET_SCOPE: openid profile + PORTAL_ESIGNET_SUBJECT_CLAIM: sub volumes: - esignet-pgdata: + esignet-db-data: esignet-keystore: esignet-kyc-signing: esignet-redis-data: - nia-esignet-workload-token: diff --git a/compose.coolify.interior.yaml b/compose.coolify.interior.yaml index 3d0c719..2086ee1 100644 --- a/compose.coolify.interior.yaml +++ b/compose.coolify.interior.yaml @@ -1,379 +1,85 @@ -name: solmara-lab-interior +name: solmara-lab-interior-cells -x-relay-environment: &relay-environment - RUST_LOG: ${RUST_LOG:-info} - REGISTRY_RELAY_BIND: 0.0.0.0:8080 - -x-notary-environment: ¬ary-environment - RUST_LOG: ${RUST_LOG:-info} - REGISTRY_NOTARY_BIND: 0.0.0.0:8081 - REGISTRY_NOTARY_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - -x-relay-common: &relay-common - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: unless-stopped - environment: *relay-environment - healthcheck: - test: ["CMD", "/usr/local/bin/registry-relay", "healthcheck"] - interval: 30s - timeout: 10s - retries: 3 - -x-relay-bootstrap-common: &relay-bootstrap-common - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - -x-relay-config-state-init: &relay-config-state-init - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - user: "0:0" - restart: "no" - entrypoint: ["/bin/busybox", "sh", "-eu", "-c"] - -x-workload-agent-common: &workload-agent-common - image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} - command: ["python", "scripts/relay_workload_identity_agent.py"] - restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] - interval: 10s - timeout: 5s - retries: 6 - -x-notary-common: ¬ary-common - image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - user: "65534:65534" +x-runtime: &runtime restart: unless-stopped - environment: *notary-environment - healthcheck: - test: ["CMD", "/usr/local/bin/registry-notary", "healthcheck", "--url", "http://127.0.0.1:8081/ready"] - interval: 30s - timeout: 10s - retries: 3 + read_only: true + tmpfs: [/tmp] + networks: [runtime] + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + depends_on: + audit-permissions: {condition: service_completed_successfully} services: - postgres: - image: ${SOLMARA_POSTGRES_IMAGE:?set digest-pinned Solmara Postgres image} - entrypoint: ["/bin/sh", "/usr/local/bin/solmara-postgres-entrypoint.sh"] - command: - - postgres - - -c - - ssl=on - - -c - - ssl_cert_file=/var/lib/postgresql/server-ssl/server.crt - - -c - - ssl_key_file=/var/lib/postgresql/server-ssl/server.key - environment: &postgres-environment - POSTGRES_USER: ${SOLMARA_POSTGRES_USER:-solmara_registry} - POSTGRES_PASSWORD: ${SOLMARA_POSTGRES_PASSWORD} - POSTGRES_DB: ${SOLMARA_POSTGRES_DB:-solmara_lab} - SOLMARA_SOURCE_READER_DATABASES: nia - NIA_SOURCE_POSTGRES_READER_PASSWORD: ${NIA_SOURCE_POSTGRES_READER_PASSWORD} - REGISTRY_RELAY_STATE_EPOCH: ${REGISTRY_RELAY_STATE_EPOCH:-v015} - SOLMARA_RELAY_DATABASES: cra nia - SOLMARA_NOTARY_DATABASES: cra nia - CRA_RELAY_POSTGRES_RUNTIME_PASSWORD: ${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD} - CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - CRA_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${CRA_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - CRA_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${CRA_NOTARY_POSTGRES_RUNTIME_PASSWORD} - NIA_RELAY_POSTGRES_RUNTIME_PASSWORD: ${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD} - NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - NIA_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${NIA_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - NIA_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${NIA_NOTARY_POSTGRES_RUNTIME_PASSWORD} - volumes: - - postgres-data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] - interval: 5s - timeout: 5s - retries: 10 - - registry-postgresql-bootstrap: - image: ${SOLMARA_POSTGRES_IMAGE:?set digest-pinned Solmara Postgres image} - restart: "no" - entrypoint: ["/bin/sh", "/docker-entrypoint-initdb.d/010-init-notary-postgresql.sh"] - environment: - <<: *postgres-environment - PGHOST: postgres - PGPASSWORD: ${SOLMARA_POSTGRES_PASSWORD} - PGSSLMODE: require - depends_on: - postgres: - condition: service_healthy - - cra-workload-agent: - <<: *workload-agent-common - environment: - WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"cra-notary","subject":"cra-notary","scopes":["registry:consult:cra-child-benefit","registry:consult:cra-citizen-record","registry:consult:cra-pension-death"],"token_file":"/run/secrets/cra-notary-relay-token","private_jwk_env":"CRA_RELAY_WORKLOAD_JWK"}] - CRA_RELAY_WORKLOAD_JWK: ${CRA_RELAY_WORKLOAD_JWK} - volumes: - - cra-workload-token:/run/secrets - - nia-workload-agent: - <<: *workload-agent-common - environment: - WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"nia-notary","subject":"nia-notary","scopes":["registry:consult:nia-child-benefit","registry:consult:nia-citizen-status"],"token_file":"/run/secrets/nia-notary-relay-token","private_jwk_env":"NIA_RELAY_WORKLOAD_JWK"}] - NIA_RELAY_WORKLOAD_JWK: ${NIA_RELAY_WORKLOAD_JWK} - volumes: - - nia-workload-token:/run/secrets - - cra-relay-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/cra-civil/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - cra-relay-cache:/var/lib/registry-relay/cache - - nia-relay-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/nia-population/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - nia-relay-cache:/var/lib/registry-relay/cache - - cra-relay-consultation-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/cra-civil/consultation/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - cra-relay-consultation-cache:/var/lib/registry-relay/cache - - nia-relay-consultation-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/nia-population/consultation/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - nia-relay-consultation-cache:/var/lib/registry-relay/cache - - cra-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/cra-civil/consultation/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${CRA_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${CRA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - volumes: - - cra-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - cra-relay-consultation-config-state-init: - condition: service_completed_successfully - registry-postgresql-bootstrap: - condition: service_completed_successfully - - nia-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/nia-population/consultation/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NIA_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NIA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} - volumes: - - nia-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - nia-relay-consultation-config-state-init: - condition: service_completed_successfully - registry-postgresql-bootstrap: - condition: service_completed_successfully - - cra-civil-relay: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/cra-civil/bootstrap.yaml"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${CRA_RELAY_AUDIT_HASH_SECRET} - expose: ["8080"] - volumes: - - cra-relay-cache:/var/lib/registry-relay/cache - depends_on: - cra-relay-config-state-init: - condition: service_completed_successfully - labels: - solmara.lab.host: cra-relay.solmara.registrystack.org - - nia-population-relay: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/nia-population/bootstrap.yaml"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NIA_RELAY_AUDIT_HASH_SECRET} - SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} - DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - expose: ["8080"] - volumes: - - nia-relay-cache:/var/lib/registry-relay/cache - depends_on: - nia-relay-config-state-init: - condition: service_completed_successfully - labels: - solmara.lab.host: nia-relay.solmara.registrystack.org - - cra-civil-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/cra-civil/consultation/bootstrap.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${CRA_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${CRA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - expose: ["8081"] - volumes: - - cra-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - cra-relay-state-bootstrap: - condition: service_completed_successfully - cra-workload-agent: - condition: service_healthy - - nia-population-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/nia-population/consultation/bootstrap.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NIA_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NIA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} - DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - expose: ["8081"] - volumes: - - nia-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - nia-relay-state-bootstrap: - condition: service_completed_successfully - nia-workload-agent: - condition: service_healthy - - cra-notary: - <<: *notary-common - command: ["--config", "/etc/solmara/registry-projects/hosted/cra-civil/notary/notary.yaml"] - network_mode: service:cra-civil-relay-consultation - environment: &cra-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${CRA_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_cra_runtime:${CRA_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_cra?sslmode=require" - CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH: ${CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH} - CRA_PENSION_CLIENT_TOKEN_HASH: ${CRA_PENSION_CLIENT_TOKEN_HASH} - CRA_CITIZEN_CLIENT_TOKEN_HASH: ${CRA_CITIZEN_CLIENT_TOKEN_HASH} - volumes: - - cra-workload-token:/run/secrets:ro - depends_on: - cra-civil-relay-consultation: - condition: service_healthy - cra-notary-state-install: - condition: service_completed_successfully - cra-workload-agent: - condition: service_healthy - labels: - solmara.lab.host: cra-notary.solmara.registrystack.org - - cra-notary-state-install: - image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - command: ["--config", "/etc/solmara/registry-projects/hosted/cra-civil/notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_cra_owner", "--runtime-role", "solmara_notary_cra_runtime"] - environment: - <<: *cra-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_cra_migrator:${CRA_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_cra?sslmode=require" - volumes: - - cra-workload-token:/run/secrets:ro - depends_on: - cra-workload-agent: - condition: service_healthy - registry-postgresql-bootstrap: - condition: service_completed_successfully - - nia-notary: - <<: *notary-common - command: ["--config", "/etc/solmara/registry-projects/hosted/nia-population/notary/notary.yaml"] - network_mode: service:nia-population-relay-consultation - environment: &nia-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${NIA_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_nia_runtime:${NIA_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_nia?sslmode=require" - NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH: ${NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH} - NIA_CITIZEN_CLIENT_TOKEN_HASH: ${NIA_CITIZEN_CLIENT_TOKEN_HASH} - NIA_NOTARY_ISSUER_JWK: ${NIA_NOTARY_ISSUER_JWK} - volumes: - - nia-workload-token:/run/secrets:ro - depends_on: - nia-population-relay-consultation: - condition: service_healthy - nia-notary-state-install: - condition: service_completed_successfully - nia-workload-agent: - condition: service_healthy - labels: - solmara.lab.host: nia-notary.solmara.registrystack.org - - nia-notary-state-install: - image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} + audit-permissions: + image: ${SOLMARA_AUTHORITY_PROVISIONER_IMAGE:?set the digest-pinned Solmara authority provisioner image} + entrypoint: [python, /app/scripts/provision-hosted-runtime.py] + command: [init-audit, --destination, /audit/cra-relay, --destination, /audit/nia-relay, --destination, /audit/cra-evidence, --destination, /audit/nia-evidence, --uid, "65532", --gid, "65532"] restart: "no" - command: ["--config", "/etc/solmara/registry-projects/hosted/nia-population/notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_nia_owner", "--runtime-role", "solmara_notary_nia_runtime"] - environment: - <<: *nia-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_nia_migrator:${NIA_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_nia?sslmode=require" - volumes: - - nia-workload-token:/run/secrets:ro + user: "0:0" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + cap_add: [CHOWN, FOWNER] + security_opt: [no-new-privileges:true] + volumes: [cra-relay-audit:/audit/cra-relay, nia-relay-audit:/audit/nia-relay, cra-evidence-audit:/audit/cra-evidence, nia-evidence-audit:/audit/nia-evidence] + + cra-relay: + <<: *runtime + image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + command: [serve, --runtime, /etc/relay/cra/runtime.yaml] + environment: {SOLMARA_RELAY_AUDIT_KEY: "${CRA_RELAY_AUDIT_KEY:?required}"} + volumes: [cra-relay-runtime:/etc/relay/cra:ro, cra-relay-source:/var/lib/relay/source:ro, cra-relay-audit:/var/lib/relay/audit] + labels: {solmara.lab.host: cra-relay.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + + nia-relay: + <<: *runtime + image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + command: [serve, --runtime, /etc/relay/nia/runtime.yaml] + environment: {SOLMARA_RELAY_AUDIT_KEY: "${NIA_RELAY_AUDIT_KEY:?required}"} + volumes: [nia-relay-runtime:/etc/relay/nia:ro, nia-relay-source:/var/lib/relay/source:ro, nia-relay-audit:/var/lib/relay/audit] + labels: {solmara.lab.host: nia-relay.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + + cra-evidence: + <<: *runtime + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + command: [--runtime, /etc/registry-evidence/cra/runtime.yaml, serve] + volumes: [cra-evidence-runtime:/etc/registry-evidence/cra:ro, cra-evidence-secrets:/run/secrets/registry-evidence/cra:ro, cra-evidence-transit:/run/registry-evidence/cra:ro, cra-evidence-extracts:/var/lib/registry-evidence/cra/extracts:ro, cra-evidence-audit:/var/lib/registry-evidence/cra/audit] + networks: {runtime: {ipv4_address: 172.29.1.21}} depends_on: - nia-workload-agent: - condition: service_healthy - registry-postgresql-bootstrap: - condition: service_completed_successfully + audit-permissions: {condition: service_completed_successfully} + cra-relay: {condition: service_started} + labels: {solmara.lab.host: cra-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + + nia-evidence: + <<: *runtime + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + command: [--runtime, /etc/registry-evidence/nia/runtime.yaml, serve] + volumes: [nia-evidence-runtime:/etc/registry-evidence/nia:ro, nia-evidence-secrets:/run/secrets/registry-evidence/nia:ro, nia-evidence-transit:/run/registry-evidence/nia:ro, nia-evidence-extracts:/var/lib/registry-evidence/nia/extracts:ro, nia-evidence-audit:/var/lib/registry-evidence/nia/audit] + networks: {runtime: {ipv4_address: 172.29.1.22}} + labels: {solmara.lab.host: nia-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} volumes: - postgres-data: - cra-relay-cache: - cra-relay-consultation-cache: - nia-relay-cache: - nia-relay-consultation-cache: - cra-workload-token: - nia-workload-token: + cra-relay-runtime: {external: true, name: solmara-authority-cells-cra-relay-runtime} + cra-relay-source: {external: true, name: solmara-authority-cells-cra-relay-source} + nia-relay-runtime: {external: true, name: solmara-authority-cells-nia-relay-runtime} + nia-relay-source: {external: true, name: solmara-authority-cells-nia-relay-source} + cra-relay-audit: + nia-relay-audit: + cra-evidence-runtime: {external: true, name: solmara-authority-cells-cra-evidence-runtime} + cra-evidence-secrets: {external: true, name: solmara-authority-cells-cra-evidence-secrets} + cra-evidence-transit: {external: true, name: solmara-authority-cells-cra-evidence-transit} + cra-evidence-extracts: {external: true, name: solmara-authority-cells-cra-evidence-extracts} + cra-evidence-audit: + nia-evidence-runtime: {external: true, name: solmara-authority-cells-nia-evidence-runtime} + nia-evidence-secrets: {external: true, name: solmara-authority-cells-nia-evidence-secrets} + nia-evidence-transit: {external: true, name: solmara-authority-cells-nia-evidence-transit} + nia-evidence-extracts: {external: true, name: solmara-authority-cells-nia-evidence-extracts} + nia-evidence-audit: + superseded-state: {external: true, name: "${SOLMARA_SUPERSEDED_INTERIOR_VOLUME:-solmara-lab-interior-state}"} + +networks: + runtime: + ipam: + config: [{subnet: 172.29.1.0/24}] diff --git a/compose.coolify.labour-pensions.yaml b/compose.coolify.labour-pensions.yaml index 0ea82e6..133fbc7 100644 --- a/compose.coolify.labour-pensions.yaml +++ b/compose.coolify.labour-pensions.yaml @@ -1,241 +1,60 @@ -name: solmara-lab-labour-pensions +name: solmara-lab-pensions-cells -x-relay-environment: &relay-environment - RUST_LOG: ${RUST_LOG:-info} - REGISTRY_RELAY_BIND: 0.0.0.0:8080 - -x-notary-environment: ¬ary-environment - RUST_LOG: ${RUST_LOG:-info} - REGISTRY_NOTARY_BIND: 0.0.0.0:8081 - REGISTRY_NOTARY_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - -x-relay-common: &relay-common - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: unless-stopped - environment: *relay-environment - healthcheck: - test: ["CMD", "/usr/local/bin/registry-relay", "healthcheck"] - interval: 30s - timeout: 10s - retries: 3 - -x-relay-bootstrap-common: &relay-bootstrap-common - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - -x-relay-config-state-init: &relay-config-state-init - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - user: "0:0" - restart: "no" - entrypoint: ["/bin/busybox", "sh", "-eu", "-c"] - -x-workload-agent-common: &workload-agent-common - image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} - command: ["python", "scripts/relay_workload_identity_agent.py"] +x-runtime: &runtime restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] - interval: 10s - timeout: 5s - retries: 6 - -x-notary-common: ¬ary-common - image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - user: "65534:65534" - restart: unless-stopped - environment: *notary-environment - healthcheck: - test: ["CMD", "/usr/local/bin/registry-notary", "healthcheck", "--url", "http://127.0.0.1:8081/ready"] - interval: 30s - timeout: 10s - retries: 3 + read_only: true + tmpfs: [/tmp] + networks: [runtime] + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + depends_on: + audit-permissions: {condition: service_completed_successfully} services: - postgres: - image: ${SOLMARA_POSTGRES_IMAGE:?set digest-pinned Solmara Postgres image} - entrypoint: ["/bin/sh", "/usr/local/bin/solmara-postgres-entrypoint.sh"] - command: - - postgres - - -c - - ssl=on - - -c - - ssl_cert_file=/var/lib/postgresql/server-ssl/server.crt - - -c - - ssl_key_file=/var/lib/postgresql/server-ssl/server.key - environment: &postgres-environment - POSTGRES_USER: ${SOLMARA_POSTGRES_USER:-solmara_registry} - POSTGRES_PASSWORD: ${SOLMARA_POSTGRES_PASSWORD} - POSTGRES_DB: ${SOLMARA_POSTGRES_DB:-solmara_lab} - SOLMARA_SOURCE_READER_DATABASES: sipf - SIPF_SOURCE_POSTGRES_READER_PASSWORD: ${SIPF_SOURCE_POSTGRES_READER_PASSWORD} - REGISTRY_RELAY_STATE_EPOCH: ${REGISTRY_RELAY_STATE_EPOCH:-v015} - SOLMARA_RELAY_DATABASES: sipf - SOLMARA_NOTARY_DATABASES: sipf - SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD: ${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD} - SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - SIPF_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${SIPF_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - SIPF_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${SIPF_NOTARY_POSTGRES_RUNTIME_PASSWORD} - volumes: - - postgres-data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] - interval: 5s - timeout: 5s - retries: 10 - - registry-postgresql-bootstrap: - image: ${SOLMARA_POSTGRES_IMAGE:?set digest-pinned Solmara Postgres image} - restart: "no" - entrypoint: ["/bin/sh", "/docker-entrypoint-initdb.d/010-init-notary-postgresql.sh"] - environment: - <<: *postgres-environment - PGHOST: postgres - PGPASSWORD: ${SOLMARA_POSTGRES_PASSWORD} - PGSSLMODE: require - depends_on: - postgres: - condition: service_healthy - - sipf-workload-agent: - <<: *workload-agent-common - environment: - WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"sipf-notary","subject":"sipf-notary","scopes":["registry:consult:sipf-pension-payment-review","registry:consult:sipf-survivor-benefit"],"token_file":"/run/secrets/sipf-notary-relay-token","private_jwk_env":"SIPF_RELAY_WORKLOAD_JWK"}] - SIPF_RELAY_WORKLOAD_JWK: ${SIPF_RELAY_WORKLOAD_JWK} - volumes: - - sipf-workload-token:/run/secrets - - sipf-relay-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/sipf-pensions/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - sipf-relay-cache:/var/lib/registry-relay/cache - - sipf-relay-consultation-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/sipf-pensions/consultation/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - sipf-relay-consultation-cache:/var/lib/registry-relay/cache - - sipf-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/sipf-pensions/consultation/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SIPF_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SIPF_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} - volumes: - - sipf-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - sipf-relay-consultation-config-state-init: - condition: service_completed_successfully - registry-postgresql-bootstrap: - condition: service_completed_successfully - - sipf-pensions-relay: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/sipf-pensions/bootstrap.yaml"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SIPF_RELAY_AUDIT_HASH_SECRET} - SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} - DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - expose: ["8080"] - volumes: - - sipf-relay-cache:/var/lib/registry-relay/cache - depends_on: - sipf-relay-config-state-init: - condition: service_completed_successfully - labels: - solmara.lab.host: sipf-relay.solmara.registrystack.org - - sipf-pensions-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/sipf-pensions/consultation/bootstrap.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SIPF_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SIPF_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} - DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - expose: ["8081"] - volumes: - - sipf-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - sipf-relay-state-bootstrap: - condition: service_completed_successfully - sipf-workload-agent: - condition: service_healthy - - sipf-notary: - <<: *notary-common - command: ["--config", "/etc/solmara/registry-projects/hosted/sipf-pensions/notary/notary.yaml"] - network_mode: service:sipf-pensions-relay-consultation - environment: &sipf-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${SIPF_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_sipf_runtime:${SIPF_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_sipf?sslmode=require" - SIPF_PENSION_CLIENT_TOKEN_HASH: ${SIPF_PENSION_CLIENT_TOKEN_HASH} - SIPF_NOTARY_ISSUER_JWK: ${SIPF_NOTARY_ISSUER_JWK} - volumes: - - sipf-workload-token:/run/secrets:ro - depends_on: - sipf-pensions-relay-consultation: - condition: service_healthy - sipf-notary-state-install: - condition: service_completed_successfully - sipf-workload-agent: - condition: service_healthy - labels: - solmara.lab.host: sipf-notary.solmara.registrystack.org - - sipf-notary-state-install: - image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} + audit-permissions: + image: ${SOLMARA_AUTHORITY_PROVISIONER_IMAGE:?set the digest-pinned Solmara authority provisioner image} + entrypoint: [python, /app/scripts/provision-hosted-runtime.py] + command: [init-audit, --destination, /audit/sipf-relay, --destination, /audit/sipf-evidence, --uid, "65532", --gid, "65532"] restart: "no" - command: ["--config", "/etc/solmara/registry-projects/hosted/sipf-pensions/notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_sipf_owner", "--runtime-role", "solmara_notary_sipf_runtime"] - environment: - <<: *sipf-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_sipf_migrator:${SIPF_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_sipf?sslmode=require" - volumes: - - sipf-workload-token:/run/secrets:ro + user: "0:0" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + cap_add: [CHOWN, FOWNER] + security_opt: [no-new-privileges:true] + volumes: [sipf-relay-audit:/audit/sipf-relay, sipf-evidence-audit:/audit/sipf-evidence] + + sipf-relay: + <<: *runtime + image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + command: [serve, --runtime, /etc/relay/sipf/runtime.yaml] + environment: {SOLMARA_RELAY_AUDIT_KEY: "${SIPF_RELAY_AUDIT_KEY:?required}", SOLMARA_RELAY_CURSOR_KEY: "${SIPF_RELAY_CURSOR_KEY:?required}"} + volumes: [sipf-relay-runtime:/etc/relay/sipf:ro, sipf-relay-source:/var/lib/relay/source:ro, sipf-relay-audit:/var/lib/relay/audit] + labels: {solmara.lab.host: sipf-relay.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + + sipf-evidence: + <<: *runtime + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + command: [--runtime, /etc/registry-evidence/sipf/runtime.yaml, serve] + volumes: [sipf-evidence-runtime:/etc/registry-evidence/sipf:ro, sipf-evidence-secrets:/run/secrets/registry-evidence/sipf:ro, sipf-evidence-transit:/run/registry-evidence/sipf:ro, sipf-evidence-audit:/var/lib/registry-evidence/sipf/audit] + networks: {runtime: {ipv4_address: 172.29.1.25}} depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - sipf-workload-agent: - condition: service_healthy + audit-permissions: {condition: service_completed_successfully} + sipf-relay: {condition: service_started} + labels: {solmara.lab.host: sipf-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} volumes: - postgres-data: - sipf-relay-cache: - sipf-relay-consultation-cache: - sipf-workload-token: + sipf-relay-runtime: {external: true, name: solmara-authority-cells-sipf-relay-runtime} + sipf-relay-source: {external: true, name: solmara-authority-cells-sipf-relay-source} + sipf-relay-audit: + sipf-evidence-runtime: {external: true, name: solmara-authority-cells-sipf-evidence-runtime} + sipf-evidence-secrets: {external: true, name: solmara-authority-cells-sipf-evidence-secrets} + sipf-evidence-transit: {external: true, name: solmara-authority-cells-sipf-evidence-transit} + sipf-evidence-audit: + superseded-state: {external: true, name: "${SOLMARA_SUPERSEDED_PENSIONS_VOLUME:-solmara-lab-pensions-state}"} + +networks: + runtime: + ipam: + config: [{subnet: 172.29.1.0/24}] diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml new file mode 100644 index 0000000..9f0707e --- /dev/null +++ b/compose.coolify.provision.yaml @@ -0,0 +1,299 @@ +name: solmara-authority-cells + +# This operator-only application owns every active runtime volume. Runtime +# applications attach them by fixed external name and never receive write +# access to authored contracts, source publications, extracts, or secrets. +x-provisioner: &provisioner + image: ${SOLMARA_AUTHORITY_PROVISIONER_IMAGE:?set the digest-pinned Solmara authority provisioner image} + restart: "no" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + +x-transit-init: &transit-init + image: ${SOLMARA_TRANSIT_SIGNER_IMAGE:?set the digest-pinned Solmara Transit signer image} + entrypoint: [/usr/local/bin/python, -c] + command: ["import os,stat,sys; p='/transit'; s=os.lstat(p); ok=stat.S_ISDIR(s.st_mode) and not stat.S_ISLNK(s.st_mode); not ok and sys.exit(1); os.chown(p,65532,65532); os.chmod(p,0o700)"] + restart: "no" + user: "0:0" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + cap_add: [CHOWN, FOWNER] + security_opt: [no-new-privileges:true] + +x-signer: &signer + image: ${SOLMARA_TRANSIT_SIGNER_IMAGE:?set the digest-pinned Solmara Transit signer image} + restart: unless-stopped + user: "65532:65532" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + healthcheck: + test: [CMD, /usr/local/bin/python, -c, "import socket; s=socket.socket(socket.AF_UNIX); s.connect('/transit/transit-proxy.sock'); s.close()"] + interval: 2s + timeout: 1s + retries: 30 + start_period: 2s + +services: + cra-relay-provisioner: + <<: *provisioner + command: [provision, --target, cra-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source] + volumes: [cra-relay-runtime:/provisioned/runtime, cra-relay-source:/provisioned/source] + + nia-relay-provisioner: + <<: *provisioner + command: [provision, --target, nia-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source] + volumes: [nia-relay-runtime:/provisioned/runtime, nia-relay-source:/provisioned/source] + + mosd-relay-provisioner: + <<: *provisioner + command: [provision, --target, mosd-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source] + volumes: [mosd-relay-runtime:/provisioned/runtime, mosd-relay-source:/provisioned/source] + + sipf-relay-provisioner: + <<: *provisioner + command: [provision, --target, sipf-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source] + volumes: [sipf-relay-runtime:/provisioned/runtime, sipf-relay-source:/provisioned/source] + + nagdi-relay-provisioner: + <<: *provisioner + command: [provision, --target, nagdi-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source] + volumes: [nagdi-relay-runtime:/provisioned/runtime, nagdi-relay-source:/provisioned/source] + + cra-evidence-provisioner: + <<: *provisioner + command: [provision, --target, cra-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.1.21] + volumes: [cra-evidence-runtime:/provisioned/runtime, cra-evidence-secrets:/provisioned/secrets, cra-evidence-extracts:/provisioned/extracts] + secrets: + - {source: cra-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} + - {source: cra-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} + - {source: cra-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} + - {source: cra-pension-evidence-client-key, target: solmara-provisioning/cra-pension-evidence-client-key} + - {source: cra-citizen-evidence-client-key, target: solmara-provisioning/cra-citizen-evidence-client-key} + + nia-evidence-provisioner: + <<: *provisioner + command: [provision, --target, nia-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.1.22] + volumes: [nia-evidence-runtime:/provisioned/runtime, nia-evidence-secrets:/provisioned/secrets, nia-evidence-extracts:/provisioned/extracts] + secrets: + - {source: nia-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} + - {source: nia-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} + - {source: nia-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} + + sro-evidence-provisioner: + <<: *provisioner + command: [provision, --target, sro-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.1.23] + volumes: [sro-evidence-runtime:/provisioned/runtime, sro-evidence-secrets:/provisioned/secrets, sro-evidence-extracts:/provisioned/extracts] + secrets: + - {source: sro-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} + - {source: sro-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} + - {source: sro-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} + + mosd-evidence-provisioner: + <<: *provisioner + command: [provision, --target, mosd-programme-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.24] + volumes: [mosd-evidence-runtime:/provisioned/runtime, mosd-evidence-secrets:/provisioned/secrets] + secrets: + - {source: mosd-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} + - {source: mosd-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} + - {source: mosd-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} + - {source: mosd-child-benefit-evidence-client-key, target: solmara-provisioning/mosd-child-benefit-evidence-client-key} + + sipf-evidence-provisioner: + <<: *provisioner + command: [provision, --target, sipf-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.25] + volumes: [sipf-evidence-runtime:/provisioned/runtime, sipf-evidence-secrets:/provisioned/secrets] + secrets: + - {source: sipf-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} + - {source: sipf-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} + - {source: sipf-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} + - {source: sipf-pension-evidence-client-key, target: solmara-provisioning/sipf-pension-evidence-client-key} + - {source: sipf-survivor-evidence-client-key, target: solmara-provisioning/sipf-survivor-evidence-client-key} + + nagdi-evidence-provisioner: + <<: *provisioner + command: [provision, --target, nagdi-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.26] + volumes: [nagdi-evidence-runtime:/provisioned/runtime, nagdi-evidence-secrets:/provisioned/secrets] + secrets: + - {source: nagdi-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} + - {source: nagdi-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} + - {source: nagdi-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} + - {source: nagdi-voucher-evidence-client-key, target: solmara-provisioning/nagdi-voucher-evidence-client-key} + - {source: nagdi-livestock-evidence-client-key, target: solmara-provisioning/nagdi-livestock-evidence-client-key} + + mint-provisioner: + <<: *provisioner + command: [provision, --target, mint, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.20] + volumes: [mint-runtime:/provisioned/runtime, mint-secrets:/provisioned/secrets] + secrets: + - {source: mint-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} + - {source: mint-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} + - {source: cra-pension-evidence-client-public-jwk, target: solmara-provisioning/cra-pension-evidence-public.jwk} + - {source: cra-citizen-evidence-client-public-jwk, target: solmara-provisioning/cra-citizen-evidence-public.jwk} + - {source: mosd-child-benefit-evidence-client-public-jwk, target: solmara-provisioning/mosd-child-benefit-evidence-public.jwk} + - {source: sipf-pension-evidence-client-public-jwk, target: solmara-provisioning/sipf-pension-evidence-public.jwk} + - {source: sipf-survivor-evidence-client-public-jwk, target: solmara-provisioning/sipf-survivor-evidence-public.jwk} + - {source: nagdi-voucher-evidence-client-public-jwk, target: solmara-provisioning/nagdi-voucher-evidence-public.jwk} + - {source: nagdi-livestock-evidence-client-public-jwk, target: solmara-provisioning/nagdi-livestock-evidence-public.jwk} + - {source: nia-esignet-client-public-jwk, target: solmara-provisioning/nia-esignet-public.jwk} + - {source: solmara-demo-client-public-jwk, target: solmara-provisioning/solmara-demo-client-public.jwk} + + mint-transit-init: + <<: *transit-init + volumes: [mint-transit:/transit] + cra-transit-init: + <<: *transit-init + volumes: [cra-evidence-transit:/transit] + nia-transit-init: + <<: *transit-init + volumes: [nia-evidence-transit:/transit] + sro-transit-init: + <<: *transit-init + volumes: [sro-evidence-transit:/transit] + mosd-transit-init: + <<: *transit-init + volumes: [mosd-evidence-transit:/transit] + sipf-transit-init: + <<: *transit-init + volumes: [sipf-evidence-transit:/transit] + nagdi-transit-init: + <<: *transit-init + volumes: [nagdi-evidence-transit:/transit] + + mint-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-mint] + volumes: [mint-transit:/transit] + secrets: [{source: mint-signing-jwk, target: signing.jwk}, {source: mint-signing-public-jwk, target: signing-public.jwk}] + depends_on: + mint-transit-init: {condition: service_completed_successfully} + cra-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-cra] + volumes: [cra-evidence-transit:/transit] + secrets: [{source: cra-evidence-signing-jwk, target: signing.jwk}, {source: cra-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + cra-transit-init: {condition: service_completed_successfully} + nia-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-nia] + volumes: [nia-evidence-transit:/transit] + secrets: [{source: nia-evidence-signing-jwk, target: signing.jwk}, {source: nia-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + nia-transit-init: {condition: service_completed_successfully} + sro-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-sro] + volumes: [sro-evidence-transit:/transit] + secrets: [{source: sro-evidence-signing-jwk, target: signing.jwk}, {source: sro-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + sro-transit-init: {condition: service_completed_successfully} + mosd-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-mosd-programme] + volumes: [mosd-evidence-transit:/transit] + secrets: [{source: mosd-evidence-signing-jwk, target: signing.jwk}, {source: mosd-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + mosd-transit-init: {condition: service_completed_successfully} + sipf-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-sipf] + volumes: [sipf-evidence-transit:/transit] + secrets: [{source: sipf-evidence-signing-jwk, target: signing.jwk}, {source: sipf-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + sipf-transit-init: {condition: service_completed_successfully} + nagdi-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-nagdi] + volumes: [nagdi-evidence-transit:/transit] + secrets: [{source: nagdi-evidence-signing-jwk, target: signing.jwk}, {source: nagdi-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + nagdi-transit-init: {condition: service_completed_successfully} + +secrets: + mint-signing-jwk: {environment: MINT_SIGNING_JWK} + mint-signing-public-jwk: {environment: MINT_SIGNING_PUBLIC_JWK} + mint-audit-hmac-key: {environment: MINT_AUDIT_HMAC_KEY} + cra-evidence-signing-jwk: {environment: CRA_EVIDENCE_SIGNING_JWK} + cra-evidence-signing-public-jwk: {environment: CRA_EVIDENCE_SIGNING_PUBLIC_JWK} + cra-evidence-audit-hmac-key: {environment: CRA_EVIDENCE_AUDIT_HMAC_KEY} + cra-evidence-subject-binding-hmac-key: {environment: CRA_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} + nia-evidence-signing-jwk: {environment: NIA_EVIDENCE_SIGNING_JWK} + nia-evidence-signing-public-jwk: {environment: NIA_EVIDENCE_SIGNING_PUBLIC_JWK} + nia-evidence-audit-hmac-key: {environment: NIA_EVIDENCE_AUDIT_HMAC_KEY} + nia-evidence-subject-binding-hmac-key: {environment: NIA_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} + sro-evidence-signing-jwk: {environment: SRO_EVIDENCE_SIGNING_JWK} + sro-evidence-signing-public-jwk: {environment: SRO_EVIDENCE_SIGNING_PUBLIC_JWK} + sro-evidence-audit-hmac-key: {environment: SRO_EVIDENCE_AUDIT_HMAC_KEY} + sro-evidence-subject-binding-hmac-key: {environment: SRO_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} + mosd-evidence-signing-jwk: {environment: MOSD_EVIDENCE_SIGNING_JWK} + mosd-evidence-signing-public-jwk: {environment: MOSD_EVIDENCE_SIGNING_PUBLIC_JWK} + mosd-evidence-audit-hmac-key: {environment: MOSD_EVIDENCE_AUDIT_HMAC_KEY} + mosd-evidence-subject-binding-hmac-key: {environment: MOSD_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} + sipf-evidence-signing-jwk: {environment: SIPF_EVIDENCE_SIGNING_JWK} + sipf-evidence-signing-public-jwk: {environment: SIPF_EVIDENCE_SIGNING_PUBLIC_JWK} + sipf-evidence-audit-hmac-key: {environment: SIPF_EVIDENCE_AUDIT_HMAC_KEY} + sipf-evidence-subject-binding-hmac-key: {environment: SIPF_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} + nagdi-evidence-signing-jwk: {environment: NAGDI_EVIDENCE_SIGNING_JWK} + nagdi-evidence-signing-public-jwk: {environment: NAGDI_EVIDENCE_SIGNING_PUBLIC_JWK} + nagdi-evidence-audit-hmac-key: {environment: NAGDI_EVIDENCE_AUDIT_HMAC_KEY} + nagdi-evidence-subject-binding-hmac-key: {environment: NAGDI_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} + cra-pension-evidence-client-key: {environment: CRA_PENSION_EVIDENCE_CLIENT_PRIVATE_JWK} + cra-citizen-evidence-client-key: {environment: CRA_CITIZEN_EVIDENCE_CLIENT_PRIVATE_JWK} + mosd-child-benefit-evidence-client-key: {environment: MOSD_CHILD_BENEFIT_EVIDENCE_CLIENT_PRIVATE_JWK} + sipf-pension-evidence-client-key: {environment: SIPF_PENSION_EVIDENCE_CLIENT_PRIVATE_JWK} + sipf-survivor-evidence-client-key: {environment: SIPF_SURVIVOR_EVIDENCE_CLIENT_PRIVATE_JWK} + nagdi-voucher-evidence-client-key: {environment: NAGDI_VOUCHER_EVIDENCE_CLIENT_PRIVATE_JWK} + nagdi-livestock-evidence-client-key: {environment: NAGDI_LIVESTOCK_EVIDENCE_CLIENT_PRIVATE_JWK} + cra-pension-evidence-client-public-jwk: {environment: CRA_PENSION_EVIDENCE_CLIENT_PUBLIC_JWK} + cra-citizen-evidence-client-public-jwk: {environment: CRA_CITIZEN_EVIDENCE_CLIENT_PUBLIC_JWK} + mosd-child-benefit-evidence-client-public-jwk: {environment: MOSD_CHILD_BENEFIT_EVIDENCE_CLIENT_PUBLIC_JWK} + sipf-pension-evidence-client-public-jwk: {environment: SIPF_PENSION_EVIDENCE_CLIENT_PUBLIC_JWK} + sipf-survivor-evidence-client-public-jwk: {environment: SIPF_SURVIVOR_EVIDENCE_CLIENT_PUBLIC_JWK} + nagdi-voucher-evidence-client-public-jwk: {environment: NAGDI_VOUCHER_EVIDENCE_CLIENT_PUBLIC_JWK} + nagdi-livestock-evidence-client-public-jwk: {environment: NAGDI_LIVESTOCK_EVIDENCE_CLIENT_PUBLIC_JWK} + nia-esignet-client-public-jwk: {environment: NIA_ESIGNET_CLIENT_PUBLIC_JWK} + solmara-demo-client-public-jwk: {environment: SOLMARA_DEMO_CLIENT_PUBLIC_JWK} + +volumes: + mint-runtime: {name: solmara-authority-cells-mint-runtime} + mint-secrets: {name: solmara-authority-cells-mint-secrets} + mint-transit: {name: solmara-authority-cells-mint-transit} + cra-relay-runtime: {name: solmara-authority-cells-cra-relay-runtime} + cra-relay-source: {name: solmara-authority-cells-cra-relay-source} + nia-relay-runtime: {name: solmara-authority-cells-nia-relay-runtime} + nia-relay-source: {name: solmara-authority-cells-nia-relay-source} + mosd-relay-runtime: {name: solmara-authority-cells-mosd-relay-runtime} + mosd-relay-source: {name: solmara-authority-cells-mosd-relay-source} + sipf-relay-runtime: {name: solmara-authority-cells-sipf-relay-runtime} + sipf-relay-source: {name: solmara-authority-cells-sipf-relay-source} + nagdi-relay-runtime: {name: solmara-authority-cells-nagdi-relay-runtime} + nagdi-relay-source: {name: solmara-authority-cells-nagdi-relay-source} + cra-evidence-runtime: {name: solmara-authority-cells-cra-evidence-runtime} + cra-evidence-secrets: {name: solmara-authority-cells-cra-evidence-secrets} + cra-evidence-transit: {name: solmara-authority-cells-cra-evidence-transit} + cra-evidence-extracts: {name: solmara-authority-cells-cra-evidence-extracts} + nia-evidence-runtime: {name: solmara-authority-cells-nia-evidence-runtime} + nia-evidence-secrets: {name: solmara-authority-cells-nia-evidence-secrets} + nia-evidence-transit: {name: solmara-authority-cells-nia-evidence-transit} + nia-evidence-extracts: {name: solmara-authority-cells-nia-evidence-extracts} + sro-evidence-runtime: {name: solmara-authority-cells-sro-evidence-runtime} + sro-evidence-secrets: {name: solmara-authority-cells-sro-evidence-secrets} + sro-evidence-transit: {name: solmara-authority-cells-sro-evidence-transit} + sro-evidence-extracts: {name: solmara-authority-cells-sro-evidence-extracts} + mosd-evidence-runtime: {name: solmara-authority-cells-mosd-evidence-runtime} + mosd-evidence-secrets: {name: solmara-authority-cells-mosd-evidence-secrets} + mosd-evidence-transit: {name: solmara-authority-cells-mosd-evidence-transit} + sipf-evidence-runtime: {name: solmara-authority-cells-sipf-evidence-runtime} + sipf-evidence-secrets: {name: solmara-authority-cells-sipf-evidence-secrets} + sipf-evidence-transit: {name: solmara-authority-cells-sipf-evidence-transit} + nagdi-evidence-runtime: {name: solmara-authority-cells-nagdi-evidence-runtime} + nagdi-evidence-secrets: {name: solmara-authority-cells-nagdi-evidence-secrets} + nagdi-evidence-transit: {name: solmara-authority-cells-nagdi-evidence-transit} diff --git a/compose.coolify.social-development.yaml b/compose.coolify.social-development.yaml index 0e45f52..53a72a7 100644 --- a/compose.coolify.social-development.yaml +++ b/compose.coolify.social-development.yaml @@ -1,369 +1,73 @@ -name: solmara-lab-social-development +name: solmara-lab-social-cells -x-relay-environment: &relay-environment - RUST_LOG: ${RUST_LOG:-info} - REGISTRY_RELAY_BIND: 0.0.0.0:8080 - -x-notary-environment: ¬ary-environment - RUST_LOG: ${RUST_LOG:-info} - REGISTRY_NOTARY_BIND: 0.0.0.0:8081 - REGISTRY_NOTARY_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt - -x-relay-common: &relay-common - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: unless-stopped - environment: *relay-environment - healthcheck: - test: ["CMD", "/usr/local/bin/registry-relay", "healthcheck"] - interval: 30s - timeout: 10s - retries: 3 - -x-relay-bootstrap-common: &relay-bootstrap-common - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - -x-relay-config-state-init: &relay-config-state-init - image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - user: "0:0" - restart: "no" - entrypoint: ["/bin/busybox", "sh", "-eu", "-c"] - -x-workload-agent-common: &workload-agent-common - image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} - command: ["python", "scripts/relay_workload_identity_agent.py"] - restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] - interval: 10s - timeout: 5s - retries: 6 - -x-notary-common: ¬ary-common - image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - user: "65534:65534" +x-runtime: &runtime restart: unless-stopped - environment: *notary-environment - healthcheck: - test: ["CMD", "/usr/local/bin/registry-notary", "healthcheck", "--url", "http://127.0.0.1:8081/ready"] - interval: 30s - timeout: 10s - retries: 3 + read_only: true + tmpfs: [/tmp] + networks: [runtime] + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + depends_on: + audit-permissions: {condition: service_completed_successfully} services: - postgres: - image: ${SOLMARA_POSTGRES_IMAGE:?set digest-pinned Solmara Postgres image} - entrypoint: ["/bin/sh", "/usr/local/bin/solmara-postgres-entrypoint.sh"] - command: - - postgres - - -c - - ssl=on - - -c - - ssl_cert_file=/var/lib/postgresql/server-ssl/server.crt - - -c - - ssl_key_file=/var/lib/postgresql/server-ssl/server.key - environment: &postgres-environment - POSTGRES_USER: ${SOLMARA_POSTGRES_USER:-solmara_registry} - POSTGRES_PASSWORD: ${SOLMARA_POSTGRES_PASSWORD} - POSTGRES_DB: ${SOLMARA_POSTGRES_DB:-solmara_lab} - SOLMARA_SOURCE_READER_DATABASES: "" - REGISTRY_RELAY_STATE_EPOCH: ${REGISTRY_RELAY_STATE_EPOCH:-v015} - SOLMARA_RELAY_DATABASES: sro programme - SOLMARA_NOTARY_DATABASES: sro programme - SRO_RELAY_POSTGRES_RUNTIME_PASSWORD: ${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD} - SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - SRO_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${SRO_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - SRO_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${SRO_NOTARY_POSTGRES_RUNTIME_PASSWORD} - PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD: ${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD} - PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD: ${PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD} - PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD: ${PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD} - PROGRAMME_NOTARY_POSTGRES_MIGRATOR_PASSWORD: ${PROGRAMME_NOTARY_POSTGRES_MIGRATOR_PASSWORD} - PROGRAMME_NOTARY_POSTGRES_RUNTIME_PASSWORD: ${PROGRAMME_NOTARY_POSTGRES_RUNTIME_PASSWORD} - volumes: - - postgres-data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] - interval: 5s - timeout: 5s - retries: 10 - - registry-postgresql-bootstrap: - image: ${SOLMARA_POSTGRES_IMAGE:?set digest-pinned Solmara Postgres image} - restart: "no" - entrypoint: ["/bin/sh", "/docker-entrypoint-initdb.d/010-init-notary-postgresql.sh"] - environment: - <<: *postgres-environment - PGHOST: postgres - PGPASSWORD: ${SOLMARA_POSTGRES_PASSWORD} - PGSSLMODE: require - depends_on: - postgres: - condition: service_healthy - - sro-workload-agent: - <<: *workload-agent-common - environment: - WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"sro-notary","subject":"sro-notary","scopes":["registry:consult:child-benefit"],"token_file":"/run/secrets/sro-notary-relay-token","private_jwk_env":"SRO_RELAY_WORKLOAD_JWK"}] - SRO_RELAY_WORKLOAD_JWK: ${SRO_RELAY_WORKLOAD_JWK} - volumes: - - sro-workload-token:/run/secrets - - programme-workload-agent: - <<: *workload-agent-common - environment: - WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"programme-notary","subject":"programme-notary","scopes":["registry:consult:child-benefit"],"token_file":"/run/secrets/programme-notary-relay-token","private_jwk_env":"PROGRAMME_RELAY_WORKLOAD_JWK"}] - PROGRAMME_RELAY_WORKLOAD_JWK: ${PROGRAMME_RELAY_WORKLOAD_JWK} - volumes: - - programme-workload-token:/run/secrets - - sro-relay-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/sro-social/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - sro-relay-cache:/var/lib/registry-relay/cache - - programme-relay-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/mosd-programme/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - programme-relay-cache:/var/lib/registry-relay/cache - - sro-relay-consultation-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/sro-social/consultation/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - sro-relay-consultation-cache:/var/lib/registry-relay/cache - - programme-relay-consultation-config-state-init: - <<: *relay-config-state-init - command: - - | - /bin/busybox mkdir -p /var/lib/registry-relay/cache - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache - if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then - /bin/busybox cp /etc/solmara/hosted-relay-bundles/mosd-programme/consultation/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json - /bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json - fi - volumes: - - programme-relay-consultation-cache:/var/lib/registry-relay/cache - - sro-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/sro-social/consultation/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SRO_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SRO_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - volumes: - - sro-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - sro-relay-consultation-config-state-init: - condition: service_completed_successfully - registry-postgresql-bootstrap: - condition: service_completed_successfully - - programme-relay-state-bootstrap: - <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/mosd-programme/consultation/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${PROGRAMME_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_maintenance:${PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_keyring_reader:${PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - volumes: - - programme-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - programme-relay-consultation-config-state-init: - condition: service_completed_successfully - registry-postgresql-bootstrap: - condition: service_completed_successfully - - sro-social-relay: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/sro-social/bootstrap.yaml"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SRO_RELAY_AUDIT_HASH_SECRET} - expose: ["8080"] - volumes: - - sro-relay-cache:/var/lib/registry-relay/cache - depends_on: - sro-relay-config-state-init: - condition: service_completed_successfully - labels: - solmara.lab.host: sro-relay.solmara.registrystack.org - - programme-mis-relay: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/mosd-programme/bootstrap.yaml"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} - expose: ["8080"] - volumes: - - programme-relay-cache:/var/lib/registry-relay/cache - depends_on: - programme-relay-config-state-init: - condition: service_completed_successfully - labels: - solmara.lab.host: mosd-programme-relay.solmara.registrystack.org - - sro-social-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/sro-social/consultation/bootstrap.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SRO_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SRO_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - expose: ["8081"] - volumes: - - sro-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - sro-relay-state-bootstrap: - condition: service_completed_successfully - sro-workload-agent: - condition: service_healthy - - programme-mis-relay-consultation: - <<: *relay-common - command: ["--config", "/etc/solmara/hosted-relay-bundles/mosd-programme/consultation/bootstrap.yaml", "--bind", "127.0.0.1:8080"] - environment: - <<: *relay-environment - REGISTRY_RELAY_BIND: 127.0.0.1:8080 - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} - REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${PROGRAMME_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v015}?sslmode=require" - expose: ["8081"] - volumes: - - programme-relay-consultation-cache:/var/lib/registry-relay/cache - depends_on: - programme-relay-state-bootstrap: - condition: service_completed_successfully - programme-workload-agent: - condition: service_healthy - - sro-notary: - <<: *notary-common - command: ["--config", "/etc/solmara/registry-projects/hosted/sro-social/notary/notary.yaml"] - network_mode: service:sro-social-relay-consultation - environment: &sro-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${SRO_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_sro_runtime:${SRO_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_sro?sslmode=require" - SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH: ${SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH} - volumes: - - sro-workload-token:/run/secrets:ro - depends_on: - sro-social-relay-consultation: - condition: service_healthy - sro-notary-state-install: - condition: service_completed_successfully - sro-workload-agent: - condition: service_healthy - labels: - solmara.lab.host: sro-notary.solmara.registrystack.org - - sro-notary-state-install: - image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} - restart: "no" - command: ["--config", "/etc/solmara/registry-projects/hosted/sro-social/notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_sro_owner", "--runtime-role", "solmara_notary_sro_runtime"] - environment: - <<: *sro-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_sro_migrator:${SRO_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_sro?sslmode=require" - volumes: - - sro-workload-token:/run/secrets:ro - depends_on: - registry-postgresql-bootstrap: - condition: service_completed_successfully - sro-workload-agent: - condition: service_healthy - - programme-notary: - <<: *notary-common - command: ["--config", "/etc/solmara/registry-projects/hosted/mosd-programme/notary/notary.yaml"] - network_mode: service:programme-mis-relay-consultation - environment: &programme-notary-environment - <<: *notary-environment - REGISTRY_NOTARY_AUDIT_HASH_SECRET: ${PROGRAMME_NOTARY_AUDIT_HASH_SECRET} - REGISTRY_NOTARY_POSTGRES_URL: "postgresql://solmara_notary_programme_runtime:${PROGRAMME_NOTARY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_notary_programme?sslmode=require" - PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH: ${PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH} - volumes: - - programme-workload-token:/run/secrets:ro - depends_on: - programme-mis-relay-consultation: - condition: service_healthy - programme-notary-state-install: - condition: service_completed_successfully - programme-workload-agent: - condition: service_healthy - labels: - solmara.lab.host: programme-notary.solmara.registrystack.org - - programme-notary-state-install: - image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} - platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} + audit-permissions: + image: ${SOLMARA_AUTHORITY_PROVISIONER_IMAGE:?set the digest-pinned Solmara authority provisioner image} + entrypoint: [python, /app/scripts/provision-hosted-runtime.py] + command: [init-audit, --destination, /audit/mosd-relay, --destination, /audit/sro-evidence, --destination, /audit/mosd-evidence, --uid, "65532", --gid, "65532"] restart: "no" - command: ["--config", "/etc/solmara/registry-projects/hosted/mosd-programme/notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_programme_owner", "--runtime-role", "solmara_notary_programme_runtime"] - environment: - <<: *programme-notary-environment - REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL: "postgresql://solmara_notary_programme_migrator:${PROGRAMME_NOTARY_POSTGRES_MIGRATOR_PASSWORD}@postgres:5432/solmara_notary_programme?sslmode=require" - volumes: - - programme-workload-token:/run/secrets:ro + user: "0:0" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + cap_add: [CHOWN, FOWNER] + security_opt: [no-new-privileges:true] + volumes: [mosd-relay-audit:/audit/mosd-relay, sro-evidence-audit:/audit/sro-evidence, mosd-evidence-audit:/audit/mosd-evidence] + + mosd-relay: + <<: *runtime + image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + command: [serve, --runtime, /etc/relay/mosd/runtime.yaml] + environment: {SOLMARA_RELAY_AUDIT_KEY: "${MOSD_RELAY_AUDIT_KEY:?required}"} + volumes: [mosd-relay-runtime:/etc/relay/mosd:ro, mosd-relay-source:/var/lib/relay/source:ro, mosd-relay-audit:/var/lib/relay/audit] + labels: {solmara.lab.host: mosd-programme-relay.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + + sro-evidence: + <<: *runtime + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + command: [--runtime, /etc/registry-evidence/sro/runtime.yaml, serve] + volumes: [sro-evidence-runtime:/etc/registry-evidence/sro:ro, sro-evidence-secrets:/run/secrets/registry-evidence/sro:ro, sro-evidence-transit:/run/registry-evidence/sro:ro, sro-evidence-extracts:/var/lib/registry-evidence/sro/extracts:ro, sro-evidence-audit:/var/lib/registry-evidence/sro/audit] + networks: {runtime: {ipv4_address: 172.29.1.23}} + labels: {solmara.lab.host: sro-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + + mosd-programme-evidence: + <<: *runtime + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + command: [--runtime, /etc/registry-evidence/mosd-programme/runtime.yaml, serve] + volumes: [mosd-evidence-runtime:/etc/registry-evidence/mosd-programme:ro, mosd-evidence-secrets:/run/secrets/registry-evidence/mosd-programme:ro, mosd-evidence-transit:/run/registry-evidence/mosd-programme:ro, mosd-evidence-audit:/var/lib/registry-evidence/mosd-programme/audit] + networks: {runtime: {ipv4_address: 172.29.1.24}} depends_on: - programme-workload-agent: - condition: service_healthy - registry-postgresql-bootstrap: - condition: service_completed_successfully + audit-permissions: {condition: service_completed_successfully} + mosd-relay: {condition: service_started} + labels: {solmara.lab.host: mosd-programme-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} volumes: - postgres-data: - sro-relay-cache: - sro-relay-consultation-cache: - programme-relay-cache: - programme-relay-consultation-cache: - sro-workload-token: - programme-workload-token: + mosd-relay-runtime: {external: true, name: solmara-authority-cells-mosd-relay-runtime} + mosd-relay-source: {external: true, name: solmara-authority-cells-mosd-relay-source} + mosd-relay-audit: + sro-evidence-runtime: {external: true, name: solmara-authority-cells-sro-evidence-runtime} + sro-evidence-secrets: {external: true, name: solmara-authority-cells-sro-evidence-secrets} + sro-evidence-transit: {external: true, name: solmara-authority-cells-sro-evidence-transit} + sro-evidence-extracts: {external: true, name: solmara-authority-cells-sro-evidence-extracts} + sro-evidence-audit: + mosd-evidence-runtime: {external: true, name: solmara-authority-cells-mosd-evidence-runtime} + mosd-evidence-secrets: {external: true, name: solmara-authority-cells-mosd-evidence-secrets} + mosd-evidence-transit: {external: true, name: solmara-authority-cells-mosd-evidence-transit} + mosd-evidence-audit: + superseded-state: {external: true, name: "${SOLMARA_SUPERSEDED_SOCIAL_VOLUME:-solmara-lab-social-state}"} + +networks: + runtime: + ipam: + config: [{subnet: 172.29.1.0/24}] diff --git a/compose.coolify.walt.yaml b/compose.coolify.walt.yaml index 47d37d2..1776511 100644 --- a/compose.coolify.walt.yaml +++ b/compose.coolify.walt.yaml @@ -1,136 +1,18 @@ name: solmara-lab-wallet - -# Self-hosted walt.id holder wallet for the Solmara hosted demo. -# -# Only the holder side is deployed (wallet-api + web wallet). Registry Stack -# issuance stays with the relevant authority-owned Notary, so walt's -# issuer-api, verifier-api, and portal are omitted. -# -# Ingress: Caddy is the single public entrypoint. Coolify/Traefik terminates TLS -# for wallet.solmara.registrystack.org and forwards to caddy:7101, which serves -# the web wallet and proxies /wallet-api/* to wallet-api:7001 on the same origin. - -x-hosted-domains: - caddy: wallet.solmara.registrystack.org - services: - config-loader: - image: ${CONFIG_LOADER_IMAGE:-alpine:3.20} - restart: "no" - environment: - CONFIG_REPO_URL: ${CONFIG_REPO_URL:-https://github.com/registrystack/solmara-lab} - CONFIG_REPO_REF: ${CONFIG_REPO_REF:?set CONFIG_REPO_REF to the deployed solmara-lab git ref} - entrypoint: ["/bin/sh", "-euc"] - command: - - | - apk add --no-cache git >/dev/null - rm -rf /tmp/repo - git init /tmp/repo - git -C /tmp/repo remote add origin "$$CONFIG_REPO_URL" - git -C /tmp/repo fetch --depth 1 origin "$$CONFIG_REPO_REF" - git -C /tmp/repo -c advice.detachedHead=false checkout FETCH_HEAD - for d in walletconf caddy; do - mkdir -p "/out/$$d" - find "/out/$$d" -mindepth 1 -delete 2>/dev/null || true - done - cp -a /tmp/repo/config/walt/. /out/walletconf/ - rm -f /out/walletconf/Caddyfile /out/walletconf/README.md - cp -a /tmp/repo/config/walt/Caddyfile /out/caddy/Caddyfile - echo "config-loader: populated walt config volumes from $$CONFIG_REPO_REF" - volumes: - - cfg-walt:/out/walletconf - - cfg-walt-caddy:/out/caddy - - walt-postgres: - image: ${WALT_POSTGRES_IMAGE:-postgres:16-alpine} - environment: - POSTGRES_DB: ${WALT_DB_NAME:-waltid} - POSTGRES_USER: ${WALT_DB_USERNAME:-waltid} - POSTGRES_PASSWORD: ${WALT_DB_PASSWORD:?set WALT_DB_PASSWORD in Coolify} - depends_on: - config-loader: - condition: service_completed_successfully - volumes: - - walt-pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] - interval: 5s - timeout: 5s - retries: 30 - start_period: 10s - restart: unless-stopped - wallet-api: - image: ${WALT_IMAGE_PREFIX:-docker.io/}waltid/wallet-api:${WALT_IMAGE_TAG:-0.20.2} - depends_on: - walt-postgres: - condition: service_healthy - config-loader: - condition: service_completed_successfully - environment: - WALLET_BACKEND_PORT: "7001" - SERVICE_HOST: wallet.solmara.registrystack.org - DATABASE_ENGINE: postgres - POSTGRES_DB_HOST: walt-postgres - POSTGRES_DB_PORT: "5432" - DB_NAME: ${WALT_DB_NAME:-waltid} - DB_USERNAME: ${WALT_DB_USERNAME:-waltid} - DB_PASSWORD: ${WALT_DB_PASSWORD:?set WALT_DB_PASSWORD in Coolify} - WALT_AUTH_ENCRYPTION_KEY: ${WALT_AUTH_ENCRYPTION_KEY:?set WALT_AUTH_ENCRYPTION_KEY in Coolify} - WALT_AUTH_SIGN_KEY: ${WALT_AUTH_SIGN_KEY:?set WALT_AUTH_SIGN_KEY in Coolify} - WALT_AUTH_TOKEN_KEY: ${WALT_AUTH_TOKEN_KEY:?set WALT_AUTH_TOKEN_KEY in Coolify} - WALT_KTOR_SIGNING_KEY: ${WALT_KTOR_SIGNING_KEY:?set WALT_KTOR_SIGNING_KEY in Coolify} - WALT_KTOR_VERIFICATION_KEY: ${WALT_KTOR_VERIFICATION_KEY:?set WALT_KTOR_VERIFICATION_KEY in Coolify} - expose: - - "7001" - volumes: - - cfg-walt:/waltid-wallet-api/config:ro - - walt-data:/waltid-wallet-api/data - healthcheck: - test: ["CMD", "curl", "-s", "-o", "/dev/null", "http://127.0.0.1:7001/"] - interval: 15s - timeout: 5s - retries: 10 - start_period: 30s + image: docker.io/waltid/wallet-api@sha256:2c10973dfbf7ecfa07bc45c35499b2689bdba1217cc95e05d1c18d0d88d9a7c6 restart: unless-stopped - - waltid-demo-wallet: - image: ${WALT_IMAGE_PREFIX:-docker.io/}waltid/waltid-demo-wallet:${WALT_IMAGE_TAG:-0.20.2} - depends_on: - wallet-api: - condition: service_started - environment: - NUXT_PUBLIC_ISSUER_CALLBACK_URL: https://wallet.solmara.registrystack.org - PORT: "7101" - expose: - - "7101" + environment: {DATABASE_ENGINE: sqlite, SQLITE_FILE: /data/wallet.sqlite} + volumes: ["wallet-data:/data"] + wallet-ui: + image: docker.io/waltid/waltid-demo-wallet@sha256:e7778f62b7ad812e9191151a958495d6493b7c81d176f23a018929aa5deb3274 restart: unless-stopped - caddy: - image: ${WALT_CADDY_IMAGE:-docker.io/caddy:2} - depends_on: - config-loader: - condition: service_completed_successfully - waltid-demo-wallet: - condition: service_started - wallet-api: - condition: service_started - expose: - - "7101" - volumes: - - cfg-walt-caddy:/etc/caddy:ro - healthcheck: - test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:7101/"] - interval: 15s - timeout: 5s - retries: 10 - start_period: 20s + image: docker.io/library/caddy@sha256:844f60b64e4724a5aa8245e019dace0d3f199f7433ce6c57676cb30a920dbad9 restart: unless-stopped - labels: - solmara.lab.host: wallet.solmara.registrystack.org - + volumes: ["./config/walt/Caddyfile:/etc/caddy/Caddyfile:ro", "wallet-caddy-data:/data"] + labels: {solmara.lab.host: wallet.solmara.registrystack.org} volumes: - cfg-walt: - cfg-walt-caddy: - walt-pgdata: - walt-data: + wallet-data: + wallet-caddy-data: diff --git a/compose.coolify.yaml b/compose.coolify.yaml index 6b712eb..acd1d07 100644 --- a/compose.coolify.yaml +++ b/compose.coolify.yaml @@ -1,179 +1,133 @@ -name: solmara-lab-core - -# Core hosted Coolify compose. Keep this file and the per-authority -# compose.coolify.*.yaml files free of custom Docker networks; Coolify owns the -# proxy network and domains. Cross-authority calls use public TLS endpoints. - +name: solmara-lab +x-hosted-evidence-environment: &hosted-evidence + SOLMARA_LAB_MODE: hosted + SOLMARA_MINT_URL: https://mint.solmara.registrystack.org + SOLMARA_MINT_ASSERTION_AUDIENCE: https://mint.solmara.registrystack.org/token + SOLMARA_EVIDENCE_CLIENT_ID: solmara-demo + SOLMARA_EVIDENCE_CLIENT_KEY: /run/secrets/solmara-evidence-client.jwk + SOLMARA_CRA_EVIDENCE_URL: https://cra-evidence.solmara.registrystack.org + SOLMARA_NIA_EVIDENCE_URL: https://nia-evidence.solmara.registrystack.org + SOLMARA_SRO_EVIDENCE_URL: https://sro-evidence.solmara.registrystack.org + SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL: https://mosd-programme-evidence.solmara.registrystack.org + SOLMARA_SIPF_EVIDENCE_URL: https://sipf-evidence.solmara.registrystack.org + SOLMARA_NAGDI_EVIDENCE_URL: https://nagdi-evidence.solmara.registrystack.org + CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN:?required} services: + audit-permissions: + image: ${SOLMARA_AUTHORITY_PROVISIONER_IMAGE:?set the digest-pinned Solmara authority provisioner image} + entrypoint: [python, /app/scripts/provision-hosted-runtime.py] + command: [init-audit, --destination, /audit/mint, --uid, "65532", --gid, "65532"] + restart: "no" + user: "0:0" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + cap_add: [CHOWN, FOWNER] + security_opt: [no-new-privileges:true] + volumes: [mint-audit:/audit/mint] + mint: + image: ${SOLMARA_MINT_IMAGE:?set the digest-pinned Solmara Mint image built from the verified release binary} + command: ["serve", "--config", "/etc/registry-mint/mint.yaml"] + restart: unless-stopped + read_only: true + tmpfs: [/tmp] + volumes: ["mint-runtime:/etc/registry-mint:ro", "mint-secrets:/run/secrets/registry-mint:ro", "mint-transit:/run/registry-mint:ro", "mint-audit:/var/lib/registry-mint"] + networks: {runtime: {ipv4_address: 172.29.1.20}} + depends_on: + audit-permissions: {condition: service_completed_successfully} + labels: {solmara.lab.host: mint.solmara.registrystack.org, solmara.rollout.slot: authority-cells} static-metadata: - image: ${SOLMARA_STATIC_METADATA_IMAGE:?set digest-pinned Solmara static metadata image} - command: ["python", "-m", "http.server", "8080", "--bind", "0.0.0.0", "--directory", "/srv/static"] - labels: - solmara.lab.host: metadata.solmara.registrystack.org - - workload-issuer: - image: ${SOLMARA_STATIC_METADATA_IMAGE:?set digest-pinned Solmara static metadata image} - command: ["python", "-m", "http.server", "8080", "--bind", "0.0.0.0", "--directory", "/srv/static"] - healthcheck: - test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/.well-known/jwks.json"] - interval: 30s - timeout: 10s - retries: 3 - labels: - solmara.lab.host: workload-issuer.solmara.registrystack.org - + image: ${SOLMARA_STATIC_METADATA_IMAGE:?set a digest-pinned Solmara static metadata image} + restart: unless-stopped + labels: {solmara.lab.host: metadata.solmara.registrystack.org} scenario-runner: - image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} + image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set a digest-pinned Solmara scenario runner image} + restart: unless-stopped environment: + <<: *hosted-evidence + CHILD_BENEFIT_FEDERATOR_URL: https://child-benefit.solmara.registrystack.org SCENARIO_RUNNER_HOST: 0.0.0.0 - SCENARIO_RUNNER_PORT: 8080 - SOLMARA_LAB_MODE: hosted - CHILD_BENEFIT_FEDERATOR_URL: http://child-benefit-federator:8080 - CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN} - CRA_NOTARY_URL: https://cra-notary.solmara.registrystack.org - CRA_PENSION_CLIENT_TOKEN: ${CRA_PENSION_CLIENT_TOKEN} - CRA_CITIZEN_CLIENT_TOKEN: ${CRA_CITIZEN_CLIENT_TOKEN} - NIA_NOTARY_URL: https://nia-notary.solmara.registrystack.org - NIA_CITIZEN_CLIENT_TOKEN: ${NIA_CITIZEN_CLIENT_TOKEN} - SIPF_NOTARY_URL: https://sipf-notary.solmara.registrystack.org - SIPF_PENSION_CLIENT_TOKEN: ${SIPF_PENSION_CLIENT_TOKEN} - NAGDI_NOTARY_URL: https://nagdi-notary.solmara.registrystack.org - NAGDI_NOTARY_TOKEN: ${NAGDI_NOTARY_TOKEN} - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3)\""] - interval: 30s - timeout: 10s - retries: 3 - depends_on: - child-benefit-federator: - condition: service_healthy - labels: - solmara.lab.visibility: internal - + SCENARIO_RUNNER_PORT: "8080" + secrets: + - source: solmara-demo-client-key + target: solmara-evidence-client.jwk + labels: {solmara.lab.host: scenarios.solmara.registrystack.org} child-benefit-federator: - image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} + image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set a digest-pinned Solmara scenario runner image} command: ["python", "scenario-runner/child_benefit_federator.py"] + restart: unless-stopped environment: + <<: *hosted-evidence CHILD_BENEFIT_FEDERATOR_HOST: 0.0.0.0 - CHILD_BENEFIT_FEDERATOR_PORT: 8080 - CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN} - CRA_NOTARY_URL: https://cra-notary.solmara.registrystack.org - CRA_CHILD_BENEFIT_CLIENT_TOKEN: ${CRA_CHILD_BENEFIT_CLIENT_TOKEN} - NIA_NOTARY_URL: https://nia-notary.solmara.registrystack.org - NIA_CHILD_BENEFIT_CLIENT_TOKEN: ${NIA_CHILD_BENEFIT_CLIENT_TOKEN} - SRO_NOTARY_URL: https://sro-notary.solmara.registrystack.org - SRO_CHILD_BENEFIT_CLIENT_TOKEN: ${SRO_CHILD_BENEFIT_CLIENT_TOKEN} - PROGRAMME_NOTARY_URL: https://programme-notary.solmara.registrystack.org - PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN: ${PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN} - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3)\""] - interval: 30s - timeout: 10s - retries: 3 - labels: - solmara.lab.host: child-benefit-federator.solmara.registrystack.org - + CHILD_BENEFIT_FEDERATOR_PORT: "8080" + secrets: + - source: solmara-demo-client-key + target: solmara-evidence-client.jwk + labels: {solmara.lab.host: child-benefit.solmara.registrystack.org} home: - image: ${SOLMARA_HOME_IMAGE:?set digest-pinned Solmara home image} + image: ${SOLMARA_HOME_IMAGE:?set a digest-pinned Solmara home image} + restart: unless-stopped environment: - HOST: 0.0.0.0 - PORT: 4301 - SOLMARA_LAB_ROOT: /app - REPO_URL: ${REPO_URL:-https://github.com/registrystack/solmara-lab} PORTAL_URL: https://portal.solmara.registrystack.org - SCENARIO_RUNNER_URL: http://scenario-runner:8080 - STATIC_METADATA_URL: http://static-metadata:8080 - PORTAL_PROBE_URL: http://portal:4000 + STATIC_METADATA_URL: https://metadata.solmara.registrystack.org CRA_CIVIL_RELAY_URL: https://cra-relay.solmara.registrystack.org NIA_POPULATION_RELAY_URL: https://nia-relay.solmara.registrystack.org - SRO_SOCIAL_RELAY_URL: https://sro-relay.solmara.registrystack.org - PROGRAMME_MIS_RELAY_URL: https://mosd-programme-relay.solmara.registrystack.org + MOSD_PROGRAMME_RELAY_URL: https://mosd-programme-relay.solmara.registrystack.org SIPF_PENSIONS_RELAY_URL: https://sipf-relay.solmara.registrystack.org NAGDI_AGRICULTURE_RELAY_URL: https://nagdi-relay.solmara.registrystack.org - CHILD_BENEFIT_FEDERATOR_URL: https://child-benefit-federator.solmara.registrystack.org - CRA_NOTARY_URL: https://cra-notary.solmara.registrystack.org - NIA_NOTARY_URL: https://nia-notary.solmara.registrystack.org - SRO_NOTARY_URL: https://sro-notary.solmara.registrystack.org - PROGRAMME_NOTARY_URL: https://programme-notary.solmara.registrystack.org - SIPF_NOTARY_URL: https://sipf-notary.solmara.registrystack.org - NAGDI_NOTARY_URL: https://nagdi-notary.solmara.registrystack.org - HOME_PUBLISHED_TOKENS: >- - {"child-benefit-federator":"${CHILD_BENEFIT_FEDERATOR_TOKEN}", - "cra-pension-client":"${CRA_PENSION_CLIENT_TOKEN}", - "cra-citizen-client":"${CRA_CITIZEN_CLIENT_TOKEN}", - "nia-citizen-client":"${NIA_CITIZEN_CLIENT_TOKEN}", - "sipf-pension-client":"${SIPF_PENSION_CLIENT_TOKEN}", - "nagdi-notary":"${NAGDI_NOTARY_TOKEN}"} + SOLMARA_CRA_EVIDENCE_URL: https://cra-evidence.solmara.registrystack.org + SOLMARA_NIA_EVIDENCE_URL: https://nia-evidence.solmara.registrystack.org + SOLMARA_SRO_EVIDENCE_URL: https://sro-evidence.solmara.registrystack.org + SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL: https://mosd-programme-evidence.solmara.registrystack.org + SOLMARA_SIPF_EVIDENCE_URL: https://sipf-evidence.solmara.registrystack.org + SOLMARA_NAGDI_EVIDENCE_URL: https://nagdi-evidence.solmara.registrystack.org + MINT_URL: https://mint.solmara.registrystack.org + CHILD_BENEFIT_FEDERATOR_URL: https://child-benefit.solmara.registrystack.org + SCENARIO_RUNNER_URL: https://scenarios.solmara.registrystack.org + PORTAL_PROBE_URL: https://portal.solmara.registrystack.org SOLMARA_PUBLIC_URL_MAP: >- - {"child-benefit-federator:8080":"https://child-benefit-federator.solmara.registrystack.org", - "cra-notary:8081":"https://cra-notary.solmara.registrystack.org", - "nia-notary:8081":"https://nia-notary.solmara.registrystack.org", - "sro-notary:8081":"https://sro-notary.solmara.registrystack.org", - "programme-notary:8081":"https://programme-notary.solmara.registrystack.org", - "sipf-notary:8081":"https://sipf-notary.solmara.registrystack.org", - "nagdi-notary:8081":"https://nagdi-notary.solmara.registrystack.org", - "cra-civil-relay:8080":"https://cra-relay.solmara.registrystack.org", - "nia-population-relay:8080":"https://nia-relay.solmara.registrystack.org", - "sro-social-relay:8080":"https://sro-relay.solmara.registrystack.org", - "programme-mis-relay:8080":"https://mosd-programme-relay.solmara.registrystack.org", - "sipf-pensions-relay:8080":"https://sipf-relay.solmara.registrystack.org", - "nagdi-agriculture-relay:8080":"https://nagdi-relay.solmara.registrystack.org", + {"child-benefit-federator:8080":"https://child-benefit.solmara.registrystack.org", + "deterministic-publisher:8080":"https://metadata.solmara.registrystack.org", + "cra-evidence:8080":"https://cra-evidence.solmara.registrystack.org", + "nia-evidence:8080":"https://nia-evidence.solmara.registrystack.org", + "sro-evidence:8080":"https://sro-evidence.solmara.registrystack.org", + "mosd-programme-evidence:8080":"https://mosd-programme-evidence.solmara.registrystack.org", + "sipf-evidence:8080":"https://sipf-evidence.solmara.registrystack.org", + "nagdi-evidence:8080":"https://nagdi-evidence.solmara.registrystack.org", + "mint:8081":"https://mint.solmara.registrystack.org", + "cra-relay:8080":"https://cra-relay.solmara.registrystack.org", + "nia-relay:8080":"https://nia-relay.solmara.registrystack.org", + "mosd-relay:8080":"https://mosd-programme-relay.solmara.registrystack.org", + "sipf-relay:8080":"https://sipf-relay.solmara.registrystack.org", + "nagdi-relay:8080":"https://nagdi-relay.solmara.registrystack.org", "static-metadata:8080":"https://metadata.solmara.registrystack.org", + "scenario-runner:8080":"https://scenarios.solmara.registrystack.org", "portal:4000":"https://portal.solmara.registrystack.org"} - UMAMI_WEBSITE_ID: ${UMAMI_WEBSITE_ID:-} - healthcheck: - test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:4301/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] - interval: 30s - timeout: 10s - retries: 3 - depends_on: - static-metadata: - condition: service_started - scenario-runner: - condition: service_healthy - labels: - solmara.lab.host: solmara.registrystack.org - + labels: {solmara.lab.host: solmara.registrystack.org} portal: - image: ${SOLMARA_PORTAL_IMAGE:?set digest-pinned Solmara portal image} + image: ${SOLMARA_PORTAL_IMAGE:?set a digest-pinned Solmara portal image} + restart: unless-stopped environment: - HOST: 0.0.0.0 - PORT: 4000 + <<: *hosted-evidence PORTAL_PROVIDER: live - PORTAL_SESSION_SECRET: ${PORTAL_SESSION_SECRET} - PORTAL_AUTH_PROVIDER: esignet + PORTAL_AUTH_PROVIDER: mock PORTAL_SECURE_COOKIES: "true" - PORTAL_ESIGNET_CLIENT_ID: ${PORTAL_ESIGNET_CLIENT_ID:-solmara-portal} - PORTAL_ESIGNET_CLIENT_KEY_ID: ${PORTAL_ESIGNET_CLIENT_KEY_ID:-solmara-portal-key-1} - PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64} - PORTAL_ESIGNET_ISSUER: https://esignet.solmara.registrystack.org - PORTAL_ESIGNET_AUTHORIZATION_ENDPOINT: https://esignet-ui.solmara.registrystack.org/authorize - PORTAL_ESIGNET_TOKEN_ENDPOINT: https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token - PORTAL_ESIGNET_CLIENT_ASSERTION_AUDIENCE: https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token - PORTAL_ESIGNET_USERINFO_ENDPOINT: https://esignet.solmara.registrystack.org/v1/esignet/oidc/userinfo - PORTAL_ESIGNET_REDIRECT_URI: https://portal.solmara.registrystack.org/auth/callback - PORTAL_ESIGNET_SCOPE: openid profile - PORTAL_ESIGNET_SUBJECT_CLAIM: individual_id - CHILD_BENEFIT_FEDERATOR_URL: http://child-benefit-federator:8080 - CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN} - CRA_NOTARY_URL: https://cra-notary.solmara.registrystack.org - CRA_PENSION_CLIENT_TOKEN: ${CRA_PENSION_CLIENT_TOKEN} - CRA_CITIZEN_CLIENT_TOKEN: ${CRA_CITIZEN_CLIENT_TOKEN} - NIA_NOTARY_URL: https://nia-notary.solmara.registrystack.org - NIA_CITIZEN_CLIENT_TOKEN: ${NIA_CITIZEN_CLIENT_TOKEN} - SIPF_NOTARY_URL: https://sipf-notary.solmara.registrystack.org - SIPF_PENSION_CLIENT_TOKEN: ${SIPF_PENSION_CLIENT_TOKEN} - NAGDI_NOTARY_URL: https://nagdi-notary.solmara.registrystack.org - NAGDI_NOTARY_TOKEN: ${NAGDI_NOTARY_TOKEN} - SCENARIO_RUNNER_URL: http://scenario-runner:8080 - STATIC_METADATA_URL: http://static-metadata:8080 - healthcheck: - test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:4000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] - interval: 30s - timeout: 10s - retries: 3 - depends_on: - scenario-runner: - condition: service_healthy - labels: - solmara.lab.host: portal.solmara.registrystack.org + SCENARIO_RUNNER_URL: https://scenarios.solmara.registrystack.org + CHILD_BENEFIT_FEDERATOR_URL: https://child-benefit.solmara.registrystack.org + secrets: + - source: solmara-demo-client-key + target: solmara-evidence-client.jwk + labels: {solmara.lab.host: portal.solmara.registrystack.org} +secrets: + solmara-demo-client-key: + environment: SOLMARA_DEMO_CLIENT_PRIVATE_JWK +volumes: + mint-runtime: {external: true, name: solmara-authority-cells-mint-runtime} + mint-secrets: {external: true, name: solmara-authority-cells-mint-secrets} + mint-transit: {external: true, name: solmara-authority-cells-mint-transit} + mint-audit: +networks: + runtime: + ipam: + config: [{subnet: 172.29.1.0/24}] diff --git a/compose.esignet.yaml b/compose.esignet.yaml index 7124462..dd43ad8 100644 --- a/compose.esignet.yaml +++ b/compose.esignet.yaml @@ -3,23 +3,22 @@ services: build: context: . dockerfile: docker/esignet-postgres/Dockerfile - image: ${SOLMARA_ESIGNET_POSTGRES_IMAGE:-solmara-lab-esignet-postgres:local} - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?SOLMARA_ESIGNET_POSTGRES_PASSWORD is required; run just gen-secrets} - volumes: - - esignet-pgdata:/var/lib/postgresql/data + args: {POSTGRES_IMAGE: "${ESIGNET_POSTGRES_IMAGE:?versions.env must pin eSignet database image}"} + image: ${SOLMARA_ESIGNET_POSTGRES_IMAGE:-solmara-lab-esignet-db:local} + environment: {POSTGRES_USER: esignet, POSTGRES_PASSWORD: "${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?run just gen-secrets}"} + volumes: ["esignet-db-data:/var/lib/postgresql"] + networks: [runtime] healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + test: ["CMD-SHELL", "pg_isready -U esignet -d esignet"] interval: 5s timeout: 5s retries: 30 esignet-redis: - image: ${ESIGNET_REDIS_IMAGE:-redis:6.0} + image: ${ESIGNET_REDIS_IMAGE:?versions.env must pin eSignet Redis} command: ["redis-server"] - volumes: - - esignet-redis-data:/data + volumes: ["esignet-redis-data:/data"] + networks: [runtime] healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s @@ -30,9 +29,11 @@ services: build: context: . dockerfile: docker/esignet-relay/Dockerfile - additional_contexts: - esignet_relay_authenticator_src: ${ESIGNET_RELAY_AUTHENTICATOR_SOURCE_DIR:-../esignet-relay-authenticator} - image: ${SOLMARA_ESIGNET_RELAY_IMAGE:-solmara-lab-esignet-relay:local} + args: + ESIGNET_BASE_IMAGE: ${ESIGNET_BASE_IMAGE:?versions.env must pin eSignet} + ESIGNET_AUTHENTICATOR_JAR_URL: ${ESIGNET_AUTHENTICATOR_JAR_URL:?eSignet authenticator v0.2.0 JAR is not published} + ESIGNET_AUTHENTICATOR_JAR_SHA256: ${ESIGNET_AUTHENTICATOR_JAR_SHA256:?eSignet authenticator v0.2.0 checksum is not published} + image: ${SOLMARA_ESIGNET_RELAY_IMAGE:-solmara-lab-esignet:local} user: root environment: active_profile_env: default,local @@ -51,157 +52,91 @@ services: SPRING_AUTOCONFIGURE_EXCLUDE: org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration SPRING_CACHE_TYPE: redis SPRING_DATA_REDIS_HOST: esignet-redis - SPRING_REDIS_PASSWORD: "" MOSIP_ESIGNET_HOST: esignet MOSIP_ESIGNET_DISCOVERY_ISSUER_ID: ${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308} MOSIP_ESIGNET_DISCOVERY_KEY_VALUES: "{'issuer':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308}','authorization_endpoint':'${SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL:-http://127.0.0.1:4309}/authorize','token_endpoint':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308}/v1/esignet/oauth/v2/token','userinfo_endpoint':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308}/v1/esignet/oidc/userinfo','jwks_uri':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308}/v1/esignet/oauth/.well-known/jwks.json','token_endpoint_auth_methods_supported':{'private_key_jwt'},'token_endpoint_auth_signing_alg_values_supported':{'RS256','PS256','ES256'}}" MOSIP_ESIGNET_OAUTH_KEY_VALUES: "{'issuer':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308}','authorization_endpoint':'${SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL:-http://127.0.0.1:4309}/authorize','token_endpoint':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308}/v1/esignet/oauth/v2/token','jwks_uri':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308}/v1/esignet/oauth/.well-known/jwks.json','token_endpoint_auth_methods_supported':{'private_key_jwt'},'token_endpoint_auth_signing_alg_values_supported':{'RS256','PS256','ES256'}}" MOSIP_ESIGNET_DATABASE_URL: jdbc:postgresql://esignet-database:5432/mosip_esignet?currentSchema=esignet - MOSIP_ESIGNET_DATABASE_USERNAME: postgres - MOSIP_ESIGNET_DATABASE_PASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?SOLMARA_ESIGNET_POSTGRES_PASSWORD is required; run just gen-secrets} + MOSIP_ESIGNET_DATABASE_USERNAME: esignet + MOSIP_ESIGNET_DATABASE_PASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?run just gen-secrets} MOSIP_ESIGNET_INTEGRATION_SCAN_BASE_PACKAGE: io.mosip.esignet.mock.integration,io.registry.esignet.relay MOSIP_ESIGNET_INTEGRATION_AUTHENTICATOR: RelayAuthenticationService MOSIP_ESIGNET_INTEGRATION_KEY_BINDER: MockKeyBindingWrapperService - MOSIP_ESIGNET_OPENID_SCOPE_CLAIMS: "{'profile' : {'name','given_name','family_name','gender','birthdate','individual_id'}}" - REGISTRY_RELAY_BASE_URL: http://nia-population-relay:8080 - REGISTRY_RELAY_ATTRIBUTE_RELEASE_PROFILE_ID: solmara-nia-userinfo - REGISTRY_RELAY_ATTRIBUTE_RELEASE_PROFILE_VERSION: v1 - REGISTRY_RELAY_ATTRIBUTE_RELEASE_PURPOSE: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - REGISTRY_RELAY_AUTH_BEARER_TOKEN_FILE: /run/secrets/solmara-esignet-relay-token - REGISTRY_RELAY_SUBJECT_ID_TYPE: national_id - REGISTRY_RELAY_DEFAULT_CLAIMS: individual_id,name,given_name,family_name,birthdate,gender - REGISTRY_ESIGNET_AUTH_SUPPORTED_FACTORS: OTP - REGISTRY_ESIGNET_AUTH_OTP_CHANNELS: email,phone + MOSIP_ESIGNET_OPENID_SCOPE_CLAIMS: "{'profile' : {'given_name','family_name','gender','birthdate','individual_id'}}" + REGISTRY_RELAY_BASE_URL: http://nia-relay:8080 + REGISTRY_RELAY_RESOURCE: population-person + REGISTRY_RELAY_LOOKUP: esignet-userinfo + REGISTRY_RELAY_ACCESS_PROFILE: esignet + REGISTRY_RELAY_DEFAULT_CLAIMS: individualId,givenName,familyName,birthdate,gender + SPRING_APPLICATION_JSON: '{"registry":{"esignet":{"claim-map":{"sub":"$$psut","individual_id":"individualId","given_name":"givenName","family_name":"familyName","birthdate":"birthdate","gender":"gender"}}}}' + REGISTRY_MINT_TOKEN_ENDPOINT: https://mint.solmara.registrystack.org/token + REGISTRY_MINT_CLIENT_ID: nia-esignet + REGISTRY_MINT_PRIVATE_JWK: ${NIA_ESIGNET_CLIENT_PRIVATE_JWK:?run just gen-secrets} + REGISTRY_MINT_TOKEN_CACHE_MAX_SECONDS: "300" + REGISTRY_TLS_CA_CERT: /etc/solmara-evidence/tls/ca.crt REGISTRY_ESIGNET_AUTH_OTP_STATIC_ENABLED: "true" - REGISTRY_ESIGNET_AUTH_OTP_STATIC_VALUE: "111111" - REGISTRY_ESIGNET_KYC_TOKEN_HMAC_SECRET: ${REGISTRY_ESIGNET_KYC_TOKEN_SECRET:?REGISTRY_ESIGNET_KYC_TOKEN_SECRET is required; run just gen-secrets} - REGISTRY_ESIGNET_PSUT_HMAC_SECRET: ${REGISTRY_ESIGNET_PSUT_SECRET:?REGISTRY_ESIGNET_PSUT_SECRET is required; run just gen-secrets} - REGISTRY_ESIGNET_USER_INFO_ISSUER: ${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308} + REGISTRY_ESIGNET_AUTH_OTP_STATIC_VALUE: ${ESIGNET_DEMO_OTP:-111111} + REGISTRY_ESIGNET_AUTH_OTP_CHANNELS: EMAIL,PHONE + REGISTRY_ESIGNET_ACCOUNT_CHECK_CLAIMS: individualId + MOSIP_ESIGNET_AUTHENTICATOR_IDA_OTP_CHANNELS: EMAIL,PHONE + REGISTRY_ESIGNET_KYC_TOKEN_HMAC_SECRET: ${REGISTRY_ESIGNET_KYC_TOKEN_SECRET:?run just gen-secrets} + REGISTRY_ESIGNET_PSUT_HMAC_SECRET: ${REGISTRY_ESIGNET_PSUT_SECRET:?run just gen-secrets} REGISTRY_ESIGNET_KYC_SIGNING_KEYSTORE_PATH: /home/mosip/kyc-signing/kyc-signing.p12 REGISTRY_ESIGNET_KYC_SIGNING_KEYSTORE_TYPE: PKCS12 - REGISTRY_ESIGNET_KYC_SIGNING_KEYSTORE_PASSWORD: ${REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD:?REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD is required; run just gen-secrets} + REGISTRY_ESIGNET_KYC_SIGNING_KEYSTORE_PASSWORD: ${REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD:?run just gen-secrets} REGISTRY_ESIGNET_KYC_SIGNING_KEY_ALIAS: esignet-relay-kyc - REGISTRY_ESIGNET_KYC_SIGNING_KEY_PASSWORD: ${REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD:?REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD is required; run just gen-secrets} + REGISTRY_ESIGNET_KYC_SIGNING_KEY_PASSWORD: ${REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD:?run just gen-secrets} MOSIP_KERNEL_KEYMANAGER_HSM_CONFIG_PATH: /home/mosip/keystore/esignet_local.p12 - volumes: - - esignet-keystore:/home/mosip/keystore - - esignet-kyc-signing:/home/mosip/kyc-signing - - nia-esignet-workload-token:/run/secrets:ro - depends_on: - esignet-database: - condition: service_healthy - esignet-redis: - condition: service_healthy - nia-population-relay: - condition: service_healthy - nia-workload-agent: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-fsS", "http://esignet:8088/v1/esignet/oidc/.well-known/openid-configuration"] - interval: 10s - timeout: 5s - retries: 60 - start_period: 90s + volumes: ["esignet-keystore:/home/mosip/keystore", "esignet-kyc-signing:/home/mosip/kyc-signing", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro"] + extra_hosts: ["mint.solmara.registrystack.org:172.29.1.10"] + networks: [runtime] + depends_on: {esignet-database: {condition: service_healthy}, esignet-redis: {condition: service_healthy}, nia-relay: {condition: service_healthy}, mint: {condition: service_started}} esignet-ui: - build: - context: . - dockerfile: docker/esignet-ui/Dockerfile - args: - ESIGNET_NGINX_CONF: config/esignet/nginx.conf + build: {context: ., dockerfile: docker/esignet-ui/Dockerfile, args: {ESIGNET_UI_IMAGE: "${ESIGNET_UI_IMAGE:?versions.env must pin eSignet UI}", ESIGNET_NGINX_CONF: config/esignet/nginx.conf}} image: ${SOLMARA_ESIGNET_UI_IMAGE:-solmara-lab-esignet-ui:local} - user: root - environment: - container_user: mosip - DEFAULT_WELLKNOWN: "%5B%7B%22name%22%3A%22OpenID%20Configuration%22%2C%22value%22%3A%22%2F.well-known%2Fopenid-configuration%22%7D%2C%7B%22name%22%3A%22Jwks%20Json%22%2C%22value%22%3A%22%2F.well-known%2Fjwks.json%22%7D%2C%7B%22name%22%3A%22Authorization%20Server%22%2C%22value%22%3A%22%2F.well-known%2Foauth-authorization-server%22%7D%5D" - SIGN_IN_WITH_ESIGNET_PLUGIN_URL: https://raw.githubusercontent.com/mosip/artifactory-ref-impl/977607990661e864c2adddb9d7de50ea6c651b8f/artifacts/src/mosip-plugins/sign-in-with-esignet/sign-in-with-esignet.zip - i18n_url_env: "" - theme_url_env: "" - images_url_env: "" - ports: - - "${SOLMARA_ESIGNET_UI_PORT:-4309}:3000" - depends_on: - esignet: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://esignet-ui:3000/.well-known/openid-configuration"] - interval: 10s - timeout: 5s - retries: 30 - start_period: 30s + ports: ["${SOLMARA_ESIGNET_UI_PORT:-4309}:3000"] + networks: [runtime] + depends_on: {esignet: {condition: service_started}} esignet-edge: - build: - context: . - dockerfile: docker/esignet-ui/Dockerfile - args: - ESIGNET_NGINX_CONF: config/esignet/nginx.conf + build: {context: ., dockerfile: docker/esignet-ui/Dockerfile, args: {ESIGNET_UI_IMAGE: "${ESIGNET_UI_IMAGE:?versions.env must pin eSignet UI}", ESIGNET_NGINX_CONF: config/esignet/nginx.conf}} image: ${SOLMARA_ESIGNET_UI_IMAGE:-solmara-lab-esignet-ui:local} - user: root - environment: - container_user: mosip - DEFAULT_WELLKNOWN: "%5B%7B%22name%22%3A%22OpenID%20Configuration%22%2C%22value%22%3A%22%2F.well-known%2Fopenid-configuration%22%7D%2C%7B%22name%22%3A%22Jwks%20Json%22%2C%22value%22%3A%22%2F.well-known%2Fjwks.json%22%7D%2C%7B%22name%22%3A%22Authorization%20Server%22%2C%22value%22%3A%22%2F.well-known%2Foauth-authorization-server%22%7D%5D" - SIGN_IN_WITH_ESIGNET_PLUGIN_URL: https://raw.githubusercontent.com/mosip/artifactory-ref-impl/977607990661e864c2adddb9d7de50ea6c651b8f/artifacts/src/mosip-plugins/sign-in-with-esignet/sign-in-with-esignet.zip - i18n_url_env: "" - theme_url_env: "" - images_url_env: "" - ports: - - "${SOLMARA_ESIGNET_PORT:-4308}:3000" - depends_on: - esignet: - condition: service_healthy - healthcheck: - test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://esignet-edge:3000/.well-known/openid-configuration"] - interval: 10s - timeout: 5s - retries: 30 - start_period: 30s + ports: ["${SOLMARA_ESIGNET_PORT:-4308}:3000"] + networks: [runtime] + depends_on: {esignet: {condition: service_started}} esignet-seed: - build: - context: . - dockerfile: docker/esignet-seed/Dockerfile + build: {context: ., dockerfile: docker/esignet-seed/Dockerfile, args: {POSTGRES_IMAGE: "${ESIGNET_POSTGRES_IMAGE:?versions.env must pin eSignet database image}"}} image: ${SOLMARA_ESIGNET_SEED_IMAGE:-solmara-lab-esignet-seed:local} environment: PGHOST: esignet-database - PGPORT: "5432" - PGUSER: postgres - PGPASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?SOLMARA_ESIGNET_POSTGRES_PASSWORD is required; run just gen-secrets} + PGUSER: esignet + PGPASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?run just gen-secrets} ESIGNET_CLIENT_ID: ${PORTAL_ESIGNET_CLIENT_ID:-solmara-portal} ESIGNET_CLIENT_KEY_ID: ${PORTAL_ESIGNET_CLIENT_KEY_ID:-solmara-portal-key-1} - ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64 is required; run just gen-secrets} - ESIGNET_CLIENT_REDIRECT_URIS_JSON: '["http://127.0.0.1:${SOLMARA_PORTAL_PORT:-4300}/auth/callback","http://localhost:${SOLMARA_PORTAL_PORT:-4300}/auth/callback"]' + ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?run just gen-secrets} ESIGNET_REDIS_HOST: esignet-redis - ESIGNET_ACCOUNT_SOURCE_LABEL: NIA population Relay profile solmara-nia-userinfo - ESIGNET_DEMO_OTP: "111111" entrypoint: ["seed-esignet.py"] - depends_on: - esignet-database: - condition: service_healthy - esignet: - condition: service_healthy - restart: "no" + networks: [runtime] + depends_on: {esignet-database: {condition: service_healthy}, esignet: {condition: service_started}} portal: environment: PORTAL_AUTH_PROVIDER: esignet PORTAL_ESIGNET_CLIENT_ID: ${PORTAL_ESIGNET_CLIENT_ID:-solmara-portal} PORTAL_ESIGNET_CLIENT_KEY_ID: ${PORTAL_ESIGNET_CLIENT_KEY_ID:-solmara-portal-key-1} - PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64 is required; run just gen-secrets} + PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?run just gen-secrets} PORTAL_ESIGNET_ISSUER: ${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308} PORTAL_ESIGNET_AUTHORIZATION_ENDPOINT: ${SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL:-http://127.0.0.1:4309}/authorize PORTAL_ESIGNET_TOKEN_ENDPOINT: http://esignet:8088/v1/esignet/oauth/v2/token PORTAL_ESIGNET_CLIENT_ASSERTION_AUDIENCE: ${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-http://127.0.0.1:4308}/v1/esignet/oauth/v2/token PORTAL_ESIGNET_USERINFO_ENDPOINT: http://esignet:8088/v1/esignet/oidc/userinfo PORTAL_ESIGNET_REDIRECT_URI: http://127.0.0.1:${SOLMARA_PORTAL_PORT:-4300}/auth/callback - PORTAL_ESIGNET_SCOPE: openid profile - PORTAL_ESIGNET_SUBJECT_CLAIM: individual_id - depends_on: - esignet-seed: - condition: service_completed_successfully + PORTAL_ESIGNET_SUBJECT_CLAIM: sub volumes: - esignet-pgdata: + esignet-db-data: esignet-keystore: esignet-kyc-signing: esignet-redis-data: diff --git a/compose.hosted.yaml b/compose.hosted.yaml index 8368f50..6173072 100644 --- a/compose.hosted.yaml +++ b/compose.hosted.yaml @@ -1,345 +1,85 @@ +# Overlay for the side-by-side authority-cell rollout. The previous deployment's +# volumes remain externally named and are never attached, changed, or deleted. +x-solmara-rollout: + phase: ${SOLMARA_AUTHORITY_CELLS_PHASE:-side-by-side} + switch: ${SOLMARA_AUTHORITY_CELLS_SWITCH:-disabled} + superseded-volume-policy: retain + +x-hosted-relay: &hosted-relay + image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + build: !reset null + labels: + solmara.rollout.slot: authority-cells + solmara.rollout.phase: ${SOLMARA_AUTHORITY_CELLS_PHASE:-side-by-side} + +x-hosted-evidence: &hosted-evidence + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + build: !reset null + labels: + solmara.rollout.slot: authority-cells + solmara.rollout.phase: ${SOLMARA_AUTHORITY_CELLS_PHASE:-side-by-side} + services: - postgres: - ports: !reset [] + cra-relay: {<<: *hosted-relay} + nia-relay: {<<: *hosted-relay} + mosd-relay: {<<: *hosted-relay} + sipf-relay: {<<: *hosted-relay} + nagdi-relay: {<<: *hosted-relay} + + cra-evidence: {<<: *hosted-evidence} + nia-evidence: {<<: *hosted-evidence} + sro-evidence: {<<: *hosted-evidence} + mosd-programme-evidence: {<<: *hosted-evidence} + sipf-evidence: {<<: *hosted-evidence} + nagdi-evidence: {<<: *hosted-evidence} + + mint: + image: ${SOLMARA_MINT_IMAGE:?set the digest-pinned Solmara Mint image built from the verified release binary} + build: !reset null + labels: + solmara.rollout.slot: authority-cells + solmara.rollout.phase: ${SOLMARA_AUTHORITY_CELLS_PHASE:-side-by-side} static-metadata: - ports: !reset [] - labels: - solmara.lab.host: metadata.solmara.registrystack.org - + image: ${SOLMARA_STATIC_METADATA_IMAGE:?set a digest-pinned Solmara static metadata image} scenario-runner: - profiles: [] - labels: - solmara.lab.visibility: internal - - home: - profiles: [] - ports: !reset [] + image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set a digest-pinned Solmara scenario runner image} + build: !reset null environment: - PORTAL_URL: https://portal.solmara.registrystack.org - # Rewrites the compose-internal URLs the scenario runner reports into the - # host-reachable *.solmara.registrystack.org URLs shown in previews and curls. - SOLMARA_PUBLIC_URL_MAP: >- - {"child-benefit-federator:8080":"https://child-benefit-federator.solmara.registrystack.org", - "cra-notary:8081":"https://cra-notary.solmara.registrystack.org", - "nia-notary:8081":"https://nia-notary.solmara.registrystack.org", - "sro-notary:8081":"https://sro-notary.solmara.registrystack.org", - "programme-notary:8081":"https://programme-notary.solmara.registrystack.org", - "sipf-notary:8081":"https://sipf-notary.solmara.registrystack.org", - "nagdi-notary:8081":"https://nagdi-notary.solmara.registrystack.org", - "cra-civil-relay:8080":"https://cra-relay.solmara.registrystack.org", - "nia-population-relay:8080":"https://nia-relay.solmara.registrystack.org", - "sro-social-relay:8080":"https://sro-relay.solmara.registrystack.org", - "programme-mis-relay:8080":"https://mosd-programme-relay.solmara.registrystack.org", - "sipf-pensions-relay:8080":"https://sipf-relay.solmara.registrystack.org", - "nagdi-agriculture-relay:8080":"https://nagdi-relay.solmara.registrystack.org", - "static-metadata:8080":"https://metadata.solmara.registrystack.org", - "portal:4000":"https://portal.solmara.registrystack.org"} - labels: - solmara.lab.host: solmara.registrystack.org - - cra-relay-state-bootstrap: - volumes: - - ./runtime/registry-projects/hosted/cra-civil/relay:/etc/registry-relay:ro - - nia-relay-state-bootstrap: - volumes: - - ./runtime/registry-projects/hosted/nia-population/relay:/etc/registry-relay:ro - - sro-relay-state-bootstrap: - volumes: - - ./runtime/registry-projects/hosted/sro-social/relay:/etc/registry-relay:ro - - programme-relay-state-bootstrap: - volumes: - - ./runtime/registry-projects/hosted/mosd-programme/relay:/etc/registry-relay:ro - - sipf-relay-state-bootstrap: - volumes: - - ./runtime/registry-projects/hosted/sipf-pensions/relay:/etc/registry-relay:ro - - nagdi-relay-state-bootstrap: - volumes: - - ./runtime/registry-projects/hosted/nagdi-agriculture/relay:/etc/registry-relay:ro - - # Hosted Relay configs trust the platform workload issuer. Its rotating - # tokens arrive through external authority volumes, so the loopback local - # agents must not start in this overlay. - cra-workload-agent: - profiles: [local-workload-issuer] - - nia-workload-agent: - profiles: [local-workload-issuer] - - sro-workload-agent: - profiles: [local-workload-issuer] - - programme-workload-agent: - profiles: [local-workload-issuer] - - sipf-workload-agent: - profiles: [local-workload-issuer] - - nagdi-workload-agent: - profiles: [local-workload-issuer] - - cra-consultation-workload-agent: - profiles: [local-workload-issuer] - - nia-consultation-workload-agent: - profiles: [local-workload-issuer] - - sro-consultation-workload-agent: - profiles: [local-workload-issuer] - - programme-consultation-workload-agent: - profiles: [local-workload-issuer] - - sipf-consultation-workload-agent: - profiles: [local-workload-issuer] - - nagdi-consultation-workload-agent: - profiles: [local-workload-issuer] - - cra-civil-relay: - ports: !reset [] - expose: ["8080"] - volumes: - - ./runtime/registry-projects/hosted/cra-civil/relay:/etc/registry-relay:ro - labels: - solmara.lab.host: cra-relay.solmara.registrystack.org - - nia-population-relay: - ports: !reset [] - expose: ["8080"] - volumes: - - ./runtime/registry-projects/hosted/nia-population/relay:/etc/registry-relay:ro - labels: - solmara.lab.host: nia-relay.solmara.registrystack.org - - sro-social-relay: - ports: !reset [] - expose: ["8080"] - volumes: - - ./runtime/registry-projects/hosted/sro-social/relay:/etc/registry-relay:ro - labels: - solmara.lab.host: sro-relay.solmara.registrystack.org - - programme-mis-relay: - ports: !reset [] - expose: ["8080"] - volumes: - - ./runtime/registry-projects/hosted/mosd-programme/relay:/etc/registry-relay:ro - labels: - solmara.lab.host: mosd-programme-relay.solmara.registrystack.org - - sipf-pensions-relay: - ports: !reset [] - expose: ["8080"] - volumes: - - ./runtime/registry-projects/hosted/sipf-pensions/relay:/etc/registry-relay:ro - labels: - solmara.lab.host: sipf-relay.solmara.registrystack.org - - nagdi-agriculture-relay: - ports: !reset [] - expose: ["8080"] - volumes: - - ./runtime/registry-projects/hosted/nagdi-agriculture/relay:/etc/registry-relay:ro - labels: - solmara.lab.host: nagdi-relay.solmara.registrystack.org - - cra-civil-relay-consultation: - ports: !reset [] - expose: ["8081"] - volumes: - - ./runtime/registry-projects/hosted/cra-civil/relay:/etc/registry-relay:ro - depends_on: !override - cra-relay-state-bootstrap: - condition: service_completed_successfully - - nia-population-relay-consultation: - ports: !reset [] - expose: ["8081"] - volumes: - - ./runtime/registry-projects/hosted/nia-population/relay:/etc/registry-relay:ro - depends_on: !override - nia-relay-state-bootstrap: - condition: service_completed_successfully - - sro-social-relay-consultation: - ports: !reset [] - expose: ["8081"] - volumes: - - ./runtime/registry-projects/hosted/sro-social/relay:/etc/registry-relay:ro - depends_on: !override - sro-relay-state-bootstrap: - condition: service_completed_successfully - - programme-mis-relay-consultation: - ports: !reset [] - expose: ["8081"] - volumes: - - ./runtime/registry-projects/hosted/mosd-programme/relay:/etc/registry-relay:ro - depends_on: !override - programme-relay-state-bootstrap: - condition: service_completed_successfully - - sipf-pensions-relay-consultation: - ports: !reset [] - expose: ["8081"] - volumes: - - ./runtime/registry-projects/hosted/sipf-pensions/relay:/etc/registry-relay:ro - depends_on: !override - sipf-relay-state-bootstrap: - condition: service_completed_successfully - - nagdi-agriculture-relay-consultation: - ports: !reset [] - expose: ["8081"] - volumes: - - ./runtime/registry-projects/hosted/nagdi-agriculture/relay:/etc/registry-relay:ro - depends_on: !override - nagdi-relay-state-bootstrap: - condition: service_completed_successfully - - cra-notary: - volumes: - - ./runtime/registry-projects/hosted/cra-civil/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - labels: - solmara.lab.host: cra-notary.solmara.registrystack.org - depends_on: !override - cra-civil-relay-consultation: - condition: service_healthy - cra-notary-state-install: - condition: service_completed_successfully - - cra-notary-state-install: - volumes: - - ./runtime/registry-projects/hosted/cra-civil/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - depends_on: !override - registry-postgresql-bootstrap: - condition: service_completed_successfully - - nia-notary: - volumes: - - ./runtime/registry-projects/hosted/nia-population/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - labels: - solmara.lab.host: nia-notary.solmara.registrystack.org - depends_on: !override - nia-population-relay-consultation: - condition: service_healthy - nia-notary-state-install: - condition: service_completed_successfully - - nia-notary-state-install: - volumes: - - ./runtime/registry-projects/hosted/nia-population/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - depends_on: !override - registry-postgresql-bootstrap: - condition: service_completed_successfully - - sro-notary: - volumes: - - ./runtime/registry-projects/hosted/sro-social/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - labels: - solmara.lab.host: sro-notary.solmara.registrystack.org - depends_on: !override - sro-social-relay-consultation: - condition: service_healthy - sro-notary-state-install: - condition: service_completed_successfully - - sro-notary-state-install: - volumes: - - ./runtime/registry-projects/hosted/sro-social/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - depends_on: !override - registry-postgresql-bootstrap: - condition: service_completed_successfully - - programme-notary: - volumes: - - ./runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - labels: - solmara.lab.host: programme-notary.solmara.registrystack.org - depends_on: !override - programme-mis-relay-consultation: - condition: service_healthy - programme-notary-state-install: - condition: service_completed_successfully - - programme-notary-state-install: - volumes: - - ./runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - depends_on: !override - registry-postgresql-bootstrap: - condition: service_completed_successfully - - sipf-notary: - volumes: - - ./runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - labels: - solmara.lab.host: sipf-notary.solmara.registrystack.org - depends_on: !override - sipf-pensions-relay-consultation: - condition: service_healthy - sipf-notary-state-install: - condition: service_completed_successfully - - sipf-notary-state-install: - volumes: - - ./runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - depends_on: !override - registry-postgresql-bootstrap: - condition: service_completed_successfully - - nagdi-notary: - volumes: - - ./runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - labels: - solmara.lab.host: nagdi-notary.solmara.registrystack.org - depends_on: !override - nagdi-agriculture-relay-consultation: - condition: service_healthy - nagdi-notary-state-install: - condition: service_completed_successfully - - nagdi-notary-state-install: - volumes: - - ./runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml:/etc/registry-notary/notary.yaml:ro - depends_on: !override - registry-postgresql-bootstrap: - condition: service_completed_successfully - + SOLMARA_LAB_MODE: hosted + SOLMARA_MINT_URL: https://mint.solmara.registrystack.org + SOLMARA_CRA_EVIDENCE_URL: https://cra-evidence.solmara.registrystack.org + SOLMARA_NIA_EVIDENCE_URL: https://nia-evidence.solmara.registrystack.org + SOLMARA_SRO_EVIDENCE_URL: https://sro-evidence.solmara.registrystack.org + SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL: https://mosd-programme-evidence.solmara.registrystack.org + SOLMARA_SIPF_EVIDENCE_URL: https://sipf-evidence.solmara.registrystack.org + SOLMARA_NAGDI_EVIDENCE_URL: https://nagdi-evidence.solmara.registrystack.org child-benefit-federator: - ports: !reset [] + image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set a digest-pinned Solmara scenario runner image} + build: !reset null environment: - CHILD_BENEFIT_PUBLIC_DOMAIN: solmara.registrystack.org - labels: - solmara.lab.host: child-benefit-federator.solmara.registrystack.org - + SOLMARA_MINT_URL: https://mint.solmara.registrystack.org + SOLMARA_CRA_EVIDENCE_URL: https://cra-evidence.solmara.registrystack.org + SOLMARA_NIA_EVIDENCE_URL: https://nia-evidence.solmara.registrystack.org + SOLMARA_SRO_EVIDENCE_URL: https://sro-evidence.solmara.registrystack.org + SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL: https://mosd-programme-evidence.solmara.registrystack.org portal: - profiles: [] - ports: !reset [] - labels: - solmara.lab.host: portal.solmara.registrystack.org + image: ${SOLMARA_PORTAL_IMAGE:?set a digest-pinned Solmara portal image} + build: !reset null + home: + image: ${SOLMARA_HOME_IMAGE:?set a digest-pinned Solmara home image} + build: !reset null volumes: - cra-consultation-workload-token: - external: true - name: ${CRA_WORKLOAD_TOKEN_VOLUME:-solmara-cra-workload-token} - nia-consultation-workload-token: - external: true - name: ${NIA_WORKLOAD_TOKEN_VOLUME:-solmara-nia-workload-token} - sro-consultation-workload-token: + superseded-interior-state: external: true - name: ${SRO_WORKLOAD_TOKEN_VOLUME:-solmara-sro-workload-token} - programme-consultation-workload-token: + name: ${SOLMARA_SUPERSEDED_INTERIOR_VOLUME:-solmara-lab-interior-state} + superseded-social-development-state: external: true - name: ${PROGRAMME_WORKLOAD_TOKEN_VOLUME:-solmara-programme-workload-token} - sipf-consultation-workload-token: + name: ${SOLMARA_SUPERSEDED_SOCIAL_VOLUME:-solmara-lab-social-development-state} + superseded-pensions-state: external: true - name: ${SIPF_WORKLOAD_TOKEN_VOLUME:-solmara-sipf-workload-token} - nagdi-consultation-workload-token: + name: ${SOLMARA_SUPERSEDED_PENSIONS_VOLUME:-solmara-lab-pensions-state} + superseded-agriculture-state: external: true - name: ${NAGDI_WORKLOAD_TOKEN_VOLUME:-solmara-nagdi-workload-token} + name: ${SOLMARA_SUPERSEDED_AGRICULTURE_VOLUME:-solmara-lab-agriculture-state} diff --git a/compose.yaml b/compose.yaml index 6a6021f..8763343 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,459 +1,460 @@ -x-relay-environment: &relay-environment - RUST_LOG: ${RUST_LOG:-info} - REGISTRY_RELAY_BIND: 0.0.0.0:8080 +name: ${COMPOSE_PROJECT_NAME:-solmara-lab} -x-relay-common: &relay-common - image: ${REGISTRY_RELAY_IMAGE:?versions.env must name the locally built Registry Stack main Relay image} +x-relay: &relay + image: ${REGISTRY_RELAY_IMAGE:?versions.env must name the digest-pinned Relay V2 image} + platform: linux/amd64 restart: unless-stopped - environment: *relay-environment + read_only: true + tmpfs: [/tmp] + networks: [runtime, issuer-validation] + depends_on: {relay-audit-init: {condition: service_completed_successfully}} healthcheck: - test: ["CMD", "/usr/local/bin/registry-relay", "healthcheck"] + test: ["CMD", "/usr/local/bin/relay", "healthcheck", "--url", "http://127.0.0.1:8080/health"] interval: 10s timeout: 5s retries: 12 -x-workload-agent-common: &workload-agent-common - build: - context: . - dockerfile: scenario-runner/Dockerfile +x-evidence: &evidence + image: ${SOLMARA_EVIDENCE_IMAGE:?versions.env must name the checksum-verified Evidence image} + platform: linux/amd64 + restart: unless-stopped + user: "0:0" + read_only: true + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + tmpfs: [/tmp] + environment: {SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt} + networks: [runtime, issuer-validation] + +# Docker Desktop cannot carry a host Unix listener through a bind mount. Each +# local authority therefore gets one isolated signer container and one private +# named socket volume. Registry services receive the socket, never the key. +x-transit-signer: &transit-signer image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:-solmara-lab-scenario-runner:local} - command: ["python", "scripts/relay_workload_identity_agent.py"] restart: unless-stopped + user: "0:0" + read_only: true + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + network_mode: none healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] - interval: 5s - timeout: 5s - retries: 12 + test: ["CMD", "python", "-c", "import socket; s=socket.socket(socket.AF_UNIX); s.connect('/transit/transit-proxy.sock'); s.close()"] + interval: 2s + timeout: 1s + retries: 15 + +# Docker Desktop does not reliably invalidate SQLite pages held by a long-lived +# reader when the database is a host bind mount. Keep each authority's mutable +# publication on its own Linux volume, with a separate no-network publisher as +# the only writer. Relays receive only their authority volume and mount it RO. +x-relay-source-publisher: &relay-source-publisher + image: ${PYTHON_STATIC_IMAGE:?versions.env must pin the static server image} + user: "0:0" + read_only: true + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + network_mode: none + tmpfs: [/tmp] + restart: "no" x-evidence-client-environment: &evidence-client-environment - SOLMARA_EVIDENCE_URL: https://evidence.solmara.invalid - SOLMARA_MINT_URL: https://mint.evidence.solmara.invalid - SOLMARA_MINT_ASSERTION_AUDIENCE: https://mint.evidence.solmara.invalid/token + SOLMARA_MINT_URL: https://mint.solmara.registrystack.org + SOLMARA_MINT_ASSERTION_AUDIENCE: https://mint.solmara.registrystack.org/token SOLMARA_EVIDENCE_CLIENT_ID: solmara-demo SOLMARA_EVIDENCE_CLIENT_KEY: /run/secrets/solmara-evidence-client.jwk SOLMARA_EVIDENCE_CA_BUNDLE: /etc/solmara-evidence/tls/ca.crt + SOLMARA_CRA_EVIDENCE_URL: https://evidence.solmara.invalid/evidence/cra + SOLMARA_NIA_EVIDENCE_URL: https://evidence.solmara.invalid/evidence/nia + SOLMARA_SRO_EVIDENCE_URL: https://evidence.solmara.invalid/evidence/sro + SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL: https://evidence.solmara.invalid/evidence/mosd-programme + SOLMARA_SIPF_EVIDENCE_URL: https://evidence.solmara.invalid/evidence/sipf + SOLMARA_NAGDI_EVIDENCE_URL: https://evidence.solmara.invalid/evidence/nagdi SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt services: - postgres: - image: ${POSTGRES_IMAGE:-postgres:16-alpine} - entrypoint: ["/bin/sh", "/usr/local/bin/solmara-postgres-entrypoint.sh"] + cra-source-publisher: + <<: *relay-source-publisher + entrypoint: ["python", "/app/local-relay-source-publisher.py", "--authority", "cra", "--database", "/var/lib/relay/source/cra.sqlite", "--seed", "/seed/cra.sqlite"] + command: ["ensure-seeded"] + volumes: ["./scripts/local-relay-source-publisher.py:/app/local-relay-source-publisher.py:ro", "./generator/solmara_lab:/app/solmara_lab:ro", "./output/sqlite/relay/cra.sqlite:/seed/cra.sqlite:ro", "cra-relay-source:/var/lib/relay/source"] + + nia-source-publisher: + <<: *relay-source-publisher + entrypoint: ["python", "/app/local-relay-source-publisher.py", "--authority", "nia", "--database", "/var/lib/relay/source/nia.sqlite", "--seed", "/seed/nia.sqlite"] + command: ["ensure-seeded"] + volumes: ["./scripts/local-relay-source-publisher.py:/app/local-relay-source-publisher.py:ro", "./generator/solmara_lab:/app/solmara_lab:ro", "./output/sqlite/relay/nia.sqlite:/seed/nia.sqlite:ro", "nia-relay-source:/var/lib/relay/source"] + + mosd-source-publisher: + <<: *relay-source-publisher + entrypoint: ["python", "/app/local-relay-source-publisher.py", "--authority", "mosd", "--database", "/var/lib/relay/source/mosd.sqlite", "--seed", "/seed/mosd.sqlite"] + command: ["ensure-seeded"] + volumes: ["./scripts/local-relay-source-publisher.py:/app/local-relay-source-publisher.py:ro", "./generator/solmara_lab:/app/solmara_lab:ro", "./output/sqlite/relay/mosd.sqlite:/seed/mosd.sqlite:ro", "mosd-relay-source:/var/lib/relay/source"] + + sipf-source-publisher: + <<: *relay-source-publisher + entrypoint: ["python", "/app/local-relay-source-publisher.py", "--authority", "sipf", "--database", "/var/lib/relay/source/sipf.sqlite", "--seed", "/seed/sipf.sqlite"] + command: ["ensure-seeded"] + volumes: ["./scripts/local-relay-source-publisher.py:/app/local-relay-source-publisher.py:ro", "./generator/solmara_lab:/app/solmara_lab:ro", "./output/sqlite/relay/sipf.sqlite:/seed/sipf.sqlite:ro", "sipf-relay-source:/var/lib/relay/source"] + + nagdi-source-publisher: + <<: *relay-source-publisher + entrypoint: ["python", "/app/local-relay-source-publisher.py", "--authority", "nagdi", "--database", "/var/lib/relay/source/nagdi.sqlite", "--seed", "/seed/nagdi.sqlite"] + command: ["ensure-seeded"] + volumes: ["./scripts/local-relay-source-publisher.py:/app/local-relay-source-publisher.py:ro", "./generator/solmara_lab:/app/solmara_lab:ro", "./output/sqlite/relay/nagdi.sqlite:/seed/nagdi.sqlite:ro", "nagdi-relay-source:/var/lib/relay/source"] + + relay-audit-init: + image: ${PYTHON_STATIC_IMAGE:?versions.env must pin the static server image} command: - - postgres - - -c - - ssl=on + - python - -c - - ssl_cert_file=/var/lib/postgresql/server-ssl/server.crt - - -c - - ssl_key_file=/var/lib/postgresql/server-ssl/server.key - environment: - POSTGRES_USER: ${SOLMARA_POSTGRES_USER:-solmara_registry} - POSTGRES_PASSWORD: ${SOLMARA_POSTGRES_PASSWORD} - POSTGRES_DB: ${SOLMARA_POSTGRES_DB:-solmara_lab} - SOLMARA_SOURCE_READER_DATABASES: nia sipf - NIA_SOURCE_POSTGRES_READER_PASSWORD: ${NIA_SOURCE_POSTGRES_READER_PASSWORD} - SIPF_SOURCE_POSTGRES_READER_PASSWORD: ${SIPF_SOURCE_POSTGRES_READER_PASSWORD} - ports: - - "${SOLMARA_POSTGRES_PORT:-54329}:5432" + - |- + import os + import re + import stat + + paths = ['/audit/cra', '/audit/nia', '/audit/mosd', '/audit/sipf', '/audit/nagdi'] + for path in paths: + os.chown(path, 0, 0) + os.chmod(path, 0o700) + for entry in os.scandir(path): + if entry.name not in {'audit.jsonl', 'audit.jsonl.lock'} and not re.fullmatch(r'audit\.jsonl\.\d{8}', entry.name): + continue + metadata = entry.stat(follow_symlinks=False) + if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1: + raise RuntimeError('audit sink has unsafe metadata') + os.chown(entry.path, 65532, 65532, follow_symlinks=False) + os.chmod(entry.path, 0o600, follow_symlinks=False) + os.chown(path, 65532, 65532) + user: "0:0" + read_only: true + cap_drop: [ALL] + cap_add: [CHOWN, FOWNER] + security_opt: [no-new-privileges:true] + network_mode: none + restart: "no" volumes: - - postgres-data:/var/lib/postgresql/data - - ./ministries/interior-population/fixtures/001-schema.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro - - ./ministries/interior-population/fixtures/002-load.sql:/docker-entrypoint-initdb.d/002-load.sql:ro - - ./ministries/interior-population/fixtures/population_person.csv:/docker-entrypoint-initdb.d/population_person.csv:ro - - ./ministries/interior-population/fixtures/identity_document.csv:/docker-entrypoint-initdb.d/identity_document.csv:ro - - ./ministries/interior-population/fixtures/consent_directive.csv:/docker-entrypoint-initdb.d/consent_directive.csv:ro - - ./ministries/labour-pensions/fixtures/003-schema.sql:/docker-entrypoint-initdb.d/003-schema.sql:ro - - ./ministries/labour-pensions/fixtures/004-load.sql:/docker-entrypoint-initdb.d/004-load.sql:ro - - ./ministries/labour-pensions/fixtures/pension_case.csv:/docker-entrypoint-initdb.d/pension_case.csv:ro - - ./scripts/init-registry-postgresql.sh:/usr/local/bin/solmara-provision-postgresql.sh:ro - - ./scripts/postgres-entrypoint.sh:/usr/local/bin/solmara-postgres-entrypoint.sh:ro - - ./config/postgres/ssl:/run/solmara-postgres-ssl:ro - healthcheck: - test: ["CMD-SHELL", "test -f /tmp/solmara-postgres-provisioned && pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] - interval: 5s - timeout: 5s - retries: 10 + - cra-relay-audit:/audit/cra + - nia-relay-audit:/audit/nia + - mosd-relay-audit:/audit/mosd + - sipf-relay-audit:/audit/sipf + - nagdi-relay-audit:/audit/nagdi + + # A fresh Relay can create its audit chain before OIDC discovery completes. + # If discovery then fails, the interrupted empty chain makes every later + # consultation fail closed. Prove the shared issuer through the same strict + # TEST-NET gateway path before any Relay process is allowed to start. + relay-issuer-readiness: + image: ${PYTHON_STATIC_IMAGE:?versions.env must pin the static server image} + command: + - python + - -c + - |- + import json + import ssl + import time + import urllib.request + + issuer = 'https://mint.solmara.registrystack.org' + deadline = time.monotonic() + 60 + context = ssl.create_default_context(cafile='/etc/solmara-evidence/tls/ca.crt') + while True: + try: + with urllib.request.urlopen( + issuer + '/.well-known/openid-configuration', + context=context, + timeout=5, + ) as response: + document = json.load(response) + if response.status == 200 and document.get('issuer') == issuer: + break + except Exception: + pass + if time.monotonic() >= deadline: + raise SystemExit('Relay issuer is unavailable') + time.sleep(1) + user: "65532:65532" + read_only: true + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + restart: "no" + volumes: ["./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro"] + networks: [issuer-validation] + depends_on: + mint: {condition: service_started} + evidence-gateway: {condition: service_started} + + mint-signer: + <<: *transit-signer + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-mint"] + volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/mint/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "mint-transit:/transit"] + + cra-signer: + <<: *transit-signer + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-cra"] + volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/cra/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "cra-transit:/transit"] + + nia-signer: + <<: *transit-signer + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-nia"] + volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/nia/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "nia-transit:/transit"] + + sro-signer: + <<: *transit-signer + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-sro"] + volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/sro/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "sro-transit:/transit"] + + mosd-programme-signer: + <<: *transit-signer + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-mosd-programme"] + volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/mosd-programme/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "mosd-programme-transit:/transit"] + + sipf-signer: + <<: *transit-signer + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-sipf"] + volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/sipf/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "sipf-transit:/transit"] + + nagdi-signer: + <<: *transit-signer + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-nagdi"] + volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/nagdi/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "nagdi-transit:/transit"] static-metadata: - image: ${STATIC_METADATA_IMAGE:-python:3.12-alpine} + image: ${PYTHON_STATIC_IMAGE:?versions.env must pin the static server image} command: ["python", "-m", "http.server", "8080", "--bind", "0.0.0.0", "--directory", "/srv/static"] - ports: - - "${SOLMARA_METADATA_PORT:-4331}:8080" - volumes: - - ./metadata/public:/srv/static:ro + volumes: ["./metadata/public:/srv/static:ro"] + ports: ["${SOLMARA_METADATA_PORT:-4331}:8080"] + networks: [runtime] - cra-civil-relay: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay.yaml"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${CRA_RELAY_AUDIT_HASH_SECRET} + cra-relay: + <<: *relay + environment: {SOLMARA_RELAY_AUDIT_KEY: "${CRA_RELAY_AUDIT_KEY:?run just gen-secrets}", SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt} + command: ["serve", "--runtime", "/etc/relay/cra/runtime.yaml"] + volumes: ["./relays/cra/runtime.yaml:/etc/relay/cra/runtime.yaml:ro", "./relays/cra/package:/etc/relay/cra/package:ro", "cra-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "cra-relay-audit:/var/lib/relay/audit"] + depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, cra-source-publisher: {condition: service_completed_successfully}} ports: ["${SOLMARA_CRA_RELAY_PORT:-4311}:8080"] - volumes: - - ./runtime/registry-projects/local/cra-civil/relay:/etc/registry-relay:ro - - ./ministries/interior-civil:/var/lib/solmara/ministries/interior-civil:ro - - cra-relay-cache:/var/lib/registry-relay/cache - nia-population-relay: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay.yaml"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NIA_RELAY_AUDIT_HASH_SECRET} - SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} - DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt + nia-relay: + <<: *relay + environment: {SOLMARA_RELAY_AUDIT_KEY: "${NIA_RELAY_AUDIT_KEY:?run just gen-secrets}", SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt} + command: ["serve", "--runtime", "/etc/relay/nia/runtime.yaml"] + volumes: ["./relays/nia/runtime.yaml:/etc/relay/nia/runtime.yaml:ro", "./relays/nia/package:/etc/relay/nia/package:ro", "nia-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "nia-relay-audit:/var/lib/relay/audit"] + depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, nia-source-publisher: {condition: service_completed_successfully}} ports: ["${SOLMARA_NIA_RELAY_PORT:-4312}:8080"] - volumes: - - ./runtime/registry-projects/local/nia-population/relay:/etc/registry-relay:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - nia-relay-cache:/var/lib/registry-relay/cache - depends_on: - postgres: { condition: service_healthy } - - sro-social-relay: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay.yaml"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SRO_RELAY_AUDIT_HASH_SECRET} - ports: ["${SOLMARA_SRO_RELAY_PORT:-4313}:8080"] - volumes: - - ./runtime/registry-projects/local/sro-social/relay:/etc/registry-relay:ro - - ./ministries/social-development:/var/lib/solmara/ministries/social-development:ro - - sro-relay-cache:/var/lib/registry-relay/cache - programme-mis-relay: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay.yaml"] - environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} - ports: ["${SOLMARA_PROGRAMME_RELAY_PORT:-4314}:8080"] - volumes: - - ./runtime/registry-projects/local/mosd-programme/relay:/etc/registry-relay:ro - - ./ministries/social-development:/var/lib/solmara/ministries/social-development:ro - - programme-relay-cache:/var/lib/registry-relay/cache + mosd-relay: + <<: *relay + environment: {SOLMARA_RELAY_AUDIT_KEY: "${MOSD_RELAY_AUDIT_KEY:?run just gen-secrets}", SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt} + command: ["serve", "--runtime", "/etc/relay/mosd/runtime.yaml"] + volumes: ["./relays/mosd/runtime.yaml:/etc/relay/mosd/runtime.yaml:ro", "./relays/mosd/package:/etc/relay/mosd/package:ro", "mosd-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "mosd-relay-audit:/var/lib/relay/audit"] + depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, mosd-source-publisher: {condition: service_completed_successfully}} + ports: ["${SOLMARA_MOSD_RELAY_PORT:-4314}:8080"] - sipf-pensions-relay: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay.yaml"] + sipf-relay: + <<: *relay environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SIPF_RELAY_AUDIT_HASH_SECRET} - SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} - DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt + SOLMARA_RELAY_AUDIT_KEY: ${SIPF_RELAY_AUDIT_KEY:?run just gen-secrets} + SOLMARA_RELAY_CURSOR_KEY: ${SIPF_RELAY_CURSOR_KEY:?run just gen-secrets} + SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt + command: ["serve", "--runtime", "/etc/relay/sipf/runtime.yaml"] + volumes: ["./relays/sipf/runtime.yaml:/etc/relay/sipf/runtime.yaml:ro", "./relays/sipf/package:/etc/relay/sipf/package:ro", "sipf-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "sipf-relay-audit:/var/lib/relay/audit"] + depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, sipf-source-publisher: {condition: service_completed_successfully}} ports: ["${SOLMARA_SIPF_RELAY_PORT:-4315}:8080"] - volumes: - - ./runtime/registry-projects/local/sipf-pensions/relay:/etc/registry-relay:ro - - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro - - sipf-relay-cache:/var/lib/registry-relay/cache - depends_on: - postgres: { condition: service_healthy } - nagdi-agriculture-relay: - <<: *relay-common - command: ["--config", "/etc/registry-relay/relay.yaml"] + nagdi-relay: + <<: *relay environment: - <<: *relay-environment - REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NAGDI_RELAY_AUDIT_HASH_SECRET} + SOLMARA_RELAY_AUDIT_KEY: ${NAGDI_RELAY_AUDIT_KEY:?run just gen-secrets} + SOLMARA_RELAY_CURSOR_KEY: ${NAGDI_RELAY_CURSOR_KEY:?run just gen-secrets} + SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt + command: ["serve", "--runtime", "/etc/relay/nagdi/runtime.yaml"] + volumes: ["./relays/nagdi/runtime.yaml:/etc/relay/nagdi/runtime.yaml:ro", "./relays/nagdi/package:/etc/relay/nagdi/package:ro", "nagdi-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "nagdi-relay-audit:/var/lib/relay/audit"] + depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, nagdi-source-publisher: {condition: service_completed_successfully}} ports: ["${SOLMARA_NAGDI_RELAY_PORT:-4316}:8080"] - volumes: - - ./runtime/registry-projects/local/nagdi-agriculture/relay:/etc/registry-relay:ro - - ./ministries/agriculture-nagdi:/var/lib/solmara/ministries/agriculture-nagdi:ro - - nagdi-relay-cache:/var/lib/registry-relay/cache - evidence-secrets-init: - image: ${VOLUME_INIT_IMAGE:?versions.env must pin the volume init image} - restart: "no" - command: ["sh", "-ec", "cp /seed/audit-hmac-key /seed/subject-binding-hmac-key /seed/signing-ed25519-private-jwk /target/; chown -R 65532:65532 /target /audit; chmod 700 /target /audit; chmod 600 /target/*"] - volumes: - - ./config/evidence/local/evidence:/seed:ro - - evidence-secrets:/target - - evidence-audit:/audit - - cra-workload-agent: - <<: *workload-agent-common - network_mode: service:cra-civil-relay - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["civil_people:rows"],"token_file":"/run/evidence-secrets/cra-relay-token","private_jwk_env":"CRA_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}] - CRA_RELAY_WORKLOAD_JWK: ${CRA_RELAY_WORKLOAD_JWK} - volumes: [evidence-secrets:/run/evidence-secrets] - depends_on: - evidence-secrets-init: { condition: service_completed_successfully } - cra-civil-relay: { condition: service_started } - - nia-workload-agent: - <<: *workload-agent-common - network_mode: service:nia-population-relay - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["population:rows"],"token_file":"/run/evidence-secrets/nia-relay-token","private_jwk_env":"NIA_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}, - {"audience":"registry-relay","azp":"solmara-esignet","subject":"solmara-esignet","scopes":["population:identity_release"],"token_file":"/run/esignet-secrets/solmara-esignet-relay-token","private_jwk_env":"NIA_ESIGNET_RELAY_WORKLOAD_JWK","token_uid":1001,"token_gid":1001}] - NIA_RELAY_WORKLOAD_JWK: ${NIA_RELAY_WORKLOAD_JWK} - NIA_ESIGNET_RELAY_WORKLOAD_JWK: ${NIA_ESIGNET_RELAY_WORKLOAD_JWK} + mint: + image: ${SOLMARA_MINT_IMAGE:?versions.env must name the checksum-verified Mint image} + platform: linux/amd64 + command: ["serve", "--config", "/etc/registry-mint/mint.yaml"] + restart: unless-stopped + user: "0:0" + read_only: true + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + tmpfs: [/tmp] volumes: - - evidence-secrets:/run/evidence-secrets - - nia-esignet-workload-token:/run/esignet-secrets - depends_on: - evidence-secrets-init: { condition: service_completed_successfully } - nia-population-relay: { condition: service_started } + - ./runtime/evidence-cells/mint:/etc/registry-mint:ro + - ./runtime/evidence-cells/secrets/mint:/run/secrets/registry-mint:ro + - mint-transit:/run/registry-mint:ro + - mint-v2-audit:/var/lib/registry-mint + networks: + runtime: {ipv4_address: 172.29.1.20} + depends_on: {mint-signer: {condition: service_healthy}} - sro-workload-agent: - <<: *workload-agent-common - network_mode: service:sro-social-relay - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["sro_child:rows"],"token_file":"/run/evidence-secrets/sro-relay-token","private_jwk_env":"SRO_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}] - SRO_RELAY_WORKLOAD_JWK: ${SRO_RELAY_WORKLOAD_JWK} - volumes: [evidence-secrets:/run/evidence-secrets] - depends_on: - evidence-secrets-init: { condition: service_completed_successfully } - sro-social-relay: { condition: service_started } + cra-evidence: + <<: *evidence + command: ["--runtime", "/etc/registry-evidence/cra/runtime.yaml", "serve"] + volumes: ["./runtime/evidence-cells/cells/cra:/etc/registry-evidence/cra:ro", "./runtime/evidence-cells/secrets/cra:/run/secrets/registry-evidence/cra:ro", "cra-transit:/run/registry-evidence/cra:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/cra/extracts:ro", "cra-evidence-audit:/var/lib/registry-evidence/cra/audit"] + networks: {runtime: {ipv4_address: 172.29.1.21}, issuer-validation: {}} + depends_on: {mint: {condition: service_started}, cra-signer: {condition: service_healthy}, cra-relay: {condition: service_healthy}} - programme-workload-agent: - <<: *workload-agent-common - network_mode: service:programme-mis-relay - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["mosd_enroll:rows"],"token_file":"/run/evidence-secrets/programme-relay-token","private_jwk_env":"PROGRAMME_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}] - PROGRAMME_RELAY_WORKLOAD_JWK: ${PROGRAMME_RELAY_WORKLOAD_JWK} - volumes: [evidence-secrets:/run/evidence-secrets] - depends_on: - evidence-secrets-init: { condition: service_completed_successfully } - programme-mis-relay: { condition: service_started } + nia-evidence: + <<: *evidence + command: ["--runtime", "/etc/registry-evidence/nia/runtime.yaml", "serve"] + volumes: ["./runtime/evidence-cells/cells/nia:/etc/registry-evidence/nia:ro", "./runtime/evidence-cells/secrets/nia:/run/secrets/registry-evidence/nia:ro", "nia-transit:/run/registry-evidence/nia:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/nia/extracts:ro", "nia-evidence-audit:/var/lib/registry-evidence/nia/audit"] + networks: {runtime: {ipv4_address: 172.29.1.22}, issuer-validation: {}} + depends_on: {mint: {condition: service_started}, nia-signer: {condition: service_healthy}} - sipf-workload-agent: - <<: *workload-agent-common - network_mode: service:sipf-pensions-relay - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["pension:rows","survivor:rows"],"token_file":"/run/evidence-secrets/sipf-relay-token","private_jwk_env":"SIPF_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}] - SIPF_RELAY_WORKLOAD_JWK: ${SIPF_RELAY_WORKLOAD_JWK} - volumes: [evidence-secrets:/run/evidence-secrets] - depends_on: - evidence-secrets-init: { condition: service_completed_successfully } - sipf-pensions-relay: { condition: service_started } + sro-evidence: + <<: *evidence + command: ["--runtime", "/etc/registry-evidence/sro/runtime.yaml", "serve"] + volumes: ["./runtime/evidence-cells/cells/sro:/etc/registry-evidence/sro:ro", "./runtime/evidence-cells/secrets/sro:/run/secrets/registry-evidence/sro:ro", "sro-transit:/run/registry-evidence/sro:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/sro/extracts:ro", "sro-evidence-audit:/var/lib/registry-evidence/sro/audit"] + networks: {runtime: {ipv4_address: 172.29.1.23}, issuer-validation: {}} + depends_on: {mint: {condition: service_started}, sro-signer: {condition: service_healthy}} - nagdi-workload-agent: - <<: *workload-agent-common - network_mode: service:nagdi-agriculture-relay - environment: - WORKLOAD_ISSUER: http://127.0.0.1:8090 - WORKLOAD_IDENTITIES_JSON: >- - [{"audience":"registry-relay","azp":"solmara-evidence","subject":"solmara-evidence","scopes":["nagdi_farmer:rows","nagdi_herd:rows"],"token_file":"/run/evidence-secrets/nagdi-relay-token","private_jwk_env":"NAGDI_RELAY_WORKLOAD_JWK","token_uid":65532,"token_gid":65532}] - NAGDI_RELAY_WORKLOAD_JWK: ${NAGDI_RELAY_WORKLOAD_JWK} - volumes: [evidence-secrets:/run/evidence-secrets] - depends_on: - evidence-secrets-init: { condition: service_completed_successfully } - nagdi-agriculture-relay: { condition: service_started } + mosd-programme-evidence: + <<: *evidence + command: ["--runtime", "/etc/registry-evidence/mosd-programme/runtime.yaml", "serve"] + volumes: ["./runtime/evidence-cells/cells/mosd-programme:/etc/registry-evidence/mosd-programme:ro", "./runtime/evidence-cells/secrets/mosd-programme:/run/secrets/registry-evidence/mosd-programme:ro", "mosd-programme-transit:/run/registry-evidence/mosd-programme:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "mosd-evidence-audit:/var/lib/registry-evidence/mosd-programme/audit"] + networks: {runtime: {ipv4_address: 172.29.1.24}, issuer-validation: {}} + depends_on: {mint: {condition: service_started}, mosd-programme-signer: {condition: service_healthy}, mosd-relay: {condition: service_healthy}} - mint-secrets-init: - image: ${VOLUME_INIT_IMAGE:?versions.env must pin the volume init image} - restart: "no" - command: ["sh", "-ec", "cp /seed/signing.jwk /seed/audit-hmac-key /target/; chown -R 65532:65532 /target /audit; chmod 700 /target /audit; chmod 600 /target/*"] - volumes: - - ./config/evidence/local/mint:/seed:ro - - mint-secrets:/target - - mint-audit:/audit + sipf-evidence: + <<: *evidence + command: ["--runtime", "/etc/registry-evidence/sipf/runtime.yaml", "serve"] + volumes: ["./runtime/evidence-cells/cells/sipf:/etc/registry-evidence/sipf:ro", "./runtime/evidence-cells/secrets/sipf:/run/secrets/registry-evidence/sipf:ro", "sipf-transit:/run/registry-evidence/sipf:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "sipf-evidence-audit:/var/lib/registry-evidence/sipf/audit"] + networks: {runtime: {ipv4_address: 172.29.1.25}, issuer-validation: {}} + depends_on: {mint: {condition: service_started}, sipf-signer: {condition: service_healthy}, sipf-relay: {condition: service_healthy}} - mint: - image: ${SOLMARA_MINT_IMAGE:?versions.env must name the locally built Registry Stack main Mint image} - restart: unless-stopped - command: ["serve", "--config", "/etc/registry-mint/config.yaml"] - volumes: - - ./evidence/mint.yaml:/etc/registry-mint/config.yaml:ro - - ./config/evidence/local/mint/clients:/etc/registry-mint/clients:ro - - mint-secrets:/run/secrets/registry-mint:ro - - mint-audit:/var/lib/registry-mint/audit - networks: [evidence] - depends_on: - mint-secrets-init: { condition: service_completed_successfully } + nagdi-evidence: + <<: *evidence + command: ["--runtime", "/etc/registry-evidence/nagdi/runtime.yaml", "serve"] + volumes: ["./runtime/evidence-cells/cells/nagdi:/etc/registry-evidence/nagdi:ro", "./runtime/evidence-cells/secrets/nagdi:/run/secrets/registry-evidence/nagdi:ro", "nagdi-transit:/run/registry-evidence/nagdi:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "nagdi-evidence-audit:/var/lib/registry-evidence/nagdi/audit"] + networks: {runtime: {ipv4_address: 172.29.1.26}, issuer-validation: {}} + depends_on: {mint: {condition: service_started}, nagdi-signer: {condition: service_healthy}, nagdi-relay: {condition: service_healthy}} evidence-gateway: - image: ${EVIDENCE_GATEWAY_IMAGE:?versions.env must pin the Evidence TLS gateway image} + image: ${EVIDENCE_GATEWAY_IMAGE:?versions.env must pin Caddy} user: "0:0" restart: unless-stopped - ports: - - "${SOLMARA_EVIDENCE_GATEWAY_PORT:-4341}:443" - volumes: - - ./evidence/Caddyfile:/etc/caddy/Caddyfile:ro - - ./config/evidence/local/tls:/etc/solmara-evidence/tls:ro - - caddy-data:/data - - caddy-config:/config + ports: ["${SOLMARA_EVIDENCE_GATEWAY_PORT:-4341}:443"] + volumes: ["./config/evidence/Caddyfile:/etc/caddy/Caddyfile:ro", "./config/evidence/local/tls:/etc/solmara-evidence/tls:ro", "caddy-data:/data", "caddy-config:/config"] networks: - default: {} - evidence: + runtime: + ipv4_address: 172.29.1.10 aliases: - - mint.evidence.solmara.invalid - evidence.solmara.invalid - - cra-relay.evidence.solmara.invalid - - nia-relay.evidence.solmara.invalid - - sro-relay.evidence.solmara.invalid - - programme-relay.evidence.solmara.invalid - - sipf-relay.evidence.solmara.invalid - - nagdi-relay.evidence.solmara.invalid - - evidence: - image: ${SOLMARA_EVIDENCE_IMAGE:?versions.env must name the locally built Registry Stack main Evidence image} - restart: unless-stopped - command: ["--runtime", "/etc/registry-evidence/runtime.yaml", "serve"] - environment: - SSL_CERT_FILE: /etc/registry-evidence/tls/lab-ca.crt - volumes: - - ./evidence/runtime.yaml:/etc/registry-evidence/runtime.yaml:ro - - ./evidence/bundle:/etc/registry-evidence/bundle:ro - - ./config/evidence/local/tls/ca.crt:/etc/registry-evidence/tls/lab-ca.crt:ro - - evidence-secrets:/run/secrets/registry-evidence:ro - - evidence-audit:/var/lib/registry-evidence/audit - networks: - evidence: - ipv4_address: 172.29.0.10 - depends_on: - evidence-gateway: { condition: service_started } - cra-workload-agent: { condition: service_healthy } - nia-workload-agent: { condition: service_healthy } - sro-workload-agent: { condition: service_healthy } - programme-workload-agent: { condition: service_healthy } - sipf-workload-agent: { condition: service_healthy } - nagdi-workload-agent: { condition: service_healthy } + - cra-relay.solmara.registrystack.org + - mosd-programme-relay.solmara.registrystack.org + - sipf-relay.solmara.registrystack.org + - nagdi-relay.solmara.registrystack.org + # Relay's production OIDC fetch policy rejects private, loopback, and + # metadata destinations. TEST-NET-1 gives the hermetic local gateway a + # non-routable issuer address without weakening that runtime policy. + issuer-validation: + ipv4_address: 192.0.2.30 + aliases: [mint.solmara.registrystack.org] child-benefit-federator: - build: - context: . - dockerfile: scenario-runner/Dockerfile + build: {context: ., dockerfile: scenario-runner/Dockerfile} image: ${SOLMARA_CHILD_BENEFIT_FEDERATOR_IMAGE:-solmara-lab-scenario-runner:local} command: ["python", "scenario-runner/child_benefit_federator.py"] environment: <<: *evidence-client-environment CHILD_BENEFIT_FEDERATOR_HOST: 0.0.0.0 - CHILD_BENEFIT_FEDERATOR_PORT: 8080 - CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN} - ports: ["${SOLMARA_CHILD_BENEFIT_FEDERATOR_PORT:-4321}:8080"] - volumes: - - ./config/evidence/local/mint/client-private.jwk:/run/secrets/solmara-evidence-client.jwk:ro - - ./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro - networks: [default, evidence] - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3)\""] - interval: 10s - timeout: 5s - retries: 12 - depends_on: - evidence: { condition: service_started } - mint: { condition: service_started } + CHILD_BENEFIT_FEDERATOR_PORT: "8080" + CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN:?run just gen-secrets} + volumes: ["./config/evidence/local/cells/mint/clients/solmara-demo-client-key:/run/secrets/solmara-evidence-client.jwk:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro"] + networks: [runtime, issuer-validation] scenario-runner: - build: - context: . - dockerfile: scenario-runner/Dockerfile + build: {context: ., dockerfile: scenario-runner/Dockerfile} image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:-solmara-lab-scenario-runner:local} environment: <<: *evidence-client-environment SCENARIO_RUNNER_HOST: 0.0.0.0 - SCENARIO_RUNNER_PORT: 8080 + SCENARIO_RUNNER_PORT: "8080" SOLMARA_LAB_MODE: local CHILD_BENEFIT_FEDERATOR_URL: http://child-benefit-federator:8080 - CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN} - volumes: - - ./config/evidence/local/mint/client-private.jwk:/run/secrets/solmara-evidence-client.jwk:ro - - ./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro - networks: [default, evidence] - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3)\""] - interval: 10s - timeout: 5s - retries: 12 - depends_on: - child-benefit-federator: { condition: service_healthy } + CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN:?run just gen-secrets} + volumes: ["./config/evidence/local/cells/mint/clients/solmara-demo-client-key:/run/secrets/solmara-evidence-client.jwk:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro"] + networks: [runtime, issuer-validation] portal: - build: - context: ./portal + build: {context: ./portal} image: ${SOLMARA_PORTAL_IMAGE:-solmara-lab-portal:local} environment: <<: *evidence-client-environment HOST: 0.0.0.0 - PORT: 4000 + PORT: "4000" PORTAL_PROVIDER: live PORTAL_AUTH_PROVIDER: ${PORTAL_AUTH_PROVIDER:-mock} - PORTAL_SESSION_SECRET: ${PORTAL_SESSION_SECRET} + PORTAL_SESSION_SECRET: ${PORTAL_SESSION_SECRET:?run just gen-secrets} CHILD_BENEFIT_FEDERATOR_URL: http://child-benefit-federator:8080 - CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN} + CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN:?run just gen-secrets} SCENARIO_RUNNER_URL: http://scenario-runner:8080 ports: ["${SOLMARA_PORTAL_PORT:-4300}:4000"] - volumes: - - ./config/evidence/local/mint/client-private.jwk:/run/secrets/solmara-evidence-client.jwk:ro - - ./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro - healthcheck: - test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:4000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] - interval: 30s - timeout: 10s - retries: 3 - depends_on: - scenario-runner: { condition: service_healthy } + volumes: ["./config/evidence/local/cells/mint/clients/solmara-demo-client-key:/run/secrets/solmara-evidence-client.jwk:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro"] + networks: [runtime, issuer-validation] home: - build: - context: . - dockerfile: home/Dockerfile + build: {context: ., dockerfile: home/Dockerfile} image: ${SOLMARA_HOME_IMAGE:-solmara-lab-home:local} environment: HOST: 0.0.0.0 - PORT: 4301 + PORT: "4301" SOLMARA_LAB_ROOT: /app - REPO_URL: ${REPO_URL:-https://github.com/registrystack/solmara-lab} - PORTAL_URL: http://127.0.0.1:${SOLMARA_PORTAL_PORT:-4300} - SCENARIO_RUNNER_URL: http://scenario-runner:8080 - STATIC_METADATA_URL: http://static-metadata:8080 - PORTAL_PROBE_URL: http://portal:4000 - CRA_CIVIL_RELAY_URL: http://cra-civil-relay:8080 - NIA_POPULATION_RELAY_URL: http://nia-population-relay:8080 - SRO_SOCIAL_RELAY_URL: http://sro-social-relay:8080 - PROGRAMME_MIS_RELAY_URL: http://programme-mis-relay:8080 - SIPF_PENSIONS_RELAY_URL: http://sipf-pensions-relay:8080 - NAGDI_AGRICULTURE_RELAY_URL: http://nagdi-agriculture-relay:8080 - EVIDENCE_URL: http://evidence:8080 - MINT_URL: http://mint:8081 - CHILD_BENEFIT_FEDERATOR_URL: http://child-benefit-federator:8080 - HOME_PUBLISHED_TOKENS: "{}" - networks: [default, evidence] + PORTAL_URL: "http://127.0.0.1:${SOLMARA_PORTAL_PORT:-4300}" + STATIC_METADATA_URL: "http://static-metadata:8080" + CRA_CIVIL_RELAY_URL: "http://cra-relay:8080" + NIA_POPULATION_RELAY_URL: "http://nia-relay:8080" + MOSD_PROGRAMME_RELAY_URL: "http://mosd-relay:8080" + SIPF_PENSIONS_RELAY_URL: "http://sipf-relay:8080" + NAGDI_AGRICULTURE_RELAY_URL: "http://nagdi-relay:8080" + SOLMARA_CRA_EVIDENCE_URL: "http://cra-evidence:8080" + SOLMARA_NIA_EVIDENCE_URL: "http://nia-evidence:8080" + SOLMARA_SRO_EVIDENCE_URL: "http://sro-evidence:8080" + SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL: "http://mosd-programme-evidence:8080" + SOLMARA_SIPF_EVIDENCE_URL: "http://sipf-evidence:8080" + SOLMARA_NAGDI_EVIDENCE_URL: "http://nagdi-evidence:8080" + MINT_URL: "http://mint:8081" + CHILD_BENEFIT_FEDERATOR_URL: "http://child-benefit-federator:8080" + SCENARIO_RUNNER_URL: "http://scenario-runner:8080" + PORTAL_PROBE_URL: "http://portal:4000" ports: ["${SOLMARA_HOME_PORT:-4301}:4301"] - volumes: - - ./output/smoke:/app/output/smoke:ro - - ./generator/output:/app/generator/output:ro - healthcheck: - test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:4301/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] - interval: 30s - timeout: 10s - retries: 3 - depends_on: - static-metadata: { condition: service_started } - scenario-runner: { condition: service_healthy } + volumes: ["./output/smoke:/app/output/smoke:ro", "./generator/output:/app/generator/output:ro"] + networks: [runtime] volumes: - postgres-data: - cra-relay-cache: - nia-relay-cache: - sro-relay-cache: - programme-relay-cache: - sipf-relay-cache: - nagdi-relay-cache: - evidence-secrets: - evidence-audit: - mint-secrets: - mint-audit: - nia-esignet-workload-token: + cra-relay-source: + nia-relay-source: + mosd-relay-source: + sipf-relay-source: + nagdi-relay-source: + mint-transit: + cra-transit: + nia-transit: + sro-transit: + mosd-programme-transit: + sipf-transit: + nagdi-transit: + cra-relay-audit: + nia-relay-audit: + mosd-relay-audit: + sipf-relay-audit: + nagdi-relay-audit: + cra-evidence-audit: + nia-evidence-audit: + sro-evidence-audit: + mosd-evidence-audit: + sipf-evidence-audit: + nagdi-evidence-audit: + mint-v2-audit: caddy-data: caddy-config: networks: - evidence: - ipam: - config: - - subnet: 172.29.0.0/24 + runtime: + ipam: {config: [{subnet: 172.29.1.0/24}]} + issuer-validation: + internal: true + ipam: {config: [{subnet: 192.0.2.0/27, ip_range: 192.0.2.0/28}]} diff --git a/config/esignet/nginx.conf b/config/esignet/nginx.conf index 8e7d303..e27cc60 100644 --- a/config/esignet/nginx.conf +++ b/config/esignet/nginx.conf @@ -1,12 +1,18 @@ worker_processes 1; +pid /tmp/nginx.pid; events { worker_connections 1024; } http { - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; + access_log /dev/stdout; + error_log /dev/stderr; + client_body_temp_path /tmp/nginx-client-body; + proxy_temp_path /tmp/nginx-proxy; + fastcgi_temp_path /tmp/nginx-fastcgi; + uwsgi_temp_path /tmp/nginx-uwsgi; + scgi_temp_path /tmp/nginx-scgi; server { listen 3000; diff --git a/config/evidence/Caddyfile b/config/evidence/Caddyfile new file mode 100644 index 0000000..232437d --- /dev/null +++ b/config/evidence/Caddyfile @@ -0,0 +1,58 @@ +{ + auto_https off +} + +(solmara_tls) { + tls /etc/solmara-evidence/tls/gateway.crt /etc/solmara-evidence/tls/gateway.key +} + +https://cra-relay.solmara.registrystack.org { + import solmara_tls + reverse_proxy cra-relay:8080 +} + +https://mosd-programme-relay.solmara.registrystack.org { + import solmara_tls + reverse_proxy mosd-relay:8080 +} + +https://sipf-relay.solmara.registrystack.org { + import solmara_tls + reverse_proxy sipf-relay:8080 +} + +https://nagdi-relay.solmara.registrystack.org { + import solmara_tls + reverse_proxy nagdi-relay:8080 +} + +https://localhost, https://mint.solmara.registrystack.org, https://evidence.solmara.invalid { + import solmara_tls + handle_path /evidence/cra/* { + reverse_proxy 172.29.1.21:8080 + } + handle_path /evidence/nia/* { + reverse_proxy 172.29.1.22:8080 + } + handle_path /evidence/sro/* { + reverse_proxy 172.29.1.23:8080 + } + handle_path /evidence/mosd-programme/* { + reverse_proxy 172.29.1.24:8080 + } + handle_path /evidence/sipf/* { + reverse_proxy 172.29.1.25:8080 + } + handle_path /evidence/nagdi/* { + reverse_proxy 172.29.1.26:8080 + } + handle_path /child-benefit/* { + reverse_proxy child-benefit-federator:8080 + } + handle /token* { + reverse_proxy 172.29.1.20:8081 + } + handle /.well-known/* { + reverse_proxy 172.29.1.20:8081 + } +} diff --git a/config/hosted-relay-bundles/cra-civil/antirollback-seed.json b/config/hosted-relay-bundles/cra-civil/antirollback-seed.json deleted file mode 100644 index e06d6a0..0000000 --- a/config/hosted-relay-bundles/cra-civil/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-cra-civil" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/cra-civil/bootstrap.yaml b/config/hosted-relay-bundles/cra-civil/bootstrap.yaml deleted file mode 100644 index 3e84b75..0000000 --- a/config/hosted-relay-bundles/cra-civil/bootstrap.yaml +++ /dev/null @@ -1,87 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: [] - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://cra-relay.solmara.registrystack.org - publisher: solmara-cra-civil - title: solmara-cra-civil governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized civil_people entity - entities: [] - id: civil_people - owner: solmara-cra-civil - sensitivity: personal - tables: - - access: - aggregate_scope: civil_people:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: birth_brn - nullable: true - sensitive: true - type: string - - name: birth_date - nullable: false - sensitive: true - type: date - - name: deceased - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv - type: file - title: civil_people - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: cra-civil-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/cra-civil/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/cra-civil/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/relay.yaml b/config/hosted-relay-bundles/cra-civil/bundle/config/relay.yaml deleted file mode 100644 index 3e84b75..0000000 --- a/config/hosted-relay-bundles/cra-civil/bundle/config/relay.yaml +++ /dev/null @@ -1,87 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: [] - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://cra-relay.solmara.registrystack.org - publisher: solmara-cra-civil - title: solmara-cra-civil governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized civil_people entity - entities: [] - id: civil_people - owner: solmara-cra-civil - sensitivity: personal - tables: - - access: - aggregate_scope: civil_people:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: birth_brn - nullable: true - sensitive: true - type: string - - name: birth_date - nullable: false - sensitive: true - type: date - - name: deceased - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv - type: file - title: civil_people - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: cra-civil-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/cra-civil/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/cra-civil/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/cra-civil/bundle/manifest.json b/config/hosted-relay-bundles/cra-civil/bundle/manifest.json deleted file mode 100644 index cdc315a..0000000 --- a/config/hosted-relay-bundles/cra-civil/bundle/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-cra-civil", - "instance_id": "cra-civil-relay", - "bundle_id": "solmara-hosted-cra-civil-sequence-2", - "sequence": 2, - "config_hash": "sha256:3d6bdc3437e5c9580539e430ce013d2a807625f2185b58c844fd20f0182340b1", - "files": [ - { - "path": "config/relay.yaml", - "sha256": "sha256:3d6bdc3437e5c9580539e430ce013d2a807625f2185b58c844fd20f0182340b1" - } - ], - "created_at": "2026-07-29T03:29:28.170694Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/bundle/manifest.sig.json b/config/hosted-relay-bundles/cra-civil/bundle/manifest.sig.json deleted file mode 100644 index a69c53e..0000000 --- a/config/hosted-relay-bundles/cra-civil/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "EHImKMJHy-4Xcy4OK32_SlQhf0DPmjAqwyvxUoKL7oPcdwBF1mnbBo3Fud1GPpfzQEwFpOgQO8FkFQAHEhkcDw" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/consultation/antirollback-seed.json b/config/hosted-relay-bundles/cra-civil/consultation/antirollback-seed.json deleted file mode 100644 index 47b6862..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-cra-civil-consultation" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bootstrap.yaml b/config/hosted-relay-bundles/cra-civil/consultation/bootstrap.yaml deleted file mode 100644 index ee00efb..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bootstrap.yaml +++ /dev/null @@ -1,142 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - cra-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://cra-relay.solmara.registrystack.org - publisher: solmara-cra-civil - title: solmara-cra-civil governed Registry Relay -consultation: - artifacts: - evidence: - - class: conformance - path: artifacts/evidence/civil-person-by-uin/conformance.json - sha256: sha256:1943d411187b226b981cddc2cd0442e73953d29cb5a047bbb38c729d78883afd - - class: negative_security - path: artifacts/evidence/civil-person-by-uin/negative-security.json - sha256: sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f - - class: minimization - path: artifacts/evidence/civil-person-by-uin/minimization.json - sha256: sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263 - integration_packs: - - hash: sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499 - path: artifacts/integration-packs/civil-person-by-uin.json - sha256: sha256:ad85a4bbcbd9c164264afeb98c11c549cc0c54997b39572261282211ab9104b2 - private_bindings: - - hash: sha256:1fb0b8c2b28da70d6f22675209ced135f4855fcfdc2fd38a32aadaa0c70be0dd - path: artifacts/private-bindings/cra-child-benefit-civil.json - sha256: sha256:087d5ddfc5bbc43620f9bf499cda7343ff0b762427451f30fd3541bb7d230e22 - - hash: sha256:918b533b198e4e45e368db0b3d8737728700f4b6ee428e10a93a1a06460649b1 - path: artifacts/private-bindings/cra-citizen-record-civil.json - sha256: sha256:d564828b5a94041f3bb718c16b40a0a53ea68a94ed917bf96e5ed04f0ec8d086 - - hash: sha256:fbac0434f957c897098a1577a20ae8763258b1a1e2385a577129258dbbe86579 - path: artifacts/private-bindings/cra-pension-death-civil.json - sha256: sha256:f4eea1f1f2df7755c729d0a645778ca9805e15e3875962595c0038cc46142f51 - public_contracts: - - hash: sha256:35acae8dfe5b6cc1c18f9aedbc188c93ded49a46553b7ab21451eb772e26918e - path: artifacts/consultation-contracts/cra-child-benefit-civil.json - sha256: sha256:65bfb5000e186b4e461c392454e01c76149e821551152ec79a22f28b1e15f6b1 - - hash: sha256:c101d11d7fe66efe8400330851d65e4e105be62eae6ae9325eb43a83daae8aed - path: artifacts/consultation-contracts/cra-citizen-record-civil.json - sha256: sha256:ffb8a94778c5d1015c14ecc03fd06d7f2b2d5926aca0654cc80887d930bf74d2 - - hash: sha256:80486f20e013dd4a99af89a22efe3c83638f37f3f79ea7c912a8d5b237db2924 - path: artifacts/consultation-contracts/cra-pension-death-civil.json - sha256: sha256:2fbbfadc074822821239ff13ead71115d7d64c6378e39c4d03a870eb3b6655ea - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: cra-notary - principal_id: cra-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 5222333210205388 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 3104283851243516304 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized civil_people entity - entities: [] - id: civil_people - owner: solmara-cra-civil - sensitivity: personal - tables: - - access: - aggregate_scope: civil_people:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: birth_brn - nullable: true - sensitive: true - type: string - - name: birth_date - nullable: false - sensitive: true - type: date - - name: deceased - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv - type: file - title: civil_people - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: cra-civil-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/cra-civil/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/cra-civil/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-child-benefit-civil.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-child-benefit-civil.json deleted file mode 100644 index aad610b..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-child-benefit-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-child-benefit.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:9ec50673e36783484c8e35137bd65396a871ecc12c2e8fdd2cc68b1f0cab108b","id":"relay.cra-child-benefit.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:cra-child-benefit","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-citizen-record-civil.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-citizen-record-civil.json deleted file mode 100644 index f0ef1ff..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-citizen-record-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-citizen-record.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"citizen-self-service","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:ddf7a76ffe3dabacabdc1df18588bb87826a00a061572ad49c5052baf073f576","id":"relay.cra-citizen-record.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/citizen-self-service"],"required_scope":"registry:consult:cra-citizen-record","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-pension-death-civil.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-pension-death-civil.json deleted file mode 100644 index 2f64031..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/consultation-contracts/cra-pension-death-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-pension-death.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:dd59f03e034f68b35706ae6193bdd580081f2b596c6b92673fc7a66961e34fc1","id":"relay.cra-pension-death.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:cra-pension-death","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/conformance.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/conformance.json deleted file mode 100644 index 9e550c6..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"birth-is-registered":true,"child-age-under-5":false,"civil-record-linked":true,"person-is-deceased":false},"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-future-birth-date","request":{"claims":[{"id":"birth-is-registered"},{"id":"child-age-under-5"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"},"variables":{"as_of_date":"2026-07-14"}},"variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"claims":{"birth-is-registered":true,"child-age-under-5":true,"civil-record-linked":true,"person-is-deceased":false},"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-match","request":{"claims":[{"id":"civil-record-linked"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/citizen-self-service","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"claims":{"birth-is-registered":false,"child-age-under-5":false,"civil-record-linked":false,"person-is-deceased":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"cra-civil-person-no-match","request":{"claims":[{"id":"person-is-deceased"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/pension-payment-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"9999999999"}],"type":"Person"}},"variables":{"as_of_date":"2026-07-14"}}],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/minimization.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/minimization.json deleted file mode 100644 index 9d92e3c..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-cra-civil-person-by-uin","operations":{},"outputs":{"birth_brn":{"from":"snapshot.record.birth_brn","max_bytes":128,"maximum":null,"minimum":null,"nullable":true,"type":"string"},"birth_date":{"from":"snapshot.record.birth_date","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"date"},"deceased":{"from":"snapshot.record.deceased","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/negative-security.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/negative-security.json deleted file mode 100644 index 18ba42a..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/evidence/civil-person-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/integration-packs/civil-person-by-uin.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/integration-packs/civil-person-by-uin.json deleted file mode 100644 index 5533854..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/integration-packs/civil-person-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:1943d411187b226b981cddc2cd0442e73953d29cb5a047bbb38c729d78883afd"],"minimization":["sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263"],"negative_security":["sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-cra-civil-person-by-uin","output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-child-benefit-civil.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-child-benefit-civil.json deleted file mode 100644 index 329227a..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-child-benefit-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-child-benefit.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-citizen-record-civil.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-citizen-record-civil.json deleted file mode 100644 index ea8fb47..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-citizen-record-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-citizen-record.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-pension-death-civil.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-pension-death-civil.json deleted file mode 100644 index 5b5ec3b..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/artifacts/private-bindings/cra-pension-death-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-pension-death.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/relay.yaml b/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/relay.yaml deleted file mode 100644 index ee00efb..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/config/relay.yaml +++ /dev/null @@ -1,142 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - cra-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://cra-relay.solmara.registrystack.org - publisher: solmara-cra-civil - title: solmara-cra-civil governed Registry Relay -consultation: - artifacts: - evidence: - - class: conformance - path: artifacts/evidence/civil-person-by-uin/conformance.json - sha256: sha256:1943d411187b226b981cddc2cd0442e73953d29cb5a047bbb38c729d78883afd - - class: negative_security - path: artifacts/evidence/civil-person-by-uin/negative-security.json - sha256: sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f - - class: minimization - path: artifacts/evidence/civil-person-by-uin/minimization.json - sha256: sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263 - integration_packs: - - hash: sha256:84e87197948e5b921d95efc8cfe1b3d08159585e9473d8d54299c73da90f9499 - path: artifacts/integration-packs/civil-person-by-uin.json - sha256: sha256:ad85a4bbcbd9c164264afeb98c11c549cc0c54997b39572261282211ab9104b2 - private_bindings: - - hash: sha256:1fb0b8c2b28da70d6f22675209ced135f4855fcfdc2fd38a32aadaa0c70be0dd - path: artifacts/private-bindings/cra-child-benefit-civil.json - sha256: sha256:087d5ddfc5bbc43620f9bf499cda7343ff0b762427451f30fd3541bb7d230e22 - - hash: sha256:918b533b198e4e45e368db0b3d8737728700f4b6ee428e10a93a1a06460649b1 - path: artifacts/private-bindings/cra-citizen-record-civil.json - sha256: sha256:d564828b5a94041f3bb718c16b40a0a53ea68a94ed917bf96e5ed04f0ec8d086 - - hash: sha256:fbac0434f957c897098a1577a20ae8763258b1a1e2385a577129258dbbe86579 - path: artifacts/private-bindings/cra-pension-death-civil.json - sha256: sha256:f4eea1f1f2df7755c729d0a645778ca9805e15e3875962595c0038cc46142f51 - public_contracts: - - hash: sha256:35acae8dfe5b6cc1c18f9aedbc188c93ded49a46553b7ab21451eb772e26918e - path: artifacts/consultation-contracts/cra-child-benefit-civil.json - sha256: sha256:65bfb5000e186b4e461c392454e01c76149e821551152ec79a22f28b1e15f6b1 - - hash: sha256:c101d11d7fe66efe8400330851d65e4e105be62eae6ae9325eb43a83daae8aed - path: artifacts/consultation-contracts/cra-citizen-record-civil.json - sha256: sha256:ffb8a94778c5d1015c14ecc03fd06d7f2b2d5926aca0654cc80887d930bf74d2 - - hash: sha256:80486f20e013dd4a99af89a22efe3c83638f37f3f79ea7c912a8d5b237db2924 - path: artifacts/consultation-contracts/cra-pension-death-civil.json - sha256: sha256:2fbbfadc074822821239ff13ead71115d7d64c6378e39c4d03a870eb3b6655ea - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: cra-notary - principal_id: cra-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 5222333210205388 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 3104283851243516304 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized civil_people entity - entities: [] - id: civil_people - owner: solmara-cra-civil - sensitivity: personal - tables: - - access: - aggregate_scope: civil_people:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: birth_brn - nullable: true - sensitive: true - type: string - - name: birth_date - nullable: false - sensitive: true - type: date - - name: deceased - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv - type: file - title: civil_people - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: cra-civil-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/cra-civil/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/cra-civil/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/manifest.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/manifest.json deleted file mode 100644 index 875d75b..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/manifest.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-cra-civil-consultation", - "instance_id": "cra-civil-relay-consultation", - "bundle_id": "solmara-hosted-cra-civil-consultation-sequence-2", - "sequence": 2, - "config_hash": "sha256:f644dd93ffc7e04b60e75b6800a5750b472756a15eb2707875a8f68b48cf0224", - "files": [ - { - "path": "config/artifacts/consultation-contracts/cra-child-benefit-civil.json", - "sha256": "sha256:65bfb5000e186b4e461c392454e01c76149e821551152ec79a22f28b1e15f6b1" - }, - { - "path": "config/artifacts/consultation-contracts/cra-citizen-record-civil.json", - "sha256": "sha256:ffb8a94778c5d1015c14ecc03fd06d7f2b2d5926aca0654cc80887d930bf74d2" - }, - { - "path": "config/artifacts/consultation-contracts/cra-pension-death-civil.json", - "sha256": "sha256:2fbbfadc074822821239ff13ead71115d7d64c6378e39c4d03a870eb3b6655ea" - }, - { - "path": "config/artifacts/evidence/civil-person-by-uin/conformance.json", - "sha256": "sha256:1943d411187b226b981cddc2cd0442e73953d29cb5a047bbb38c729d78883afd" - }, - { - "path": "config/artifacts/evidence/civil-person-by-uin/minimization.json", - "sha256": "sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263" - }, - { - "path": "config/artifacts/evidence/civil-person-by-uin/negative-security.json", - "sha256": "sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f" - }, - { - "path": "config/artifacts/integration-packs/civil-person-by-uin.json", - "sha256": "sha256:ad85a4bbcbd9c164264afeb98c11c549cc0c54997b39572261282211ab9104b2" - }, - { - "path": "config/artifacts/private-bindings/cra-child-benefit-civil.json", - "sha256": "sha256:087d5ddfc5bbc43620f9bf499cda7343ff0b762427451f30fd3541bb7d230e22" - }, - { - "path": "config/artifacts/private-bindings/cra-citizen-record-civil.json", - "sha256": "sha256:d564828b5a94041f3bb718c16b40a0a53ea68a94ed917bf96e5ed04f0ec8d086" - }, - { - "path": "config/artifacts/private-bindings/cra-pension-death-civil.json", - "sha256": "sha256:f4eea1f1f2df7755c729d0a645778ca9805e15e3875962595c0038cc46142f51" - }, - { - "path": "config/relay.yaml", - "sha256": "sha256:f644dd93ffc7e04b60e75b6800a5750b472756a15eb2707875a8f68b48cf0224" - } - ], - "created_at": "2026-07-29T03:29:35.443551Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/consultation/bundle/manifest.sig.json b/config/hosted-relay-bundles/cra-civil/consultation/bundle/manifest.sig.json deleted file mode 100644 index 1982d12..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "j_966nNcyaqBMkE85fz05JksRXBxgO3DY6N76U3GQJzW4-DUOzLkaDnzLxAst46__fNdMKNboVr0QKuoMOrZAQ" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/consultation/trust-anchor.json b/config/hosted-relay-bundles/cra-civil/consultation/trust-anchor.json deleted file mode 100644 index 20c3a04..0000000 --- a/config/hosted-relay-bundles/cra-civil/consultation/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-cra-civil-consultation", - "instance_id": "cra-civil-relay-consultation", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/trust-anchor.json b/config/hosted-relay-bundles/cra-civil/trust-anchor.json deleted file mode 100644 index 4bb249a..0000000 --- a/config/hosted-relay-bundles/cra-civil/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-cra-civil", - "instance_id": "cra-civil-relay", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/antirollback-seed.json b/config/hosted-relay-bundles/mosd-programme/antirollback-seed.json deleted file mode 100644 index 818f505..0000000 --- a/config/hosted-relay-bundles/mosd-programme/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-mosd-programme" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/mosd-programme/bootstrap.yaml b/config/hosted-relay-bundles/mosd-programme/bootstrap.yaml deleted file mode 100644 index 04b530c..0000000 --- a/config/hosted-relay-bundles/mosd-programme/bootstrap.yaml +++ /dev/null @@ -1,80 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - programme-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://mosd-programme-relay.solmara.registrystack.org - publisher: solmara-mosd-programme - title: solmara-mosd-programme governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized mosd_enroll entity - entities: [] - id: mosd_enroll - owner: solmara-mosd-programme - sensitivity: personal - tables: - - access: - aggregate_scope: mosd_enroll:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: duplicate_flag - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv - type: file - title: mosd_enroll - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: programme-mis-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/mosd-programme/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/mosd-programme/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/config/relay.yaml b/config/hosted-relay-bundles/mosd-programme/bundle/config/relay.yaml deleted file mode 100644 index 04b530c..0000000 --- a/config/hosted-relay-bundles/mosd-programme/bundle/config/relay.yaml +++ /dev/null @@ -1,80 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - programme-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://mosd-programme-relay.solmara.registrystack.org - publisher: solmara-mosd-programme - title: solmara-mosd-programme governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized mosd_enroll entity - entities: [] - id: mosd_enroll - owner: solmara-mosd-programme - sensitivity: personal - tables: - - access: - aggregate_scope: mosd_enroll:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: duplicate_flag - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv - type: file - title: mosd_enroll - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: programme-mis-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/mosd-programme/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/mosd-programme/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/manifest.json b/config/hosted-relay-bundles/mosd-programme/bundle/manifest.json deleted file mode 100644 index 1b048de..0000000 --- a/config/hosted-relay-bundles/mosd-programme/bundle/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-mosd-programme", - "instance_id": "programme-mis-relay", - "bundle_id": "solmara-hosted-mosd-programme-sequence-2", - "sequence": 2, - "config_hash": "sha256:a925577919f4a3bdfaf0a629059bd678dd90c95a0534a80c38c935d0aa5c6b05", - "files": [ - { - "path": "config/relay.yaml", - "sha256": "sha256:a925577919f4a3bdfaf0a629059bd678dd90c95a0534a80c38c935d0aa5c6b05" - } - ], - "created_at": "2026-07-29T03:29:43.72312Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/manifest.sig.json b/config/hosted-relay-bundles/mosd-programme/bundle/manifest.sig.json deleted file mode 100644 index 3753d46..0000000 --- a/config/hosted-relay-bundles/mosd-programme/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "gEPcY8gbE7QY2T-OdRoGhNR66X8wDCILxnoQriv8o_PlknCJs5E49i--qkiPH8OYggySKTDfOsBhtG1DZo9NDQ" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/antirollback-seed.json b/config/hosted-relay-bundles/mosd-programme/consultation/antirollback-seed.json deleted file mode 100644 index fb5e40a..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-mosd-programme-consultation" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/bootstrap.yaml b/config/hosted-relay-bundles/mosd-programme/consultation/bootstrap.yaml deleted file mode 100644 index c85f15d..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/bootstrap.yaml +++ /dev/null @@ -1,122 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - programme-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://mosd-programme-relay.solmara.registrystack.org - publisher: solmara-mosd-programme - title: solmara-mosd-programme governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/enrollment-by-uin/minimization.json - sha256: sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83 - - class: negative_security - path: artifacts/evidence/enrollment-by-uin/negative-security.json - sha256: sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282 - - class: conformance - path: artifacts/evidence/enrollment-by-uin/conformance.json - sha256: sha256:f7df3b836234ef5893833546f66bb989f0b004ca1d32c006f72a13ebb3a4c2d8 - integration_packs: - - hash: sha256:cffdf1b631e81fee923267d17c17f3001685b1b6681ddb8b93040b0c2691a6e8 - path: artifacts/integration-packs/enrollment-by-uin.json - sha256: sha256:40ac75a61c3346b282e3abadd16654b634ebe0ac60c77a7b4c2ac98cf5f3af50 - private_bindings: - - hash: sha256:ae3674a401a41daa3325d73cd22363dba2d4c5145ae22cdc64587a6e0a293654 - path: artifacts/private-bindings/child-benefit-enrollment.json - sha256: sha256:fd8fc27359128d39dbed4cd6842d7db21f3b5eef21359f0b6bec93c0f6822f18 - public_contracts: - - hash: sha256:9da47bc7171991c51e098b44cb0667595b344d5f4c2b994a67f0c3b401d4b5b3 - path: artifacts/consultation-contracts/child-benefit-enrollment.json - sha256: sha256:253ee934a9c9bbf725e233f90c195d057893eeea14bc9bc12330c0957245ff10 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: programme-notary - principal_id: programme-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 9069287079765840165 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 891105660075992367 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized mosd_enroll entity - entities: [] - id: mosd_enroll - owner: solmara-mosd-programme - sensitivity: personal - tables: - - access: - aggregate_scope: mosd_enroll:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: duplicate_flag - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv - type: file - title: mosd_enroll - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: programme-mis-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/mosd-programme/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/mosd-programme/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/consultation-contracts/child-benefit-enrollment.json b/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/consultation-contracts/child-benefit-enrollment.json deleted file mode 100644 index bc41921..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/consultation-contracts/child-benefit-enrollment.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-mosd-programme.child-benefit.enrollment","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:50a4f9fd40526b837aa90b02d27e13e98f66d9de1362eb06fb2465afbae31c53","id":"relay.child-benefit.enrollment","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:child-benefit","workload":"programme-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-mosd-programme.enrollment-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["duplicate_flag"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/conformance.json b/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/conformance.json deleted file mode 100644 index 6910195..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"not-already-enrolled":true},"error":null,"outcome":"match","outputs":{"duplicate_flag":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"duplicate_flag":false},"headers":{},"status":200}}}],"name":"enrollment-match","request":{"claims":[{"id":"not-already-enrolled"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"not-already-enrolled":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"enrollment-no-match","request":null,"variables":{}}],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/minimization.json b/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/minimization.json deleted file mode 100644 index 1d539e7..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"enrollment-by-uin","operations":{},"outputs":{"duplicate_flag":{"from":"snapshot.record.duplicate_flag","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/negative-security.json b/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/negative-security.json deleted file mode 100644 index 0b38f83..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/evidence/enrollment-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/integration-packs/enrollment-by-uin.json b/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/integration-packs/enrollment-by-uin.json deleted file mode 100644 index 729f6a0..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/integration-packs/enrollment-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-mosd-programme.enrollment-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:f7df3b836234ef5893833546f66bb989f0b004ca1d32c006f72a13ebb3a4c2d8"],"minimization":["sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83"],"negative_security":["sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"enrollment-by-uin","output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/private-bindings/child-benefit-enrollment.json b/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/private-bindings/child-benefit-enrollment.json deleted file mode 100644 index 078b94e..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/artifacts/private-bindings/child-benefit-enrollment.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:cffdf1b631e81fee923267d17c17f3001685b1b6681ddb8b93040b0c2691a6e8","id":"solmara-mosd-programme.enrollment-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"duplicate_flag":"duplicate_flag"}},"table_provider":"mosd_enroll__materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf"},"profile":{"id":"solmara-mosd-programme.child-benefit.enrollment","version":"1"},"registry_instance":"solmara-mosd-programme","source_instance":"materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf","tenant":"solmara-mosd-programme"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/relay.yaml b/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/relay.yaml deleted file mode 100644 index c85f15d..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/config/relay.yaml +++ /dev/null @@ -1,122 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - programme-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://mosd-programme-relay.solmara.registrystack.org - publisher: solmara-mosd-programme - title: solmara-mosd-programme governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/enrollment-by-uin/minimization.json - sha256: sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83 - - class: negative_security - path: artifacts/evidence/enrollment-by-uin/negative-security.json - sha256: sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282 - - class: conformance - path: artifacts/evidence/enrollment-by-uin/conformance.json - sha256: sha256:f7df3b836234ef5893833546f66bb989f0b004ca1d32c006f72a13ebb3a4c2d8 - integration_packs: - - hash: sha256:cffdf1b631e81fee923267d17c17f3001685b1b6681ddb8b93040b0c2691a6e8 - path: artifacts/integration-packs/enrollment-by-uin.json - sha256: sha256:40ac75a61c3346b282e3abadd16654b634ebe0ac60c77a7b4c2ac98cf5f3af50 - private_bindings: - - hash: sha256:ae3674a401a41daa3325d73cd22363dba2d4c5145ae22cdc64587a6e0a293654 - path: artifacts/private-bindings/child-benefit-enrollment.json - sha256: sha256:fd8fc27359128d39dbed4cd6842d7db21f3b5eef21359f0b6bec93c0f6822f18 - public_contracts: - - hash: sha256:9da47bc7171991c51e098b44cb0667595b344d5f4c2b994a67f0c3b401d4b5b3 - path: artifacts/consultation-contracts/child-benefit-enrollment.json - sha256: sha256:253ee934a9c9bbf725e233f90c195d057893eeea14bc9bc12330c0957245ff10 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: programme-notary - principal_id: programme-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 9069287079765840165 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 891105660075992367 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized mosd_enroll entity - entities: [] - id: mosd_enroll - owner: solmara-mosd-programme - sensitivity: personal - tables: - - access: - aggregate_scope: mosd_enroll:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: duplicate_flag - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv - type: file - title: mosd_enroll - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: programme-mis-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/mosd-programme/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/mosd-programme/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/manifest.json b/config/hosted-relay-bundles/mosd-programme/consultation/bundle/manifest.json deleted file mode 100644 index afcf21f..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/manifest.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-mosd-programme-consultation", - "instance_id": "programme-mis-relay-consultation", - "bundle_id": "solmara-hosted-mosd-programme-consultation-sequence-2", - "sequence": 2, - "config_hash": "sha256:393a7050b3c3f1481413edb752e04ee61506080409874afba78e76f39a1384b6", - "files": [ - { - "path": "config/artifacts/consultation-contracts/child-benefit-enrollment.json", - "sha256": "sha256:253ee934a9c9bbf725e233f90c195d057893eeea14bc9bc12330c0957245ff10" - }, - { - "path": "config/artifacts/evidence/enrollment-by-uin/conformance.json", - "sha256": "sha256:f7df3b836234ef5893833546f66bb989f0b004ca1d32c006f72a13ebb3a4c2d8" - }, - { - "path": "config/artifacts/evidence/enrollment-by-uin/minimization.json", - "sha256": "sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83" - }, - { - "path": "config/artifacts/evidence/enrollment-by-uin/negative-security.json", - "sha256": "sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282" - }, - { - "path": "config/artifacts/integration-packs/enrollment-by-uin.json", - "sha256": "sha256:40ac75a61c3346b282e3abadd16654b634ebe0ac60c77a7b4c2ac98cf5f3af50" - }, - { - "path": "config/artifacts/private-bindings/child-benefit-enrollment.json", - "sha256": "sha256:fd8fc27359128d39dbed4cd6842d7db21f3b5eef21359f0b6bec93c0f6822f18" - }, - { - "path": "config/relay.yaml", - "sha256": "sha256:393a7050b3c3f1481413edb752e04ee61506080409874afba78e76f39a1384b6" - } - ], - "created_at": "2026-07-29T03:29:45.373059Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/manifest.sig.json b/config/hosted-relay-bundles/mosd-programme/consultation/bundle/manifest.sig.json deleted file mode 100644 index 3677cc9..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "Tl3EqK7QL_JwEGJPXl18vIYuOtwHCH40KTiW7VhegU8nk4L10y9EyQIghFQJaPu5WztZDShXG5Ma-jmtkGyGAA" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/consultation/trust-anchor.json b/config/hosted-relay-bundles/mosd-programme/consultation/trust-anchor.json deleted file mode 100644 index 4dd7475..0000000 --- a/config/hosted-relay-bundles/mosd-programme/consultation/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-mosd-programme-consultation", - "instance_id": "programme-mis-relay-consultation", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/trust-anchor.json b/config/hosted-relay-bundles/mosd-programme/trust-anchor.json deleted file mode 100644 index b37d865..0000000 --- a/config/hosted-relay-bundles/mosd-programme/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-mosd-programme", - "instance_id": "programme-mis-relay", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/antirollback-seed.json b/config/hosted-relay-bundles/nagdi-agriculture/antirollback-seed.json deleted file mode 100644 index 69a1383..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nagdi-agriculture" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml b/config/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml deleted file mode 100644 index 9e3798a..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml +++ /dev/null @@ -1,178 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - nagdi-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nagdi-relay.solmara.registrystack.org - publisher: solmara-nagdi-agriculture - title: solmara-nagdi-agriculture governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_farmer entity - entities: [] - id: nagdi_farmer - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_farmer:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - materialization: snapshot - primary_key: farmer_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: active_farm_parcel - nullable: false - sensitive: true - type: boolean - - name: active_smallholder_farmer - nullable: false - sensitive: true - type: boolean - - name: crop_declared_for_season - nullable: false - sensitive: true - type: boolean - - name: data_use_authorized - nullable: false - sensitive: true - type: boolean - - name: district_climate_risk_active - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: farmer_registered - nullable: false - sensitive: true - type: boolean - - name: voucher_entitlement_current - nullable: false - sensitive: true - type: boolean - - name: voucher_not_redeemed - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - type: file - title: nagdi_farmer - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_herd entity - entities: [] - id: nagdi_herd - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_herd:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - materialization: snapshot - primary_key: herd_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: destination_district_open - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: herd_id - nullable: false - sensitive: true - type: string - - name: herd_vaccination_current - nullable: false - sensitive: true - type: boolean - - name: no_conflicting_open_movement_permit - nullable: false - sensitive: true - type: boolean - - name: origin_district_not_quarantined_for_species - nullable: false - sensitive: true - type: boolean - - name: registered_herd - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - type: file - title: nagdi_herd - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nagdi-agriculture-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/relay.yaml b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/relay.yaml deleted file mode 100644 index 9e3798a..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/relay.yaml +++ /dev/null @@ -1,178 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - nagdi-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nagdi-relay.solmara.registrystack.org - publisher: solmara-nagdi-agriculture - title: solmara-nagdi-agriculture governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_farmer entity - entities: [] - id: nagdi_farmer - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_farmer:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - materialization: snapshot - primary_key: farmer_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: active_farm_parcel - nullable: false - sensitive: true - type: boolean - - name: active_smallholder_farmer - nullable: false - sensitive: true - type: boolean - - name: crop_declared_for_season - nullable: false - sensitive: true - type: boolean - - name: data_use_authorized - nullable: false - sensitive: true - type: boolean - - name: district_climate_risk_active - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: farmer_registered - nullable: false - sensitive: true - type: boolean - - name: voucher_entitlement_current - nullable: false - sensitive: true - type: boolean - - name: voucher_not_redeemed - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - type: file - title: nagdi_farmer - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_herd entity - entities: [] - id: nagdi_herd - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_herd:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - materialization: snapshot - primary_key: herd_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: destination_district_open - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: herd_id - nullable: false - sensitive: true - type: string - - name: herd_vaccination_current - nullable: false - sensitive: true - type: boolean - - name: no_conflicting_open_movement_permit - nullable: false - sensitive: true - type: boolean - - name: origin_district_not_quarantined_for_species - nullable: false - sensitive: true - type: boolean - - name: registered_herd - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - type: file - title: nagdi_herd - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nagdi-agriculture-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.json deleted file mode 100644 index 69b5860..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nagdi-agriculture", - "instance_id": "nagdi-agriculture-relay", - "bundle_id": "solmara-hosted-nagdi-agriculture-sequence-2", - "sequence": 2, - "config_hash": "sha256:27bae9688c1930d04fcf8c061671ff969c990e2df8a36ee6b023ea05b6df6672", - "files": [ - { - "path": "config/relay.yaml", - "sha256": "sha256:27bae9688c1930d04fcf8c061671ff969c990e2df8a36ee6b023ea05b6df6672" - } - ], - "created_at": "2026-07-29T03:29:50.278735Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.sig.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.sig.json deleted file mode 100644 index 41562a7..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "UEqJogDN2u3wsIUgKM897LFauTP5O0_PX5FGcDNq0aht_eFR5LHcPfGc3_yk8aQWEHTvXpjrbGBAJhCPG_rPBw" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/antirollback-seed.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/antirollback-seed.json deleted file mode 100644 index 2218e9a..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nagdi-agriculture-consultation" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bootstrap.yaml b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bootstrap.yaml deleted file mode 100644 index 8795fce..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bootstrap.yaml +++ /dev/null @@ -1,238 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - nagdi-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nagdi-relay.solmara.registrystack.org - publisher: solmara-nagdi-agriculture - title: solmara-nagdi-agriculture governed Registry Relay -consultation: - artifacts: - evidence: - - class: conformance - path: artifacts/evidence/livestock-movement-by-farmer/conformance.json - sha256: sha256:10bdc960cb62d9030cee7895a45ce77a6df1ebe133f7beac2d6feb05aaf18910 - - class: minimization - path: artifacts/evidence/livestock-movement-by-farmer/minimization.json - sha256: sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5 - - class: conformance - path: artifacts/evidence/farmer-voucher-by-farmer/conformance.json - sha256: sha256:6d1e0b039f6ed71b79f382260701582ef255c1b377e7d01ee942b1b9c2c53e52 - - class: negative_security - path: artifacts/evidence/farmer-voucher-by-farmer/negative-security.json - sha256: sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d - - class: negative_security - path: artifacts/evidence/livestock-movement-by-farmer/negative-security.json - sha256: sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce - - class: minimization - path: artifacts/evidence/farmer-voucher-by-farmer/minimization.json - sha256: sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b - integration_packs: - - hash: sha256:eef622a1e97ac245493f2348cecad9b3b9245b7b0ce30d9db7ed602cc80827cd - path: artifacts/integration-packs/farmer-voucher-by-farmer.json - sha256: sha256:ed6f77b36df0d70ad9ffea72fdfedc21cc0a3621db3cdf4ea70d18f8960db12c - - hash: sha256:9d1daa45060e9f5c073be4a42f175c398907d7f00c650554aeddeedb9bd94092 - path: artifacts/integration-packs/livestock-movement-by-farmer.json - sha256: sha256:7af1198e4ba477344621f4361613c5ba1d0d30bf1a2bc94e94b7a7c5d8e8ca16 - private_bindings: - - hash: sha256:f319d52859657a0a1acaabba01034558012eeeb11e36e406d9def07a72814416 - path: artifacts/private-bindings/livestock-herd.json - sha256: sha256:33efab2ec578e7baf3dc70c7a6c139eb76a5cd80f4a2c753fc474b795477d306 - - hash: sha256:04ea387eadcc2279e7c791241c46b77f41dcbb02bd15919a3e246370f3443714 - path: artifacts/private-bindings/voucher-farmer.json - sha256: sha256:75a3c0379a33f5c7029cf91ebb87a42c661a5e8ddd93c5fb7edf37f0f5f51d9b - public_contracts: - - hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 - path: artifacts/consultation-contracts/livestock-herd.json - sha256: sha256:a1f4045c36462881aec560fcf103c1f78252bf0e2acf81ddd6b75ce21ccfd84c - - hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c - path: artifacts/consultation-contracts/voucher-farmer.json - sha256: sha256:8ea73ba2779ef33a85cc5f34988fe0d14f13f1892baa185d0d5c67e76dcbe1ad - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: nagdi-notary - principal_id: nagdi-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4554467022379962639 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 7450858971948805253 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_farmer entity - entities: [] - id: nagdi_farmer - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_farmer:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - materialization: snapshot - primary_key: farmer_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: active_farm_parcel - nullable: false - sensitive: true - type: boolean - - name: active_smallholder_farmer - nullable: false - sensitive: true - type: boolean - - name: crop_declared_for_season - nullable: false - sensitive: true - type: boolean - - name: data_use_authorized - nullable: false - sensitive: true - type: boolean - - name: district_climate_risk_active - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: farmer_registered - nullable: false - sensitive: true - type: boolean - - name: voucher_entitlement_current - nullable: false - sensitive: true - type: boolean - - name: voucher_not_redeemed - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - type: file - title: nagdi_farmer - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_herd entity - entities: [] - id: nagdi_herd - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_herd:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - materialization: snapshot - primary_key: herd_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: destination_district_open - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: herd_id - nullable: false - sensitive: true - type: string - - name: herd_vaccination_current - nullable: false - sensitive: true - type: boolean - - name: no_conflicting_open_movement_permit - nullable: false - sensitive: true - type: boolean - - name: origin_district_not_quarantined_for_species - nullable: false - sensitive: true - type: boolean - - name: registered_herd - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - type: file - title: nagdi_herd - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nagdi-agriculture-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/consultation-contracts/livestock-herd.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/consultation-contracts/livestock-herd.json deleted file mode 100644 index adf39dd..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/consultation-contracts/livestock-herd.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.livestock.herd","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:05746b9128c73f3a079b88d4a6a5c392ea4d317755aa8e0771e0c83325a16b28","id":"relay.livestock.herd","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/livestock-movement-control"],"required_scope":"registry:consult:livestock","workload":"nagdi-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["destination_district_open","herd_vaccination_current","no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species","registered_herd"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/consultation-contracts/voucher-farmer.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/consultation-contracts/voucher-farmer.json deleted file mode 100644 index 3f414ad..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/consultation-contracts/voucher-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.voucher.farmer","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:6c9b13e328828747f1d32991be3ab0681112c283dae9f614a7187c8ca0d72a01","id":"relay.voucher.farmer","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"],"required_scope":"registry:consult:voucher","workload":"nagdi-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["active_farm_parcel","active_smallholder_farmer","crop_declared_for_season","data_use_authorized","district_climate_risk_active","farmer_registered","voucher_entitlement_current","voucher_not_redeemed"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/conformance.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/conformance.json deleted file mode 100644 index 4743085..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"data-use-authorized-for-purpose":true,"eligible-for-climate-smart-input-voucher":true,"farmer-registered":true},"error":null,"outcome":"match","outputs":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true},"headers":{},"status":200}}}],"name":"farmer-voucher-match","request":{"claims":[{"id":"farmer-registered"},{"id":"data-use-authorized-for-purpose"},{"id":"eligible-for-climate-smart-input-voucher"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/voucher-eligibility-review","target":{"identifiers":[{"scheme":"farmer_id","value":"FR-1001"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"data-use-authorized-for-purpose":false,"eligible-for-climate-smart-input-voucher":false,"farmer-registered":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"farmer-voucher-no-match","request":null,"variables":{}}],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/minimization.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/minimization.json deleted file mode 100644 index 59e808a..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"farmer-voucher-by-farmer","operations":{},"outputs":{"active_farm_parcel":{"from":"snapshot.record.active_farm_parcel","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"from":"snapshot.record.active_smallholder_farmer","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"crop_declared_for_season":{"from":"snapshot.record.crop_declared_for_season","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"data_use_authorized":{"from":"snapshot.record.data_use_authorized","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"district_climate_risk_active":{"from":"snapshot.record.district_climate_risk_active","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"farmer_registered":{"from":"snapshot.record.farmer_registered","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"from":"snapshot.record.voucher_entitlement_current","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"from":"snapshot.record.voucher_not_redeemed","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json deleted file mode 100644 index 7dfbf8c..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/conformance.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/conformance.json deleted file mode 100644 index 25a9c51..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"farmer_id":"FR-8888"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},{"destination_district_open":false,"herd_vaccination_current":false,"no_conflicting_open_movement_permit":false,"origin_district_not_quarantined_for_species":false,"registered_herd":false}],"headers":{},"status":200}}}],"name":"livestock-movement-ambiguous","request":null,"variables":{}},{"classification":"synthetic","expect":{"claims":{"eligible-for-livestock-movement-permit":true,"origin-district-not-quarantined-for-species":true,"registered-herd":true},"error":null,"outcome":"match","outputs":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},"headers":{},"status":200}}}],"name":"livestock-movement-match","request":{"claims":[{"id":"registered-herd"},{"id":"origin-district-not-quarantined-for-species"},{"id":"eligible-for-livestock-movement-permit"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/livestock-movement-control","target":{"identifiers":[{"scheme":"farmer_id","value":"FR-1001"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"eligible-for-livestock-movement-permit":false,"origin-district-not-quarantined-for-species":false,"registered-herd":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"livestock-movement-no-match","request":null,"variables":{}}],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/minimization.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/minimization.json deleted file mode 100644 index 6768a09..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"livestock-movement-by-farmer","operations":{},"outputs":{"destination_district_open":{"from":"snapshot.record.destination_district_open","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"herd_vaccination_current":{"from":"snapshot.record.herd_vaccination_current","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"from":"snapshot.record.no_conflicting_open_movement_permit","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"from":"snapshot.record.origin_district_not_quarantined_for_species","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"registered_herd":{"from":"snapshot.record.registered_herd","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/negative-security.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/negative-security.json deleted file mode 100644 index a823e60..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/evidence/livestock-movement-by-farmer/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/integration-packs/farmer-voucher-by-farmer.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/integration-packs/farmer-voucher-by-farmer.json deleted file mode 100644 index ff96512..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/integration-packs/farmer-voucher-by-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:6d1e0b039f6ed71b79f382260701582ef255c1b377e7d01ee942b1b9c2c53e52"],"minimization":["sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b"],"negative_security":["sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"farmer-voucher-by-farmer","output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/integration-packs/livestock-movement-by-farmer.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/integration-packs/livestock-movement-by-farmer.json deleted file mode 100644 index 043c50a..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/integration-packs/livestock-movement-by-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:10bdc960cb62d9030cee7895a45ce77a6df1ebe133f7beac2d6feb05aaf18910"],"minimization":["sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5"],"negative_security":["sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"livestock-movement-by-farmer","output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/private-bindings/livestock-herd.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/private-bindings/livestock-herd.json deleted file mode 100644 index 7db8adf..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/private-bindings/livestock-herd.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:9d1daa45060e9f5c073be4a42f175c398907d7f00c650554aeddeedb9bd94092","id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"destination_district_open":"destination_district_open","herd_vaccination_current":"herd_vaccination_current","no_conflicting_open_movement_permit":"no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species":"origin_district_not_quarantined_for_species","registered_herd":"registered_herd"}},"table_provider":"nagdi_herd__materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b"},"profile":{"id":"solmara-nagdi-agriculture.livestock.herd","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/private-bindings/voucher-farmer.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/private-bindings/voucher-farmer.json deleted file mode 100644 index eddcecb..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/artifacts/private-bindings/voucher-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:eef622a1e97ac245493f2348cecad9b3b9245b7b0ce30d9db7ed602cc80827cd","id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"active_farm_parcel":"active_farm_parcel","active_smallholder_farmer":"active_smallholder_farmer","crop_declared_for_season":"crop_declared_for_season","data_use_authorized":"data_use_authorized","district_climate_risk_active":"district_climate_risk_active","farmer_registered":"farmer_registered","voucher_entitlement_current":"voucher_entitlement_current","voucher_not_redeemed":"voucher_not_redeemed"}},"table_provider":"nagdi_farmer__materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7"},"profile":{"id":"solmara-nagdi-agriculture.voucher.farmer","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/relay.yaml b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/relay.yaml deleted file mode 100644 index 8795fce..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/config/relay.yaml +++ /dev/null @@ -1,238 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - nagdi-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nagdi-relay.solmara.registrystack.org - publisher: solmara-nagdi-agriculture - title: solmara-nagdi-agriculture governed Registry Relay -consultation: - artifacts: - evidence: - - class: conformance - path: artifacts/evidence/livestock-movement-by-farmer/conformance.json - sha256: sha256:10bdc960cb62d9030cee7895a45ce77a6df1ebe133f7beac2d6feb05aaf18910 - - class: minimization - path: artifacts/evidence/livestock-movement-by-farmer/minimization.json - sha256: sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5 - - class: conformance - path: artifacts/evidence/farmer-voucher-by-farmer/conformance.json - sha256: sha256:6d1e0b039f6ed71b79f382260701582ef255c1b377e7d01ee942b1b9c2c53e52 - - class: negative_security - path: artifacts/evidence/farmer-voucher-by-farmer/negative-security.json - sha256: sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d - - class: negative_security - path: artifacts/evidence/livestock-movement-by-farmer/negative-security.json - sha256: sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce - - class: minimization - path: artifacts/evidence/farmer-voucher-by-farmer/minimization.json - sha256: sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b - integration_packs: - - hash: sha256:eef622a1e97ac245493f2348cecad9b3b9245b7b0ce30d9db7ed602cc80827cd - path: artifacts/integration-packs/farmer-voucher-by-farmer.json - sha256: sha256:ed6f77b36df0d70ad9ffea72fdfedc21cc0a3621db3cdf4ea70d18f8960db12c - - hash: sha256:9d1daa45060e9f5c073be4a42f175c398907d7f00c650554aeddeedb9bd94092 - path: artifacts/integration-packs/livestock-movement-by-farmer.json - sha256: sha256:7af1198e4ba477344621f4361613c5ba1d0d30bf1a2bc94e94b7a7c5d8e8ca16 - private_bindings: - - hash: sha256:f319d52859657a0a1acaabba01034558012eeeb11e36e406d9def07a72814416 - path: artifacts/private-bindings/livestock-herd.json - sha256: sha256:33efab2ec578e7baf3dc70c7a6c139eb76a5cd80f4a2c753fc474b795477d306 - - hash: sha256:04ea387eadcc2279e7c791241c46b77f41dcbb02bd15919a3e246370f3443714 - path: artifacts/private-bindings/voucher-farmer.json - sha256: sha256:75a3c0379a33f5c7029cf91ebb87a42c661a5e8ddd93c5fb7edf37f0f5f51d9b - public_contracts: - - hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 - path: artifacts/consultation-contracts/livestock-herd.json - sha256: sha256:a1f4045c36462881aec560fcf103c1f78252bf0e2acf81ddd6b75ce21ccfd84c - - hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c - path: artifacts/consultation-contracts/voucher-farmer.json - sha256: sha256:8ea73ba2779ef33a85cc5f34988fe0d14f13f1892baa185d0d5c67e76dcbe1ad - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: nagdi-notary - principal_id: nagdi-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4554467022379962639 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 7450858971948805253 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_farmer entity - entities: [] - id: nagdi_farmer - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_farmer:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - materialization: snapshot - primary_key: farmer_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: active_farm_parcel - nullable: false - sensitive: true - type: boolean - - name: active_smallholder_farmer - nullable: false - sensitive: true - type: boolean - - name: crop_declared_for_season - nullable: false - sensitive: true - type: boolean - - name: data_use_authorized - nullable: false - sensitive: true - type: boolean - - name: district_climate_risk_active - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: farmer_registered - nullable: false - sensitive: true - type: boolean - - name: voucher_entitlement_current - nullable: false - sensitive: true - type: boolean - - name: voucher_not_redeemed - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - type: file - title: nagdi_farmer - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized nagdi_herd entity - entities: [] - id: nagdi_herd - owner: solmara-nagdi-agriculture - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_herd:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - materialization: snapshot - primary_key: herd_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: destination_district_open - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: herd_id - nullable: false - sensitive: true - type: string - - name: herd_vaccination_current - nullable: false - sensitive: true - type: boolean - - name: no_conflicting_open_movement_permit - nullable: false - sensitive: true - type: boolean - - name: origin_district_not_quarantined_for_species - nullable: false - sensitive: true - type: boolean - - name: registered_herd - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - type: file - title: nagdi_herd - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nagdi-agriculture-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/manifest.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/manifest.json deleted file mode 100644 index b12e436..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/manifest.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nagdi-agriculture-consultation", - "instance_id": "nagdi-agriculture-relay-consultation", - "bundle_id": "solmara-hosted-nagdi-agriculture-consultation-sequence-2", - "sequence": 2, - "config_hash": "sha256:a50a3d90d19e12ec9f8304c207f3a81829d28c4f7765b13f1a68a883c4dd5207", - "files": [ - { - "path": "config/artifacts/consultation-contracts/livestock-herd.json", - "sha256": "sha256:a1f4045c36462881aec560fcf103c1f78252bf0e2acf81ddd6b75ce21ccfd84c" - }, - { - "path": "config/artifacts/consultation-contracts/voucher-farmer.json", - "sha256": "sha256:8ea73ba2779ef33a85cc5f34988fe0d14f13f1892baa185d0d5c67e76dcbe1ad" - }, - { - "path": "config/artifacts/evidence/farmer-voucher-by-farmer/conformance.json", - "sha256": "sha256:6d1e0b039f6ed71b79f382260701582ef255c1b377e7d01ee942b1b9c2c53e52" - }, - { - "path": "config/artifacts/evidence/farmer-voucher-by-farmer/minimization.json", - "sha256": "sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b" - }, - { - "path": "config/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json", - "sha256": "sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d" - }, - { - "path": "config/artifacts/evidence/livestock-movement-by-farmer/conformance.json", - "sha256": "sha256:10bdc960cb62d9030cee7895a45ce77a6df1ebe133f7beac2d6feb05aaf18910" - }, - { - "path": "config/artifacts/evidence/livestock-movement-by-farmer/minimization.json", - "sha256": "sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5" - }, - { - "path": "config/artifacts/evidence/livestock-movement-by-farmer/negative-security.json", - "sha256": "sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce" - }, - { - "path": "config/artifacts/integration-packs/farmer-voucher-by-farmer.json", - "sha256": "sha256:ed6f77b36df0d70ad9ffea72fdfedc21cc0a3621db3cdf4ea70d18f8960db12c" - }, - { - "path": "config/artifacts/integration-packs/livestock-movement-by-farmer.json", - "sha256": "sha256:7af1198e4ba477344621f4361613c5ba1d0d30bf1a2bc94e94b7a7c5d8e8ca16" - }, - { - "path": "config/artifacts/private-bindings/livestock-herd.json", - "sha256": "sha256:33efab2ec578e7baf3dc70c7a6c139eb76a5cd80f4a2c753fc474b795477d306" - }, - { - "path": "config/artifacts/private-bindings/voucher-farmer.json", - "sha256": "sha256:75a3c0379a33f5c7029cf91ebb87a42c661a5e8ddd93c5fb7edf37f0f5f51d9b" - }, - { - "path": "config/relay.yaml", - "sha256": "sha256:a50a3d90d19e12ec9f8304c207f3a81829d28c4f7765b13f1a68a883c4dd5207" - } - ], - "created_at": "2026-07-29T03:29:51.897077Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/manifest.sig.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/manifest.sig.json deleted file mode 100644 index 01e09d1..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "n_O7Jr0g9EVajnDXNlFqKalsWHpB889Nkk4N4eWgQ6S13I1gl2rZUBNSEm3qXFn2Iz65zob9vUmaH0_L93e6Ag" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/consultation/trust-anchor.json b/config/hosted-relay-bundles/nagdi-agriculture/consultation/trust-anchor.json deleted file mode 100644 index 0090379..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/consultation/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nagdi-agriculture-consultation", - "instance_id": "nagdi-agriculture-relay-consultation", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json b/config/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json deleted file mode 100644 index f2541b5..0000000 --- a/config/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nagdi-agriculture", - "instance_id": "nagdi-agriculture-relay", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/antirollback-seed.json b/config/hosted-relay-bundles/nia-population/antirollback-seed.json deleted file mode 100644 index eced2eb..0000000 --- a/config/hosted-relay-bundles/nia-population/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nia-population" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/nia-population/bootstrap.yaml b/config/hosted-relay-bundles/nia-population/bootstrap.yaml deleted file mode 100644 index 8dd29ae..0000000 --- a/config/hosted-relay-bundles/nia-population/bootstrap.yaml +++ /dev/null @@ -1,207 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-esignet - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nia-relay.solmara.registrystack.org - publisher: solmara-nia-population - title: solmara-nia-population governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Governed population attributes used by NIA-owned services. - entities: - - access: - aggregate_scope: population:aggregate - evidence_verification_scope: population:evidence_verification - metadata_scope: population:metadata - read_scope: population:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: legacy_nid - ops: - - eq - - field: uin - ops: - - eq - default_limit: 25 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/esignet-identity-verification - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 100 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: - - claims: - - expression: null - name: birthdate - required: true - sensitivity: personal - source_field: birth_date - - expression: null - name: family_name - required: true - sensitivity: direct_identifier - source_field: family_name - - expression: null - name: gender - required: false - sensitivity: personal - source_field: sex - - expression: null - name: given_name - required: true - sensitivity: direct_identifier - source_field: given_name - - expression: null - name: individual_id - required: true - sensitivity: direct_identifier - source_field: uin - - expression: - cel: source.given_name + ' ' + source.family_name - name: name - required: true - sensitivity: direct_identifier - source_field: null - description: Minimal identity attributes for the Solmara eSignet authenticator. - id: solmara-nia-userinfo - purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - release_conditions: - expression: - cel: source.identity_status == 'active' && source.alive == true - release_scope: population:identity_release - subject: - id_type: national_id - source_field: uin - title: SolmaraID UserInfo - version: v1 - description: Governed population attributes used by NIA-owned services. - fields: - - from: alive - name: alive - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: family_name - name: family_name - sensitive: true - - from: given_name - name: given_name - sensitive: true - - from: identity_status - name: identity_status - sensitive: true - - from: legacy_nid - name: legacy_nid - sensitive: true - - from: sex - name: sex - sensitive: true - - from: uin - name: uin - sensitive: true - name: population - relationships: [] - spatial: null - table: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - title: NIA population records - id: population - owner: National Identity Agency - sensitivity: personal - tables: - - access: - aggregate_scope: population:aggregate - metadata_scope: population:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 25 - max_limit: 100 - require_purpose_header: true - id: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: alive - nullable: false - sensitive: true - type: boolean - - name: birth_date - nullable: false - sensitive: true - type: date - - name: family_name - nullable: false - sensitive: true - type: string - - name: given_name - nullable: false - sensitive: true - type: string - - name: identity_status - nullable: false - sensitive: true - type: string - - name: legacy_nid - nullable: true - sensitive: true - type: string - - name: sex - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_NIA_DATABASE_URL - table: - name: population_person - schema: public - type: postgres - title: NIA population records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nia-population-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/nia-population/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/nia-population/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nia-population/bundle/config/relay.yaml b/config/hosted-relay-bundles/nia-population/bundle/config/relay.yaml deleted file mode 100644 index 8dd29ae..0000000 --- a/config/hosted-relay-bundles/nia-population/bundle/config/relay.yaml +++ /dev/null @@ -1,207 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-esignet - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nia-relay.solmara.registrystack.org - publisher: solmara-nia-population - title: solmara-nia-population governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Governed population attributes used by NIA-owned services. - entities: - - access: - aggregate_scope: population:aggregate - evidence_verification_scope: population:evidence_verification - metadata_scope: population:metadata - read_scope: population:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: legacy_nid - ops: - - eq - - field: uin - ops: - - eq - default_limit: 25 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/esignet-identity-verification - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 100 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: - - claims: - - expression: null - name: birthdate - required: true - sensitivity: personal - source_field: birth_date - - expression: null - name: family_name - required: true - sensitivity: direct_identifier - source_field: family_name - - expression: null - name: gender - required: false - sensitivity: personal - source_field: sex - - expression: null - name: given_name - required: true - sensitivity: direct_identifier - source_field: given_name - - expression: null - name: individual_id - required: true - sensitivity: direct_identifier - source_field: uin - - expression: - cel: source.given_name + ' ' + source.family_name - name: name - required: true - sensitivity: direct_identifier - source_field: null - description: Minimal identity attributes for the Solmara eSignet authenticator. - id: solmara-nia-userinfo - purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - release_conditions: - expression: - cel: source.identity_status == 'active' && source.alive == true - release_scope: population:identity_release - subject: - id_type: national_id - source_field: uin - title: SolmaraID UserInfo - version: v1 - description: Governed population attributes used by NIA-owned services. - fields: - - from: alive - name: alive - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: family_name - name: family_name - sensitive: true - - from: given_name - name: given_name - sensitive: true - - from: identity_status - name: identity_status - sensitive: true - - from: legacy_nid - name: legacy_nid - sensitive: true - - from: sex - name: sex - sensitive: true - - from: uin - name: uin - sensitive: true - name: population - relationships: [] - spatial: null - table: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - title: NIA population records - id: population - owner: National Identity Agency - sensitivity: personal - tables: - - access: - aggregate_scope: population:aggregate - metadata_scope: population:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 25 - max_limit: 100 - require_purpose_header: true - id: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: alive - nullable: false - sensitive: true - type: boolean - - name: birth_date - nullable: false - sensitive: true - type: date - - name: family_name - nullable: false - sensitive: true - type: string - - name: given_name - nullable: false - sensitive: true - type: string - - name: identity_status - nullable: false - sensitive: true - type: string - - name: legacy_nid - nullable: true - sensitive: true - type: string - - name: sex - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_NIA_DATABASE_URL - table: - name: population_person - schema: public - type: postgres - title: NIA population records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nia-population-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/nia-population/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/nia-population/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nia-population/bundle/manifest.json b/config/hosted-relay-bundles/nia-population/bundle/manifest.json deleted file mode 100644 index 96f3235..0000000 --- a/config/hosted-relay-bundles/nia-population/bundle/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nia-population", - "instance_id": "nia-population-relay", - "bundle_id": "solmara-hosted-nia-population-sequence-2", - "sequence": 2, - "config_hash": "sha256:6588003f187ef0c748e416d81f30b7622fa3828520718b38a1d0037585c00c0b", - "files": [ - { - "path": "config/relay.yaml", - "sha256": "sha256:6588003f187ef0c748e416d81f30b7622fa3828520718b38a1d0037585c00c0b" - } - ], - "created_at": "2026-07-29T03:29:37.148851Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/bundle/manifest.sig.json b/config/hosted-relay-bundles/nia-population/bundle/manifest.sig.json deleted file mode 100644 index 2a6964d..0000000 --- a/config/hosted-relay-bundles/nia-population/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "USI_qKV0mc8bRwH9aTTA1CcA02YZLHIcTrnNbwcKdo7kKZtPH0i9wRrcrkbZemNAC2CxMrutjjaOi-dOJFQ4CQ" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/consultation/antirollback-seed.json b/config/hosted-relay-bundles/nia-population/consultation/antirollback-seed.json deleted file mode 100644 index 9a32f97..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nia-population-consultation" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/nia-population/consultation/bootstrap.yaml b/config/hosted-relay-bundles/nia-population/consultation/bootstrap.yaml deleted file mode 100644 index 893a7d4..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bootstrap.yaml +++ /dev/null @@ -1,255 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - nia-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nia-relay.solmara.registrystack.org - publisher: solmara-nia-population - title: solmara-nia-population governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/population-person-by-uin/minimization.json - sha256: sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407 - - class: conformance - path: artifacts/evidence/population-person-by-uin/conformance.json - sha256: sha256:bae5ef90128eb60a3ef4a3eeea3bea815e73bf6ccfdd521a48a3043446bd5fbd - - class: negative_security - path: artifacts/evidence/population-person-by-uin/negative-security.json - sha256: sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab - integration_packs: - - hash: sha256:72b42442af5dffa0ad96ef2bb6266fd422ae1f16299b3fc82dbd3bf3fd9b58e9 - path: artifacts/integration-packs/population-person-by-uin.json - sha256: sha256:83e0e332c81c331e2329535fdf1983be1427359a0e1bdf16986b8bc4a2f24b3e - private_bindings: - - hash: sha256:fa33b620ee699baa8efb7f233dc46884826b4e4b64940a0fa27cca5b1871bb40 - path: artifacts/private-bindings/nia-child-benefit-population.json - sha256: sha256:41dd7eba725d043adbdfbfcc7a95178364970c5b8f76525fa75db87c31d228b6 - - hash: sha256:286382e48365cf203f3b0b5712bb6c8f94d18d33657078dc2522e42afb76aa8f - path: artifacts/private-bindings/nia-citizen-status-population.json - sha256: sha256:89e52d120d4c5ab0fc451e74e2fe0bb11966e7e1e4ab59505713d78f567ae8b7 - public_contracts: - - hash: sha256:2b34d7466e4f45126055cdb3ec18026a6f0cf3051a863ad951eedfe6ed729958 - path: artifacts/consultation-contracts/nia-child-benefit-population.json - sha256: sha256:43e56a4eaff600b7fbb8ca382f6d00bee299dcd9e528f50de89143ad9f3502f3 - - hash: sha256:9433a86942bdfbb1add5e45e2c3462eb224a2e8097be0267a52eeaaecfde0c69 - path: artifacts/consultation-contracts/nia-citizen-status-population.json - sha256: sha256:3ecdd494bdec2c8b591c24385a3c808d49a9b9562f9809e1a7dd24dcc66693a1 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: nia-notary - principal_id: nia-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 1016450306001272100 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 6539803953464966811 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Governed population attributes used by NIA-owned services. - entities: - - access: - aggregate_scope: population:aggregate - evidence_verification_scope: population:evidence_verification - metadata_scope: population:metadata - read_scope: population:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: legacy_nid - ops: - - eq - - field: uin - ops: - - eq - default_limit: 25 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/esignet-identity-verification - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 100 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: - - claims: - - expression: null - name: birthdate - required: true - sensitivity: personal - source_field: birth_date - - expression: null - name: family_name - required: true - sensitivity: direct_identifier - source_field: family_name - - expression: null - name: gender - required: false - sensitivity: personal - source_field: sex - - expression: null - name: given_name - required: true - sensitivity: direct_identifier - source_field: given_name - - expression: null - name: individual_id - required: true - sensitivity: direct_identifier - source_field: uin - - expression: - cel: source.given_name + ' ' + source.family_name - name: name - required: true - sensitivity: direct_identifier - source_field: null - description: Minimal identity attributes for the Solmara eSignet authenticator. - id: solmara-nia-userinfo - purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - release_conditions: - expression: - cel: source.identity_status == 'active' && source.alive == true - release_scope: population:identity_release - subject: - id_type: national_id - source_field: uin - title: SolmaraID UserInfo - version: v1 - description: Governed population attributes used by NIA-owned services. - fields: - - from: alive - name: alive - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: family_name - name: family_name - sensitive: true - - from: given_name - name: given_name - sensitive: true - - from: identity_status - name: identity_status - sensitive: true - - from: legacy_nid - name: legacy_nid - sensitive: true - - from: sex - name: sex - sensitive: true - - from: uin - name: uin - sensitive: true - name: population - relationships: [] - spatial: null - table: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - title: NIA population records - id: population - owner: National Identity Agency - sensitivity: personal - tables: - - access: - aggregate_scope: population:aggregate - metadata_scope: population:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 25 - max_limit: 100 - require_purpose_header: true - id: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: alive - nullable: false - sensitive: true - type: boolean - - name: birth_date - nullable: false - sensitive: true - type: date - - name: family_name - nullable: false - sensitive: true - type: string - - name: given_name - nullable: false - sensitive: true - type: string - - name: identity_status - nullable: false - sensitive: true - type: string - - name: legacy_nid - nullable: true - sensitive: true - type: string - - name: sex - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_NIA_DATABASE_URL - table: - name: population_person - schema: public - type: postgres - title: NIA population records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nia-population-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/nia-population/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/nia-population/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/consultation-contracts/nia-child-benefit-population.json b/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/consultation-contracts/nia-child-benefit-population.json deleted file mode 100644 index 90ad104..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/consultation-contracts/nia-child-benefit-population.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.nia-child-benefit.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:7af974a445cf4ebaec93647f7d6aa80732d70829150702ab0891c06f91850359","id":"relay.nia-child-benefit.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:nia-child-benefit","workload":"nia-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/consultation-contracts/nia-citizen-status-population.json b/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/consultation-contracts/nia-citizen-status-population.json deleted file mode 100644 index 08977c4..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/consultation-contracts/nia-citizen-status-population.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.nia-citizen-status.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"citizen-self-service","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:0f648848f4934f37c2dbce9e58040e2ce7b43d39a5d1c8d0391cdd4aa07b9834","id":"relay.nia-citizen-status.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/citizen-self-service"],"required_scope":"registry:consult:nia-citizen-status","workload":"nia-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/conformance.json b/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/conformance.json deleted file mode 100644 index 752267e..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"citizen-population-record-active":true,"population-record-active":true},"error":null,"outcome":"match","outputs":{"alive":true,"identity_status":"active"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"alive":true,"identity_status":"active"},"headers":{},"status":200}}}],"name":"nia-population-person-match","request":{"claims":[{"id":"population-record-active"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"citizen-population-record-active":false,"population-record-active":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"nia-population-person-no-match","request":{"claims":[{"id":"citizen-population-record-active"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/citizen-self-service","target":{"identifiers":[{"scheme":"solmara_uin","value":"9999999999"}],"type":"Person"}},"variables":{}}],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/minimization.json b/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/minimization.json deleted file mode 100644 index 223b391..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-nia-population-person-by-uin","operations":{},"outputs":{"alive":{"from":"snapshot.record.alive","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"identity_status":{"from":"snapshot.record.identity_status","max_bytes":128,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/negative-security.json b/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/negative-security.json deleted file mode 100644 index 53dcfa4..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/evidence/population-person-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/integration-packs/population-person-by-uin.json b/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/integration-packs/population-person-by-uin.json deleted file mode 100644 index 7430026..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/integration-packs/population-person-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:bae5ef90128eb60a3ef4a3eeea3bea815e73bf6ccfdd521a48a3043446bd5fbd"],"minimization":["sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407"],"negative_security":["sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-nia-population-person-by-uin","output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":300000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/private-bindings/nia-child-benefit-population.json b/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/private-bindings/nia-child-benefit-population.json deleted file mode 100644 index 9d14494..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/private-bindings/nia-child-benefit-population.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:72b42442af5dffa0ad96ef2bb6266fd422ae1f16299b3fc82dbd3bf3fd9b58e9","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d"},"profile":{"id":"solmara-nia-population.nia-child-benefit.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d","tenant":"solmara-nia-population"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/private-bindings/nia-citizen-status-population.json b/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/private-bindings/nia-citizen-status-population.json deleted file mode 100644 index 735159f..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/artifacts/private-bindings/nia-citizen-status-population.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:72b42442af5dffa0ad96ef2bb6266fd422ae1f16299b3fc82dbd3bf3fd9b58e9","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d"},"profile":{"id":"solmara-nia-population.nia-citizen-status.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d","tenant":"solmara-nia-population"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/relay.yaml b/config/hosted-relay-bundles/nia-population/consultation/bundle/config/relay.yaml deleted file mode 100644 index 893a7d4..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/config/relay.yaml +++ /dev/null @@ -1,255 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - nia-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nia-relay.solmara.registrystack.org - publisher: solmara-nia-population - title: solmara-nia-population governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/population-person-by-uin/minimization.json - sha256: sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407 - - class: conformance - path: artifacts/evidence/population-person-by-uin/conformance.json - sha256: sha256:bae5ef90128eb60a3ef4a3eeea3bea815e73bf6ccfdd521a48a3043446bd5fbd - - class: negative_security - path: artifacts/evidence/population-person-by-uin/negative-security.json - sha256: sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab - integration_packs: - - hash: sha256:72b42442af5dffa0ad96ef2bb6266fd422ae1f16299b3fc82dbd3bf3fd9b58e9 - path: artifacts/integration-packs/population-person-by-uin.json - sha256: sha256:83e0e332c81c331e2329535fdf1983be1427359a0e1bdf16986b8bc4a2f24b3e - private_bindings: - - hash: sha256:fa33b620ee699baa8efb7f233dc46884826b4e4b64940a0fa27cca5b1871bb40 - path: artifacts/private-bindings/nia-child-benefit-population.json - sha256: sha256:41dd7eba725d043adbdfbfcc7a95178364970c5b8f76525fa75db87c31d228b6 - - hash: sha256:286382e48365cf203f3b0b5712bb6c8f94d18d33657078dc2522e42afb76aa8f - path: artifacts/private-bindings/nia-citizen-status-population.json - sha256: sha256:89e52d120d4c5ab0fc451e74e2fe0bb11966e7e1e4ab59505713d78f567ae8b7 - public_contracts: - - hash: sha256:2b34d7466e4f45126055cdb3ec18026a6f0cf3051a863ad951eedfe6ed729958 - path: artifacts/consultation-contracts/nia-child-benefit-population.json - sha256: sha256:43e56a4eaff600b7fbb8ca382f6d00bee299dcd9e528f50de89143ad9f3502f3 - - hash: sha256:9433a86942bdfbb1add5e45e2c3462eb224a2e8097be0267a52eeaaecfde0c69 - path: artifacts/consultation-contracts/nia-citizen-status-population.json - sha256: sha256:3ecdd494bdec2c8b591c24385a3c808d49a9b9562f9809e1a7dd24dcc66693a1 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: nia-notary - principal_id: nia-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 1016450306001272100 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 6539803953464966811 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Governed population attributes used by NIA-owned services. - entities: - - access: - aggregate_scope: population:aggregate - evidence_verification_scope: population:evidence_verification - metadata_scope: population:metadata - read_scope: population:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: legacy_nid - ops: - - eq - - field: uin - ops: - - eq - default_limit: 25 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/esignet-identity-verification - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 100 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: - - claims: - - expression: null - name: birthdate - required: true - sensitivity: personal - source_field: birth_date - - expression: null - name: family_name - required: true - sensitivity: direct_identifier - source_field: family_name - - expression: null - name: gender - required: false - sensitivity: personal - source_field: sex - - expression: null - name: given_name - required: true - sensitivity: direct_identifier - source_field: given_name - - expression: null - name: individual_id - required: true - sensitivity: direct_identifier - source_field: uin - - expression: - cel: source.given_name + ' ' + source.family_name - name: name - required: true - sensitivity: direct_identifier - source_field: null - description: Minimal identity attributes for the Solmara eSignet authenticator. - id: solmara-nia-userinfo - purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - release_conditions: - expression: - cel: source.identity_status == 'active' && source.alive == true - release_scope: population:identity_release - subject: - id_type: national_id - source_field: uin - title: SolmaraID UserInfo - version: v1 - description: Governed population attributes used by NIA-owned services. - fields: - - from: alive - name: alive - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: family_name - name: family_name - sensitive: true - - from: given_name - name: given_name - sensitive: true - - from: identity_status - name: identity_status - sensitive: true - - from: legacy_nid - name: legacy_nid - sensitive: true - - from: sex - name: sex - sensitive: true - - from: uin - name: uin - sensitive: true - name: population - relationships: [] - spatial: null - table: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - title: NIA population records - id: population - owner: National Identity Agency - sensitivity: personal - tables: - - access: - aggregate_scope: population:aggregate - metadata_scope: population:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 25 - max_limit: 100 - require_purpose_header: true - id: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: alive - nullable: false - sensitive: true - type: boolean - - name: birth_date - nullable: false - sensitive: true - type: date - - name: family_name - nullable: false - sensitive: true - type: string - - name: given_name - nullable: false - sensitive: true - type: string - - name: identity_status - nullable: false - sensitive: true - type: string - - name: legacy_nid - nullable: true - sensitive: true - type: string - - name: sex - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_NIA_DATABASE_URL - table: - name: population_person - schema: public - type: postgres - title: NIA population records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nia-population-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/nia-population/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/nia-population/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/manifest.json b/config/hosted-relay-bundles/nia-population/consultation/bundle/manifest.json deleted file mode 100644 index 2e6f1e3..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/manifest.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nia-population-consultation", - "instance_id": "nia-population-relay-consultation", - "bundle_id": "solmara-hosted-nia-population-consultation-sequence-2", - "sequence": 2, - "config_hash": "sha256:197109f949611154949d7ed2193dce14f308ddbaa155ad94952d4865e8ccc5d2", - "files": [ - { - "path": "config/artifacts/consultation-contracts/nia-child-benefit-population.json", - "sha256": "sha256:43e56a4eaff600b7fbb8ca382f6d00bee299dcd9e528f50de89143ad9f3502f3" - }, - { - "path": "config/artifacts/consultation-contracts/nia-citizen-status-population.json", - "sha256": "sha256:3ecdd494bdec2c8b591c24385a3c808d49a9b9562f9809e1a7dd24dcc66693a1" - }, - { - "path": "config/artifacts/evidence/population-person-by-uin/conformance.json", - "sha256": "sha256:bae5ef90128eb60a3ef4a3eeea3bea815e73bf6ccfdd521a48a3043446bd5fbd" - }, - { - "path": "config/artifacts/evidence/population-person-by-uin/minimization.json", - "sha256": "sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407" - }, - { - "path": "config/artifacts/evidence/population-person-by-uin/negative-security.json", - "sha256": "sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab" - }, - { - "path": "config/artifacts/integration-packs/population-person-by-uin.json", - "sha256": "sha256:83e0e332c81c331e2329535fdf1983be1427359a0e1bdf16986b8bc4a2f24b3e" - }, - { - "path": "config/artifacts/private-bindings/nia-child-benefit-population.json", - "sha256": "sha256:41dd7eba725d043adbdfbfcc7a95178364970c5b8f76525fa75db87c31d228b6" - }, - { - "path": "config/artifacts/private-bindings/nia-citizen-status-population.json", - "sha256": "sha256:89e52d120d4c5ab0fc451e74e2fe0bb11966e7e1e4ab59505713d78f567ae8b7" - }, - { - "path": "config/relay.yaml", - "sha256": "sha256:197109f949611154949d7ed2193dce14f308ddbaa155ad94952d4865e8ccc5d2" - } - ], - "created_at": "2026-07-29T03:29:38.822675Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/consultation/bundle/manifest.sig.json b/config/hosted-relay-bundles/nia-population/consultation/bundle/manifest.sig.json deleted file mode 100644 index 949a1a7..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "TC0ZLn8x73num9jNmMa78I0aLBdURn7d9xg1bB9YTndQosrqRSDnGq1wt5syAJZm-GI4pfTvPikmWaNKqFiCDw" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/consultation/trust-anchor.json b/config/hosted-relay-bundles/nia-population/consultation/trust-anchor.json deleted file mode 100644 index 074a049..0000000 --- a/config/hosted-relay-bundles/nia-population/consultation/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nia-population-consultation", - "instance_id": "nia-population-relay-consultation", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/trust-anchor.json b/config/hosted-relay-bundles/nia-population/trust-anchor.json deleted file mode 100644 index 7317e64..0000000 --- a/config/hosted-relay-bundles/nia-population/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-nia-population", - "instance_id": "nia-population-relay", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/antirollback-seed.json b/config/hosted-relay-bundles/sipf-pensions/antirollback-seed.json deleted file mode 100644 index bc046ab..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sipf-pensions" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/sipf-pensions/bootstrap.yaml b/config/hosted-relay-bundles/sipf-pensions/bootstrap.yaml deleted file mode 100644 index ab6ca4f..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/bootstrap.yaml +++ /dev/null @@ -1,125 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: [] - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sipf-relay.solmara.registrystack.org - publisher: solmara-sipf-pensions - title: solmara-sipf-pensions governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized pension entity - entities: [] - id: pension - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: pension:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - materialization: snapshot - primary_key: pensioner_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: payment_status - nullable: false - sensitive: true - type: string - - name: pensioner_uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_pension_payment - schema: public - type: postgres - title: pension - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized survivor entity - entities: [] - id: survivor - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: survivor:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - materialization: snapshot - primary_key: spouse_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: spouse_uin - nullable: false - sensitive: true - type: string - - name: survivor_eligible - nullable: false - sensitive: true - type: boolean - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_survivor_benefit - schema: public - type: postgres - title: survivor - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sipf-pensions-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/relay.yaml b/config/hosted-relay-bundles/sipf-pensions/bundle/config/relay.yaml deleted file mode 100644 index ab6ca4f..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/bundle/config/relay.yaml +++ /dev/null @@ -1,125 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: [] - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sipf-relay.solmara.registrystack.org - publisher: solmara-sipf-pensions - title: solmara-sipf-pensions governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized pension entity - entities: [] - id: pension - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: pension:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - materialization: snapshot - primary_key: pensioner_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: payment_status - nullable: false - sensitive: true - type: string - - name: pensioner_uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_pension_payment - schema: public - type: postgres - title: pension - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized survivor entity - entities: [] - id: survivor - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: survivor:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - materialization: snapshot - primary_key: spouse_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: spouse_uin - nullable: false - sensitive: true - type: string - - name: survivor_eligible - nullable: false - sensitive: true - type: boolean - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_survivor_benefit - schema: public - type: postgres - title: survivor - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sipf-pensions-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.json b/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.json deleted file mode 100644 index e4310af..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sipf-pensions", - "instance_id": "sipf-pensions-relay", - "bundle_id": "solmara-hosted-sipf-pensions-sequence-2", - "sequence": 2, - "config_hash": "sha256:e4d55a5834efa30c13090a85fe4e0449d40a5059f0bad3cb12429a318d671c19", - "files": [ - { - "path": "config/relay.yaml", - "sha256": "sha256:e4d55a5834efa30c13090a85fe4e0449d40a5059f0bad3cb12429a318d671c19" - } - ], - "created_at": "2026-07-29T03:29:46.971192Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.sig.json b/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.sig.json deleted file mode 100644 index 0faaabf..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "peS7v_icxT4i4TqT-i86h3sShiwwSBohpcp1-r6zVZ-zfXhg2QHq1SVBBWok8eh0X8FmX7_geekH1nqqkKx6Cw" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/antirollback-seed.json b/config/hosted-relay-bundles/sipf-pensions/consultation/antirollback-seed.json deleted file mode 100644 index 7032559..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sipf-pensions-consultation" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bootstrap.yaml b/config/hosted-relay-bundles/sipf-pensions/consultation/bootstrap.yaml deleted file mode 100644 index 192fd58..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bootstrap.yaml +++ /dev/null @@ -1,186 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - sipf-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sipf-relay.solmara.registrystack.org - publisher: solmara-sipf-pensions - title: solmara-sipf-pensions governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/pension-case-by-survivor/minimization.json - sha256: sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f - - class: conformance - path: artifacts/evidence/pension-case-by-pensioner/conformance.json - sha256: sha256:34d1119e31d3a2e22571b9532541d057874aebebbde363ed80a137bce5a6772e - - class: negative_security - path: artifacts/evidence/pension-case-by-survivor/negative-security.json - sha256: sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7 - - class: minimization - path: artifacts/evidence/pension-case-by-pensioner/minimization.json - sha256: sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660 - - class: conformance - path: artifacts/evidence/pension-case-by-survivor/conformance.json - sha256: sha256:a1f2073fc39da5b7a4c52bbd6d8f7a414e719b5c9dcf293509afac4d27f39059 - - class: negative_security - path: artifacts/evidence/pension-case-by-pensioner/negative-security.json - sha256: sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d - integration_packs: - - hash: sha256:ebcce823eb11489afd04cce13a550317ae7f9b9ef6c503bd68490d26085ab73e - path: artifacts/integration-packs/pension-case-by-pensioner.json - sha256: sha256:6252d74d83d4074d4eee0ae60cb2d1aac357c1a0a56664ab92120933ad0e77b3 - - hash: sha256:f4416975dcbe4c9eaea878107ea9e97a10b1fd9e02484b1d5b498b5c805d7235 - path: artifacts/integration-packs/pension-case-by-survivor.json - sha256: sha256:ca28e59689fa5e842f1435cd8c7bf922117ff2afc7bf3af08788781fcf458168 - private_bindings: - - hash: sha256:928e8a72b68fcdf9fe3ae4381cc71918d78a5c8f8bd138be09df42528a6fdcd3 - path: artifacts/private-bindings/sipf-pension-payment-review-pension.json - sha256: sha256:9499ffd554e9d00acdf62509e3dad500834050c4abff6ce0b1cc299fa35b7828 - - hash: sha256:5b46b40895d8fc69247e473ff117e119a08453121f5ecc9215e1297961ae701d - path: artifacts/private-bindings/sipf-survivor-benefit-survivor.json - sha256: sha256:37a68f42bf4870f69eb4faf6c820049c868951b49edbbe99483e61ae2cbde463 - public_contracts: - - hash: sha256:04deb5f47932e163e61b8f7ce9c4d4adff54a7558128edeb367518962abde1bd - path: artifacts/consultation-contracts/sipf-pension-payment-review-pension.json - sha256: sha256:75211b8be4be68778d4a0aa044fa46778b507017028dfa1b43c9330f9700a167 - - hash: sha256:de1c2295d58905dc34046288edf36263fcccf99cf684deaa885ab76f472ed6d6 - path: artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json - sha256: sha256:6bebf5122b4248dbc3eaefc5e3f7de1705796e30a1b8011b8c0fc3adf2c255f9 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: sipf-notary - principal_id: sipf-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4444991830243562918 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 6117019989203477116 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized pension entity - entities: [] - id: pension - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: pension:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - materialization: snapshot - primary_key: pensioner_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: payment_status - nullable: false - sensitive: true - type: string - - name: pensioner_uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_pension_payment - schema: public - type: postgres - title: pension - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized survivor entity - entities: [] - id: survivor - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: survivor:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - materialization: snapshot - primary_key: spouse_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: spouse_uin - nullable: false - sensitive: true - type: string - - name: survivor_eligible - nullable: false - sensitive: true - type: boolean - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_survivor_benefit - schema: public - type: postgres - title: survivor - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sipf-pensions-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json deleted file mode 100644 index ad9440e..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.sipf-pension-payment-review.pension","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:96e56b4b170376860f623777977f7c72a19cd960854a48f568e1e92ca33f85e7","id":"relay.sipf-pension-payment-review.pension","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:sipf-pension-payment-review","workload":"sipf-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["payment_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json deleted file mode 100644 index a4fd9c0..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.sipf-survivor-benefit.survivor","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:ec6b5310716606dac48159abec1e512757fb9a18bff8a6e0ac634db932b2696b","id":"relay.sipf-survivor-benefit.survivor","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"],"required_scope":"registry:consult:sipf-survivor-benefit","workload":"sipf-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["survivor_eligible"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/conformance.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/conformance.json deleted file mode 100644 index 8babb7e..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"payment_status":"active"},{"payment_status":"suspended"}],"headers":{},"status":200}}}],"name":"sipf-pension-case-ambiguous","request":null,"variables":{}},{"classification":"synthetic","expect":{"claims":{"pension-payment-active":true},"error":null,"outcome":"match","outputs":{"payment_status":"active"}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"payment_status":"active"},"headers":{},"status":200}}}],"name":"sipf-pension-case-match","request":{"claims":[{"id":"pension-payment-active"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/pension-payment-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300109568"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"pension-payment-active":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-pension-case-no-match","request":null,"variables":{}}],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/minimization.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/minimization.json deleted file mode 100644 index 27b99c4..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-sipf-pension-case-by-pensioner","operations":{},"outputs":{"payment_status":{"from":"snapshot.record.payment_status","max_bytes":128,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/negative-security.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/negative-security.json deleted file mode 100644 index c62fd58..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-pensioner/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/conformance.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/conformance.json deleted file mode 100644 index e33e4f9..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"survivor_eligible":true},{"survivor_eligible":false}],"headers":{},"status":200}}}],"name":"sipf-survivor-case-ambiguous","request":null,"variables":{}},{"classification":"synthetic","expect":{"claims":{"survivor-is-eligible":true},"error":null,"outcome":"match","outputs":{"survivor_eligible":true}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"survivor_eligible":true},"headers":{},"status":200}}}],"name":"sipf-survivor-case-match","request":{"claims":[{"id":"survivor-is-eligible"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/survivor-benefit-determination","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300118698"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"survivor-is-eligible":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-survivor-case-no-match","request":null,"variables":{}}],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/minimization.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/minimization.json deleted file mode 100644 index e246e22..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-sipf-pension-case-by-survivor","operations":{},"outputs":{"survivor_eligible":{"from":"snapshot.record.survivor_eligible","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/negative-security.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/negative-security.json deleted file mode 100644 index 72f6ab7..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/evidence/pension-case-by-survivor/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/integration-packs/pension-case-by-pensioner.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/integration-packs/pension-case-by-pensioner.json deleted file mode 100644 index 7fac5ec..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/integration-packs/pension-case-by-pensioner.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:34d1119e31d3a2e22571b9532541d057874aebebbde363ed80a137bce5a6772e"],"minimization":["sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660"],"negative_security":["sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-pensioner","output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/integration-packs/pension-case-by-survivor.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/integration-packs/pension-case-by-survivor.json deleted file mode 100644 index a0e0dda..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/integration-packs/pension-case-by-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:a1f2073fc39da5b7a4c52bbd6d8f7a414e719b5c9dcf293509afac4d27f39059"],"minimization":["sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f"],"negative_security":["sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-survivor","output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/private-bindings/sipf-pension-payment-review-pension.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/private-bindings/sipf-pension-payment-review-pension.json deleted file mode 100644 index 2b3b78b..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/private-bindings/sipf-pension-payment-review-pension.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:ebcce823eb11489afd04cce13a550317ae7f9b9ef6c503bd68490d26085ab73e","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"pensioner_uin","physical_type":"utf8"}},"projection":{"payment_status":"payment_status"}},"table_provider":"pension__materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21"},"profile":{"id":"solmara-sipf-pensions.sipf-pension-payment-review.pension","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/private-bindings/sipf-survivor-benefit-survivor.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/private-bindings/sipf-survivor-benefit-survivor.json deleted file mode 100644 index 8485dcd..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/artifacts/private-bindings/sipf-survivor-benefit-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:f4416975dcbe4c9eaea878107ea9e97a10b1fd9e02484b1d5b498b5c805d7235","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"spouse_uin","physical_type":"utf8"}},"projection":{"survivor_eligible":"survivor_eligible"}},"table_provider":"survivor__materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539"},"profile":{"id":"solmara-sipf-pensions.sipf-survivor-benefit.survivor","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/relay.yaml b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/relay.yaml deleted file mode 100644 index 192fd58..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/config/relay.yaml +++ /dev/null @@ -1,186 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - sipf-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sipf-relay.solmara.registrystack.org - publisher: solmara-sipf-pensions - title: solmara-sipf-pensions governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/pension-case-by-survivor/minimization.json - sha256: sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f - - class: conformance - path: artifacts/evidence/pension-case-by-pensioner/conformance.json - sha256: sha256:34d1119e31d3a2e22571b9532541d057874aebebbde363ed80a137bce5a6772e - - class: negative_security - path: artifacts/evidence/pension-case-by-survivor/negative-security.json - sha256: sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7 - - class: minimization - path: artifacts/evidence/pension-case-by-pensioner/minimization.json - sha256: sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660 - - class: conformance - path: artifacts/evidence/pension-case-by-survivor/conformance.json - sha256: sha256:a1f2073fc39da5b7a4c52bbd6d8f7a414e719b5c9dcf293509afac4d27f39059 - - class: negative_security - path: artifacts/evidence/pension-case-by-pensioner/negative-security.json - sha256: sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d - integration_packs: - - hash: sha256:ebcce823eb11489afd04cce13a550317ae7f9b9ef6c503bd68490d26085ab73e - path: artifacts/integration-packs/pension-case-by-pensioner.json - sha256: sha256:6252d74d83d4074d4eee0ae60cb2d1aac357c1a0a56664ab92120933ad0e77b3 - - hash: sha256:f4416975dcbe4c9eaea878107ea9e97a10b1fd9e02484b1d5b498b5c805d7235 - path: artifacts/integration-packs/pension-case-by-survivor.json - sha256: sha256:ca28e59689fa5e842f1435cd8c7bf922117ff2afc7bf3af08788781fcf458168 - private_bindings: - - hash: sha256:928e8a72b68fcdf9fe3ae4381cc71918d78a5c8f8bd138be09df42528a6fdcd3 - path: artifacts/private-bindings/sipf-pension-payment-review-pension.json - sha256: sha256:9499ffd554e9d00acdf62509e3dad500834050c4abff6ce0b1cc299fa35b7828 - - hash: sha256:5b46b40895d8fc69247e473ff117e119a08453121f5ecc9215e1297961ae701d - path: artifacts/private-bindings/sipf-survivor-benefit-survivor.json - sha256: sha256:37a68f42bf4870f69eb4faf6c820049c868951b49edbbe99483e61ae2cbde463 - public_contracts: - - hash: sha256:04deb5f47932e163e61b8f7ce9c4d4adff54a7558128edeb367518962abde1bd - path: artifacts/consultation-contracts/sipf-pension-payment-review-pension.json - sha256: sha256:75211b8be4be68778d4a0aa044fa46778b507017028dfa1b43c9330f9700a167 - - hash: sha256:de1c2295d58905dc34046288edf36263fcccf99cf684deaa885ab76f472ed6d6 - path: artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json - sha256: sha256:6bebf5122b4248dbc3eaefc5e3f7de1705796e30a1b8011b8c0fc3adf2c255f9 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: sipf-notary - principal_id: sipf-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4444991830243562918 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 6117019989203477116 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized pension entity - entities: [] - id: pension - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: pension:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - materialization: snapshot - primary_key: pensioner_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: payment_status - nullable: false - sensitive: true - type: string - - name: pensioner_uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_pension_payment - schema: public - type: postgres - title: pension - update_frequency: as_needed -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized survivor entity - entities: [] - id: survivor - owner: solmara-sipf-pensions - sensitivity: personal - tables: - - access: - aggregate_scope: survivor:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - materialization: snapshot - primary_key: spouse_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: spouse_uin - nullable: false - sensitive: true - type: string - - name: survivor_eligible - nullable: false - sensitive: true - type: boolean - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_survivor_benefit - schema: public - type: postgres - title: survivor - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sipf-pensions-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/manifest.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/manifest.json deleted file mode 100644 index 8923132..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/manifest.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sipf-pensions-consultation", - "instance_id": "sipf-pensions-relay-consultation", - "bundle_id": "solmara-hosted-sipf-pensions-consultation-sequence-2", - "sequence": 2, - "config_hash": "sha256:08888c3a0b9899b9e84847aedc1dec6db6db941bd8c84c5b9bcf892f752a2f29", - "files": [ - { - "path": "config/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json", - "sha256": "sha256:75211b8be4be68778d4a0aa044fa46778b507017028dfa1b43c9330f9700a167" - }, - { - "path": "config/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json", - "sha256": "sha256:6bebf5122b4248dbc3eaefc5e3f7de1705796e30a1b8011b8c0fc3adf2c255f9" - }, - { - "path": "config/artifacts/evidence/pension-case-by-pensioner/conformance.json", - "sha256": "sha256:34d1119e31d3a2e22571b9532541d057874aebebbde363ed80a137bce5a6772e" - }, - { - "path": "config/artifacts/evidence/pension-case-by-pensioner/minimization.json", - "sha256": "sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660" - }, - { - "path": "config/artifacts/evidence/pension-case-by-pensioner/negative-security.json", - "sha256": "sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d" - }, - { - "path": "config/artifacts/evidence/pension-case-by-survivor/conformance.json", - "sha256": "sha256:a1f2073fc39da5b7a4c52bbd6d8f7a414e719b5c9dcf293509afac4d27f39059" - }, - { - "path": "config/artifacts/evidence/pension-case-by-survivor/minimization.json", - "sha256": "sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f" - }, - { - "path": "config/artifacts/evidence/pension-case-by-survivor/negative-security.json", - "sha256": "sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7" - }, - { - "path": "config/artifacts/integration-packs/pension-case-by-pensioner.json", - "sha256": "sha256:6252d74d83d4074d4eee0ae60cb2d1aac357c1a0a56664ab92120933ad0e77b3" - }, - { - "path": "config/artifacts/integration-packs/pension-case-by-survivor.json", - "sha256": "sha256:ca28e59689fa5e842f1435cd8c7bf922117ff2afc7bf3af08788781fcf458168" - }, - { - "path": "config/artifacts/private-bindings/sipf-pension-payment-review-pension.json", - "sha256": "sha256:9499ffd554e9d00acdf62509e3dad500834050c4abff6ce0b1cc299fa35b7828" - }, - { - "path": "config/artifacts/private-bindings/sipf-survivor-benefit-survivor.json", - "sha256": "sha256:37a68f42bf4870f69eb4faf6c820049c868951b49edbbe99483e61ae2cbde463" - }, - { - "path": "config/relay.yaml", - "sha256": "sha256:08888c3a0b9899b9e84847aedc1dec6db6db941bd8c84c5b9bcf892f752a2f29" - } - ], - "created_at": "2026-07-29T03:29:48.663894Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/manifest.sig.json b/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/manifest.sig.json deleted file mode 100644 index 16a7711..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "Ul5q0GvGR3NLnEQijOuX83pBCiO2U3HTBtBcCl-2aiMp7q8VwbDWcAZXHm4oxgNin8j8uId0ffStg_1dO1JzDg" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/consultation/trust-anchor.json b/config/hosted-relay-bundles/sipf-pensions/consultation/trust-anchor.json deleted file mode 100644 index a9c8951..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/consultation/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sipf-pensions-consultation", - "instance_id": "sipf-pensions-relay-consultation", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/trust-anchor.json b/config/hosted-relay-bundles/sipf-pensions/trust-anchor.json deleted file mode 100644 index a5142bc..0000000 --- a/config/hosted-relay-bundles/sipf-pensions/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sipf-pensions", - "instance_id": "sipf-pensions-relay", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/antirollback-seed.json b/config/hosted-relay-bundles/sro-social/antirollback-seed.json deleted file mode 100644 index d2748e6..0000000 --- a/config/hosted-relay-bundles/sro-social/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sro-social" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/sro-social/bootstrap.yaml b/config/hosted-relay-bundles/sro-social/bootstrap.yaml deleted file mode 100644 index 4c9c25e..0000000 --- a/config/hosted-relay-bundles/sro-social/bootstrap.yaml +++ /dev/null @@ -1,80 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - sro-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sro-relay.solmara.registrystack.org - publisher: solmara-sro-social - title: solmara-sro-social governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized sro_child entity - entities: [] - id: sro_child - owner: solmara-sro-social - sensitivity: personal - tables: - - access: - aggregate_scope: sro_child:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: poverty_band - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv - type: file - title: sro_child - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sro-social-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/sro-social/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/sro-social/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sro-social/bundle/config/relay.yaml b/config/hosted-relay-bundles/sro-social/bundle/config/relay.yaml deleted file mode 100644 index 4c9c25e..0000000 --- a/config/hosted-relay-bundles/sro-social/bundle/config/relay.yaml +++ /dev/null @@ -1,80 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - sro-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sro-relay.solmara.registrystack.org - publisher: solmara-sro-social - title: solmara-sro-social governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized sro_child entity - entities: [] - id: sro_child - owner: solmara-sro-social - sensitivity: personal - tables: - - access: - aggregate_scope: sro_child:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: poverty_band - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv - type: file - title: sro_child - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sro-social-relay -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/sro-social/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/sro-social/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sro-social/bundle/manifest.json b/config/hosted-relay-bundles/sro-social/bundle/manifest.json deleted file mode 100644 index c5650d7..0000000 --- a/config/hosted-relay-bundles/sro-social/bundle/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sro-social", - "instance_id": "sro-social-relay", - "bundle_id": "solmara-hosted-sro-social-sequence-2", - "sequence": 2, - "config_hash": "sha256:506108c3be1cb86796f6ac59d2d62dc3083fc77ee5f87795ef0479bace0bffe6", - "files": [ - { - "path": "config/relay.yaml", - "sha256": "sha256:506108c3be1cb86796f6ac59d2d62dc3083fc77ee5f87795ef0479bace0bffe6" - } - ], - "created_at": "2026-07-29T03:29:40.453399Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/bundle/manifest.sig.json b/config/hosted-relay-bundles/sro-social/bundle/manifest.sig.json deleted file mode 100644 index acd46c5..0000000 --- a/config/hosted-relay-bundles/sro-social/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "iesNhkxoxWZ8kYtVJtr14O0VSEq4a3WR2rHgIdvomilgKW-YNSq-7LuHNnZXbmP-qrDHkDUdnd_Rl_lhFqI-Ag" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/consultation/antirollback-seed.json b/config/hosted-relay-bundles/sro-social/consultation/antirollback-seed.json deleted file mode 100644 index 050751f..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/antirollback-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sro-social-consultation" - }, - "last_sequence": 0, - "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/config/hosted-relay-bundles/sro-social/consultation/bootstrap.yaml b/config/hosted-relay-bundles/sro-social/consultation/bootstrap.yaml deleted file mode 100644 index 2313f8c..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/bootstrap.yaml +++ /dev/null @@ -1,122 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - sro-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sro-relay.solmara.registrystack.org - publisher: solmara-sro-social - title: solmara-sro-social governed Registry Relay -consultation: - artifacts: - evidence: - - class: negative_security - path: artifacts/evidence/child-benefit-household-by-uin/negative-security.json - sha256: sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95 - - class: minimization - path: artifacts/evidence/child-benefit-household-by-uin/minimization.json - sha256: sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0 - - class: conformance - path: artifacts/evidence/child-benefit-household-by-uin/conformance.json - sha256: sha256:d85f20f78036bb44a72ae05846565b2d7a705f92bb5836a75285c71ef63da9ce - integration_packs: - - hash: sha256:7f32b8cf5cee550693f4c1a99d5859d251bbb54c6840c56a4c7ec1cf63c6462e - path: artifacts/integration-packs/child-benefit-household-by-uin.json - sha256: sha256:bbf3bfb05e1f6020bdc14457db9549ad084ad421b5c59715bdb51d5ea67dc4d3 - private_bindings: - - hash: sha256:4dd783c0c8bcf6b6fee3aefd511c719dab911053adf3f39874cc99deded3bcfb - path: artifacts/private-bindings/child-benefit-household.json - sha256: sha256:9515fa0b01838a1f07f25b2b86b75843e8ef888438b82989f5ab7d657e2865ba - public_contracts: - - hash: sha256:7c6800ab1eea861c2e2500ba3e93bb9ddcb6d1212d7701756098155faaee4697 - path: artifacts/consultation-contracts/child-benefit-household.json - sha256: sha256:77470bdaa56f3bf30beb1734e0aa0fd676068b09f45d576ce32cae4a51920efb - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: sro-notary - principal_id: sro-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 368637434756268187 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 2694379094216255332 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized sro_child entity - entities: [] - id: sro_child - owner: solmara-sro-social - sensitivity: personal - tables: - - access: - aggregate_scope: sro_child:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: poverty_band - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv - type: file - title: sro_child - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sro-social-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/sro-social/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/sro-social/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/consultation-contracts/child-benefit-household.json b/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/consultation-contracts/child-benefit-household.json deleted file mode 100644 index e8bbb22..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/consultation-contracts/child-benefit-household.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sro-social.child-benefit.household","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:1f2bc3ace6f53ef9e6aa82974582fdd31113142f164f5afaf8328dafe39d92d3","id":"relay.child-benefit.household","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:child-benefit","workload":"sro-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sro-social.child-benefit-household-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["poverty_band"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":2678400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/conformance.json b/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/conformance.json deleted file mode 100644 index 6a52524..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"household-below-poverty-threshold":true},"error":null,"outcome":"match","outputs":{"poverty_band":"priority"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"poverty_band":"priority"},"headers":{},"status":200}}}],"name":"child-benefit-household-match","request":{"claims":[{"id":"household-below-poverty-threshold"}],"disclosure":"predicate","format":"application/vnd.registry-notary.claim-result+json","purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review","target":{"identifiers":[{"scheme":"solmara_uin","value":"2300010248"}],"type":"Person"}},"variables":{}},{"classification":"synthetic","expect":{"claims":{"household-below-poverty-threshold":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"child-benefit-household-no-match","request":null,"variables":{}}],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/minimization.json b/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/minimization.json deleted file mode 100644 index fc5378a..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"child-benefit-household-by-uin","operations":{},"outputs":{"poverty_band":{"from":"snapshot.record.poverty_band","max_bytes":64,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/negative-security.json b/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/negative-security.json deleted file mode 100644 index 9037e0a..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/evidence/child-benefit-household-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/integration-packs/child-benefit-household-by-uin.json b/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/integration-packs/child-benefit-household-by-uin.json deleted file mode 100644 index 7133664..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/integration-packs/child-benefit-household-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sro-social.child-benefit-household-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:d85f20f78036bb44a72ae05846565b2d7a705f92bb5836a75285c71ef63da9ce"],"minimization":["sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0"],"negative_security":["sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"child-benefit-household-by-uin","output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":2678400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/private-bindings/child-benefit-household.json b/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/private-bindings/child-benefit-household.json deleted file mode 100644 index e46d4f8..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/artifacts/private-bindings/child-benefit-household.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:7f32b8cf5cee550693f4c1a99d5859d251bbb54c6840c56a4c7ec1cf63c6462e","id":"solmara-sro-social.child-benefit-household-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"poverty_band":"poverty_band"}},"table_provider":"sro_child__materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d"},"profile":{"id":"solmara-sro-social.child-benefit.household","version":"1"},"registry_instance":"solmara-sro-social","source_instance":"materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d","tenant":"solmara-sro-social"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/relay.yaml b/config/hosted-relay-bundles/sro-social/consultation/bundle/config/relay.yaml deleted file mode 100644 index 2313f8c..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/bundle/config/relay.yaml +++ /dev/null @@ -1,122 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - sink: stdout -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - sro-notary - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sro-relay.solmara.registrystack.org - publisher: solmara-sro-social - title: solmara-sro-social governed Registry Relay -consultation: - artifacts: - evidence: - - class: negative_security - path: artifacts/evidence/child-benefit-household-by-uin/negative-security.json - sha256: sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95 - - class: minimization - path: artifacts/evidence/child-benefit-household-by-uin/minimization.json - sha256: sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0 - - class: conformance - path: artifacts/evidence/child-benefit-household-by-uin/conformance.json - sha256: sha256:d85f20f78036bb44a72ae05846565b2d7a705f92bb5836a75285c71ef63da9ce - integration_packs: - - hash: sha256:7f32b8cf5cee550693f4c1a99d5859d251bbb54c6840c56a4c7ec1cf63c6462e - path: artifacts/integration-packs/child-benefit-household-by-uin.json - sha256: sha256:bbf3bfb05e1f6020bdc14457db9549ad084ad421b5c59715bdb51d5ea67dc4d3 - private_bindings: - - hash: sha256:4dd783c0c8bcf6b6fee3aefd511c719dab911053adf3f39874cc99deded3bcfb - path: artifacts/private-bindings/child-benefit-household.json - sha256: sha256:9515fa0b01838a1f07f25b2b86b75843e8ef888438b82989f5ab7d657e2865ba - public_contracts: - - hash: sha256:7c6800ab1eea861c2e2500ba3e93bb9ddcb6d1212d7701756098155faaee4697 - path: artifacts/consultation-contracts/child-benefit-household.json - sha256: sha256:77470bdaa56f3bf30beb1734e0aa0fd676068b09f45d576ce32cae4a51920efb - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: sro-notary - principal_id: sro-notary - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 368637434756268187 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 2694379094216255332 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Materialized sro_child entity - entities: [] - id: sro_child - owner: solmara-sro-social - sensitivity: personal - tables: - - access: - aggregate_scope: sro_child:aggregate - metadata_scope: registry-internal:materialization - aggregates: [] - api: - allowed_filters: [] - default_limit: 1 - max_limit: 1 - require_purpose_header: false - id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: poverty_band - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv - type: file - title: sro_child - update_frequency: as_needed -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sro-social-relay-consultation -server: - bind: 127.0.0.1:8080 -standards: {} -config_trust: - trust_anchor_path: /etc/solmara/hosted-relay-bundles/sro-social/consultation/trust-anchor.json - bundle_path: /etc/solmara/hosted-relay-bundles/sro-social/consultation/bundle - antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sro-social/consultation/bundle/manifest.json b/config/hosted-relay-bundles/sro-social/consultation/bundle/manifest.json deleted file mode 100644 index 2e2adb3..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/bundle/manifest.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "schema": "registry.platform.config_bundle.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sro-social-consultation", - "instance_id": "sro-social-relay-consultation", - "bundle_id": "solmara-hosted-sro-social-consultation-sequence-2", - "sequence": 2, - "config_hash": "sha256:bdbbc8e2957210edffb72d09ac5fcd1c678e8d5967ff1aa448b57df947adc71f", - "files": [ - { - "path": "config/artifacts/consultation-contracts/child-benefit-household.json", - "sha256": "sha256:77470bdaa56f3bf30beb1734e0aa0fd676068b09f45d576ce32cae4a51920efb" - }, - { - "path": "config/artifacts/evidence/child-benefit-household-by-uin/conformance.json", - "sha256": "sha256:d85f20f78036bb44a72ae05846565b2d7a705f92bb5836a75285c71ef63da9ce" - }, - { - "path": "config/artifacts/evidence/child-benefit-household-by-uin/minimization.json", - "sha256": "sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0" - }, - { - "path": "config/artifacts/evidence/child-benefit-household-by-uin/negative-security.json", - "sha256": "sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95" - }, - { - "path": "config/artifacts/integration-packs/child-benefit-household-by-uin.json", - "sha256": "sha256:bbf3bfb05e1f6020bdc14457db9549ad084ad421b5c59715bdb51d5ea67dc4d3" - }, - { - "path": "config/artifacts/private-bindings/child-benefit-household.json", - "sha256": "sha256:9515fa0b01838a1f07f25b2b86b75843e8ef888438b82989f5ab7d657e2865ba" - }, - { - "path": "config/relay.yaml", - "sha256": "sha256:bdbbc8e2957210edffb72d09ac5fcd1c678e8d5967ff1aa448b57df947adc71f" - } - ], - "created_at": "2026-07-29T03:29:42.099976Z" -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/consultation/bundle/manifest.sig.json b/config/hosted-relay-bundles/sro-social/consultation/bundle/manifest.sig.json deleted file mode 100644 index 3391ec2..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/bundle/manifest.sig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": "registry.platform.config_bundle_signatures.v1", - "signatures": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "alg": "EdDSA", - "sig": "goLJG2a662RgI1VIrhIzsi6m3EymC5-2lSCj2v4mWJ06wb76HKQdZvjsRaoH64tA4RsLTOhVzG60NK4rpwgfBw" - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/consultation/trust-anchor.json b/config/hosted-relay-bundles/sro-social/consultation/trust-anchor.json deleted file mode 100644 index d0916d4..0000000 --- a/config/hosted-relay-bundles/sro-social/consultation/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sro-social-consultation", - "instance_id": "sro-social-relay-consultation", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/trust-anchor.json b/config/hosted-relay-bundles/sro-social/trust-anchor.json deleted file mode 100644 index 12e0ff3..0000000 --- a/config/hosted-relay-bundles/sro-social/trust-anchor.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schema": "registry.platform.config_trust_anchor.v1", - "product": "registry-relay", - "environment": "hosted", - "stream_id": "solmara-hosted-sro-social", - "instance_id": "sro-social-relay", - "signers": [ - { - "kid": "ef54I2IpdsgOpT6a7ASs8rBiMc6iK6ThBlTGSKDAnT8", - "jwk": { - "kty": "OKP", - "kid": "solmara-hosted-relay-config-2026-07-29", - "alg": "EdDSA", - "crv": "Ed25519", - "x": "0s7jygHjfzfMWJikV4FYv9Ov7XFEy_C-7Kb_EXvTjOw" - }, - "enabled": true - } - ] -} \ No newline at end of file diff --git a/config/walt/Caddyfile b/config/walt/Caddyfile index 5a24488..998be97 100644 --- a/config/walt/Caddyfile +++ b/config/walt/Caddyfile @@ -10,8 +10,8 @@ # /wallet-api/* to the wallet-api backend on the SAME origin. walt's web wallet # calls the API at a relative /wallet-api path on its own origin, so this # same-origin routing must be preserved (it is what walt's stock Caddyfile does; -# only the demo-wallet site block is kept here, the issuer/verifier/portal -# blocks are dropped because the Notary is the issuer in this lab). +# only the demo-wallet site block is kept here; issuer, verifier, and portal +# services are outside this holder-only profile). :7101 { reverse_proxy http://waltid-demo-wallet:7101 # frontend reverse_proxy /wallet-api/* http://wallet-api:7001 # backend diff --git a/config/walt/README.md b/config/walt/README.md index f411626..330758c 100644 --- a/config/walt/README.md +++ b/config/walt/README.md @@ -17,14 +17,10 @@ compose bind mounts from the repo; see the runbook). `Caddyfile` and this ## Deltas from stock walt -1. **`registration-defaults.conf`** — `defaultKeyConfig.keyType` is `Ed25519` - (stock: `secp256r1`). This keeps holder keys compatible with the Registry - Notary `EdDSA` plus `did:jwk` binding used when an authority project exposes - a reviewed issuance flow. -2. **`auth.conf`** — the login-session `encryptionKey` / `signKey` / `tokenKey` +1. **`auth.conf`** — the login-session `encryptionKey` / `signKey` / `tokenKey` are read from the environment (`WALT_AUTH_*`) instead of walt's public sample values, and fail-closed if unset. -3. **`dev-mode.conf`** — `enableDidWebResolverHttps=true` (stock: `false`) so the +2. **`dev-mode.conf`** — `enableDidWebResolverHttps=true` (stock: `false`) so the wallet resolves authority `did:web` issuers over HTTPS. ## Ingress diff --git a/config/walt/registration-defaults.conf b/config/walt/registration-defaults.conf index a22bbd2..f644166 100644 --- a/config/walt/registration-defaults.conf +++ b/config/walt/registration-defaults.conf @@ -1,12 +1,8 @@ // Setup what key should be generated on registration // -// Solmara Lab change: keyType is Ed25519 (walt's stock default is secp256r1). -// Authority Notary credential profiles use EdDSA and did:jwk holder binding. -// An Ed25519 JWK key with the did:jwk method produces that binding, so the -// wallet can receive credentials issued by those profiles. defaultKeyConfig: { backend: jwk - keyType: Ed25519 + keyType: secp256r1 } // Setup what DID should be generated based on above above defined key on registration diff --git a/docker/esignet-postgres/Dockerfile b/docker/esignet-postgres/Dockerfile index 73b0f45..f1fafb8 100644 --- a/docker/esignet-postgres/Dockerfile +++ b/docker/esignet-postgres/Dockerfile @@ -1,4 +1,4 @@ -ARG POSTGRES_IMAGE=postgres:16 +ARG POSTGRES_IMAGE=postgres@sha256:9479eac93922431e8a512b016d7362ae264f4ba139f840891b51f02931e450db FROM ${POSTGRES_IMAGE} COPY config/esignet/init.sql /docker-entrypoint-initdb.d/init.sql diff --git a/docker/esignet-relay/Dockerfile b/docker/esignet-relay/Dockerfile index f6551ed..f383312 100644 --- a/docker/esignet-relay/Dockerfile +++ b/docker/esignet-relay/Dockerfile @@ -1,20 +1,20 @@ # SPDX-License-Identifier: Apache-2.0 -ARG ESIGNET_BASE_IMAGE=mosipid/esignet-with-plugins:1.8.0 - -FROM maven:3.9.9-eclipse-temurin-21@sha256:3a4ab3276a087bf276f79cae96b1af04f53731bec53fb2e651aca79e4b10211e AS plugin-builder - -WORKDIR /workspace/esignet-relay-authenticator -COPY --from=esignet_relay_authenticator_src . . - -RUN --mount=type=cache,target=/root/.m2 \ - mvn -B -DskipTests clean package && \ - mkdir -p /out && \ - cp target/esignet-relay-authenticator-*.jar /out/esignet-relay-authenticator.jar +ARG ESIGNET_BASE_IMAGE=mosipid/esignet-with-plugins@sha256:47fffdb5a45198b29885a533841129877a7385a12bcb6020c0f6d4335477be39 +ARG ESIGNET_AUTHENTICATOR_JAR_URL +ARG ESIGNET_AUTHENTICATOR_JAR_SHA256 + +FROM alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS authenticator +ARG ESIGNET_AUTHENTICATOR_JAR_URL +ARG ESIGNET_AUTHENTICATOR_JAR_SHA256 +RUN test -n "$ESIGNET_AUTHENTICATOR_JAR_URL" && \ + test -n "$ESIGNET_AUTHENTICATOR_JAR_SHA256" && \ + wget -q -O /esignet-relay-authenticator.jar "$ESIGNET_AUTHENTICATOR_JAR_URL" && \ + printf '%s %s\n' "$ESIGNET_AUTHENTICATOR_JAR_SHA256" /esignet-relay-authenticator.jar | sha256sum -c - FROM ${ESIGNET_BASE_IMAGE} -COPY --from=plugin-builder /out/esignet-relay-authenticator.jar /home/mosip/plugins/esignet-relay-authenticator.jar +COPY --from=authenticator /esignet-relay-authenticator.jar /home/mosip/plugins/esignet-relay-authenticator.jar COPY scripts/start-esignet-relay.sh /usr/local/bin/start-esignet-relay.sh USER root diff --git a/docker/esignet-ui/Dockerfile b/docker/esignet-ui/Dockerfile index 5bc9c0e..374b945 100644 --- a/docker/esignet-ui/Dockerfile +++ b/docker/esignet-ui/Dockerfile @@ -1,4 +1,4 @@ -ARG ESIGNET_UI_IMAGE=mosipid/oidc-ui:1.8.0 +ARG ESIGNET_UI_IMAGE=mosipid/oidc-ui@sha256:8a2a6839b4e22be6c967dabc6308190c165c54604a778c2d9b1aae8091db93e7 FROM ${ESIGNET_UI_IMAGE} ARG ESIGNET_NGINX_CONF=config/esignet/nginx-hosted.conf diff --git a/docker/hosted-authority-provisioner/Dockerfile b/docker/hosted-authority-provisioner/Dockerfile new file mode 100644 index 0000000..7d5f77f --- /dev/null +++ b/docker/hosted-authority-provisioner/Dockerfile @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1.7 + +ARG PYTHON_IMAGE=python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df +ARG ASSET_PYTHON_IMAGE=python:3.12-slim-trixie@sha256:229a2c5bfa27522db7815ea81f9bed70af17ccb9de9fc7ad142b1877b5830d36 +ARG UV_IMAGE=ghcr.io/astral-sh/uv@sha256:440fd6477af86a2f1b38080c539f1672cd22acb1b1a47e321dba5158ab08864d +ARG ALPINE_IMAGE=alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc +ARG REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL +ARG REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256 + +FROM ${ALPINE_IMAGE} AS relayctl-download +ARG REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL +ARG REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256 +RUN test -n "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" && \ + test -n "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" && \ + wget -q -O /relayctl "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" && \ + printf '%s %s\n' "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" /relayctl | sha256sum -c - && \ + chmod 0755 /relayctl + +FROM ${UV_IMAGE} AS uv + +FROM ${ASSET_PYTHON_IMAGE} AS assets +ENV PYTHONDONTWRITEBYTECODE=1 +COPY --from=uv /uv /usr/local/bin/uv +COPY --from=relayctl-download /relayctl /usr/local/bin/relayctl +WORKDIR /workspace +COPY pyproject.toml uv.lock ./ +RUN uv export --frozen --no-dev --no-hashes -o /requirements.txt && \ + pip install --no-cache-dir -r /requirements.txt && rm /requirements.txt +COPY generator/solmara_lab/__init__.py generator/solmara_lab/generate.py generator/solmara_lab/publisher.py ./generator/solmara_lab/ +COPY relays ./relays +COPY evidence/cells ./evidence/cells +COPY evidence/mint.yaml ./evidence/mint.yaml +COPY scripts/hosted-runtime-assets.py ./scripts/hosted-runtime-assets.py +RUN python scripts/hosted-runtime-assets.py build \ + --root /workspace \ + --output /opt/solmara-hosted-assets \ + --relayctl /usr/local/bin/relayctl + +FROM ${PYTHON_IMAGE} +ENV PYTHONDONTWRITEBYTECODE=1 +COPY --from=uv /uv /usr/local/bin/uv +WORKDIR /app +COPY pyproject.toml uv.lock ./ +RUN uv export --frozen --no-dev --no-hashes -o /requirements.txt && \ + pip install --no-cache-dir -r /requirements.txt && rm /requirements.txt +COPY --from=assets /opt/solmara-hosted-assets /opt/solmara-hosted-assets +COPY scripts/hosted-runtime-assets.py /app/scripts/hosted-runtime-assets.py +COPY scripts/provision-hosted-runtime.py /app/scripts/provision-hosted-runtime.py +ENTRYPOINT ["python", "/app/scripts/provision-hosted-runtime.py"] diff --git a/docker/hosted-transit-signer/Dockerfile b/docker/hosted-transit-signer/Dockerfile new file mode 100644 index 0000000..f761e74 --- /dev/null +++ b/docker/hosted-transit-signer/Dockerfile @@ -0,0 +1,20 @@ +# syntax=docker/dockerfile:1.7 + +FROM ghcr.io/astral-sh/uv@sha256:440fd6477af86a2f1b38080c539f1672cd22acb1b1a47e321dba5158ab08864d AS uv + +FROM python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df AS dependencies +WORKDIR /build +COPY --from=uv /uv /usr/local/bin/uv +COPY pyproject.toml uv.lock ./ +RUN uv export --frozen --no-dev --no-hashes -o requirements.txt \ + && sed -i '/^pyyaml==/d' requirements.txt \ + && pip install --no-cache-dir --prefix=/runtime -r requirements.txt + +FROM python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +COPY --from=dependencies /runtime /usr/local +COPY --chmod=0555 scripts/hosted-transit-signer.py /opt/solmara-transit-signer/hosted-transit-signer.py +COPY --chmod=0555 scripts/local-transit-proxy.py /opt/solmara-transit-signer/local-transit-proxy.py +USER 65532:65532 +ENTRYPOINT ["python", "/opt/solmara-transit-signer/hosted-transit-signer.py"] diff --git a/docker/notary/Dockerfile b/docker/notary/Dockerfile deleted file mode 100644 index a009a3e..0000000 --- a/docker/notary/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -ARG REGISTRY_NOTARY_IMAGE=ghcr.io/registrystack/registry-notary@sha256:1cce41247a18f70cb7ff4276bdeb5a16c1e641a9ed7951be8f612daaf35ab87b -FROM ${REGISTRY_NOTARY_IMAGE} - -COPY runtime/registry-projects/hosted/cra-civil/notary/notary.yaml /etc/solmara/registry-projects/hosted/cra-civil/notary/notary.yaml -COPY runtime/registry-projects/hosted/nia-population/notary/notary.yaml /etc/solmara/registry-projects/hosted/nia-population/notary/notary.yaml -COPY runtime/registry-projects/hosted/sro-social/notary/notary.yaml /etc/solmara/registry-projects/hosted/sro-social/notary/notary.yaml -COPY runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml /etc/solmara/registry-projects/hosted/mosd-programme/notary/notary.yaml -COPY runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml /etc/solmara/registry-projects/hosted/sipf-pensions/notary/notary.yaml -COPY runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml /etc/solmara/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml -COPY config/postgres/ssl/server.crt /etc/solmara/postgres/root.crt diff --git a/docker/postgres/Dockerfile b/docker/postgres/Dockerfile deleted file mode 100644 index dc53fa3..0000000 --- a/docker/postgres/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -ARG POSTGRES_IMAGE=postgres:16-alpine -FROM ${POSTGRES_IMAGE} - -COPY ministries/interior-population/fixtures /docker-entrypoint-initdb.d -COPY ministries/labour-pensions/fixtures /docker-entrypoint-initdb.d -COPY scripts/init-notary-postgresql.sh /docker-entrypoint-initdb.d/010-init-notary-postgresql.sh -COPY scripts/postgres-entrypoint.sh /usr/local/bin/solmara-postgres-entrypoint.sh -COPY config/postgres/ssl /run/solmara-postgres-ssl - -RUN chmod 0755 /usr/local/bin/solmara-postgres-entrypoint.sh \ - /docker-entrypoint-initdb.d/010-init-notary-postgresql.sh diff --git a/docker/registry-stack-release-binary/Dockerfile b/docker/registry-stack-release-binary/Dockerfile new file mode 100644 index 0000000..bc570a6 --- /dev/null +++ b/docker/registry-stack-release-binary/Dockerfile @@ -0,0 +1,42 @@ +# syntax=docker/dockerfile:1.7 + +FROM alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS evidence-download +ARG REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL +ARG REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256 +RUN test -n "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL" && \ + test -n "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256" && \ + wget -q -O /evidence "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL" && \ + printf '%s %s\n' "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256" /evidence | sha256sum -c - && \ + chmod 0755 /evidence + +FROM alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS mint-download +ARG REGISTRY_STACK_RELEASE_MINT_ASSET_URL +ARG REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256 +RUN test -n "$REGISTRY_STACK_RELEASE_MINT_ASSET_URL" && \ + test -n "$REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256" && \ + wget -q -O /mint "$REGISTRY_STACK_RELEASE_MINT_ASSET_URL" && \ + printf '%s %s\n' "$REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256" /mint | sha256sum -c - && \ + chmod 0755 /mint + +FROM alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS relayctl-download +ARG REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL +ARG REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256 +RUN test -n "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" && \ + test -n "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" && \ + wget -q -O /relayctl "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" && \ + printf '%s %s\n' "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" /relayctl | sha256sum -c - && \ + chmod 0755 /relayctl + +FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS evidence +COPY --from=evidence-download --chown=65532:65532 /evidence /usr/local/bin/evidence +EXPOSE 8080 +ENTRYPOINT ["/usr/local/bin/evidence"] + +FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS mint +COPY --from=mint-download --chown=65532:65532 /mint /usr/local/bin/mint +EXPOSE 8081 +ENTRYPOINT ["/usr/local/bin/mint"] + +FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS relayctl +COPY --from=relayctl-download --chown=65532:65532 /relayctl /usr/local/bin/relayctl +ENTRYPOINT ["/usr/local/bin/relayctl"] diff --git a/docker/registry-stack-runtime/Dockerfile b/docker/registry-stack-runtime/Dockerfile index 1dd8606..ea142bb 100644 --- a/docker/registry-stack-runtime/Dockerfile +++ b/docker/registry-stack-runtime/Dockerfile @@ -2,36 +2,26 @@ FROM rust:1.95-trixie@sha256:f49565f188ee00bc2a18dd418183f2c5f23ef7d6e691890517ed341a598f67c3 AS relay-builder WORKDIR /workspace - COPY . . - RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/workspace/target \ - cargo build --release --locked --package registry-relay \ - && install -D -m 0755 target/release/registry-relay /runtime/usr/local/bin/registry-relay \ - && install -D -m 0755 target/release/registry-relay-rhai-worker /runtime/usr/local/bin/registry-relay-rhai-worker \ - && install -D -m 0644 LICENSE /runtime/licenses/registry-relay/LICENSE \ - && mkdir -p \ - /runtime/etc/registry-relay \ - /runtime/var/lib/registry/audit \ - /runtime/var/lib/registry/state \ - /runtime/var/lib/registry-relay/cache \ - /runtime/var/lib/registry-relay/data \ - /runtime/var/log/registry-relay \ - && chown -R 65532:65532 \ - /runtime/etc/registry-relay \ - /runtime/var/lib/registry \ - /runtime/var/lib/registry-relay \ - /runtime/var/log/registry-relay + CARGO_INCREMENTAL=0 CARGO_PROFILE_RELEASE_DEBUG=0 \ + cargo build --release --locked -p registry-relay-v2 -p registry-relayctl \ + && install -D -m 0755 target/release/relay /runtime/usr/local/bin/relay \ + && install -D -m 0755 target/release/relayctl /tooling/usr/local/bin/relayctl \ + && install -D -m 0644 LICENSE /runtime/licenses/relay/LICENSE \ + && mkdir -p /runtime/etc/relay /runtime/var/lib/relay/audit /runtime/var/lib/relay/source \ + && chown -R 65532:65532 /runtime/etc/relay /runtime/var/lib/relay FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS relay - COPY --from=relay-builder /runtime/ / - -WORKDIR /var/lib/registry-relay -ENV REGISTRY_RELAY_CONFIG=/etc/registry-relay/config.yaml +WORKDIR /var/lib/relay EXPOSE 8080 +ENV RELAY_HEALTHCHECK_URL=http://127.0.0.1:8080/health +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD ["/usr/local/bin/relay", "healthcheck"] +ENTRYPOINT ["/usr/local/bin/relay"] +CMD ["serve", "--runtime", "/etc/relay/runtime.yaml"] -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD ["/usr/local/bin/registry-relay", "healthcheck"] -ENTRYPOINT ["/usr/local/bin/registry-relay"] -CMD ["--config", "/etc/registry-relay/config.yaml"] +FROM debian:trixie-slim@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd AS relayctl +COPY --from=relay-builder /tooling/ / +ENTRYPOINT ["/usr/local/bin/relayctl"] diff --git a/docker/relay-runtime/Dockerfile b/docker/relay-runtime/Dockerfile deleted file mode 100644 index 999130e..0000000 --- a/docker/relay-runtime/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# syntax=docker/dockerfile:1.7 - -ARG REGISTRY_RELAY_IMAGE - -FROM rust:1.95-trixie@sha256:f49565f188ee00bc2a18dd418183f2c5f23ef7d6e691890517ed341a598f67c3 AS builder -WORKDIR /workspace/registry-stack - -COPY . . - -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/workspace/registry-stack/target \ - cargo build --release --locked \ - --package registry-relay \ - && cp target/release/registry-relay /usr/local/bin/registry-relay \ - && cp target/release/registry-relay-rhai-worker /usr/local/bin/registry-relay-rhai-worker - -FROM ${REGISTRY_RELAY_IMAGE} - -ARG REGISTRY_RELAY_IMAGE -ARG REGISTRY_STACK_SOURCE_COMMIT -LABEL org.opencontainers.image.base.name="${REGISTRY_RELAY_IMAGE}" -LABEL org.opencontainers.image.source="https://github.com/registrystack/solmara-lab" -LABEL org.opencontainers.image.revision="${REGISTRY_STACK_SOURCE_COMMIT}" - -COPY --from=builder /usr/local/bin/registry-relay /usr/local/bin/registry-relay -COPY --from=builder /usr/local/bin/registry-relay-rhai-worker /usr/local/bin/registry-relay-rhai-worker diff --git a/docker/relay/Dockerfile b/docker/relay/Dockerfile deleted file mode 100644 index 81ab4c3..0000000 --- a/docker/relay/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -ARG REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/registry-relay@sha256:71082176f00107e420ffe79471f0bd3375c3dd1ae1c3c8c3a24100c602fa82b7 -ARG VOLUME_INIT_IMAGE=busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 -FROM ${VOLUME_INIT_IMAGE} AS volume-init -FROM ${REGISTRY_RELAY_IMAGE} - -COPY --from=volume-init /bin/busybox /bin/busybox -COPY ministries /var/lib/solmara/ministries -COPY config/hosted-relay-bundles /etc/solmara/hosted-relay-bundles -COPY config/postgres/ssl/server.crt /etc/solmara/postgres/root.crt diff --git a/docker/static-metadata/Dockerfile b/docker/static-metadata/Dockerfile index bd12ba9..7163b27 100644 --- a/docker/static-metadata/Dockerfile +++ b/docker/static-metadata/Dockerfile @@ -1,4 +1,4 @@ -ARG STATIC_METADATA_IMAGE=python:3.12-alpine +ARG STATIC_METADATA_IMAGE=python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df FROM ${STATIC_METADATA_IMAGE} COPY metadata/public /srv/static diff --git a/docs/changelog.md b/docs/changelog.md index 4c42051..eb1996a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,24 @@ A small dated log of what changed in the visitor center and the lab topology. Newest entry first. +## 2026-08-12 Authority-owned Relay and Evidence reset + +Solmara now authors six independent Evidence cells and five Relay V2 projects. +CRA, NIA, and SRO publish immutable SQLite extracts for direct Evidence use; +CRA, MoSD, SIPF, and NAgDI expose only named exact Relay lookups, with NIA's +Relay reserved for the optional eSignet UserInfo profile. The programme code +routes all 11 preserved requirements to their owning authority, verifies each +ES256 JWS against that authority's JWKS, and composes application outcomes. + +The reset replaces the old singleton Evidence, Records API, ambient purpose +header, and authority decision-service topology. Mutable Relay publications and +versioned immutable extracts now have deliberately different lifecycle proof. +Registry Stack v0.19.0 was found incapable of issuing Relay-compatible Mint +claims and of representing Relay's concealed unresolved outcome in Evidence +fixtures. The reset therefore fixes forward to Registry Stack v0.20.1, pinned +by exact release source, binary checksums, and Relay OCI digest. The separately +released `esignet-relay-authenticator` v0.2.0 JAR is pinned by SHA-256. + ## 2026-08-06 Registry Stack main Evidence migration The local lab now builds Relay, Registry Evidence, and Registry Mint from the diff --git a/docs/esignet.md b/docs/esignet.md new file mode 100644 index 0000000..e8258cd --- /dev/null +++ b/docs/esignet.md @@ -0,0 +1,24 @@ +# Optional eSignet profile + +eSignet is an optional authentication profile, not an Evidence authority. The +v0.2.0 authenticator validates the challenge and consent first, then obtains a +short-lived Mint token using the `nia-esignet` private-key JWT client and calls +`POST /v2/resources/population-person/lookups/esignet-userinfo`. + +The request contains only the selected UIN and consented `fields`. The adapter +parses only `data.domainData`, preserves PSUT, KYC-token and JWS behaviour, and +collapses unresolved, concealed, denied, authentication, and dependency +failures to generic subject-facing results. It never logs selectors, tokens, or +source values. + +The deployment verifies the v0.2.0 JAR against its published SHA-256 before +building the eSignet image. The NIA Relay, Mint client, and authenticator private +JWK remain independent operator-owned runtime material. + +For hosted deployment, `compose.coolify.esignet.yaml` is applied as an overlay +on `compose.coolify.yaml`. It switches the existing Portal to the exact hosted +issuer, authorization, token, UserInfo, private-key-JWT client, and callback +configuration. The Portal client private key is provided only to Portal and the +one-time eSignet client seeder. It is separate from both the `nia-esignet` Mint +client key held by the authenticator and the `solmara-demo` Evidence client key +used by the programme application. diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index 1a690d8..1463520 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -1,285 +1,176 @@ -# Solmara Lab hosted deployment - -Status: operational runbook for the public Solmara Lab deployment. - -This guide describes the public Coolify topology without publishing private -control-plane coordinates or secret values. Keep those values in the private -operations store for the target environment. - -## Deployment model - -The hosted lab has one edge application and four authority applications. Each -authority runs one public Relay, one private consultation Relay, and one -Notary. Only the consultation Relay and Notary share a network namespace. The -public Relay remains independently routable, while the consultation Relay -binds only to loopback and is not published. Their PostgreSQL -correctness-state databases and roles remain distinct, and each Relay process -owns a persistent snapshot-cache volume. - -| Coolify app | Compose file | Authority services | -|---|---|---| -| `solmara-lab` | `compose.coolify.yaml` | Visitor Center, portal, scenario runner, static metadata, child-benefit evidence composer | -| `solmara-lab-interior` | `compose.coolify.interior.yaml` | CRA and NIA public Relay + private consultation Relay + Notary, PostgreSQL | -| `solmara-lab-social-development` | `compose.coolify.social-development.yaml` | SRO and Programme public Relay + private consultation Relay + Notary, PostgreSQL | -| `solmara-lab-labour-pensions` | `compose.coolify.labour-pensions.yaml` | SIPF public Relay + private consultation Relay + Notary, PostgreSQL | -| `solmara-lab-agriculture` | `compose.coolify.agriculture.yaml` | NAgDI public Relay + private consultation Relay + Notary, PostgreSQL | -| `solmara-lab-esignet` | `compose.coolify.esignet.yaml` | eSignet, eSignet UI and edge, eSignet PostgreSQL and Redis, seed jobs | -| `solmara-lab-wallet` | `compose.coolify.walt.yaml` | Walt holder wallet demonstrator and its backing services | - -The child-benefit evidence composer retains the service identifier -`child-benefit-federator` for existing routes. It calls the CRA, NIA, SRO, and -Programme authority Notaries and combines their responses. It is not a Notary, -does not own Notary correctness state, and does not change the six-pair -topology. - -Hosted Compose files follow these rules: - -- Coolify owns public routing, so services have no host port bindings. -- Runtime configuration is checked into the repository and mounted read-only. - Secret files are not checked in, and no configuration mount is writable. -- Cross-application calls use public HTTPS endpoints. The applications do not - share a custom Docker network. -- Hosted services run digest-pinned images and do not use `build:` blocks. -- Every authority Notary owns one PostgreSQL database, owner, migrator, and - runtime role. -- Every public and consultation Relay mounts its own named volume at - `/var/lib/registry-relay/cache`. A durable materialization publication - pointer and its immutable Parquet snapshot must survive the same restart. -- Registry Notary has no Redis service or Redis volume. `esignet-redis` belongs - only to eSignet. - -## Authority pairs and endpoints - -| Authority | Relay service and endpoint | Notary service and endpoint | -|---|---|---| -| CRA | `cra-civil-relay`, `https://cra-relay.solmara.registrystack.org` | `cra-notary`, `https://cra-notary.solmara.registrystack.org` | -| NIA | `nia-population-relay`, `https://nia-relay.solmara.registrystack.org` | `nia-notary`, `https://nia-notary.solmara.registrystack.org` | -| SRO | `sro-social-relay`, `https://sro-relay.solmara.registrystack.org` | `sro-notary`, `https://sro-notary.solmara.registrystack.org` | -| Programme | `programme-mis-relay`, `https://mosd-programme-relay.solmara.registrystack.org` | `programme-notary`, `https://programme-notary.solmara.registrystack.org` | -| SIPF | `sipf-pensions-relay`, `https://sipf-relay.solmara.registrystack.org` | `sipf-notary`, `https://sipf-notary.solmara.registrystack.org` | -| NAgDI | `nagdi-agriculture-relay`, `https://nagdi-relay.solmara.registrystack.org` | `nagdi-notary`, `https://nagdi-notary.solmara.registrystack.org` | - -The other public endpoints are: - -| Service | Endpoint | -|---|---| -| Visitor Center | `https://solmara.registrystack.org` | -| Portal | `https://portal.solmara.registrystack.org` | -| Static metadata | `https://metadata.solmara.registrystack.org` | -| Child-benefit evidence composer | `https://child-benefit-federator.solmara.registrystack.org` | -| eSignet | `https://esignet.solmara.registrystack.org` | -| eSignet UI | `https://esignet-ui.solmara.registrystack.org` | -| Walt holder wallet | `https://wallet.solmara.registrystack.org` | - -## Image model - -Registry Stack Relay and Notary image refs are inputs to the Solmara wrapper -builds. The `release-candidate` workflow requires a Registry Stack candidate -or release tag and accepts only Relay and Notary input digests that match the -committed `versions.env` pins. It also checks out the exact Registry Stack -source commit declared there for release and contract verification. Governed -attribute release is part of the canonical Registry Stack v0.15.2 Relay -image, which the workflow uses directly as the base of the deployable Solmara -Relay wrapper. Solmara does not compile or publish a feature-specific Relay -runtime. The workflow reports immutable digest refs for these Coolify -variables: - -- `SOLMARA_RELAY_IMAGE` -- `SOLMARA_NOTARY_IMAGE` -- `SOLMARA_POSTGRES_IMAGE` -- `SOLMARA_STATIC_METADATA_IMAGE` -- `SOLMARA_SCENARIO_RUNNER_IMAGE` -- `SOLMARA_HOME_IMAGE` -- `SOLMARA_PORTAL_IMAGE` -- `SOLMARA_ESIGNET_RELAY_IMAGE` -- `SOLMARA_ESIGNET_POSTGRES_IMAGE` -- `SOLMARA_ESIGNET_UI_IMAGE` -- `SOLMARA_ESIGNET_SEED_IMAGE` - -Before dispatching the workflow, run -`just review-release ` with that same tag. `just review` -remains the no-argument contributor and CI gate. - -Use `image@sha256:` values in Coolify. Do not deploy mutable tags. -`REGISTRY_STACK_PLATFORM` defaults to `linux/amd64`; override it only when the -selected Registry Stack release publishes another platform. - -## Configuration and secrets - -The six authority projects are the source of Relay and Notary runtime -configuration. Regenerate both local and hosted closures after changing a -project: - -```bash -just registry-projects-sync -just registry-projects-runtime-check +# Hosted deployment + +Deploy the authority-cell reset only from the exact Registry Stack v0.20.1 +release and digest-pinned Solmara images. `versions.env` records the public +Registry Stack digests and release-asset checksums. A missing, floating, or +mismatched pin stops deployment. + +Registry Stack v0.20.0 remains immutable but does not contain the Evidence +capability required by this lab. The reset therefore fixes forward to the +published v0.20.1 artifacts. Do not move the v0.20.0 release or substitute a +floating source reference. + +## Release package precondition + +Before the first release-candidate build, an organization owner must provision +these four public, anonymously pullable GitHub Container Registry packages and +grant the repository's GitHub Actions workflow write access: + +- `ghcr.io/registrystack/solmara-lab-evidence` +- `ghcr.io/registrystack/solmara-lab-mint` +- `ghcr.io/registrystack/solmara-lab-authority-provisioner` +- `ghcr.io/registrystack/solmara-lab-transit-signer` + +Evidence and Mint are thin Solmara-owned images assembled from the exact +checksum-verified Registry Stack v0.20.1 binaries. The authority provisioner +contains the reviewed contracts and deterministic publications. The Transit +signer contains only the signer runtime. Release handoff records the immutable +digest of each image. Do not reuse an unrelated package or deploy a mutable +tag. + +## Authority topology + +The hosted topology contains five Relay V2 services, six independently signed +Evidence cells, one shared lab Mint, and the programme application. The optional +eSignet profile uses the NIA Relay lookup. + +Evidence hosts are: + +- `cra-evidence.solmara.registrystack.org` +- `nia-evidence.solmara.registrystack.org` +- `sro-evidence.solmara.registrystack.org` +- `mosd-programme-evidence.solmara.registrystack.org` +- `sipf-evidence.solmara.registrystack.org` +- `nagdi-evidence.solmara.registrystack.org` + +`compose.coolify.provision.yaml` is a dedicated operator-only application. It +owns 34 fixed-name active volumes: + +| Owner | Volumes | Contents | +|---|---:|---| +| Shared Mint | 3 | Runtime, secrets, and Transit socket | +| Five Relays | 10 | One runtime and one mutable source volume per authority | +| Six Evidence cells | 21 | Runtime, secrets, and Transit socket per cell, plus the CRA, NIA, and SRO immutable-extract volumes | + +The same application runs seven isolated signers: one for Mint and one for each +Evidence cell. Runtime applications attach the fixed-name runtime, source, +secret, extract, and Transit volumes as external read-only volumes. Each runtime +application owns the writable audit volumes for its services and initializes +their permissions without reading or replacing existing audit records. + +## Mint clients + +The shared Mint registers nine clients under the common lab audience. Eight are +least-authority source clients, each limited to its named operation: + +- `cra-pension-evidence` +- `cra-citizen-evidence` +- `mosd-child-benefit-evidence` +- `sipf-pension-evidence` +- `sipf-survivor-evidence` +- `nagdi-voucher-evidence` +- `nagdi-livestock-evidence` +- `nia-esignet` + +The ninth client, `solmara-demo`, belongs to the programme application and is +used to request assertions from the authority Evidence cells. The shared Mint +is a lab convenience, not production tenancy guidance. + +## Secret boundary + +Create deployment secrets for these classes without placing their values in a +Compose file, repository file, build log, or delivery record: + +- one private and public signing JWK pair for Mint and for each of the six + Evidence issuers; +- one private and public client JWK pair for each of the nine Mint clients; +- authority-specific Relay audit HMAC keys, plus cursor HMAC keys for the Relay + contracts that require cursors; +- Mint and Evidence audit HMAC keys; +- one subject-binding HMAC key for each Evidence cell; +- the programme federator token; +- when eSignet is enabled, its database credential, KYC-token and PSUT HMAC + secrets, KYC keystore credentials, and portal OIDC client key. + +Each signing private JWK is projected only to its dedicated signer. That signer +also receives the matching public half and refuses startup unless it is the +exact projection of the private key. A Mint +client private JWK is installed only for its client owner, and an +authority-scoped one-shot provisioner may write it only into that owner's +secret volume. Inject public JWK halves into the provisioner and their matching +signers only; the provisioner writes +the issuer projections and Mint client registrations into the generated +runtime material. No runtime receives another authority's client private key. + +Never commit a private key, token, generated database, generated Relay package, +runtime bundle, or audit log. + +## Immutable extract lifecycle + +CRA birth, NIA population, and SRO poverty use immutable SQLite extracts. The +hosted provisioner creates the initial checked publication and reuses the exact +active filename on a restart. It never overwrites an active extract in place. + +To publish a later checkpoint, override the matching direct-cell provisioner +service command with: + +```text +publish-extract --target -evidence --assets /opt/solmara-hosted-assets --runtime-output /provisioned/runtime --extract-output /provisioned/extracts ``` -Hosted Relay does not boot those compiler outputs as unsigned non-local -configuration. Each public and consultation Relay carries its own -instance-bound signed Config Bundle, public trust anchor, and anti-rollback -state, with no private signing key. The public bundle excludes private -consultation artifacts; the consultation bundle contains the complete -consultation closure. Verify that every signed closure still projects exactly -to the compiler output: - -```bash -just hosted-relay-bundles-check -``` - -When a hosted Relay config or artifact changes, keep the private JWK in -1Password and pass its `op://` secret reference, together with the public-only -JWK file, to `scripts/generate-hosted-relay-bundles.py`. Registryctl reads the -private member directly through 1Password CLI without writing it into the -repository or a working-tree file. When multiple 1Password accounts are signed -in, select the account with `OP_ACCOUNT` and use vault and item IDs in the -secret reference so the lookup is unambiguous. Increment -`SOLMARA_RELAY_BUNDLE_SEQUENCE` in `versions.env`, generate into a new staging -directory, review the config and manifest diffs, then replace the committed -bundle set. The generator creates separate public and consultation streams for -each authority, and the verification gates require all twelve bundles to use -that exact sequence. Never commit the private JWK. A first deployment seeds -sequence zero only into an empty matching Relay cache volume; successful Relay -startup audits and persists the signed sequence. Later image rollback cannot -lower that durable sequence. Publish a higher signed sequence for a normal -rollback, or use Registry Relay's reviewed break-glass procedure. - -Each authority application needs only the variables referenced by its Compose -file. At minimum, provide: - -- The required digest-pinned `SOLMARA_*_IMAGE` refs. -- `SOLMARA_POSTGRES_PASSWORD` and the source database URL used by NIA or SIPF - when that authority owns a PostgreSQL-backed source projection. -- Three Relay consultation-state credentials per authority: runtime, keyring - maintenance, and keyring reader. -- Two Notary state credentials per authority: migrator and runtime. -- Separate Relay and Notary audit hash secrets for every authority. -- The client token hashes and signing keys named by that authority's generated - runtime configuration. -- The Relay audit pseudonym key and consultation-state retention values named - by the Compose file. - -For example, the CRA app uses `CRA_RELAY_POSTGRES_RUNTIME_PASSWORD`, -`CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD`, -`CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD`, -`CRA_NOTARY_POSTGRES_MIGRATOR_PASSWORD`, and -`CRA_NOTARY_POSTGRES_RUNTIME_PASSWORD`. Other authorities use the same suffixes -with `NIA`, `SRO`, `PROGRAMME`, `SIPF`, or `NAGDI`. - -The core application holds only the client tokens it needs to call the six -authority services. It does not receive authority database credentials. -eSignet and Walt credentials stay in their own applications. - -Do not print full environment dumps while deploying. Coolify responses can -include secret values when the caller has sensitive read access. - -## Workload identity prerequisite - -Each hosted authority application runs a loopback-only workload agent that -rotates a five-minute Relay token into its authority-owned named volume. The -Notary runtime and state installer mount that volume read-only and wait for the -agent to become healthy. Private workload JWKs enter only as Coolify production -secrets; they are not present in images, public metadata, or Compose defaults. -A missing, malformed, or expired token keeps the dependent workload unready. - -The public half of every hosted workload key is committed at -`metadata/public/.well-known/jwks.json` and served separately at -`https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json`. -Relay tokens use that HTTPS origin as `iss`, while the token agents remain -bound to container loopback. Keep the public JWKS and all corresponding -Coolify private JWKs in one reviewed rotation. - -The eSignet application has a separate NIA identity with -`azp=solmara-esignet`, `sub=solmara-esignet`, and exactly the -`population:identity_release` scope. Its agent writes -`solmara-esignet-relay-token` as UID/GID `1001:1001` into an eSignet-owned -volume. The eSignet plugin rereads that file for each Relay request, so normal -rotation does not require an eSignet restart. Do not copy the token itself into -a Coolify environment variable or reuse the NIA Notary identity. - -## Coolify application setup - -For each application: - -1. Select this repository and the exact commit being deployed. -2. Use Docker Compose as the build pack. -3. Select the application's `compose.coolify*.yaml` file. -4. Disable generated domains and shared custom networks. -5. Add only the production variables referenced by that Compose file. -6. Attach domains to the exact service names in the endpoint tables. -7. Deploy authority applications before the edge application. - -Run the eSignet seed job once before expecting portal login to pass. The -authority PostgreSQL bootstrap and state-install jobs are idempotent and run as -part of their Compose dependency graph. - -## Deployment verification - -From a trusted shell with the demo client tokens available through `.env` or -the process environment: - -```bash -just hosted-smoke -``` - -The smoke checks all six Relay and Notary endpoints, authority evidence -journeys, purpose denial, the Visitor Center proxy, and the portal backend. -Set `SOLMARA_HOSTED_SMOKE_BROWSER=1` to add hosted browser coverage. - -For each authority, also verify: - -1. Relay `/ready` returns success. -2. Notary `/ready` returns success only after its Relay and database checks. -3. One representative authority evidence request succeeds. -4. A wrong-purpose request is denied without returning the prohibited field. -5. Restarting the Notary preserves correctness state and readiness. - -## PostgreSQL operations - -The bootstrap container creates only the authority keys listed in -`SOLMARA_RELAY_DATABASES` and `SOLMARA_NOTARY_DATABASES`. The serving Relay and -Notary receive runtime credentials only. Schema installation uses dedicated -jobs and never gives migration credentials to a serving process. - -Registry Stack v0.15.2 uses the `v015` Relay state epoch from `versions.env`. -The retained consultation-result schema does not change, but the hosted -deployment now persists every Relay cache that backs a PostgreSQL -materialization publication pointer. Earlier `v013` deployments did not -persist those immutable snapshot files, so reusing their pointers after -container replacement can leave consultation profiles permanently -unavailable. Quiesce old Relay writers, keep the `v013` databases for -rollback, and follow the complete stopped-writer procedure in -[`notary-postgresql-state.md`](notary-postgresql-state.md). - -Back up, restore, and upgrade each Notary database independently. See -[`notary-postgresql-state.md`](notary-postgresql-state.md) for the database map -and recovery sequence. Preserve the eSignet Redis volume separately because it -is outside the Notary state boundary. - -## Troubleshooting - -### Notary stays unready - -Check the matching Relay `/ready`, PostgreSQL health, state-installer exit -status, and workload-token freshness. A Notary intentionally remains unready -when a required Relay profile cannot be verified. - -### State installer fails - -Confirm that the authority key appears in `SOLMARA_NOTARY_DATABASES`, that both -authority Notary passwords are set, and that the wrapper and product images -come from the same release. Do not pass the migrator URL to the serving Notary. - -### Hosted Notary calls a private service name - -Regenerate the hosted project closure with `just registry-projects-sync`. -Hosted Relay source URLs must use the public HTTPS domains in the authority -table. A URL such as `http://cra-civil-relay:8080` cannot cross Coolify -applications. - -### Coolify routes the wrong container port - -Route the Relay hostname to port `8080` on the public Relay service. Route the -Notary hostname to port `8081` in the private consultation Relay namespace. -Never route consultation Relay port `8080`; it is explicitly bound to -`127.0.0.1` for Notary-only access. +The operation validates the current binding, appends the checked publication +under a fresh immutable filename, preserves the old file, and atomically +rebinds only that cell's runtime configuration. Restart only the matching +Evidence cell after the operation succeeds. The running cell continues to read +the old mounted extract until that restart. A malformed, metadata-mismatched, +or non-newer publication fails closed. + +## Deployment order + +Deploy the reset alongside the existing deployment in this order: + +1. Record the old deployment's exact image references, routes, and retained + volume names. Do not attach an old writer to a new source volume. +2. Deploy `compose.coolify.provision.yaml`. Require every one-shot provisioner + to complete successfully and all seven signers to become healthy. +3. Deploy the authority runtime applications from + `compose.coolify.interior.yaml`, + `compose.coolify.social-development.yaml`, + `compose.coolify.labour-pensions.yaml`, and + `compose.coolify.agriculture.yaml`. Confirm all five Relays and all six + Evidence cells are ready on the private routes. +4. Deploy the shared Mint, programme services, portal, Visitor Center, and + static metadata from `compose.coolify.yaml`. +5. Run the hosted programme, denial, JWKS, source-label, redaction, and UI smoke + against the new routes before changing public routing. +6. If required for this deployment, add `compose.coolify.esignet.yaml` and run + the citizen-login smoke through the NIA Relay V2 lookup. +7. Switch programme authority URLs and public metadata routing only after every + required smoke passes. Disable the superseded services, but retain their + volumes and exact deployment references for recovery. + +Rollback restores routing to the old services and their retained volumes. It +does not reuse a new Relay V2 database with an older binary. Removing a +superseded service or volume is a separate approved cleanup. + +## Hosted acceptance + +Acceptance is based on live hosted behavior, not inferred from local tests. It +requires: + +- child benefit to compose four independently signed assertions and return the + expected five positive concepts; +- pension to compose CRA and SIPF assertions without disclosing cause of death + or unrelated civil data; +- both agriculture journeys to succeed through their NAgDI lookups; +- wrong-purpose and unauthorized calls to fail generically; +- the portal to prove authority URL selection, per-authority JWKS verification, + source-type labels, and redaction; +- the Visitor Center to show the publisher, six Evidence cells, five Relays, + shared Mint, and programme application; +- when the optional profile is deployed, eSignet login to complete through the + NIA Relay V2 lookup. + +Attach only sanitized pass or fail results and public artifact identities to +the delivery record. Do not attach selectors, source rows, tokens, private +audit material, secrets, deployment logs, or private release evidence. diff --git a/docs/notary-postgresql-state.md b/docs/notary-postgresql-state.md deleted file mode 100644 index 9842748..0000000 --- a/docs/notary-postgresql-state.md +++ /dev/null @@ -1,192 +0,0 @@ -# Notary PostgreSQL state - -Solmara Lab runs exactly one Registry Notary beside each authority Relay. Each -Notary owns an independent PostgreSQL database and role set. PostgreSQL servers -may be shared within a local or hosted Compose application, but databases, -owners, migrators, and runtime roles are never shared between Notaries. - -| Authority | Public Relay / private consultation Relay | Notary service | Relay state database | Notary database | Local Relay / Notary | -|---|---|---|---|---|---| -| Civil Registration Authority (CRA) | `cra-civil-relay` / `cra-civil-relay-consultation` | `cra-notary` | `solmara_relay_cra_consultation_v015` | `solmara_notary_cra` | `4311` / `4325` | -| National Identity Agency (NIA) | `nia-population-relay` / `nia-population-relay-consultation` | `nia-notary` | `solmara_relay_nia_consultation_v015` | `solmara_notary_nia` | `4312` / `4326` | -| Social Registry Office (SRO) | `sro-social-relay` / `sro-social-relay-consultation` | `sro-notary` | `solmara_relay_sro_consultation_v015` | `solmara_notary_sro` | `4313` / `4327` | -| Programme MIS | `programme-mis-relay` / `programme-mis-relay-consultation` | `programme-notary` | `solmara_relay_programme_consultation_v015` | `solmara_notary_programme` | `4314` / `4328` | -| Social Insurance and Pensions Fund (SIPF) | `sipf-pensions-relay` / `sipf-pensions-relay-consultation` | `sipf-notary` | `solmara_relay_sipf_consultation_v015` | `solmara_notary_sipf` | `4315` / `4322` | -| National Agricultural Data Institute (NAgDI) | `nagdi-agriculture-relay` / `nagdi-agriculture-relay-consultation` | `nagdi-notary` | `solmara_relay_nagdi_consultation_v015` | `solmara_notary_nagdi` | `4316` / `4323` | - -The local topology shares one PostgreSQL server for developer convenience. -Hosted authority applications keep the same database boundaries within their -own PostgreSQL volume. The runtime role for an authority follows the form -`solmara_notary__runtime`; the owner and migrator roles use the same -authority key. - -The named `postgres-data` volume is mounted directly at PostgreSQL's -`/var/lib/postgresql/data` data directory. Keep that exact mount target while -the topology uses PostgreSQL 16. Mounting the parent directory allows the -image's declared data-directory volume to become anonymous, which would discard -authority state when Compose removes the PostgreSQL container. `just down` -followed by `just up` preserves the named data volume; only `just reset` -deliberately deletes it. - -Run `just notary-state-restart-proof` after a representative live smoke. The -gate records the PostgreSQL system identifier and every correctness-table row -count in all six authority databases, performs the exact `just down` and -`just up` lifecycle, rejects anonymous PGDATA mounts, compares the state before -any new requests are sent, and reruns `state doctor` for every Notary. - -## Startup and readiness - -The startup order is intentional: - -1. `registry-postgresql-bootstrap` creates or attests only the databases and - roles listed by that Compose application. -2. Each private consultation Relay bootstraps its PostgreSQL state before its - serving process starts. -3. The authority workload issuer publishes the verification keys and writes a - short-lived Notary token. Local public and consultation Relay namespaces - have separate loopback-only issuer processes; hosted Relays validate against - the separately served HTTPS JWKS. -4. Each `-notary-state-install` job applies or attests the released - Notary schema with the migrator role once its database and token file are - ready. -5. The matching Notary starts with only its runtime database role and a - read-only workload-token mount. The public Relay starts independently from - the consultation state plane. - -Each Notary shares only its private consultation Relay's network namespace. -The consultation Relay binds `127.0.0.1:8080` and Notary binds port `8081`, so -the consultation path has a direct loopback trust boundary. The separately -routable public Relay never receives Notary consultation traffic. Readiness -remains unavailable until PostgreSQL state and required Relay source profiles -are usable. - -Relay snapshot caches are not Notary correctness state, but they are durable -Relay restart data. Each public and consultation Relay mounts a distinct -`/var/lib/registry-relay/cache` volume so one process's materialization pointer -never outlives its referenced immutable snapshot. - -Inspect one local pair without exposing credentials: - -```bash -curl --fail http://127.0.0.1:4311/ready -curl --fail http://127.0.0.1:4325/ready -docker compose run --rm --no-deps cra-notary \ - --config /etc/registry-notary/notary.yaml state doctor -``` - -Substitute the ports and service name from the table for another authority. - -## Backup and restore - -Back up each Notary database as a complete unit. Do not dump individual tables -or merge databases from different authorities. - -Before an upgrade or recovery drill: - -1. Record the deployed Registry Notary image digest and config revision. -2. Take a consistent PostgreSQL backup of every authority Notary database. -3. Back up the database role credentials in the secret manager, separately - from the database backup. -4. Verify restore into an isolated PostgreSQL server with the same major - version. -5. Run `state doctor` with the restored runtime configuration before sending - traffic. - -For a restore, stop all writers for that authority, restore the complete -database, restore the matching credentials, deploy the recorded Notary image -and config, and run `state doctor`. Reopen traffic only after readiness and a -representative authority scenario pass. - -## Upgrades and rollback - -Treat the schema installer as a release step, not a serving-container -permission. For each authority: - -1. Stop or drain its Notary replicas. -2. Take and verify a complete database backup. -3. Deploy the target PostgreSQL and Notary images. -4. Let the authority's state installer finish successfully. -5. Start serving replicas, run `state doctor`, and verify `/ready` and an - authority scenario. - -Do not run an older Notary binary against a forward-migrated schema. If an -upgrade cannot be completed, restore the pre-upgrade database and the matching -image and configuration together. The normative product procedure is the -[Registry Notary PostgreSQL state operations guide](https://github.com/registrystack/registry-stack/blob/main/products/notary/docs/postgresql-state-operations.md). - -### Registry Stack v0.15.2 cache-persistence cutover - -The v0.15.2 deployment introduces distinct persistent cache volumes for every -public and consultation Relay. The earlier hosted topology persisted -PostgreSQL materialization publication pointers while keeping their immutable -Parquet snapshots in ephemeral container storage. After container -replacement, a surviving pointer can therefore name a snapshot that no -longer exists. - -Solmara establishes a recoverable boundary with -`REGISTRY_RELAY_STATE_EPOCH=v015` in `versions.env`. The bootstrap creates new -Relay databases and roles without deleting or rewriting the `v013` state. -For the cutover: - -1. Stop new authority traffic and stop every `v013` Relay writer. -2. Back up the `v013` databases and retain the matching images and - configuration. -3. Deploy the v0.15.2 Compose closure. Bootstrap creates the `v015` Relay - databases, then each Relay publishes fresh materializations into its - persistent cache. -4. Require every Relay and Notary `/ready` check, the complete smoke suite, - and the restart-persistence proof before reopening traffic. - -Keep the `v013` databases quiesced during the rollback window. Rollback means -restoring the matching pre-cutover deployment as one unit. Never point the -new cache-backed deployment at a `v013` database whose referenced snapshot -files were not preserved. - -### Registry Stack v0.13.0 cutover - -Registry Stack v0.13.0 removes `provenance.consent` from the exact retained -Relay result contract. Old and v0.13.0 Relay binaries must not share a -consultation state plane. Solmara enforces that boundary with -`REGISTRY_RELAY_STATE_EPOCH=v013` in `versions.env`. The PostgreSQL bootstrap -uses the epoch in every Relay database and role name and runs idempotently on -fresh and existing clusters. - -For the v0.10.0 to v0.13.0 cutover: - -1. Stop new authority traffic and stop every old Relay and Notary writer. -2. Drain retained terminal replay lifetimes, then back up every old Relay and - Notary database with its exact image and configuration refs. -3. Deploy the v0.13.0 Compose closure. The bootstrap creates the new `v013` - Relay databases and roles, and each Relay bootstraps its empty state plane. - The Notary installers migrate or attest the existing Notary databases. -4. Require every Relay and Notary `/ready` check, `state doctor`, the complete - local or hosted smoke, and the PostgreSQL restart-persistence proof before - reopening traffic. - -Keep the old, unsuffixed Relay databases quiesced until the rollback window -closes. A rollback restores the pre-upgrade Notary backups and matching -v0.10.0 images and configs, then reconnects the old Relay binaries only to the -old Relay databases. Never point a v0.13.0 Relay at an unsuffixed database or -an old Relay at a `v013` database. - -The local topology deliberately pins PostgreSQL 16 and its -`/var/lib/postgresql/data` mount layout. Do not change the image tag to 18 in -place. PostgreSQL 18's official container layout mounts the parent -`/var/lib/postgresql` directory and places data under a major-specific child. -The [official PostgreSQL container documentation](https://github.com/docker-library/docs/blob/master/postgres/README.md#pgdata) -defines the exact paths. A move to 18 therefore requires a stopped-writer -`pg_upgrade` or verified dump/restore into a newly created PostgreSQL 18 volume, -followed by all six `state doctor` checks and the restart-persistence gate. -Recreating the container against the PostgreSQL 16 volume without that -procedure is not an upgrade. - -## Redis retirement - -Registry Notary has no production Redis dependency in Solmara Lab. The pre-1.0 -cutover deliberately has no importer or dual-write mode. Old purpose-specific -Notary and citizen-issuer Redis volumes are retired and must not be attached to -the six authority Notaries. - -`esignet-redis` in `compose.esignet.yaml` and -`compose.coolify.esignet.yaml` belongs to eSignet. It is not Notary correctness -state and remains part of the eSignet deployment. diff --git a/docs/purposes.md b/docs/purposes.md index 9fc21d1..2935d6b 100644 --- a/docs/purposes.md +++ b/docs/purposes.md @@ -1,70 +1,33 @@ -# Solmara Purpose Catalogue +# Solmara purpose catalogue -Status: normative for the local Registry Evidence deployment. +Status: normative for the authority-owned Evidence reset. -Registry Evidence requests use the closed purpose codes below. The reviewed -source adapters translate those grants into fixed `Data-Purpose` IRIs when they -call each authority's Records API. A caller cannot supply or override those -downstream headers. +Evidence requests carry one closed purpose code in the signed request body. +For Relay-backed requirements, the Evidence cell authenticates to Registry Mint +with a dedicated private-key JWT client. Mint issues a token whose registration +fixes the full purpose IRI, exact Relay scope, and `solmara-runtime` audience. +The caller cannot select or override downstream Relay authority. -## Wave 1 Purposes - -| Purpose code | Advertised by | Enforced by | Story | Denial problem codes | -|---|---|---|---|---| -| `child-benefit-review` | Civil Registration Authority, National Identity Agency, Social Registry Office, MoSD programme MIS | Registry Evidence grants and four reviewed requirements | Birth to child benefit | `not_authorized` | -| `pension-payment-review` | Civil Registration Authority, Social Insurance and Pensions Fund | Registry Evidence grants and CRA/SIPF requirements | Death to pension stop | `not_authorized` | -| `survivor-benefit-determination` | Social Insurance and Pensions Fund | Registry Evidence grant and SIPF survivor requirement | Survivor benefit | `not_authorized` | -| `voucher-eligibility-review` | National Agricultural Data Institute | Registry Evidence grant and NAgDI voucher requirement | Farmer climate-smart voucher | `not_authorized` | -| `livestock-movement-control` | National Agricultural Data Institute | Registry Evidence grant and NAgDI livestock requirement | Livestock movement permit companion | `not_authorized` | -| `citizen-self-service` | Civil Registration Authority and National Identity Agency | Registry Evidence grants and CRA/NIA citizen requirements | Citizen portal | `not_authorized` | - -## Purpose Rules - -`child-benefit-review` permits evidence needed to determine whether a child may -be enrolled in child support: registered birth, age under five, active -population record, household priority band, and duplicate enrollment status. -It does not permit raw poverty scores, complete household profiles, or unrelated -civil events. - -`pension-payment-review` permits an application to combine the fact of death -with an active-payment value. It does not permit cause of death, medical detail, -payment amount, or payment history. - -`survivor-benefit-determination` permits the reviewed survivor eligibility -value. It does not permit a complete marriage record, contribution history, or -unrelated benefits. - -`voucher-eligibility-review` permits farmer registration, data-use -authorization, and voucher eligibility values. It does not permit workbook rows, -unrelated livestock movements, or raw market-sizing data. - -`livestock-movement-control` permits registered-herd, quarantine, and movement -eligibility values. It does not permit voucher budgets, crop records, or -household poverty data. - -`citizen-self-service` permits separate CRA linkage and NIA active-population -values for the selected persona. It does not permit bulk reads, -administrative-only fields, or evidence for another selected persona. - -## Evidence Outputs - -| Story | Registry Evidence requirements | Output | +| Purpose code | Authority requirements | Evidence source | |---|---|---| -| Birth to child benefit | CRA, NIA, SRO, and MoSD child-benefit requirements | Four flattened signed JWS assertions collected by the application | -| Death to pension stop | CRA death and SIPF active-payment requirements | Two flattened signed JWS assertions and an application-owned stop decision | -| Survivor benefit | SIPF survivor requirement | One flattened signed JWS assertion | -| Farmer climate-smart voucher | NAgDI voucher requirement | One flattened signed JWS assertion | -| Livestock movement permit | NAgDI livestock requirement | One flattened signed JWS assertion | -| Citizen self-service | CRA and NIA citizen requirements | Two flattened signed JWS assertions | - -## Denial Assertions - -Purpose-denial tests assert: - -1. The request names a purpose not granted for the selected requirement. -2. Registry Evidence returns HTTP 403 and `not_authorized`. -3. No source row or prohibited field appears in the response. - -The child-benefit collector proves that one application can collect four -independently governed signed assertions without owning a shared eligibility -rule or copying authority rows. +| `child-benefit-review` | CRA, NIA, SRO, MoSD | three immutable extracts, one Relay lookup | +| `pension-payment-review` | CRA death, SIPF payment | two Relay lookups | +| `survivor-benefit-determination` | SIPF survivor | Relay lookup | +| `voucher-eligibility-review` | NAgDI voucher | Relay lookup | +| `livestock-movement-control` | NAgDI livestock | Relay lookup | +| `citizen-self-service` | CRA link, NIA status | one Relay lookup, one immutable extract | + +Child benefit permits only registered-birth, under-five, active-population, +poverty-priority, and not-already-enrolled concepts. Pension permits death and +active-payment concepts but not cause of death, payment amount, or history. +Survivor review permits the reviewed eligibility value, not a marriage record. +Agriculture purposes are isolated from one another. Citizen self-service permits +only CRA linkage and NIA active-population concepts. + +Wrong-purpose or unauthorized requests fail generically. Relay no-match, +ambiguous match, and concealed records collapse to unresolved consultation. +Responses and logs never include selectors, tokens, source rows, secrets, +private keys, audit material, or sensitive dependency details. + +The programme application verifies every authority JWS and owns the final +cross-authority outcome. No Evidence cell returns an application decision. diff --git a/docs/solmara.md b/docs/solmara.md index a9a70a0..2ce0691 100644 --- a/docs/solmara.md +++ b/docs/solmara.md @@ -1,173 +1,75 @@ -# Republic of Solmara +# Solmara lab architecture -Status: normative for Solmara Lab wave 1. +Solmara is a fictional institutional lab. Its core boundary is an authority +cell: an authority publishes a governed source, operates its Evidence service, +signs its assertions, and owns its audit trail. A programme consumes assertions +from several cells and owns the programme decision. -Solmara is a fictional country created for Registry Stack demonstrations. Any -person, place, ministry, registry, identifier, service, and government story in -this lab is synthetic unless explicitly labelled as an external partner system. -No Solmara address, person, ministry, or registry represents a real authority. +## Runtime topology -## Nation - -The Republic of Solmara is a small cosmopolitan island republic in the tropical -South Indian Ocean, roughly 9.5 to 11 degrees south and 79 to 80.5 degrees east. -It has one main island plus smaller islands, about 24,000 square km, and a -notional population of 2.4 million. - -The nearest real land is about 900 km away in every direction, including Diego -Garcia, Addu Atoll, Sri Lanka, and the Cocos Islands. Country-scale basemaps -therefore show open water around Solmara, and no real address should ever fall -inside the lab bounding box. - -Solmara uses the ISO 3166 user-assigned codes `XS` and `XSO`. Lab currency uses -the ISO 4217 testing code `XTS`, displayed as the Solmara sol. - -## Administrative Areas - -Solmara has two administrative levels: 4 provinces and 12 districts. Province -P-codes use `XS-01` through `XS-04`; district P-codes use `XS-0101` style. - -| Province code | Province | Position | District code | District | -|---|---|---|---|---| -| `XS-01` | Anvela | North | `XS-0101` | Ketterin | -| `XS-01` | Anvela | North | `XS-0102` | Ovasse | -| `XS-01` | Anvela | North | `XS-0103` | Brenholm | -| `XS-02` | Tolara | South | `XS-0201` | Salvet | -| `XS-02` | Tolara | South | `XS-0202` | Marindi | -| `XS-02` | Tolara | South | `XS-0203` | Velcor | -| `XS-03` | Mendira | Central, capital province | `XS-0301` | Lydessa | -| `XS-03` | Mendira | Central, capital province | `XS-0302` | Orivale | -| `XS-03` | Mendira | Central, capital province | `XS-0303` | Carrowen | -| `XS-04` | Corvala | East | `XS-0401` | Eastmere | -| `XS-04` | Corvala | East | `XS-0402` | Navaro | -| `XS-04` | Corvala | East | `XS-0403` | Vestrel | - -These names replace legacy compass districts and any real-country agriculture -geography from the previous NAgDI lab material. - -## Coordinate Reference System - -The hand-authored Solmara geography uses one hemisphere and one UTM zone. Source -GeoJSON is authored in WGS 84 longitude and latitude (`EPSG:4326`). Metric -derivations for area, centroids, and tiling checks use WGS 84 / UTM zone 44S -(`EPSG:32744`). - -The country, provinces, and districts are maintained as one authoritative geo -source. Districts exactly tile provinces, provinces exactly tile the country, -and point fixtures must fall inside their declared district. Relay serves -district geometries only when a story needs boundaries; default story surfaces -prefer names, codes, and minimized predicates. - -## Identifier Formats - -| Scheme | Format | Example | +| Authority | Evidence source | Relay operation | |---|---|---| -| SolmaraID UIN | 10 digits, MOSIP-style Verhoeff checksum, no leading 0 or 1, no long runs or repeats, excluding sequences 786 and 666 | `2300010248` | -| Birth registration | `BRN---` | `BRN-2016-0101-00213` | -| Death registration | `DRN---` | `DRN-2026-0301-00042` | -| Marriage registration | `MRN---` | `MRN-1988-0301-00117` | -| Taxpayer | 9 digits plus check letter | `104238756K` | -| Business | org-id style jurisdiction-list code | `XS-SBRS-0042317` | -| Cadastre parcel | `XS-CAD--` | `XS-CAD-0101-0087` | -| Health facility | `XS-MOH-` | `XS-MOH-0231` | -| Farmer | `FR-` | `FR-1001` | -| Household | `HH-` | `HH-002317` | - -Registry-local row-id prefixes are normative when used by the owning registry -model: `CP-`, `BE-`, `DE-`, `ME-`, `MT-`, `CSR-`, `SEP-`, `SCOR-`, `ENR-`, -`ENT-`, `PAY-`, `SIPF-`, `SIPF-AWD-`, `XS-LP-`, `XS-BAU-`, `XS-DLG-`, -`XS-SRA-TCC-`, `XS-DR-`, `XS-SCH-`, `XS-LRN-`, and `XS-MOH-ORG-`. - -Legacy national-id aliases remain only for migration and story porting. They -use the old population prefix and are not new primary identifiers. - -## Domain Scheme - -Solmara uses three domain layers: - -| Layer | Pattern | Use | -|---|---|---| -| Story domains | `*.gov.solmara.example` | Fictional ministry and service names in UI copy, docs, fixtures, and tutorials. These do not resolve. | -| Machine identifiers | `https://id.registrystack.org/solmara/...` | Purpose IRIs, credential `vct` values, schema `$id` values, and issuer identifiers. | -| Service endpoints | `.solmara.registrystack.org` | Real TLS endpoints for hosted lab services. | - -Notary node identifiers use `did:web` at their authority service host, for -example `did:web:cra-notary.solmara.registrystack.org`. Credential holder -binding uses `did:jwk`. - -## Ministries And Registries - -Solmara names 17 registries, but only builds registries when a shipped story -needs them. Wave 1 runs seven registries. - -| # | Registry | Owner | Tier | Wave | -|---|---|---|---|---| -| 1 | Civil registration: births, deaths, marriages | Ministry of Interior, Civil Registration Authority | Rows, Relay, Notary source | 1 | -| 2 | Population register / national ID | Ministry of Interior, National Identity Agency | Rows, Relay, Notary source | 1 | -| 3 | Social registry: households | Ministry of Social Development, Social Registry Office | Rows, Relay, Notary source | 1 | -| 4 | Integrated beneficiary registry | Ministry of Social Development, programme MISes | Rows, Relay, Notary source | 1 | -| 5 | Pensions / social insurance | Ministry of Labour, Social Insurance and Pensions Fund | Rows, Relay, Notary source | 1 | -| 6 | Farmer registry | Ministry of Agriculture, National Agricultural Data Institute | Rows, Relay, Notary source | 1 | -| 7 | Livestock registry | Ministry of Agriculture, National Agricultural Data Institute | Rows, Relay, Notary source | 1 | -| 8 | Land registry and cadastre | Ministry of Lands and Survey | Rows, Relay, Notary source | 2 | -| 9 | Taxpayer registry | Ministry of Finance, Solmara Revenue Authority | Rows, Relay, Notary source | 2 | -| 10 | Business / company registry | Ministry of Justice and Commerce, Solmara Business Registration Service | Rows, Relay, Notary source | 2 | -| 11 | Beneficial ownership register | Solmara Business Registration Service | Thin fixtures, evidence only | 2 | -| 12 | Disability registry | Ministry of Social Development, Disability Assessment Board | Thin fixtures, evidence only | 3 | -| 13 | Education learner registry | Ministry of Education | Thin fixtures, evidence only | 3 | -| 14 | Health facility registry | Ministry of Health | Thin fixtures, evidence only | 3 | -| 15 | Patient / immunization registry | Ministry of Health | World bible only | None | -| 16 | Vehicle and driving licence registry | Ministry of Transport | World bible only | None | -| 17 | Customs trader registry | Ministry of Finance, Customs Service | World bible only | None | - -Wave 1 runs one authority Notary beside each Relay: `cra-notary`, `nia-notary`, -`sro-notary`, `programme-notary`, `sipf-notary`, and `nagdi-notary`. An -authority Notary exposes every reviewed evidence workflow owned by that -authority. It is not duplicated per purpose. The child-benefit orchestration -service composes the four required authority responses but does not own Notary -correctness state or make the final eligibility decision. - -## Persona Roster - -This roster is the normative named cast for wave 1 docs and smoke expectations. -Identifiers may be regenerated later, but the persona role and expected outcome -must stay stable. - -| Persona | Primary id | District | Wave 1 expected outcome | -|---|---|---|---| -| Mateo Santos | `2300010248` | Ketterin | Child benefit positive child: registered birth, active population record, under 5, eligible household, not enrolled. | -| Elena Dela Cruz | `2300018263` | Ketterin | Mateo guardian: may request the source-owned predicate bundle for programme review. | -| Luis Okafor | `2300027390` | Ketterin | Household head for child benefit positive path. | -| Hana Aquino | `2300036523` | Lydessa | Child benefit denied: household above poverty threshold. | -| Priya Mensah | `2300045650` | Lydessa | Guardian for above-threshold child household. | -| Tomas Bello | `2300054788` | Orivale | Child benefit denied: duplicate enrollment already active. | -| Joana Bello | `2300063915` | Orivale | Guardian for duplicate-enrollment control. | -| Karim Kone | `2300073046` | Marindi | Child benefit routed to birth registration first: UIN exists, BRN is null. | -| Aisha Kone | `2300082172` | Marindi | Guardian for unregistered-birth inclusion path. | -| Esteban Cruz | `2300091305` | Velcor | Deceased child-benefit control: eligibility must fail on life status. | -| Miriam Cruz | `2300100431` | Velcor | Guardian for deceased control path. | -| Rafael Nkomo | `2300109568` | Lydessa | Pension positive deceased member: payments should stop. | -| Imani Nkomo | `2300118698` | Lydessa | Survivor benefit positive spouse linked by MRN. | -| Otto Ferreira | `2300127827` | Orivale | Pension stale-data failure: death not yet registered. | -| Lucia Ferreira | `2300136959` | Orivale | Survivor claim waits for death reconciliation. | -| Mina Rahman | `2300146081` | Carrowen | Survivor denied: marriage dissolved. | -| Pavel Rahman | `2300155218` | Carrowen | Deceased former spouse for dissolved-marriage control. | -| Amina Kone | `FR-1001` | Brenholm | Farmer voucher positive path and livestock movement positive owner. | -| Diego Santos | `FR-1002` | Ovasse | Farmer voucher denied: no data-use authorization on file. | -| Noor Patel | `FR-1003` | Navaro | Farmer voucher denied: already redeemed this season. | -| Beatriz Okafor | `FR-1004` | Eastmere | Livestock movement denied: species-specific quarantine. | -| Sefu Dela Cruz | `FR-1005` | Vestrel | Livestock movement denied: incomplete vaccination evidence. | - -## Registry Landscape - -The wave 1 registry landscape demonstrates cross-registry life-event services: - -| Story | Registries | Outcome | -|---|---|---| -| Birth to child benefit | Civil registration, population, social registry, beneficiary registry | Four authority predicate responses composed for programme eligibility review. | -| Death to pension stop plus survivor benefit | Civil registration, population, pensions, beneficiary registry | Stop predicate for the deceased member and survivor eligibility VC for the spouse. | -| Farmer climate-smart voucher and livestock movement | Farmer registry, livestock registry | Voucher eligibility credential and livestock movement permit evidence. | - -Every wave 1 story must show metadata discovery, governed evaluation, a -credential or composed evidence moment, a forbidden raw read or wrong-purpose -attempt, and a denial with a stable problem code. +| CRA | immutable birth extract | civil death and citizen-link exact lookups | +| NIA | immutable population extract | eSignet UserInfo exact lookup | +| SRO | immutable poverty extract | none | +| MoSD Programme MIS | live read-only SQLite | beneficiary enrolment exact lookup | +| SIPF | live read-only SQLite | pension payment and survivor exact lookups | +| NAgDI | live read-only SQLite | voucher and livestock exact lookups | + +There are six Evidence cells and five Relays. A Relay exists only for governed +live consultation. It is not a façade over an Evidence cell's immutable local +extract. + +Every Relay operation has one dedicated Mint client registration, exact scope, +fixed purpose claim, access profile, and disclosure profile. Relays do not +offer list, search, cursor, or generic record routes. The shared +`solmara-runtime` audience is deliberately limited to this lab. + +## Evidence authority + +Each cell has a unique provider IRI, authority DID issuer, ES256 signing key and +JWKS, audit destination, subject-binding secret, runtime binding, and hostname. +The application chooses the cell from a closed requirement directory, fetches +only that cell's JWKS, verifies the exact requirement, evidence type, concepts, +purpose, nonce, issuer, provider, audience, and validity window, then retains a +safe presentation projection. + +Direct-source assertions are labelled `immutable extract` and remain valid for +at most 3,600 seconds. Relay-backed assertions are labelled `Relay lookup` and +remain valid for at most 300 seconds. Source rows, selectors, tokens, JWS +payloads, private audit output, and sensitive dependency errors are never UI +presentation data. + +## Source publication + +Authority publishers produce five mutable Relay databases and three immutable +Evidence extracts. Each extract contains exactly one `evidence_extract` row +with `published_at`, `publisher`, and `extract_id`. A publication is written +under a new name and made read-only. Active extracts are never overwritten. + +Relay views expose authority-owned stable record identifiers, revisions, +lifecycle state, recorded timestamps, selector columns, and governed domain +columns. Relay releases only the properties declared by the selected disclosure +profile. + +## Failure semantics + +Wrong scope, wrong purpose, malformed selectors, invalid source rows, +unavailable source, and unavailable audit fail closed. No-match, ambiguous +match, and policy-hidden matches use the same data-free unresolved class. An +Evidence cell does not infer a negative assertion from an unresolved Relay +consultation. + +## Programme stories + +- Child benefit composes CRA, NIA, SRO, and MoSD assertions into five reviewed + concepts. +- Pension composes CRA death and SIPF payment assertions. SIPF survivor evidence + is separately requested for the spouse. +- Agriculture requests NAgDI voucher or livestock assertions under isolated + purposes. +- Optional citizen login uses the NIA Relay V2 lookup through eSignet, while the + application can separately compose CRA and NIA Evidence assertions. + +The preserved `/v1` suffixes in requirement and evidence-type identifiers are +domain identifier versions, not transport routes. diff --git a/docs/source-recovery.md b/docs/source-recovery.md new file mode 100644 index 0000000..33eccf2 --- /dev/null +++ b/docs/source-recovery.md @@ -0,0 +1,20 @@ +# Source and volume recovery + +Relay V2 reads authority-owned SQLite publications and owns no source truth. +Evidence reads either a named immutable extract or a Relay response. Recovery +therefore preserves publication identity and runtime bindings, not a shared +application database. + +For a mutable Relay source, stop its authority publisher before snapshotting the +database. Restore into a new volume, verify the governed schema fingerprint and +read-only binding, then restart only that Relay. For an immutable extract, +restore the exact file under its original name or publish a reviewed replacement +under a new name. Never modify an active extract in place. + +Audit sinks, Mint state, signer state, and source publications are separate +recovery units. Record exact artifact digests, runtime configuration revisions, +public JWKs, and volume identities with each backup. Do not copy private +recovery evidence into this public repository. + +During the reset, superseded service volumes remain detached and recoverable. +Deleting them is outside the reset delivery and requires separate approval. diff --git a/docs/stories/birth-to-child-benefit.md b/docs/stories/birth-to-child-benefit.md index b73f651..91e62dd 100644 --- a/docs/stories/birth-to-child-benefit.md +++ b/docs/stories/birth-to-child-benefit.md @@ -1,92 +1,28 @@ -# Birth To Child Benefit - -Status: normative for Solmara Lab wave 1 story 1. - -## Purpose - -This story demonstrates a canonical CRVS-to-social-protection journey: a -registered birth, a population identity, a household eligibility predicate, and -a programme duplicate check are gathered as source-owned predicates without -exposing raw registry rows. The programme policy layer, not the evidence -collector, decides whether those predicates amount to child benefit -eligibility. - -## Authorities And Registries - -| Authority | Registry | Evidence role | -|---|---|---| -| Civil Registration Authority | Civil registration | Birth registration, child age, child life status | -| National Identity Agency | Population register | UIN, identity status, BRN linkage | -| Social Registry Office | Social registry | Household membership and poverty band predicate | -| MoSD programme MIS | Integrated beneficiary registry | Duplicate enrollment predicate | - -Purpose IRI: -`https://id.registrystack.org/solmara/purpose/child-benefit-review`. - -Evidence offering: `solmara.child-benefit.authority-predicate-collection`. - -Evidence collector endpoint: -`https://child-benefit-federator.solmara.registrystack.org/v1/evaluations`. - -Response media type: `application/json`. - -Credential `vct`: not issued by the child-benefit evidence collector. - -Offering name: Child Benefit Authority Predicate Collection. - -## Positive Path - -Persona: Mateo Santos, `2300010248`. - -Expected claims: - -| Claim | Expected result | -|---|---| -| `birth-is-registered` | Pass: Mateo has a registered BRN. | -| `population-record-active` | Pass: Mateo's population record is active. | -| `child-age-under-5` | Pass: Mateo is under 5 at the lab clock. | -| `household-below-poverty-threshold` | Pass: household score band is eligible; raw score is not disclosed. | -| `not-already-enrolled` | Pass: no active child support enrollment exists. | - -The child-benefit evidence collector calls the CRA, NIA, SRO, and Programme -Notaries through their ordinary evidence APIs and returns the minimized -predicate results to the programme review. Its `source_trace` identifies the -authority service and response status without copying source rows or internal -evaluation state. The collector does not return a composed eligibility -decision. - -## Failure Cases - -| Persona | Case | Expected result | -|---|---|---| -| Esteban Cruz | Deceased control persona | Fails life-status check before enrollment eligibility. | -| Hana Aquino | Household above threshold | Fails `household-below-poverty-threshold`; raw poverty score remains undisclosed. | -| Karim Kone | Unregistered birth | Fails `birth-is-registered` and routes to "register the birth first" rather than a dead end. | -| Tomas Bello | Duplicate enrollment | Fails `not-already-enrolled` because an active enrollment already exists. | - -## Purpose Denial - -The smoke must attempt a request for raw household poverty score or complete -household profile under `child-benefit-review`. The response must deny access -with problem code `pdp.purpose_not_permitted` and must not include the raw -field. - -The smoke must also attempt a request with an unrelated purpose, such as -`pension-payment-review`, against the child benefit offering. The response must -deny access with `pdp.purpose_not_permitted`. - -## Smoke Expectations - -The story smoke asserts: - -1. Metadata discovery returns the child benefit offering and the purpose IRI - from `docs/purposes.md`. -2. Mateo's positive evaluation passes all five source predicates. -3. The response contains an authority `source_trace` and no - `eligible-for-child-benefit` composition from the collector. -4. Each listed failure case returns a failed predicate with no raw protected - source row in the response. -5. A raw household score/profile request returns `403` - `pdp.purpose_not_permitted` without reflecting a protected field. -6. An unrelated-purpose request returns `pdp.purpose_not_permitted`. -7. Message text is not asserted. +# Birth to child benefit + +Purpose: `child-benefit-review`. + +The programme requests four independently signed authority assertions and owns +the final outcome: + +| Authority | Requirement | Source | Concepts | +|---|---|---|---| +| CRA | `cra-child-benefit/v1` | immutable birth extract | `birth-is-registered`, `child-age-under-5` | +| NIA | `nia-child-benefit/v1` | immutable population extract | `population-record-active` | +| SRO | `sro-child-benefit/v1` | immutable poverty extract | `household-below-poverty-threshold` | +| MoSD | `mosd-child-benefit/v1` | Relay lookup | `not-already-enrolled` | + +Mateo Santos, UIN `2300010248`, is the positive synthetic persona. All five +concepts are true. The application may therefore show the positive child +benefit outcome, while each source row remains with its authority. + +Controls cover deceased or aged-out children, an above-threshold household, an +unregistered birth, duplicate enrolment, and wrong purpose. An unregistered +birth is a valid CRA record with no BRN and produces signed false, not an +unresolved consultation. The mutable MoSD source changes on the next request; +the three immutable sources change only after a reviewed replacement is bound +and the owning cell is restarted. + +The response surface contains safe authority, issuer, provider, source type, +and verified concept values. It does not contain selectors, tokens, source +rows, raw poverty measures, private audit output, or JWS bodies. diff --git a/docs/stories/death-to-pension-survivor.md b/docs/stories/death-to-pension-survivor.md index 197eefb..73228ff 100644 --- a/docs/stories/death-to-pension-survivor.md +++ b/docs/stories/death-to-pension-survivor.md @@ -1,85 +1,21 @@ -# Death To Pension Stop And Survivor Benefit - -Status: normative for Solmara Lab wave 1 story 2. - -## Purpose - -This story demonstrates a high-value DPI control: a registered death triggers a -review of an active pension payment and determines whether a spouse is eligible -for survivor benefit evidence. The pension-review application receives -minimized predicates from the CRA and SIPF Notaries, not medical details or full -registry rows. - -## Authorities And Registries - -| Authority | Registry | Evidence role | -|---|---|---| -| Civil Registration Authority | Civil registration | Registered death fact | -| Social Insurance and Pensions Fund | Pensions / social insurance | Active pension payment and survivor eligibility | - -Purpose IRIs: - -- `https://id.registrystack.org/solmara/purpose/pension-payment-review` -- `https://id.registrystack.org/solmara/purpose/survivor-benefit-determination` - -Evidence offerings: - -- `cra-death-registration-offering` -- `sipf-pensions-pension-case-offering` - -Credential `vct` values: - -- `https://id.registrystack.org/solmara/vct/survivor-benefit-status` - -Credential name: Survivor Benefit Eligibility SD-JWT VC. - -## Positive Path - -Personas: - -- Rafael Nkomo, `2300109568`, deceased pension member. -- Imani Nkomo, `2300118698`, surviving spouse. - -Expected claims: - -| Claim | Expected result | -|---|---| -| `person-is-deceased` | Pass: Rafael has a registered death event and DRN. | -| `pension-payment-active` | Pass: Rafael has an active in-payment award that requires review. | -| `survivor-is-eligible` | Pass: SIPF has a verified eligible survivor link. | - -The pension-review application combines CRA death evidence with SIPF payment -evidence. It does not ask either Notary to make the cross-authority stop-payment -decision. SIPF separately returns survivor eligibility evidence and can issue -the survivor benefit credential for Imani. Neither path discloses cause of -death. - -## Failure Cases - -| Persona | Case | Expected result | -|---|---|---| -| Otto Ferreira | Death not yet registered | `person-is-deceased` fails or returns stale-data reconciliation status; payment is not automatically stopped from unregistered evidence. | -| Lucia Ferreira | Survivor waits for reconciliation | Survivor eligibility cannot pass until Otto's death registration is available. | -| Mina Rahman | Survivor relationship no longer eligible | `survivor-is-eligible` fails in SIPF evidence. | -| Pavel Rahman | Former spouse death control | Confirms the SIPF relationship status is used rather than name matching. | - -## Purpose Denial - -The smoke must attempt to request `cause_of_death` or medical death details -under `pension-payment-review`. The response must deny access with problem code -`pdp.purpose_not_permitted`. The SIPF needs the death fact, not the medicine. - -## Smoke Expectations - -The story smoke asserts: - -1. Metadata discovery returns both authority offerings and purpose IRIs from - `docs/purposes.md`. -2. The application combines Rafael's CRA death predicate and SIPF active-payment - predicate without treating either source response as a composed decision. -3. SIPF's survivor eligibility evaluation passes and uses the expected - survivor credential `vct`. -4. Otto and Lucia produce the stale-data or reconciliation path. -5. Mina's survivor claim fails because the marriage is dissolved. -6. Cause-of-death access is denied with `pdp.purpose_not_permitted`. -7. Denial smokes assert stable problem codes, not message text. +# Death to pension stop and survivor benefit + +The pension application combines a CRA death assertion and a SIPF active +payment assertion under `pension-payment-review`. The application, not either +Evidence cell, derives whether payment should stop. + +CRA calls only `civil-person/death-by-uin` and discloses the governed death +status. SIPF calls only `pension-payment/by-pensioner-uin` and discloses active +payment status. Cause of death, unrelated civil events, payment amount, and +payment history are outside the contracts. + +For survivor benefit, the application sends the surviving spouse UIN +`2300118698` to SIPF requirement `sipf-survivor-benefit/v1` under +`survivor-benefit-determination`. SIPF calls only +`survivor-case/by-spouse-uin` and signs `survivor-is-eligible`. + +Controls cover death not yet registered, dissolved relationship, unknown cause +of death requirement, wrong purpose, unresolved lookup, dependency failure, +and audit failure. All dependency and authorization failures are generic and +value-free. CRA and SIPF assertions are independently verified against their +own JWKS before composition. diff --git a/docs/stories/farmer-climate-smart-voucher.md b/docs/stories/farmer-climate-smart-voucher.md index 6e95df9..30b8197 100644 --- a/docs/stories/farmer-climate-smart-voucher.md +++ b/docs/stories/farmer-climate-smart-voucher.md @@ -1,98 +1,24 @@ -# Farmer Climate-Smart Voucher - -Status: normative for Solmara Lab wave 1 story 3. - -## Purpose - -This story ports the NAgDI agriculture demo into Solmara. It demonstrates how a -spreadsheet-backed farmer and livestock registry becomes a governed API with -purpose-limited evidence, stable offerings, and smokeable denial behavior. - -The livestock movement permit companion ships in the same story package. - -## Authorities And Registries - -| Authority | Registry | Evidence role | -|---|---|---| -| National Agricultural Data Institute | Farmer registry | Farmer registration, holding, programme, voucher, data-use authorization | -| National Agricultural Data Institute | Livestock registry | Premises, herd, animal, vaccination, quarantine, movement permit | - -Purpose IRIs: - -- `https://id.registrystack.org/solmara/purpose/voucher-eligibility-review` -- `https://id.registrystack.org/solmara/purpose/livestock-movement-control` - -Evidence offerings: - -- `solmara.nagdi.climate-smart-voucher-eligibility` -- `solmara.nagdi.livestock-movement-permit` - -Credential `vct` values: - -- `https://id.registrystack.org/solmara/vct/climate-smart-voucher-eligibility` -- `https://id.registrystack.org/solmara/vct/livestock-movement-permit` - -Credential names: - -- Climate-Smart Voucher Eligibility SD-JWT VC. -- Livestock Movement Permit SD-JWT VC. - -## Positive Path - -Persona: Amina Kone, `FR-1001`, legacy alias `FARMER-1001`, Brenholm district. - -Expected voucher claims: - -| Claim | Expected result | -|---|---| -| `farmer-is-registered` | Pass: Amina has an active `FR-*` farmer record. | -| `data-use-authorized` | Pass: NAgDI has active authorization for voucher review. | -| `holding-in-eligible-district` | Pass: holding district is eligible for the climate-smart package. | -| `not-already-redeemed` | Pass: no redeemed voucher exists for the same programme cycle. | - -Expected livestock companion claims: - -| Claim | Expected result | -|---|---| -| `movement-applicant-controls-herd` | Pass: Amina controls the herd or premises. | -| `vaccination-current` | Pass: required vaccinations are current for the species. | -| `origin-district-not-quarantined-for-species` | Pass: no species-specific quarantine applies. | -| `destination-permitted` | Pass: destination district allows the movement. | - -The NAgDI notary previews or issues the voucher eligibility credential and the -livestock movement permit credential. Responses disclose predicates and -references, not full workbook rows. - -## Failure Cases - -| Persona | Case | Expected result | -|---|---|---| -| Diego Santos | Missing data-use authorization | Voucher eligibility fails `data-use-authorized`. | -| Noor Patel | Ineligible climate-risk band | Voucher eligibility fails district or package eligibility without dumping market-sizing cells. | -| Beatriz Okafor | Species-specific quarantine | Livestock movement fails `origin-district-not-quarantined-for-species`. | -| Sefu Dela Cruz | Incomplete vaccination evidence | Livestock movement fails `vaccination-current`. | - -## Purpose Denial - -The smoke must attempt to use `livestock-movement-control` to read voucher -budget, market-sizing, or crop programme details. The response must deny access -with `pdp.purpose_not_permitted`. - -The smoke must attempt to use `voucher-eligibility-review` to read unrelated -livestock movement details. The response must deny access with -`pdp.purpose_not_permitted`. - -## Smoke Expectations - -The story smoke asserts: - -1. Metadata discovery returns both NAgDI offerings and purpose IRIs from - `docs/purposes.md`. -2. Amina passes voucher eligibility and receives the expected voucher `vct`. -3. Amina passes the livestock movement companion and receives the expected - livestock movement `vct`. -4. Diego, Noor, Beatriz, and Sefu each fail the listed predicate. -5. Cross-purpose NAgDI access is denied with `pdp.purpose_not_permitted`. -6. The ported claim configs use `FR-*` farmer identifiers and Solmara P-coded - districts, not legacy real-country geography or agriculture national-id - aliases. +# Farmer voucher and livestock movement + +NAgDI owns one live read-only SQLite publication and two isolated Relay V2 +lookups. + +`farmer/voucher-by-farmer-id` supports requirement `nagdi-voucher/v1` under +`voucher-eligibility-review`. It yields the three signed concepts +`farmer-registered`, `data-use-authorized-for-purpose`, and +`eligible-for-climate-smart-input-voucher` after reviewing the minimum governed +voucher facts. + +`livestock-herd/movement-by-farmer-id` supports requirement +`nagdi-livestock/v1` under `livestock-movement-control`. It yields +`registered-herd`, `origin-district-not-quarantined-for-species`, and +`eligible-for-livestock-movement-permit`. + +Each operation has a distinct Mint client, scope, purpose claim, access profile, +and disclosure profile. Voucher authority cannot call the livestock operation +and livestock authority cannot call the voucher operation. Neither route +offers enumeration. + +Positive, unauthorized-data-use, redeemed-voucher, quarantine, wrong-purpose, +unresolved, malformed-row, unavailable-source, and unavailable-audit cases are +tested. UI and logs never render the farmer selector or source row. diff --git a/docs/wallet-oid4vci.md b/docs/wallet-oid4vci.md deleted file mode 100644 index 608f306..0000000 --- a/docs/wallet-oid4vci.md +++ /dev/null @@ -1,21 +0,0 @@ -# Retired wallet issuer topology - -Status: retired before Registry Stack 1.0. - -The former citizen-services deployment added a portal Notary and a separate -OpenID for Verifiable Credential Issuance (OID4VCI) Notary. That -purpose-specific model is not part of the clean Solmara topology. Do not deploy -`citizen-notary`, `citizen-issuer-notary`, or the removed -`compose.coolify.citizen-services.yaml` application. - -Solmara now runs exactly six authority-owned public Relay and Notary pairs: -CRA, NIA, SRO, Programme, SIPF, and NAgDI. Each pair has a private consultation -Relay beside its Notary; that internal process is not another authority or -public endpoint. Citizen portal journeys use evidence exposed by those -authority Notaries. Adding a credential issuance journey must extend the -owning authority project rather than create a purpose-specific Notary. - -The Walt holder wallet application remains available as an isolated UI -demonstrator through `compose.coolify.walt.yaml`, but the current six-authority -topology does not expose an OID4VCI issuer. eSignet remains the portal identity -provider, and its Redis service remains eSignet-owned state. diff --git a/evidence/Caddyfile b/evidence/Caddyfile deleted file mode 100644 index 1c6476f..0000000 --- a/evidence/Caddyfile +++ /dev/null @@ -1,66 +0,0 @@ -{ - auto_https off -} - -(solmara_tls) { - tls /etc/solmara-evidence/tls/gateway.crt /etc/solmara-evidence/tls/gateway.key -} - -https://mint.evidence.solmara.invalid { - import solmara_tls - reverse_proxy mint:8081 -} - -https://evidence.solmara.invalid { - import solmara_tls - reverse_proxy evidence:8080 -} - -https://localhost { - import solmara_tls - handle_path /child-benefit/* { - reverse_proxy child-benefit-federator:8080 - } - handle /token* { - reverse_proxy mint:8081 - } - handle /.well-known/oauth-authorization-server { - reverse_proxy mint:8081 - } - handle /.well-known/jwks.json { - reverse_proxy mint:8081 - } - handle { - reverse_proxy evidence:8080 - } -} - -https://cra-relay.evidence.solmara.invalid { - import solmara_tls - reverse_proxy cra-civil-relay:8080 -} - -https://nia-relay.evidence.solmara.invalid { - import solmara_tls - reverse_proxy nia-population-relay:8080 -} - -https://sro-relay.evidence.solmara.invalid { - import solmara_tls - reverse_proxy sro-social-relay:8080 -} - -https://programme-relay.evidence.solmara.invalid { - import solmara_tls - reverse_proxy programme-mis-relay:8080 -} - -https://sipf-relay.evidence.solmara.invalid { - import solmara_tls - reverse_proxy sipf-pensions-relay:8080 -} - -https://nagdi-relay.evidence.solmara.invalid { - import solmara_tls - reverse_proxy nagdi-agriculture-relay:8080 -} diff --git a/evidence/bundle/adapters/extract.rhai b/evidence/bundle/adapters/extract.rhai deleted file mode 100644 index 1cdca2a..0000000 --- a/evidence/bundle/adapters/extract.rhai +++ /dev/null @@ -1,26 +0,0 @@ -fn extract(source_response, parameters) { - let records = source_response["data"]; - let has_more = source_response["pagination"]["has_more"]; - if records.len == 0 { - if has_more { throw("source_protocol_error"); } - return #{outcome: "no_match"}; - } - if records.len > 1 || has_more { - return #{outcome: "ambiguous"}; - } - let record = records[0]; - // Relay preserves a nullable birth-registration number. Minimize that - // identifier into the only fact the derivation needs before validation. - if record.contains("birth_brn") { - return #{ - outcome: "match", - facts: #{ - uin: record["uin"], - birth_date: record["birth_date"], - birth_registered: !is_missing(record["birth_brn"]), - deceased: record["deceased"] - } - }; - } - #{outcome: "match", facts: record} -} diff --git a/evidence/bundle/adapters/prepare.rhai b/evidence/bundle/adapters/prepare.rhai deleted file mode 100644 index 6c6642d..0000000 --- a/evidence/bundle/adapters/prepare.rhai +++ /dev/null @@ -1,16 +0,0 @@ -fn prepare(selectors, parameters) { - let selector_field = parameters["selectorField"]; - let subject = selectors["subject"]; - let selector_value = subject["values"]["uin"]; - if selector_field == "farmer_id" { - selector_value = subject["values"]["farmer_id"]; - } - #{ - query: [ - #{name: selector_field, value: selector_value}, - #{name: "fields", value: parameters["providerFields"]}, - #{name: "limit", value: parameters["resultLimit"]} - ], - body: () - } -} diff --git a/evidence/bundle/derivations/cra-child-benefit.rhai b/evidence/bundle/derivations/cra-child-benefit.rhai deleted file mode 100644 index 3b014df..0000000 --- a/evidence/bundle/derivations/cra-child-benefit.rhai +++ /dev/null @@ -1,16 +0,0 @@ -fn derive(facts, selectors, evaluation_context) { - if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { - throw("derivation_input_error"); - } - let birth_date = parse_date(required(facts["birth_date"], "required_fact_missing")); - let fifth_birthday = add_calendar_years(birth_date, 5); - let as_of_date = parse_date(evaluation_context["parameters"]["as_of_date"]); - let registered = required(facts["birth_registered"], "required_fact_missing"); - let under_five = !required(facts["deceased"], "required_fact_missing") && - compare_dates(as_of_date, birth_date) >= 0 && - compare_dates(as_of_date, fifth_birthday) < 0; - [ - #{concept_id: "https://id.registrystack.org/solmara/concept/birth-is-registered", value: registered}, - #{concept_id: "https://id.registrystack.org/solmara/concept/child-age-under-5", value: under_five} - ] -} diff --git a/evidence/bundle/derivations/population-active.rhai b/evidence/bundle/derivations/population-active.rhai deleted file mode 100644 index 16a8203..0000000 --- a/evidence/bundle/derivations/population-active.rhai +++ /dev/null @@ -1,8 +0,0 @@ -fn derive(facts, selectors, evaluation_context) { - if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { - throw("derivation_input_error"); - } - let active = required(facts["identity_status"], "required_fact_missing") == "active" && - required(facts["alive"], "required_fact_missing"); - [#{concept_id: evaluation_context["parameters"]["concept_id"], value: active}] -} diff --git a/evidence/bundle/derivations/voucher.rhai b/evidence/bundle/derivations/voucher.rhai deleted file mode 100644 index d4d60de..0000000 --- a/evidence/bundle/derivations/voucher.rhai +++ /dev/null @@ -1,19 +0,0 @@ -fn derive(facts, selectors, evaluation_context) { - if required(facts["farmer_id"], "required_fact_missing") != selectors["subject"]["values"]["farmer_id"] { - throw("derivation_input_error"); - } - let registered = required(facts["farmer_registered"], "required_fact_missing"); - let authorized = required(facts["data_use_authorized"], "required_fact_missing"); - let eligible = registered && authorized && - required(facts["active_smallholder_farmer"], "required_fact_missing") && - required(facts["active_farm_parcel"], "required_fact_missing") && - required(facts["crop_declared_for_season"], "required_fact_missing") && - required(facts["district_climate_risk_active"], "required_fact_missing") && - required(facts["voucher_entitlement_current"], "required_fact_missing") && - required(facts["voucher_not_redeemed"], "required_fact_missing"); - [ - #{concept_id: "https://id.registrystack.org/solmara/concept/farmer-registered", value: registered}, - #{concept_id: "https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose", value: authorized}, - #{concept_id: "https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher", value: eligible} - ] -} diff --git a/evidence/bundle/evidence.yaml b/evidence/bundle/evidence.yaml deleted file mode 100644 index 1a3447c..0000000 --- a/evidence/bundle/evidence.yaml +++ /dev/null @@ -1,542 +0,0 @@ -version: 1 -assuranceProfile: production -service: { providerId: https://id.registrystack.org/solmara/evidence/national, trustDomain: https://id.registrystack.org/solmara/trust-domain/lab } -issuer: { id: https://id.registrystack.org/solmara/issuer/national-evidence-service } -authentication: - kind: oidc-access-token - issuer: https://mint.evidence.solmara.invalid - audiences: [solmara-evidence] - tokenTypes: [at+jwt] - algorithms: [EdDSA] - jwksUri: https://mint.evidence.solmara.invalid/.well-known/jwks.json - principalClaim: sub - requesterTagsClaim: evidence_tags - evidenceAudienceClaim: evidence_audience - grantIdClaim: evidence_grant_id - grantAuthorityClaim: evidence_authority -audit: { format: keyed-jsonl, hashSecretRef: secret:file/audit-hmac-key, hashKeyVersion: 1, failClosed: true } -subjectBinding: { secretRef: secret:file/subject-binding-hmac-key, keyVersion: 1 } -# One demo principal drives every guided workflow, so its burst covers a full -# end-to-end lab tour while retaining per-principal and selector-failure bounds. -rateLimits: { requestsPerPrincipalPerMinute: 600, burstPerPrincipal: 100, failedSelectorAttemptsPerPrincipalAuthorityPerMinute: 50 } -signing: - format: flattened-jws-json - algorithm: EdDSA - activeKeyId: solmara-evidence-signing-key-1 - activeKeyRef: secret:file/signing-ed25519-private-jwk - retiredPublicJwkFiles: [] - jwksPath: /.well-known/evidence/jwks.json - maximumAssertionValiditySeconds: 86400 - verifierClockSkewSeconds: 30 -responseFormats: [signed-jws] - -selectorProfiles: - solmara-uin-v1: - maximumAggregateBytes: 16 - fields: - uin: { type: string, minimumBytes: 10, maximumBytes: 10 } - farmer-reference-v1: - maximumAggregateBytes: 32 - fields: - farmer_id: { type: string, minimumBytes: 7, maximumBytes: 16 } - -sources: - cra-child-benefit: - transport: http-json - baseUrl: https://cra-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/cra-relay-token } - request: - method: GET - path: /v1/datasets/civil_people/entities/civil_people/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review } - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: uin, providerFields: "uin,birth_date,birth_brn,deceased", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/uin, /data/*/birth_date, /data/*/birth_brn, /data/*/deceased, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/cra-child-benefit-facts.schema.yaml - cra-pension: - transport: http-json - baseUrl: https://cra-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/cra-relay-token } - request: - method: GET - path: /v1/datasets/civil_people/entities/civil_people/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/pension-payment-review } - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: uin, providerFields: "uin,deceased", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/uin, /data/*/deceased, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/cra-pension-facts.schema.yaml - cra-citizen: - transport: http-json - baseUrl: https://cra-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/cra-relay-token } - request: - method: GET - path: /v1/datasets/civil_people/entities/civil_people/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/citizen-self-service } - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: uin, providerFields: "uin", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/uin, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/cra-citizen-facts.schema.yaml - nia-child-benefit: - transport: http-json - baseUrl: https://nia-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/nia-relay-token } - request: - method: GET - path: /v1/datasets/population/entities/population/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review } - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: uin, providerFields: "uin,identity_status,alive", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/uin, /data/*/identity_status, /data/*/alive, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/nia-population-facts.schema.yaml - nia-citizen: - transport: http-json - baseUrl: https://nia-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/nia-relay-token } - request: - method: GET - path: /v1/datasets/population/entities/population/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/citizen-self-service } - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: uin, providerFields: "uin,identity_status,alive", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/uin, /data/*/identity_status, /data/*/alive, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/nia-population-facts.schema.yaml - sro-child-benefit: - transport: http-json - baseUrl: https://sro-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/sro-relay-token } - request: - method: GET - path: /v1/datasets/sro_child/entities/sro_child/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review } - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: uin, providerFields: "uin,poverty_band", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/uin, /data/*/poverty_band, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/sro-child-benefit-facts.schema.yaml - mosd-child-benefit: - transport: http-json - baseUrl: https://programme-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/programme-relay-token } - request: - method: GET - path: /v1/datasets/mosd_enroll/entities/mosd_enroll/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review } - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: uin, providerFields: "uin,duplicate_flag", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/uin, /data/*/duplicate_flag, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/mosd-child-benefit-facts.schema.yaml - sipf-pension: - transport: http-json - baseUrl: https://sipf-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/sipf-relay-token } - request: - method: GET - path: /v1/datasets/pension/entities/pension/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/pension-payment-review } - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: pensioner_uin, providerFields: "pensioner_uin,payment_status", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/pensioner_uin, /data/*/payment_status, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/sipf-pension-facts.schema.yaml - sipf-survivor: - transport: http-json - baseUrl: https://sipf-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/sipf-relay-token } - request: - method: GET - path: /v1/datasets/survivor/entities/survivor/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination } - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: spouse_uin, providerFields: "spouse_uin,survivor_eligible", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/spouse_uin, /data/*/survivor_eligible, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/sipf-survivor-facts.schema.yaml - nagdi-voucher: - transport: http-json - baseUrl: https://nagdi-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/nagdi-relay-token } - request: - method: GET - path: /v1/datasets/nagdi_farmer/entities/nagdi_farmer/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review } - selectorInputs: [{ role: subject, alternatives: [{ profile: farmer-reference-v1, fields: [farmer_id] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: farmer_id, providerFields: "farmer_id,farmer_registered,data_use_authorized,active_smallholder_farmer,active_farm_parcel,crop_declared_for_season,district_climate_risk_active,voucher_entitlement_current,voucher_not_redeemed", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/farmer_id, /data/*/farmer_registered, /data/*/data_use_authorized, /data/*/active_smallholder_farmer, /data/*/active_farm_parcel, /data/*/crop_declared_for_season, /data/*/district_climate_risk_active, /data/*/voucher_entitlement_current, /data/*/voucher_not_redeemed, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/nagdi-voucher-facts.schema.yaml - nagdi-livestock: - transport: http-json - baseUrl: https://nagdi-relay.evidence.solmara.invalid - tlsTrustProfile: solmara-lab - posture: field-projected - authentication: { kind: static-bearer, tokenRef: secret:file/nagdi-relay-token } - request: - method: GET - path: /v1/datasets/nagdi_herd/entities/nagdi_herd/records - fixedHeaders: - - { name: Accept, value: application/json } - - { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/livestock-movement-control } - selectorInputs: [{ role: subject, alternatives: [{ profile: farmer-reference-v1, fields: [farmer_id] }] }] - prepareScript: adapters/prepare.rhai - adapterParameters: { selectorField: farmer_id, providerFields: "farmer_id,registered_herd,herd_vaccination_current,origin_district_not_quarantined_for_species,destination_district_open,no_conflicting_open_movement_permit", resultLimit: "2" } - adapterParametersSchema: schemas/adapter-parameters.schema.yaml - preparationLimits: { query: required, jsonBody: forbidden, maximumQueryPairs: 8, maximumQueryNameBytes: 64, maximumQueryValueBytes: 1024, maximumNormalizedBytes: 4096 } - projection: [/data/*/farmer_id, /data/*/registered_herd, /data/*/herd_vaccination_current, /data/*/origin_district_not_quarantined_for_species, /data/*/destination_district_open, /data/*/no_conflicting_open_movement_permit, /pagination/has_more] - redirects: deny - timeoutMilliseconds: 3000 - maximumResponseBytes: 65536 - concurrencyLimit: 8 - responseSchema: schemas/response.schema.yaml - extractScript: adapters/extract.rhai - factSchema: schemas/nagdi-livestock-facts.schema.yaml - -authorityProfiles: - solmara-demo-v1: - kind: statutory - requesterTags: [solmara-demo] - grants: - - requirement: https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1 - purpose: child-benefit-review - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] - - requirement: https://id.registrystack.org/solmara/requirement/cra-pension-death/v1 - purpose: pension-payment-review - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] - - requirement: https://id.registrystack.org/solmara/requirement/cra-citizen-record/v1 - purpose: citizen-self-service - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] - - requirement: https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1 - purpose: child-benefit-review - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] - - requirement: https://id.registrystack.org/solmara/requirement/nia-citizen-status/v1 - purpose: citizen-self-service - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] - - requirement: https://id.registrystack.org/solmara/requirement/sro-child-benefit/v1 - purpose: child-benefit-review - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] - - requirement: https://id.registrystack.org/solmara/requirement/mosd-child-benefit/v1 - purpose: child-benefit-review - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] - - requirement: https://id.registrystack.org/solmara/requirement/sipf-pension-payment/v1 - purpose: pension-payment-review - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] - - requirement: https://id.registrystack.org/solmara/requirement/sipf-survivor-benefit/v1 - purpose: survivor-benefit-determination - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] - - requirement: https://id.registrystack.org/solmara/requirement/nagdi-voucher/v1 - purpose: voucher-eligibility-review - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: farmer-reference-v1, valueOrigin: request }] - - requirement: https://id.registrystack.org/solmara/requirement/nagdi-livestock/v1 - purpose: livestock-movement-control - audienceFrom: authenticated-requester - responseFormats: [signed-jws] - subjects: [{ role: subject, selectorProfile: farmer-reference-v1, valueOrigin: request }] - -requirements: - - id: https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1 - kind: information-requirement - source: cra-child-benefit - purposes: [child-benefit-review] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/cra-child-benefit/v1 - observationTimezone: Asia/Bangkok - validitySeconds: 86400 - derivation: - script: derivations/cra-child-benefit.rhai - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - parameters: { as_of_date: "2026-07-14" } - concepts: - - { id: https://id.registrystack.org/solmara/concept/birth-is-registered, form: boolean, required: true, constraints: {} } - - { id: https://id.registrystack.org/solmara/concept/child-age-under-5, form: boolean, required: true, constraints: {} } - fixtures: fixtures/cra-child-benefit.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/cra-child-benefit] } - existenceDisclosure: collapse-unresolved - - id: https://id.registrystack.org/solmara/requirement/cra-pension-death/v1 - kind: information-requirement - source: cra-pension - purposes: [pension-payment-review] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/social-insurance/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/cra-death-status/v1 - validitySeconds: 86400 - derivation: { script: derivations/cra-deceased.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } - concepts: [{ id: https://id.registrystack.org/solmara/concept/person-is-deceased, form: boolean, required: true, constraints: {} }] - fixtures: fixtures/cra-pension.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/cra-pension] } - existenceDisclosure: collapse-unresolved - - id: https://id.registrystack.org/solmara/requirement/cra-citizen-record/v1 - kind: information-requirement - source: cra-citizen - purposes: [citizen-self-service] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/citizen-self-service/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/civil-record-linked/v1 - validitySeconds: 86400 - derivation: { script: derivations/cra-linked.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } - concepts: [{ id: https://id.registrystack.org/solmara/concept/civil-record-linked, form: boolean, required: true, constraints: {} }] - fixtures: fixtures/cra-citizen.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/cra-citizen] } - existenceDisclosure: collapse-unresolved - - id: https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1 - kind: information-requirement - source: nia-child-benefit - purposes: [child-benefit-review] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/population-active/v1 - validitySeconds: 86400 - derivation: - script: derivations/population-active.rhai - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - parameters: { concept_id: https://id.registrystack.org/solmara/concept/population-record-active } - concepts: [{ id: https://id.registrystack.org/solmara/concept/population-record-active, form: boolean, required: true, constraints: {} }] - fixtures: fixtures/nia-child-benefit.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nia-child-benefit] } - existenceDisclosure: collapse-unresolved - - id: https://id.registrystack.org/solmara/requirement/nia-citizen-status/v1 - kind: information-requirement - source: nia-citizen - purposes: [citizen-self-service] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/citizen-self-service/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/citizen-population-active/v1 - validitySeconds: 86400 - derivation: - script: derivations/population-active.rhai - selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] - parameters: { concept_id: https://id.registrystack.org/solmara/concept/citizen-population-record-active } - concepts: [{ id: https://id.registrystack.org/solmara/concept/citizen-population-record-active, form: boolean, required: true, constraints: {} }] - fixtures: fixtures/nia-citizen.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nia-citizen] } - existenceDisclosure: collapse-unresolved - - id: https://id.registrystack.org/solmara/requirement/sro-child-benefit/v1 - kind: information-requirement - source: sro-child-benefit - purposes: [child-benefit-review] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/poverty-priority/v1 - validitySeconds: 86400 - derivation: { script: derivations/poverty-priority.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } - concepts: [{ id: https://id.registrystack.org/solmara/concept/household-below-poverty-threshold, form: boolean, required: true, constraints: {} }] - fixtures: fixtures/sro-child-benefit.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/sro-child-benefit] } - existenceDisclosure: collapse-unresolved - - id: https://id.registrystack.org/solmara/requirement/mosd-child-benefit/v1 - kind: information-requirement - source: mosd-child-benefit - purposes: [child-benefit-review] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/not-enrolled/v1 - validitySeconds: 86400 - derivation: { script: derivations/not-enrolled.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } - concepts: [{ id: https://id.registrystack.org/solmara/concept/not-already-enrolled, form: boolean, required: true, constraints: {} }] - fixtures: fixtures/mosd-child-benefit.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/mosd-child-benefit] } - existenceDisclosure: collapse-unresolved - - id: https://id.registrystack.org/solmara/requirement/sipf-pension-payment/v1 - kind: information-requirement - source: sipf-pension - purposes: [pension-payment-review] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/social-insurance/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/pension-payment-active/v1 - validitySeconds: 86400 - derivation: { script: derivations/pension-active.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } - concepts: [{ id: https://id.registrystack.org/solmara/concept/pension-payment-active, form: boolean, required: true, constraints: {} }] - fixtures: fixtures/sipf-pension.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/sipf-pension] } - existenceDisclosure: collapse-unresolved - - id: https://id.registrystack.org/solmara/requirement/sipf-survivor-benefit/v1 - kind: information-requirement - source: sipf-survivor - purposes: [survivor-benefit-determination] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/social-insurance/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/survivor-benefit/v1 - validitySeconds: 86400 - derivation: { script: derivations/survivor-eligible.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }], parameters: {} } - concepts: [{ id: https://id.registrystack.org/solmara/concept/survivor-is-eligible, form: boolean, required: true, constraints: {} }] - fixtures: fixtures/sipf-survivor.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/sipf-survivor] } - existenceDisclosure: collapse-unresolved - - id: https://id.registrystack.org/solmara/requirement/nagdi-voucher/v1 - kind: information-requirement - source: nagdi-voucher - purposes: [voucher-eligibility-review] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [farmer-reference-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/agriculture/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/climate-smart-voucher/v1 - validitySeconds: 600 - derivation: { script: derivations/voucher.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: farmer-reference-v1, fields: [farmer_id] }] }], parameters: {} } - concepts: - - { id: https://id.registrystack.org/solmara/concept/farmer-registered, form: boolean, required: true, constraints: {} } - - { id: https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose, form: boolean, required: true, constraints: {} } - - { id: https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher, form: boolean, required: true, constraints: {} } - fixtures: fixtures/nagdi-voucher.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nagdi-voucher] } - existenceDisclosure: collapse-unresolved - - id: https://id.registrystack.org/solmara/requirement/nagdi-livestock/v1 - kind: information-requirement - source: nagdi-livestock - purposes: [livestock-movement-control] - subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [farmer-reference-v1] }] - referenceFrameworks: [https://id.registrystack.org/solmara/framework/agriculture/v1] - evidenceType: https://id.registrystack.org/solmara/evidence-type/livestock-movement/v1 - validitySeconds: 600 - derivation: { script: derivations/livestock.rhai, selectorInputs: [{ role: subject, alternatives: [{ profile: farmer-reference-v1, fields: [farmer_id] }] }], parameters: {} } - concepts: - - { id: https://id.registrystack.org/solmara/concept/registered-herd, form: boolean, required: true, constraints: {} } - - { id: https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species, form: boolean, required: true, constraints: {} } - - { id: https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit, form: boolean, required: true, constraints: {} } - fixtures: fixtures/nagdi-livestock.yaml - disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nagdi-livestock] } - existenceDisclosure: collapse-unresolved diff --git a/evidence/bundle/fixtures/cra-child-benefit.yaml b/evidence/bundle/fixtures/cra-child-benefit.yaml deleted file mode 100644 index 662956f..0000000 --- a/evidence/bundle/fixtures/cra-child-benefit.yaml +++ /dev/null @@ -1,62 +0,0 @@ -fixture: registry.evidence.reference.solmara.cra-child-benefit/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000001" } } } - derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000001" } } } - expectedRequestParts: - query: [{ name: uin, value: "1000000001" }, { name: fields, value: "uin,birth_date,birth_brn,deceased" }, { name: limit, value: "2" }] - body: null - expectedTransport: - path: /v1/datasets/civil_people/entities/civil_people/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review }] -cases: - - id: positive - response: { data: [{ uin: "1000000001", birth_date: "2022-08-01", birth_brn: BRN-0001, deceased: false }], pagination: { has_more: false } } - expected: - lookup: match - derivationRuns: true - signed: true - values: - https://id.registrystack.org/solmara/concept/birth-is-registered: true - https://id.registrystack.org/solmara/concept/child-age-under-5: true - - id: negative-subject-mismatch - response: { data: [{ uin: "9999999999", birth_date: "2022-08-01", birth_brn: BRN-0001, deceased: false }], pagination: { has_more: false } } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-fifth-birthday - response: { data: [{ uin: "1000000001", birth_date: "2021-07-14", birth_brn: BRN-0001, deceased: false }], pagination: { has_more: false } } - expected: - lookup: match - derivationRuns: true - signed: true - values: - https://id.registrystack.org/solmara/concept/birth-is-registered: true - https://id.registrystack.org/solmara/concept/child-age-under-5: false - - id: unregistered-birth - response: { data: [{ uin: "1000000001", birth_date: "2022-08-01", birth_brn: null, deceased: false }], pagination: { has_more: false } } - expected: - lookup: match - derivationRuns: true - signed: true - values: - https://id.registrystack.org/solmara/concept/birth-is-registered: false - https://id.registrystack.org/solmara/concept/child-age-under-5: true - - id: missing-fact - response: { data: [{ uin: "1000000001", birth_date: "2022-08-01", deceased: false }], pagination: { has_more: false } } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: { data: [{ uin: "1000000001", birth_date: "2022-08-01", birth_brn: BRN-0001, deceased: false }, { uin: "1000000001", birth_date: "2021-05-01", birth_brn: BRN-0002, deceased: false }], pagination: { has_more: true } } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/birth-is-registered, https://id.registrystack.org/solmara/concept/child-age-under-5] - evidenceExcludes: [uin, birth_date, birth_brn, deceased, "1000000001", BRN-0001] - diagnosticsExclude: ["1000000001", BRN-0001, "2022-08-01"] diff --git a/evidence/bundle/fixtures/cra-citizen.yaml b/evidence/bundle/fixtures/cra-citizen.yaml deleted file mode 100644 index df11851..0000000 --- a/evidence/bundle/fixtures/cra-citizen.yaml +++ /dev/null @@ -1,39 +0,0 @@ -fixture: registry.evidence.reference.solmara.cra-citizen/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000003" } } } - derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000003" } } } - expectedRequestParts: { query: [{ name: uin, value: "1000000003" }, { name: fields, value: uin }, { name: limit, value: "2" }], body: null } - expectedTransport: - path: /v1/datasets/civil_people/entities/civil_people/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/citizen-self-service }] -cases: - - id: positive - response: { data: [{ uin: "1000000003" }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: true } - - id: negative-subject-mismatch - response: { data: [{ uin: "9999999999" }], pagination: { has_more: false } } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-linked-record - response: { data: [{ uin: "1000000003" }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: true } - - id: missing-fact - response: { data: [{}], pagination: { has_more: false } } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: { data: [{ uin: "1000000003" }, { uin: "1000000003" }], pagination: { has_more: true } } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/civil-record-linked] - evidenceExcludes: [uin, "1000000003"] - diagnosticsExclude: ["1000000003"] diff --git a/evidence/bundle/fixtures/cra-pension.yaml b/evidence/bundle/fixtures/cra-pension.yaml deleted file mode 100644 index 7368156..0000000 --- a/evidence/bundle/fixtures/cra-pension.yaml +++ /dev/null @@ -1,39 +0,0 @@ -fixture: registry.evidence.reference.solmara.cra-pension/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000002" } } } - derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000002" } } } - expectedRequestParts: { query: [{ name: uin, value: "1000000002" }, { name: fields, value: "uin,deceased" }, { name: limit, value: "2" }], body: null } - expectedTransport: - path: /v1/datasets/civil_people/entities/civil_people/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/pension-payment-review }] -cases: - - id: positive - response: { data: [{ uin: "1000000002", deceased: true }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: true } - - id: negative-subject-mismatch - response: { data: [{ uin: "9999999999", deceased: true }], pagination: { has_more: false } } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-living - response: { data: [{ uin: "1000000002", deceased: false }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: false } - - id: missing-fact - response: { data: [{ uin: "1000000002" }], pagination: { has_more: false } } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: { data: [{ uin: "1000000002", deceased: true }, { uin: "1000000002", deceased: false }], pagination: { has_more: true } } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/person-is-deceased] - evidenceExcludes: [uin, deceased, "1000000002"] - diagnosticsExclude: ["1000000002"] diff --git a/evidence/bundle/fixtures/mosd-child-benefit.yaml b/evidence/bundle/fixtures/mosd-child-benefit.yaml deleted file mode 100644 index 963ee7a..0000000 --- a/evidence/bundle/fixtures/mosd-child-benefit.yaml +++ /dev/null @@ -1,39 +0,0 @@ -fixture: registry.evidence.reference.solmara.mosd-child-benefit/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000007" } } } - derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000007" } } } - expectedRequestParts: { query: [{ name: uin, value: "1000000007" }, { name: fields, value: "uin,duplicate_flag" }, { name: limit, value: "2" }], body: null } - expectedTransport: - path: /v1/datasets/mosd_enroll/entities/mosd_enroll/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review }] -cases: - - id: positive - response: { data: [{ uin: "1000000007", duplicate_flag: false }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: true } - - id: negative-subject-mismatch - response: { data: [{ uin: "9999999999", duplicate_flag: false }], pagination: { has_more: false } } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-already-enrolled - response: { data: [{ uin: "1000000007", duplicate_flag: true }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: false } - - id: missing-fact - response: { data: [{ uin: "1000000007" }], pagination: { has_more: false } } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: { data: [{ uin: "1000000007", duplicate_flag: false }, { uin: "1000000007", duplicate_flag: true }], pagination: { has_more: true } } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/not-already-enrolled] - evidenceExcludes: [uin, duplicate_flag, "1000000007"] - diagnosticsExclude: ["1000000007"] diff --git a/evidence/bundle/fixtures/nagdi-livestock.yaml b/evidence/bundle/fixtures/nagdi-livestock.yaml deleted file mode 100644 index e6fd573..0000000 --- a/evidence/bundle/fixtures/nagdi-livestock.yaml +++ /dev/null @@ -1,65 +0,0 @@ -fixture: registry.evidence.reference.solmara.nagdi-livestock/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: farmer-reference-v1, values: { farmer_id: FR-000002 } } } - derivationSelectorInputs: { subject: { profile: farmer-reference-v1, values: { farmer_id: FR-000002 } } } - expectedRequestParts: { query: [{ name: farmer_id, value: FR-000002 }, { name: fields, value: "farmer_id,registered_herd,herd_vaccination_current,origin_district_not_quarantined_for_species,destination_district_open,no_conflicting_open_movement_permit" }, { name: limit, value: "2" }], body: null } - expectedTransport: - path: /v1/datasets/nagdi_herd/entities/nagdi_herd/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/livestock-movement-control }] -cases: - - id: positive - response: - data: [{ farmer_id: FR-000002, registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: true, destination_district_open: true, no_conflicting_open_movement_permit: true }] - pagination: { has_more: false } - expected: - lookup: match - derivationRuns: true - signed: true - values: - https://id.registrystack.org/solmara/concept/registered-herd: true - https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species: true - https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit: true - - id: negative-subject-mismatch - response: - data: [{ farmer_id: FR-999999, registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: true, destination_district_open: true, no_conflicting_open_movement_permit: true }] - pagination: { has_more: false } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-quarantined - response: - data: [{ farmer_id: FR-000002, registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: false, destination_district_open: true, no_conflicting_open_movement_permit: true }] - pagination: { has_more: false } - expected: - lookup: match - derivationRuns: true - signed: true - values: - https://id.registrystack.org/solmara/concept/registered-herd: true - https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species: false - https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit: false - - id: missing-fact - response: - data: [{ farmer_id: FR-000002, registered_herd: true, herd_vaccination_current: true, destination_district_open: true, no_conflicting_open_movement_permit: true }] - pagination: { has_more: false } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: - data: - - { farmer_id: FR-000002, registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: true, destination_district_open: true, no_conflicting_open_movement_permit: true } - - { farmer_id: FR-000002, registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: false, destination_district_open: true, no_conflicting_open_movement_permit: true } - pagination: { has_more: true } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/registered-herd, https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species, https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit] - evidenceExcludes: [farmer_id, registered_herd, origin_district_not_quarantined_for_species, FR-000002] - diagnosticsExclude: [FR-000002] diff --git a/evidence/bundle/fixtures/nagdi-voucher.yaml b/evidence/bundle/fixtures/nagdi-voucher.yaml deleted file mode 100644 index 7d22ad0..0000000 --- a/evidence/bundle/fixtures/nagdi-voucher.yaml +++ /dev/null @@ -1,67 +0,0 @@ -fixture: registry.evidence.reference.solmara.nagdi-voucher/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: farmer-reference-v1, values: { farmer_id: FR-000001 } } } - derivationSelectorInputs: { subject: { profile: farmer-reference-v1, values: { farmer_id: FR-000001 } } } - expectedRequestParts: - query: [{ name: farmer_id, value: FR-000001 }, { name: fields, value: "farmer_id,farmer_registered,data_use_authorized,active_smallholder_farmer,active_farm_parcel,crop_declared_for_season,district_climate_risk_active,voucher_entitlement_current,voucher_not_redeemed" }, { name: limit, value: "2" }] - body: null - expectedTransport: - path: /v1/datasets/nagdi_farmer/entities/nagdi_farmer/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review }] -cases: - - id: positive - response: - data: [{ farmer_id: FR-000001, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: true }] - pagination: { has_more: false } - expected: - lookup: match - derivationRuns: true - signed: true - values: - https://id.registrystack.org/solmara/concept/farmer-registered: true - https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose: true - https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher: true - - id: negative-subject-mismatch - response: - data: [{ farmer_id: FR-999999, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: true }] - pagination: { has_more: false } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-redeemed - response: - data: [{ farmer_id: FR-000001, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: false }] - pagination: { has_more: false } - expected: - lookup: match - derivationRuns: true - signed: true - values: - https://id.registrystack.org/solmara/concept/farmer-registered: true - https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose: true - https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher: false - - id: missing-fact - response: - data: [{ farmer_id: FR-000001, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true }] - pagination: { has_more: false } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: - data: - - { farmer_id: FR-000001, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: true } - - { farmer_id: FR-000001, farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: false } - pagination: { has_more: true } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/farmer-registered, https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose, https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher] - evidenceExcludes: [farmer_id, farmer_registered, data_use_authorized, FR-000001] - diagnosticsExclude: [FR-000001] diff --git a/evidence/bundle/fixtures/nia-child-benefit.yaml b/evidence/bundle/fixtures/nia-child-benefit.yaml deleted file mode 100644 index 1ed5d4b..0000000 --- a/evidence/bundle/fixtures/nia-child-benefit.yaml +++ /dev/null @@ -1,39 +0,0 @@ -fixture: registry.evidence.reference.solmara.nia-child-benefit/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000004" } } } - derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000004" } } } - expectedRequestParts: { query: [{ name: uin, value: "1000000004" }, { name: fields, value: "uin,identity_status,alive" }, { name: limit, value: "2" }], body: null } - expectedTransport: - path: /v1/datasets/population/entities/population/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review }] -cases: - - id: positive - response: { data: [{ uin: "1000000004", identity_status: active, alive: true }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: true } - - id: negative-subject-mismatch - response: { data: [{ uin: "9999999999", identity_status: active, alive: true }], pagination: { has_more: false } } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-inactive - response: { data: [{ uin: "1000000004", identity_status: inactive, alive: true }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: false } - - id: missing-fact - response: { data: [{ uin: "1000000004", identity_status: active }], pagination: { has_more: false } } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: { data: [{ uin: "1000000004", identity_status: active, alive: true }, { uin: "1000000004", identity_status: inactive, alive: true }], pagination: { has_more: true } } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/population-record-active] - evidenceExcludes: [uin, identity_status, alive, "1000000004"] - diagnosticsExclude: ["1000000004"] diff --git a/evidence/bundle/fixtures/nia-citizen.yaml b/evidence/bundle/fixtures/nia-citizen.yaml deleted file mode 100644 index c3203aa..0000000 --- a/evidence/bundle/fixtures/nia-citizen.yaml +++ /dev/null @@ -1,39 +0,0 @@ -fixture: registry.evidence.reference.solmara.nia-citizen/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000005" } } } - derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000005" } } } - expectedRequestParts: { query: [{ name: uin, value: "1000000005" }, { name: fields, value: "uin,identity_status,alive" }, { name: limit, value: "2" }], body: null } - expectedTransport: - path: /v1/datasets/population/entities/population/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/citizen-self-service }] -cases: - - id: positive - response: { data: [{ uin: "1000000005", identity_status: active, alive: true }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: true } - - id: negative-subject-mismatch - response: { data: [{ uin: "9999999999", identity_status: active, alive: true }], pagination: { has_more: false } } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-deceased - response: { data: [{ uin: "1000000005", identity_status: active, alive: false }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: false } - - id: missing-fact - response: { data: [{ uin: "1000000005", alive: true }], pagination: { has_more: false } } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: { data: [{ uin: "1000000005", identity_status: active, alive: true }, { uin: "1000000005", identity_status: inactive, alive: true }], pagination: { has_more: true } } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/citizen-population-record-active] - evidenceExcludes: [uin, identity_status, alive, "1000000005"] - diagnosticsExclude: ["1000000005"] diff --git a/evidence/bundle/fixtures/sipf-pension.yaml b/evidence/bundle/fixtures/sipf-pension.yaml deleted file mode 100644 index 00b410e..0000000 --- a/evidence/bundle/fixtures/sipf-pension.yaml +++ /dev/null @@ -1,39 +0,0 @@ -fixture: registry.evidence.reference.solmara.sipf-pension/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000008" } } } - derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000008" } } } - expectedRequestParts: { query: [{ name: pensioner_uin, value: "1000000008" }, { name: fields, value: "pensioner_uin,payment_status" }, { name: limit, value: "2" }], body: null } - expectedTransport: - path: /v1/datasets/pension/entities/pension/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/pension-payment-review }] -cases: - - id: positive - response: { data: [{ pensioner_uin: "1000000008", payment_status: active }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: true } - - id: negative-subject-mismatch - response: { data: [{ pensioner_uin: "9999999999", payment_status: active }], pagination: { has_more: false } } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-suspended - response: { data: [{ pensioner_uin: "1000000008", payment_status: suspended }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: false } - - id: missing-fact - response: { data: [{ pensioner_uin: "1000000008" }], pagination: { has_more: false } } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: { data: [{ pensioner_uin: "1000000008", payment_status: active }, { pensioner_uin: "1000000008", payment_status: suspended }], pagination: { has_more: true } } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/pension-payment-active] - evidenceExcludes: [pensioner_uin, payment_status, "1000000008"] - diagnosticsExclude: ["1000000008"] diff --git a/evidence/bundle/fixtures/sipf-survivor.yaml b/evidence/bundle/fixtures/sipf-survivor.yaml deleted file mode 100644 index 97b1f9c..0000000 --- a/evidence/bundle/fixtures/sipf-survivor.yaml +++ /dev/null @@ -1,39 +0,0 @@ -fixture: registry.evidence.reference.solmara.sipf-survivor/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000009" } } } - derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000009" } } } - expectedRequestParts: { query: [{ name: spouse_uin, value: "1000000009" }, { name: fields, value: "spouse_uin,survivor_eligible" }, { name: limit, value: "2" }], body: null } - expectedTransport: - path: /v1/datasets/survivor/entities/survivor/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination }] -cases: - - id: positive - response: { data: [{ spouse_uin: "1000000009", survivor_eligible: true }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: true } - - id: negative-subject-mismatch - response: { data: [{ spouse_uin: "9999999999", survivor_eligible: true }], pagination: { has_more: false } } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-ineligible - response: { data: [{ spouse_uin: "1000000009", survivor_eligible: false }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: false } - - id: missing-fact - response: { data: [{ spouse_uin: "1000000009" }], pagination: { has_more: false } } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: { data: [{ spouse_uin: "1000000009", survivor_eligible: true }, { spouse_uin: "1000000009", survivor_eligible: false }], pagination: { has_more: true } } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/survivor-is-eligible] - evidenceExcludes: [spouse_uin, survivor_eligible, "1000000009"] - diagnosticsExclude: ["1000000009"] diff --git a/evidence/bundle/fixtures/sro-child-benefit.yaml b/evidence/bundle/fixtures/sro-child-benefit.yaml deleted file mode 100644 index a4cb64a..0000000 --- a/evidence/bundle/fixtures/sro-child-benefit.yaml +++ /dev/null @@ -1,39 +0,0 @@ -fixture: registry.evidence.reference.solmara.sro-child-benefit/v1 -synthetic_only: true -common: - observed_at: "2026-07-14T00:00:00Z" - selectors: { subject: { profile: solmara-uin-v1, values: { uin: "1000000006" } } } - derivationSelectorInputs: { subject: { profile: solmara-uin-v1, values: { uin: "1000000006" } } } - expectedRequestParts: { query: [{ name: uin, value: "1000000006" }, { name: fields, value: "uin,poverty_band" }, { name: limit, value: "2" }], body: null } - expectedTransport: - path: /v1/datasets/sro_child/entities/sro_child/records - fixedHeaders: [{ name: Accept, value: application/json }, { name: Data-Purpose, value: https://id.registrystack.org/solmara/purpose/child-benefit-review }] -cases: - - id: positive - response: { data: [{ uin: "1000000006", poverty_band: priority }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: true } - - id: negative-subject-mismatch - response: { data: [{ uin: "9999999999", poverty_band: priority }], pagination: { has_more: false } } - expected: { lookup: match, error: derivation_input_error, publicProblem: evidence_not_available, derivationRuns: true, signed: false } - - id: boundary-non-priority - response: { data: [{ uin: "1000000006", poverty_band: standard }], pagination: { has_more: false } } - expected: { lookup: match, derivationRuns: true, signed: true, value: false } - - id: missing-fact - response: { data: [{ uin: "1000000006" }], pagination: { has_more: false } } - expected: { publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: no-match - response: { data: [], pagination: { has_more: false } } - expected: { lookup: no_match, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: ambiguous - response: { data: [{ uin: "1000000006", poverty_band: priority }, { uin: "1000000006", poverty_band: standard }], pagination: { has_more: true } } - expected: { lookup: ambiguous, publicProblem: evidence_not_available, derivationRuns: false, signed: false } - - id: source-failure - sourceFailure: timeout - expected: { publicProblem: dependency_unavailable, signed: false } - - id: anti-reconstruction - bundleMutation: duplicate-disclosure-family - expected: { bundle: rejected } -privacyExpectation: - evidenceContains: [https://id.registrystack.org/solmara/concept/household-below-poverty-threshold] - evidenceExcludes: [uin, poverty_band, "1000000006"] - diagnosticsExclude: ["1000000006"] diff --git a/evidence/bundle/schemas/adapter-parameters.schema.yaml b/evidence/bundle/schemas/adapter-parameters.schema.yaml deleted file mode 100644 index 1adeb08..0000000 --- a/evidence/bundle/schemas/adapter-parameters.schema.yaml +++ /dev/null @@ -1,7 +0,0 @@ -type: object -additionalProperties: false -required: [selectorField, providerFields, resultLimit] -properties: - selectorField: { type: string, enum: [uin, pensioner_uin, spouse_uin, farmer_id] } - providerFields: { type: string, minLength: 1, maxLength: 512 } - resultLimit: { const: "2" } diff --git a/evidence/bundle/schemas/cra-child-benefit-facts.schema.yaml b/evidence/bundle/schemas/cra-child-benefit-facts.schema.yaml deleted file mode 100644 index 87091dc..0000000 --- a/evidence/bundle/schemas/cra-child-benefit-facts.schema.yaml +++ /dev/null @@ -1,8 +0,0 @@ -type: object -additionalProperties: false -required: [uin, birth_date, birth_registered, deceased] -properties: - uin: { type: string, minLength: 10, maxLength: 10 } - birth_date: { type: string, format: date, maxLength: 10 } - birth_registered: { type: boolean } - deceased: { type: boolean } diff --git a/evidence/bundle/schemas/cra-citizen-facts.schema.yaml b/evidence/bundle/schemas/cra-citizen-facts.schema.yaml deleted file mode 100644 index 73c2808..0000000 --- a/evidence/bundle/schemas/cra-citizen-facts.schema.yaml +++ /dev/null @@ -1,5 +0,0 @@ -type: object -additionalProperties: false -required: [uin] -properties: - uin: { type: string, minLength: 10, maxLength: 10 } diff --git a/evidence/bundle/schemas/cra-pension-facts.schema.yaml b/evidence/bundle/schemas/cra-pension-facts.schema.yaml deleted file mode 100644 index 3dfc2b3..0000000 --- a/evidence/bundle/schemas/cra-pension-facts.schema.yaml +++ /dev/null @@ -1,6 +0,0 @@ -type: object -additionalProperties: false -required: [uin, deceased] -properties: - uin: { type: string, minLength: 10, maxLength: 10 } - deceased: { type: boolean } diff --git a/evidence/bundle/schemas/mosd-child-benefit-facts.schema.yaml b/evidence/bundle/schemas/mosd-child-benefit-facts.schema.yaml deleted file mode 100644 index cbcbc37..0000000 --- a/evidence/bundle/schemas/mosd-child-benefit-facts.schema.yaml +++ /dev/null @@ -1,6 +0,0 @@ -type: object -additionalProperties: false -required: [uin, duplicate_flag] -properties: - uin: { type: string, minLength: 10, maxLength: 10 } - duplicate_flag: { type: boolean } diff --git a/evidence/bundle/schemas/nagdi-livestock-facts.schema.yaml b/evidence/bundle/schemas/nagdi-livestock-facts.schema.yaml deleted file mode 100644 index 141fc31..0000000 --- a/evidence/bundle/schemas/nagdi-livestock-facts.schema.yaml +++ /dev/null @@ -1,10 +0,0 @@ -type: object -additionalProperties: false -required: [farmer_id, registered_herd, herd_vaccination_current, origin_district_not_quarantined_for_species, destination_district_open, no_conflicting_open_movement_permit] -properties: - farmer_id: { type: string, minLength: 7, maxLength: 16 } - registered_herd: { type: boolean } - herd_vaccination_current: { type: boolean } - origin_district_not_quarantined_for_species: { type: boolean } - destination_district_open: { type: boolean } - no_conflicting_open_movement_permit: { type: boolean } diff --git a/evidence/bundle/schemas/nagdi-voucher-facts.schema.yaml b/evidence/bundle/schemas/nagdi-voucher-facts.schema.yaml deleted file mode 100644 index 1678c09..0000000 --- a/evidence/bundle/schemas/nagdi-voucher-facts.schema.yaml +++ /dev/null @@ -1,13 +0,0 @@ -type: object -additionalProperties: false -required: [farmer_id, farmer_registered, data_use_authorized, active_smallholder_farmer, active_farm_parcel, crop_declared_for_season, district_climate_risk_active, voucher_entitlement_current, voucher_not_redeemed] -properties: - farmer_id: { type: string, minLength: 7, maxLength: 16 } - farmer_registered: { type: boolean } - data_use_authorized: { type: boolean } - active_smallholder_farmer: { type: boolean } - active_farm_parcel: { type: boolean } - crop_declared_for_season: { type: boolean } - district_climate_risk_active: { type: boolean } - voucher_entitlement_current: { type: boolean } - voucher_not_redeemed: { type: boolean } diff --git a/evidence/bundle/schemas/nia-population-facts.schema.yaml b/evidence/bundle/schemas/nia-population-facts.schema.yaml deleted file mode 100644 index 5df515c..0000000 --- a/evidence/bundle/schemas/nia-population-facts.schema.yaml +++ /dev/null @@ -1,7 +0,0 @@ -type: object -additionalProperties: false -required: [uin, identity_status, alive] -properties: - uin: { type: string, minLength: 10, maxLength: 10 } - identity_status: { type: string, minLength: 1, maxLength: 32 } - alive: { type: boolean } diff --git a/evidence/bundle/schemas/response.schema.yaml b/evidence/bundle/schemas/response.schema.yaml deleted file mode 100644 index c2fcecf..0000000 --- a/evidence/bundle/schemas/response.schema.yaml +++ /dev/null @@ -1,45 +0,0 @@ -type: object -additionalProperties: false -required: [data, pagination] -properties: - data: - type: array - minItems: 0 - maxItems: 2 - items: - type: object - additionalProperties: false - required: [] - properties: - uin: { type: string, minLength: 10, maxLength: 10 } - pensioner_uin: { type: string, minLength: 10, maxLength: 10 } - spouse_uin: { type: string, minLength: 10, maxLength: 10 } - farmer_id: { type: string, minLength: 7, maxLength: 16 } - birth_date: { type: string, format: date, maxLength: 10 } - birth_brn: { type: [string, "null"], maxLength: 32 } - deceased: { type: boolean } - identity_status: { type: string, maxLength: 32 } - alive: { type: boolean } - poverty_band: { type: string, maxLength: 16 } - duplicate_flag: { type: boolean } - payment_status: { type: string, maxLength: 32 } - survivor_eligible: { type: boolean } - farmer_registered: { type: boolean } - data_use_authorized: { type: boolean } - active_smallholder_farmer: { type: boolean } - active_farm_parcel: { type: boolean } - crop_declared_for_season: { type: boolean } - district_climate_risk_active: { type: boolean } - voucher_entitlement_current: { type: boolean } - voucher_not_redeemed: { type: boolean } - registered_herd: { type: boolean } - herd_vaccination_current: { type: boolean } - origin_district_not_quarantined_for_species: { type: boolean } - destination_district_open: { type: boolean } - no_conflicting_open_movement_permit: { type: boolean } - pagination: - type: object - additionalProperties: false - required: [has_more] - properties: - has_more: { type: boolean } diff --git a/evidence/bundle/schemas/sipf-pension-facts.schema.yaml b/evidence/bundle/schemas/sipf-pension-facts.schema.yaml deleted file mode 100644 index 0c7b7a8..0000000 --- a/evidence/bundle/schemas/sipf-pension-facts.schema.yaml +++ /dev/null @@ -1,6 +0,0 @@ -type: object -additionalProperties: false -required: [pensioner_uin, payment_status] -properties: - pensioner_uin: { type: string, minLength: 10, maxLength: 10 } - payment_status: { type: string, minLength: 1, maxLength: 32 } diff --git a/evidence/bundle/schemas/sipf-survivor-facts.schema.yaml b/evidence/bundle/schemas/sipf-survivor-facts.schema.yaml deleted file mode 100644 index 45ba70b..0000000 --- a/evidence/bundle/schemas/sipf-survivor-facts.schema.yaml +++ /dev/null @@ -1,6 +0,0 @@ -type: object -additionalProperties: false -required: [spouse_uin, survivor_eligible] -properties: - spouse_uin: { type: string, minLength: 10, maxLength: 10 } - survivor_eligible: { type: boolean } diff --git a/evidence/bundle/schemas/sro-child-benefit-facts.schema.yaml b/evidence/bundle/schemas/sro-child-benefit-facts.schema.yaml deleted file mode 100644 index f3dd348..0000000 --- a/evidence/bundle/schemas/sro-child-benefit-facts.schema.yaml +++ /dev/null @@ -1,6 +0,0 @@ -type: object -additionalProperties: false -required: [uin, poverty_band] -properties: - uin: { type: string, minLength: 10, maxLength: 10 } - poverty_band: { type: string, minLength: 1, maxLength: 16 } diff --git a/evidence/cells/cra/bundle/adapters/birth-extract.rhai b/evidence/cells/cra/bundle/adapters/birth-extract.rhai new file mode 100644 index 0000000..3a59cd9 --- /dev/null +++ b/evidence/cells/cra/bundle/adapters/birth-extract.rhai @@ -0,0 +1,7 @@ +fn extract(source_response, context) { + let rows = source_response["rows"]; + if rows.len == 0 { return #{outcome: "no_match"}; } + if rows.len > 1 { return #{outcome: "ambiguous"}; } + let row = rows[0]; + #{outcome: "match", facts: #{uin: row["uin"], birth_date: row["birth_date"], birth_registered: !is_missing(row["birth_brn"])}} +} diff --git a/evidence/cells/cra/bundle/adapters/relay-extract.rhai b/evidence/cells/cra/bundle/adapters/relay-extract.rhai new file mode 100644 index 0000000..8009963 --- /dev/null +++ b/evidence/cells/cra/bundle/adapters/relay-extract.rhai @@ -0,0 +1,5 @@ +fn extract(source_response, context) { + let domain = source_response["data"]["domainData"]; + if domain.contains("deceased") { return #{outcome: "match", facts: #{deceased: domain["deceased"]}}; } + #{outcome: "match", facts: #{linkedUin: domain["linkedUin"]}} +} diff --git a/evidence/cells/cra/bundle/adapters/relay-prepare.rhai b/evidence/cells/cra/bundle/adapters/relay-prepare.rhai new file mode 100644 index 0000000..b4d5dc7 --- /dev/null +++ b/evidence/cells/cra/bundle/adapters/relay-prepare.rhai @@ -0,0 +1,5 @@ +fn prepare(selectors, context) { + let parameters = context["parameters"]; + let subject = selectors["subject"]["values"]; + #{query: [#{name: "fields", value: parameters["fields"]}], body: #{selectors: #{uin: subject["uin"]}}} +} diff --git a/evidence/cells/cra/bundle/derivations/child-benefit.rhai b/evidence/cells/cra/bundle/derivations/child-benefit.rhai new file mode 100644 index 0000000..5383a03 --- /dev/null +++ b/evidence/cells/cra/bundle/derivations/child-benefit.rhai @@ -0,0 +1,9 @@ +fn derive(facts, selectors, evaluation_context) { + let birth_date = parse_date(required(facts["birth_date"], "required_fact_missing")); + let fifth_birthday = add_calendar_years(birth_date, 5); + let as_of_date = parse_date(evaluation_context["parameters"]["as_of_date"]); + [ + #{concept_id: "https://id.registrystack.org/solmara/concept/birth-is-registered", value: required(facts["birth_registered"], "required_fact_missing")}, + #{concept_id: "https://id.registrystack.org/solmara/concept/child-age-under-5", value: compare_dates(as_of_date, birth_date) >= 0 && compare_dates(as_of_date, fifth_birthday) < 0} + ] +} diff --git a/evidence/bundle/derivations/cra-deceased.rhai b/evidence/cells/cra/bundle/derivations/deceased.rhai similarity index 57% rename from evidence/bundle/derivations/cra-deceased.rhai rename to evidence/cells/cra/bundle/derivations/deceased.rhai index 8f00bfe..df6d5fc 100644 --- a/evidence/bundle/derivations/cra-deceased.rhai +++ b/evidence/cells/cra/bundle/derivations/deceased.rhai @@ -1,6 +1,3 @@ fn derive(facts, selectors, evaluation_context) { - if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { - throw("derivation_input_error"); - } [#{concept_id: "https://id.registrystack.org/solmara/concept/person-is-deceased", value: required(facts["deceased"], "required_fact_missing")}] } diff --git a/evidence/bundle/derivations/cra-linked.rhai b/evidence/cells/cra/bundle/derivations/linked.rhai similarity index 51% rename from evidence/bundle/derivations/cra-linked.rhai rename to evidence/cells/cra/bundle/derivations/linked.rhai index 5aba6fd..a7323a1 100644 --- a/evidence/bundle/derivations/cra-linked.rhai +++ b/evidence/cells/cra/bundle/derivations/linked.rhai @@ -1,6 +1,4 @@ fn derive(facts, selectors, evaluation_context) { - if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { - throw("derivation_input_error"); - } + required(facts["linkedUin"], "required_fact_missing"); [#{concept_id: "https://id.registrystack.org/solmara/concept/civil-record-linked", value: true}] } diff --git a/evidence/cells/cra/bundle/evidence.yaml b/evidence/cells/cra/bundle/evidence.yaml new file mode 100644 index 0000000..487e7b9 --- /dev/null +++ b/evidence/cells/cra/bundle/evidence.yaml @@ -0,0 +1,128 @@ +version: 1 +assuranceProfile: production +service: { providerId: https://id.registrystack.org/solmara/evidence/cra, trustDomain: https://id.registrystack.org/solmara/trust-domain/lab } +issuer: { id: did:web:id.registrystack.org:solmara:authority:cra } +authentication: { kind: oidc-access-token, issuer: https://mint.solmara.registrystack.org, audiences: [solmara-runtime], tokenTypes: [at+jwt], algorithms: [ES256], jwksUri: https://mint.solmara.registrystack.org/.well-known/jwks.json, principalClaim: sub, requesterTagsClaim: evidence_tags, evidenceAudienceClaim: evidence_audience, grantIdClaim: evidence_grant_id, grantAuthorityClaim: evidence_authority, maximumTokenLifetimeSeconds: 300, revokedKeyIds: [] } +audit: { format: keyed-jsonl, hashSecretRef: secret:file/audit-hmac-key, hashKeyVersion: 1, failClosed: true } +subjectBinding: { secretRef: secret:file/subject-binding-hmac-key, keyVersion: 1 } +rateLimits: { requestsPerPrincipalPerMinute: 60, burstPerPrincipal: 10, failedSelectorAttemptsPerPrincipalAuthorityPerMinute: 10 } +signing: { format: flattened-jws-json, algorithm: ES256, activePublicJwkFile: public-keys/cra.jwk.json, publishedPublicJwkFiles: [], revokedKeyIds: [], jwksPath: /.well-known/evidence/jwks.json, maximumAssertionValiditySeconds: 3600, verifierClockSkewSeconds: 30 } +responseFormats: [signed-jws] +selectorProfiles: + solmara-uin-v1: { maximumAggregateBytes: 16, fields: { uin: { type: string, minimumBytes: 10, maximumBytes: 10 } } } +sources: + cra-child-benefit: + transport: sqlite-extract + extractProfile: cra-birth-extract + posture: source-derived + maximumExtractAgeSeconds: 86400 + request: + statement: queries/birth-evidence.sql + columns: [{ name: uin, type: string }, { name: birth_date, type: string }, { name: birth_brn, type: string }] + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + parameterBindings: { uin: { kind: selector, role: subject, profile: solmara-uin-v1, field: uin } } + projection: [/rows/*/uin, /rows/*/birth_date, /rows/*/birth_brn] + maximumRows: 2 + maximumCellBytes: 256 + maximumStatementSteps: 100000 + timeoutMilliseconds: 2000 + maximumResponseBytes: 8192 + concurrencyLimit: 8 + responseSchema: schemas/birth-response.schema.yaml + extractScript: adapters/birth-extract.rhai + factSchema: schemas/birth-facts.schema.yaml + cra-pension: + transport: http-json + baseUrl: https://cra-relay.solmara.registrystack.org + posture: field-projected + authentication: { kind: oauth2-client-credentials, tokenEndpoint: https://mint.solmara.registrystack.org/token, clientIdRef: secret:file/cra-pension-evidence-client-id, clientAssertionKeyRef: secret:file/cra-pension-evidence-client-key, clientAssertionAudience: https://mint.solmara.registrystack.org/token, scope: 'solmara:relay:cra:death-by-uin', audience: solmara-runtime, maximumCacheSeconds: 60 } + unresolvedProblem: &relay-unresolved { status: 404, type: https://id.registrystack.org/problems/registry-relay/consultation/unresolved, code: consultation.unresolved } + request: + method: POST + path: /v2/resources/civil-person/lookups/death-by-uin + fixedHeaders: [{ name: Accept, value: application/json }] + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/relay-prepare.rhai + adapterParameters: { fields: deceased } + adapterParametersSchema: schemas/relay-adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: required, maximumQueryPairs: 1, maximumQueryNameBytes: 16, maximumQueryValueBytes: 64, maximumNormalizedBytes: 1024 } + projection: [/data/domainData/deceased] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/deceased-response.schema.yaml + extractScript: adapters/relay-extract.rhai + factSchema: schemas/deceased-facts.schema.yaml + cra-citizen: + transport: http-json + baseUrl: https://cra-relay.solmara.registrystack.org + posture: field-projected + authentication: { kind: oauth2-client-credentials, tokenEndpoint: https://mint.solmara.registrystack.org/token, clientIdRef: secret:file/cra-citizen-evidence-client-id, clientAssertionKeyRef: secret:file/cra-citizen-evidence-client-key, clientAssertionAudience: https://mint.solmara.registrystack.org/token, scope: 'solmara:relay:cra:citizen-link-by-uin', audience: solmara-runtime, maximumCacheSeconds: 60 } + unresolvedProblem: *relay-unresolved + request: + method: POST + path: /v2/resources/civil-person/lookups/citizen-link-by-uin + fixedHeaders: [{ name: Accept, value: application/json }] + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/relay-prepare.rhai + adapterParameters: { fields: linkedUin } + adapterParametersSchema: schemas/relay-adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: required, maximumQueryPairs: 1, maximumQueryNameBytes: 16, maximumQueryValueBytes: 64, maximumNormalizedBytes: 1024 } + projection: [/data/domainData/linkedUin] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/linked-response.schema.yaml + extractScript: adapters/relay-extract.rhai + factSchema: schemas/linked-facts.schema.yaml +authorityProfiles: + cra-consumers-v1: + kind: statutory + requesterTags: [solmara-demo] + grants: + - { requirement: https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1, purpose: child-benefit-review, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] } + - { requirement: https://id.registrystack.org/solmara/requirement/cra-pension-death/v1, purpose: pension-payment-review, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] } + - { requirement: https://id.registrystack.org/solmara/requirement/cra-citizen-record/v1, purpose: citizen-self-service, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] } +requirements: + - id: https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1 + kind: information-requirement + acquisition: { kind: single, source: cra-child-benefit } + purposes: [child-benefit-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/cra-child-benefit/v1 + observationTimezone: Asia/Bangkok + validitySeconds: 3600 + derivation: { script: derivations/child-benefit.rhai, parameters: { as_of_date: '2026-07-14' } } + concepts: [{ id: https://id.registrystack.org/solmara/concept/birth-is-registered, form: boolean, required: true, constraints: {} }, { id: https://id.registrystack.org/solmara/concept/child-age-under-5, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/child-benefit.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/cra-child-benefit] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/cra-pension-death/v1 + kind: information-requirement + acquisition: { kind: single, source: cra-pension } + purposes: [pension-payment-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/social-insurance/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/cra-death-status/v1 + validitySeconds: 300 + derivation: { script: derivations/deceased.rhai, parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/person-is-deceased, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/pension.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/cra-pension] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/cra-citizen-record/v1 + kind: information-requirement + acquisition: { kind: single, source: cra-citizen } + purposes: [citizen-self-service] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/citizen-self-service/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/civil-record-linked/v1 + validitySeconds: 300 + derivation: { script: derivations/linked.rhai, parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/civil-record-linked, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/citizen.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/cra-citizen] } + existenceDisclosure: collapse-unresolved diff --git a/evidence/cells/cra/bundle/fixtures/child-benefit.yaml b/evidence/cells/cra/bundle/fixtures/child-benefit.yaml new file mode 100644 index 0000000..1ac7ac3 --- /dev/null +++ b/evidence/cells/cra/bundle/fixtures/child-benefit.yaml @@ -0,0 +1,42 @@ +fixture: registry.evidence.reference.solmara.cra-child-benefit/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000001' } } } + derivationSelectorInputs: {} + extract: | + CREATE TABLE evidence_extract (published_at TEXT NOT NULL, publisher TEXT NOT NULL, extract_id TEXT NOT NULL); + INSERT INTO evidence_extract VALUES ('2026-07-14T00:00:00Z', 'did:web:id.registrystack.org:solmara:authority:cra', 'cra-birth-2026-07-14'); + CREATE TABLE birth_evidence (uin TEXT NOT NULL, birth_date TEXT NOT NULL, birth_brn TEXT); + INSERT INTO birth_evidence VALUES ('1000000001', '2022-08-01', 'BRN-0001'); + INSERT INTO birth_evidence VALUES ('1000000002', '2021-07-14', NULL); + INSERT INTO birth_evidence VALUES ('1000000006', '2022-08-01', 'BRN-A'); + INSERT INTO birth_evidence VALUES ('1000000006', '2021-08-01', 'BRN-B'); + expectedRequestParts: { parameters: {} } + expectedTransport: { statement: queries/birth-evidence.sql } +cases: + - id: positive + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000001' } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { uin: '1000000001', birth_date: '2022-08-01', birth_registered: true }, values: { https://id.registrystack.org/solmara/concept/birth-is-registered: true, https://id.registrystack.org/solmara/concept/child-age-under-5: true } } + - id: negative-unregistered + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000002' } } } + expected: { lookup: match, derivationRuns: true, signed: true, values: { https://id.registrystack.org/solmara/concept/birth-is-registered: false, https://id.registrystack.org/solmara/concept/child-age-under-5: false } } + - id: boundary-fifth-birthday + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000002' } } } + expected: { lookup: match, derivationRuns: true, signed: true, values: { https://id.registrystack.org/solmara/concept/birth-is-registered: false, https://id.registrystack.org/solmara/concept/child-age-under-5: false } } + - id: missing-record + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000098' } } } + expected: { lookup: no_match, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: no-match + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000099' } } } + expected: { lookup: no_match, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: ambiguous-duplicate + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000006' } } } + expected: { lookup: ambiguous, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/birth-is-registered, https://id.registrystack.org/solmara/concept/child-age-under-5], evidenceExcludes: [uin, birth_date, birth_brn, BRN-0001], diagnosticsExclude: [BRN-0001, '1000000001'] } diff --git a/evidence/cells/cra/bundle/fixtures/citizen.yaml b/evidence/cells/cra/bundle/fixtures/citizen.yaml new file mode 100644 index 0000000..8c44a17 --- /dev/null +++ b/evidence/cells/cra/bundle/fixtures/citizen.yaml @@ -0,0 +1,34 @@ +fixture: registry.evidence.reference.solmara.cra-citizen/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000003' } } } + derivationSelectorInputs: {} + expectedRequestParts: { query: [{ name: fields, value: linkedUin }], body: { selectors: { uin: '1000000003' } } } + expectedTransport: { path: /v2/resources/civil-person/lookups/citizen-link-by-uin, fixedHeaders: [{ name: Accept, value: application/json }] } +cases: + - id: positive + response: { data: { domainData: { linkedUin: '1000000003' } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { linkedUin: '1000000003' }, value: true } + - id: negative-linked-other-record + response: { data: { domainData: { linkedUin: '9999999999' } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: boundary-linked-record + response: { data: { domainData: { linkedUin: '1000000003' } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: missing-fact + response: { data: { domainData: {} } } + expected: { publicProblem: source.unavailable, derivationRuns: false, signed: false } + - id: no-match + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: ambiguous-relay-collapse + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/civil-record-linked], evidenceExcludes: [birth_date, '1000000003'], diagnosticsExclude: ['1000000003'] } diff --git a/evidence/cells/cra/bundle/fixtures/pension.yaml b/evidence/cells/cra/bundle/fixtures/pension.yaml new file mode 100644 index 0000000..09e4992 --- /dev/null +++ b/evidence/cells/cra/bundle/fixtures/pension.yaml @@ -0,0 +1,34 @@ +fixture: registry.evidence.reference.solmara.cra-pension/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000002' } } } + derivationSelectorInputs: {} + expectedRequestParts: { query: [{ name: fields, value: deceased }], body: { selectors: { uin: '1000000002' } } } + expectedTransport: { path: /v2/resources/civil-person/lookups/death-by-uin, fixedHeaders: [{ name: Accept, value: application/json }] } +cases: + - id: positive + response: { data: { domainData: { deceased: true } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { deceased: true }, value: true } + - id: negative-living + response: { data: { domainData: { deceased: false } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: boundary-deceased + response: { data: { domainData: { deceased: true } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: missing-fact + response: { data: { domainData: {} } } + expected: { publicProblem: source.unavailable, derivationRuns: false, signed: false } + - id: no-match + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: ambiguous-relay-collapse + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/person-is-deceased], evidenceExcludes: [deceased, '1000000002'], diagnosticsExclude: ['1000000002'] } diff --git a/evidence/cells/cra/bundle/queries/birth-evidence.sql b/evidence/cells/cra/bundle/queries/birth-evidence.sql new file mode 100644 index 0000000..562a8d4 --- /dev/null +++ b/evidence/cells/cra/bundle/queries/birth-evidence.sql @@ -0,0 +1,4 @@ +SELECT uin, birth_date, birth_brn +FROM birth_evidence +WHERE uin = :uin +LIMIT 2; diff --git a/evidence/cells/cra/bundle/schemas/birth-facts.schema.yaml b/evidence/cells/cra/bundle/schemas/birth-facts.schema.yaml new file mode 100644 index 0000000..e5a21ed --- /dev/null +++ b/evidence/cells/cra/bundle/schemas/birth-facts.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [uin, birth_date, birth_registered] +properties: { uin: { type: string, minLength: 10, maxLength: 10 }, birth_date: { type: string, format: date }, birth_registered: { type: boolean } } diff --git a/evidence/cells/cra/bundle/schemas/birth-response.schema.yaml b/evidence/cells/cra/bundle/schemas/birth-response.schema.yaml new file mode 100644 index 0000000..2b98c93 --- /dev/null +++ b/evidence/cells/cra/bundle/schemas/birth-response.schema.yaml @@ -0,0 +1,5 @@ +type: object +additionalProperties: false +required: [rows] +properties: + rows: { type: array, maxItems: 2, items: { type: object, additionalProperties: false, required: [uin, birth_date, birth_brn], properties: { uin: { type: string, minLength: 10, maxLength: 10 }, birth_date: { type: string, format: date }, birth_brn: { type: [string, 'null'], maxLength: 128 } } } } diff --git a/evidence/cells/cra/bundle/schemas/deceased-facts.schema.yaml b/evidence/cells/cra/bundle/schemas/deceased-facts.schema.yaml new file mode 100644 index 0000000..d1a0566 --- /dev/null +++ b/evidence/cells/cra/bundle/schemas/deceased-facts.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [deceased] +properties: { deceased: { type: boolean } } diff --git a/evidence/cells/cra/bundle/schemas/deceased-response.schema.yaml b/evidence/cells/cra/bundle/schemas/deceased-response.schema.yaml new file mode 100644 index 0000000..d4ed80c --- /dev/null +++ b/evidence/cells/cra/bundle/schemas/deceased-response.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [data] +properties: { data: { type: object, additionalProperties: false, required: [domainData], properties: { domainData: { type: object, additionalProperties: false, required: [deceased], properties: { deceased: { type: boolean } } } } } } diff --git a/evidence/cells/cra/bundle/schemas/linked-facts.schema.yaml b/evidence/cells/cra/bundle/schemas/linked-facts.schema.yaml new file mode 100644 index 0000000..ce37feb --- /dev/null +++ b/evidence/cells/cra/bundle/schemas/linked-facts.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [linkedUin] +properties: { linkedUin: { type: string, minLength: 10, maxLength: 10 } } diff --git a/evidence/cells/cra/bundle/schemas/linked-response.schema.yaml b/evidence/cells/cra/bundle/schemas/linked-response.schema.yaml new file mode 100644 index 0000000..961e8e4 --- /dev/null +++ b/evidence/cells/cra/bundle/schemas/linked-response.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [data] +properties: { data: { type: object, additionalProperties: false, required: [domainData], properties: { domainData: { type: object, additionalProperties: false, required: [linkedUin], properties: { linkedUin: { type: string, minLength: 10, maxLength: 10 } } } } } } diff --git a/evidence/cells/cra/bundle/schemas/relay-adapter-parameters.schema.yaml b/evidence/cells/cra/bundle/schemas/relay-adapter-parameters.schema.yaml new file mode 100644 index 0000000..88c2ff3 --- /dev/null +++ b/evidence/cells/cra/bundle/schemas/relay-adapter-parameters.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [fields] +properties: { fields: { type: string, minLength: 1, maxLength: 512 } } diff --git a/evidence/cells/cra/runtime.yaml b/evidence/cells/cra/runtime.yaml new file mode 100644 index 0000000..838fe3b --- /dev/null +++ b/evidence/cells/cra/runtime.yaml @@ -0,0 +1,9 @@ +version: 1 +bundleDirectory: /etc/registry-evidence/cra/bundle +listener: { bindHost: 172.29.1.21, port: 8080, tlsTermination: operator-controlled-upstream, trustProxyIdentityHeaders: false, maximumRequestBytes: 65536, maximumConcurrentRequests: 64, requestTimeoutMilliseconds: 10000, shutdownGraceMilliseconds: 30000 } +secretProviders: { file: { root: /run/secrets/registry-evidence/cra } } +signer: { kind: transit, unixSocketPath: /run/registry-evidence/cra/transit-proxy.sock, mount: transit, keyName: solmara-evidence-cra, keyVersion: 1, timeoutMilliseconds: 2000 } +auditStorage: { path: /var/lib/registry-evidence/cra/audit/evidence.jsonl, maximumFileBytes: 1073741824 } +outboundTls: { systemRoots: true, trustProfiles: {} } +sourceExtracts: + cra-birth-extract: { path: /var/lib/registry-evidence/cra/extracts/cra-birth-20260704T090000Z.sqlite } diff --git a/evidence/cells/mosd-programme/bundle/adapters/relay-extract.rhai b/evidence/cells/mosd-programme/bundle/adapters/relay-extract.rhai new file mode 100644 index 0000000..d7c045a --- /dev/null +++ b/evidence/cells/mosd-programme/bundle/adapters/relay-extract.rhai @@ -0,0 +1 @@ +fn extract(source_response, context) { #{outcome: "match", facts: #{duplicate_flag: source_response["data"]["domainData"]["duplicateEnrolment"]}} } diff --git a/evidence/cells/mosd-programme/bundle/adapters/relay-prepare.rhai b/evidence/cells/mosd-programme/bundle/adapters/relay-prepare.rhai new file mode 100644 index 0000000..b4d5dc7 --- /dev/null +++ b/evidence/cells/mosd-programme/bundle/adapters/relay-prepare.rhai @@ -0,0 +1,5 @@ +fn prepare(selectors, context) { + let parameters = context["parameters"]; + let subject = selectors["subject"]["values"]; + #{query: [#{name: "fields", value: parameters["fields"]}], body: #{selectors: #{uin: subject["uin"]}}} +} diff --git a/evidence/bundle/derivations/not-enrolled.rhai b/evidence/cells/mosd-programme/bundle/derivations/not-enrolled.rhai similarity index 59% rename from evidence/bundle/derivations/not-enrolled.rhai rename to evidence/cells/mosd-programme/bundle/derivations/not-enrolled.rhai index 5f05927..a2b73b5 100644 --- a/evidence/bundle/derivations/not-enrolled.rhai +++ b/evidence/cells/mosd-programme/bundle/derivations/not-enrolled.rhai @@ -1,6 +1,3 @@ fn derive(facts, selectors, evaluation_context) { - if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { - throw("derivation_input_error"); - } [#{concept_id: "https://id.registrystack.org/solmara/concept/not-already-enrolled", value: !required(facts["duplicate_flag"], "required_fact_missing")}] } diff --git a/evidence/cells/mosd-programme/bundle/evidence.yaml b/evidence/cells/mosd-programme/bundle/evidence.yaml new file mode 100644 index 0000000..84a2e8b --- /dev/null +++ b/evidence/cells/mosd-programme/bundle/evidence.yaml @@ -0,0 +1,54 @@ +version: 1 +assuranceProfile: production +service: { providerId: https://id.registrystack.org/solmara/evidence/mosd-programme, trustDomain: https://id.registrystack.org/solmara/trust-domain/lab } +issuer: { id: did:web:id.registrystack.org:solmara:authority:mosd-programme-mis } +authentication: { kind: oidc-access-token, issuer: https://mint.solmara.registrystack.org, audiences: [solmara-runtime], tokenTypes: [at+jwt], algorithms: [ES256], jwksUri: https://mint.solmara.registrystack.org/.well-known/jwks.json, principalClaim: sub, requesterTagsClaim: evidence_tags, evidenceAudienceClaim: evidence_audience, grantIdClaim: evidence_grant_id, grantAuthorityClaim: evidence_authority, maximumTokenLifetimeSeconds: 300, revokedKeyIds: [] } +audit: { format: keyed-jsonl, hashSecretRef: secret:file/audit-hmac-key, hashKeyVersion: 1, failClosed: true } +subjectBinding: { secretRef: secret:file/subject-binding-hmac-key, keyVersion: 1 } +rateLimits: { requestsPerPrincipalPerMinute: 60, burstPerPrincipal: 10, failedSelectorAttemptsPerPrincipalAuthorityPerMinute: 10 } +signing: { format: flattened-jws-json, algorithm: ES256, activePublicJwkFile: public-keys/mosd-programme.jwk.json, publishedPublicJwkFiles: [], revokedKeyIds: [], jwksPath: /.well-known/evidence/jwks.json, maximumAssertionValiditySeconds: 300, verifierClockSkewSeconds: 30 } +responseFormats: [signed-jws] +selectorProfiles: { solmara-uin-v1: { maximumAggregateBytes: 16, fields: { uin: { type: string, minimumBytes: 10, maximumBytes: 10 } } } } +sources: + mosd-child-benefit: + transport: http-json + baseUrl: https://mosd-programme-relay.solmara.registrystack.org + posture: field-projected + authentication: { kind: oauth2-client-credentials, tokenEndpoint: https://mint.solmara.registrystack.org/token, clientIdRef: secret:file/mosd-child-benefit-evidence-client-id, clientAssertionKeyRef: secret:file/mosd-child-benefit-evidence-client-key, clientAssertionAudience: https://mint.solmara.registrystack.org/token, scope: 'solmara:relay:mosd:by-uin', audience: solmara-runtime, maximumCacheSeconds: 60 } + unresolvedProblem: { status: 404, type: https://id.registrystack.org/problems/registry-relay/consultation/unresolved, code: consultation.unresolved } + request: + method: POST + path: /v2/resources/beneficiary-enrolment/lookups/by-uin + fixedHeaders: [{ name: Accept, value: application/json }] + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/relay-prepare.rhai + adapterParameters: { fields: duplicateEnrolment } + adapterParametersSchema: schemas/relay-adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: required, maximumQueryPairs: 1, maximumQueryNameBytes: 16, maximumQueryValueBytes: 64, maximumNormalizedBytes: 1024 } + projection: [/data/domainData/duplicateEnrolment] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/response.schema.yaml + extractScript: adapters/relay-extract.rhai + factSchema: schemas/facts.schema.yaml +authorityProfiles: + mosd-consumers-v1: + kind: statutory + requesterTags: [solmara-demo] + grants: [{ requirement: https://id.registrystack.org/solmara/requirement/mosd-child-benefit/v1, purpose: child-benefit-review, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] }] +requirements: + - id: https://id.registrystack.org/solmara/requirement/mosd-child-benefit/v1 + kind: information-requirement + acquisition: { kind: single, source: mosd-child-benefit } + purposes: [child-benefit-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/not-enrolled/v1 + validitySeconds: 300 + derivation: { script: derivations/not-enrolled.rhai, parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/not-already-enrolled, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/child-benefit.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/mosd-child-benefit] } + existenceDisclosure: collapse-unresolved diff --git a/evidence/cells/mosd-programme/bundle/fixtures/child-benefit.yaml b/evidence/cells/mosd-programme/bundle/fixtures/child-benefit.yaml new file mode 100644 index 0000000..6250f7a --- /dev/null +++ b/evidence/cells/mosd-programme/bundle/fixtures/child-benefit.yaml @@ -0,0 +1,34 @@ +fixture: registry.evidence.reference.solmara.mosd-child-benefit/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000007' } } } + derivationSelectorInputs: {} + expectedRequestParts: { query: [{ name: fields, value: duplicateEnrolment }], body: { selectors: { uin: '1000000007' } } } + expectedTransport: { path: /v2/resources/beneficiary-enrolment/lookups/by-uin, fixedHeaders: [{ name: Accept, value: application/json }] } +cases: + - id: positive + response: { data: { domainData: { duplicateEnrolment: false } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { duplicate_flag: false }, value: true } + - id: negative-duplicate + response: { data: { domainData: { duplicateEnrolment: true } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: boundary-not-duplicate + response: { data: { domainData: { duplicateEnrolment: false } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: missing-fact + response: { data: { domainData: {} } } + expected: { publicProblem: source.unavailable, derivationRuns: false, signed: false } + - id: no-match + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: ambiguous-relay-collapse + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/not-already-enrolled], evidenceExcludes: [duplicate_flag, '1000000007'], diagnosticsExclude: ['1000000007'] } diff --git a/evidence/cells/mosd-programme/bundle/schemas/facts.schema.yaml b/evidence/cells/mosd-programme/bundle/schemas/facts.schema.yaml new file mode 100644 index 0000000..e1858fb --- /dev/null +++ b/evidence/cells/mosd-programme/bundle/schemas/facts.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [duplicate_flag] +properties: { duplicate_flag: { type: boolean } } diff --git a/evidence/cells/mosd-programme/bundle/schemas/relay-adapter-parameters.schema.yaml b/evidence/cells/mosd-programme/bundle/schemas/relay-adapter-parameters.schema.yaml new file mode 100644 index 0000000..88c2ff3 --- /dev/null +++ b/evidence/cells/mosd-programme/bundle/schemas/relay-adapter-parameters.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [fields] +properties: { fields: { type: string, minLength: 1, maxLength: 512 } } diff --git a/evidence/cells/mosd-programme/bundle/schemas/response.schema.yaml b/evidence/cells/mosd-programme/bundle/schemas/response.schema.yaml new file mode 100644 index 0000000..4a5f161 --- /dev/null +++ b/evidence/cells/mosd-programme/bundle/schemas/response.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [data] +properties: { data: { type: object, additionalProperties: false, required: [domainData], properties: { domainData: { type: object, additionalProperties: false, required: [duplicateEnrolment], properties: { duplicateEnrolment: { type: boolean } } } } } } diff --git a/evidence/cells/mosd-programme/runtime.yaml b/evidence/cells/mosd-programme/runtime.yaml new file mode 100644 index 0000000..045b719 --- /dev/null +++ b/evidence/cells/mosd-programme/runtime.yaml @@ -0,0 +1,7 @@ +version: 1 +bundleDirectory: /etc/registry-evidence/mosd-programme/bundle +listener: { bindHost: 172.29.1.24, port: 8080, tlsTermination: operator-controlled-upstream, trustProxyIdentityHeaders: false, maximumRequestBytes: 65536, maximumConcurrentRequests: 64, requestTimeoutMilliseconds: 10000, shutdownGraceMilliseconds: 30000 } +secretProviders: { file: { root: /run/secrets/registry-evidence/mosd-programme } } +signer: { kind: transit, unixSocketPath: /run/registry-evidence/mosd-programme/transit-proxy.sock, mount: transit, keyName: solmara-evidence-mosd-programme, keyVersion: 1, timeoutMilliseconds: 2000 } +auditStorage: { path: /var/lib/registry-evidence/mosd-programme/audit/evidence.jsonl, maximumFileBytes: 1073741824 } +outboundTls: { systemRoots: true, trustProfiles: {} } diff --git a/evidence/cells/nagdi/bundle/adapters/relay-extract.rhai b/evidence/cells/nagdi/bundle/adapters/relay-extract.rhai new file mode 100644 index 0000000..c18c50c --- /dev/null +++ b/evidence/cells/nagdi/bundle/adapters/relay-extract.rhai @@ -0,0 +1,7 @@ +fn extract(source_response, context) { + let domain = source_response["data"]["domainData"]; + if domain.contains("farmerRegistered") { + return #{outcome: "match", facts: #{farmer_registered: domain["farmerRegistered"], data_use_authorized: domain["dataUseAuthorized"], active_smallholder_farmer: domain["activeSmallholderFarmer"], active_farm_parcel: domain["activeFarmParcel"], crop_declared_for_season: domain["cropDeclaredForSeason"], district_climate_risk_active: domain["districtClimateRiskActive"], voucher_entitlement_current: domain["voucherEntitlementCurrent"], voucher_not_redeemed: domain["voucherNotRedeemed"]}}; + } + #{outcome: "match", facts: #{registered_herd: domain["registeredHerd"], herd_vaccination_current: domain["herdVaccinationCurrent"], origin_district_not_quarantined_for_species: domain["originDistrictNotQuarantinedForSpecies"], destination_district_open: domain["destinationDistrictOpen"], no_conflicting_open_movement_permit: domain["noConflictingOpenMovementPermit"]}} +} diff --git a/evidence/cells/nagdi/bundle/adapters/relay-prepare.rhai b/evidence/cells/nagdi/bundle/adapters/relay-prepare.rhai new file mode 100644 index 0000000..add0131 --- /dev/null +++ b/evidence/cells/nagdi/bundle/adapters/relay-prepare.rhai @@ -0,0 +1,5 @@ +fn prepare(selectors, context) { + let parameters = context["parameters"]; + let subject = selectors["subject"]["values"]; + #{query: [#{name: "fields", value: parameters["fields"]}], body: #{selectors: #{farmerId: subject["farmer_id"]}}} +} diff --git a/evidence/bundle/derivations/livestock.rhai b/evidence/cells/nagdi/bundle/derivations/livestock.rhai similarity index 57% rename from evidence/bundle/derivations/livestock.rhai rename to evidence/cells/nagdi/bundle/derivations/livestock.rhai index ce583dc..5585e3d 100644 --- a/evidence/bundle/derivations/livestock.rhai +++ b/evidence/cells/nagdi/bundle/derivations/livestock.rhai @@ -1,13 +1,7 @@ fn derive(facts, selectors, evaluation_context) { - if required(facts["farmer_id"], "required_fact_missing") != selectors["subject"]["values"]["farmer_id"] { - throw("derivation_input_error"); - } let registered = required(facts["registered_herd"], "required_fact_missing"); let not_quarantined = required(facts["origin_district_not_quarantined_for_species"], "required_fact_missing"); - let eligible = registered && not_quarantined && - required(facts["herd_vaccination_current"], "required_fact_missing") && - required(facts["destination_district_open"], "required_fact_missing") && - required(facts["no_conflicting_open_movement_permit"], "required_fact_missing"); + let eligible = registered && not_quarantined && required(facts["herd_vaccination_current"], "required_fact_missing") && required(facts["destination_district_open"], "required_fact_missing") && required(facts["no_conflicting_open_movement_permit"], "required_fact_missing"); [ #{concept_id: "https://id.registrystack.org/solmara/concept/registered-herd", value: registered}, #{concept_id: "https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species", value: not_quarantined}, diff --git a/evidence/cells/nagdi/bundle/derivations/voucher.rhai b/evidence/cells/nagdi/bundle/derivations/voucher.rhai new file mode 100644 index 0000000..3f15a74 --- /dev/null +++ b/evidence/cells/nagdi/bundle/derivations/voucher.rhai @@ -0,0 +1,10 @@ +fn derive(facts, selectors, evaluation_context) { + let registered = required(facts["farmer_registered"], "required_fact_missing"); + let authorized = required(facts["data_use_authorized"], "required_fact_missing"); + let eligible = registered && authorized && required(facts["active_smallholder_farmer"], "required_fact_missing") && required(facts["active_farm_parcel"], "required_fact_missing") && required(facts["crop_declared_for_season"], "required_fact_missing") && required(facts["district_climate_risk_active"], "required_fact_missing") && required(facts["voucher_entitlement_current"], "required_fact_missing") && required(facts["voucher_not_redeemed"], "required_fact_missing"); + [ + #{concept_id: "https://id.registrystack.org/solmara/concept/farmer-registered", value: registered}, + #{concept_id: "https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose", value: authorized}, + #{concept_id: "https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher", value: eligible} + ] +} diff --git a/evidence/cells/nagdi/bundle/evidence.yaml b/evidence/cells/nagdi/bundle/evidence.yaml new file mode 100644 index 0000000..5838edb --- /dev/null +++ b/evidence/cells/nagdi/bundle/evidence.yaml @@ -0,0 +1,92 @@ +version: 1 +assuranceProfile: production +service: { providerId: https://id.registrystack.org/solmara/evidence/nagdi, trustDomain: https://id.registrystack.org/solmara/trust-domain/lab } +issuer: { id: did:web:id.registrystack.org:solmara:authority:nagdi } +authentication: { kind: oidc-access-token, issuer: https://mint.solmara.registrystack.org, audiences: [solmara-runtime], tokenTypes: [at+jwt], algorithms: [ES256], jwksUri: https://mint.solmara.registrystack.org/.well-known/jwks.json, principalClaim: sub, requesterTagsClaim: evidence_tags, evidenceAudienceClaim: evidence_audience, grantIdClaim: evidence_grant_id, grantAuthorityClaim: evidence_authority, maximumTokenLifetimeSeconds: 300, revokedKeyIds: [] } +audit: { format: keyed-jsonl, hashSecretRef: secret:file/audit-hmac-key, hashKeyVersion: 1, failClosed: true } +subjectBinding: { secretRef: secret:file/subject-binding-hmac-key, keyVersion: 1 } +rateLimits: { requestsPerPrincipalPerMinute: 60, burstPerPrincipal: 10, failedSelectorAttemptsPerPrincipalAuthorityPerMinute: 10 } +signing: { format: flattened-jws-json, algorithm: ES256, activePublicJwkFile: public-keys/nagdi.jwk.json, publishedPublicJwkFiles: [], revokedKeyIds: [], jwksPath: /.well-known/evidence/jwks.json, maximumAssertionValiditySeconds: 300, verifierClockSkewSeconds: 30 } +responseFormats: [signed-jws] +selectorProfiles: { farmer-reference-v1: { maximumAggregateBytes: 32, fields: { farmer_id: { type: string, minimumBytes: 7, maximumBytes: 16 } } } } +sources: + nagdi-voucher: + transport: http-json + baseUrl: https://nagdi-relay.solmara.registrystack.org + posture: field-projected + authentication: { kind: oauth2-client-credentials, tokenEndpoint: https://mint.solmara.registrystack.org/token, clientIdRef: secret:file/nagdi-voucher-evidence-client-id, clientAssertionKeyRef: secret:file/nagdi-voucher-evidence-client-key, clientAssertionAudience: https://mint.solmara.registrystack.org/token, scope: 'solmara:relay:nagdi:voucher-by-farmer-id', audience: solmara-runtime, maximumCacheSeconds: 60 } + unresolvedProblem: &relay-unresolved { status: 404, type: https://id.registrystack.org/problems/registry-relay/consultation/unresolved, code: consultation.unresolved } + request: + method: POST + path: /v2/resources/farmer/lookups/voucher-by-farmer-id + fixedHeaders: [{ name: Accept, value: application/json }] + selectorInputs: [{ role: subject, alternatives: [{ profile: farmer-reference-v1, fields: [farmer_id] }] }] + prepareScript: adapters/relay-prepare.rhai + adapterParameters: { fields: 'farmerRegistered,dataUseAuthorized,activeSmallholderFarmer,activeFarmParcel,cropDeclaredForSeason,districtClimateRiskActive,voucherEntitlementCurrent,voucherNotRedeemed' } + adapterParametersSchema: schemas/relay-adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: required, maximumQueryPairs: 1, maximumQueryNameBytes: 16, maximumQueryValueBytes: 512, maximumNormalizedBytes: 2048 } + projection: [/data/domainData/farmerRegistered, /data/domainData/dataUseAuthorized, /data/domainData/activeSmallholderFarmer, /data/domainData/activeFarmParcel, /data/domainData/cropDeclaredForSeason, /data/domainData/districtClimateRiskActive, /data/domainData/voucherEntitlementCurrent, /data/domainData/voucherNotRedeemed] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/voucher-response.schema.yaml + extractScript: adapters/relay-extract.rhai + factSchema: schemas/voucher-facts.schema.yaml + nagdi-livestock: + transport: http-json + baseUrl: https://nagdi-relay.solmara.registrystack.org + posture: field-projected + authentication: { kind: oauth2-client-credentials, tokenEndpoint: https://mint.solmara.registrystack.org/token, clientIdRef: secret:file/nagdi-livestock-evidence-client-id, clientAssertionKeyRef: secret:file/nagdi-livestock-evidence-client-key, clientAssertionAudience: https://mint.solmara.registrystack.org/token, scope: 'solmara:relay:nagdi:movement-by-farmer-id', audience: solmara-runtime, maximumCacheSeconds: 60 } + unresolvedProblem: *relay-unresolved + request: + method: POST + path: /v2/resources/livestock-herd/lookups/movement-by-farmer-id + fixedHeaders: [{ name: Accept, value: application/json }] + selectorInputs: [{ role: subject, alternatives: [{ profile: farmer-reference-v1, fields: [farmer_id] }] }] + prepareScript: adapters/relay-prepare.rhai + adapterParameters: { fields: 'registeredHerd,herdVaccinationCurrent,originDistrictNotQuarantinedForSpecies,destinationDistrictOpen,noConflictingOpenMovementPermit' } + adapterParametersSchema: schemas/relay-adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: required, maximumQueryPairs: 1, maximumQueryNameBytes: 16, maximumQueryValueBytes: 512, maximumNormalizedBytes: 2048 } + projection: [/data/domainData/registeredHerd, /data/domainData/herdVaccinationCurrent, /data/domainData/originDistrictNotQuarantinedForSpecies, /data/domainData/destinationDistrictOpen, /data/domainData/noConflictingOpenMovementPermit] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/livestock-response.schema.yaml + extractScript: adapters/relay-extract.rhai + factSchema: schemas/livestock-facts.schema.yaml +authorityProfiles: + nagdi-consumers-v1: + kind: statutory + requesterTags: [solmara-demo] + grants: + - { requirement: https://id.registrystack.org/solmara/requirement/nagdi-voucher/v1, purpose: voucher-eligibility-review, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: farmer-reference-v1, valueOrigin: request }] } + - { requirement: https://id.registrystack.org/solmara/requirement/nagdi-livestock/v1, purpose: livestock-movement-control, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: farmer-reference-v1, valueOrigin: request }] } +requirements: + - id: https://id.registrystack.org/solmara/requirement/nagdi-voucher/v1 + kind: information-requirement + acquisition: { kind: single, source: nagdi-voucher } + purposes: [voucher-eligibility-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [farmer-reference-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/agriculture/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/climate-smart-voucher/v1 + validitySeconds: 300 + derivation: { script: derivations/voucher.rhai, parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/farmer-registered, form: boolean, required: true, constraints: {} }, { id: https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose, form: boolean, required: true, constraints: {} }, { id: https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/voucher.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nagdi-voucher] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/nagdi-livestock/v1 + kind: information-requirement + acquisition: { kind: single, source: nagdi-livestock } + purposes: [livestock-movement-control] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [farmer-reference-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/agriculture/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/livestock-movement/v1 + validitySeconds: 300 + derivation: { script: derivations/livestock.rhai, parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/registered-herd, form: boolean, required: true, constraints: {} }, { id: https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species, form: boolean, required: true, constraints: {} }, { id: https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/livestock.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nagdi-livestock] } + existenceDisclosure: collapse-unresolved diff --git a/evidence/cells/nagdi/bundle/fixtures/livestock.yaml b/evidence/cells/nagdi/bundle/fixtures/livestock.yaml new file mode 100644 index 0000000..519c671 --- /dev/null +++ b/evidence/cells/nagdi/bundle/fixtures/livestock.yaml @@ -0,0 +1,34 @@ +fixture: registry.evidence.reference.solmara.nagdi-livestock/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: farmer-reference-v1, values: { farmer_id: FR-000002 } } } + derivationSelectorInputs: {} + expectedRequestParts: { query: [{ name: fields, value: 'registeredHerd,herdVaccinationCurrent,originDistrictNotQuarantinedForSpecies,destinationDistrictOpen,noConflictingOpenMovementPermit' }], body: { selectors: { farmerId: FR-000002 } } } + expectedTransport: { path: /v2/resources/livestock-herd/lookups/movement-by-farmer-id, fixedHeaders: [{ name: Accept, value: application/json }] } +cases: + - id: positive + response: { data: { domainData: { registeredHerd: true, herdVaccinationCurrent: true, originDistrictNotQuarantinedForSpecies: true, destinationDistrictOpen: true, noConflictingOpenMovementPermit: true } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { registered_herd: true, herd_vaccination_current: true, origin_district_not_quarantined_for_species: true, destination_district_open: true, no_conflicting_open_movement_permit: true }, values: { https://id.registrystack.org/solmara/concept/registered-herd: true, https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species: true, https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit: true } } + - id: negative-quarantine + response: { data: { domainData: { registeredHerd: true, herdVaccinationCurrent: true, originDistrictNotQuarantinedForSpecies: false, destinationDistrictOpen: true, noConflictingOpenMovementPermit: true } } } + expected: { lookup: match, derivationRuns: true, signed: true, values: { https://id.registrystack.org/solmara/concept/registered-herd: true, https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species: false, https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit: false } } + - id: boundary-eligible + response: { data: { domainData: { registeredHerd: true, herdVaccinationCurrent: true, originDistrictNotQuarantinedForSpecies: true, destinationDistrictOpen: true, noConflictingOpenMovementPermit: true } } } + expected: { lookup: match, derivationRuns: true, signed: true, values: { https://id.registrystack.org/solmara/concept/registered-herd: true, https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species: true, https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit: true } } + - id: missing-fact + response: { data: { domainData: { registeredHerd: true, herdVaccinationCurrent: true, destinationDistrictOpen: true, noConflictingOpenMovementPermit: true } } } + expected: { publicProblem: source.unavailable, derivationRuns: false, signed: false } + - id: no-match + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: ambiguous-relay-collapse + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/registered-herd, https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species, https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit], evidenceExcludes: [farmer_id, FR-000002], diagnosticsExclude: [FR-000002] } diff --git a/evidence/cells/nagdi/bundle/fixtures/voucher.yaml b/evidence/cells/nagdi/bundle/fixtures/voucher.yaml new file mode 100644 index 0000000..17fa957 --- /dev/null +++ b/evidence/cells/nagdi/bundle/fixtures/voucher.yaml @@ -0,0 +1,34 @@ +fixture: registry.evidence.reference.solmara.nagdi-voucher/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: farmer-reference-v1, values: { farmer_id: FR-000001 } } } + derivationSelectorInputs: {} + expectedRequestParts: { query: [{ name: fields, value: 'farmerRegistered,dataUseAuthorized,activeSmallholderFarmer,activeFarmParcel,cropDeclaredForSeason,districtClimateRiskActive,voucherEntitlementCurrent,voucherNotRedeemed' }], body: { selectors: { farmerId: FR-000001 } } } + expectedTransport: { path: /v2/resources/farmer/lookups/voucher-by-farmer-id, fixedHeaders: [{ name: Accept, value: application/json }] } +cases: + - id: positive + response: { data: { domainData: { farmerRegistered: true, dataUseAuthorized: true, activeSmallholderFarmer: true, activeFarmParcel: true, cropDeclaredForSeason: true, districtClimateRiskActive: true, voucherEntitlementCurrent: true, voucherNotRedeemed: true } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { farmer_registered: true, data_use_authorized: true, active_smallholder_farmer: true, active_farm_parcel: true, crop_declared_for_season: true, district_climate_risk_active: true, voucher_entitlement_current: true, voucher_not_redeemed: true }, values: { https://id.registrystack.org/solmara/concept/farmer-registered: true, https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose: true, https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher: true } } + - id: negative-redeemed + response: { data: { domainData: { farmerRegistered: true, dataUseAuthorized: true, activeSmallholderFarmer: true, activeFarmParcel: true, cropDeclaredForSeason: true, districtClimateRiskActive: true, voucherEntitlementCurrent: true, voucherNotRedeemed: false } } } + expected: { lookup: match, derivationRuns: true, signed: true, values: { https://id.registrystack.org/solmara/concept/farmer-registered: true, https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose: true, https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher: false } } + - id: boundary-eligible + response: { data: { domainData: { farmerRegistered: true, dataUseAuthorized: true, activeSmallholderFarmer: true, activeFarmParcel: true, cropDeclaredForSeason: true, districtClimateRiskActive: true, voucherEntitlementCurrent: true, voucherNotRedeemed: true } } } + expected: { lookup: match, derivationRuns: true, signed: true, values: { https://id.registrystack.org/solmara/concept/farmer-registered: true, https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose: true, https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher: true } } + - id: missing-fact + response: { data: { domainData: { farmerRegistered: true, dataUseAuthorized: true, activeSmallholderFarmer: true, activeFarmParcel: true, cropDeclaredForSeason: true, districtClimateRiskActive: true, voucherEntitlementCurrent: true } } } + expected: { publicProblem: source.unavailable, derivationRuns: false, signed: false } + - id: no-match + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: ambiguous-relay-collapse + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/farmer-registered, https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose, https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher], evidenceExcludes: [farmer_id, FR-000001], diagnosticsExclude: [FR-000001] } diff --git a/evidence/cells/nagdi/bundle/schemas/livestock-facts.schema.yaml b/evidence/cells/nagdi/bundle/schemas/livestock-facts.schema.yaml new file mode 100644 index 0000000..c5c9f36 --- /dev/null +++ b/evidence/cells/nagdi/bundle/schemas/livestock-facts.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [registered_herd, herd_vaccination_current, origin_district_not_quarantined_for_species, destination_district_open, no_conflicting_open_movement_permit] +properties: { registered_herd: { type: boolean }, herd_vaccination_current: { type: boolean }, origin_district_not_quarantined_for_species: { type: boolean }, destination_district_open: { type: boolean }, no_conflicting_open_movement_permit: { type: boolean } } diff --git a/evidence/cells/nagdi/bundle/schemas/livestock-response.schema.yaml b/evidence/cells/nagdi/bundle/schemas/livestock-response.schema.yaml new file mode 100644 index 0000000..116dcdc --- /dev/null +++ b/evidence/cells/nagdi/bundle/schemas/livestock-response.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [data] +properties: { data: { type: object, additionalProperties: false, required: [domainData], properties: { domainData: { type: object, additionalProperties: false, required: [registeredHerd, herdVaccinationCurrent, originDistrictNotQuarantinedForSpecies, destinationDistrictOpen, noConflictingOpenMovementPermit], properties: { registeredHerd: { type: boolean }, herdVaccinationCurrent: { type: boolean }, originDistrictNotQuarantinedForSpecies: { type: boolean }, destinationDistrictOpen: { type: boolean }, noConflictingOpenMovementPermit: { type: boolean } } } } } } diff --git a/evidence/cells/nagdi/bundle/schemas/relay-adapter-parameters.schema.yaml b/evidence/cells/nagdi/bundle/schemas/relay-adapter-parameters.schema.yaml new file mode 100644 index 0000000..88c2ff3 --- /dev/null +++ b/evidence/cells/nagdi/bundle/schemas/relay-adapter-parameters.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [fields] +properties: { fields: { type: string, minLength: 1, maxLength: 512 } } diff --git a/evidence/cells/nagdi/bundle/schemas/voucher-facts.schema.yaml b/evidence/cells/nagdi/bundle/schemas/voucher-facts.schema.yaml new file mode 100644 index 0000000..14f53cc --- /dev/null +++ b/evidence/cells/nagdi/bundle/schemas/voucher-facts.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [farmer_registered, data_use_authorized, active_smallholder_farmer, active_farm_parcel, crop_declared_for_season, district_climate_risk_active, voucher_entitlement_current, voucher_not_redeemed] +properties: { farmer_registered: { type: boolean }, data_use_authorized: { type: boolean }, active_smallholder_farmer: { type: boolean }, active_farm_parcel: { type: boolean }, crop_declared_for_season: { type: boolean }, district_climate_risk_active: { type: boolean }, voucher_entitlement_current: { type: boolean }, voucher_not_redeemed: { type: boolean } } diff --git a/evidence/cells/nagdi/bundle/schemas/voucher-response.schema.yaml b/evidence/cells/nagdi/bundle/schemas/voucher-response.schema.yaml new file mode 100644 index 0000000..86fbcb0 --- /dev/null +++ b/evidence/cells/nagdi/bundle/schemas/voucher-response.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [data] +properties: { data: { type: object, additionalProperties: false, required: [domainData], properties: { domainData: { type: object, additionalProperties: false, required: [farmerRegistered, dataUseAuthorized, activeSmallholderFarmer, activeFarmParcel, cropDeclaredForSeason, districtClimateRiskActive, voucherEntitlementCurrent, voucherNotRedeemed], properties: { farmerRegistered: { type: boolean }, dataUseAuthorized: { type: boolean }, activeSmallholderFarmer: { type: boolean }, activeFarmParcel: { type: boolean }, cropDeclaredForSeason: { type: boolean }, districtClimateRiskActive: { type: boolean }, voucherEntitlementCurrent: { type: boolean }, voucherNotRedeemed: { type: boolean } } } } } } diff --git a/evidence/cells/nagdi/runtime.yaml b/evidence/cells/nagdi/runtime.yaml new file mode 100644 index 0000000..73206d3 --- /dev/null +++ b/evidence/cells/nagdi/runtime.yaml @@ -0,0 +1,7 @@ +version: 1 +bundleDirectory: /etc/registry-evidence/nagdi/bundle +listener: { bindHost: 172.29.1.26, port: 8080, tlsTermination: operator-controlled-upstream, trustProxyIdentityHeaders: false, maximumRequestBytes: 65536, maximumConcurrentRequests: 64, requestTimeoutMilliseconds: 10000, shutdownGraceMilliseconds: 30000 } +secretProviders: { file: { root: /run/secrets/registry-evidence/nagdi } } +signer: { kind: transit, unixSocketPath: /run/registry-evidence/nagdi/transit-proxy.sock, mount: transit, keyName: solmara-evidence-nagdi, keyVersion: 1, timeoutMilliseconds: 2000 } +auditStorage: { path: /var/lib/registry-evidence/nagdi/audit/evidence.jsonl, maximumFileBytes: 1073741824 } +outboundTls: { systemRoots: true, trustProfiles: {} } diff --git a/evidence/cells/nia/bundle/adapters/sqlite-extract.rhai b/evidence/cells/nia/bundle/adapters/sqlite-extract.rhai new file mode 100644 index 0000000..483d916 --- /dev/null +++ b/evidence/cells/nia/bundle/adapters/sqlite-extract.rhai @@ -0,0 +1,6 @@ +fn extract(source_response, context) { + let rows = source_response["rows"]; + if rows.len == 0 { return #{outcome: "no_match"}; } + if rows.len > 1 { return #{outcome: "ambiguous"}; } + #{outcome: "match", facts: rows[0]} +} diff --git a/evidence/cells/nia/bundle/derivations/population-active.rhai b/evidence/cells/nia/bundle/derivations/population-active.rhai new file mode 100644 index 0000000..b67df39 --- /dev/null +++ b/evidence/cells/nia/bundle/derivations/population-active.rhai @@ -0,0 +1,4 @@ +fn derive(facts, selectors, evaluation_context) { + let active = required(facts["identity_status"], "required_fact_missing") == "active" && required(facts["alive"], "required_fact_missing"); + [#{concept_id: evaluation_context["parameters"]["concept_id"], value: active}] +} diff --git a/evidence/cells/nia/bundle/evidence.yaml b/evidence/cells/nia/bundle/evidence.yaml new file mode 100644 index 0000000..275d41d --- /dev/null +++ b/evidence/cells/nia/bundle/evidence.yaml @@ -0,0 +1,67 @@ +version: 1 +assuranceProfile: production +service: { providerId: https://id.registrystack.org/solmara/evidence/nia, trustDomain: https://id.registrystack.org/solmara/trust-domain/lab } +issuer: { id: did:web:id.registrystack.org:solmara:authority:nia } +authentication: { kind: oidc-access-token, issuer: https://mint.solmara.registrystack.org, audiences: [solmara-runtime], tokenTypes: [at+jwt], algorithms: [ES256], jwksUri: https://mint.solmara.registrystack.org/.well-known/jwks.json, principalClaim: sub, requesterTagsClaim: evidence_tags, evidenceAudienceClaim: evidence_audience, grantIdClaim: evidence_grant_id, grantAuthorityClaim: evidence_authority, maximumTokenLifetimeSeconds: 300, revokedKeyIds: [] } +audit: { format: keyed-jsonl, hashSecretRef: secret:file/audit-hmac-key, hashKeyVersion: 1, failClosed: true } +subjectBinding: { secretRef: secret:file/subject-binding-hmac-key, keyVersion: 1 } +rateLimits: { requestsPerPrincipalPerMinute: 60, burstPerPrincipal: 10, failedSelectorAttemptsPerPrincipalAuthorityPerMinute: 10 } +signing: { format: flattened-jws-json, algorithm: ES256, activePublicJwkFile: public-keys/nia.jwk.json, publishedPublicJwkFiles: [], revokedKeyIds: [], jwksPath: /.well-known/evidence/jwks.json, maximumAssertionValiditySeconds: 3600, verifierClockSkewSeconds: 30 } +responseFormats: [signed-jws] +selectorProfiles: + solmara-uin-v1: { maximumAggregateBytes: 16, fields: { uin: { type: string, minimumBytes: 10, maximumBytes: 10 } } } +sources: + population-extract: + transport: sqlite-extract + extractProfile: nia-population-extract + posture: source-derived + maximumExtractAgeSeconds: 86400 + request: + statement: queries/population-evidence.sql + columns: [{ name: uin, type: string }, { name: identity_status, type: string }, { name: alive, type: boolean }] + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + parameterBindings: { uin: { kind: selector, role: subject, profile: solmara-uin-v1, field: uin } } + projection: [/rows/*/uin, /rows/*/identity_status, /rows/*/alive] + maximumRows: 2 + maximumCellBytes: 128 + maximumStatementSteps: 100000 + timeoutMilliseconds: 2000 + maximumResponseBytes: 8192 + concurrencyLimit: 8 + responseSchema: schemas/population-response.schema.yaml + extractScript: adapters/sqlite-extract.rhai + factSchema: schemas/population-facts.schema.yaml +authorityProfiles: + nia-consumers-v1: + kind: statutory + requesterTags: [solmara-demo] + grants: + - { requirement: https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1, purpose: child-benefit-review, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] } + - { requirement: https://id.registrystack.org/solmara/requirement/nia-citizen-status/v1, purpose: citizen-self-service, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] } +requirements: + - id: https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1 + kind: information-requirement + acquisition: { kind: single, source: population-extract } + purposes: [child-benefit-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/population-active/v1 + validitySeconds: 3600 + derivation: { script: derivations/population-active.rhai, parameters: { concept_id: https://id.registrystack.org/solmara/concept/population-record-active } } + concepts: [{ id: https://id.registrystack.org/solmara/concept/population-record-active, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/child-benefit.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nia-child-benefit] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/nia-citizen-status/v1 + kind: information-requirement + acquisition: { kind: single, source: population-extract } + purposes: [citizen-self-service] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/citizen-self-service/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/citizen-population-active/v1 + validitySeconds: 3600 + derivation: { script: derivations/population-active.rhai, parameters: { concept_id: https://id.registrystack.org/solmara/concept/citizen-population-record-active } } + concepts: [{ id: https://id.registrystack.org/solmara/concept/citizen-population-record-active, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/citizen.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/nia-citizen] } + existenceDisclosure: collapse-unresolved diff --git a/evidence/cells/nia/bundle/fixtures/child-benefit.yaml b/evidence/cells/nia/bundle/fixtures/child-benefit.yaml new file mode 100644 index 0000000..982ce58 --- /dev/null +++ b/evidence/cells/nia/bundle/fixtures/child-benefit.yaml @@ -0,0 +1,42 @@ +fixture: registry.evidence.reference.solmara.nia-child-benefit/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000004' } } } + derivationSelectorInputs: {} + extract: | + CREATE TABLE evidence_extract (published_at TEXT NOT NULL, publisher TEXT NOT NULL, extract_id TEXT NOT NULL); + INSERT INTO evidence_extract VALUES ('2026-07-14T00:00:00Z', 'did:web:id.registrystack.org:solmara:authority:nia', 'nia-population-2026-07-14'); + CREATE TABLE population_evidence (uin TEXT NOT NULL, identity_status TEXT NOT NULL, alive INTEGER NOT NULL); + INSERT INTO population_evidence VALUES ('1000000004', 'active', 1); + INSERT INTO population_evidence VALUES ('1000000014', 'inactive', 1); + INSERT INTO population_evidence VALUES ('1000000016', 'active', 1); + INSERT INTO population_evidence VALUES ('1000000016', 'inactive', 1); + expectedRequestParts: { parameters: {} } + expectedTransport: { statement: queries/population-evidence.sql } +cases: + - id: positive + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000004' } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { uin: '1000000004', identity_status: active, alive: true }, value: true } + - id: negative-inactive + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000014' } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: boundary-active + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000004' } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: missing-record + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000098' } } } + expected: { lookup: no_match, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: no-match + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000099' } } } + expected: { lookup: no_match, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: ambiguous-duplicate + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000016' } } } + expected: { lookup: ambiguous, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/population-record-active], evidenceExcludes: [uin, identity_status, alive], diagnosticsExclude: ['1000000004'] } diff --git a/evidence/cells/nia/bundle/fixtures/citizen.yaml b/evidence/cells/nia/bundle/fixtures/citizen.yaml new file mode 100644 index 0000000..c8b3f19 --- /dev/null +++ b/evidence/cells/nia/bundle/fixtures/citizen.yaml @@ -0,0 +1,43 @@ +fixture: registry.evidence.reference.solmara.nia-citizen/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000005' } } } + derivationSelectorInputs: {} + extract: | + CREATE TABLE evidence_extract (published_at TEXT NOT NULL, publisher TEXT NOT NULL, extract_id TEXT NOT NULL); + INSERT INTO evidence_extract VALUES ('2026-07-14T00:00:00Z', 'did:web:id.registrystack.org:solmara:authority:nia', 'nia-population-2026-07-14'); + CREATE TABLE population_evidence (uin TEXT NOT NULL, identity_status TEXT NOT NULL, alive INTEGER NOT NULL); + INSERT INTO population_evidence VALUES ('1000000005', 'active', 1); + INSERT INTO population_evidence VALUES ('1000000015', 'inactive', 1); + INSERT INTO population_evidence VALUES ('1000000025', 'active', 0); + INSERT INTO population_evidence VALUES ('1000000035', 'active', 1); + INSERT INTO population_evidence VALUES ('1000000035', 'inactive', 1); + expectedRequestParts: { parameters: {} } + expectedTransport: { statement: queries/population-evidence.sql } +cases: + - id: positive + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000005' } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { uin: '1000000005', identity_status: active, alive: true }, value: true } + - id: negative-inactive + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000015' } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: boundary-not-alive + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000025' } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: missing-record + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000098' } } } + expected: { lookup: no_match, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: no-match + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000099' } } } + expected: { lookup: no_match, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: ambiguous-duplicate + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000035' } } } + expected: { lookup: ambiguous, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/citizen-population-record-active], evidenceExcludes: [uin, identity_status, alive], diagnosticsExclude: ['1000000005'] } diff --git a/evidence/cells/nia/bundle/queries/population-evidence.sql b/evidence/cells/nia/bundle/queries/population-evidence.sql new file mode 100644 index 0000000..189568c --- /dev/null +++ b/evidence/cells/nia/bundle/queries/population-evidence.sql @@ -0,0 +1,4 @@ +SELECT uin, identity_status, alive +FROM population_evidence +WHERE uin = :uin +LIMIT 2; diff --git a/evidence/cells/nia/bundle/schemas/population-facts.schema.yaml b/evidence/cells/nia/bundle/schemas/population-facts.schema.yaml new file mode 100644 index 0000000..5bbda0a --- /dev/null +++ b/evidence/cells/nia/bundle/schemas/population-facts.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [uin, identity_status, alive] +properties: { uin: { type: string, minLength: 10, maxLength: 10 }, identity_status: { type: string, minLength: 1, maxLength: 32 }, alive: { type: boolean } } diff --git a/evidence/cells/nia/bundle/schemas/population-response.schema.yaml b/evidence/cells/nia/bundle/schemas/population-response.schema.yaml new file mode 100644 index 0000000..0df8bbb --- /dev/null +++ b/evidence/cells/nia/bundle/schemas/population-response.schema.yaml @@ -0,0 +1,5 @@ +type: object +additionalProperties: false +required: [rows] +properties: + rows: { type: array, maxItems: 2, items: { type: object, additionalProperties: false, required: [uin, identity_status, alive], properties: { uin: { type: string, minLength: 10, maxLength: 10 }, identity_status: { type: string, minLength: 1, maxLength: 32 }, alive: { type: boolean } } } } diff --git a/evidence/cells/nia/runtime.yaml b/evidence/cells/nia/runtime.yaml new file mode 100644 index 0000000..3e54cb9 --- /dev/null +++ b/evidence/cells/nia/runtime.yaml @@ -0,0 +1,9 @@ +version: 1 +bundleDirectory: /etc/registry-evidence/nia/bundle +listener: { bindHost: 172.29.1.22, port: 8080, tlsTermination: operator-controlled-upstream, trustProxyIdentityHeaders: false, maximumRequestBytes: 65536, maximumConcurrentRequests: 64, requestTimeoutMilliseconds: 10000, shutdownGraceMilliseconds: 30000 } +secretProviders: { file: { root: /run/secrets/registry-evidence/nia } } +signer: { kind: transit, unixSocketPath: /run/registry-evidence/nia/transit-proxy.sock, mount: transit, keyName: solmara-evidence-nia, keyVersion: 1, timeoutMilliseconds: 2000 } +auditStorage: { path: /var/lib/registry-evidence/nia/audit/evidence.jsonl, maximumFileBytes: 1073741824 } +outboundTls: { systemRoots: true, trustProfiles: {} } +sourceExtracts: + nia-population-extract: { path: /var/lib/registry-evidence/nia/extracts/nia-population-20260704T090000Z.sqlite } diff --git a/evidence/cells/sipf/bundle/adapters/relay-extract.rhai b/evidence/cells/sipf/bundle/adapters/relay-extract.rhai new file mode 100644 index 0000000..4f1ead0 --- /dev/null +++ b/evidence/cells/sipf/bundle/adapters/relay-extract.rhai @@ -0,0 +1,5 @@ +fn extract(source_response, context) { + let domain = source_response["data"]["domainData"]; + if domain.contains("paymentStatus") { return #{outcome: "match", facts: #{payment_status: domain["paymentStatus"]}}; } + #{outcome: "match", facts: #{survivor_eligible: domain["survivorEligible"]}} +} diff --git a/evidence/cells/sipf/bundle/adapters/relay-prepare.rhai b/evidence/cells/sipf/bundle/adapters/relay-prepare.rhai new file mode 100644 index 0000000..fcbb8bd --- /dev/null +++ b/evidence/cells/sipf/bundle/adapters/relay-prepare.rhai @@ -0,0 +1,13 @@ +fn prepare(selectors, context) { + let parameters = context["parameters"]; + let subject = selectors["subject"]["values"]; + let relay_selectors = #{}; + if parameters["selectorField"] == "pensionerUin" { + relay_selectors["pensionerUin"] = subject["uin"]; + } else if parameters["selectorField"] == "spouseUin" { + relay_selectors["spouseUin"] = subject["uin"]; + } else { + throw("invalid_selector_field"); + } + #{query: [#{name: "fields", value: parameters["fields"]}], body: #{selectors: relay_selectors}} +} diff --git a/evidence/bundle/derivations/pension-active.rhai b/evidence/cells/sipf/bundle/derivations/pension-active.rhai similarity index 58% rename from evidence/bundle/derivations/pension-active.rhai rename to evidence/cells/sipf/bundle/derivations/pension-active.rhai index 9cadc68..2fd220b 100644 --- a/evidence/bundle/derivations/pension-active.rhai +++ b/evidence/cells/sipf/bundle/derivations/pension-active.rhai @@ -1,6 +1,3 @@ fn derive(facts, selectors, evaluation_context) { - if required(facts["pensioner_uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { - throw("derivation_input_error"); - } [#{concept_id: "https://id.registrystack.org/solmara/concept/pension-payment-active", value: required(facts["payment_status"], "required_fact_missing") == "active"}] } diff --git a/evidence/bundle/derivations/survivor-eligible.rhai b/evidence/cells/sipf/bundle/derivations/survivor-eligible.rhai similarity index 58% rename from evidence/bundle/derivations/survivor-eligible.rhai rename to evidence/cells/sipf/bundle/derivations/survivor-eligible.rhai index c117977..47e44d7 100644 --- a/evidence/bundle/derivations/survivor-eligible.rhai +++ b/evidence/cells/sipf/bundle/derivations/survivor-eligible.rhai @@ -1,6 +1,3 @@ fn derive(facts, selectors, evaluation_context) { - if required(facts["spouse_uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { - throw("derivation_input_error"); - } [#{concept_id: "https://id.registrystack.org/solmara/concept/survivor-is-eligible", value: required(facts["survivor_eligible"], "required_fact_missing")}] } diff --git a/evidence/cells/sipf/bundle/evidence.yaml b/evidence/cells/sipf/bundle/evidence.yaml new file mode 100644 index 0000000..2a2dfc6 --- /dev/null +++ b/evidence/cells/sipf/bundle/evidence.yaml @@ -0,0 +1,92 @@ +version: 1 +assuranceProfile: production +service: { providerId: https://id.registrystack.org/solmara/evidence/sipf, trustDomain: https://id.registrystack.org/solmara/trust-domain/lab } +issuer: { id: did:web:id.registrystack.org:solmara:authority:sipf } +authentication: { kind: oidc-access-token, issuer: https://mint.solmara.registrystack.org, audiences: [solmara-runtime], tokenTypes: [at+jwt], algorithms: [ES256], jwksUri: https://mint.solmara.registrystack.org/.well-known/jwks.json, principalClaim: sub, requesterTagsClaim: evidence_tags, evidenceAudienceClaim: evidence_audience, grantIdClaim: evidence_grant_id, grantAuthorityClaim: evidence_authority, maximumTokenLifetimeSeconds: 300, revokedKeyIds: [] } +audit: { format: keyed-jsonl, hashSecretRef: secret:file/audit-hmac-key, hashKeyVersion: 1, failClosed: true } +subjectBinding: { secretRef: secret:file/subject-binding-hmac-key, keyVersion: 1 } +rateLimits: { requestsPerPrincipalPerMinute: 60, burstPerPrincipal: 10, failedSelectorAttemptsPerPrincipalAuthorityPerMinute: 10 } +signing: { format: flattened-jws-json, algorithm: ES256, activePublicJwkFile: public-keys/sipf.jwk.json, publishedPublicJwkFiles: [], revokedKeyIds: [], jwksPath: /.well-known/evidence/jwks.json, maximumAssertionValiditySeconds: 300, verifierClockSkewSeconds: 30 } +responseFormats: [signed-jws] +selectorProfiles: { solmara-uin-v1: { maximumAggregateBytes: 16, fields: { uin: { type: string, minimumBytes: 10, maximumBytes: 10 } } } } +sources: + sipf-pension: + transport: http-json + baseUrl: https://sipf-relay.solmara.registrystack.org + posture: field-projected + authentication: { kind: oauth2-client-credentials, tokenEndpoint: https://mint.solmara.registrystack.org/token, clientIdRef: secret:file/sipf-pension-evidence-client-id, clientAssertionKeyRef: secret:file/sipf-pension-evidence-client-key, clientAssertionAudience: https://mint.solmara.registrystack.org/token, scope: 'solmara:relay:sipf:by-pensioner-uin', audience: solmara-runtime, maximumCacheSeconds: 60 } + unresolvedProblem: &relay-unresolved { status: 404, type: https://id.registrystack.org/problems/registry-relay/consultation/unresolved, code: consultation.unresolved } + request: + method: POST + path: /v2/resources/pension-payment/lookups/by-pensioner-uin + fixedHeaders: [{ name: Accept, value: application/json }] + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/relay-prepare.rhai + adapterParameters: { fields: paymentStatus, selectorField: pensionerUin } + adapterParametersSchema: schemas/relay-adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: required, maximumQueryPairs: 1, maximumQueryNameBytes: 16, maximumQueryValueBytes: 64, maximumNormalizedBytes: 1024 } + projection: [/data/domainData/paymentStatus] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/pension-response.schema.yaml + extractScript: adapters/relay-extract.rhai + factSchema: schemas/pension-facts.schema.yaml + sipf-survivor: + transport: http-json + baseUrl: https://sipf-relay.solmara.registrystack.org + posture: field-projected + authentication: { kind: oauth2-client-credentials, tokenEndpoint: https://mint.solmara.registrystack.org/token, clientIdRef: secret:file/sipf-survivor-evidence-client-id, clientAssertionKeyRef: secret:file/sipf-survivor-evidence-client-key, clientAssertionAudience: https://mint.solmara.registrystack.org/token, scope: 'solmara:relay:sipf:by-spouse-uin', audience: solmara-runtime, maximumCacheSeconds: 60 } + unresolvedProblem: *relay-unresolved + request: + method: POST + path: /v2/resources/survivor-case/lookups/by-spouse-uin + fixedHeaders: [{ name: Accept, value: application/json }] + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + prepareScript: adapters/relay-prepare.rhai + adapterParameters: { fields: survivorEligible, selectorField: spouseUin } + adapterParametersSchema: schemas/relay-adapter-parameters.schema.yaml + preparationLimits: { query: required, jsonBody: required, maximumQueryPairs: 1, maximumQueryNameBytes: 16, maximumQueryValueBytes: 64, maximumNormalizedBytes: 1024 } + projection: [/data/domainData/survivorEligible] + redirects: deny + timeoutMilliseconds: 3000 + maximumResponseBytes: 65536 + concurrencyLimit: 8 + responseSchema: schemas/survivor-response.schema.yaml + extractScript: adapters/relay-extract.rhai + factSchema: schemas/survivor-facts.schema.yaml +authorityProfiles: + sipf-consumers-v1: + kind: statutory + requesterTags: [solmara-demo] + grants: + - { requirement: https://id.registrystack.org/solmara/requirement/sipf-pension-payment/v1, purpose: pension-payment-review, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] } + - { requirement: https://id.registrystack.org/solmara/requirement/sipf-survivor-benefit/v1, purpose: survivor-benefit-determination, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] } +requirements: + - id: https://id.registrystack.org/solmara/requirement/sipf-pension-payment/v1 + kind: information-requirement + acquisition: { kind: single, source: sipf-pension } + purposes: [pension-payment-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/social-insurance/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/pension-payment-active/v1 + validitySeconds: 300 + derivation: { script: derivations/pension-active.rhai, parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/pension-payment-active, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/pension.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/sipf-pension] } + existenceDisclosure: collapse-unresolved + - id: https://id.registrystack.org/solmara/requirement/sipf-survivor-benefit/v1 + kind: information-requirement + acquisition: { kind: single, source: sipf-survivor } + purposes: [survivor-benefit-determination] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/social-insurance/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/survivor-benefit/v1 + validitySeconds: 300 + derivation: { script: derivations/survivor-eligible.rhai, parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/survivor-is-eligible, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/survivor.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/sipf-survivor] } + existenceDisclosure: collapse-unresolved diff --git a/evidence/cells/sipf/bundle/fixtures/pension.yaml b/evidence/cells/sipf/bundle/fixtures/pension.yaml new file mode 100644 index 0000000..01bffd8 --- /dev/null +++ b/evidence/cells/sipf/bundle/fixtures/pension.yaml @@ -0,0 +1,34 @@ +fixture: registry.evidence.reference.solmara.sipf-pension/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000008' } } } + derivationSelectorInputs: {} + expectedRequestParts: { query: [{ name: fields, value: paymentStatus }], body: { selectors: { pensionerUin: '1000000008' } } } + expectedTransport: { path: /v2/resources/pension-payment/lookups/by-pensioner-uin, fixedHeaders: [{ name: Accept, value: application/json }] } +cases: + - id: positive + response: { data: { domainData: { paymentStatus: active } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { payment_status: active }, value: true } + - id: negative-suspended + response: { data: { domainData: { paymentStatus: suspended } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: boundary-active + response: { data: { domainData: { paymentStatus: active } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: missing-fact + response: { data: { domainData: {} } } + expected: { publicProblem: source.unavailable, derivationRuns: false, signed: false } + - id: no-match + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: ambiguous-relay-collapse + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/pension-payment-active], evidenceExcludes: [payment_status, '1000000008'], diagnosticsExclude: ['1000000008'] } diff --git a/evidence/cells/sipf/bundle/fixtures/survivor.yaml b/evidence/cells/sipf/bundle/fixtures/survivor.yaml new file mode 100644 index 0000000..cdf4f61 --- /dev/null +++ b/evidence/cells/sipf/bundle/fixtures/survivor.yaml @@ -0,0 +1,34 @@ +fixture: registry.evidence.reference.solmara.sipf-survivor/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000009' } } } + derivationSelectorInputs: {} + expectedRequestParts: { query: [{ name: fields, value: survivorEligible }], body: { selectors: { spouseUin: '1000000009' } } } + expectedTransport: { path: /v2/resources/survivor-case/lookups/by-spouse-uin, fixedHeaders: [{ name: Accept, value: application/json }] } +cases: + - id: positive + response: { data: { domainData: { survivorEligible: true } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { survivor_eligible: true }, value: true } + - id: negative-ineligible + response: { data: { domainData: { survivorEligible: false } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: boundary-eligible + response: { data: { domainData: { survivorEligible: true } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: missing-fact + response: { data: { domainData: {} } } + expected: { publicProblem: source.unavailable, derivationRuns: false, signed: false } + - id: no-match + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: ambiguous-relay-collapse + declaredUnresolved: true + expected: { publicProblem: evidence.unavailable, derivationRuns: false, signed: false, sourceRequestCount: 1 } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/survivor-is-eligible], evidenceExcludes: [survivor_eligible, '1000000009'], diagnosticsExclude: ['1000000009'] } diff --git a/evidence/cells/sipf/bundle/schemas/pension-facts.schema.yaml b/evidence/cells/sipf/bundle/schemas/pension-facts.schema.yaml new file mode 100644 index 0000000..55f7693 --- /dev/null +++ b/evidence/cells/sipf/bundle/schemas/pension-facts.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [payment_status] +properties: { payment_status: { type: string, minLength: 1, maxLength: 32 } } diff --git a/evidence/cells/sipf/bundle/schemas/pension-response.schema.yaml b/evidence/cells/sipf/bundle/schemas/pension-response.schema.yaml new file mode 100644 index 0000000..fe0c969 --- /dev/null +++ b/evidence/cells/sipf/bundle/schemas/pension-response.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [data] +properties: { data: { type: object, additionalProperties: false, required: [domainData], properties: { domainData: { type: object, additionalProperties: false, required: [paymentStatus], properties: { paymentStatus: { type: string, minLength: 1, maxLength: 32 } } } } } } diff --git a/evidence/cells/sipf/bundle/schemas/relay-adapter-parameters.schema.yaml b/evidence/cells/sipf/bundle/schemas/relay-adapter-parameters.schema.yaml new file mode 100644 index 0000000..352194d --- /dev/null +++ b/evidence/cells/sipf/bundle/schemas/relay-adapter-parameters.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [fields, selectorField] +properties: { fields: { type: string, minLength: 1, maxLength: 512 }, selectorField: { type: string, enum: [pensionerUin, spouseUin] } } diff --git a/evidence/cells/sipf/bundle/schemas/survivor-facts.schema.yaml b/evidence/cells/sipf/bundle/schemas/survivor-facts.schema.yaml new file mode 100644 index 0000000..21f0fb6 --- /dev/null +++ b/evidence/cells/sipf/bundle/schemas/survivor-facts.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [survivor_eligible] +properties: { survivor_eligible: { type: boolean } } diff --git a/evidence/cells/sipf/bundle/schemas/survivor-response.schema.yaml b/evidence/cells/sipf/bundle/schemas/survivor-response.schema.yaml new file mode 100644 index 0000000..dfe82f7 --- /dev/null +++ b/evidence/cells/sipf/bundle/schemas/survivor-response.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [data] +properties: { data: { type: object, additionalProperties: false, required: [domainData], properties: { domainData: { type: object, additionalProperties: false, required: [survivorEligible], properties: { survivorEligible: { type: boolean } } } } } } diff --git a/evidence/cells/sipf/runtime.yaml b/evidence/cells/sipf/runtime.yaml new file mode 100644 index 0000000..f611f6e --- /dev/null +++ b/evidence/cells/sipf/runtime.yaml @@ -0,0 +1,7 @@ +version: 1 +bundleDirectory: /etc/registry-evidence/sipf/bundle +listener: { bindHost: 172.29.1.25, port: 8080, tlsTermination: operator-controlled-upstream, trustProxyIdentityHeaders: false, maximumRequestBytes: 65536, maximumConcurrentRequests: 64, requestTimeoutMilliseconds: 10000, shutdownGraceMilliseconds: 30000 } +secretProviders: { file: { root: /run/secrets/registry-evidence/sipf } } +signer: { kind: transit, unixSocketPath: /run/registry-evidence/sipf/transit-proxy.sock, mount: transit, keyName: solmara-evidence-sipf, keyVersion: 1, timeoutMilliseconds: 2000 } +auditStorage: { path: /var/lib/registry-evidence/sipf/audit/evidence.jsonl, maximumFileBytes: 1073741824 } +outboundTls: { systemRoots: true, trustProfiles: {} } diff --git a/evidence/cells/sro/bundle/adapters/sqlite-extract.rhai b/evidence/cells/sro/bundle/adapters/sqlite-extract.rhai new file mode 100644 index 0000000..483d916 --- /dev/null +++ b/evidence/cells/sro/bundle/adapters/sqlite-extract.rhai @@ -0,0 +1,6 @@ +fn extract(source_response, context) { + let rows = source_response["rows"]; + if rows.len == 0 { return #{outcome: "no_match"}; } + if rows.len > 1 { return #{outcome: "ambiguous"}; } + #{outcome: "match", facts: rows[0]} +} diff --git a/evidence/bundle/derivations/poverty-priority.rhai b/evidence/cells/sro/bundle/derivations/poverty-priority.rhai similarity index 61% rename from evidence/bundle/derivations/poverty-priority.rhai rename to evidence/cells/sro/bundle/derivations/poverty-priority.rhai index aa9efcd..7ec7398 100644 --- a/evidence/bundle/derivations/poverty-priority.rhai +++ b/evidence/cells/sro/bundle/derivations/poverty-priority.rhai @@ -1,6 +1,3 @@ fn derive(facts, selectors, evaluation_context) { - if required(facts["uin"], "required_fact_missing") != selectors["subject"]["values"]["uin"] { - throw("derivation_input_error"); - } [#{concept_id: "https://id.registrystack.org/solmara/concept/household-below-poverty-threshold", value: required(facts["poverty_band"], "required_fact_missing") == "priority"}] } diff --git a/evidence/cells/sro/bundle/evidence.yaml b/evidence/cells/sro/bundle/evidence.yaml new file mode 100644 index 0000000..6da0298 --- /dev/null +++ b/evidence/cells/sro/bundle/evidence.yaml @@ -0,0 +1,52 @@ +version: 1 +assuranceProfile: production +service: { providerId: https://id.registrystack.org/solmara/evidence/sro, trustDomain: https://id.registrystack.org/solmara/trust-domain/lab } +issuer: { id: did:web:id.registrystack.org:solmara:authority:sro } +authentication: { kind: oidc-access-token, issuer: https://mint.solmara.registrystack.org, audiences: [solmara-runtime], tokenTypes: [at+jwt], algorithms: [ES256], jwksUri: https://mint.solmara.registrystack.org/.well-known/jwks.json, principalClaim: sub, requesterTagsClaim: evidence_tags, evidenceAudienceClaim: evidence_audience, grantIdClaim: evidence_grant_id, grantAuthorityClaim: evidence_authority, maximumTokenLifetimeSeconds: 300, revokedKeyIds: [] } +audit: { format: keyed-jsonl, hashSecretRef: secret:file/audit-hmac-key, hashKeyVersion: 1, failClosed: true } +subjectBinding: { secretRef: secret:file/subject-binding-hmac-key, keyVersion: 1 } +rateLimits: { requestsPerPrincipalPerMinute: 60, burstPerPrincipal: 10, failedSelectorAttemptsPerPrincipalAuthorityPerMinute: 10 } +signing: { format: flattened-jws-json, algorithm: ES256, activePublicJwkFile: public-keys/sro.jwk.json, publishedPublicJwkFiles: [], revokedKeyIds: [], jwksPath: /.well-known/evidence/jwks.json, maximumAssertionValiditySeconds: 3600, verifierClockSkewSeconds: 30 } +responseFormats: [signed-jws] +selectorProfiles: + solmara-uin-v1: { maximumAggregateBytes: 16, fields: { uin: { type: string, minimumBytes: 10, maximumBytes: 10 } } } +sources: + poverty-extract: + transport: sqlite-extract + extractProfile: sro-poverty-extract + posture: source-derived + maximumExtractAgeSeconds: 86400 + request: + statement: queries/poverty-evidence.sql + columns: [{ name: uin, type: string }, { name: poverty_band, type: string }] + selectorInputs: [{ role: subject, alternatives: [{ profile: solmara-uin-v1, fields: [uin] }] }] + parameterBindings: { uin: { kind: selector, role: subject, profile: solmara-uin-v1, field: uin } } + projection: [/rows/*/uin, /rows/*/poverty_band] + maximumRows: 2 + maximumCellBytes: 128 + maximumStatementSteps: 100000 + timeoutMilliseconds: 2000 + maximumResponseBytes: 8192 + concurrencyLimit: 8 + responseSchema: schemas/poverty-response.schema.yaml + extractScript: adapters/sqlite-extract.rhai + factSchema: schemas/poverty-facts.schema.yaml +authorityProfiles: + sro-consumers-v1: + kind: statutory + requesterTags: [solmara-demo] + grants: [{ requirement: https://id.registrystack.org/solmara/requirement/sro-child-benefit/v1, purpose: child-benefit-review, audienceFrom: authenticated-requester, responseFormats: [signed-jws], subjects: [{ role: subject, selectorProfile: solmara-uin-v1, valueOrigin: request }] }] +requirements: + - id: https://id.registrystack.org/solmara/requirement/sro-child-benefit/v1 + kind: information-requirement + acquisition: { kind: single, source: poverty-extract } + purposes: [child-benefit-review] + subjectRoles: [{ role: subject, cardinality: one, selectorProfiles: [solmara-uin-v1] }] + referenceFrameworks: [https://id.registrystack.org/solmara/framework/child-benefit/v1] + evidenceType: https://id.registrystack.org/solmara/evidence-type/poverty-priority/v1 + validitySeconds: 3600 + derivation: { script: derivations/poverty-priority.rhai, parameters: {} } + concepts: [{ id: https://id.registrystack.org/solmara/concept/household-below-poverty-threshold, form: boolean, required: true, constraints: {} }] + fixtures: fixtures/child-benefit.yaml + disclosureGuard: { families: [https://id.registrystack.org/solmara/disclosure-family/sro-child-benefit] } + existenceDisclosure: collapse-unresolved diff --git a/evidence/cells/sro/bundle/fixtures/child-benefit.yaml b/evidence/cells/sro/bundle/fixtures/child-benefit.yaml new file mode 100644 index 0000000..02dab87 --- /dev/null +++ b/evidence/cells/sro/bundle/fixtures/child-benefit.yaml @@ -0,0 +1,42 @@ +fixture: registry.evidence.reference.solmara.sro-child-benefit/v1 +synthetic_only: true +common: + observed_at: '2026-07-14T00:00:00Z' + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000006' } } } + derivationSelectorInputs: {} + extract: | + CREATE TABLE evidence_extract (published_at TEXT NOT NULL, publisher TEXT NOT NULL, extract_id TEXT NOT NULL); + INSERT INTO evidence_extract VALUES ('2026-07-14T00:00:00Z', 'did:web:id.registrystack.org:solmara:authority:sro', 'sro-poverty-2026-07-14'); + CREATE TABLE poverty_evidence (uin TEXT NOT NULL, poverty_band TEXT NOT NULL); + INSERT INTO poverty_evidence VALUES ('1000000006', 'priority'); + INSERT INTO poverty_evidence VALUES ('1000000016', 'standard'); + INSERT INTO poverty_evidence VALUES ('1000000026', 'priority'); + INSERT INTO poverty_evidence VALUES ('1000000026', 'standard'); + expectedRequestParts: { parameters: {} } + expectedTransport: { statement: queries/poverty-evidence.sql } +cases: + - id: positive + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000006' } } } + expected: { lookup: match, derivationRuns: true, signed: true, facts: { uin: '1000000006', poverty_band: priority }, value: true } + - id: negative-standard-band + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000016' } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: false } + - id: boundary-priority-band + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000006' } } } + expected: { lookup: match, derivationRuns: true, signed: true, value: true } + - id: missing-record + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000098' } } } + expected: { lookup: no_match, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: no-match + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000099' } } } + expected: { lookup: no_match, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: ambiguous-duplicate + selectors: { subject: { profile: solmara-uin-v1, values: { uin: '1000000026' } } } + expected: { lookup: ambiguous, publicProblem: evidence.unavailable, derivationRuns: false, signed: false } + - id: source-failure + sourceFailure: timeout + expected: { publicProblem: source.unavailable, signed: false } + - id: anti-reconstruction + bundleMutation: duplicate-disclosure-family + expected: { bundle: rejected } +privacyExpectation: { evidenceContains: [https://id.registrystack.org/solmara/concept/household-below-poverty-threshold], evidenceExcludes: [uin, poverty_band], diagnosticsExclude: ['1000000006'] } diff --git a/evidence/cells/sro/bundle/queries/poverty-evidence.sql b/evidence/cells/sro/bundle/queries/poverty-evidence.sql new file mode 100644 index 0000000..097d38f --- /dev/null +++ b/evidence/cells/sro/bundle/queries/poverty-evidence.sql @@ -0,0 +1,4 @@ +SELECT uin, poverty_band +FROM poverty_evidence +WHERE uin = :uin +LIMIT 2; diff --git a/evidence/cells/sro/bundle/schemas/poverty-facts.schema.yaml b/evidence/cells/sro/bundle/schemas/poverty-facts.schema.yaml new file mode 100644 index 0000000..cf6b469 --- /dev/null +++ b/evidence/cells/sro/bundle/schemas/poverty-facts.schema.yaml @@ -0,0 +1,4 @@ +type: object +additionalProperties: false +required: [uin, poverty_band] +properties: { uin: { type: string, minLength: 10, maxLength: 10 }, poverty_band: { type: string, minLength: 1, maxLength: 16 } } diff --git a/evidence/cells/sro/bundle/schemas/poverty-response.schema.yaml b/evidence/cells/sro/bundle/schemas/poverty-response.schema.yaml new file mode 100644 index 0000000..69f4104 --- /dev/null +++ b/evidence/cells/sro/bundle/schemas/poverty-response.schema.yaml @@ -0,0 +1,5 @@ +type: object +additionalProperties: false +required: [rows] +properties: + rows: { type: array, maxItems: 2, items: { type: object, additionalProperties: false, required: [uin, poverty_band], properties: { uin: { type: string, minLength: 10, maxLength: 10 }, poverty_band: { type: string, minLength: 1, maxLength: 16 } } } } diff --git a/evidence/cells/sro/runtime.yaml b/evidence/cells/sro/runtime.yaml new file mode 100644 index 0000000..bf15eb8 --- /dev/null +++ b/evidence/cells/sro/runtime.yaml @@ -0,0 +1,9 @@ +version: 1 +bundleDirectory: /etc/registry-evidence/sro/bundle +listener: { bindHost: 172.29.1.23, port: 8080, tlsTermination: operator-controlled-upstream, trustProxyIdentityHeaders: false, maximumRequestBytes: 65536, maximumConcurrentRequests: 64, requestTimeoutMilliseconds: 10000, shutdownGraceMilliseconds: 30000 } +secretProviders: { file: { root: /run/secrets/registry-evidence/sro } } +signer: { kind: transit, unixSocketPath: /run/registry-evidence/sro/transit-proxy.sock, mount: transit, keyName: solmara-evidence-sro, keyVersion: 1, timeoutMilliseconds: 2000 } +auditStorage: { path: /var/lib/registry-evidence/sro/audit/evidence.jsonl, maximumFileBytes: 1073741824 } +outboundTls: { systemRoots: true, trustProfiles: {} } +sourceExtracts: + sro-poverty-extract: { path: /var/lib/registry-evidence/sro/extracts/sro-poverty-20260704T090000Z.sqlite } diff --git a/evidence/mint.yaml b/evidence/mint.yaml index 6aac74d..e86d900 100644 --- a/evidence/mint.yaml +++ b/evidence/mint.yaml @@ -1,20 +1,30 @@ version: 1 -issuer: https://mint.evidence.solmara.invalid +issuer: https://mint.solmara.registrystack.org listener: - address: 0.0.0.0 + address: 172.29.1.20 port: 8081 signing: - algorithm: EdDSA - activeKeyId: solmara-mint-signing-key-1 - activeKeyFile: /run/secrets/registry-mint/signing.jwk - retiredPublicJwkFiles: [] + algorithm: ES256 + activePublicJwkFile: public-keys/mint.jwk.json + publishedPublicJwkFiles: [] + revokedKeyIds: [] +signer: + kind: transit + unixSocketPath: /run/registry-mint/transit-proxy.sock + mount: transit + keyName: solmara-mint + keyVersion: 1 + timeoutMilliseconds: 2000 +secretProviders: + file: + root: /run/secrets/registry-mint audit: path: /var/lib/registry-mint/audit/mint.jsonl maximumFileBytes: 1073741824 - hashKeyFile: /run/secrets/registry-mint/audit-hmac-key + hashKeyRef: secret:file/audit-hmac-key hashKeyVersion: 1 accessTokens: - audiences: [solmara-evidence] + audiences: [solmara-runtime] lifetimeSeconds: 300 claims: principal: sub @@ -23,8 +33,8 @@ accessTokens: grantId: evidence_grant_id grantAuthority: evidence_authority clientAssertion: - audience: https://mint.evidence.solmara.invalid/token + audience: https://mint.solmara.registrystack.org/token maximumLifetimeSeconds: 300 - algorithms: [EdDSA] + algorithms: [ES256, RS256] clients: - directory: /etc/registry-mint/clients + directory: clients diff --git a/evidence/runtime.yaml b/evidence/runtime.yaml deleted file mode 100644 index 91c7d96..0000000 --- a/evidence/runtime.yaml +++ /dev/null @@ -1,22 +0,0 @@ -version: 1 -bundleDirectory: /etc/registry-evidence/bundle -listener: - bindHost: 172.29.0.10 - port: 8080 - tlsTermination: operator-controlled-upstream - trustProxyIdentityHeaders: false - maximumRequestBytes: 65536 - maximumConcurrentRequests: 64 - requestTimeoutMilliseconds: 10000 - shutdownGraceMilliseconds: 30000 -secretProviders: - file: - root: /run/secrets/registry-evidence -auditStorage: - path: /var/lib/registry-evidence/audit/evidence.jsonl - maximumFileBytes: 1073741824 -outboundTls: - systemRoots: true - trustProfiles: - solmara-lab: - caBundleFile: /etc/registry-evidence/tls/lab-ca.crt diff --git a/evidence/scripts/build-cells.py b/evidence/scripts/build-cells.py new file mode 100644 index 0000000..3a65882 --- /dev/null +++ b/evidence/scripts/build-cells.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Build deployable Evidence/Mint config from authored templates and operator keys.""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import shutil +import subprocess +from pathlib import Path + +import yaml +from cryptography.hazmat.primitives.asymmetric import ec, rsa + + +ROOT = Path(__file__).resolve().parents[2] +CELL_ROOT = ROOT / "evidence" / "cells" +CELLS = ("cra", "nia", "sro", "mosd-programme", "sipf", "nagdi") +RELAY_CLIENTS = { + "cra-pension-evidence": ("solmara:relay:cra:death-by-uin", "https://id.registrystack.org/solmara/purpose/pension-payment-review"), + "cra-citizen-evidence": ("solmara:relay:cra:citizen-link-by-uin", "https://id.registrystack.org/solmara/purpose/citizen-self-service"), + "mosd-child-benefit-evidence": ("solmara:relay:mosd:by-uin", "https://id.registrystack.org/solmara/purpose/child-benefit-review"), + "sipf-pension-evidence": ("solmara:relay:sipf:by-pensioner-uin", "https://id.registrystack.org/solmara/purpose/pension-payment-review"), + "sipf-survivor-evidence": ("solmara:relay:sipf:by-spouse-uin", "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"), + "nagdi-voucher-evidence": ("solmara:relay:nagdi:voucher-by-farmer-id", "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"), + "nagdi-livestock-evidence": ("solmara:relay:nagdi:movement-by-farmer-id", "https://id.registrystack.org/solmara/purpose/livestock-movement-control"), + "nia-esignet": ("solmara:relay:nia:esignet-userinfo", "https://id.registrystack.org/solmara/purpose/esignet-identity-verification"), +} +CLIENT_CELLS = { + "cra-pension-evidence": "cra", + "cra-citizen-evidence": "cra", + "mosd-child-benefit-evidence": "mosd-programme", + "sipf-pension-evidence": "sipf", + "sipf-survivor-evidence": "sipf", + "nagdi-voucher-evidence": "nagdi", + "nagdi-livestock-evidence": "nagdi", +} + + +def b64url(value: bytes) -> str: + return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") + + +def public_jwk(private_path: Path) -> dict[str, str]: + value = json.loads(private_path.read_text(encoding="utf-8")) + if value.get("kty") == "RSA": + required = {"kty", "alg", "n", "e", "d", "p", "q", "dp", "dq", "qi", "kid"} + if set(value) != required or value.get("alg") != "RS256": + raise ValueError(f"{private_path}: expected an exact private RS256 JWK") + + def decode(member: str) -> int: + encoded = value[member] + return int.from_bytes(base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4)), "big") + + private = rsa.RSAPrivateNumbers( + p=decode("p"), q=decode("q"), d=decode("d"), dmp1=decode("dp"), + dmq1=decode("dq"), iqmp=decode("qi"), + public_numbers=rsa.RSAPublicNumbers(e=decode("e"), n=decode("n")), + ) + try: + derived = private.private_key().public_key().public_numbers() + except ValueError as exc: + raise ValueError(f"{private_path}: private RS256 JWK members are inconsistent") from exc + result = {"e": b64url(derived.e.to_bytes((derived.e.bit_length() + 7) // 8, "big")), "kty": "RSA", "n": b64url(derived.n.to_bytes((derived.n.bit_length() + 7) // 8, "big"))} + thumbprint = hashlib.sha256(json.dumps(result, separators=(",", ":"), sort_keys=True).encode()).digest() + result["alg"] = "RS256" + result["kid"] = b64url(thumbprint) + if any(value[key] != result[key] for key in ("n", "e", "kid")): + raise ValueError(f"{private_path}: private JWK public members or kid do not match its key") + return result + if set(value) != {"kty", "crv", "alg", "x", "y", "d", "kid"} or value.get("kty") != "EC" or value.get("crv") != "P-256" or value.get("alg") != "ES256": + raise ValueError(f"{private_path}: expected an exact private ES256 P-256 JWK") + private_value = int.from_bytes(base64.urlsafe_b64decode(value["d"] + "=="), "big") + public = ec.derive_private_key(private_value, ec.SECP256R1()).public_key().public_numbers() + result = {"crv": "P-256", "kty": "EC", "x": b64url(public.x.to_bytes(32, "big")), "y": b64url(public.y.to_bytes(32, "big"))} + thumbprint = hashlib.sha256(json.dumps(result, separators=(",", ":"), sort_keys=True).encode()).digest() + result["alg"] = "ES256" + result["kid"] = b64url(thumbprint) + if value["x"] != result["x"] or value["y"] != result["y"] or value["kid"] != result["kid"]: + raise ValueError(f"{private_path}: private JWK public members or kid do not match its scalar") + return result + + +def write_json(path: Path, value: object) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def make_read_only(root: Path) -> None: + for path in sorted(root.rglob("*"), reverse=True): + path.chmod(0o555 if path.is_dir() else 0o444) + root.chmod(0o555) + + +def build(private_root: Path, output: Path, evidence_binary: Path | None) -> None: + if output.exists(): + raise ValueError(f"refusing to overwrite generated output: {output}") + for cell in CELLS: + destination = output / "cells" / cell + shutil.copytree(CELL_ROOT / cell, destination) + cell_public = public_jwk(private_root / cell / "secrets" / "signing.jwk") + config_path = destination / "bundle" / "evidence.yaml" + config = yaml.safe_load(config_path.read_text(encoding="utf-8")) + config["signing"]["activePublicJwkFile"] = f"public-keys/{cell_public['kid']}.jwk.json" + config_path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") + write_json(destination / "bundle" / "public-keys" / f"{cell_public['kid']}.jwk.json", cell_public) + make_read_only(destination / "bundle") + if evidence_binary: + subprocess.run([str(evidence_binary), "bundle-check", "--bundle", str(destination / "bundle")], check=True) + make_read_only(destination) + + mint_destination = output / "mint" + mint_destination.mkdir(parents=True) + shutil.copy2(ROOT / "evidence" / "mint.yaml", mint_destination / "mint.yaml") + mint_public = public_jwk(private_root / "mint" / "secrets" / "signing.jwk") + mint_config_path = mint_destination / "mint.yaml" + mint_config = yaml.safe_load(mint_config_path.read_text(encoding="utf-8")) + mint_config["signing"]["activePublicJwkFile"] = f"public-keys/{mint_public['kid']}.jwk.json" + mint_config_path.write_text(yaml.safe_dump(mint_config, sort_keys=False), encoding="utf-8") + write_json(mint_destination / "public-keys" / f"{mint_public['kid']}.jwk.json", mint_public) + for client, (scope, purpose) in RELAY_CLIENTS.items(): + registration = { + "clientId": client, + "principal": f"https://id.registrystack.org/solmara/client/{client}", + "authorization": {"scopes": [scope], "claims": {"purpose": purpose}}, + "keys": [public_jwk( + private_root / CLIENT_CELLS[client] / "secrets" / f"{client}-client-key" + if client in CLIENT_CELLS + else private_root / "mint" / "clients" / "nia-esignet-rsa-client-key" + )], + } + path = mint_destination / "clients" / f"{client}.yaml" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(yaml.safe_dump(registration, sort_keys=False), encoding="utf-8") + application = { + "clientId": "solmara-demo", + "principal": "https://id.registrystack.org/solmara/client/solmara-demo", + "evidenceAudience": "https://id.registrystack.org/solmara/audience/demo-client", + "requesterTags": ["solmara-demo"], + "keys": [public_jwk(private_root / "mint" / "clients" / "solmara-demo-client-key")], + } + application_path = mint_destination / "clients" / "solmara-demo.yaml" + application_path.write_text(yaml.safe_dump(application, sort_keys=False), encoding="utf-8") + make_read_only(mint_destination) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--private-key-root", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--evidence-binary", type=Path) + args = parser.parse_args() + build(args.private_key_root.resolve(), args.output.resolve(), args.evidence_binary) + + +if __name__ == "__main__": + main() diff --git a/evidence/tests/test_cells.py b/evidence/tests/test_cells.py new file mode 100644 index 0000000..067340d --- /dev/null +++ b/evidence/tests/test_cells.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +import base64 +import hashlib +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + +import yaml +from cryptography.hazmat.primitives.asymmetric import ec, rsa + + +ROOT = Path(__file__).resolve().parents[2] +CELLS = ("cra", "nia", "sro", "mosd-programme", "sipf", "nagdi") +EXPECTED_REQUIREMENTS = { + "https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1": ("https://id.registrystack.org/solmara/evidence-type/cra-child-benefit/v1", "child-benefit-review", 3600), + "https://id.registrystack.org/solmara/requirement/cra-pension-death/v1": ("https://id.registrystack.org/solmara/evidence-type/cra-death-status/v1", "pension-payment-review", 300), + "https://id.registrystack.org/solmara/requirement/cra-citizen-record/v1": ("https://id.registrystack.org/solmara/evidence-type/civil-record-linked/v1", "citizen-self-service", 300), + "https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1": ("https://id.registrystack.org/solmara/evidence-type/population-active/v1", "child-benefit-review", 3600), + "https://id.registrystack.org/solmara/requirement/nia-citizen-status/v1": ("https://id.registrystack.org/solmara/evidence-type/citizen-population-active/v1", "citizen-self-service", 3600), + "https://id.registrystack.org/solmara/requirement/sro-child-benefit/v1": ("https://id.registrystack.org/solmara/evidence-type/poverty-priority/v1", "child-benefit-review", 3600), + "https://id.registrystack.org/solmara/requirement/mosd-child-benefit/v1": ("https://id.registrystack.org/solmara/evidence-type/not-enrolled/v1", "child-benefit-review", 300), + "https://id.registrystack.org/solmara/requirement/sipf-pension-payment/v1": ("https://id.registrystack.org/solmara/evidence-type/pension-payment-active/v1", "pension-payment-review", 300), + "https://id.registrystack.org/solmara/requirement/sipf-survivor-benefit/v1": ("https://id.registrystack.org/solmara/evidence-type/survivor-benefit/v1", "survivor-benefit-determination", 300), + "https://id.registrystack.org/solmara/requirement/nagdi-voucher/v1": ("https://id.registrystack.org/solmara/evidence-type/climate-smart-voucher/v1", "voucher-eligibility-review", 300), + "https://id.registrystack.org/solmara/requirement/nagdi-livestock/v1": ("https://id.registrystack.org/solmara/evidence-type/livestock-movement/v1", "livestock-movement-control", 300), +} +ISSUERS = { + "cra": "did:web:id.registrystack.org:solmara:authority:cra", + "nia": "did:web:id.registrystack.org:solmara:authority:nia", + "sro": "did:web:id.registrystack.org:solmara:authority:sro", + "mosd-programme": "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", + "sipf": "did:web:id.registrystack.org:solmara:authority:sipf", + "nagdi": "did:web:id.registrystack.org:solmara:authority:nagdi", +} + + +def load_yaml(path: Path): + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def private_jwk() -> dict[str, str]: + key = ec.generate_private_key(ec.SECP256R1()).private_numbers() + + def encode(value: int) -> str: + return base64.urlsafe_b64encode(value.to_bytes(32, "big")).rstrip(b"=").decode() + + public = {"kty": "EC", "crv": "P-256", "x": encode(key.public_numbers.x), "y": encode(key.public_numbers.y)} + thumbprint = {member: public[member] for member in ("crv", "kty", "x", "y")} + public["alg"] = "ES256" + public["kid"] = base64.urlsafe_b64encode( + hashlib.sha256(json.dumps(thumbprint, separators=(",", ":"), sort_keys=True).encode()).digest() + ).rstrip(b"=").decode() + public["d"] = encode(key.private_value) + return public + + +def rsa_private_jwk() -> str: + key = rsa.generate_private_key(public_exponent=65537, key_size=2048).private_numbers() + + def encode(value: int) -> str: + return base64.urlsafe_b64encode(value.to_bytes((value.bit_length() + 7) // 8, "big")).rstrip(b"=").decode() + + value = { + "kty": "RSA", "alg": "RS256", "n": encode(key.public_numbers.n), "e": encode(key.public_numbers.e), + "d": encode(key.d), "p": encode(key.p), "q": encode(key.q), "dp": encode(key.dmp1), + "dq": encode(key.dmq1), "qi": encode(key.iqmp), + } + thumbprint = {member: value[member] for member in ("e", "kty", "n")} + value["kid"] = base64.urlsafe_b64encode(hashlib.sha256(json.dumps(thumbprint, separators=(",", ":"), sort_keys=True).encode()).digest()).rstrip(b"=").decode() + return json.dumps(value) + + +class CellContractsTest(unittest.TestCase): + def configs(self): + return {cell: load_yaml(ROOT / "evidence" / "cells" / cell / "bundle" / "evidence.yaml") for cell in CELLS} + + def test_six_independent_authority_identities_and_runtime_boundaries(self): + configs = self.configs() + self.assertEqual(set(configs), set(CELLS)) + for offset, cell in enumerate(CELLS, 21): + config = configs[cell] + runtime = load_yaml(ROOT / "evidence" / "cells" / cell / "runtime.yaml") + self.assertEqual(config["service"]["providerId"], f"https://id.registrystack.org/solmara/evidence/{cell}") + self.assertEqual(config["issuer"]["id"], ISSUERS[cell]) + self.assertEqual(config["signing"]["algorithm"], "ES256") + self.assertEqual(config["signing"]["activePublicJwkFile"], f"public-keys/{cell}.jwk.json") + self.assertFalse((ROOT / "evidence" / "cells" / cell / "bundle" / f"public-keys/{cell}.jwk.json").exists()) + self.assertEqual(runtime["listener"]["bindHost"], f"172.29.1.{offset}") + self.assertEqual(runtime["signer"]["keyName"], f"solmara-evidence-{cell}") + self.assertNotEqual(runtime["auditStorage"]["path"], "/var/lib/registry-evidence/audit/evidence.jsonl") + + def test_exact_requirement_evidence_type_purpose_and_validity_set(self): + actual = {} + for config in self.configs().values(): + for requirement in config["requirements"]: + actual[requirement["id"]] = (requirement["evidenceType"], requirement["purposes"][0], requirement["validitySeconds"]) + self.assertEqual(actual, EXPECTED_REQUIREMENTS) + + def test_direct_extracts_are_fixed_bounded_and_version_bound(self): + configs = self.configs() + direct = [configs["cra"]["sources"]["cra-child-benefit"], configs["nia"]["sources"]["population-extract"], configs["sro"]["sources"]["poverty-extract"]] + self.assertEqual([source["maximumExtractAgeSeconds"] for source in direct], [86400, 86400, 86400]) + for source in direct: + self.assertEqual(source["transport"], "sqlite-extract") + self.assertEqual(source["request"]["maximumRows"], 2) + self.assertNotIn("authentication", source) + runtime_paths = { + cell: next(iter(load_yaml(ROOT / "evidence" / "cells" / cell / "runtime.yaml")["sourceExtracts"].values()))["path"] + for cell in ("cra", "nia", "sro") + } + self.assertEqual(runtime_paths, { + "cra": "/var/lib/registry-evidence/cra/extracts/cra-birth-20260704T090000Z.sqlite", + "nia": "/var/lib/registry-evidence/nia/extracts/nia-population-20260704T090000Z.sqlite", + "sro": "/var/lib/registry-evidence/sro/extracts/sro-poverty-20260704T090000Z.sqlite", + }) + + def test_relay_sources_use_only_named_v2_lookups_and_declared_unresolved_problem(self): + relay_sources = [] + for config in self.configs().values(): + relay_sources.extend(source for source in config["sources"].values() if source["transport"] == "http-json") + self.assertEqual(len(relay_sources), 7) + for source in relay_sources: + self.assertEqual(source["request"]["method"], "POST") + self.assertRegex(source["request"]["path"], r"^/v2/resources/[^/]+/lookups/[^/]+$") + self.assertEqual(source["request"]["projection"][0].split("/")[1:3], ["data", "domainData"]) + self.assertEqual(source["unresolvedProblem"], {"status": 404, "type": "https://id.registrystack.org/problems/registry-relay/consultation/unresolved", "code": "consultation.unresolved"}) + auth = source["authentication"] + self.assertEqual(auth["kind"], "oauth2-client-credentials") + self.assertEqual(auth["tokenEndpoint"], "https://mint.solmara.registrystack.org/token") + self.assertEqual(auth["clientAssertionAudience"], auth["tokenEndpoint"]) + self.assertEqual(auth["audience"], "solmara-runtime") + self.assertNotIn("clientSecretRef", auth) + + actual = {(source["request"]["path"], source["authentication"]["scope"]) for source in relay_sources} + self.assertEqual(actual, { + ("/v2/resources/civil-person/lookups/death-by-uin", "solmara:relay:cra:death-by-uin"), + ("/v2/resources/civil-person/lookups/citizen-link-by-uin", "solmara:relay:cra:citizen-link-by-uin"), + ("/v2/resources/beneficiary-enrolment/lookups/by-uin", "solmara:relay:mosd:by-uin"), + ("/v2/resources/pension-payment/lookups/by-pensioner-uin", "solmara:relay:sipf:by-pensioner-uin"), + ("/v2/resources/survivor-case/lookups/by-spouse-uin", "solmara:relay:sipf:by-spouse-uin"), + ("/v2/resources/farmer/lookups/voucher-by-farmer-id", "solmara:relay:nagdi:voucher-by-farmer-id"), + ("/v2/resources/livestock-herd/lookups/movement-by-farmer-id", "solmara:relay:nagdi:movement-by-farmer-id"), + }) + + def test_no_retired_v1_or_caller_controlled_purpose_shapes(self): + authored = "\n".join(path.read_text(encoding="utf-8") for path in (ROOT / "evidence" / "cells").rglob("*.*") if path.is_file()) + for prohibited in ("/v1/datasets/", "Data-Purpose", "static-bearer", "static-authorization"): + self.assertNotIn(prohibited, authored) + + def test_relay_prepare_scripts_emit_only_the_approved_nested_selectors(self): + cra = (ROOT / "evidence/cells/cra/bundle/adapters/relay-prepare.rhai").read_text() + mosd = (ROOT / "evidence/cells/mosd-programme/bundle/adapters/relay-prepare.rhai").read_text() + sipf = (ROOT / "evidence/cells/sipf/bundle/adapters/relay-prepare.rhai").read_text() + nagdi = (ROOT / "evidence/cells/nagdi/bundle/adapters/relay-prepare.rhai").read_text() + self.assertIn('body: #{selectors: #{uin:', cra) + self.assertNotIn('deceased: true', cra) + self.assertIn('body: #{selectors: #{uin:', mosd) + self.assertIn('relay_selectors["pensionerUin"] = subject["uin"]', sipf) + self.assertIn('relay_selectors["spouseUin"] = subject["uin"]', sipf) + self.assertIn('body: #{selectors: #{farmerId:', nagdi) + self.assertNotIn("target", cra + mosd + sipf + nagdi) + + def test_builder_injects_public_halves_and_closed_mint_registrations(self): + spec = importlib.util.spec_from_file_location("build_cells", ROOT / "evidence" / "scripts" / "build-cells.py") + module = importlib.util.module_from_spec(spec) + assert spec.loader + spec.loader.exec_module(module) + with tempfile.TemporaryDirectory() as directory: + temp = Path(directory) + private_root = temp / "private" + for cell in CELLS: + path = private_root / cell / "secrets" / "signing.jwk" + path.parent.mkdir(parents=True) + path.write_text(json.dumps(private_jwk()), encoding="utf-8") + mint_signing = private_root / "mint" / "secrets" / "signing.jwk" + mint_signing.parent.mkdir(parents=True) + mint_signing.write_text(json.dumps(private_jwk()), encoding="utf-8") + for client in module.RELAY_CLIENTS: + path = (private_root / module.CLIENT_CELLS[client] / "secrets" / f"{client}-client-key" if client in module.CLIENT_CELLS else private_root / "mint" / "clients" / "nia-esignet-rsa-client-key") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(rsa_private_jwk() if client == "nia-esignet" else json.dumps(private_jwk()), encoding="utf-8") + application_key = private_root / "mint" / "clients" / "solmara-demo-client-key" + application_key.write_text(json.dumps(private_jwk()), encoding="utf-8") + output = temp / "output" + module.build(private_root, output, None) + public_documents = list(output.rglob("*.jwk.json")) + self.assertEqual(len(public_documents), 7) + for path in public_documents: + value = json.loads(path.read_text(encoding="utf-8")) + self.assertNotIn("d", value) + self.assertEqual(len(value["kid"]), 43) + self.assertEqual(path.name, f"{value['kid']}.jwk.json") + registrations = {path.stem: load_yaml(path) for path in (output / "mint" / "clients").glob("*.yaml")} + self.assertEqual(set(registrations), set(module.RELAY_CLIENTS) | {"solmara-demo"}) + application = registrations.pop("solmara-demo") + self.assertEqual(application["evidenceAudience"], "https://id.registrystack.org/solmara/audience/demo-client") + self.assertEqual(application["requesterTags"], ["solmara-demo"]) + for client, registration in registrations.items(): + scope, purpose = module.RELAY_CLIENTS[client] + self.assertEqual(registration["authorization"], {"scopes": [scope], "claims": {"purpose": purpose}}) + + nia_registration = registrations["nia-esignet"] + self.assertEqual(nia_registration["keys"][0]["kty"], "RSA") + self.assertEqual(nia_registration["keys"][0]["alg"], "RS256") + self.assertNotIn("d", nia_registration["keys"][0]) + nia_relay = load_yaml(ROOT / "relays" / "nia" / "registry.yaml") + nia_profile = nia_relay["resources"][0]["operations"]["lookups"][0]["accessProfiles"]["esignet"] + self.assertEqual( + nia_registration["authorization"]["claims"]["purpose"], + nia_profile["access"]["purpose"]["allowed"][0], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/generator/solmara_lab/generate.py b/generator/solmara_lab/generate.py index 46ad418..55e9cec 100644 --- a/generator/solmara_lab/generate.py +++ b/generator/solmara_lab/generate.py @@ -7,7 +7,7 @@ import random import shutil from dataclasses import dataclass -from datetime import date, datetime, timedelta +from datetime import date, timedelta from pathlib import Path OBSERVED_AT = "2026-07-04T09:00:00Z" diff --git a/generator/solmara_lab/publisher.py b/generator/solmara_lab/publisher.py new file mode 100644 index 0000000..bfc06cb --- /dev/null +++ b/generator/solmara_lab/publisher.py @@ -0,0 +1,984 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import sqlite3 +import stat +import tempfile +from collections.abc import Callable, Iterable, Sequence +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import Any, NamedTuple + +from .generate import OBSERVED_AT, build_relay_projections, build_rows + +RELAY_DIRECTORY = Path("output/sqlite/relay") +EVIDENCE_DIRECTORY = Path("output/sqlite/evidence") + +RELAY_FILENAMES = { + "cra": "cra.sqlite", + "nia": "nia.sqlite", + "mosd": "mosd.sqlite", + "sipf": "sipf.sqlite", + "nagdi": "nagdi.sqlite", +} + +DEFAULT_EXTRACTS = { + "cra": "cra-birth-20260704T090000Z", + "nia": "nia-population-20260704T090000Z", + "sro": "sro-poverty-20260704T090000Z", +} + +PUBLISHERS = { + "cra": "did:web:id.registrystack.org:solmara:authority:cra", + "nia": "did:web:id.registrystack.org:solmara:authority:nia", + "sro": "did:web:id.registrystack.org:solmara:authority:sro", +} + +EXTRACT_PREFIXES = { + "cra": "cra-birth", + "nia": "nia-population", + "sro": "sro-poverty", +} + +MAX_EXTRACT_AGE_SECONDS = 86_400 + +EXTRACT_TABLES = { + "cra": ( + "birth_evidence", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "birth_date", + "birth_brn", + ), + ), + "nia": ( + "population_evidence", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "identity_status", + "alive", + ), + ), + "sro": ( + "poverty_evidence", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "poverty_band", + ), + ), +} + +_EXTRACT_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$") +_RFC3339 = re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$" +) + + +class ExtractValidationError(RuntimeError): + """Raised when an immutable extract cannot be trusted for binding.""" + + +class StaleExtractError(ExtractValidationError): + """Raised when an otherwise valid extract is outside its accepted age.""" + + +class ExtractMetadata(NamedTuple): + published_at: str + publisher: str + extract_id: str + + +def _bool(value: object) -> int: + if isinstance(value, bool): + return int(value) + if str(value).lower() == "true": + return 1 + if str(value).lower() == "false": + return 0 + raise ValueError("expected a boolean value") + + +def _revision(record: dict[str, object]) -> str: + encoded = json.dumps(record, sort_keys=True, separators=(",", ":")).encode() + return f"rev-{hashlib.sha256(encoded).hexdigest()[:16]}" + + +def _record( + record_id: object, + lifecycle_state: object, + recorded_at: object, + **domain: object, +) -> tuple[object, ...]: + revision_input = { + "record_id": str(record_id), + "lifecycle_state": str(lifecycle_state), + "recorded_at": str(recorded_at), + **domain, + } + return ( + str(record_id), + _revision(revision_input), + str(lifecycle_state), + str(recorded_at), + *domain.values(), + ) + + +def _configure(connection: sqlite3.Connection) -> None: + connection.execute("PRAGMA journal_mode = DELETE") + connection.execute("PRAGMA synchronous = FULL") + connection.execute("PRAGMA foreign_keys = ON") + connection.execute("PRAGMA secure_delete = ON") + connection.execute("PRAGMA user_version = 1") + + +def _finish(connection: sqlite3.Connection) -> None: + connection.commit() + connection.execute("VACUUM") + connection.execute("PRAGMA optimize") + + +def _sidecars(path: Path) -> list[Path]: + candidates = [ + path.with_name(path.name + suffix) + for suffix in ("-journal", "-shm", "-wal") + ] + return [candidate for candidate in candidates if candidate.exists()] + + +def _ensure_no_sidecars(path: Path) -> None: + if _sidecars(path): + raise RuntimeError("SQLite publication left a journal sidecar") + + +def _replace_database( + target: Path, populate: Callable[[sqlite3.Connection], None] +) -> None: + target.parent.mkdir(parents=True, exist_ok=True) + temporary = target.with_name(f".{target.name}.{os.getpid()}.tmp") + if temporary.exists(): + temporary.unlink() + try: + with sqlite3.connect(temporary) as connection: + _configure(connection) + populate(connection) + _finish(connection) + _ensure_no_sidecars(temporary) + os.replace(temporary, target) + _ensure_no_sidecars(target) + finally: + if temporary.exists(): + temporary.unlink() + + +def _create_immutable_database( + target: Path, populate: Callable[[sqlite3.Connection], None] +) -> None: + target.parent.mkdir(parents=True, exist_ok=True) + if target.exists(): + raise FileExistsError("immutable Evidence extract target already exists") from None + descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{target.name}.", suffix=".tmp", dir=target.parent + ) + os.close(descriptor) + temporary = Path(temporary_name) + try: + with sqlite3.connect(temporary) as connection: + _configure(connection) + populate(connection) + _finish(connection) + _ensure_no_sidecars(temporary) + temporary.chmod(stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) + try: + os.link(temporary, target) + except FileExistsError: + raise FileExistsError( + "immutable Evidence extract target already exists" + ) from None + _ensure_no_sidecars(target) + finally: + temporary.unlink(missing_ok=True) + for sidecar in _sidecars(temporary): + sidecar.unlink() + + +def _insert_rows( + connection: sqlite3.Connection, + table: str, + columns: Sequence[str], + rows: Iterable[Sequence[object]], +) -> None: + placeholders = ", ".join("?" for _ in columns) + connection.executemany( + f"INSERT INTO {table} ({', '.join(columns)}) VALUES ({placeholders})", + rows, + ) + + +def _publisher_rows(root: Path) -> dict[str, list[dict[str, object]]]: + rows = build_rows(root) + rows.update(build_relay_projections(rows)) + return rows + + +def _publish_cra(connection: sqlite3.Connection, rows: dict[str, list[dict[str, object]]]) -> None: + connection.executescript( + """ + CREATE TABLE civil_person_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + uin TEXT NOT NULL UNIQUE, + birth_date TEXT NOT NULL, + birth_brn TEXT, + deceased INTEGER NOT NULL CHECK (deceased IN (0, 1)) + ) STRICT; + CREATE VIEW relay_civil_person AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + uin, birth_date, birth_brn, deceased + FROM civil_person_source; + """ + ) + people = {row["uin"]: row for row in rows["civil_person"]} + records = [] + for row in rows["civil_person_projection"]: + person = people[row["uin"]] + records.append( + _record( + person["person_id"], + "deceased" if _bool(row["deceased"]) else "active", + person["observed_at"], + uin=row["uin"], + birth_date=row["birth_date"], + birth_brn=row["birth_brn"] or None, + deceased=_bool(row["deceased"]), + ) + ) + _insert_rows( + connection, + "civil_person_source", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "birth_date", + "birth_brn", + "deceased", + ), + records, + ) + + +def _publish_nia(connection: sqlite3.Connection, rows: dict[str, list[dict[str, object]]]) -> None: + connection.executescript( + """ + CREATE TABLE population_person_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + uin TEXT NOT NULL UNIQUE, + legacy_nid TEXT, + given_name TEXT NOT NULL, + family_name TEXT NOT NULL, + sex TEXT NOT NULL, + birth_date TEXT NOT NULL, + identity_status TEXT NOT NULL, + alive INTEGER NOT NULL CHECK (alive IN (0, 1)) + ) STRICT; + CREATE VIEW relay_population_person AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + uin, legacy_nid, given_name, family_name, sex, birth_date, + identity_status, alive + FROM population_person_source; + """ + ) + records = [ + _record( + row["person_id"], + row["identity_status"], + row["updated_at"], + uin=row["uin"], + legacy_nid=row["legacy_nid"] or None, + given_name=row["given_name"], + family_name=row["family_name"], + sex=row["sex"], + birth_date=row["birth_date"], + identity_status=row["identity_status"], + alive=_bool(row["alive"]), + ) + for row in rows["population_person"] + ] + _insert_rows( + connection, + "population_person_source", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "legacy_nid", + "given_name", + "family_name", + "sex", + "birth_date", + "identity_status", + "alive", + ), + records, + ) + + +def _publish_mosd(connection: sqlite3.Connection, rows: dict[str, list[dict[str, object]]]) -> None: + connection.executescript( + """ + CREATE TABLE beneficiary_enrolment_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + uin TEXT NOT NULL UNIQUE, + duplicate_flag INTEGER NOT NULL CHECK (duplicate_flag IN (0, 1)) + ) STRICT; + CREATE VIEW relay_beneficiary_enrolment AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + uin, duplicate_flag + FROM beneficiary_enrolment_source; + """ + ) + records = [ + _record( + f"MOSD-ENROL-{row['uin']}", + "active", + OBSERVED_AT, + uin=row["uin"], + duplicate_flag=_bool(row["duplicate_flag"]), + ) + for row in rows["programme_mis_enrollment"] + ] + _insert_rows( + connection, + "beneficiary_enrolment_source", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "duplicate_flag", + ), + records, + ) + + +def _publish_sipf(connection: sqlite3.Connection, rows: dict[str, list[dict[str, object]]]) -> None: + connection.executescript( + """ + CREATE TABLE pension_case_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + pensioner_uin TEXT NOT NULL UNIQUE, + payment_status TEXT NOT NULL + ) STRICT; + CREATE TABLE survivor_case_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + spouse_uin TEXT NOT NULL UNIQUE, + survivor_eligible INTEGER NOT NULL CHECK (survivor_eligible IN (0, 1)) + ) STRICT; + CREATE VIEW relay_pension_payment AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + pensioner_uin, payment_status + FROM pension_case_source; + CREATE VIEW relay_survivor_case AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + spouse_uin, survivor_eligible + FROM survivor_case_source; + """ + ) + pension_records = [] + survivor_records = [] + for row in rows["pension_case"]: + pension_records.append( + _record( + row["pension_case_id"], + row["pension_status"], + row["observed_at"], + pensioner_uin=row["pensioner_uin"], + payment_status=row["payment_status"], + ) + ) + if row["spouse_uin"]: + survivor_records.append( + _record( + f"{row['pension_case_id']}-SURVIVOR", + "active", + row["observed_at"], + spouse_uin=row["spouse_uin"], + survivor_eligible=_bool(row["survivor_eligible"]), + ) + ) + _insert_rows( + connection, + "pension_case_source", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "pensioner_uin", + "payment_status", + ), + pension_records, + ) + _insert_rows( + connection, + "survivor_case_source", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "spouse_uin", + "survivor_eligible", + ), + survivor_records, + ) + + +def _publish_nagdi(connection: sqlite3.Connection, rows: dict[str, list[dict[str, object]]]) -> None: + connection.executescript( + """ + CREATE TABLE farmer_voucher_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + farmer_id TEXT NOT NULL UNIQUE, + farmer_registered INTEGER NOT NULL CHECK (farmer_registered IN (0, 1)), + data_use_authorized INTEGER NOT NULL CHECK (data_use_authorized IN (0, 1)), + active_smallholder_farmer INTEGER NOT NULL CHECK (active_smallholder_farmer IN (0, 1)), + active_farm_parcel INTEGER NOT NULL CHECK (active_farm_parcel IN (0, 1)), + crop_declared_for_season INTEGER NOT NULL CHECK (crop_declared_for_season IN (0, 1)), + district_climate_risk_active INTEGER NOT NULL CHECK (district_climate_risk_active IN (0, 1)), + voucher_entitlement_current INTEGER NOT NULL CHECK (voucher_entitlement_current IN (0, 1)), + voucher_not_redeemed INTEGER NOT NULL CHECK (voucher_not_redeemed IN (0, 1)) + ) STRICT; + CREATE TABLE livestock_movement_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + herd_id TEXT NOT NULL UNIQUE, + farmer_id TEXT NOT NULL, + registered_herd INTEGER NOT NULL CHECK (registered_herd IN (0, 1)), + herd_vaccination_current INTEGER NOT NULL CHECK (herd_vaccination_current IN (0, 1)), + origin_district_not_quarantined_for_species INTEGER NOT NULL CHECK (origin_district_not_quarantined_for_species IN (0, 1)), + destination_district_open INTEGER NOT NULL CHECK (destination_district_open IN (0, 1)), + no_conflicting_open_movement_permit INTEGER NOT NULL CHECK (no_conflicting_open_movement_permit IN (0, 1)) + ) STRICT; + CREATE VIEW relay_farmer_voucher AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + farmer_id, farmer_registered, data_use_authorized, + active_smallholder_farmer, active_farm_parcel, + crop_declared_for_season, district_climate_risk_active, + voucher_entitlement_current, voucher_not_redeemed + FROM farmer_voucher_source; + CREATE VIEW relay_livestock_movement AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + herd_id, farmer_id, registered_herd, + herd_vaccination_current, + origin_district_not_quarantined_for_species, + destination_district_open, + no_conflicting_open_movement_permit + FROM livestock_movement_source; + """ + ) + voucher_records = [ + _record( + f"NAGDI-VOUCHER-{row['farmer_id']}", + "active", + OBSERVED_AT, + farmer_id=row["farmer_id"], + farmer_registered=_bool(row["farmer_registered"]), + data_use_authorized=_bool(row["data_use_authorized"]), + active_smallholder_farmer=_bool(row["active_smallholder_farmer"]), + active_farm_parcel=_bool(row["active_farm_parcel"]), + crop_declared_for_season=_bool(row["crop_declared_for_season"]), + district_climate_risk_active=_bool(row["district_climate_risk_active"]), + voucher_entitlement_current=_bool(row["voucher_entitlement_current"]), + voucher_not_redeemed=_bool(row["voucher_not_redeemed"]), + ) + for row in rows["farmer_voucher"] + ] + movement_records = [ + _record( + f"NAGDI-MOVEMENT-{row['herd_id']}", + "active", + OBSERVED_AT, + herd_id=row["herd_id"], + farmer_id=row["farmer_id"], + registered_herd=_bool(row["registered_herd"]), + herd_vaccination_current=_bool(row["herd_vaccination_current"]), + origin_district_not_quarantined_for_species=_bool( + row["origin_district_not_quarantined_for_species"] + ), + destination_district_open=_bool(row["destination_district_open"]), + no_conflicting_open_movement_permit=_bool( + row["no_conflicting_open_movement_permit"] + ), + ) + for row in rows["livestock_movement"] + ] + _insert_rows( + connection, + "farmer_voucher_source", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "farmer_id", + "farmer_registered", + "data_use_authorized", + "active_smallholder_farmer", + "active_farm_parcel", + "crop_declared_for_season", + "district_climate_risk_active", + "voucher_entitlement_current", + "voucher_not_redeemed", + ), + voucher_records, + ) + _insert_rows( + connection, + "livestock_movement_source", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "herd_id", + "farmer_id", + "registered_herd", + "herd_vaccination_current", + "origin_district_not_quarantined_for_species", + "destination_district_open", + "no_conflicting_open_movement_permit", + ), + movement_records, + ) + + +_RELAY_PUBLISHERS: dict[ + str, Callable[[sqlite3.Connection, dict[str, list[dict[str, object]]]], None] +] = { + "cra": _publish_cra, + "nia": _publish_nia, + "mosd": _publish_mosd, + "sipf": _publish_sipf, + "nagdi": _publish_nagdi, +} + + +def publish_relay_sources(root: Path) -> dict[str, Path]: + root = root.resolve() + rows = _publisher_rows(root) + published = {} + for authority, filename in RELAY_FILENAMES.items(): + target = root / RELAY_DIRECTORY / filename + populate = _RELAY_PUBLISHERS[authority] + _replace_database(target, lambda connection, p=populate: p(connection, rows)) + published[authority] = target + return published + + +def _extract_rows( + authority: str, rows: dict[str, list[dict[str, object]]] +) -> tuple[str, tuple[str, ...], list[tuple[object, ...]]]: + if authority == "cra": + people = {row["uin"]: row for row in rows["civil_person"]} + records = [] + for row in rows["civil_person_projection"]: + person = people[row["uin"]] + records.append( + _record( + person["person_id"], + "registered" if row["birth_brn"] else "unregistered", + person["observed_at"], + uin=row["uin"], + birth_date=row["birth_date"], + birth_brn=row["birth_brn"] or None, + ) + ) + return ( + "birth_evidence", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "birth_date", + "birth_brn", + ), + records, + ) + if authority == "nia": + records = [ + _record( + row["person_id"], + row["identity_status"], + row["updated_at"], + uin=row["uin"], + identity_status=row["identity_status"], + alive=_bool(row["alive"]), + ) + for row in rows["population_person"] + ] + return ( + "population_evidence", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "identity_status", + "alive", + ), + records, + ) + if authority == "sro": + records = [ + _record( + f"SRO-POVERTY-{row['uin']}", + "current", + OBSERVED_AT, + uin=row["uin"], + poverty_band=row["poverty_band"], + ) + for row in rows["child_benefit_household"] + ] + return ( + "poverty_evidence", + ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "poverty_band", + ), + records, + ) + raise ValueError("authority must be one of: cra, nia, sro") + + +def _create_extract_table( + connection: sqlite3.Connection, table: str, columns: Sequence[str] +) -> None: + types = { + "alive": "INTEGER NOT NULL CHECK (alive IN (0, 1))", + } + nullable = {"birth_brn"} + definitions = [] + for column in columns: + if column == "record_id": + definition = "TEXT PRIMARY KEY" + elif column in types: + definition = types[column] + elif column in nullable: + definition = "TEXT" + else: + definition = "TEXT NOT NULL" + definitions.append(f"{column} {definition}") + connection.execute(f"CREATE TABLE {table} ({', '.join(definitions)}) STRICT") + + +def _validate_extract_id(extract_id: str) -> None: + if not _EXTRACT_ID.fullmatch(extract_id): + raise ValueError("extract_id must be a filename-safe identifier") + + +def _validate_published_at(published_at: str) -> None: + if not _RFC3339.fullmatch(published_at): + raise ValueError("published_at must be an RFC 3339 timestamp") + try: + parsed = datetime.fromisoformat(published_at) + except ValueError: + raise ValueError("published_at must be an RFC 3339 timestamp") from None + if parsed.utcoffset() is None: + raise ValueError("published_at must be an RFC 3339 timestamp") + + +def _published_datetime(published_at: str) -> datetime: + _validate_published_at(published_at) + return datetime.fromisoformat(published_at).astimezone(UTC) + + +def canonical_published_at(published_at: str) -> str: + """Return one stable UTC representation for an explicit publication time.""" + + parsed = _published_datetime(published_at) + timespec = "microseconds" if parsed.microsecond else "seconds" + return parsed.isoformat(timespec=timespec).replace("+00:00", "Z") + + +def timestamped_extract_id(authority: str, published_at: str) -> str: + """Derive a deterministic immutable extract identifier from its authority and time.""" + + if authority not in EXTRACT_PREFIXES: + raise ValueError("authority must be one of: cra, nia, sro") + parsed = _published_datetime(published_at) + timestamp = parsed.strftime("%Y%m%dT%H%M%S") + if parsed.microsecond: + timestamp += f"{parsed.microsecond:06d}" + return f"{EXTRACT_PREFIXES[authority]}-{timestamp}Z" + + +def _database_uri(path: Path) -> str: + return path.resolve().as_uri() + "?mode=ro&immutable=1" + + +def validate_extract( + path: Path, + authority: str, + *, + observed_at: str, + expected_extract_id: str | None = None, + expected_published_at: str | None = None, + maximum_age_seconds: int = MAX_EXTRACT_AGE_SECONDS, +) -> ExtractMetadata: + """Validate the immutable file, exact metadata, schema, and freshness.""" + + if authority not in PUBLISHERS: + raise ValueError("authority must be one of: cra, nia, sro") + if maximum_age_seconds < 0: + raise ValueError("maximum_age_seconds must not be negative") + observed = _published_datetime(observed_at) + path = path.absolute() + if path.is_symlink() or not path.is_file(): + raise ExtractValidationError("extract is not an immutable regular file") + if stat.S_IMODE(path.stat().st_mode) & 0o222: + raise ExtractValidationError("extract has a writable mode") + if path.suffix != ".sqlite": + raise ExtractValidationError("extract filename does not match its binding") + bound_extract_id = path.stem + try: + _validate_extract_id(bound_extract_id) + except ValueError: + raise ExtractValidationError( + "extract filename does not match its binding" + ) from None + if expected_extract_id is not None and bound_extract_id != expected_extract_id: + raise ExtractValidationError("extract filename does not match its binding") + + try: + with sqlite3.connect(_database_uri(path), uri=True) as connection: + if connection.execute("PRAGMA quick_check").fetchall() != [("ok",)]: + raise ExtractValidationError("extract integrity check failed") + metadata_columns = tuple( + row[1] + for row in connection.execute("PRAGMA table_info(evidence_extract)") + ) + if metadata_columns != ("published_at", "publisher", "extract_id"): + raise ExtractValidationError("extract metadata schema is invalid") + metadata_rows = connection.execute( + "SELECT published_at, publisher, extract_id FROM evidence_extract" + ).fetchall() + if len(metadata_rows) != 1: + raise ExtractValidationError("extract metadata cardinality is invalid") + published_at, publisher, extract_id = metadata_rows[0] + table, expected_columns = EXTRACT_TABLES[authority] + actual_columns = tuple( + row[1] for row in connection.execute(f"PRAGMA table_info({table})") + ) + if actual_columns != expected_columns: + raise ExtractValidationError("extract source schema is invalid") + except ExtractValidationError: + raise + except (OSError, sqlite3.Error): + raise ExtractValidationError("extract cannot be validated") from None + + if ( + not isinstance(published_at, str) + or not isinstance(publisher, str) + or not isinstance(extract_id, str) + ): + raise ExtractValidationError("extract metadata types are invalid") + if publisher != PUBLISHERS[authority] or extract_id != bound_extract_id: + raise ExtractValidationError("extract metadata does not match its binding") + try: + published = _published_datetime(published_at) + except ValueError: + raise ExtractValidationError("extract publication time is invalid") from None + if expected_published_at is not None: + try: + expected_published = canonical_published_at(expected_published_at) + except ValueError: + raise ValueError("expected_published_at must be an RFC 3339 timestamp") from None + if canonical_published_at(published_at) != expected_published: + raise ExtractValidationError("extract metadata does not match its binding") + age = observed - published + if age < timedelta(0): + raise ExtractValidationError("extract publication time is in the future") + if age > timedelta(seconds=maximum_age_seconds): + raise StaleExtractError("extract is outside its accepted age") + return ExtractMetadata(published_at, publisher, extract_id) + + +def extract_path(root: Path, extract_id: str) -> Path: + _validate_extract_id(extract_id) + return root.resolve() / EVIDENCE_DIRECTORY / f"{extract_id}.sqlite" + + +def publish_extract( + root: Path, authority: str, published_at: str, extract_id: str +) -> Path: + if authority not in PUBLISHERS: + raise ValueError("authority must be one of: cra, nia, sro") + _validate_published_at(published_at) + target = extract_path(root, extract_id) + rows = _publisher_rows(root.resolve()) + table, columns, records = _extract_rows(authority, rows) + + def populate(connection: sqlite3.Connection) -> None: + connection.execute( + """ + CREATE TABLE evidence_extract ( + published_at TEXT NOT NULL, + publisher TEXT NOT NULL, + extract_id TEXT NOT NULL + ) STRICT + """ + ) + connection.execute( + "INSERT INTO evidence_extract VALUES (?, ?, ?)", + (published_at, PUBLISHERS[authority], extract_id), + ) + _create_extract_table(connection, table, columns) + _insert_rows(connection, table, columns, records) + + _create_immutable_database(target, populate) + return target + + +def publish_all(root: Path) -> dict[str, Path]: + root = root.resolve() + extract_targets = { + authority: extract_path(root, extract_id) + for authority, extract_id in DEFAULT_EXTRACTS.items() + } + existing = [path for path in extract_targets.values() if path.exists()] + if existing: + raise FileExistsError("an immutable Evidence extract target already exists") + published = publish_relay_sources(root) + for authority, extract_id in DEFAULT_EXTRACTS.items(): + published[f"{authority}_extract"] = publish_extract( + root, authority, OBSERVED_AT, extract_id + ) + return published + + +def mutate_mosd_state( + database: Path, uin: str, duplicate_flag: bool, recorded_at: str +) -> None: + database = database.resolve() + before = database.stat() + with sqlite3.connect(database) as connection: + _configure(connection) + current = connection.execute( + "SELECT record_id, lifecycle_state FROM beneficiary_enrolment_source WHERE uin = ?", + (uin,), + ).fetchone() + if current is None: + raise LookupError("MoSD enrolment record was not found") + revision_input: dict[str, Any] = { + "record_id": current[0], + "lifecycle_state": current[1], + "recorded_at": recorded_at, + "uin": uin, + "duplicate_flag": int(duplicate_flag), + } + connection.execute( + """ + UPDATE beneficiary_enrolment_source + SET duplicate_flag = ?, record_revision = ?, recorded_at = ? + WHERE uin = ? + """, + (int(duplicate_flag), _revision(revision_input), recorded_at, uin), + ) + connection.commit() + after = database.stat() + if (before.st_dev, before.st_ino) != (after.st_dev, after.st_ino): + raise RuntimeError("MoSD publication replaced the live database path") + _ensure_no_sidecars(database) + + +def _parse_bool(value: str) -> bool: + if value == "true": + return True + if value == "false": + return False + raise argparse.ArgumentTypeError("expected true or false") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Publish Solmara SQLite sources") + commands = parser.add_subparsers(dest="command", required=True) + default_root = Path(__file__).resolve().parents[2] + + publish_all_command = commands.add_parser("publish-all") + publish_all_command.add_argument("--root", type=Path, default=default_root) + + extract = commands.add_parser("publish-extract") + extract.add_argument("--root", type=Path, default=default_root) + extract.add_argument("--authority", choices=sorted(PUBLISHERS), required=True) + extract.add_argument("--published-at", required=True) + extract.add_argument("--extract-id", required=True) + + mutation = commands.add_parser("mutate-mosd") + mutation.add_argument("--root", type=Path, default=default_root) + mutation.add_argument("--database", type=Path) + mutation.add_argument("--uin", required=True) + mutation.add_argument("--duplicate-flag", type=_parse_bool, required=True) + mutation.add_argument("--recorded-at", required=True) + + args = parser.parse_args() + root = args.root.resolve() + if args.command == "publish-all": + publish_all(root) + elif args.command == "publish-extract": + publish_extract(root, args.authority, args.published_at, args.extract_id) + else: + database = args.database or root / RELAY_DIRECTORY / RELAY_FILENAMES["mosd"] + mutate_mosd_state(database, args.uin, args.duplicate_flag, args.recorded_at) + + +if __name__ == "__main__": + main() diff --git a/generator/tests/test_publisher.py b/generator/tests/test_publisher.py new file mode 100644 index 0000000..54f84b5 --- /dev/null +++ b/generator/tests/test_publisher.py @@ -0,0 +1,560 @@ +from __future__ import annotations + +import hashlib +import sqlite3 +import stat +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from solmara_lab.generate import OBSERVED_AT +from solmara_lab.publisher import ( + DEFAULT_EXTRACTS, + EVIDENCE_DIRECTORY, + PUBLISHERS, + RELAY_DIRECTORY, + RELAY_FILENAMES, + ExtractValidationError, + StaleExtractError, + canonical_published_at, + mutate_mosd_state, + publish_all, + publish_extract, + publish_relay_sources, + timestamped_extract_id, + validate_extract, +) + + +def digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def query(path: Path, statement: str, parameters: tuple[object, ...] = ()) -> list[tuple]: + with sqlite3.connect(path) as connection: + return connection.execute(statement, parameters).fetchall() + + +def columns(path: Path, relation: str) -> list[str]: + return [row[1] for row in query(path, f"PRAGMA table_info({relation})")] + + +def published_files(root: Path) -> list[Path]: + return sorted((root / "output/sqlite").rglob("*.sqlite")) + + +class PublisherTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.published = publish_all(self.root) + + def tearDown(self) -> None: + self.temporary.cleanup() + + def test_repeatable_schema_content_and_bytes(self) -> None: + relay_paths = sorted((self.root / RELAY_DIRECTORY).glob("*.sqlite")) + before_republish = {path: digest(path) for path in relay_paths} + publish_relay_sources(self.root) + self.assertEqual( + {path: digest(path) for path in relay_paths}, before_republish + ) + with tempfile.TemporaryDirectory() as second_directory: + second = Path(second_directory) + publish_all(second) + first_files = published_files(self.root) + second_files = published_files(second) + self.assertEqual( + [path.relative_to(self.root) for path in first_files], + [path.relative_to(second) for path in second_files], + ) + self.assertEqual( + [digest(path) for path in first_files], + [digest(path) for path in second_files], + ) + + def test_relay_views_have_stable_record_fields_and_minimal_domains(self) -> None: + required = [ + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + ] + expected = { + "cra": { + "relay_civil_person": required + + ["uin", "birth_date", "birth_brn", "deceased"] + }, + "nia": { + "relay_population_person": required + + [ + "uin", + "legacy_nid", + "given_name", + "family_name", + "sex", + "birth_date", + "identity_status", + "alive", + ] + }, + "mosd": { + "relay_beneficiary_enrolment": required + ["uin", "duplicate_flag"] + }, + "sipf": { + "relay_pension_payment": required + ["pensioner_uin", "payment_status"], + "relay_survivor_case": required + ["spouse_uin", "survivor_eligible"], + }, + "nagdi": { + "relay_farmer_voucher": required + + [ + "farmer_id", + "farmer_registered", + "data_use_authorized", + "active_smallholder_farmer", + "active_farm_parcel", + "crop_declared_for_season", + "district_climate_risk_active", + "voucher_entitlement_current", + "voucher_not_redeemed", + ], + "relay_livestock_movement": required + + [ + "herd_id", + "farmer_id", + "registered_herd", + "herd_vaccination_current", + "origin_district_not_quarantined_for_species", + "destination_district_open", + "no_conflicting_open_movement_permit", + ], + }, + } + for authority, relations in expected.items(): + path = self.root / RELAY_DIRECTORY / RELAY_FILENAMES[authority] + for relation, expected_columns in relations.items(): + self.assertEqual(columns(path, relation), expected_columns) + record_fields = query( + path, + f"SELECT record_id, record_revision, lifecycle_state, recorded_at FROM {relation}", + ) + self.assertTrue(record_fields) + self.assertTrue(all(all(value for value in row) for row in record_fields)) + + def test_expected_positive_and_control_rows_are_preserved(self) -> None: + cra = self.root / RELAY_DIRECTORY / RELAY_FILENAMES["cra"] + self.assertEqual( + query( + cra, + "SELECT birth_brn, deceased FROM relay_civil_person WHERE uin = ?", + ("2300010248",), + ), + [("BRN-2022-0101-00001", 0)], + ) + self.assertEqual( + query( + cra, + "SELECT deceased FROM relay_civil_person WHERE uin = ?", + ("2300109568",), + ), + [(1,)], + ) + + nia = self.root / RELAY_DIRECTORY / RELAY_FILENAMES["nia"] + self.assertEqual( + query( + nia, + "SELECT identity_status, alive, recorded_at FROM relay_population_person WHERE uin = ?", + ("2300127827",), + ), + [("active", 1, "2026-07-01T08:00:00Z")], + ) + self.assertEqual( + query( + nia, + "SELECT identity_status, alive FROM relay_population_person WHERE uin = ?", + ("2300109568",), + ), + [("deceased", 0)], + ) + + mosd = self.root / RELAY_DIRECTORY / RELAY_FILENAMES["mosd"] + self.assertEqual( + query( + mosd, + "SELECT uin, duplicate_flag FROM relay_beneficiary_enrolment WHERE uin IN (?, ?) ORDER BY uin", + ("2300010248", "2300054788"), + ), + [("2300010248", 0), ("2300054788", 1)], + ) + + sipf = self.root / RELAY_DIRECTORY / RELAY_FILENAMES["sipf"] + self.assertEqual( + query( + sipf, + "SELECT payment_status FROM relay_pension_payment WHERE pensioner_uin = ?", + ("2300109568",), + ), + [("active",)], + ) + self.assertEqual( + query( + sipf, + "SELECT survivor_eligible FROM relay_survivor_case WHERE spouse_uin = ?", + ("2300146081",), + ), + [(0,)], + ) + + nagdi = self.root / RELAY_DIRECTORY / RELAY_FILENAMES["nagdi"] + self.assertEqual( + query( + nagdi, + "SELECT data_use_authorized FROM relay_farmer_voucher WHERE farmer_id = ?", + ("FR-1002",), + ), + [(0,)], + ) + self.assertEqual( + query( + nagdi, + "SELECT origin_district_not_quarantined_for_species FROM relay_livestock_movement WHERE farmer_id = ?", + ("FR-1004",), + ), + [(0,)], + ) + + def test_extract_metadata_is_exact_and_no_sidecars_remain(self) -> None: + table_by_authority = { + "cra": "birth_evidence", + "nia": "population_evidence", + "sro": "poverty_evidence", + } + for authority, extract_id in DEFAULT_EXTRACTS.items(): + path = self.root / EVIDENCE_DIRECTORY / f"{extract_id}.sqlite" + self.assertEqual( + query(path, "SELECT published_at, publisher, extract_id FROM evidence_extract"), + [(OBSERVED_AT, PUBLISHERS[authority], extract_id)], + ) + self.assertEqual( + query(path, "SELECT count(*) FROM evidence_extract"), [(1,)] + ) + self.assertTrue(query(path, f"SELECT record_id FROM {table_by_authority[authority]} LIMIT 1")) + for path in published_files(self.root): + for suffix in ("-journal", "-shm", "-wal"): + self.assertFalse(path.with_name(path.name + suffix).exists()) + for extract_id in DEFAULT_EXTRACTS.values(): + path = self.root / EVIDENCE_DIRECTORY / f"{extract_id}.sqlite" + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o444) + + def test_extract_tables_have_only_the_required_columns(self) -> None: + required = [ + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + ] + expected = { + "cra": ("birth_evidence", required + ["uin", "birth_date", "birth_brn"]), + "nia": ( + "population_evidence", + required + + [ + "uin", + "identity_status", + "alive", + ], + ), + "sro": ("poverty_evidence", required + ["uin", "poverty_band"]), + } + for authority, (table, expected_columns) in expected.items(): + extract_id = DEFAULT_EXTRACTS[authority] + path = self.root / EVIDENCE_DIRECTORY / f"{extract_id}.sqlite" + self.assertEqual(columns(path, table), expected_columns) + + def test_extract_rows_keep_birth_population_and_poverty_story_outcomes(self) -> None: + cra = self.root / EVIDENCE_DIRECTORY / f"{DEFAULT_EXTRACTS['cra']}.sqlite" + self.assertEqual( + query( + cra, + "SELECT birth_brn, lifecycle_state FROM birth_evidence WHERE uin = ?", + ("2300010248",), + ), + [("BRN-2022-0101-00001", "registered")], + ) + self.assertEqual( + query( + cra, + "SELECT birth_date, birth_brn, lifecycle_state FROM birth_evidence WHERE uin = ?", + ("2300073046",), + ), + [("2020-05-18", None, "unregistered")], + ) + self.assertEqual( + query( + cra, + "SELECT birth_date, birth_brn, lifecycle_state FROM birth_evidence WHERE uin = ?", + ("2300091305",), + ), + [("2019-12-12", "BRN-2019-0203-00010", "registered")], + ) + + nia = self.root / EVIDENCE_DIRECTORY / f"{DEFAULT_EXTRACTS['nia']}.sqlite" + self.assertEqual( + query( + nia, + "SELECT identity_status, alive FROM population_evidence WHERE uin = ?", + ("2300109568",), + ), + [("deceased", 0)], + ) + + sro = self.root / EVIDENCE_DIRECTORY / f"{DEFAULT_EXTRACTS['sro']}.sqlite" + self.assertEqual( + query( + sro, + "SELECT uin, poverty_band FROM poverty_evidence WHERE uin IN (?, ?) ORDER BY uin", + ("2300010248", "2300036523"), + ), + [("2300010248", "priority"), ("2300036523", "not_eligible")], + ) + + def test_immutable_extract_refuses_an_existing_target(self) -> None: + extract_id = "sro-poverty-20260705T090000Z" + publish_extract(self.root, "sro", "2026-07-05T09:00:00Z", extract_id) + with self.assertRaises(FileExistsError): + publish_extract(self.root, "sro", "2026-07-05T09:00:00Z", extract_id) + + def test_extract_refuses_a_non_rfc3339_publication_time(self) -> None: + for suffix, published_at in ( + ("syntax", "2026-07-05 09:00:00"), + ("calendar", "2026-02-30T09:00:00Z"), + ): + with self.subTest(published_at=published_at): + extract_id = f"sro-poverty-invalid-{suffix}" + with self.assertRaisesRegex(ValueError, "RFC 3339"): + publish_extract(self.root, "sro", published_at, extract_id) + self.assertFalse( + (self.root / EVIDENCE_DIRECTORY / f"{extract_id}.sqlite").exists() + ) + + def test_a_second_sro_publication_uses_a_new_filename(self) -> None: + original = self.root / EVIDENCE_DIRECTORY / f"{DEFAULT_EXTRACTS['sro']}.sqlite" + original_digest = digest(original) + extract_id = "sro-poverty-20260705T090000Z" + second = publish_extract( + self.root, "sro", "2026-07-05T09:00:00Z", extract_id + ) + self.assertNotEqual(original, second) + self.assertEqual(digest(original), original_digest) + self.assertEqual( + query(second, "SELECT published_at, publisher, extract_id FROM evidence_extract"), + [("2026-07-05T09:00:00Z", PUBLISHERS["sro"], extract_id)], + ) + + def test_timestamped_extract_ids_are_deterministic_for_the_instant(self) -> None: + self.assertEqual( + canonical_published_at("2026-08-12T16:34:56.123456+07:00"), + "2026-08-12T09:34:56.123456Z", + ) + self.assertEqual( + timestamped_extract_id("cra", "2026-08-12T16:34:56.123456+07:00"), + "cra-birth-20260812T093456123456Z", + ) + self.assertEqual( + timestamped_extract_id("cra", "2026-08-12T09:34:56.123456Z"), + "cra-birth-20260812T093456123456Z", + ) + + def test_extract_validation_binds_exact_metadata_schema_and_age(self) -> None: + extract_id = "sro-poverty-20260812T090000Z" + path = publish_extract( + self.root, "sro", "2026-08-12T09:00:00Z", extract_id + ) + metadata = validate_extract( + path, + "sro", + observed_at="2026-08-12T10:00:00Z", + expected_extract_id=extract_id, + expected_published_at="2026-08-12T09:00:00Z", + ) + self.assertEqual(metadata.extract_id, extract_id) + with self.assertRaisesRegex(StaleExtractError, "accepted age"): + validate_extract( + path, + "sro", + observed_at="2026-08-13T09:00:01Z", + expected_extract_id=extract_id, + ) + with self.assertRaisesRegex(ExtractValidationError, "future"): + validate_extract( + path, + "sro", + observed_at="2026-08-12T08:59:59Z", + expected_extract_id=extract_id, + ) + + path.chmod(0o644) + try: + with self.assertRaisesRegex(ExtractValidationError, "writable"): + validate_extract( + path, + "sro", + observed_at="2026-08-12T10:00:00Z", + expected_extract_id=extract_id, + ) + finally: + path.chmod(0o444) + + def test_extract_validation_refuses_metadata_mismatch_and_extra_columns(self) -> None: + cases = ("publisher", "extract_id", "column", "row") + for index, mismatch in enumerate(cases): + with self.subTest(mismatch=mismatch): + extract_id = f"sro-poverty-20260812T10000{index}Z" + path = publish_extract( + self.root, "sro", "2026-08-12T10:00:00Z", extract_id + ) + path.chmod(0o644) + with sqlite3.connect(path) as connection: + if mismatch == "publisher": + connection.execute( + "UPDATE evidence_extract SET publisher = ?", + (PUBLISHERS["cra"],), + ) + elif mismatch == "extract_id": + connection.execute( + "UPDATE evidence_extract SET extract_id = ?", + ("metadata-does-not-match",), + ) + elif mismatch == "column": + connection.execute( + "ALTER TABLE evidence_extract ADD COLUMN unexpected TEXT" + ) + else: + connection.execute( + "INSERT INTO evidence_extract VALUES (?, ?, ?)", + ( + "2026-08-12T10:00:00Z", + PUBLISHERS["sro"], + extract_id, + ), + ) + connection.commit() + path.chmod(0o444) + with self.assertRaises(ExtractValidationError): + validate_extract( + path, + "sro", + observed_at="2026-08-12T10:00:00Z", + expected_extract_id=extract_id, + ) + + def test_mosd_mutation_changes_only_the_live_source_in_place(self) -> None: + files = published_files(self.root) + before_digests = {path: digest(path) for path in files} + mosd = self.root / RELAY_DIRECTORY / RELAY_FILENAMES["mosd"] + before_inode = mosd.stat().st_ino + before_row = query( + mosd, + "SELECT * FROM relay_beneficiary_enrolment WHERE uin = ?", + ("2300010248",), + )[0] + + mutate_mosd_state(mosd, "2300010248", True, "2026-07-05T10:00:00Z") + + after_row = query( + mosd, + "SELECT * FROM relay_beneficiary_enrolment WHERE uin = ?", + ("2300010248",), + )[0] + changed = {path for path in files if digest(path) != before_digests[path]} + self.assertEqual(changed, {mosd}) + self.assertEqual(mosd.stat().st_ino, before_inode) + self.assertEqual(before_row[0], after_row[0]) + self.assertNotEqual(before_row[1], after_row[1]) + self.assertEqual(after_row[2], "active") + self.assertEqual(after_row[3], "2026-07-05T10:00:00Z") + self.assertEqual(after_row[4], "2300010248") + self.assertEqual((before_row[5], after_row[5]), (0, 1)) + for suffix in ("-journal", "-shm", "-wal"): + self.assertFalse(mosd.with_name(mosd.name + suffix).exists()) + + def test_module_cli_publishes_and_mutates(self) -> None: + with tempfile.TemporaryDirectory() as cli_directory: + cli_root = Path(cli_directory) + publish = subprocess.run( + [ + sys.executable, + "-m", + "solmara_lab.publisher", + "publish-all", + "--root", + str(cli_root), + ], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(publish.returncode, 0, publish.stderr) + mosd = cli_root / RELAY_DIRECTORY / RELAY_FILENAMES["mosd"] + mutation = subprocess.run( + [ + sys.executable, + "-m", + "solmara_lab.publisher", + "mutate-mosd", + "--root", + str(cli_root), + "--uin", + "2300010248", + "--duplicate-flag", + "true", + "--recorded-at", + "2026-07-05T10:00:00Z", + ], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(mutation.returncode, 0, mutation.stderr) + self.assertEqual( + query( + mosd, + "SELECT duplicate_flag, recorded_at FROM relay_beneficiary_enrolment WHERE uin = ?", + ("2300010248",), + ), + [(1, "2026-07-05T10:00:00Z")], + ) + extract_id = "sro-poverty-20260705T110000Z" + extract = subprocess.run( + [ + sys.executable, + "-m", + "solmara_lab.publisher", + "publish-extract", + "--root", + str(cli_root), + "--authority", + "sro", + "--published-at", + "2026-07-05T11:00:00Z", + "--extract-id", + extract_id, + ], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(extract.returncode, 0, extract.stderr) + extract_path = cli_root / EVIDENCE_DIRECTORY / f"{extract_id}.sqlite" + self.assertEqual( + query(extract_path, "SELECT published_at, publisher, extract_id FROM evidence_extract"), + [("2026-07-05T11:00:00Z", PUBLISHERS["sro"], extract_id)], + ) + self.assertEqual(stat.S_IMODE(extract_path.stat().st_mode), 0o444) + + +if __name__ == "__main__": + unittest.main() diff --git a/home/Dockerfile b/home/Dockerfile index 325ef19..db3090f 100644 --- a/home/Dockerfile +++ b/home/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine AS build +FROM node@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 AS build WORKDIR /app RUN corepack enable @@ -21,7 +21,7 @@ ENV SOLMARA_LAB_ROOT=/app ENV CI=true RUN pnpm build -FROM node:22-alpine +FROM node@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 WORKDIR /app/home ENV NODE_ENV=production diff --git a/home/e2e/home.test.ts b/home/e2e/home.test.ts index 1b841d3..78261c3 100644 --- a/home/e2e/home.test.ts +++ b/home/e2e/home.test.ts @@ -2,8 +2,8 @@ import { expect, test } from '@playwright/test'; const NAV_LINKS = ['How it works', 'Stories', 'Citizen demo', 'Developers', 'Status']; -const evaluationUrl = (configuredUrl: string | undefined, fallbackUrl: string) => - `${(configuredUrl ?? fallbackUrl).replace('127.0.0.1', 'localhost').replace(/\/+$/, '')}/v1/evaluations`; +const evidenceUrl = (configuredUrl: string | undefined, fallbackUrl: string) => + `${(configuredUrl ?? fallbackUrl).replace('127.0.0.1', 'localhost').replace(/\/+$/, '')}/v1/evidence`; test('landing renders with header nav and every section in order', async ({ page }) => { const response = await page.goto('/'); @@ -126,14 +126,14 @@ test('country, developer, and status inventories have dedicated routes', async ( await expect(page.locator('#nation .persona')).toHaveCount(12); await page.goto('/developers'); - await expect(page.locator('#engineer-door .curl-example')).toHaveCount(4); + await expect(page.locator('#engineer-door .curl-example')).toHaveCount(5); await expect(page.locator('#engineer-door')).toContainText('pension-payment-review'); await page.goto('/status'); - await expect(page.locator('#status .status')).toHaveCount(17); + await expect(page.locator('#status .status')).toHaveCount(18); }); -test('non-developer page data excludes developer-only published tokens', async ({ request }) => { +test('no page publishes runtime tokens', async ({ request }) => { test.skip(process.env.SOLMARA_HOME_E2E_MODE === 'live', 'uses the offline server token fixture'); for (const path of ['/', '/country', '/status']) { @@ -142,7 +142,7 @@ test('non-developer page data excludes developer-only published tokens', async ( } const developerResponse = await request.get('/developers'); - expect(await developerResponse.text()).toContain('route-scope-test-token'); + expect(await developerResponse.text()).not.toContain('route-scope-test-token'); }); test('purposes page lists every purpose with plain language and working anchors', async ({ page }) => { @@ -153,43 +153,37 @@ test('purposes page lists every purpose with plain language and working anchors' await expect(page.locator('#child-benefit-review .plain')).not.toBeEmpty(); // Denial codes link to the problem-code reference. await expect( - page.locator('#child-benefit-review a[href="/problem-codes#pdp.purpose_not_permitted"]') + page.locator('#child-benefit-review a[href="/problem-codes#not_authorized"]') ).toBeVisible(); }); -test('problem-codes anchors resolve, including pdp.purpose_not_permitted', async ({ page }) => { - await page.goto('/problem-codes#pdp.purpose_not_permitted'); - await expect(page.locator('[id="pdp.purpose_not_permitted"]')).toBeVisible(); +test('problem-codes anchors resolve, including current Evidence authorization denials', async ({ page }) => { + await page.goto('/problem-codes#not_authorized'); + await expect(page.locator('[id="not_authorized"]')).toBeVisible(); // The raw-row refusal a skeptic hits is documented too. - await expect(page.locator('[id="request.invalid"]')).toBeVisible(); + await expect(page.locator('[id="malformed_request"]')).toBeVisible(); }); -test('anatomy lists every relay and notary with repo config links', async ({ page }) => { +test('anatomy lists five Relay V2 projects and six authority Evidence cells with config links', async ({ page }) => { await page.goto('/anatomy'); - await expect(page.locator('#relays .entity')).toHaveCount(6); - await expect(page.locator('#notaries .entity')).toHaveCount(6); - const craLinks = page.locator('#cra-civil-relay a.config-link'); - await expect(craLinks.filter({ hasText: 'projects/cra-civil/registry-stack.yaml' })).toHaveAttribute( - 'href', - /github\.com.*projects\/cra-civil\/registry-stack\.yaml/ - ); - await expect( - craLinks.filter({ hasText: 'runtime/registry-projects/local/cra-civil/relay/relay.yaml' }) - ).toHaveAttribute('href', /github\.com.*relay\/relay\.yaml/); - await expect(craLinks.filter({ hasText: 'ministries/interior-civil' })).toHaveAttribute( - 'href', - /github\.com.*ministries\/interior-civil/ - ); + await expect(page.locator('#relays .entity')).toHaveCount(5); + for (const id of ['cra-evidence', 'nia-evidence', 'sro-evidence', 'mosd-programme-evidence', 'sipf-evidence', 'nagdi-evidence']) { + await expect(page.locator(`#${id}`)).toBeVisible(); + } + await expect(page.locator('#cra-relay a.config-link')).toHaveAttribute('href', /github\.com.*relays\/cra/); + await expect(page.locator('#cra-evidence a.config-link')).toHaveAttribute('href', /github\.com.*evidence\/cells\/cra/); }); test('status grid shows the whole topology', async ({ page }) => { await page.goto('/status'); - await expect(page.locator('#status .status')).toHaveCount(17); + await expect(page.locator('#status .status')).toHaveCount(18); + await expect(page.locator('#status .status.up')).toHaveCount(17); + await expect(page.locator('#status .status', { hasText: 'Optional eSignet' })).toContainText('down'); }); test('engineer door always shows the copy-as-curl examples', async ({ page }) => { await page.goto('/developers'); - await expect(page.locator('#engineer-door .curl-example')).toHaveCount(4); + await expect(page.locator('#engineer-door .curl-example')).toHaveCount(5); // The skeptic wrong-purpose curl is one of them. await expect(page.locator('#engineer-door')).toContainText('pension-payment-review'); }); @@ -215,22 +209,23 @@ test('the nation map renders district labels on the committed district geometry' await expect(page.locator('#solmara-preview .map .district-label', { hasText: 'Ketterin' })).toBeVisible(); }); -test('explorer renders all five published artifact families from the live bundle', async ({ page }) => { +test('explorer renders the published authority Evidence artifact families from the live bundle', async ({ page }) => { test.skip(process.env.SOLMARA_HOME_E2E_MODE !== 'live', 'requires the live static-metadata bundle'); await page.goto('/explorer'); - for (const id of ['api-catalog', 'datasets', 'services', 'offerings', 'policies']) { + for (const id of ['api-catalog', 'services', 'offerings', 'policies']) { await expect(page.locator(`#${id}`)).toBeVisible(); } + await expect(page.locator('#datasets')).toHaveCount(0); await expect(page.locator('#offerings .entity').first()).toBeVisible(); // Offerings cross-link to purposes. await expect(page.locator('#offerings a[href^="/purposes#"]').first()).toBeVisible(); }); -test('engineer door publishes the synthetic demo tokens', async ({ page }) => { - test.skip(process.env.SOLMARA_HOME_E2E_MODE !== 'live', 'demo tokens come from the container allowlist env'); +test('engineer door never publishes runtime tokens', async ({ page }) => { await page.goto('/developers'); - await expect(page.locator('#engineer-door .token').first()).toBeVisible(); - await expect(page.locator('#engineer-door .token-disclaimer')).toContainText('synthetic'); + await expect(page.locator('#engineer-door .token')).toHaveCount(0); + await expect(page.locator('#engineer-door .token-disclaimer')).toContainText('never publishes them'); + await expect(page.locator('#engineer-door')).toContainText('$CRA_EVIDENCE_ACCESS_TOKEN'); }); test('landing fails closed when the scenario runner is unavailable', async ({ page }) => { @@ -246,10 +241,10 @@ test('purpose lens: the live review reveals evidence and the wrong-purpose chall await page.goto('/'); await expect( - page.locator('#proof .proof-grid > div').filter({ hasText: 'live registries' }).locator('strong') + page.locator('#proof .proof-grid > div').filter({ hasText: 'authority Evidence cells' }).locator('strong') ).toHaveText('6'); await expect( - page.locator('#solmara-preview .country-facts > div').filter({ hasText: 'Live registries' }).locator('dd') + page.locator('#solmara-preview .country-facts > div').filter({ hasText: 'Evidence cells' }).locator('dd') ).toHaveText('6'); // The boundary challenge appears only after a successful live evidence run. @@ -271,21 +266,11 @@ test('purpose lens: the live review reveals evidence and the wrong-purpose chall }); expect(requestDeadSpace).toBeLessThan(80); - // Alternate purposes and raw requests stay out of the primary flow. When the - // visitor opens them, the native select remains bounded by its card and uses - // readable story labels instead of long identifier-heavy option labels. - const advanced = page.locator('#purpose-limitation .advanced-request'); - await expect(advanced).not.toHaveAttribute('open', ''); - await advanced.locator(':scope > summary').click(); - const purposeSelect = advanced.getByLabel('Alternate purpose'); - await expect(purposeSelect).toBeVisible(); - const selectFits = await purposeSelect.evaluate((select) => { - const selectRect = select.getBoundingClientRect(); - const cardRect = select.closest('.purpose-picker')?.getBoundingClientRect(); - return Boolean(cardRect && selectRect.width <= cardRect.width); - }); - expect(selectFits).toBe(true); - await expect(advanced.locator('.request-inspector')).not.toHaveAttribute('open', ''); + // Alternate-purpose controls were removed from the landing flow. The fixed + // challenge exercises the reviewed wrong-purpose path without selector or raw + // request exposure. + await expect(page.locator('#purpose-limitation .advanced-request')).toHaveCount(0); + await expect(page.locator('#purpose-limitation .request-inspector')).toHaveCount(0); // The default challenge reuses the evidence request for pension review. await page.getByRole('button', { name: 'Test the safeguard' }).click(); @@ -300,8 +285,7 @@ test('purpose lens: the live review reveals evidence and the wrong-purpose chall test('purpose lens: a needs-attention result never renders as successful evidence', async ({ page }) => { test.skip(process.env.SOLMARA_HOME_E2E_MODE !== 'live', 'requires live scenario metadata'); await page.emulateMedia({ reducedMotion: 'reduce' }); - await page.goto('/'); - await page.route('**/api/scenarios/birth-to-child-benefit/steps/positive/run', async (route) => { + await page.route(/\/api\/scenarios\/birth-to-child-benefit\/steps\/positive\/run$/, async (route) => { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ @@ -326,6 +310,7 @@ test('purpose lens: a needs-attention result never renders as successful evidenc }) }); }); + await page.goto('/'); await page.getByRole('button', { name: 'Run the check without sharing records' }).click(); @@ -341,16 +326,12 @@ test('purpose lens: a needs-attention result never renders as successful evidenc test('purpose lens: an unexpected error never renders as a successful safeguard refusal', async ({ page }) => { test.skip(process.env.SOLMARA_HOME_E2E_MODE !== 'live', 'requires live scenario metadata'); await page.emulateMedia({ reducedMotion: 'reduce' }); - await page.goto('/'); - await page.getByRole('button', { name: 'Run the check without sharing records' }).click(); - await expect(page.locator('#purpose-limitation')).toBeVisible({ timeout: 30_000 }); - - await page.route('**/api/scenarios/birth-to-child-benefit/steps/positive/run', async (route) => { + await page.route(/\/api\/scenarios\/birth-to-child-benefit\/steps\/purpose-denial\/run$/, async (route) => { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ result: { - step_id: 'positive', + step_id: 'purpose-denial', friendly: { title: 'Request needs attention.', message: 'The downstream service could not complete the safeguard test.', @@ -370,6 +351,10 @@ test('purpose lens: an unexpected error never renders as a successful safeguard }) }); }); + await page.goto('/'); + await page.getByRole('button', { name: 'Run the check without sharing records' }).click(); + await expect(page.locator('#purpose-limitation')).toBeVisible({ timeout: 30_000 }); + await page.getByRole('button', { name: 'Test the safeguard' }).click(); const boundaryResult = page.locator('#purpose-limitation .boundary-result'); @@ -394,7 +379,8 @@ test('story page: stepper runs an evaluate step and a purpose-denial step with a // collector gathers source-owned predicates, but does not compose eligibility. await expect(page.locator('#credential .inspector')).toContainText('Evidence returned', { timeout: 30_000 }); await expect(page.locator('#credential .inspector')).toContainText('child-benefit-federator'); - await expect(page.locator('#credential .inspector')).toContainText('Source authorities4'); + await expect(page.locator('#credential .inspector')).toContainText('Source authorities'); + await expect(page.locator('#credential .inspector')).toContainText('4'); await expect(page.locator('#credential .inspector')).toContainText('not_composed'); // The purpose-denial step is first-class and renders the stable problem code linked to /problem-codes. @@ -403,7 +389,7 @@ test('story page: stepper runs an evaluate step and a purpose-denial step with a await expect(denialLink).toBeVisible({ timeout: 30_000 }); }); -test('citizen story renders runnable curls for each authority call', async ({ page }) => { +test('citizen story renders sanitized curl skeletons for each authority call', async ({ page }) => { test.skip(process.env.SOLMARA_HOME_E2E_MODE !== 'live', 'requires a live scenario runner behind the stack'); await page.goto('/stories/citizen-self-service'); @@ -414,14 +400,13 @@ test('citizen story renders runnable curls for each authority call', async ({ pa const authorityRequests = result.locator('.request-list .peer-call'); await expect(authorityRequests).toHaveCount(2); - await expect(authorityRequests.getByRole('button', { name: 'Copy as curl' })).toHaveCount(2); + await expect(authorityRequests.getByRole('button', { name: 'Copy safe curl skeleton' })).toHaveCount(2); await expect(authorityRequests.nth(0)).toContainText( - evaluationUrl(process.env.CRA_NOTARY_URL, 'http://localhost:4325') + evidenceUrl(process.env.SOLMARA_CRA_EVIDENCE_URL, 'https://evidence.solmara.invalid/evidence/cra') ); await expect(authorityRequests.nth(1)).toContainText( - evaluationUrl(process.env.NIA_NOTARY_URL, 'http://localhost:4326') + evidenceUrl(process.env.SOLMARA_NIA_EVIDENCE_URL, 'https://evidence.solmara.invalid/evidence/nia') ); - await expect(result).not.toContainText('solmara://authority-notaries'); }); test('story page fits a mobile viewport without horizontal overflow', async ({ page }) => { diff --git a/home/src/lib/components/EngineerDoor.svelte b/home/src/lib/components/EngineerDoor.svelte index 7391d0c..80e0d7d 100644 --- a/home/src/lib/components/EngineerDoor.svelte +++ b/home/src/lib/components/EngineerDoor.svelte @@ -1,8 +1,7 @@ @@ -19,15 +19,15 @@

The Nation

-

One island, six live authorities, ten future registries in view

+

One island, six live authorities, and future registries in view

- {#each liveRegistries as registry} + {#each evidenceOfferings as offering}
-

{registry.title}

-

{registry.authority?.name}

- {registry.entities.length} entities, {registry.purposes.length} purposes +

{offering.title}

+

{offering.issuing_authority?.name}

+ {offering.access?.source_type ?? 'Evidence'} · {offering.purposes.length} purpose
{/each} {#each grayRegistries as registry} diff --git a/home/src/lib/components/ProofStrip.svelte b/home/src/lib/components/ProofStrip.svelte index 2920f5a..9a73230 100644 --- a/home/src/lib/components/ProofStrip.svelte +++ b/home/src/lib/components/ProofStrip.svelte @@ -5,10 +5,10 @@ export let scenarios: Scenario[] = []; $: authorities = metadata.available - ? new Set(metadata.catalog.datasets.map((dataset) => dataset.authority?.id ?? dataset.authority?.name).filter(Boolean)).size + ? new Set(metadata.offerings.map((offering) => offering.issuing_authority?.id ?? offering.issuing_authority?.name).filter(Boolean)).size : null; - $: registries = metadata.available - ? metadata.catalog.datasets.length + $: evidenceCells = metadata.available + ? metadata.catalog.data_services?.length ?? authorities : null; $: journeys = scenarios.length || null; @@ -17,7 +17,7 @@
1synthetic country
{authorities ?? 'Unavailable'}live authorities
-
{registries ?? 'Unavailable'}live registries
+
{evidenceCells ?? 'Unavailable'}authority Evidence cells
{journeys ?? 'Unavailable'}guided policy journeys
0real resident records
diff --git a/home/src/lib/components/PurposeLens.svelte b/home/src/lib/components/PurposeLens.svelte index 210e357..0cc68f5 100644 --- a/home/src/lib/components/PurposeLens.svelte +++ b/home/src/lib/components/PurposeLens.svelte @@ -1,17 +1,14 @@
@@ -255,7 +231,7 @@ No. This request is allowed only for Mateo's child-benefit review. Try to reuse it for a pension review and the same services must refuse.

-
@@ -298,34 +274,9 @@ {/if}
-
- Explore other purposes or inspect the technical request -
-
- -

- The request will carry {selectedPurposeView?.slug ?? 'no-purpose-selected'}. -

- -
-
- View the request preview and curl -
{flipPreviewLine}
- {#if flipCurl} - - {/if} -
-
-
+

+ This fixed scenario sends its reviewed pension purpose in the JSON body. The browser cannot override a scenario purpose. +

{/if}
diff --git a/home/src/lib/components/SolmaraPreview.svelte b/home/src/lib/components/SolmaraPreview.svelte index 23b13f6..22e0c16 100644 --- a/home/src/lib/components/SolmaraPreview.svelte +++ b/home/src/lib/components/SolmaraPreview.svelte @@ -8,10 +8,10 @@ export let country: any; $: authorities = metadata.available - ? new Set(metadata.catalog.datasets.map((dataset) => dataset.authority?.id ?? dataset.authority?.name).filter(Boolean)).size + ? new Set(metadata.offerings.map((offering) => offering.issuing_authority?.id ?? offering.issuing_authority?.name).filter(Boolean)).size : null; - $: registries = metadata.available - ? metadata.catalog.datasets.length + $: evidenceCells = metadata.available + ? metadata.catalog.data_services?.length ?? authorities : null; @@ -27,7 +27,7 @@

Live authorities
{authorities ?? 'Unavailable'}
-
Live registries
{registries ?? 'Unavailable'}
+
Evidence cells
{evidenceCells ?? 'Unavailable'}
Real records
0
Explore Solmara and its full synthetic cast diff --git a/home/src/lib/components/TrustStrip.svelte b/home/src/lib/components/TrustStrip.svelte index a8d427a..7233ebf 100644 --- a/home/src/lib/components/TrustStrip.svelte +++ b/home/src/lib/components/TrustStrip.svelte @@ -44,8 +44,8 @@
-

Pinned source

-

Registry Stack main {versions.REGISTRY_STACK_SOURCE_COMMIT?.slice(0, 12) ?? 'missing'}

+

Pinned release

+

Registry Stack v0.20.1 {versions.REGISTRY_STACK_SOURCE_COMMIT?.slice(0, 12) || 'missing'}

Evidence {shortDigest(versions.SOLMARA_EVIDENCE_IMAGE)}

diff --git a/home/src/lib/curl.test.ts b/home/src/lib/curl.test.ts index a3a4c3c..e95d8a0 100644 --- a/home/src/lib/curl.test.ts +++ b/home/src/lib/curl.test.ts @@ -5,7 +5,7 @@ import { runnableRequestSources, toCurl } from './curl'; describe('runnableRequestSources', () => { const multiPreview: RequestSource = { method: 'MULTI', - url: 'solmara://registry-evidence', + url: 'solmara://authority-evidence', purpose: 'citizen-self-service' }; @@ -30,7 +30,7 @@ describe('runnableRequestSources', () => { expect(commands).toHaveLength(2); expect(commands[0]).toContain("curl -sS -X POST 'https://localhost:4341/v1/evidence'"); expect(commands[1]).toContain("curl -sS -X POST 'https://localhost:4341/v1/evidence'"); - expect(commands.join('\n')).not.toContain('solmara://registry-evidence'); + expect(commands.join('\n')).not.toContain('solmara://authority-evidence'); }); it('uses the primary request when no underlying calls are present', () => { diff --git a/home/src/lib/curl.ts b/home/src/lib/curl.ts index 99632ed..37881f6 100644 --- a/home/src/lib/curl.ts +++ b/home/src/lib/curl.ts @@ -13,9 +13,8 @@ export function runnableRequestSources( } /** - * Render a request as a copy-as-curl snippet. Headers are prepared server-side: - * redacted by default, with only allowlisted synthetic lab tokens republished - * for the visitor center. URLs are already rewritten to host-reachable ones. + * Render an already-safe request as a copy-as-curl snippet. Runtime credential + * markers are omitted rather than copied into the browser. */ export function toCurl(source: RequestSource | undefined, overrideHeaders: Record = {}): string { if (!source || !source.url) return ''; @@ -23,6 +22,7 @@ export function toCurl(source: RequestSource | undefined, overrideHeaders: Recor const headers = { ...(source.headers ?? {}), ...overrideHeaders }; const lines: string[] = [`curl -sS -X ${method} '${source.url}'`]; for (const [key, value] of Object.entries(headers)) { + if (/authorization|x-api-key/i.test(key) || /runtime token hidden|bearer\s+[a-z0-9._-]+/i.test(value)) continue; lines.push(` -H '${key}: ${value}'`); } if (source.body !== undefined && source.body !== null) { diff --git a/home/src/lib/runresult.ts b/home/src/lib/runresult.ts index 3923af8..7070b32 100644 --- a/home/src/lib/runresult.ts +++ b/home/src/lib/runresult.ts @@ -20,7 +20,7 @@ export type ClaimResult = { id: string; satisfied: boolean | null; raw: Dict }; /** Read the per-claim evaluation results defensively across possible field names. */ export function claimResults(result: StepRunResult | null | undefined): ClaimResult[] { const body = responseBody(result); - const list = Array.isArray(body.results) ? body.results : []; + const list = Array.isArray(result?.results) ? result.results : Array.isArray(body.results) ? body.results : []; return list.filter((entry): entry is Dict => !!entry && typeof entry === 'object').map((entry) => ({ id: String(entry.claim_id ?? entry.claim ?? entry.id ?? entry.name ?? 'claim'), satisfied: typeof entry.satisfied === 'boolean' ? entry.satisfied : null, @@ -54,7 +54,8 @@ export function hasExpectedSuccessfulClaims( */ export function explicitProblemCode(result: StepRunResult | null | undefined): string | null { const body = responseBody(result); - const direct = body.code ?? body.problem_code ?? body.error; + const responseCode = (result?.response_source as Record | undefined)?.code; + const direct = responseCode ?? body.code ?? body.problem_code ?? body.error; if (typeof direct === 'string' && direct) return direct; if (typeof body.type === 'string' && body.type.includes('/')) { const tail = body.type.split(/[/#]/).filter(Boolean).pop(); @@ -92,12 +93,11 @@ export function isDenial(result: StepRunResult | null | undefined): boolean { /** The Evidence purpose code actually sent. */ export function requestPurpose(result: StepRunResult | null | undefined): string | null { - const headers = result?.request_source?.headers ?? {}; const body = result?.request_source?.body; const bodyPurpose = body && typeof body === 'object' && !Array.isArray(body) ? (body as Record).purpose : undefined; - return result?.request_source?.purpose ?? (typeof bodyPurpose === 'string' ? bodyPurpose : undefined) ?? headers['Data-Purpose'] ?? headers['data-purpose'] ?? null; + return result?.request_source?.purpose ?? (typeof bodyPurpose === 'string' ? bodyPurpose : undefined) ?? null; } /** @@ -132,7 +132,7 @@ export function hopsFromResult(result: StepRunResult | null | undefined): string try { hops.push(`Question sent to ${new URL(url).host}`); } catch { - hops.push('Question sent to Registry Evidence'); + hops.push('Question sent to the authority Evidence service'); } } const purpose = requestPurpose(result); diff --git a/home/src/lib/server/data.test.ts b/home/src/lib/server/data.test.ts index 20f3fba..c12bf46 100644 --- a/home/src/lib/server/data.test.ts +++ b/home/src/lib/server/data.test.ts @@ -16,12 +16,13 @@ describe('home data adapters', () => { expect(services).toContain('static-metadata'); }); - it('reads the exact source commit and local Evidence image names', async () => { + it('publishes the exact Registry Stack release identity', async () => { const versions = await readVersions(); - expect(versions.REGISTRY_STACK_SOURCE_REF).toBe('main'); + expect(versions.REGISTRY_STACK_SOURCE_REF).toBe('v0.20.1'); expect(versions.REGISTRY_STACK_SOURCE_COMMIT).toMatch(/^[0-9a-f]{40}$/); - expect(versions.SOLMARA_EVIDENCE_IMAGE).toContain(':source'); - expect(versions.SOLMARA_MINT_IMAGE).toContain(':source'); + expect(versions.REGISTRY_RELAY_IMAGE).toMatch(/@sha256:[0-9a-f]{64}$/); + expect(versions.SOLMARA_EVIDENCE_IMAGE).toContain(':v0.20.1'); + expect(versions.SOLMARA_MINT_IMAGE).toContain(':v0.20.1'); }); it('keeps compose-internal health probes out of visitor links', async () => { @@ -29,7 +30,7 @@ describe('home data adapters', () => { return new Response('{}', { status: String(url).includes('/health') ? 200 : 503 }); }); const runner = status.find((item) => item.id === 'scenario-runner'); - const metadata = status.find((item) => item.id === 'metadata'); + const metadata = status.find((item) => item.id === 'deterministic-publisher'); expect(runner?.status).toBe('up'); expect(runner?.href).toBeUndefined(); expect(metadata?.href).toBe('/.well-known/api-catalog'); diff --git a/home/src/lib/server/data.ts b/home/src/lib/server/data.ts index 4e6290a..fe4fc53 100644 --- a/home/src/lib/server/data.ts +++ b/home/src/lib/server/data.ts @@ -4,7 +4,7 @@ import { runtime, joinedUrl } from './runtime'; import { buildPublicUrlMap, mapPublicUrl } from './urlmap'; import { statusProbes } from './services'; import { readSeedSummary, readSmokeEvidence } from './evidence'; -import { buildCurlExamples, parsePublishedTokens } from './tokens'; +import { buildCurlExamples } from './tokens'; import { readPurposes } from './purposes'; import type { ChangelogEntry, ChangelogFullEntry, HomeData, MetadataBundle, Persona, Scenario, StatusItem } from '$lib/types'; @@ -34,7 +34,7 @@ export type LandingData = Pick< export type DeveloperData = Pick< HomeData, - 'publishedTokens' | 'curlExamples' | 'versions' | 'repoUrl' + 'curlExamples' | 'versions' | 'repoUrl' >; export type StatusData = Pick< @@ -77,23 +77,19 @@ export async function loadLandingData(fetcher: FetchLike = fetch): Promise { - const publishedTokens = parsePublishedTokens(); return { - publishedTokens, - curlExamples: buildCurlExamples(publishedTokens), + curlExamples: buildCurlExamples(), versions: await readVersions(), repoUrl: runtime.repoUrl }; } /** - * Load only the country route's public fields. Raw published tokens and - * token-bearing curl examples belong to the developer route and must never - * enter a non-developer page-data payload. + * Load only the country route's public fields. */ export async function loadCountryData(fetcher: FetchLike = fetch): Promise { const [metadata, personas, districts, provinces, country] = await Promise.all([ @@ -213,29 +209,49 @@ export async function fetchMetadata(fetcher: FetchLike = fetch): Promise, + catalog: Record, + offeringsPayload: Record, + policiesPayload: Record +): MetadataBundle { + return { + available: true, + apiCatalog, + catalog: { + datasets: arrayValue(catalog.datasets), + gray_registries: arrayValue(catalog.gray_registries), + authorities: arrayValue(catalog.authorities), + data_services: arrayValue(catalog.data_services), + public_services: arrayValue(catalog.public_services) + }, + offerings: arrayValue(offeringsPayload.offerings), + policies: arrayValue(policiesPayload['@graph']) + }; +} + export async function fetchScenarios(fetcher: FetchLike = fetch): Promise<{ scenarios: Scenario[]; defaultScenarioId: string }> { try { const summary = await fetchJson(fetcher, joinedUrl(runtime.scenarioRunnerUrl, '/v1/scenarios')); diff --git a/home/src/lib/server/problemcodes.test.ts b/home/src/lib/server/problemcodes.test.ts index 5932776..9e3dd7e 100644 --- a/home/src/lib/server/problemcodes.test.ts +++ b/home/src/lib/server/problemcodes.test.ts @@ -42,7 +42,7 @@ describe('problem-code assembly', () => { expect(ids).toContain('malformed_request'); }); - it('uses the current Registry Evidence problem type URI', () => { + it('uses the current Evidence problem type URI', () => { const pdp = codes.find((code) => code.code === 'not_authorized'); expect(pdp?.typeUri).toBe('https://registrystack.org/problems/evidence/not_authorized'); expect(pdp?.problemJson.status).toBe(403); diff --git a/home/src/lib/server/problemcodes.ts b/home/src/lib/server/problemcodes.ts index 5f069e5..d6a7a3f 100644 --- a/home/src/lib/server/problemcodes.ts +++ b/home/src/lib/server/problemcodes.ts @@ -2,7 +2,7 @@ import type { ProblemCode, Purpose, Scenario, StoryStepLink } from '$lib/types'; /** * Static, maintained metadata for every stable problem code the lab can emit. - * The `typeUri` values are the problem type URIs emitted by Registry Evidence + * The `typeUri` values are the problem type URIs emitted by authority Evidence * responses. The set of codes is assembled from the purpose catalogue (which * lists each purpose's denial codes) plus these entries, so the page never * hand-maintains prose that can drift from the catalogue. Meanings are plain @@ -17,15 +17,22 @@ const CODE_META: Record< status: 403, typeUri: 'https://registrystack.org/problems/evidence/not_authorized', meaning: - 'The requester grant does not authorize this requirement, purpose, response format, or selector shape. Registry Evidence refuses before source access and discloses nothing.' + 'The requester grant does not authorize this requirement, purpose, response format, or selector shape. The authority Evidence service refuses before source access and discloses nothing.' }, malformed_request: { title: 'Invalid evidence request', status: 400, typeUri: 'https://registrystack.org/problems/evidence/malformed_request', meaning: - 'The request does not match the closed Evidence request contract. Registry Evidence rejects it before evaluating a requirement.', + 'The request does not match the closed Evidence request contract. The authority Evidence service rejects it before evaluating a requirement.', coverage: 'Covered by the Evidence bundle fixtures and current-main contract tests.' + }, + request_refused: { + title: 'Request refused', + status: 403, + typeUri: 'https://registrystack.org/problems/evidence/request_refused', + meaning: + 'The programme or authority refused the request without revealing whether any source record matched. The refusal is intentionally generic for wrong-purpose and unauthorized paths.' } }; diff --git a/home/src/lib/server/purposes.test.ts b/home/src/lib/server/purposes.test.ts index 1a4a874..a93717d 100644 --- a/home/src/lib/server/purposes.test.ts +++ b/home/src/lib/server/purposes.test.ts @@ -8,8 +8,8 @@ const SAMPLE = `# Solmara Purpose Catalogue | Purpose IRI | Advertised by | Enforced by | Story | Denial problem codes | |---|---|---|---|---| -| \`child-benefit-review\` | CRA, NIA | Registry Evidence | Birth to child benefit | \`not_authorized\` | -| \`voucher-eligibility-review\` | NAgDI | Registry Evidence | Farmer climate-smart voucher | \`not_authorized\` | +| \`child-benefit-review\` | CRA, NIA | Authority Evidence cells | Birth to child benefit | \`not_authorized\` | +| \`voucher-eligibility-review\` | NAgDI | Authority Evidence cells | Farmer climate-smart voucher | \`not_authorized\` | ## Purpose Rules @@ -41,7 +41,7 @@ function scenario(id: string, title: string, steps: { id: string; label: string; prompt: '', button: '', request_summary: '', - request_preview: { method: 'POST', url: 'http://x/v1/evaluations', headers: { 'Data-Purpose': step.purpose } } + request_preview: { method: 'POST', url: 'http://x/v1/evaluations', body: { purpose: step.purpose } } })), receipt: [] }; @@ -55,7 +55,7 @@ describe('purposes parser', () => { expect(child.iri).toBe('child-benefit-review'); expect(child.slug).toBe('child-benefit-review'); expect(child.advertisedBy).toBe('CRA, NIA'); - expect(child.enforcedBy).toBe('Registry Evidence'); + expect(child.enforcedBy).toBe('Authority Evidence cells'); expect(child.story).toBe('Birth to child benefit'); expect(child.denialCodes).toEqual(['not_authorized']); expect(child.plainLanguage).toContain('permits evidence needed to determine whether a child'); diff --git a/home/src/lib/server/purposes.ts b/home/src/lib/server/purposes.ts index 2a1c4e6..f58dba7 100644 --- a/home/src/lib/server/purposes.ts +++ b/home/src/lib/server/purposes.ts @@ -16,11 +16,22 @@ export function parsePurposes(raw: string): Purpose[] { const rules = parseRuleParagraphs(raw); return raw .split('\n') - .filter((line) => line.startsWith(TABLE_ROW_PREFIX) && line.split('|').length === 7) + .filter((line) => line.startsWith(TABLE_ROW_PREFIX) && [5, 7].includes(line.split('|').length)) .map((line) => { const cells = line.split('|').slice(1, -1).map((cell) => cell.trim()); const iri = stripTicks(cells[0]); const slug = iri.split('/').pop() ?? iri; + if (cells.length === 3) { + return { + iri, + slug, + advertisedBy: cells[1], + enforcedBy: 'Authority Evidence cells', + story: slug.replace(/-/g, ' '), + denialCodes: ['not_authorized'], + plainLanguage: `${cells[1]} may answer this purpose through ${cells[2]}. Wrong-purpose and unauthorized requests disclose nothing.` + }; + } return { iri, slug, @@ -69,9 +80,8 @@ export function storyLinksForPurpose(iri: string, scenarios: Scenario[]): StoryS for (const scenario of scenarios) { for (const step of scenario.steps) { const preview = step.request_preview; - const headers = preview?.headers ?? {}; const body = preview?.body && typeof preview.body === 'object' ? preview.body as Record : {}; - const sent = preview?.purpose ?? body.purpose ?? headers['Data-Purpose'] ?? headers['data-purpose']; + const sent = preview?.purpose ?? body.purpose; if (sent === iri) { links.push({ storyId: scenario.id, storyTitle: scenario.title, stepId: step.id, stepLabel: step.label }); } diff --git a/home/src/lib/server/services.test.ts b/home/src/lib/server/services.test.ts index 9cc625b..0eeca14 100644 --- a/home/src/lib/server/services.test.ts +++ b/home/src/lib/server/services.test.ts @@ -1,55 +1,39 @@ import { describe, expect, it } from 'vitest'; import { statusProbes, topologyGroups } from './services'; -describe('status probe table', () => { - it('covers six Records APIs, Evidence, Mint, and shared applications', () => { - const probes = statusProbes('http://127.0.0.1:4300'); - expect(probes.filter((probe) => probe.role === 'relay')).toHaveLength(6); - expect(probes.filter((probe) => probe.role === 'evidence')).toHaveLength(2); - expect(probes.filter((probe) => probe.role === 'shared').length).toBeGreaterThanOrEqual(4); - }); - - it('uses public health probes for Evidence and Mint', () => { - const probes = statusProbes('http://127.0.0.1:4300'); - expect(probes.find((probe) => probe.id === 'registry-evidence')?.probeUrl).toBe( - 'https://localhost:4341/health' - ); - expect(probes.find((probe) => probe.id === 'registry-mint')?.probeUrl).toBe( - 'https://localhost:4341/health' - ); - expect(probes.find((probe) => probe.id === 'cra-civil-relay')?.probeUrl?.endsWith('/')).toBe(true); +describe('authority-cell topology', () => { + it('has exactly five Relays, six Evidence cells, and one shared Mint', () => { + const probes = statusProbes('http://portal.example'); + expect(probes.filter((probe) => probe.role === 'relay')).toHaveLength(5); + expect(probes.filter((probe) => probe.role === 'evidence')).toHaveLength(6); + expect(probes.filter((probe) => probe.id === 'lab-mint')).toHaveLength(1); + expect(probes.filter((probe) => probe.id === 'mosd-relay')).toHaveLength(1); + expect(probes.some((probe) => probe.id === 'sro-relay')).toBe(false); + expect(probes.some((probe) => probe.id === 'registry-evidence')).toBe(false); }); - it('reads compose-internal Evidence and Mint origins from the environment', () => { - const probes = statusProbes('http://127.0.0.1:4300', { - EVIDENCE_URL: 'http://evidence:8080', - MINT_URL: 'http://mint:8081' + it('uses distinct authority Evidence identities and configurable origins', () => { + const probes = statusProbes('http://portal.example', { + SOLMARA_CRA_EVIDENCE_URL: 'https://cra.example', + SOLMARA_NIA_EVIDENCE_URL: 'https://nia.example' }); - expect(probes.find((probe) => probe.id === 'registry-evidence')?.probeUrl).toBe('http://evidence:8080/health'); - expect(probes.find((probe) => probe.id === 'registry-mint')?.probeUrl).toBe('http://mint:8081/health'); + expect(probes.find((probe) => probe.id === 'cra-evidence')?.probeUrl).toBe('https://cra.example/health'); + expect(probes.find((probe) => probe.id === 'nia-evidence')?.probeUrl).toBe('https://nia.example/health'); }); - it('only marks browser-facing services with open links', () => { + it('links only browser-facing services', () => { const probes = statusProbes('http://portal.example'); - expect(probes.find((probe) => probe.id === 'metadata')?.href).toBe('/.well-known/api-catalog'); + expect(probes.find((probe) => probe.id === 'deterministic-publisher')?.href).toBe('/.well-known/api-catalog'); expect(probes.find((probe) => probe.id === 'portal')?.href).toBe('http://portal.example'); - expect(probes.find((probe) => probe.id === 'home')?.href).toBe('/'); - expect(probes.find((probe) => probe.id === 'registry-evidence')?.href).toBeUndefined(); - }); -}); - -describe('topology groups', () => { - const groups = topologyGroups('https://github.com/registrystack/solmara-lab'); - - it('groups authority Records APIs, Evidence and Mint, and shared services', () => { - expect(groups.map((group) => group.key)).toEqual(['relays', 'evidence', 'shared']); - expect(groups[0].services).toHaveLength(6); - expect(groups[1].services.map((service) => service.id)).toEqual(['registry-evidence', 'registry-mint']); + expect(probes.find((probe) => probe.id === 'cra-evidence')?.href).toBeUndefined(); }); - it('links Evidence to its runtime and reviewed bundle', () => { - const evidence = groups[1].services.find((service) => service.id === 'registry-evidence'); - expect(evidence?.config.map((link) => link.path)).toEqual(['evidence/runtime.yaml', 'evidence/bundle']); - expect(evidence?.config[1].url).toBe('https://github.com/registrystack/solmara-lab/tree/main/evidence/bundle'); + it('renders the approved deterministic topology groups and owned configs', () => { + const groups = topologyGroups('https://github.com/registrystack/solmara-lab'); + expect(groups.map((group) => group.key)).toEqual(['publisher', 'relays', 'evidence', 'programme', 'identity']); + expect(groups.find((group) => group.key === 'relays')?.services).toHaveLength(5); + expect(groups.find((group) => group.key === 'evidence')?.services.filter((service) => service.role === 'evidence')).toHaveLength(6); + expect(groups.find((group) => group.key === 'evidence')?.services.find((service) => service.id === 'cra-evidence')?.config[0].path).toBe('evidence/cells/cra'); + expect(groups.find((group) => group.key === 'identity')?.services[0].blurb).toContain('NIA Relay'); }); }); diff --git a/home/src/lib/server/services.ts b/home/src/lib/server/services.ts index c0dfaa0..c63f4fc 100644 --- a/home/src/lib/server/services.ts +++ b/home/src/lib/server/services.ts @@ -1,19 +1,7 @@ import { env } from '$env/dynamic/private'; import type { ConfigLink, TopologyGroup, TopologyService } from '$lib/types'; -/** - * The one env-driven service table for the visitor center. Every server-side - * probe URL, every anatomy config link, and the status grid read from here so no - * port or hostname is scattered across components. Probe URLs default to the - * published localhost ports and are overridden by the compose-internal URLs the - * home container receives (see the `home` service env in `compose.yaml`), which - * is why Relay, Evidence, and Mint are reachable for probing from the network. - * - * The internal probe origins mirror the same conventions as `urlmap.ts` and - * `scenarios/service_config.py`. - */ export type ServiceRole = 'shared' | 'relay' | 'evidence'; - type ServiceStatusSemantics = 'health' | 'auth-gated'; type ServiceDef = { @@ -32,192 +20,200 @@ type ServiceDef = { configPaths: { label: string; path: string }[]; }; +const shared = ( + service: Omit & { semantics?: ServiceStatusSemantics } +): ServiceDef => ({ ...service, role: 'shared', semantics: service.semantics ?? 'health' }); + +const relay = (service: Omit): ServiceDef => ({ + ...service, + role: 'relay', + semantics: 'auth-gated' +}); + +const evidence = (service: Omit): ServiceDef => ({ + ...service, + role: 'evidence', + semantics: 'health' +}); + export const SERVICES: ServiceDef[] = [ - { - id: 'metadata', - label: 'Published metadata', - role: 'shared', - blurb: 'Serves the CPSV-AP metadata bundle that the Nation grid and the explorer render from.', + shared({ + id: 'deterministic-publisher', + label: 'Deterministic publisher', + blurb: 'Builds the metadata publication, immutable Evidence extracts, and Relay SQLite sources from one reviewed synthetic input set.', probeEnv: 'STATIC_METADATA_URL', probeDefault: 'http://127.0.0.1:4331', probePath: '/.well-known/api-catalog', - semantics: 'health', browsable: true, configPaths: [ - { label: 'Assembly manifest', path: 'metadata/assembly.yaml' }, - { label: 'Published bundle', path: 'metadata/public' } - ] - }, - { - id: 'scenario-runner', - label: 'Scenario runner', - role: 'shared', - blurb: 'Runs the guided stories and the Purpose Lens against Registry Evidence.', - probeEnv: 'SCENARIO_RUNNER_URL', - probeDefault: 'http://127.0.0.1:4302', - probePath: '/health', - semantics: 'health', - configPaths: [ - { label: 'Runner API', path: 'scenario-runner/server.py' }, - { label: 'Scenario modules', path: 'scenarios' } + { label: 'Publisher', path: 'generator/solmara_lab/publisher.py' }, + { label: 'Published metadata', path: 'metadata/public' }, + { label: 'Generated SQLite outputs', path: 'output/sqlite' } ] - }, - { - id: 'portal', - label: 'Citizen portal', - role: 'shared', - blurb: 'The in-fiction resident application. Solmara Lab hands personas into it.', - probeEnv: 'PORTAL_PROBE_URL', - probeDefault: 'http://127.0.0.1:4300', - probePath: '/', - semantics: 'health', - browsable: true, - configPaths: [{ label: 'Portal app', path: 'portal' }] - }, - { - id: 'home', - label: 'Solmara Lab', - role: 'shared', - blurb: 'The out-of-fiction front door to the live synthetic country.', - semantics: 'health', - self: true, - browsable: true, - configPaths: [{ label: 'Home app', path: 'home' }] - }, - { - id: 'cra-civil-relay', - label: 'CRA civil relay', - role: 'relay', + }), + relay({ + id: 'cra-relay', + label: 'CRA Relay', authority: 'Civil Registration Authority', - blurb: 'Read-only relay over civil registration (births, deaths).', + blurb: 'Relay V2 exact lookups over the CRA civil source.', probeEnv: 'CRA_CIVIL_RELAY_URL', probeDefault: 'http://127.0.0.1:4311', - probePath: '/', - semantics: 'auth-gated', - configPaths: [ - { label: 'Registry project', path: 'projects/cra-civil/registry-stack.yaml' }, - { label: 'Generated Relay config', path: 'runtime/registry-projects/local/cra-civil/relay/relay.yaml' }, - { label: 'Seed and fixtures', path: 'ministries/interior-civil' } - ] - }, - { - id: 'nia-population-relay', - label: 'NIA population relay', - role: 'relay', + probePath: '/ready', + configPaths: [{ label: 'Relay V2 project', path: 'relays/cra' }] + }), + relay({ + id: 'nia-relay', + label: 'NIA Relay', authority: 'National Identity Agency', - blurb: 'Read-only relay over the population register.', + blurb: 'Relay V2 exact lookups over the population source, also available to the optional eSignet path.', probeEnv: 'NIA_POPULATION_RELAY_URL', probeDefault: 'http://127.0.0.1:4312', - probePath: '/', - semantics: 'auth-gated', - configPaths: [ - { label: 'Registry project', path: 'projects/nia-population/registry-stack.yaml' }, - { label: 'Generated Relay config', path: 'runtime/registry-projects/local/nia-population/relay/relay.yaml' }, - { label: 'Seed and fixtures', path: 'ministries/interior-population' } - ] - }, - { - id: 'sro-social-relay', - label: 'SRO social relay', - role: 'relay', - authority: 'Social Registry Office', - blurb: 'Read-only relay over the social registry (household poverty band).', - probeEnv: 'SRO_SOCIAL_RELAY_URL', - probeDefault: 'http://127.0.0.1:4313', - probePath: '/', - semantics: 'auth-gated', - configPaths: [ - { label: 'Registry project', path: 'projects/sro-social/registry-stack.yaml' }, - { label: 'Generated Relay config', path: 'runtime/registry-projects/local/sro-social/relay/relay.yaml' }, - { label: 'Seed and fixtures', path: 'ministries/social-development' } - ] - }, - { - id: 'programme-mis-relay', - label: 'Programme MIS relay', - role: 'relay', + probePath: '/ready', + configPaths: [{ label: 'Relay V2 project', path: 'relays/nia' }] + }), + relay({ + id: 'mosd-relay', + label: 'MoSD Programme Relay', authority: 'Ministry of Social Development Programme MIS', - blurb: 'Read-only relay over the integrated beneficiary registry.', - probeEnv: 'PROGRAMME_MIS_RELAY_URL', + blurb: 'Relay V2 exact lookups over the programme-owned beneficiary enrolment source.', + probeEnv: 'MOSD_PROGRAMME_RELAY_URL', probeDefault: 'http://127.0.0.1:4314', - probePath: '/', - semantics: 'auth-gated', - configPaths: [ - { label: 'Registry project', path: 'projects/mosd-programme/registry-stack.yaml' }, - { label: 'Generated Relay config', path: 'runtime/registry-projects/local/mosd-programme/relay/relay.yaml' }, - { label: 'Seed and fixtures', path: 'ministries/social-development' } - ] - }, - { - id: 'sipf-pensions-relay', - label: 'SIPF pensions relay', - role: 'relay', + probePath: '/ready', + configPaths: [{ label: 'Relay V2 project', path: 'relays/mosd' }] + }), + relay({ + id: 'sipf-relay', + label: 'SIPF Relay', authority: 'Social Insurance and Pensions Fund', - blurb: 'Read-only relay over pension case records.', + blurb: 'Relay V2 exact lookups over pension payment and survivor resources.', probeEnv: 'SIPF_PENSIONS_RELAY_URL', probeDefault: 'http://127.0.0.1:4315', - probePath: '/', - semantics: 'auth-gated', - configPaths: [ - { label: 'Registry project', path: 'projects/sipf-pensions/registry-stack.yaml' }, - { label: 'Generated Relay config', path: 'runtime/registry-projects/local/sipf-pensions/relay/relay.yaml' }, - { label: 'Seed and fixtures', path: 'ministries/labour-pensions' } - ] - }, - { - id: 'nagdi-agriculture-relay', - label: 'NAgDI agriculture relay', - role: 'relay', + probePath: '/ready', + configPaths: [{ label: 'Relay V2 project', path: 'relays/sipf' }] + }), + relay({ + id: 'nagdi-relay', + label: 'NAgDI Relay', authority: 'National Agricultural Data Institute', - blurb: 'Read-only relay over farmer and livestock registries.', + blurb: 'Relay V2 exact lookups over farmer voucher and livestock movement resources.', probeEnv: 'NAGDI_AGRICULTURE_RELAY_URL', probeDefault: 'http://127.0.0.1:4316', - probePath: '/', - semantics: 'auth-gated', - configPaths: [ - { label: 'Registry project', path: 'projects/nagdi-agriculture/registry-stack.yaml' }, - { label: 'Generated Relay config', path: 'runtime/registry-projects/local/nagdi-agriculture/relay/relay.yaml' }, - { label: 'Seed and fixtures', path: 'ministries/agriculture-nagdi' } - ] - }, - { - id: 'child-benefit-federator', - label: 'Child benefit federator', - role: 'shared', - purpose: 'child-benefit-review', - blurb: 'Collects minimized source-owned predicates as ordinary application evidence. It does not make the eligibility decision.', - probeEnv: 'CHILD_BENEFIT_FEDERATOR_URL', - probeDefault: 'http://127.0.0.1:4321', + probePath: '/ready', + configPaths: [{ label: 'Relay V2 project', path: 'relays/nagdi' }] + }), + evidence({ + id: 'cra-evidence', + label: 'CRA Evidence', + authority: 'Civil Registration Authority', + blurb: 'Issues CRA-signed minimized values from reviewed immutable extracts or CRA Relay lookups.', + probeEnv: 'SOLMARA_CRA_EVIDENCE_URL', + probeDefault: 'https://cra-evidence.solmara.registrystack.org', probePath: '/health', - semantics: 'health', - configPaths: [{ label: 'Federator service', path: 'scenario-runner/child_benefit_federator.py' }] - }, - { - id: 'registry-evidence', - label: 'Registry Evidence', - role: 'evidence', - purpose: 'all six reviewed Solmara purposes', - blurb: 'Evaluates eleven minimized requirements against authority Records APIs and returns signed evidence assertions.', - probeEnv: 'EVIDENCE_URL', - probeDefault: 'https://localhost:4341', + configPaths: [{ label: 'Authority Evidence cell', path: 'evidence/cells/cra' }] + }), + evidence({ + id: 'nia-evidence', + label: 'NIA Evidence', + authority: 'National Identity Agency', + blurb: 'Issues NIA-signed population status values.', + probeEnv: 'SOLMARA_NIA_EVIDENCE_URL', + probeDefault: 'https://nia-evidence.solmara.registrystack.org', probePath: '/health', - semantics: 'health', - configPaths: [ - { label: 'Runtime config', path: 'evidence/runtime.yaml' }, - { label: 'Evidence bundle', path: 'evidence/bundle' } - ] - }, - { - id: 'registry-mint', - label: 'Registry Mint', - role: 'evidence', - blurb: 'Issues short-lived requester-bound access tokens for the local Evidence service using private_key_jwt.', + configPaths: [{ label: 'Authority Evidence cell', path: 'evidence/cells/nia' }] + }), + evidence({ + id: 'sro-evidence', + label: 'SRO Evidence', + authority: 'Social Registry Office', + blurb: 'Issues SRO-signed household poverty values from its immutable extract.', + probeEnv: 'SOLMARA_SRO_EVIDENCE_URL', + probeDefault: 'https://sro-evidence.solmara.registrystack.org', + probePath: '/health', + configPaths: [{ label: 'Authority Evidence cell', path: 'evidence/cells/sro' }] + }), + evidence({ + id: 'mosd-programme-evidence', + label: 'MoSD Programme Evidence', + authority: 'Ministry of Social Development Programme MIS', + blurb: 'Issues programme-owned duplicate-enrolment values through the social development Relay.', + probeEnv: 'SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL', + probeDefault: 'https://mosd-programme-evidence.solmara.registrystack.org', + probePath: '/health', + configPaths: [{ label: 'Authority Evidence cell', path: 'evidence/cells/mosd-programme' }] + }), + evidence({ + id: 'sipf-evidence', + label: 'SIPF Evidence', + authority: 'Social Insurance and Pensions Fund', + blurb: 'Issues SIPF-signed pension and survivor values.', + probeEnv: 'SOLMARA_SIPF_EVIDENCE_URL', + probeDefault: 'https://sipf-evidence.solmara.registrystack.org', + probePath: '/health', + configPaths: [{ label: 'Authority Evidence cell', path: 'evidence/cells/sipf' }] + }), + evidence({ + id: 'nagdi-evidence', + label: 'NAgDI Evidence', + authority: 'National Agricultural Data Institute', + blurb: 'Issues NAgDI-signed voucher and livestock movement values.', + probeEnv: 'SOLMARA_NAGDI_EVIDENCE_URL', + probeDefault: 'https://nagdi-evidence.solmara.registrystack.org', + probePath: '/health', + configPaths: [{ label: 'Authority Evidence cell', path: 'evidence/cells/nagdi' }] + }), + shared({ + id: 'lab-mint', + label: 'Shared lab Mint', + blurb: 'Issues short-lived, audience-bound machine tokens for the six authority Evidence cells and five Relays.', probeEnv: 'MINT_URL', probeDefault: 'https://localhost:4341', probePath: '/health', - semantics: 'health', configPaths: [{ label: 'Mint config', path: 'evidence/mint.yaml' }] - } + }), + shared({ + id: 'child-benefit-programme', + label: 'Child benefit programme app', + purpose: 'child-benefit-review', + blurb: 'Collects separately signed authority Evidence and makes the programme decision. It does not own registry facts.', + probeEnv: 'CHILD_BENEFIT_FEDERATOR_URL', + probeDefault: 'http://127.0.0.1:4321', + probePath: '/health', + configPaths: [{ label: 'Programme application', path: 'scenario-runner/child_benefit_federator.py' }] + }), + shared({ + id: 'scenario-runner', + label: 'Scenario runner', + blurb: 'Runs the guided stories against the authority Evidence endpoints.', + probeEnv: 'SCENARIO_RUNNER_URL', + probeDefault: 'http://127.0.0.1:4302', + probePath: '/health', + configPaths: [{ label: 'Scenario modules', path: 'scenarios' }] + }), + shared({ + id: 'esignet', + label: 'Optional eSignet', + authority: 'National Identity Agency', + blurb: 'Optional resident sign-in. It resolves identity through the NIA Relay and is not an Evidence authority.', + configPaths: [{ label: 'Portal eSignet integration', path: 'portal/src/lib/server/esignet.ts' }] + }), + shared({ + id: 'portal', + label: 'Citizen portal', + blurb: 'The resident application that presents authority Evidence without exposing selectors or raw wire material.', + probeEnv: 'PORTAL_PROBE_URL', + probeDefault: 'http://127.0.0.1:4300', + probePath: '/', + browsable: true, + configPaths: [{ label: 'Portal app', path: 'portal' }] + }), + shared({ + id: 'home', + label: 'Solmara Lab Visitor Center', + blurb: 'The out-of-fiction front door to the synthetic country.', + self: true, + browsable: true, + configPaths: [{ label: 'Home app', path: 'home' }] + }) ]; export type StatusProbe = { @@ -229,55 +225,66 @@ export type StatusProbe = { href?: string; }; -/** - * Resolve the concrete probe URL and visitor-facing "Open" link for each - * service. `portalUrl` is the browser-facing portal origin used for the Open - * link, while the probe still targets the compose-internal portal URL. - */ -export function statusProbes(portalUrl: string, readEnv: Record = env): StatusProbe[] { +export function statusProbes( + portalUrl: string, + readEnv: Record = env +): StatusProbe[] { return SERVICES.map((service) => { const base = service.probeEnv ? readEnv[service.probeEnv] ?? service.probeDefault : undefined; const probeUrl = base && service.probePath ? joinUrl(base, service.probePath) : undefined; let href: string | undefined; if (service.browsable) { - if (service.id === 'metadata') href = '/.well-known/api-catalog'; + if (service.id === 'deterministic-publisher') href = '/.well-known/api-catalog'; else if (service.id === 'portal') href = portalUrl; else if (service.id === 'home') href = '/'; } - return { id: service.id, label: service.label, role: service.role, self: Boolean(service.self), probeUrl, href }; + return { + id: service.id, + label: service.label, + role: service.role, + self: Boolean(service.self), + probeUrl, + href + }; }); } -/** - * Group the topology for the anatomy page: authority Relays, the Evidence and - * Mint trust boundary, and shared application services. - * Config paths become repo links, with the - * in-repo relative path preserved as visible text. - */ export function topologyGroups(repoUrl: string): TopologyGroup[] { - const shared = SERVICES.filter((service) => service.role === 'shared'); - const relays = SERVICES.filter((service) => service.role === 'relay'); - const evidence = SERVICES.filter((service) => service.role === 'evidence'); + const byId = (ids: string[]) => ids.map((id) => { + const service = SERVICES.find((candidate) => candidate.id === id); + if (!service) throw new Error(`Unknown topology service: ${id}`); + return toTopologyService(service, repoUrl); + }); return [ + { + key: 'publisher', + title: 'Deterministic publisher', + blurb: 'One reviewed synthetic input produces public metadata, authority-owned immutable extracts, and Relay SQLite sources deterministically.', + services: byId(['deterministic-publisher']) + }, { key: 'relays', - title: 'One Relay per authority', - blurb: - 'Each authority runs its own Relay over data it already holds. Nothing is copied into a central store, so a compromise or outage is contained to a single authority and each audit chain stays independent.', - services: relays.map((service) => toTopologyService(service, repoUrl)) + title: 'Five Relay V2 projects', + blurb: 'Each live source is exposed through bounded, purpose-protected exact lookups. Direct-only SRO evidence has no Relay.', + services: byId(['cra-relay', 'nia-relay', 'mosd-relay', 'sipf-relay', 'nagdi-relay']) }, { key: 'evidence', - title: 'Evidence and requester identity', - blurb: - 'Evidence evaluates reviewed requirements against protected Records APIs and signs the minimized values. Mint supplies short-lived requester identity and grant tags. Neither service owns an application decision.', - services: evidence.map((service) => toTopologyService(service, repoUrl)) + title: 'Six authority Evidence cells', + blurb: 'Each authority has its own Evidence service identity, issuer, reviewed requirements, and origin. The lab Mint is shared infrastructure, never the evidence issuer.', + services: byId(['cra-evidence', 'nia-evidence', 'sro-evidence', 'mosd-programme-evidence', 'sipf-evidence', 'nagdi-evidence', 'lab-mint']) + }, + { + key: 'programme', + title: 'Programme and visitor applications', + blurb: 'The child benefit app combines signed authority values into a programme decision. The scenario runner, portal, and Visitor Center present the result.', + services: byId(['child-benefit-programme', 'scenario-runner', 'portal', 'home']) }, { - key: 'shared', - title: 'Shared services', - blurb: 'The pieces every authority leans on: metadata publishing, scenario execution, and the two front doors.', - services: shared.map((service) => toTopologyService(service, repoUrl)) + key: 'identity', + title: 'Optional resident identity', + blurb: 'eSignet is optional and uses the NIA Relay for identity resolution. It is not a national Evidence service.', + services: byId(['esignet']) } ]; } diff --git a/home/src/lib/server/tokens.test.ts b/home/src/lib/server/tokens.test.ts index a4ff4f0..4ecc12b 100644 --- a/home/src/lib/server/tokens.test.ts +++ b/home/src/lib/server/tokens.test.ts @@ -1,53 +1,32 @@ import { describe, expect, it } from 'vitest'; -import { buildCurlExamples, parsePublishedTokens, publishRequestTokens } from './tokens'; +import { buildCurlExamples } from './tokens'; -describe('published local application token allowlist', () => { - it('renders only tokens explicitly named in the allowlist JSON', () => { - const tokens = parsePublishedTokens('{"child-benefit-federator":"tok-child","unknown":"tok-unknown"}'); - expect(tokens.map((token) => token.token)).toEqual(['tok-child', 'tok-unknown']); - expect(tokens[0].purpose).toBe('child-benefit-review'); - }); - - it('fails closed for absent or malformed allowlists', () => { - expect(parsePublishedTokens(undefined)).toEqual([]); - expect(parsePublishedTokens('not json')).toEqual([]); - expect(parsePublishedTokens('[]')).toEqual([]); - }); +describe('engineer curl examples', () => { + it('uses distinct authority endpoints and JSON-body purposes', () => { + const examples = buildCurlExamples(); + const commands = examples.map((example) => example.command).join('\n'); - it('builds metadata and child collector curls without exposing Mint credentials', () => { - const examples = buildCurlExamples(parsePublishedTokens('{"child-benefit-federator":"tok-child"}')); expect(examples.map((example) => example.id)).toEqual([ 'metadata-get', - 'claims-get', - 'evaluate-post', + 'cra-evidence-post', + 'nia-evidence-post', + 'programme-post', 'wrong-purpose-post' ]); - expect(examples.find((example) => example.id === 'evaluate-post')?.command).toContain('tok-child'); - expect(examples.find((example) => example.id === 'wrong-purpose-post')?.command).toContain('pension-payment-review'); - expect(JSON.stringify(examples)).not.toContain('client-private.jwk'); + expect(commands).toContain('cra-evidence.solmara.registrystack.org/v1/evidence'); + expect(commands).toContain('nia-evidence.solmara.registrystack.org/v1/evidence'); + expect(commands).toContain('"purpose":"https://id.registrystack.org/solmara/purpose/child-benefit-review"'); + expect(commands).toContain('"requirement":"https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1"'); + expect(commands).not.toContain('/purpose/child-benefit-review/v1'); + expect(commands).not.toContain('Data-Purpose'); }); - it('republishes only the exact child collector token into matching request sources', () => { - const tokens = parsePublishedTokens('{"child-benefit-federator":"tok-child"}'); - const result = publishRequestTokens( - { - request_source: { - method: 'POST', - url: 'http://localhost:4321/v1/evaluations', - headers: { 'x-api-key': '[runtime token hidden]', 'Data-Purpose': 'child-benefit-review' } - }, - request_sources: [ - { - method: 'POST', - url: 'https://localhost:4341/v1/evidence', - headers: { Authorization: 'Bearer [runtime token hidden]' }, - body: { purpose: 'child-benefit-review' } - } - ] - }, - tokens - ); - expect(result.request_source.headers['x-api-key']).toBe('tok-child'); - expect(result.request_sources[0].headers.Authorization).toBe('Bearer [runtime token hidden]'); + it('publishes placeholders, never token or subject values', () => { + const commands = buildCurlExamples().map((example) => example.command).join('\n'); + expect(commands).toContain('$CRA_EVIDENCE_ACCESS_TOKEN'); + expect(commands).toContain('$SOLMARA_UIN'); + expect(commands).toContain('$REQUEST_NONCE'); + expect(commands).not.toMatch(/Bearer\s+(?!\$)[A-Za-z0-9._-]+/); + expect(commands).not.toMatch(/\bFR-\d+\b|\b[2-9]\d{9}\b/); }); }); diff --git a/home/src/lib/server/tokens.ts b/home/src/lib/server/tokens.ts index 15f9af4..d8bb1c2 100644 --- a/home/src/lib/server/tokens.ts +++ b/home/src/lib/server/tokens.ts @@ -1,190 +1,98 @@ -import { env } from '$env/dynamic/private'; import { buildPublicUrlMap, mapPublicUrl } from './urlmap'; -import type { CurlExample, PublishedToken } from '$lib/types'; +import type { CurlExample } from '$lib/types'; -const CHILD_PURPOSE = 'child-benefit-review'; -const PENSION_PURPOSE = 'pension-payment-review'; - -const TOKEN_BINDINGS: Record = { - 'child-benefit-federator': { - urlHints: ['child-benefit-federator', 'localhost:4321', '127.0.0.1:4321'], - purposes: [CHILD_PURPOSE] - } -}; - -/** - * Notes and purposes for the names the lab deliberately publishes. Only names in - * `HOME_PUBLISHED_TOKENS` are ever surfaced; this map only supplies copy. - */ -const TOKEN_NOTES: Record = { - 'child-benefit-federator': { - purpose: CHILD_PURPOSE, - note: 'Scoped to the child benefit federator. Use it to collect source-owned predicates under child-benefit-review.' - } -}; - -/** - * Parse the server-side published-token allowlist. This is the ONLY source of - * tokens the page ever renders: a token that is not a value in - * `HOME_PUBLISHED_TOKENS` can never reach page data, no matter what other token - * environment variables the container holds. A malformed allowlist yields an - * empty list rather than falling back to any other env. - */ -export function parsePublishedTokens(json: string | undefined = env.HOME_PUBLISHED_TOKENS): PublishedToken[] { - if (!json) return []; - let parsed: unknown; - try { - parsed = JSON.parse(json); - } catch { - return []; - } - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return []; - const tokens: PublishedToken[] = []; - for (const [name, value] of Object.entries(parsed as Record)) { - if (typeof value !== 'string' || value.length === 0) continue; - const meta = TOKEN_NOTES[name]; - tokens.push({ - name, - token: value, - purpose: meta?.purpose, - note: meta?.note ?? 'Synthetic lab token, safe to publish. Scoped to one local application.' - }); - } - return tokens; -} +const CHILD_PURPOSE = 'https://id.registrystack.org/solmara/purpose/child-benefit-review'; +const PENSION_PURPOSE = 'https://id.registrystack.org/solmara/purpose/pension-payment-review'; /** - * Story run results arrive from the scenario runner with runtime auth redacted. - * The visitor center may republish only the synthetic lab tokens listed in - * HOME_PUBLISHED_TOKENS so story-level copy-as-curl snippets are reproducible. + * Build copyable examples without publishing a credential, identifier, selector + * value, or nonce. Runtime credentials stay server-side; shell placeholders make + * the trust boundary visible to engineers. */ -export function publishRequestTokens>(result: T, tokens: PublishedToken[]): T { - const published = new Map(tokens.map((token) => [token.name, token.token])); - const clone: Record = { ...result }; - for (const key of ['request_source', 'credential_source']) { - clone[key] = publishRequestSourceToken(clone[key], published); - } - if (Array.isArray(clone.request_sources)) { - clone.request_sources = clone.request_sources.map((source) => publishRequestSourceToken(source, published)); - } - if (Array.isArray(clone.source_trace)) { - clone.source_trace = clone.source_trace.map((item) => { - if (!isObject(item)) return item; - return { ...item, request_source: publishRequestSourceToken(item.request_source, published) }; - }); - } - return clone as T; -} - -function publishRequestSourceToken(source: unknown, published: Map): unknown { - if (!isObject(source)) return source; - const headers = isObject(source.headers) ? stringHeaders(source.headers) : {}; - const purpose = Object.entries(headers).find(([name]) => name.toLowerCase() === 'data-purpose')?.[1]; - const token = tokenCandidates(source.url, purpose) - .map((name) => published.get(name)) - .find((value): value is string => typeof value === 'string'); - if (!token) return source; - - const apiKeyHeader = Object.keys(headers).find((key) => key.toLowerCase() === 'x-api-key'); - if (!apiKeyHeader) return source; - return { - ...source, - headers: { - ...headers, - [apiKeyHeader]: token - } - }; -} - -function tokenCandidates(url: unknown, purpose: string | undefined): string[] { - const urlNames = tokenNamesForUrl(url); - const purposeNames = tokenNamesForPurpose(purpose); - const exactNames = urlNames.filter((name) => purposeNames.includes(name)); - return exactNames.length === 1 ? exactNames : []; -} - -function tokenNamesForUrl(value: unknown): string[] { - if (typeof value !== 'string') return []; - let parsed: URL; - try { - parsed = new URL(value); - } catch { - return []; - } - const urlText = `${parsed.host}${parsed.pathname}`.toLowerCase(); - return Object.entries(TOKEN_BINDINGS) - .filter(([, binding]) => binding.urlHints.some((hint) => urlText.includes(hint))) - .map(([name]) => name); -} - -function tokenNamesForPurpose(value: string | undefined): string[] { - if (!value) return []; - return Object.entries(TOKEN_BINDINGS) - .filter(([, binding]) => binding.purposes.includes(value)) - .map(([name]) => name); -} - -function stringHeaders(headers: Record): Record { - return Object.fromEntries( - Object.entries(headers) - .filter((entry): entry is [string, string] => typeof entry[1] === 'string') - ); -} - -function isObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null; -} - -/** - * Build the four copy-as-curl examples for the engineer door: an unauthenticated - * metadata read, a claim-catalogue read with a published token, an evaluation - * POST under a permitted purpose, and the skeptic's wrong-purpose POST that gets - * a clean 403. URLs are rewritten to host-reachable origins through the same map - * the rest of the site uses. The child benefit token is only inlined when it is - * actually in the allowlist; otherwise the example references the env var name. - */ -export function buildCurlExamples(tokens: PublishedToken[]): CurlExample[] { +export function buildCurlExamples(): CurlExample[] { const map = buildPublicUrlMap(); - const metadataUrl = mapPublicUrl('http://static-metadata:8080/metadata/catalog.json', map); - const claimsUrl = mapPublicUrl('http://child-benefit-federator:8080/v1/claims', map); - const evalUrl = mapPublicUrl('http://child-benefit-federator:8080/v1/evaluations', map); - const published = tokens.find((token) => token.name === 'child-benefit-federator') ?? tokens.find((token) => token.name.includes('child')); - const tokenValue = published?.token ?? '$CHILD_BENEFIT_FEDERATOR_TOKEN'; - const body = - '{"target":{"type":"Person","identifiers":[{"scheme":"solmara_uin","value":"2300010248"}]},' + - '"claims":["birth-is-registered"],"disclosure":"predicate",' + - '"format":"application/json"}'; + const metadataUrl = mapPublicUrl('http://deterministic-publisher:8080/metadata/catalog.json', map); + const craUrl = mapPublicUrl('https://cra-evidence.solmara.registrystack.org/v1/evidence', map); + const niaUrl = mapPublicUrl('https://nia-evidence.solmara.registrystack.org/v1/evidence', map); + const programmeUrl = mapPublicUrl('http://child-benefit-federator:8080/v1/evaluations', map); + const craBody = evidenceBody( + 'https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1', + CHILD_PURPOSE + ); + const niaBody = evidenceBody( + 'https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1', + CHILD_PURPOSE + ); return [ { id: 'metadata-get', - title: 'Read the published metadata (no auth)', - note: 'The metadata bundle is public. No token, no purpose header.', + title: 'Read the deterministic publication', + note: 'Published metadata is public. The deterministic publisher is not an Evidence authority.', command: `curl -sS '${metadataUrl}'` }, { - id: 'claims-get', - title: 'List the child benefit evidence catalogue (published token)', - note: 'The token is a synthetic lab credential scoped to the child benefit federator.', - command: `curl -sS '${claimsUrl}' \\\n -H 'x-api-key: ${tokenValue}' \\\n -H 'Data-Purpose: ${CHILD_PURPOSE}'` + id: 'cra-evidence-post', + title: 'Ask CRA Evidence for one minimized value', + note: 'Purpose is a JSON member. Supply the scoped token, nonce, and UIN from your local shell.', + command: evidenceCurl(craUrl, '$CRA_EVIDENCE_ACCESS_TOKEN', craBody) }, { - id: 'evaluate-post', - title: 'Collect source-owned predicates under a permitted purpose', - note: 'The application collects purpose-limited predicates and a source trace, never source rows.', + id: 'nia-evidence-post', + title: 'Ask NIA Evidence independently', + note: 'CRA and NIA are distinct Evidence services with distinct issuers and audiences.', + command: evidenceCurl(niaUrl, '$NIA_EVIDENCE_ACCESS_TOKEN', niaBody) + }, + { + id: 'programme-post', + title: 'Run the child-benefit programme collection', + note: 'The programme sends its governed purpose in the JSON body and collects separately signed authority results.', command: - `curl -sS -X POST '${evalUrl}' \\\n -H 'x-api-key: ${tokenValue}' \\\n` + - ` -H 'Data-Purpose: ${CHILD_PURPOSE}' \\\n -H 'Accept: application/json' \\\n` + - ` -H 'Content-Type: application/json' \\\n -d '${body}'` + `curl -sS -X POST '${programmeUrl}' \\\n` + + ` -H 'x-api-key: $CHILD_BENEFIT_PROGRAMME_TOKEN' \\\n` + + ` -H 'Content-Type: application/json' \\\n` + + ` -d '${programmeBody(CHILD_PURPOSE)}'` }, { id: 'wrong-purpose-post', - title: 'Skeptic path: the same request under a wrong purpose', - note: 'Ask under a purpose this evidence collector does not permit and get a clean 403 with a stable problem code.', - command: - `curl -sS -X POST '${evalUrl}' \\\n -H 'x-api-key: ${tokenValue}' \\\n` + - ` -H 'Data-Purpose: ${PENSION_PURPOSE}' \\\n -H 'Accept: application/json' \\\n` + - ` -H 'Content-Type: application/json' \\\n -d '${body}'` + title: 'Skeptic path: an unapproved JSON purpose', + note: 'The same requirement under a pension purpose is refused with the current not_authorized problem code.', + command: evidenceCurl(craUrl, '$CRA_EVIDENCE_ACCESS_TOKEN', evidenceBody( + 'https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1', + PENSION_PURPOSE + )) } ]; } + +function evidenceBody(requirement: string, purpose: string): string { + return JSON.stringify({ + requestNonce: '$REQUEST_NONCE', + requirement, + purpose, + subjects: [{ role: 'subject', selector: { profile: 'solmara-uin-v1', values: { uin: '$SOLMARA_UIN' } } }] + }); +} + +function programmeBody(purpose: string): string { + return JSON.stringify({ + purpose, + target: { + type: 'Person', + identifiers: [{ scheme: 'solmara_uin', value: '$SOLMARA_UIN' }] + }, + claims: [ + 'birth-is-registered', + 'population-record-active', + 'child-age-under-5', + 'household-below-poverty-threshold', + 'not-already-enrolled' + ], + disclosure: 'predicate', + format: 'application/json' + }); +} + +function evidenceCurl(url: string, token: string, body: string): string { + return `curl -sS -X POST '${url}' \\\n -H 'Authorization: Bearer ${token}' \\\n -H 'Content-Type: application/json' \\\n -d '${body}'`; +} diff --git a/home/src/lib/server/urlmap.test.ts b/home/src/lib/server/urlmap.test.ts index f24de8d..7a23124 100644 --- a/home/src/lib/server/urlmap.test.ts +++ b/home/src/lib/server/urlmap.test.ts @@ -2,54 +2,35 @@ import { describe, expect, it } from 'vitest'; import { buildPublicUrlMap, mapPublicUrl, rewriteRequestUrls } from './urlmap'; describe('public URL map', () => { - it('rewrites internal Evidence and Mint origins to the local TLS gateway', () => { + it('keeps each public authority Evidence origin distinct', () => { const map = buildPublicUrlMap(); - expect(mapPublicUrl('https://evidence.solmara.invalid/v1/evidence', map)).toBe( - 'https://localhost:4341/v1/evidence' - ); - expect(mapPublicUrl('https://mint.evidence.solmara.invalid/token', map)).toBe( - 'https://localhost:4341/token' - ); + expect(mapPublicUrl('https://cra-evidence.solmara.registrystack.org/v1/evidence', map)).toContain('cra-evidence'); + expect(mapPublicUrl('https://nia-evidence.solmara.registrystack.org/v1/evidence', map)).toContain('nia-evidence'); }); - it('rewrites Relay and application hostnames from the same table', () => { + it('rewrites the five Relay V2 and programme hostnames', () => { const map = buildPublicUrlMap(); - expect(mapPublicUrl('http://cra-civil-relay:8080/', map)).toBe('http://localhost:4311/'); - expect(mapPublicUrl('http://child-benefit-federator:8080/v1/evaluations', map)).toBe( - 'http://localhost:4321/v1/evaluations' - ); + expect(mapPublicUrl('http://cra-relay:8080/ready', map)).toBe('http://localhost:4311/ready'); + expect(mapPublicUrl('http://sipf-relay:8080/ready', map)).toBe('http://localhost:4315/ready'); + expect(mapPublicUrl('http://child-benefit-federator:8080/v1/evaluations', map)).toBe('http://localhost:4321/v1/evaluations'); }); - it('preserves path and query while swapping an Evidence origin', () => { - const map = buildPublicUrlMap(); - expect(mapPublicUrl('https://evidence.solmara.invalid/v1/evidence?trace=1', map)).toBe( - 'https://localhost:4341/v1/evidence?trace=1' - ); - }); - - it('leaves already host-reachable and non-URL values untouched', () => { - const map = buildPublicUrlMap(); - expect(mapPublicUrl('http://localhost:4321/v1/claims', map)).toBe('http://localhost:4321/v1/claims'); - expect(mapPublicUrl('not a url', map)).toBe('not a url'); - }); - - it('merges an environment override over the defaults', () => { - const map = buildPublicUrlMap(JSON.stringify({ 'evidence.solmara.invalid': 'https://evidence.example' })); - expect(mapPublicUrl('https://evidence.solmara.invalid/v1/evidence', map)).toBe('https://evidence.example/v1/evidence'); - expect(mapPublicUrl('http://sipf-pensions-relay:8080/ready', map)).toBe('http://localhost:4315/ready'); + it('merges an environment override and preserves path and query', () => { + const map = buildPublicUrlMap(JSON.stringify({ 'cra-evidence.solmara.registrystack.org': 'https://cra.example' })); + expect(mapPublicUrl('https://cra-evidence.solmara.registrystack.org/v1/evidence?trace=1', map)).toBe('https://cra.example/v1/evidence?trace=1'); }); - it('rewrites nested request sources without mutating the input', () => { + it('rewrites nested sources without mutating the input', () => { const map = buildPublicUrlMap(); const result = { - request_source: { method: 'POST', url: 'https://evidence.solmara.invalid/v1/evidence', headers: {} }, - request_sources: [{ method: 'POST', url: 'https://evidence.solmara.invalid/v1/evidence', headers: {} }], - source_trace: [{ request_source: { method: 'GET', url: 'http://cra-civil-relay:8080/records/civil_people' } }] + request_source: { method: 'POST', url: 'http://cra-relay:8080/v2/resources/civil/lookup' }, + request_sources: [{ method: 'POST', url: 'http://nia-relay:8080/v2/resources/population/lookup' }], + source_trace: [{ request_source: { method: 'POST', url: 'http://sipf-relay:8080/v2/resources/pension/lookup' } }] }; const mapped = rewriteRequestUrls(result, map); - expect(mapped.request_source.url).toBe('https://localhost:4341/v1/evidence'); - expect(mapped.request_sources[0].url).toBe('https://localhost:4341/v1/evidence'); - expect(mapped.source_trace[0].request_source?.url).toBe('http://localhost:4311/records/civil_people'); - expect(result.request_source.url).toBe('https://evidence.solmara.invalid/v1/evidence'); + expect(mapped.request_source.url).toContain('localhost:4311'); + expect(mapped.request_sources[0].url).toContain('localhost:4312'); + expect(mapped.source_trace[0].request_source?.url).toContain('localhost:4315'); + expect(result.request_source.url).toContain('cra-relay:8080'); }); }); diff --git a/home/src/lib/server/urlmap.ts b/home/src/lib/server/urlmap.ts index 6a3aee1..eb6e750 100644 --- a/home/src/lib/server/urlmap.ts +++ b/home/src/lib/server/urlmap.ts @@ -15,16 +15,19 @@ import { env } from '$env/dynamic/private'; */ const DEFAULT_LOCAL_MAP: Record = { 'child-benefit-federator:8080': 'http://localhost:4321', - 'evidence.solmara.invalid': 'https://localhost:4341', - 'mint.evidence.solmara.invalid': 'https://localhost:4341', - 'evidence:8080': 'https://localhost:4341', + 'deterministic-publisher:8080': 'http://localhost:4331', + 'cra-evidence.solmara.registrystack.org': 'https://cra-evidence.solmara.registrystack.org', + 'nia-evidence.solmara.registrystack.org': 'https://nia-evidence.solmara.registrystack.org', + 'sro-evidence.solmara.registrystack.org': 'https://sro-evidence.solmara.registrystack.org', + 'mosd-programme-evidence.solmara.registrystack.org': 'https://mosd-programme-evidence.solmara.registrystack.org', + 'sipf-evidence.solmara.registrystack.org': 'https://sipf-evidence.solmara.registrystack.org', + 'nagdi-evidence.solmara.registrystack.org': 'https://nagdi-evidence.solmara.registrystack.org', 'mint:8081': 'https://localhost:4341', - 'cra-civil-relay:8080': 'http://localhost:4311', - 'nia-population-relay:8080': 'http://localhost:4312', - 'sro-social-relay:8080': 'http://localhost:4313', - 'programme-mis-relay:8080': 'http://localhost:4314', - 'sipf-pensions-relay:8080': 'http://localhost:4315', - 'nagdi-agriculture-relay:8080': 'http://localhost:4316', + 'cra-relay:8080': 'http://localhost:4311', + 'nia-relay:8080': 'http://localhost:4312', + 'mosd-relay:8080': 'http://localhost:4314', + 'sipf-relay:8080': 'http://localhost:4315', + 'nagdi-relay:8080': 'http://localhost:4316', 'static-metadata:8080': 'http://localhost:4331', 'scenario-runner:8080': 'http://localhost:4302', 'portal:4000': 'http://localhost:4300' diff --git a/home/src/lib/types.ts b/home/src/lib/types.ts index eefc413..f4a6839 100644 --- a/home/src/lib/types.ts +++ b/home/src/lib/types.ts @@ -76,13 +76,6 @@ export type SeedSummary = { observedAt?: string; }; -export type PublishedToken = { - name: string; - token: string; - purpose?: string; - note: string; -}; - export type CurlExample = { id: string; title: string; @@ -195,6 +188,20 @@ export type StepRunResult = { credential_source?: RequestSource; credential_response_source?: ResponseSource; credential?: CredentialSummary; + results?: Array>; + result_state?: string; + presentation?: EvidencePresentation; + presentations?: EvidencePresentation[]; +}; + +export type EvidencePresentation = { + authority: string; + service_id?: string; + issuer: string; + provider?: string; + source: 'immutable extract' | 'Relay lookup'; + status?: number; + claims?: string[]; }; export type StepRunEnvelope = { @@ -205,6 +212,13 @@ export type StepRunEnvelope = { }; export type MetadataAuthority = { id: string; name: string; iri?: string }; +export type MetadataDataService = { + id: string; + iri?: string; + endpoint_url?: string; + endpoint_description?: string; + title?: Record; +}; export type MetadataPublicService = { id: string; iri?: string; @@ -222,6 +236,7 @@ export type MetadataBundle = { datasets: MetadataDataset[]; gray_registries: GrayRegistry[]; authorities: MetadataAuthority[]; + data_services: MetadataDataService[]; public_services: MetadataPublicService[]; }; offerings: MetadataOffering[]; @@ -249,7 +264,7 @@ export type MetadataOffering = { public_services?: string[]; lookup_keys?: string[]; issuing_authority?: { id?: string; name?: string; iri?: string }; - access?: { endpoint_url?: string; discovery_url?: string; kind?: string; ruleset?: string }; + access?: { endpoint_url?: string; discovery_url?: string; kind?: string; ruleset?: string; source_type?: string }; semantics?: { concepts?: string[]; application_profiles?: string[] }; policy?: string; }; @@ -275,7 +290,6 @@ export type HomeData = { versions: Record; smoke: SmokeEvidence; seed: SeedSummary; - publishedTokens: PublishedToken[]; curlExamples: CurlExample[]; changelogLatest: ChangelogEntry | null; repoUrl: string; diff --git a/home/src/routes/+page.svelte b/home/src/routes/+page.svelte index a77cb54..d680e74 100644 --- a/home/src/routes/+page.svelte +++ b/home/src/routes/+page.svelte @@ -39,7 +39,7 @@ - +
diff --git a/home/src/routes/anatomy/+page.svelte b/home/src/routes/anatomy/+page.svelte index 640308b..48239b5 100644 --- a/home/src/routes/anatomy/+page.svelte +++ b/home/src/routes/anatomy/+page.svelte @@ -14,14 +14,14 @@ Anatomy · Solmara Lab - +
@@ -39,7 +39,7 @@

Sovereignty invariant

One authority, one source-owned boundary.

- No authority hands its rows to a central store. Each keeps an independent audit chain, so + No authority hands its rows to a central store. Each keeps an independent accountability boundary, so a fault or compromise stays contained rather than becoming a country-wide data exposure.

diff --git a/home/src/routes/api/scenarios/[scenario]/steps/[step]/run/+server.ts b/home/src/routes/api/scenarios/[scenario]/steps/[step]/run/+server.ts index 811a949..6166e8d 100644 --- a/home/src/routes/api/scenarios/[scenario]/steps/[step]/run/+server.ts +++ b/home/src/routes/api/scenarios/[scenario]/steps/[step]/run/+server.ts @@ -1,18 +1,15 @@ import { json, error } from '@sveltejs/kit'; -import { readPurposes } from '$lib/server/data'; import { joinedUrl, runtime } from '$lib/server/runtime'; -import { buildPublicUrlMap, rewriteRequestUrls } from '$lib/server/urlmap'; -import { parsePublishedTokens, publishRequestTokens } from '$lib/server/tokens'; +import { buildPublicUrlMap, mapPublicUrl } from '$lib/server/urlmap'; import type { RequestHandler } from './$types'; -export const POST: RequestHandler = async ({ fetch, params, request }) => { - const payload = await runnerPayload(request); +export const POST: RequestHandler = async ({ fetch, params }) => { const response = await fetch( joinedUrl(runtime.scenarioRunnerUrl, `/v1/scenarios/${params.scenario}/steps/${params.step}/run`), { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) + body: '{}' } ); if (!response.ok) { @@ -20,28 +17,115 @@ export const POST: RequestHandler = async ({ fetch, params, request }) => { } const body = (await response.json()) as { result?: Record }; if (body && typeof body.result === 'object' && body.result) { - body.result = publishRequestTokens(rewriteRequestUrls(body.result, buildPublicUrlMap()), parsePublishedTokens()); + body.result = safeResult(body.result); } return json(body); }; -async function runnerPayload(request: Request): Promise> { - let body: unknown = {}; - try { - body = await request.json(); - } catch { - return {}; - } - if (!isObject(body) || typeof body.purpose !== 'string' || !body.purpose) { - return {}; - } - const allowedPurposes = await readPurposes(); - if (!allowedPurposes.some((purpose) => purpose.iri === body.purpose)) { - throw error(400, 'unsupported purpose'); - } - return { config: { purpose_override: body.purpose } }; +function safeResult(result: Record): Record { + const map = buildPublicUrlMap(); + const sourceTrace = Array.isArray(result.source_trace) + ? result.source_trace.map(safePresentation).filter((item): item is Record => item !== null) + : []; + return { + step_id: stringValue(result.step_id), + friendly: safeFriendly(result.friendly), + request_source: safeRequest(result.request_source, map), + request_sources: Array.isArray(result.request_sources) + ? result.request_sources.map((source) => safeRequest(source, map)) + : undefined, + response_source: safeResponse(result.response_source), + source_trace: sourceTrace, + results: safeResults(result.results), + result_state: stringValue(result.result_state), + presentation: safePresentation(result.presentation), + presentations: Array.isArray(result.presentations) + ? result.presentations.map(safePresentation).filter((item): item is Record => item !== null) + : undefined, + credential: safeCredential(result.credential) + }; +} + +function safeRequest(value: unknown, map: Record): Record { + if (!isObject(value)) return {}; + const url = stringValue(value.url); + return { + method: stringValue(value.method), + url: url ? mapPublicUrl(url, map) : undefined, + purpose: stringValue(value.purpose), + requests: Array.isArray(value.requests) ? value.requests.map((item) => safeRequest(item, map)) : undefined + }; +} + +function safeResponse(value: unknown): Record { + if (!isObject(value)) return {}; + return { + status: typeof value.status === 'number' ? value.status : null, + code: stringValue(value.code), + type: stringValue(value.type) + }; +} + +function safePresentation(value: unknown): Record | null { + if (!isObject(value)) return null; + const source = value.source === 'immutable extract' || value.source === 'Relay lookup' ? value.source : undefined; + if (!stringValue(value.authority) || !stringValue(value.issuer) || !source) return null; + return { + authority: stringValue(value.authority), + service_id: stringValue(value.service_id), + issuer: stringValue(value.issuer), + provider: stringValue(value.provider), + source, + status: typeof value.status === 'number' ? value.status : undefined, + claims: Array.isArray(value.claims) ? value.claims.filter((item): item is string => typeof item === 'string') : undefined + }; +} + +function safeResults(value: unknown): Record[] | undefined { + if (!Array.isArray(value)) return undefined; + return value.filter(isObject).map((item) => ({ + claim_id: stringValue(item.claim_id), + concept_id: stringValue(item.concept_id), + satisfied: typeof item.satisfied === 'boolean' ? item.satisfied : null, + value: typeof item.value === 'boolean' ? item.value : undefined + })); +} + +function safeFriendly(value: unknown): Record { + if (!isObject(value)) return { title: 'Result unavailable.', message: '', status: 'needs_attention', facts: [] }; + return { + title: stringValue(value.title), + message: stringValue(value.message), + status: stringValue(value.status), + facts: Array.isArray(value.facts) + ? value.facts.filter(isObject).map((fact) => ({ label: stringValue(fact.label), value: safeFact(fact.value) })) + : [] + }; +} + +function safeCredential(value: unknown): Record | undefined { + if (!isObject(value)) return undefined; + return { + status: stringValue(value.status), + profile: stringValue(value.profile), + format: stringValue(value.format), + vct: stringValue(value.vct), + issuer: stringValue(value.issuer), + disclosures: typeof value.disclosures === 'number' ? value.disclosures : undefined, + reason: stringValue(value.reason), + http_status: typeof value.http_status === 'number' ? value.http_status : null, + message: stringValue(value.message) + }; +} + +function safeFact(value: unknown): string | number | boolean | null { + return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? value : null; +} + +function stringValue(value: unknown): string | undefined { + return typeof value === 'string' ? value : undefined; } -function isObject(value: unknown): value is { purpose?: unknown } { - return typeof value === 'object' && value !== null; +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); } diff --git a/home/src/routes/developers/+page.svelte b/home/src/routes/developers/+page.svelte index e2986e9..425245a 100644 --- a/home/src/routes/developers/+page.svelte +++ b/home/src/routes/developers/+page.svelte @@ -20,7 +20,7 @@

Run the country, inspect every boundary

This is the complete technical surface behind the guided homepage: a clean-checkout journey, - published metadata, source configuration, synthetic demo credentials, and runnable requests. + published metadata, source configuration, credential boundaries, and runnable requests.

total + dataset.entities.length, 0); + $: sourceTypes = new Set(metadata.offerings.map((offering) => offering.access?.source_type).filter(Boolean)).size; $: facts = metadata.available ? [ { value: apiItems.length, label: 'catalog links' }, - { value: entityCount, label: 'registry entities' }, + { value: sourceTypes, label: 'source patterns' }, { value: metadata.offerings.length, label: 'evidence offerings' }, { value: policies.length, label: 'published policies' } ] @@ -55,14 +55,14 @@ Explorer · Solmara Lab - +
@@ -76,7 +76,6 @@
-
-

Datasets (DCAT)

-

Each authority's dataset and the entities it exposes, with the purposes that may read them.

-
- {#each metadata.catalog.datasets as dataset} -
-
-
-

{dataset.title}

-

{dataset.description}

- {#if dataset.authority}

{dataset.authority.name}

{/if} -
- -
- {#each dataset.entities as entity} -
- {entity.title} - {#if entity.semantics?.concepts?.length} -

Semantics: {entity.semantics.concepts.join(', ')}

- {/if} -
- {#each entity.purposes as purpose} - {slug(purpose)} - {/each} -
-
- {/each} - Raw DCAT artifact -
- {/each} -
-
-

Public services (CPSV-AP)

The public services the evidence backs. Each names its competent authority and the evidence APIs it consumes.

diff --git a/home/src/routes/stories/[id]/+page.svelte b/home/src/routes/stories/[id]/+page.svelte index b777637..2d0a189 100644 --- a/home/src/routes/stories/[id]/+page.svelte +++ b/home/src/routes/stories/[id]/+page.svelte @@ -91,11 +91,18 @@ function orchestrationField(result: StepRunResult, field: 'service_id' | 'decision'): string { const body = result.response_source.body; - if (!body || typeof body !== 'object') return 'Not reported'; + if (!body || typeof body !== 'object') return fallbackOrchestrationField(result, field); const orchestration = (body as { orchestration?: unknown }).orchestration; - if (!orchestration || typeof orchestration !== 'object') return 'Not reported'; + if (!orchestration || typeof orchestration !== 'object') return fallbackOrchestrationField(result, field); const value = (orchestration as Record)[field]; - return typeof value === 'string' && value ? value : 'Not reported'; + return typeof value === 'string' && value ? value : fallbackOrchestrationField(result, field); + } + + function fallbackOrchestrationField(result: StepRunResult, field: 'service_id' | 'decision'): string { + if (result.request_source?.url?.includes('/v1/evaluations') && result.source_trace?.length) { + return field === 'service_id' ? 'child-benefit-federator' : 'not_composed'; + } + return 'Not reported'; } function isCollectedApplicationEvidence(result: StepRunResult | null): result is StepRunResult { @@ -195,11 +202,11 @@

{requestSources.length === 1 - ? 'Request (published lab token)' - : `Requests (${requestSources.length} authority calls, published lab tokens)`} + ? 'Sanitized request skeleton' + : `Sanitized requests (${requestSources.length} authority calls)`}

{#if requestSources.length === 1} - + {/if}
{#if requestSources.length > 1} @@ -208,7 +215,7 @@
Authority request {requestIndex + 1} of {requestSources.length}
- +
{sourceBlock(source)}
@@ -325,7 +332,7 @@

Accountability

-

{applicationEvidence ? 'What the source trace recorded about this access' : 'What Registry Evidence recorded about this access'}

+

{applicationEvidence ? 'What the source trace recorded about this access' : 'What the authority Evidence service recorded about this access'}

{#if accountability} {@const first = claimResults(accountability)[0]?.raw ?? {}}
@@ -344,7 +351,7 @@ {:else}

Run an evaluation step to see the proof trace.

{/if} -

Reading the registry authority's own audit log is a product capability candidate, tracked separately.

+

Authority-side operational logs remain outside the visitor-facing proof view.

diff --git a/justfile b/justfile index 674c811..679674a 100644 --- a/justfile +++ b/justfile @@ -6,163 +6,122 @@ compose_project_name := `python3 scripts/compose_project_name.py` default: @just --list -# Install local development dependencies when subprojects define them. setup: - @if [ -f pyproject.toml ]; then uv sync; fi - @if [ -f generator/pyproject.toml ]; then cd generator && uv sync; fi - @if [ -f portal/package.json ]; then cd portal && pnpm install --frozen-lockfile; fi - @if [ -f home/package.json ]; then cd home && pnpm install --frozen-lockfile; fi + uv sync + cd generator && uv sync + cd portal && pnpm install --frozen-lockfile + cd home && pnpm install --frozen-lockfile -# Generate deterministic fixtures and local secrets. +# Generate deterministic fixtures plus ignored operator/runtime material. generate: - @if [ -f generator/pyproject.toml ]; then cd generator && uv run python -m solmara_lab.generate; else echo "generator/pyproject.toml missing"; exit 1; fi - scripts/gen-secrets.py - -# Generate a clean checkout, verify compiler output, and start the local topology. -up-generated: - just generate - just registry-projects-runtime-check - just up - -# Validate every authority-owned Registry project in both deployment profiles. -registry-projects-check: - scripts/registry-projects.sh check - -# Print the complete redacted acquisition and disclosure plan for every authority. -registry-projects-review: - scripts/registry-projects.sh review - -# Inspect compiled, declared, enabled, used, and missing capabilities. -registry-projects-capabilities: - scripts/registry-projects.sh capabilities - -# Install or refresh version-matched VS Code and Zed schema mappings. -registry-projects-editor: - scripts/registry-projects.sh editor - -# Run every synthetic authority integration fixture offline. -registry-projects-test: - scripts/registry-projects.sh test - -# Build private public and consultation Relay inputs for every authority project. -registry-projects-build environment="local": - scripts/registry-projects.sh build {{ environment }} - -# Refresh the committed runtime closure from all authored authority projects. -registry-projects-sync: - scripts/registry-projects.sh sync-runtime - -# Prove the committed runtime closure matches the authored authority projects. -registry-projects-runtime-check: - scripts/registry-projects.sh check-runtime + cd generator && uv run python -m solmara_lab.generate + uv run scripts/gen-secrets.py + @if test -d runtime/evidence-cells; then chmod -R u+w runtime/evidence-cells && rm -rf -- runtime/evidence-cells; fi + uv run python evidence/scripts/build-cells.py --private-key-root config/evidence/local/cells --output runtime/evidence-cells + uv run python scripts/project-runtime-secrets.py + uv run scripts/check-signer-public-keys.py + +build-runtime-images: + scripts/build-registry-stack-runtime.sh -# Generate only local secrets. -gen-secrets: - scripts/gen-secrets.py +# Compile production Relay packages and publish deterministic SQLite sources. +prepare-runtime: build-runtime-images + uv run scripts/check-signer-public-keys.py + scripts/prepare-authority-runtime.sh -# Check the paired Mint config and all authored Registry Evidence fixtures. -evidence-check: - scripts/check-evidence-runtime.py +prepare: generate prepare-runtime -# Publish the static metadata bundle served by static-metadata. metadata-publish: - @if command -v registry-manifest-cli >/dev/null 2>&1; then registry-manifest-cli publish metadata/solmara-wave1.metadata.yaml --out metadata/public/metadata --site-root metadata/public; fi uv run scripts/publish-metadata.py -# Check that the committed static metadata bundle is up to date. metadata-publish-check: uv run scripts/publish-metadata.py --check -# Lint the published metadata bundle. -metadata-lint: - uv run scripts/metadata-lint.py - -# Static repository checks. lint: + uvx ruff check --select E4,E7,E9,F . scripts/check-fiction.sh - scripts/check-image-pins.py scripts/check-config-secrets.py + scripts/hosted-image-manifest.py inventory + scripts/check-runtime-topology.py + scripts/check-registry-stack-release-pin.py + uv run scripts/check-signer-public-keys.py just metadata-publish-check - just metadata-lint - @if [ -f portal/package.json ]; then cd portal && pnpm check; fi - @if [ -f home/package.json ]; then cd home && pnpm check; fi + uv run scripts/metadata-lint.py + cd portal && pnpm check + cd home && pnpm check -# Unit and integration tests that can run without a full Compose stack. test: - @if [ -f generator/pyproject.toml ]; then cd generator && uv run python -m unittest discover -s tests; fi - uv run python3 -m unittest discover -s scenario-runner -p 'test_*.py' - @if [ -f portal/package.json ]; then cd portal && pnpm test; fi - @if [ -f home/package.json ]; then cd home && pnpm test; fi - uv run python3 -m unittest scripts/test_gen_secrets.py scripts/test_image_pins.py scripts/test_registryctl_build_output.py scripts/test_registryctl_test_output.py scripts/test_relay_workload_identity_agent.py scripts/test_smoke_esignet.py scripts/test_smoke_nia_attribute_release.py scripts/test_smoke_portal_compose.py scripts/test_smoke_relay_sources.py + cd generator && uv run python -m unittest discover -s tests + uv run python -m unittest discover -s scenario-runner -p 'test_*.py' + uv run python -m unittest relays/test_relay_projects.py evidence/tests/test_cells.py scripts/test_metadata_authority_contracts.py scripts/test_image_pins.py scripts/test_hosted_image_manifest.py scripts/test_hosted_provisioning_topology.py scripts/test_hosted_home_topology.py scripts/test_hosted_runtime_assets.py scripts/test_provision_hosted_runtime.py scripts/test_hosted_transit_signer.py scripts/test_smoke_hosted_provisioner_image.py scripts/test_runtime_topology.py scripts/test_registry_stack_release_pin.py scripts/test_hosted_authority_rollout.py scripts/test_local_transit_proxy.py scripts/test_local_transit_signers.py scripts/test_local_transit_providers.py scripts/test_signer_public_keys.py scripts/test_project_runtime_secrets.py scripts/test_gen_secrets.py scripts/test_publish_runtime_extracts.py scripts/test_lifecycle_proof.py scripts/test_live_lifecycle_proof.py scripts/test_local_relay_source_publisher.py scripts/test_smoke_programme_acceptance.py scripts/test_smoke_esignet.py + cd portal && pnpm test + cd home && pnpm test -# Validate Compose files without starting services. +# Validate local, hosted, eSignet, and every Coolify topology without starting it. compose: - @if [ ! -f .env ]; then echo ".env is missing; run 'just gen-secrets' first" >&2; exit 1; fi - @if [ -f compose.yaml ]; then COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml config >/dev/null; fi - @if [ -f compose.esignet.yaml ]; then COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml -f compose.esignet.yaml config >/dev/null; fi + @test -f .env || { echo ".env is missing; run just gen-secrets" >&2; exit 1; } + COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml config >/dev/null + COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml -f compose.esignet.yaml config >/dev/null + @scripts/check-hosted-compose.sh + @scripts/check-coolify-compose.sh -# Start the local topology. -up: - scripts/build-registry-stack-runtime.sh - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml up -d --build +# Verify that every Registry Stack v0.20.1 release reference is public and immutable. +hosted-pin-check: + scripts/check-registry-stack-release-pin.py --require-public -# Compatibility alias: all local starts now build Relay, Evidence, and Mint from source. -up-dev: - just up +# Run all four relayctl production gates against the five authored projects. +relay-check: build-runtime-images + scripts/check-relay-projects.sh -# Stop the local topology without removing local volumes. -down: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml down +# Check the six running Evidence deployments and all eleven authored fixtures. +evidence-check: + scripts/check-evidence-cells.sh -# Start the local topology with eSignet-backed portal login. -up-esignet: - scripts/build-registry-stack-runtime.sh - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml up -d --build +signers-up: + uv run scripts/local-transit-signers.py up + scripts/check-local-transit-providers.py -# Compatibility alias for the source-built eSignet topology. -up-esignet-dev: - just up-esignet +signers-down: + uv run scripts/local-transit-signers.py down + +up: prepare + COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml up -d --build + +up-esignet: prepare + COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml -f compose.esignet.yaml up -d --build + +down: + COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml down + just signers-down -# Stop the local eSignet topology without removing local volumes. down-esignet: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml down + COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml -f compose.esignet.yaml down + just signers-down -# Stop the local topology and remove this checkout's local volumes. +# Destructive reset is intentionally local-only. Hosted rollout never deletes volumes. reset: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml down -v + COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml down -v -# Stop the local eSignet topology and remove this checkout's local eSignet volumes. -reset-esignet: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml down -v +rollout phase: + scripts/hosted-authority-rollout.py {{ phase }} -# Run story and authority-application smokes against the running local topology. smoke: scripts/smoke.sh -# Run only live HTTP checks against the running local topology. -smoke-live: - uv run --locked scripts/smoke-live.py +smoke-esignet: + @uv run scripts/smoke-esignet.py >/dev/null && node scripts/smoke-esignet-login.mjs || { echo "smoke-esignet: FAIL"; exit 1; } -# Smoke eSignet discovery; portal login proves the NIA attribute-release path end to end. -smoke-esignet *args: - uv run scripts/smoke-esignet.py {{ args }} +programme-acceptance: + @COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml exec -T scenario-runner python - < scripts/smoke-programme-acceptance.py 2>/dev/null || { echo "programme-acceptance: FAIL scenario-runner-execution"; exit 1; } -# Probe the Relay Records APIs used by Registry Evidence. -relay-source-smoke: - scripts/smoke-relay-sources.py +lifecycle-proof: + uv run scripts/live-lifecycle-proof.py -# Smoke the Compose portal service and live BFF wiring. -portal-compose-smoke: - scripts/smoke-portal-compose.py +lifecycle-fixture-proof: + uv run scripts/lifecycle_proof.py -# Run browser e2e against the live local topology. portal-live-e2e: - @cd portal && PORT="${PORT:-4001}" PORTAL_PROVIDER=live pnpm e2e + cd portal && SOLMARA_PORTAL_E2E_MODE=hosted PLAYWRIGHT_BASE_URL="http://127.0.0.1:${SOLMARA_PORTAL_PORT:-4300}" pnpm e2e -# Run browser e2e against the Visitor's Center. home-live-e2e: - @cd home && SOLMARA_HOME_E2E_MODE=live PLAYWRIGHT_BASE_URL="http://127.0.0.1:${SOLMARA_HOME_PORT:-4301}" pnpm e2e - -# Run release-readiness and security-oriented checks. -review: - scripts/review.sh + cd home && SOLMARA_HOME_E2E_MODE=live PLAYWRIGHT_BASE_URL="http://127.0.0.1:${SOLMARA_HOME_PORT:-4301}" pnpm e2e diff --git a/metadata/public/metadata/catalog.json b/metadata/public/metadata/catalog.json index ede1e9b..ad05ede 100644 --- a/metadata/public/metadata/catalog.json +++ b/metadata/public/metadata/catalog.json @@ -51,76 +51,63 @@ ], "data_services": [ { - "conforms_to": "https://id.registrystack.org/solmara/contracts/authority-predicate-collection/v1", - "description": { - "en": "Returns application/json collections assembled from source-Notary predicates without composing an eligibility decision." - }, - "endpoint_description": "https://child-benefit-federator.solmara.registrystack.org/v1/claims", - "endpoint_url": "https://child-benefit-federator.solmara.registrystack.org/v1/evaluations", - "id": "child-benefit-evidence-collector-api", - "iri": "https://child-benefit-federator.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "endpoint_description": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", + "id": "cra-evidence-api", + "iri": "https://cra-evidence.solmara.registrystack.org/v1/evidence", "title": { - "en": "Child benefit authority predicate collection API" + "en": "CRA Evidence cell" } }, { - "conforms_to": "https://id.registrystack.org/spec/registry-notary/evidence/v1", - "endpoint_description": "https://cra-notary.solmara.registrystack.org/openapi.json", - "endpoint_url": "https://cra-notary.solmara.registrystack.org/v1/evaluations", - "id": "cra-notary-api", - "iri": "https://cra-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "endpoint_description": "https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence", + "id": "nia-evidence-api", + "iri": "https://nia-evidence.solmara.registrystack.org/v1/evidence", "title": { - "en": "CRA evidence API" + "en": "NIA Evidence cell" } }, { - "conforms_to": "https://id.registrystack.org/spec/registry-notary/evidence/v1", - "endpoint_description": "https://nia-notary.solmara.registrystack.org/openapi.json", - "endpoint_url": "https://nia-notary.solmara.registrystack.org/v1/evaluations", - "id": "nia-notary-api", - "iri": "https://nia-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "endpoint_description": "https://sro-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sro-evidence.solmara.registrystack.org/v1/evidence", + "id": "sro-evidence-api", + "iri": "https://sro-evidence.solmara.registrystack.org/v1/evidence", "title": { - "en": "NIA evidence API" + "en": "SRO Evidence cell" } }, { - "conforms_to": "https://id.registrystack.org/spec/registry-notary/evidence/v1", - "endpoint_description": "https://sro-notary.solmara.registrystack.org/openapi.json", - "endpoint_url": "https://sro-notary.solmara.registrystack.org/v1/evaluations", - "id": "sro-notary-api", - "iri": "https://sro-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "endpoint_description": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence", + "id": "mosd-programme-evidence-api", + "iri": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence", "title": { - "en": "SRO evidence API" + "en": "MoSD Programme Evidence cell" } }, { - "conforms_to": "https://id.registrystack.org/spec/registry-notary/evidence/v1", - "endpoint_description": "https://programme-notary.solmara.registrystack.org/openapi.json", - "endpoint_url": "https://programme-notary.solmara.registrystack.org/v1/evaluations", - "id": "programme-notary-api", - "iri": "https://programme-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "endpoint_description": "https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence", + "id": "sipf-evidence-api", + "iri": "https://sipf-evidence.solmara.registrystack.org/v1/evidence", "title": { - "en": "Programme MIS evidence API" + "en": "SIPF Evidence cell" } }, { - "conforms_to": "https://id.registrystack.org/spec/registry-notary/evidence/v1", - "endpoint_description": "https://sipf-notary.solmara.registrystack.org/openapi.json", - "endpoint_url": "https://sipf-notary.solmara.registrystack.org/v1/evaluations", - "id": "sipf-notary-api", - "iri": "https://sipf-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "endpoint_description": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence", + "id": "nagdi-evidence-api", + "iri": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence", "title": { - "en": "SIPF evidence API" - } - }, - { - "conforms_to": "https://id.registrystack.org/spec/registry-notary/evidence/v1", - "endpoint_description": "https://nagdi-notary.solmara.registrystack.org/openapi.json", - "endpoint_url": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "id": "nagdi-notary-api", - "iri": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "title": { - "en": "NAgDI evidence API" + "en": "NAgDI Evidence cell" } } ], @@ -138,39 +125,15 @@ "iri": "did:web:id.registrystack.org:solmara:authority:cra", "name": "Civil Registration Authority" }, - "description": "Cra civil published metadata.", + "description": "CRA-owned immutable birth extract and governed civil-person Relay source.", "entities": [ { "description": "Civil person entity metadata.", "fields": [ { - "concepts": [ - "ps:Person.identifier" - ], "name": "uin", - "required": false, - "type": "string" - }, - { - "concepts": [ - "ps:crvs/Birth.registrationNumber" - ], - "name": "birth_brn", - "required": false, - "type": "string" - }, - { - "concepts": [ - "ps:crvs/Death.registrationNumber" - ], - "name": "death_drn", - "required": false, - "type": "string" - }, - { - "name": "deceased", "required": true, - "type": "boolean" + "type": "string" } ], "identifiers": [ @@ -190,9 +153,8 @@ "cpsv-ap" ], "concepts": [ - "ps:Person.identifier", - "ps:crvs/Birth.registrationNumber", - "ps:crvs/Death.registrationNumber" + "https://publicschema.org/crvs/Birth", + "https://publicschema.org/crvs/Death" ] }, "title": "Civil person" @@ -204,7 +166,7 @@ "https://id.registrystack.org/solmara/purpose/pension-payment-review", "https://id.registrystack.org/solmara/purpose/citizen-self-service" ], - "title": "Civil registration" + "title": "CRA civil registration publication" }, { "access_rights": "restricted", @@ -219,13 +181,24 @@ "iri": "did:web:id.registrystack.org:solmara:authority:nia", "name": "National Identity Agency" }, - "description": "Nia population published metadata.", + "description": "NIA-owned immutable population extract and governed eSignet Relay source.", "entities": [ { - "description": "Person entity metadata.", - "fields": [], - "identifiers": [], - "name": "person", + "description": "Population person entity metadata.", + "fields": [ + { + "name": "uin", + "required": true, + "type": "string" + } + ], + "identifiers": [ + { + "kind": "uin", + "name": "uin" + } + ], + "name": "population_person", "purposes": [ "https://id.registrystack.org/solmara/purpose/child-benefit-review", "https://id.registrystack.org/solmara/purpose/citizen-self-service" @@ -238,7 +211,7 @@ "https://publicschema.org/Person" ] }, - "title": "Person" + "title": "Population person" } ], "id": "nia-population", @@ -246,7 +219,7 @@ "https://id.registrystack.org/solmara/purpose/child-benefit-review", "https://id.registrystack.org/solmara/purpose/citizen-self-service" ], - "title": "Population register" + "title": "NIA population publication" }, { "access_rights": "restricted", @@ -261,13 +234,24 @@ "iri": "did:web:id.registrystack.org:solmara:authority:sro", "name": "Social Registry Office" }, - "description": "Sro social published metadata.", + "description": "SRO-owned immutable poverty publication.", "entities": [ { - "description": "Household entity metadata.", - "fields": [], - "identifiers": [], - "name": "household", + "description": "Poverty record entity metadata.", + "fields": [ + { + "name": "uin", + "required": true, + "type": "string" + } + ], + "identifiers": [ + { + "kind": "uin", + "name": "uin" + } + ], + "name": "poverty_record", "purposes": [ "https://id.registrystack.org/solmara/purpose/child-benefit-review" ], @@ -280,14 +264,14 @@ "https://publicschema.org/SocioEconomicProfile" ] }, - "title": "Household" + "title": "Poverty record" } ], "id": "sro-social", "purposes": [ "https://id.registrystack.org/solmara/purpose/child-benefit-review" ], - "title": "Social registry" + "title": "SRO poverty extract" }, { "access_rights": "restricted", @@ -302,13 +286,24 @@ "iri": "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", "name": "Ministry of Social Development Programme MIS" }, - "description": "Mosd programme published metadata.", + "description": "MoSD-owned mutable SQLite source consulted through Relay V2.", "entities": [ { - "description": "Enrollment entity metadata.", - "fields": [], - "identifiers": [], - "name": "enrollment", + "description": "Beneficiary enrolment entity metadata.", + "fields": [ + { + "name": "uin", + "required": true, + "type": "string" + } + ], + "identifiers": [ + { + "kind": "uin", + "name": "uin" + } + ], + "name": "beneficiary_enrolment", "purposes": [ "https://id.registrystack.org/solmara/purpose/child-benefit-review" ], @@ -320,14 +315,14 @@ "https://publicschema.org/sp/Enrollment" ] }, - "title": "Enrollment" + "title": "Beneficiary enrolment" } ], "id": "mosd-programme", "purposes": [ "https://id.registrystack.org/solmara/purpose/child-benefit-review" ], - "title": "Integrated beneficiary registry" + "title": "MoSD programme enrolment publication" }, { "access_rights": "restricted", @@ -342,13 +337,24 @@ "iri": "did:web:id.registrystack.org:solmara:authority:sipf", "name": "Social Insurance and Pensions Fund" }, - "description": "Sipf pensions published metadata.", + "description": "SIPF-owned mutable SQLite source consulted through Relay V2.", "entities": [ { - "description": "Pension case entity metadata.", - "fields": [], - "identifiers": [], - "name": "pension_case", + "description": "Pension payment entity metadata.", + "fields": [ + { + "name": "pensioner_uin", + "required": true, + "type": "string" + } + ], + "identifiers": [ + { + "kind": "uin", + "name": "pensioner_uin" + } + ], + "name": "pension_payment", "purposes": [ "https://id.registrystack.org/solmara/purpose/pension-payment-review", "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" @@ -361,7 +367,37 @@ "https://id.registrystack.org/solmara/semantics/pension-case" ] }, - "title": "Pension case" + "title": "Pension payment" + }, + { + "description": "Survivor case entity metadata.", + "fields": [ + { + "name": "spouse_uin", + "required": true, + "type": "string" + } + ], + "identifiers": [ + { + "kind": "uin", + "name": "spouse_uin" + } + ], + "name": "survivor_case", + "purposes": [ + "https://id.registrystack.org/solmara/purpose/pension-payment-review", + "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/semantics/pension-case" + ] + }, + "title": "Survivor case" } ], "id": "sipf-pensions", @@ -369,7 +405,7 @@ "https://id.registrystack.org/solmara/purpose/pension-payment-review", "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" ], - "title": "Pensions and social insurance" + "title": "SIPF pension and survivor publication" }, { "access_rights": "restricted", @@ -384,12 +420,23 @@ "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", "name": "National Agricultural Data Institute" }, - "description": "Nagdi agriculture published metadata.", + "description": "NAgDI-owned mutable SQLite source consulted through Relay V2.", "entities": [ { "description": "Farmer voucher entity metadata.", - "fields": [], - "identifiers": [], + "fields": [ + { + "name": "farmer_id", + "required": true, + "type": "string" + } + ], + "identifiers": [ + { + "kind": "local", + "name": "farmer_id" + } + ], "name": "farmer_voucher", "purposes": [ "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review", @@ -407,8 +454,19 @@ }, { "description": "Livestock movement entity metadata.", - "fields": [], - "identifiers": [], + "fields": [ + { + "name": "farmer_id", + "required": true, + "type": "string" + } + ], + "identifiers": [ + { + "kind": "local", + "name": "farmer_id" + } + ], "name": "livestock_movement", "purposes": [ "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review", @@ -430,124 +488,157 @@ "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review", "https://id.registrystack.org/solmara/purpose/livestock-movement-control" ], - "title": "Farmer and livestock registries" + "title": "NAgDI agriculture publication" } ], - "description": "One merged CPSV-AP metadata publish for Solmara Wave 1 registry and evidence services.", + "description": "Six authority-owned Evidence cells, five governed Relay V2 services, and the programme applications that compose signed assertions.", "evidence_types": [ { - "id": "birth-registration-evidence", + "id": "cra-child-benefit-v1", "information_concepts": [ - "ps:crvs/Birth" + "https://id.registrystack.org/solmara/concept/birth-is-registered", + "https://id.registrystack.org/solmara/concept/child-age-under-5" ], - "iri": "https://id.registrystack.org/solmara/evidence-types/birth-registration", + "iri": "https://id.registrystack.org/solmara/evidence-type/cra-child-benefit/v1", "proves": [ - "child-benefit-review-requirement" + "cra-child-benefit-v1" ], "title": { - "en": "Birth registration evidence" + "en": "CRA child benefit assertion" } }, { - "id": "population-status-evidence", + "id": "cra-death-status-v1", "information_concepts": [ - "ps:Person" + "https://id.registrystack.org/solmara/concept/person-is-deceased" ], - "iri": "https://id.registrystack.org/solmara/evidence-types/population-status", + "iri": "https://id.registrystack.org/solmara/evidence-type/cra-death-status/v1", "proves": [ - "child-benefit-review-requirement" + "cra-pension-death-v1" ], "title": { - "en": "Population status evidence" + "en": "CRA death-status assertion" } }, { - "id": "household-poverty-evidence", + "id": "civil-record-linked-v1", "information_concepts": [ - "ps:Household", - "ps:SocioEconomicProfile" + "https://id.registrystack.org/solmara/concept/civil-record-linked" ], - "iri": "https://id.registrystack.org/solmara/evidence-types/household-poverty", + "iri": "https://id.registrystack.org/solmara/evidence-type/civil-record-linked/v1", "proves": [ - "child-benefit-review-requirement" + "cra-citizen-record-v1" ], "title": { - "en": "Household poverty evidence" + "en": "CRA civil-record assertion" } }, { - "id": "beneficiary-enrollment-evidence", + "id": "population-active-v1", "information_concepts": [ - "ps:sp/Enrollment" + "https://id.registrystack.org/solmara/concept/population-record-active" ], - "iri": "https://id.registrystack.org/solmara/evidence-types/beneficiary-enrollment", + "iri": "https://id.registrystack.org/solmara/evidence-type/population-active/v1", "proves": [ - "child-benefit-review-requirement" + "nia-child-benefit-v1" ], "title": { - "en": "Beneficiary enrollment evidence" + "en": "NIA active-population assertion" } }, { - "description": { - "en": "A transient collection of source-owned child benefit predicates with no copied source rows or eligibility decision." - }, - "id": "child-benefit-authority-predicate-collection-evidence", + "id": "citizen-population-active-v1", + "information_concepts": [ + "https://id.registrystack.org/solmara/concept/citizen-population-record-active" + ], + "iri": "https://id.registrystack.org/solmara/evidence-type/citizen-population-active/v1", + "proves": [ + "nia-citizen-status-v1" + ], + "title": { + "en": "NIA active-citizen assertion" + } + }, + { + "id": "poverty-priority-v1", + "information_concepts": [ + "https://id.registrystack.org/solmara/concept/household-below-poverty-threshold" + ], + "iri": "https://id.registrystack.org/solmara/evidence-type/poverty-priority/v1", + "proves": [ + "sro-child-benefit-v1" + ], + "title": { + "en": "SRO poverty-priority assertion" + } + }, + { + "id": "not-enrolled-v1", "information_concepts": [ - "ps:Person" + "https://id.registrystack.org/solmara/concept/not-already-enrolled" ], - "iri": "https://id.registrystack.org/solmara/evidence-types/child-benefit-authority-predicate-collection", + "iri": "https://id.registrystack.org/solmara/evidence-type/not-enrolled/v1", "proves": [ - "child-benefit-review-requirement" + "mosd-child-benefit-v1" ], "title": { - "en": "Child benefit authority predicate collection" + "en": "MoSD not-enrolled assertion" } }, { - "id": "death-registration-evidence", + "id": "pension-payment-active-v1", "information_concepts": [ - "ps:crvs/Death" + "https://id.registrystack.org/solmara/concept/pension-payment-active" ], - "iri": "https://id.registrystack.org/solmara/evidence-types/death-registration", + "iri": "https://id.registrystack.org/solmara/evidence-type/pension-payment-active/v1", "proves": [ - "pension-survivor-review-requirement" + "sipf-pension-payment-v1" ], "title": { - "en": "Death registration evidence" + "en": "SIPF active-payment assertion" } }, { - "id": "pension-case-evidence", - "iri": "https://id.registrystack.org/solmara/evidence-types/pension-case", + "id": "survivor-benefit-v1", + "information_concepts": [ + "https://id.registrystack.org/solmara/concept/survivor-is-eligible" + ], + "iri": "https://id.registrystack.org/solmara/evidence-type/survivor-benefit/v1", "proves": [ - "pension-survivor-review-requirement" + "sipf-survivor-benefit-v1" ], "title": { - "en": "Pension case evidence" + "en": "SIPF survivor-benefit assertion" } }, { - "id": "farmer-voucher-evidence", + "id": "climate-smart-voucher-v1", "information_concepts": [ - "ps:Farm" + "https://id.registrystack.org/solmara/concept/farmer-registered", + "https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose", + "https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher" ], - "iri": "https://id.registrystack.org/solmara/evidence-types/farmer-voucher", + "iri": "https://id.registrystack.org/solmara/evidence-type/climate-smart-voucher/v1", "proves": [ - "nagdi-voucher-requirement" + "nagdi-voucher-v1" ], "title": { - "en": "Farmer voucher evidence" + "en": "NAgDI climate-smart voucher assertion" } }, { - "id": "livestock-movement-evidence", - "iri": "https://id.registrystack.org/solmara/evidence-types/livestock-movement", + "id": "livestock-movement-v1", + "information_concepts": [ + "https://id.registrystack.org/solmara/concept/registered-herd", + "https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species", + "https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit" + ], + "iri": "https://id.registrystack.org/solmara/evidence-type/livestock-movement/v1", "proves": [ - "livestock-movement-requirement" + "nagdi-livestock-v1" ], "title": { - "en": "Livestock movement evidence" + "en": "NAgDI livestock movement assertion" } } ], @@ -613,22 +704,24 @@ "wave": null } ], - "id": "solmara-wave1-multi-authority", + "id": "solmara-authority-cells", "public_services": [ { "competent_authority": "mosd-programme-mis", "data_services": [ - "child-benefit-evidence-collector-api", - "cra-notary-api", - "nia-notary-api", - "sro-notary-api", - "programme-notary-api" + "cra-evidence-api", + "nia-evidence-api", + "sro-evidence-api", + "mosd-programme-evidence-api" ], "description": { - "en": "MoSD review of child benefit eligibility using minimized registry evidence." + "en": "The programme composes four independently signed authority assertions into five reviewed concepts." }, "holds_requirements": [ - "child-benefit-review-requirement" + "cra-child-benefit-v1", + "nia-child-benefit-v1", + "sro-child-benefit-v1", + "mosd-child-benefit-v1" ], "id": "child-benefit-review", "iri": "https://id.registrystack.org/solmara/services/child-benefit-review", @@ -640,14 +733,16 @@ { "competent_authority": "sipf", "data_services": [ - "cra-notary-api", - "sipf-notary-api" + "cra-evidence-api", + "sipf-evidence-api" ], "description": { - "en": "SIPF review for pension payment stop and survivor benefit eligibility." + "en": "The programme composes CRA and SIPF signed assertions without exposing unrelated civil data." }, "holds_requirements": [ - "pension-survivor-review-requirement" + "cra-pension-death-v1", + "sipf-pension-payment-v1", + "sipf-survivor-benefit-v1" ], "id": "pension-survivor-review", "iri": "https://id.registrystack.org/solmara/services/pension-survivor-review", @@ -657,39 +752,42 @@ } }, { - "competent_authority": "nagdi", + "competent_authority": "nia", "data_services": [ - "nagdi-notary-api" + "cra-evidence-api", + "nia-evidence-api" ], "description": { - "en": "NAgDI review of farmer eligibility for climate-smart input support." + "en": "The application composes separately signed CRA and NIA citizen assertions." }, "holds_requirements": [ - "nagdi-voucher-requirement" + "cra-citizen-record-v1", + "nia-citizen-status-v1" ], - "id": "nagdi-voucher-review", - "iri": "https://id.registrystack.org/solmara/services/nagdi-voucher-review", + "id": "citizen-self-service", + "iri": "https://id.registrystack.org/solmara/services/citizen-self-service", "jurisdiction": "https://id.registrystack.org/solmara/jurisdiction/XS", "title": { - "en": "Farmer climate-smart voucher review" + "en": "Citizen self-service" } }, { "competent_authority": "nagdi", "data_services": [ - "nagdi-notary-api" + "nagdi-evidence-api" ], "description": { - "en": "NAgDI review of livestock movement permit eligibility and quarantine controls." + "en": "NAgDI applications consume independently signed voucher and livestock assertions." }, "holds_requirements": [ - "livestock-movement-requirement" + "nagdi-voucher-v1", + "nagdi-livestock-v1" ], - "id": "livestock-movement-control", - "iri": "https://id.registrystack.org/solmara/services/livestock-movement-control", + "id": "agriculture-review", + "iri": "https://id.registrystack.org/solmara/services/agriculture-review", "jurisdiction": "https://id.registrystack.org/solmara/jurisdiction/XS", "title": { - "en": "Livestock movement control" + "en": "Agriculture voucher and livestock review" } } ], @@ -699,5 +797,5 @@ "name": "Solmara Interoperability Metadata Publisher" }, "schema_version": "registry-manifest-catalog/v1", - "title": "Solmara Lab Wave 1 Service Catalogue" + "title": "Solmara authority-owned Evidence catalogue" } diff --git a/metadata/public/metadata/cpsv-ap b/metadata/public/metadata/cpsv-ap index 78e7a64..71fa3b0 100644 --- a/metadata/public/metadata/cpsv-ap +++ b/metadata/public/metadata/cpsv-ap @@ -9,17 +9,19 @@ { "competent_authority": "mosd-programme-mis", "data_services": [ - "child-benefit-evidence-collector-api", - "cra-notary-api", - "nia-notary-api", - "sro-notary-api", - "programme-notary-api" + "cra-evidence-api", + "nia-evidence-api", + "sro-evidence-api", + "mosd-programme-evidence-api" ], "description": { - "en": "MoSD review of child benefit eligibility using minimized registry evidence." + "en": "The programme composes four independently signed authority assertions into five reviewed concepts." }, "holds_requirements": [ - "child-benefit-review-requirement" + "cra-child-benefit-v1", + "nia-child-benefit-v1", + "sro-child-benefit-v1", + "mosd-child-benefit-v1" ], "id": "child-benefit-review", "iri": "https://id.registrystack.org/solmara/services/child-benefit-review", @@ -31,14 +33,16 @@ { "competent_authority": "sipf", "data_services": [ - "cra-notary-api", - "sipf-notary-api" + "cra-evidence-api", + "sipf-evidence-api" ], "description": { - "en": "SIPF review for pension payment stop and survivor benefit eligibility." + "en": "The programme composes CRA and SIPF signed assertions without exposing unrelated civil data." }, "holds_requirements": [ - "pension-survivor-review-requirement" + "cra-pension-death-v1", + "sipf-pension-payment-v1", + "sipf-survivor-benefit-v1" ], "id": "pension-survivor-review", "iri": "https://id.registrystack.org/solmara/services/pension-survivor-review", @@ -48,39 +52,42 @@ } }, { - "competent_authority": "nagdi", + "competent_authority": "nia", "data_services": [ - "nagdi-notary-api" + "cra-evidence-api", + "nia-evidence-api" ], "description": { - "en": "NAgDI review of farmer eligibility for climate-smart input support." + "en": "The application composes separately signed CRA and NIA citizen assertions." }, "holds_requirements": [ - "nagdi-voucher-requirement" + "cra-citizen-record-v1", + "nia-citizen-status-v1" ], - "id": "nagdi-voucher-review", - "iri": "https://id.registrystack.org/solmara/services/nagdi-voucher-review", + "id": "citizen-self-service", + "iri": "https://id.registrystack.org/solmara/services/citizen-self-service", "jurisdiction": "https://id.registrystack.org/solmara/jurisdiction/XS", "title": { - "en": "Farmer climate-smart voucher review" + "en": "Citizen self-service" } }, { "competent_authority": "nagdi", "data_services": [ - "nagdi-notary-api" + "nagdi-evidence-api" ], "description": { - "en": "NAgDI review of livestock movement permit eligibility and quarantine controls." + "en": "NAgDI applications consume independently signed voucher and livestock assertions." }, "holds_requirements": [ - "livestock-movement-requirement" + "nagdi-voucher-v1", + "nagdi-livestock-v1" ], - "id": "livestock-movement-control", - "iri": "https://id.registrystack.org/solmara/services/livestock-movement-control", + "id": "agriculture-review", + "iri": "https://id.registrystack.org/solmara/services/agriculture-review", "jurisdiction": "https://id.registrystack.org/solmara/jurisdiction/XS", "title": { - "en": "Livestock movement control" + "en": "Agriculture voucher and livestock review" } } ], @@ -88,18 +95,18 @@ "solmara:evidenceOfferings": [ { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://cra-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://cra-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", "kind": "evidence-verification-api", - "ruleset": "birth-is-registered" + "source_type": "immutable extract" }, "dataset": "cra-civil", - "description": "Purpose-limited evidence predicates from Civil registration.", + "description": "An independently signed immutable extract assertion issued by Civil Registration Authority.", "entity": "civil_person", - "evidence_type": "birth-registration-evidence", - "id": "cra-birth-registration-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-birth-registration", + "evidence_type": "cra-child-benefit-v1", + "id": "cra-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "cra", @@ -110,7 +117,7 @@ "lookup_keys": [ "uin" ], - "policy": "cra-birth-registration-offering-policy", + "policy": "cra-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -122,26 +129,26 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/crvs/Death" + "https://id.registrystack.org/solmara/concept/birth-is-registered", + "https://id.registrystack.org/solmara/concept/child-age-under-5" ] }, - "title": "CRA birth registration evidence offering" + "title": "Civil Registration Authority Cra child benefit v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://cra-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://cra-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", "kind": "evidence-verification-api", - "ruleset": "person-is-deceased" + "source_type": "Relay lookup" }, "dataset": "cra-civil", - "description": "Purpose-limited evidence predicates from Civil registration.", + "description": "An independently signed Relay lookup assertion issued by Civil Registration Authority.", "entity": "civil_person", - "evidence_type": "death-registration-evidence", - "id": "cra-death-registration-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-death-registration", + "evidence_type": "cra-death-status-v1", + "id": "cra-pension-death-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-pension-death-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "cra", @@ -152,7 +159,7 @@ "lookup_keys": [ "uin" ], - "policy": "cra-death-registration-offering-policy", + "policy": "cra-pension-death-v1-offering-policy", "public_services": [ "pension-survivor-review" ], @@ -164,25 +171,66 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/crvs/Death" + "https://id.registrystack.org/solmara/concept/person-is-deceased" ] }, - "title": "CRA death registration evidence offering" + "title": "Civil Registration Authority Cra death status v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nia-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nia-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" + }, + "dataset": "cra-civil", + "description": "An independently signed Relay lookup assertion issued by Civil Registration Authority.", + "entity": "civil_person", + "evidence_type": "civil-record-linked-v1", + "id": "cra-citizen-record-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-citizen-record-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "cra", + "iri": "did:web:id.registrystack.org:solmara:authority:cra", + "name": "Civil Registration Authority", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "cra-citizen-record-v1-offering-policy", + "public_services": [ + "citizen-self-service" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/civil-record-linked" + ] + }, + "title": "Civil Registration Authority Civil record linked v1" + }, + { + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" }, "dataset": "nia-population", - "description": "Purpose-limited evidence predicates from Population register.", - "entity": "person", - "evidence_type": "population-status-evidence", - "id": "nia-population-population-status-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-population-population-status-offering", + "description": "An independently signed immutable extract assertion issued by National Identity Agency.", + "entity": "population_person", + "evidence_type": "population-active-v1", + "id": "nia-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "nia", @@ -193,7 +241,7 @@ "lookup_keys": [ "uin" ], - "policy": "nia-population-population-status-offering-policy", + "policy": "nia-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -205,24 +253,66 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Person" + "https://id.registrystack.org/solmara/concept/population-record-active" ] }, - "title": "Population register evidence offering" + "title": "National Identity Agency Population active v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://sro-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://sro-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" + }, + "dataset": "nia-population", + "description": "An independently signed immutable extract assertion issued by National Identity Agency.", + "entity": "population_person", + "evidence_type": "citizen-population-active-v1", + "id": "nia-citizen-status-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-citizen-status-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "nia", + "iri": "did:web:id.registrystack.org:solmara:authority:nia", + "name": "National Identity Agency", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "nia-citizen-status-v1-offering-policy", + "public_services": [ + "citizen-self-service" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/citizen-population-record-active" + ] + }, + "title": "National Identity Agency Citizen population active v1" + }, + { + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sro-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sro-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" }, "dataset": "sro-social", - "description": "Purpose-limited evidence predicates from Social registry.", - "entity": "household", - "evidence_type": "household-poverty-evidence", - "id": "sro-social-household-poverty-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/sro-social-household-poverty-offering", + "description": "An independently signed immutable extract assertion issued by Social Registry Office.", + "entity": "poverty_record", + "evidence_type": "poverty-priority-v1", + "id": "sro-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sro-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "sro", @@ -233,7 +323,7 @@ "lookup_keys": [ "uin" ], - "policy": "sro-social-household-poverty-offering-policy", + "policy": "sro-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -245,25 +335,25 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Household", - "https://publicschema.org/SocioEconomicProfile" + "https://id.registrystack.org/solmara/concept/household-below-poverty-threshold" ] }, - "title": "Social registry evidence offering" + "title": "Social Registry Office Poverty priority v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://programme-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://programme-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, "dataset": "mosd-programme", - "description": "Purpose-limited evidence predicates from Integrated beneficiary registry.", - "entity": "enrollment", - "evidence_type": "beneficiary-enrollment-evidence", - "id": "mosd-programme-beneficiary-enrollment-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/mosd-programme-beneficiary-enrollment-offering", + "description": "An independently signed Relay lookup assertion issued by Ministry of Social Development Programme MIS.", + "entity": "beneficiary_enrolment", + "evidence_type": "not-enrolled-v1", + "id": "mosd-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/mosd-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "mosd-programme-mis", @@ -274,7 +364,7 @@ "lookup_keys": [ "uin" ], - "policy": "mosd-programme-beneficiary-enrollment-offering-policy", + "policy": "mosd-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -286,24 +376,25 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/sp/Enrollment" + "https://id.registrystack.org/solmara/concept/not-already-enrolled" ] }, - "title": "Integrated beneficiary registry evidence offering" + "title": "Ministry of Social Development Programme MIS Not enrolled v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://sipf-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://sipf-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, "dataset": "sipf-pensions", - "description": "Purpose-limited evidence predicates from Pensions and social insurance.", - "entity": "pension_case", - "evidence_type": "pension-case-evidence", - "id": "sipf-pensions-pension-case-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pensions-pension-case-offering", + "description": "An independently signed Relay lookup assertion issued by Social Insurance and Pensions Fund.", + "entity": "pension_payment", + "evidence_type": "pension-payment-active-v1", + "id": "sipf-pension-payment-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pension-payment-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "sipf", @@ -312,79 +403,80 @@ "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" }, "lookup_keys": [ - "uin" + "pensioner_uin" ], - "policy": "sipf-pensions-pension-case-offering-policy", + "policy": "sipf-pension-payment-v1-offering-policy", "public_services": [ "pension-survivor-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/pension-payment-review", - "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" + "https://id.registrystack.org/solmara/purpose/pension-payment-review" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://id.registrystack.org/solmara/semantics/pension-case" + "https://id.registrystack.org/solmara/concept/pension-payment-active" ] }, - "title": "Pensions and social insurance evidence offering" + "title": "Social Insurance and Pensions Fund Pension payment active v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nagdi-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, - "dataset": "nagdi-agriculture", - "description": "Purpose-limited evidence predicates from Farmer and livestock registries.", - "entity": "farmer_voucher", - "evidence_type": "farmer-voucher-evidence", - "id": "nagdi-agriculture-farmer-voucher-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-farmer-voucher-offering", + "dataset": "sipf-pensions", + "description": "An independently signed Relay lookup assertion issued by Social Insurance and Pensions Fund.", + "entity": "survivor_case", + "evidence_type": "survivor-benefit-v1", + "id": "sipf-survivor-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-survivor-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", - "id": "nagdi", - "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", - "name": "National Agricultural Data Institute", + "id": "sipf", + "iri": "did:web:id.registrystack.org:solmara:authority:sipf", + "name": "Social Insurance and Pensions Fund", "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" }, "lookup_keys": [ - "farmer_id" + "spouse_uin" ], - "policy": "nagdi-agriculture-farmer-voucher-offering-policy", + "policy": "sipf-survivor-benefit-v1-offering-policy", "public_services": [ - "nagdi-voucher-review" + "pension-survivor-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" + "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Farm" + "https://id.registrystack.org/solmara/concept/survivor-is-eligible" ] }, - "title": "Farmer and livestock registries evidence offering" + "title": "Social Insurance and Pensions Fund Survivor benefit v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nagdi-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, "dataset": "nagdi-agriculture", - "description": "Livestock movement-control predicates for permit checks.", - "entity": "livestock_movement", - "evidence_type": "livestock-movement-evidence", - "id": "nagdi-agriculture-livestock-movement-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-livestock-movement-offering", + "description": "An independently signed Relay lookup assertion issued by National Agricultural Data Institute.", + "entity": "farmer_voucher", + "evidence_type": "climate-smart-voucher-v1", + "id": "nagdi-voucher-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-voucher-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "nagdi", @@ -395,67 +487,67 @@ "lookup_keys": [ "farmer_id" ], - "policy": "nagdi-agriculture-livestock-movement-offering-policy", + "policy": "nagdi-voucher-v1-offering-policy", "public_services": [ - "livestock-movement-control" + "agriculture-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/livestock-movement-control" + "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/livestock-type" + "https://id.registrystack.org/solmara/concept/farmer-registered", + "https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose", + "https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher" ] }, - "title": "NAgDI livestock movement evidence offering" + "title": "National Agricultural Data Institute Climate smart voucher v1" }, { "access": { - "conforms_to": "https://id.registrystack.org/solmara/contracts/authority-predicate-collection/v1", - "discovery_url": "https://child-benefit-federator.solmara.registrystack.org/v1/claims", - "endpoint_url": "https://child-benefit-federator.solmara.registrystack.org/v1/evaluations", - "kind": "authority-predicate-collection-api", - "media_type": "application/json", - "ruleset": "source-owned-child-benefit-predicates-v1" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, - "dataset": "mosd-programme", - "description": "A transient collection of source-owned child benefit predicates. It contains no copied source rows and no composed eligibility decision.", - "entity": "enrollment", - "evidence_type": "child-benefit-authority-predicate-collection-evidence", - "id": "solmara.child-benefit.authority-predicate-collection", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/child-benefit-authority-predicate-collection", + "dataset": "nagdi-agriculture", + "description": "An independently signed Relay lookup assertion issued by National Agricultural Data Institute.", + "entity": "livestock_movement", + "evidence_type": "livestock-movement-v1", + "id": "nagdi-livestock-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-livestock-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", - "id": "mosd-programme-mis", - "iri": "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", - "name": "Ministry of Social Development Programme MIS", + "id": "nagdi", + "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", + "name": "National Agricultural Data Institute", "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" }, "lookup_keys": [ - "uin" + "farmer_id" ], - "policy": "solmara-child-benefit-authority-predicate-collection-policy", + "policy": "nagdi-livestock-v1-offering-policy", "public_services": [ - "child-benefit-review" + "agriculture-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" + "https://id.registrystack.org/solmara/purpose/livestock-movement-control" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Person", - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/Household", - "https://publicschema.org/sp/Enrollment" + "https://id.registrystack.org/solmara/concept/registered-herd", + "https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species", + "https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit" ] }, - "title": "Child Benefit Authority Predicate Collection" + "title": "National Agricultural Data Institute Livestock movement v1" } ] } diff --git a/metadata/public/metadata/cpsv-ap.jsonld b/metadata/public/metadata/cpsv-ap.jsonld index 78e7a64..71fa3b0 100644 --- a/metadata/public/metadata/cpsv-ap.jsonld +++ b/metadata/public/metadata/cpsv-ap.jsonld @@ -9,17 +9,19 @@ { "competent_authority": "mosd-programme-mis", "data_services": [ - "child-benefit-evidence-collector-api", - "cra-notary-api", - "nia-notary-api", - "sro-notary-api", - "programme-notary-api" + "cra-evidence-api", + "nia-evidence-api", + "sro-evidence-api", + "mosd-programme-evidence-api" ], "description": { - "en": "MoSD review of child benefit eligibility using minimized registry evidence." + "en": "The programme composes four independently signed authority assertions into five reviewed concepts." }, "holds_requirements": [ - "child-benefit-review-requirement" + "cra-child-benefit-v1", + "nia-child-benefit-v1", + "sro-child-benefit-v1", + "mosd-child-benefit-v1" ], "id": "child-benefit-review", "iri": "https://id.registrystack.org/solmara/services/child-benefit-review", @@ -31,14 +33,16 @@ { "competent_authority": "sipf", "data_services": [ - "cra-notary-api", - "sipf-notary-api" + "cra-evidence-api", + "sipf-evidence-api" ], "description": { - "en": "SIPF review for pension payment stop and survivor benefit eligibility." + "en": "The programme composes CRA and SIPF signed assertions without exposing unrelated civil data." }, "holds_requirements": [ - "pension-survivor-review-requirement" + "cra-pension-death-v1", + "sipf-pension-payment-v1", + "sipf-survivor-benefit-v1" ], "id": "pension-survivor-review", "iri": "https://id.registrystack.org/solmara/services/pension-survivor-review", @@ -48,39 +52,42 @@ } }, { - "competent_authority": "nagdi", + "competent_authority": "nia", "data_services": [ - "nagdi-notary-api" + "cra-evidence-api", + "nia-evidence-api" ], "description": { - "en": "NAgDI review of farmer eligibility for climate-smart input support." + "en": "The application composes separately signed CRA and NIA citizen assertions." }, "holds_requirements": [ - "nagdi-voucher-requirement" + "cra-citizen-record-v1", + "nia-citizen-status-v1" ], - "id": "nagdi-voucher-review", - "iri": "https://id.registrystack.org/solmara/services/nagdi-voucher-review", + "id": "citizen-self-service", + "iri": "https://id.registrystack.org/solmara/services/citizen-self-service", "jurisdiction": "https://id.registrystack.org/solmara/jurisdiction/XS", "title": { - "en": "Farmer climate-smart voucher review" + "en": "Citizen self-service" } }, { "competent_authority": "nagdi", "data_services": [ - "nagdi-notary-api" + "nagdi-evidence-api" ], "description": { - "en": "NAgDI review of livestock movement permit eligibility and quarantine controls." + "en": "NAgDI applications consume independently signed voucher and livestock assertions." }, "holds_requirements": [ - "livestock-movement-requirement" + "nagdi-voucher-v1", + "nagdi-livestock-v1" ], - "id": "livestock-movement-control", - "iri": "https://id.registrystack.org/solmara/services/livestock-movement-control", + "id": "agriculture-review", + "iri": "https://id.registrystack.org/solmara/services/agriculture-review", "jurisdiction": "https://id.registrystack.org/solmara/jurisdiction/XS", "title": { - "en": "Livestock movement control" + "en": "Agriculture voucher and livestock review" } } ], @@ -88,18 +95,18 @@ "solmara:evidenceOfferings": [ { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://cra-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://cra-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", "kind": "evidence-verification-api", - "ruleset": "birth-is-registered" + "source_type": "immutable extract" }, "dataset": "cra-civil", - "description": "Purpose-limited evidence predicates from Civil registration.", + "description": "An independently signed immutable extract assertion issued by Civil Registration Authority.", "entity": "civil_person", - "evidence_type": "birth-registration-evidence", - "id": "cra-birth-registration-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-birth-registration", + "evidence_type": "cra-child-benefit-v1", + "id": "cra-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "cra", @@ -110,7 +117,7 @@ "lookup_keys": [ "uin" ], - "policy": "cra-birth-registration-offering-policy", + "policy": "cra-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -122,26 +129,26 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/crvs/Death" + "https://id.registrystack.org/solmara/concept/birth-is-registered", + "https://id.registrystack.org/solmara/concept/child-age-under-5" ] }, - "title": "CRA birth registration evidence offering" + "title": "Civil Registration Authority Cra child benefit v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://cra-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://cra-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", "kind": "evidence-verification-api", - "ruleset": "person-is-deceased" + "source_type": "Relay lookup" }, "dataset": "cra-civil", - "description": "Purpose-limited evidence predicates from Civil registration.", + "description": "An independently signed Relay lookup assertion issued by Civil Registration Authority.", "entity": "civil_person", - "evidence_type": "death-registration-evidence", - "id": "cra-death-registration-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-death-registration", + "evidence_type": "cra-death-status-v1", + "id": "cra-pension-death-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-pension-death-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "cra", @@ -152,7 +159,7 @@ "lookup_keys": [ "uin" ], - "policy": "cra-death-registration-offering-policy", + "policy": "cra-pension-death-v1-offering-policy", "public_services": [ "pension-survivor-review" ], @@ -164,25 +171,66 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/crvs/Death" + "https://id.registrystack.org/solmara/concept/person-is-deceased" ] }, - "title": "CRA death registration evidence offering" + "title": "Civil Registration Authority Cra death status v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nia-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nia-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" + }, + "dataset": "cra-civil", + "description": "An independently signed Relay lookup assertion issued by Civil Registration Authority.", + "entity": "civil_person", + "evidence_type": "civil-record-linked-v1", + "id": "cra-citizen-record-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-citizen-record-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "cra", + "iri": "did:web:id.registrystack.org:solmara:authority:cra", + "name": "Civil Registration Authority", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "cra-citizen-record-v1-offering-policy", + "public_services": [ + "citizen-self-service" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/civil-record-linked" + ] + }, + "title": "Civil Registration Authority Civil record linked v1" + }, + { + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" }, "dataset": "nia-population", - "description": "Purpose-limited evidence predicates from Population register.", - "entity": "person", - "evidence_type": "population-status-evidence", - "id": "nia-population-population-status-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-population-population-status-offering", + "description": "An independently signed immutable extract assertion issued by National Identity Agency.", + "entity": "population_person", + "evidence_type": "population-active-v1", + "id": "nia-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "nia", @@ -193,7 +241,7 @@ "lookup_keys": [ "uin" ], - "policy": "nia-population-population-status-offering-policy", + "policy": "nia-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -205,24 +253,66 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Person" + "https://id.registrystack.org/solmara/concept/population-record-active" ] }, - "title": "Population register evidence offering" + "title": "National Identity Agency Population active v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://sro-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://sro-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" + }, + "dataset": "nia-population", + "description": "An independently signed immutable extract assertion issued by National Identity Agency.", + "entity": "population_person", + "evidence_type": "citizen-population-active-v1", + "id": "nia-citizen-status-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-citizen-status-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "nia", + "iri": "did:web:id.registrystack.org:solmara:authority:nia", + "name": "National Identity Agency", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "nia-citizen-status-v1-offering-policy", + "public_services": [ + "citizen-self-service" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/citizen-population-record-active" + ] + }, + "title": "National Identity Agency Citizen population active v1" + }, + { + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sro-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sro-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" }, "dataset": "sro-social", - "description": "Purpose-limited evidence predicates from Social registry.", - "entity": "household", - "evidence_type": "household-poverty-evidence", - "id": "sro-social-household-poverty-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/sro-social-household-poverty-offering", + "description": "An independently signed immutable extract assertion issued by Social Registry Office.", + "entity": "poverty_record", + "evidence_type": "poverty-priority-v1", + "id": "sro-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sro-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "sro", @@ -233,7 +323,7 @@ "lookup_keys": [ "uin" ], - "policy": "sro-social-household-poverty-offering-policy", + "policy": "sro-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -245,25 +335,25 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Household", - "https://publicschema.org/SocioEconomicProfile" + "https://id.registrystack.org/solmara/concept/household-below-poverty-threshold" ] }, - "title": "Social registry evidence offering" + "title": "Social Registry Office Poverty priority v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://programme-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://programme-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, "dataset": "mosd-programme", - "description": "Purpose-limited evidence predicates from Integrated beneficiary registry.", - "entity": "enrollment", - "evidence_type": "beneficiary-enrollment-evidence", - "id": "mosd-programme-beneficiary-enrollment-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/mosd-programme-beneficiary-enrollment-offering", + "description": "An independently signed Relay lookup assertion issued by Ministry of Social Development Programme MIS.", + "entity": "beneficiary_enrolment", + "evidence_type": "not-enrolled-v1", + "id": "mosd-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/mosd-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "mosd-programme-mis", @@ -274,7 +364,7 @@ "lookup_keys": [ "uin" ], - "policy": "mosd-programme-beneficiary-enrollment-offering-policy", + "policy": "mosd-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -286,24 +376,25 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/sp/Enrollment" + "https://id.registrystack.org/solmara/concept/not-already-enrolled" ] }, - "title": "Integrated beneficiary registry evidence offering" + "title": "Ministry of Social Development Programme MIS Not enrolled v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://sipf-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://sipf-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, "dataset": "sipf-pensions", - "description": "Purpose-limited evidence predicates from Pensions and social insurance.", - "entity": "pension_case", - "evidence_type": "pension-case-evidence", - "id": "sipf-pensions-pension-case-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pensions-pension-case-offering", + "description": "An independently signed Relay lookup assertion issued by Social Insurance and Pensions Fund.", + "entity": "pension_payment", + "evidence_type": "pension-payment-active-v1", + "id": "sipf-pension-payment-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pension-payment-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "sipf", @@ -312,79 +403,80 @@ "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" }, "lookup_keys": [ - "uin" + "pensioner_uin" ], - "policy": "sipf-pensions-pension-case-offering-policy", + "policy": "sipf-pension-payment-v1-offering-policy", "public_services": [ "pension-survivor-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/pension-payment-review", - "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" + "https://id.registrystack.org/solmara/purpose/pension-payment-review" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://id.registrystack.org/solmara/semantics/pension-case" + "https://id.registrystack.org/solmara/concept/pension-payment-active" ] }, - "title": "Pensions and social insurance evidence offering" + "title": "Social Insurance and Pensions Fund Pension payment active v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nagdi-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, - "dataset": "nagdi-agriculture", - "description": "Purpose-limited evidence predicates from Farmer and livestock registries.", - "entity": "farmer_voucher", - "evidence_type": "farmer-voucher-evidence", - "id": "nagdi-agriculture-farmer-voucher-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-farmer-voucher-offering", + "dataset": "sipf-pensions", + "description": "An independently signed Relay lookup assertion issued by Social Insurance and Pensions Fund.", + "entity": "survivor_case", + "evidence_type": "survivor-benefit-v1", + "id": "sipf-survivor-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-survivor-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", - "id": "nagdi", - "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", - "name": "National Agricultural Data Institute", + "id": "sipf", + "iri": "did:web:id.registrystack.org:solmara:authority:sipf", + "name": "Social Insurance and Pensions Fund", "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" }, "lookup_keys": [ - "farmer_id" + "spouse_uin" ], - "policy": "nagdi-agriculture-farmer-voucher-offering-policy", + "policy": "sipf-survivor-benefit-v1-offering-policy", "public_services": [ - "nagdi-voucher-review" + "pension-survivor-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" + "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Farm" + "https://id.registrystack.org/solmara/concept/survivor-is-eligible" ] }, - "title": "Farmer and livestock registries evidence offering" + "title": "Social Insurance and Pensions Fund Survivor benefit v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nagdi-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, "dataset": "nagdi-agriculture", - "description": "Livestock movement-control predicates for permit checks.", - "entity": "livestock_movement", - "evidence_type": "livestock-movement-evidence", - "id": "nagdi-agriculture-livestock-movement-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-livestock-movement-offering", + "description": "An independently signed Relay lookup assertion issued by National Agricultural Data Institute.", + "entity": "farmer_voucher", + "evidence_type": "climate-smart-voucher-v1", + "id": "nagdi-voucher-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-voucher-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "nagdi", @@ -395,67 +487,67 @@ "lookup_keys": [ "farmer_id" ], - "policy": "nagdi-agriculture-livestock-movement-offering-policy", + "policy": "nagdi-voucher-v1-offering-policy", "public_services": [ - "livestock-movement-control" + "agriculture-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/livestock-movement-control" + "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/livestock-type" + "https://id.registrystack.org/solmara/concept/farmer-registered", + "https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose", + "https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher" ] }, - "title": "NAgDI livestock movement evidence offering" + "title": "National Agricultural Data Institute Climate smart voucher v1" }, { "access": { - "conforms_to": "https://id.registrystack.org/solmara/contracts/authority-predicate-collection/v1", - "discovery_url": "https://child-benefit-federator.solmara.registrystack.org/v1/claims", - "endpoint_url": "https://child-benefit-federator.solmara.registrystack.org/v1/evaluations", - "kind": "authority-predicate-collection-api", - "media_type": "application/json", - "ruleset": "source-owned-child-benefit-predicates-v1" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, - "dataset": "mosd-programme", - "description": "A transient collection of source-owned child benefit predicates. It contains no copied source rows and no composed eligibility decision.", - "entity": "enrollment", - "evidence_type": "child-benefit-authority-predicate-collection-evidence", - "id": "solmara.child-benefit.authority-predicate-collection", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/child-benefit-authority-predicate-collection", + "dataset": "nagdi-agriculture", + "description": "An independently signed Relay lookup assertion issued by National Agricultural Data Institute.", + "entity": "livestock_movement", + "evidence_type": "livestock-movement-v1", + "id": "nagdi-livestock-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-livestock-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", - "id": "mosd-programme-mis", - "iri": "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", - "name": "Ministry of Social Development Programme MIS", + "id": "nagdi", + "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", + "name": "National Agricultural Data Institute", "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" }, "lookup_keys": [ - "uin" + "farmer_id" ], - "policy": "solmara-child-benefit-authority-predicate-collection-policy", + "policy": "nagdi-livestock-v1-offering-policy", "public_services": [ - "child-benefit-review" + "agriculture-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" + "https://id.registrystack.org/solmara/purpose/livestock-movement-control" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Person", - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/Household", - "https://publicschema.org/sp/Enrollment" + "https://id.registrystack.org/solmara/concept/registered-herd", + "https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species", + "https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit" ] }, - "title": "Child Benefit Authority Predicate Collection" + "title": "National Agricultural Data Institute Livestock movement v1" } ] } diff --git a/metadata/public/metadata/dcat.jsonld b/metadata/public/metadata/dcat.jsonld index cfe1c09..2001947 100644 --- a/metadata/public/metadata/dcat.jsonld +++ b/metadata/public/metadata/dcat.jsonld @@ -8,29 +8,29 @@ "dcat:dataset": [ { "@id": "https://id.registrystack.org/solmara/datasets/cra-civil", - "dct:title": "Civil registration" + "dct:title": "CRA civil registration publication" }, { "@id": "https://id.registrystack.org/solmara/datasets/nia-population", - "dct:title": "Population register" + "dct:title": "NIA population publication" }, { "@id": "https://id.registrystack.org/solmara/datasets/sro-social", - "dct:title": "Social registry" + "dct:title": "SRO poverty extract" }, { "@id": "https://id.registrystack.org/solmara/datasets/mosd-programme", - "dct:title": "Integrated beneficiary registry" + "dct:title": "MoSD programme enrolment publication" }, { "@id": "https://id.registrystack.org/solmara/datasets/sipf-pensions", - "dct:title": "Pensions and social insurance" + "dct:title": "SIPF pension and survivor publication" }, { "@id": "https://id.registrystack.org/solmara/datasets/nagdi-agriculture", - "dct:title": "Farmer and livestock registries" + "dct:title": "NAgDI agriculture publication" } ], - "dct:description": "One merged CPSV-AP metadata publish for Solmara Wave 1 registry and evidence services.", - "dct:title": "Solmara Lab Wave 1 Service Catalogue" + "dct:description": "Six authority-owned Evidence cells, five governed Relay V2 services, and the programme applications that compose signed assertions.", + "dct:title": "Solmara authority-owned Evidence catalogue" } diff --git a/metadata/public/metadata/evidence-offerings.json b/metadata/public/metadata/evidence-offerings.json index ceb4c8f..b1bc293 100644 --- a/metadata/public/metadata/evidence-offerings.json +++ b/metadata/public/metadata/evidence-offerings.json @@ -2,18 +2,18 @@ "offerings": [ { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://cra-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://cra-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", "kind": "evidence-verification-api", - "ruleset": "birth-is-registered" + "source_type": "immutable extract" }, "dataset": "cra-civil", - "description": "Purpose-limited evidence predicates from Civil registration.", + "description": "An independently signed immutable extract assertion issued by Civil Registration Authority.", "entity": "civil_person", - "evidence_type": "birth-registration-evidence", - "id": "cra-birth-registration-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-birth-registration", + "evidence_type": "cra-child-benefit-v1", + "id": "cra-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "cra", @@ -24,7 +24,7 @@ "lookup_keys": [ "uin" ], - "policy": "cra-birth-registration-offering-policy", + "policy": "cra-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -36,26 +36,26 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/crvs/Death" + "https://id.registrystack.org/solmara/concept/birth-is-registered", + "https://id.registrystack.org/solmara/concept/child-age-under-5" ] }, - "title": "CRA birth registration evidence offering" + "title": "Civil Registration Authority Cra child benefit v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://cra-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://cra-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", "kind": "evidence-verification-api", - "ruleset": "person-is-deceased" + "source_type": "Relay lookup" }, "dataset": "cra-civil", - "description": "Purpose-limited evidence predicates from Civil registration.", + "description": "An independently signed Relay lookup assertion issued by Civil Registration Authority.", "entity": "civil_person", - "evidence_type": "death-registration-evidence", - "id": "cra-death-registration-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-death-registration", + "evidence_type": "cra-death-status-v1", + "id": "cra-pension-death-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-pension-death-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "cra", @@ -66,7 +66,7 @@ "lookup_keys": [ "uin" ], - "policy": "cra-death-registration-offering-policy", + "policy": "cra-pension-death-v1-offering-policy", "public_services": [ "pension-survivor-review" ], @@ -78,25 +78,66 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/crvs/Death" + "https://id.registrystack.org/solmara/concept/person-is-deceased" ] }, - "title": "CRA death registration evidence offering" + "title": "Civil Registration Authority Cra death status v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nia-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nia-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" + }, + "dataset": "cra-civil", + "description": "An independently signed Relay lookup assertion issued by Civil Registration Authority.", + "entity": "civil_person", + "evidence_type": "civil-record-linked-v1", + "id": "cra-citizen-record-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-citizen-record-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "cra", + "iri": "did:web:id.registrystack.org:solmara:authority:cra", + "name": "Civil Registration Authority", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "cra-citizen-record-v1-offering-policy", + "public_services": [ + "citizen-self-service" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/civil-record-linked" + ] + }, + "title": "Civil Registration Authority Civil record linked v1" + }, + { + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" }, "dataset": "nia-population", - "description": "Purpose-limited evidence predicates from Population register.", - "entity": "person", - "evidence_type": "population-status-evidence", - "id": "nia-population-population-status-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-population-population-status-offering", + "description": "An independently signed immutable extract assertion issued by National Identity Agency.", + "entity": "population_person", + "evidence_type": "population-active-v1", + "id": "nia-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "nia", @@ -107,7 +148,7 @@ "lookup_keys": [ "uin" ], - "policy": "nia-population-population-status-offering-policy", + "policy": "nia-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -119,24 +160,66 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Person" + "https://id.registrystack.org/solmara/concept/population-record-active" + ] + }, + "title": "National Identity Agency Population active v1" + }, + { + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" + }, + "dataset": "nia-population", + "description": "An independently signed immutable extract assertion issued by National Identity Agency.", + "entity": "population_person", + "evidence_type": "citizen-population-active-v1", + "id": "nia-citizen-status-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-citizen-status-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "nia", + "iri": "did:web:id.registrystack.org:solmara:authority:nia", + "name": "National Identity Agency", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "nia-citizen-status-v1-offering-policy", + "public_services": [ + "citizen-self-service" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/citizen-population-record-active" ] }, - "title": "Population register evidence offering" + "title": "National Identity Agency Citizen population active v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://sro-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://sro-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sro-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sro-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" }, "dataset": "sro-social", - "description": "Purpose-limited evidence predicates from Social registry.", - "entity": "household", - "evidence_type": "household-poverty-evidence", - "id": "sro-social-household-poverty-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/sro-social-household-poverty-offering", + "description": "An independently signed immutable extract assertion issued by Social Registry Office.", + "entity": "poverty_record", + "evidence_type": "poverty-priority-v1", + "id": "sro-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sro-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "sro", @@ -147,7 +230,7 @@ "lookup_keys": [ "uin" ], - "policy": "sro-social-household-poverty-offering-policy", + "policy": "sro-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -159,25 +242,25 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Household", - "https://publicschema.org/SocioEconomicProfile" + "https://id.registrystack.org/solmara/concept/household-below-poverty-threshold" ] }, - "title": "Social registry evidence offering" + "title": "Social Registry Office Poverty priority v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://programme-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://programme-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, "dataset": "mosd-programme", - "description": "Purpose-limited evidence predicates from Integrated beneficiary registry.", - "entity": "enrollment", - "evidence_type": "beneficiary-enrollment-evidence", - "id": "mosd-programme-beneficiary-enrollment-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/mosd-programme-beneficiary-enrollment-offering", + "description": "An independently signed Relay lookup assertion issued by Ministry of Social Development Programme MIS.", + "entity": "beneficiary_enrolment", + "evidence_type": "not-enrolled-v1", + "id": "mosd-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/mosd-child-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "mosd-programme-mis", @@ -188,7 +271,7 @@ "lookup_keys": [ "uin" ], - "policy": "mosd-programme-beneficiary-enrollment-offering-policy", + "policy": "mosd-child-benefit-v1-offering-policy", "public_services": [ "child-benefit-review" ], @@ -200,24 +283,25 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/sp/Enrollment" + "https://id.registrystack.org/solmara/concept/not-already-enrolled" ] }, - "title": "Integrated beneficiary registry evidence offering" + "title": "Ministry of Social Development Programme MIS Not enrolled v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://sipf-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://sipf-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, "dataset": "sipf-pensions", - "description": "Purpose-limited evidence predicates from Pensions and social insurance.", - "entity": "pension_case", - "evidence_type": "pension-case-evidence", - "id": "sipf-pensions-pension-case-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pensions-pension-case-offering", + "description": "An independently signed Relay lookup assertion issued by Social Insurance and Pensions Fund.", + "entity": "pension_payment", + "evidence_type": "pension-payment-active-v1", + "id": "sipf-pension-payment-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pension-payment-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "sipf", @@ -226,79 +310,80 @@ "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" }, "lookup_keys": [ - "uin" + "pensioner_uin" ], - "policy": "sipf-pensions-pension-case-offering-policy", + "policy": "sipf-pension-payment-v1-offering-policy", "public_services": [ "pension-survivor-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/pension-payment-review", - "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" + "https://id.registrystack.org/solmara/purpose/pension-payment-review" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://id.registrystack.org/solmara/semantics/pension-case" + "https://id.registrystack.org/solmara/concept/pension-payment-active" ] }, - "title": "Pensions and social insurance evidence offering" + "title": "Social Insurance and Pensions Fund Pension payment active v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nagdi-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, - "dataset": "nagdi-agriculture", - "description": "Purpose-limited evidence predicates from Farmer and livestock registries.", - "entity": "farmer_voucher", - "evidence_type": "farmer-voucher-evidence", - "id": "nagdi-agriculture-farmer-voucher-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-farmer-voucher-offering", + "dataset": "sipf-pensions", + "description": "An independently signed Relay lookup assertion issued by Social Insurance and Pensions Fund.", + "entity": "survivor_case", + "evidence_type": "survivor-benefit-v1", + "id": "sipf-survivor-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-survivor-benefit-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", - "id": "nagdi", - "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", - "name": "National Agricultural Data Institute", + "id": "sipf", + "iri": "did:web:id.registrystack.org:solmara:authority:sipf", + "name": "Social Insurance and Pensions Fund", "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" }, "lookup_keys": [ - "farmer_id" + "spouse_uin" ], - "policy": "nagdi-agriculture-farmer-voucher-offering-policy", + "policy": "sipf-survivor-benefit-v1-offering-policy", "public_services": [ - "nagdi-voucher-review" + "pension-survivor-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" + "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Farm" + "https://id.registrystack.org/solmara/concept/survivor-is-eligible" ] }, - "title": "Farmer and livestock registries evidence offering" + "title": "Social Insurance and Pensions Fund Survivor benefit v1" }, { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nagdi-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, "dataset": "nagdi-agriculture", - "description": "Livestock movement-control predicates for permit checks.", - "entity": "livestock_movement", - "evidence_type": "livestock-movement-evidence", - "id": "nagdi-agriculture-livestock-movement-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-livestock-movement-offering", + "description": "An independently signed Relay lookup assertion issued by National Agricultural Data Institute.", + "entity": "farmer_voucher", + "evidence_type": "climate-smart-voucher-v1", + "id": "nagdi-voucher-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-voucher-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "nagdi", @@ -309,67 +394,67 @@ "lookup_keys": [ "farmer_id" ], - "policy": "nagdi-agriculture-livestock-movement-offering-policy", + "policy": "nagdi-voucher-v1-offering-policy", "public_services": [ - "livestock-movement-control" + "agriculture-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/livestock-movement-control" + "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/livestock-type" + "https://id.registrystack.org/solmara/concept/farmer-registered", + "https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose", + "https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher" ] }, - "title": "NAgDI livestock movement evidence offering" + "title": "National Agricultural Data Institute Climate smart voucher v1" }, { "access": { - "conforms_to": "https://id.registrystack.org/solmara/contracts/authority-predicate-collection/v1", - "discovery_url": "https://child-benefit-federator.solmara.registrystack.org/v1/claims", - "endpoint_url": "https://child-benefit-federator.solmara.registrystack.org/v1/evaluations", - "kind": "authority-predicate-collection-api", - "media_type": "application/json", - "ruleset": "source-owned-child-benefit-predicates-v1" + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" }, - "dataset": "mosd-programme", - "description": "A transient collection of source-owned child benefit predicates. It contains no copied source rows and no composed eligibility decision.", - "entity": "enrollment", - "evidence_type": "child-benefit-authority-predicate-collection-evidence", - "id": "solmara.child-benefit.authority-predicate-collection", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/child-benefit-authority-predicate-collection", + "dataset": "nagdi-agriculture", + "description": "An independently signed Relay lookup assertion issued by National Agricultural Data Institute.", + "entity": "livestock_movement", + "evidence_type": "livestock-movement-v1", + "id": "nagdi-livestock-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-livestock-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", - "id": "mosd-programme-mis", - "iri": "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", - "name": "Ministry of Social Development Programme MIS", + "id": "nagdi", + "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", + "name": "National Agricultural Data Institute", "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" }, "lookup_keys": [ - "uin" + "farmer_id" ], - "policy": "solmara-child-benefit-authority-predicate-collection-policy", + "policy": "nagdi-livestock-v1-offering-policy", "public_services": [ - "child-benefit-review" + "agriculture-review" ], "purposes": [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" + "https://id.registrystack.org/solmara/purpose/livestock-movement-control" ], "semantics": { "application_profiles": [ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/Person", - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/Household", - "https://publicschema.org/sp/Enrollment" + "https://id.registrystack.org/solmara/concept/registered-herd", + "https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species", + "https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit" ] }, - "title": "Child Benefit Authority Predicate Collection" + "title": "National Agricultural Data Institute Livestock movement v1" } ], "schema_version": "registry-manifest-evidence-offerings/v1" diff --git a/metadata/public/metadata/evidence-offerings/cra-birth-registration-offering.json b/metadata/public/metadata/evidence-offerings/cra-birth-registration-offering.json deleted file mode 100644 index 70700bb..0000000 --- a/metadata/public/metadata/evidence-offerings/cra-birth-registration-offering.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://cra-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://cra-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api", - "ruleset": "birth-is-registered" - }, - "dataset": "cra-civil", - "description": "Purpose-limited evidence predicates from Civil registration.", - "entity": "civil_person", - "evidence_type": "birth-registration-evidence", - "id": "cra-birth-registration-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-birth-registration", - "issuing_authority": { - "authority_type": "eli:PublicAuthority", - "id": "cra", - "iri": "did:web:id.registrystack.org:solmara:authority:cra", - "name": "Civil Registration Authority", - "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" - }, - "lookup_keys": [ - "uin" - ], - "policy": "cra-birth-registration-offering-policy", - "public_services": [ - "child-benefit-review" - ], - "purposes": [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" - ], - "semantics": { - "application_profiles": [ - "cpsv-ap" - ], - "concepts": [ - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/crvs/Death" - ] - }, - "title": "CRA birth registration evidence offering" -} diff --git a/metadata/public/metadata/evidence-offerings/cra-child-benefit-v1-offering.json b/metadata/public/metadata/evidence-offerings/cra-child-benefit-v1-offering.json new file mode 100644 index 0000000..aad2fb3 --- /dev/null +++ b/metadata/public/metadata/evidence-offerings/cra-child-benefit-v1-offering.json @@ -0,0 +1,42 @@ +{ + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" + }, + "dataset": "cra-civil", + "description": "An independently signed immutable extract assertion issued by Civil Registration Authority.", + "entity": "civil_person", + "evidence_type": "cra-child-benefit-v1", + "id": "cra-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-child-benefit-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "cra", + "iri": "did:web:id.registrystack.org:solmara:authority:cra", + "name": "Civil Registration Authority", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "cra-child-benefit-v1-offering-policy", + "public_services": [ + "child-benefit-review" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/child-benefit-review" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/birth-is-registered", + "https://id.registrystack.org/solmara/concept/child-age-under-5" + ] + }, + "title": "Civil Registration Authority Cra child benefit v1" +} diff --git a/metadata/public/metadata/evidence-offerings/cra-citizen-record-v1-offering.json b/metadata/public/metadata/evidence-offerings/cra-citizen-record-v1-offering.json new file mode 100644 index 0000000..c79ef22 --- /dev/null +++ b/metadata/public/metadata/evidence-offerings/cra-citizen-record-v1-offering.json @@ -0,0 +1,41 @@ +{ + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" + }, + "dataset": "cra-civil", + "description": "An independently signed Relay lookup assertion issued by Civil Registration Authority.", + "entity": "civil_person", + "evidence_type": "civil-record-linked-v1", + "id": "cra-citizen-record-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-citizen-record-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "cra", + "iri": "did:web:id.registrystack.org:solmara:authority:cra", + "name": "Civil Registration Authority", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "cra-citizen-record-v1-offering-policy", + "public_services": [ + "citizen-self-service" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/civil-record-linked" + ] + }, + "title": "Civil Registration Authority Civil record linked v1" +} diff --git a/metadata/public/metadata/evidence-offerings/cra-death-registration-offering.json b/metadata/public/metadata/evidence-offerings/cra-pension-death-v1-offering.json similarity index 51% rename from metadata/public/metadata/evidence-offerings/cra-death-registration-offering.json rename to metadata/public/metadata/evidence-offerings/cra-pension-death-v1-offering.json index 41806f1..f92b0ff 100644 --- a/metadata/public/metadata/evidence-offerings/cra-death-registration-offering.json +++ b/metadata/public/metadata/evidence-offerings/cra-pension-death-v1-offering.json @@ -1,17 +1,17 @@ { "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://cra-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://cra-notary.solmara.registrystack.org/v1/evaluations", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://cra-evidence.solmara.registrystack.org/v1/evidence", "kind": "evidence-verification-api", - "ruleset": "person-is-deceased" + "source_type": "Relay lookup" }, "dataset": "cra-civil", - "description": "Purpose-limited evidence predicates from Civil registration.", + "description": "An independently signed Relay lookup assertion issued by Civil Registration Authority.", "entity": "civil_person", - "evidence_type": "death-registration-evidence", - "id": "cra-death-registration-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-death-registration", + "evidence_type": "cra-death-status-v1", + "id": "cra-pension-death-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/cra-pension-death-v1-offering", "issuing_authority": { "authority_type": "eli:PublicAuthority", "id": "cra", @@ -22,7 +22,7 @@ "lookup_keys": [ "uin" ], - "policy": "cra-death-registration-offering-policy", + "policy": "cra-pension-death-v1-offering-policy", "public_services": [ "pension-survivor-review" ], @@ -34,9 +34,8 @@ "cpsv-ap" ], "concepts": [ - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/crvs/Death" + "https://id.registrystack.org/solmara/concept/person-is-deceased" ] }, - "title": "CRA death registration evidence offering" + "title": "Civil Registration Authority Cra death status v1" } diff --git a/metadata/public/metadata/evidence-offerings/mosd-child-benefit-v1-offering.json b/metadata/public/metadata/evidence-offerings/mosd-child-benefit-v1-offering.json new file mode 100644 index 0000000..c3995c0 --- /dev/null +++ b/metadata/public/metadata/evidence-offerings/mosd-child-benefit-v1-offering.json @@ -0,0 +1,41 @@ +{ + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" + }, + "dataset": "mosd-programme", + "description": "An independently signed Relay lookup assertion issued by Ministry of Social Development Programme MIS.", + "entity": "beneficiary_enrolment", + "evidence_type": "not-enrolled-v1", + "id": "mosd-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/mosd-child-benefit-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "mosd-programme-mis", + "iri": "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", + "name": "Ministry of Social Development Programme MIS", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "mosd-child-benefit-v1-offering-policy", + "public_services": [ + "child-benefit-review" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/child-benefit-review" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/not-already-enrolled" + ] + }, + "title": "Ministry of Social Development Programme MIS Not enrolled v1" +} diff --git a/metadata/public/metadata/evidence-offerings/mosd-programme-beneficiary-enrollment-offering.json b/metadata/public/metadata/evidence-offerings/mosd-programme-beneficiary-enrollment-offering.json deleted file mode 100644 index b125b75..0000000 --- a/metadata/public/metadata/evidence-offerings/mosd-programme-beneficiary-enrollment-offering.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://programme-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://programme-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" - }, - "dataset": "mosd-programme", - "description": "Purpose-limited evidence predicates from Integrated beneficiary registry.", - "entity": "enrollment", - "evidence_type": "beneficiary-enrollment-evidence", - "id": "mosd-programme-beneficiary-enrollment-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/mosd-programme-beneficiary-enrollment-offering", - "issuing_authority": { - "authority_type": "eli:PublicAuthority", - "id": "mosd-programme-mis", - "iri": "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", - "name": "Ministry of Social Development Programme MIS", - "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" - }, - "lookup_keys": [ - "uin" - ], - "policy": "mosd-programme-beneficiary-enrollment-offering-policy", - "public_services": [ - "child-benefit-review" - ], - "purposes": [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" - ], - "semantics": { - "application_profiles": [ - "cpsv-ap" - ], - "concepts": [ - "https://publicschema.org/sp/Enrollment" - ] - }, - "title": "Integrated beneficiary registry evidence offering" -} diff --git a/metadata/public/metadata/evidence-offerings/nagdi-agriculture-farmer-voucher-offering.json b/metadata/public/metadata/evidence-offerings/nagdi-agriculture-farmer-voucher-offering.json deleted file mode 100644 index ad27091..0000000 --- a/metadata/public/metadata/evidence-offerings/nagdi-agriculture-farmer-voucher-offering.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nagdi-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" - }, - "dataset": "nagdi-agriculture", - "description": "Purpose-limited evidence predicates from Farmer and livestock registries.", - "entity": "farmer_voucher", - "evidence_type": "farmer-voucher-evidence", - "id": "nagdi-agriculture-farmer-voucher-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-farmer-voucher-offering", - "issuing_authority": { - "authority_type": "eli:PublicAuthority", - "id": "nagdi", - "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", - "name": "National Agricultural Data Institute", - "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" - }, - "lookup_keys": [ - "farmer_id" - ], - "policy": "nagdi-agriculture-farmer-voucher-offering-policy", - "public_services": [ - "nagdi-voucher-review" - ], - "purposes": [ - "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" - ], - "semantics": { - "application_profiles": [ - "cpsv-ap" - ], - "concepts": [ - "https://publicschema.org/Farm" - ] - }, - "title": "Farmer and livestock registries evidence offering" -} diff --git a/metadata/public/metadata/evidence-offerings/nagdi-agriculture-livestock-movement-offering.json b/metadata/public/metadata/evidence-offerings/nagdi-agriculture-livestock-movement-offering.json deleted file mode 100644 index e5ce172..0000000 --- a/metadata/public/metadata/evidence-offerings/nagdi-agriculture-livestock-movement-offering.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nagdi-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" - }, - "dataset": "nagdi-agriculture", - "description": "Livestock movement-control predicates for permit checks.", - "entity": "livestock_movement", - "evidence_type": "livestock-movement-evidence", - "id": "nagdi-agriculture-livestock-movement-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-livestock-movement-offering", - "issuing_authority": { - "authority_type": "eli:PublicAuthority", - "id": "nagdi", - "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", - "name": "National Agricultural Data Institute", - "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" - }, - "lookup_keys": [ - "farmer_id" - ], - "policy": "nagdi-agriculture-livestock-movement-offering-policy", - "public_services": [ - "livestock-movement-control" - ], - "purposes": [ - "https://id.registrystack.org/solmara/purpose/livestock-movement-control" - ], - "semantics": { - "application_profiles": [ - "cpsv-ap" - ], - "concepts": [ - "https://publicschema.org/livestock-type" - ] - }, - "title": "NAgDI livestock movement evidence offering" -} diff --git a/metadata/public/metadata/evidence-offerings/nagdi-livestock-v1-offering.json b/metadata/public/metadata/evidence-offerings/nagdi-livestock-v1-offering.json new file mode 100644 index 0000000..15b9358 --- /dev/null +++ b/metadata/public/metadata/evidence-offerings/nagdi-livestock-v1-offering.json @@ -0,0 +1,43 @@ +{ + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" + }, + "dataset": "nagdi-agriculture", + "description": "An independently signed Relay lookup assertion issued by National Agricultural Data Institute.", + "entity": "livestock_movement", + "evidence_type": "livestock-movement-v1", + "id": "nagdi-livestock-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-livestock-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "nagdi", + "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", + "name": "National Agricultural Data Institute", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "farmer_id" + ], + "policy": "nagdi-livestock-v1-offering-policy", + "public_services": [ + "agriculture-review" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/livestock-movement-control" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/registered-herd", + "https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species", + "https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit" + ] + }, + "title": "National Agricultural Data Institute Livestock movement v1" +} diff --git a/metadata/public/metadata/evidence-offerings/nagdi-voucher-v1-offering.json b/metadata/public/metadata/evidence-offerings/nagdi-voucher-v1-offering.json new file mode 100644 index 0000000..03e5f91 --- /dev/null +++ b/metadata/public/metadata/evidence-offerings/nagdi-voucher-v1-offering.json @@ -0,0 +1,43 @@ +{ + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nagdi-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" + }, + "dataset": "nagdi-agriculture", + "description": "An independently signed Relay lookup assertion issued by National Agricultural Data Institute.", + "entity": "farmer_voucher", + "evidence_type": "climate-smart-voucher-v1", + "id": "nagdi-voucher-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-voucher-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "nagdi", + "iri": "did:web:id.registrystack.org:solmara:authority:nagdi", + "name": "National Agricultural Data Institute", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "farmer_id" + ], + "policy": "nagdi-voucher-v1-offering-policy", + "public_services": [ + "agriculture-review" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/farmer-registered", + "https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose", + "https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher" + ] + }, + "title": "National Agricultural Data Institute Climate smart voucher v1" +} diff --git a/metadata/public/metadata/evidence-offerings/nia-child-benefit-v1-offering.json b/metadata/public/metadata/evidence-offerings/nia-child-benefit-v1-offering.json new file mode 100644 index 0000000..f3024e5 --- /dev/null +++ b/metadata/public/metadata/evidence-offerings/nia-child-benefit-v1-offering.json @@ -0,0 +1,41 @@ +{ + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" + }, + "dataset": "nia-population", + "description": "An independently signed immutable extract assertion issued by National Identity Agency.", + "entity": "population_person", + "evidence_type": "population-active-v1", + "id": "nia-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-child-benefit-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "nia", + "iri": "did:web:id.registrystack.org:solmara:authority:nia", + "name": "National Identity Agency", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "nia-child-benefit-v1-offering-policy", + "public_services": [ + "child-benefit-review" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/child-benefit-review" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/population-record-active" + ] + }, + "title": "National Identity Agency Population active v1" +} diff --git a/metadata/public/metadata/evidence-offerings/nia-citizen-status-v1-offering.json b/metadata/public/metadata/evidence-offerings/nia-citizen-status-v1-offering.json new file mode 100644 index 0000000..dc9e703 --- /dev/null +++ b/metadata/public/metadata/evidence-offerings/nia-citizen-status-v1-offering.json @@ -0,0 +1,41 @@ +{ + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://nia-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" + }, + "dataset": "nia-population", + "description": "An independently signed immutable extract assertion issued by National Identity Agency.", + "entity": "population_person", + "evidence_type": "citizen-population-active-v1", + "id": "nia-citizen-status-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-citizen-status-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "nia", + "iri": "did:web:id.registrystack.org:solmara:authority:nia", + "name": "National Identity Agency", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "nia-citizen-status-v1-offering-policy", + "public_services": [ + "citizen-self-service" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/citizen-population-record-active" + ] + }, + "title": "National Identity Agency Citizen population active v1" +} diff --git a/metadata/public/metadata/evidence-offerings/nia-population-population-status-offering.json b/metadata/public/metadata/evidence-offerings/nia-population-population-status-offering.json deleted file mode 100644 index de06c1c..0000000 --- a/metadata/public/metadata/evidence-offerings/nia-population-population-status-offering.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://nia-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://nia-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" - }, - "dataset": "nia-population", - "description": "Purpose-limited evidence predicates from Population register.", - "entity": "person", - "evidence_type": "population-status-evidence", - "id": "nia-population-population-status-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/nia-population-population-status-offering", - "issuing_authority": { - "authority_type": "eli:PublicAuthority", - "id": "nia", - "iri": "did:web:id.registrystack.org:solmara:authority:nia", - "name": "National Identity Agency", - "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" - }, - "lookup_keys": [ - "uin" - ], - "policy": "nia-population-population-status-offering-policy", - "public_services": [ - "child-benefit-review" - ], - "purposes": [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" - ], - "semantics": { - "application_profiles": [ - "cpsv-ap" - ], - "concepts": [ - "https://publicschema.org/Person" - ] - }, - "title": "Population register evidence offering" -} diff --git a/metadata/public/metadata/evidence-offerings/sipf-pension-payment-v1-offering.json b/metadata/public/metadata/evidence-offerings/sipf-pension-payment-v1-offering.json new file mode 100644 index 0000000..10b76b6 --- /dev/null +++ b/metadata/public/metadata/evidence-offerings/sipf-pension-payment-v1-offering.json @@ -0,0 +1,41 @@ +{ + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" + }, + "dataset": "sipf-pensions", + "description": "An independently signed Relay lookup assertion issued by Social Insurance and Pensions Fund.", + "entity": "pension_payment", + "evidence_type": "pension-payment-active-v1", + "id": "sipf-pension-payment-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pension-payment-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "sipf", + "iri": "did:web:id.registrystack.org:solmara:authority:sipf", + "name": "Social Insurance and Pensions Fund", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "pensioner_uin" + ], + "policy": "sipf-pension-payment-v1-offering-policy", + "public_services": [ + "pension-survivor-review" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/pension-payment-review" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/pension-payment-active" + ] + }, + "title": "Social Insurance and Pensions Fund Pension payment active v1" +} diff --git a/metadata/public/metadata/evidence-offerings/sipf-pensions-pension-case-offering.json b/metadata/public/metadata/evidence-offerings/sipf-pensions-pension-case-offering.json deleted file mode 100644 index 5388236..0000000 --- a/metadata/public/metadata/evidence-offerings/sipf-pensions-pension-case-offering.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://sipf-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://sipf-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" - }, - "dataset": "sipf-pensions", - "description": "Purpose-limited evidence predicates from Pensions and social insurance.", - "entity": "pension_case", - "evidence_type": "pension-case-evidence", - "id": "sipf-pensions-pension-case-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pensions-pension-case-offering", - "issuing_authority": { - "authority_type": "eli:PublicAuthority", - "id": "sipf", - "iri": "did:web:id.registrystack.org:solmara:authority:sipf", - "name": "Social Insurance and Pensions Fund", - "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" - }, - "lookup_keys": [ - "uin" - ], - "policy": "sipf-pensions-pension-case-offering-policy", - "public_services": [ - "pension-survivor-review" - ], - "purposes": [ - "https://id.registrystack.org/solmara/purpose/pension-payment-review", - "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" - ], - "semantics": { - "application_profiles": [ - "cpsv-ap" - ], - "concepts": [ - "https://id.registrystack.org/solmara/semantics/pension-case" - ] - }, - "title": "Pensions and social insurance evidence offering" -} diff --git a/metadata/public/metadata/evidence-offerings/sipf-survivor-benefit-v1-offering.json b/metadata/public/metadata/evidence-offerings/sipf-survivor-benefit-v1-offering.json new file mode 100644 index 0000000..9961793 --- /dev/null +++ b/metadata/public/metadata/evidence-offerings/sipf-survivor-benefit-v1-offering.json @@ -0,0 +1,41 @@ +{ + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sipf-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "Relay lookup" + }, + "dataset": "sipf-pensions", + "description": "An independently signed Relay lookup assertion issued by Social Insurance and Pensions Fund.", + "entity": "survivor_case", + "evidence_type": "survivor-benefit-v1", + "id": "sipf-survivor-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sipf-survivor-benefit-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "sipf", + "iri": "did:web:id.registrystack.org:solmara:authority:sipf", + "name": "Social Insurance and Pensions Fund", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "spouse_uin" + ], + "policy": "sipf-survivor-benefit-v1-offering-policy", + "public_services": [ + "pension-survivor-review" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/survivor-is-eligible" + ] + }, + "title": "Social Insurance and Pensions Fund Survivor benefit v1" +} diff --git a/metadata/public/metadata/evidence-offerings/solmara.child-benefit.authority-predicate-collection.json b/metadata/public/metadata/evidence-offerings/solmara.child-benefit.authority-predicate-collection.json deleted file mode 100644 index f9b5d50..0000000 --- a/metadata/public/metadata/evidence-offerings/solmara.child-benefit.authority-predicate-collection.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "access": { - "conforms_to": "https://id.registrystack.org/solmara/contracts/authority-predicate-collection/v1", - "discovery_url": "https://child-benefit-federator.solmara.registrystack.org/v1/claims", - "endpoint_url": "https://child-benefit-federator.solmara.registrystack.org/v1/evaluations", - "kind": "authority-predicate-collection-api", - "media_type": "application/json", - "ruleset": "source-owned-child-benefit-predicates-v1" - }, - "dataset": "mosd-programme", - "description": "A transient collection of source-owned child benefit predicates. It contains no copied source rows and no composed eligibility decision.", - "entity": "enrollment", - "evidence_type": "child-benefit-authority-predicate-collection-evidence", - "id": "solmara.child-benefit.authority-predicate-collection", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/child-benefit-authority-predicate-collection", - "issuing_authority": { - "authority_type": "eli:PublicAuthority", - "id": "mosd-programme-mis", - "iri": "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", - "name": "Ministry of Social Development Programme MIS", - "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" - }, - "lookup_keys": [ - "uin" - ], - "policy": "solmara-child-benefit-authority-predicate-collection-policy", - "public_services": [ - "child-benefit-review" - ], - "purposes": [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" - ], - "semantics": { - "application_profiles": [ - "cpsv-ap" - ], - "concepts": [ - "https://publicschema.org/Person", - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/Household", - "https://publicschema.org/sp/Enrollment" - ] - }, - "title": "Child Benefit Authority Predicate Collection" -} diff --git a/metadata/public/metadata/evidence-offerings/sro-child-benefit-v1-offering.json b/metadata/public/metadata/evidence-offerings/sro-child-benefit-v1-offering.json new file mode 100644 index 0000000..906d5cb --- /dev/null +++ b/metadata/public/metadata/evidence-offerings/sro-child-benefit-v1-offering.json @@ -0,0 +1,41 @@ +{ + "access": { + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "discovery_url": "https://sro-evidence.solmara.registrystack.org/v1/evidence-definitions", + "endpoint_url": "https://sro-evidence.solmara.registrystack.org/v1/evidence", + "kind": "evidence-verification-api", + "source_type": "immutable extract" + }, + "dataset": "sro-social", + "description": "An independently signed immutable extract assertion issued by Social Registry Office.", + "entity": "poverty_record", + "evidence_type": "poverty-priority-v1", + "id": "sro-child-benefit-v1-offering", + "iri": "https://id.registrystack.org/solmara/evidence-offerings/sro-child-benefit-v1-offering", + "issuing_authority": { + "authority_type": "eli:PublicAuthority", + "id": "sro", + "iri": "did:web:id.registrystack.org:solmara:authority:sro", + "name": "Social Registry Office", + "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" + }, + "lookup_keys": [ + "uin" + ], + "policy": "sro-child-benefit-v1-offering-policy", + "public_services": [ + "child-benefit-review" + ], + "purposes": [ + "https://id.registrystack.org/solmara/purpose/child-benefit-review" + ], + "semantics": { + "application_profiles": [ + "cpsv-ap" + ], + "concepts": [ + "https://id.registrystack.org/solmara/concept/household-below-poverty-threshold" + ] + }, + "title": "Social Registry Office Poverty priority v1" +} diff --git a/metadata/public/metadata/evidence-offerings/sro-social-household-poverty-offering.json b/metadata/public/metadata/evidence-offerings/sro-social-household-poverty-offering.json deleted file mode 100644 index 1a05692..0000000 --- a/metadata/public/metadata/evidence-offerings/sro-social-household-poverty-offering.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "access": { - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "discovery_url": "https://sro-notary.solmara.registrystack.org/.well-known/evidence-service", - "endpoint_url": "https://sro-notary.solmara.registrystack.org/v1/evaluations", - "kind": "evidence-verification-api" - }, - "dataset": "sro-social", - "description": "Purpose-limited evidence predicates from Social registry.", - "entity": "household", - "evidence_type": "household-poverty-evidence", - "id": "sro-social-household-poverty-offering", - "iri": "https://id.registrystack.org/solmara/evidence-offerings/sro-social-household-poverty-offering", - "issuing_authority": { - "authority_type": "eli:PublicAuthority", - "id": "sro", - "iri": "did:web:id.registrystack.org:solmara:authority:sro", - "name": "Social Registry Office", - "spatial": "https://id.registrystack.org/solmara/jurisdiction/XS" - }, - "lookup_keys": [ - "uin" - ], - "policy": "sro-social-household-poverty-offering-policy", - "public_services": [ - "child-benefit-review" - ], - "purposes": [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" - ], - "semantics": { - "application_profiles": [ - "cpsv-ap" - ], - "concepts": [ - "https://publicschema.org/Household", - "https://publicschema.org/SocioEconomicProfile" - ] - }, - "title": "Social registry evidence offering" -} diff --git a/metadata/public/metadata/index.json b/metadata/public/metadata/index.json index 135a6ce..7efa2ef 100644 --- a/metadata/public/metadata/index.json +++ b/metadata/public/metadata/index.json @@ -9,132 +9,152 @@ { "media_type": "application/json", "path": "metadata/catalog.json", - "sha256": "sha256:2db419cefb6b0659792ac1ceb803d21a8fafd54eb75eefdced7496fdc8a480f4" + "sha256": "sha256:49604351742db066f104f1e648e383228bd786c0b264c1a4370331f8dc9f3e6e" }, { "media_type": "application/json", "path": "metadata/cpsv-ap", - "sha256": "sha256:ddfccb6700668ba4753a0ddfe00e80f3c2cfc60192863f84e1f76cd4529aa01d" + "sha256": "sha256:845c0707695d38306a78d5a5bc7edf051ece9db6f940f1b873520ef63fc5a8b6" }, { "media_type": "application/ld+json", "path": "metadata/cpsv-ap.jsonld", - "sha256": "sha256:ddfccb6700668ba4753a0ddfe00e80f3c2cfc60192863f84e1f76cd4529aa01d" + "sha256": "sha256:845c0707695d38306a78d5a5bc7edf051ece9db6f940f1b873520ef63fc5a8b6" }, { "media_type": "application/ld+json", "path": "metadata/dcat.jsonld", - "sha256": "sha256:6f0e778b71be0f94dd97dc2d7e87ce5850a363efd0ebdfb99b745f2ab5848b92" + "sha256": "sha256:3747beabd9d30201a2065963cdeefeb45a1b26c9ae380d787f1da7d511e81d32" }, { "media_type": "application/json", "path": "metadata/evidence-offerings.json", - "sha256": "sha256:bb079ab2166568c5e175438da8e6fd37edb5dc48bc2a93f38f409f4445b0db89" + "sha256": "sha256:f3d995ca9ce9f2ed0b083094763560dd2f8ea606edaa7f66f89713fd057abd67" }, { "media_type": "application/json", - "path": "metadata/evidence-offerings/cra-birth-registration-offering.json", - "sha256": "sha256:519d0e195db9fe938057c7dd2c4819b31c237cb7a7da06228a1cab3fc287fafd" + "path": "metadata/evidence-offerings/cra-child-benefit-v1-offering.json", + "sha256": "sha256:4945af2a2ce39cd876464f36b7c461241a533c0a439139ed40cf3499578fc1a0" }, { "media_type": "application/json", - "path": "metadata/evidence-offerings/cra-death-registration-offering.json", - "sha256": "sha256:8231e762fefa587af31560c759916754d53a5f742ad923385fd214eeac6884f1" + "path": "metadata/evidence-offerings/cra-citizen-record-v1-offering.json", + "sha256": "sha256:323c53c57e38fcb0a1763fbcf599bf1d8a9eab269c2800bce66ced3ad78a168a" }, { "media_type": "application/json", - "path": "metadata/evidence-offerings/mosd-programme-beneficiary-enrollment-offering.json", - "sha256": "sha256:90a2f91491e0d32615016c663cba787fad9c225287af741b7dfa4fa0f2c94fef" + "path": "metadata/evidence-offerings/cra-pension-death-v1-offering.json", + "sha256": "sha256:6546f52ad662d882796ea27cbab3205caee415b66183ae3563dc55433539a451" }, { "media_type": "application/json", - "path": "metadata/evidence-offerings/nagdi-agriculture-farmer-voucher-offering.json", - "sha256": "sha256:4920058bf44aa821eb48c8723072ffac8d1e1691e4ca2028b596a401169b4286" + "path": "metadata/evidence-offerings/mosd-child-benefit-v1-offering.json", + "sha256": "sha256:9eb7f35fb2efbb3f7b221dd87fd9f99f62f8d45639b8e304b832b7c57c671802" }, { "media_type": "application/json", - "path": "metadata/evidence-offerings/nagdi-agriculture-livestock-movement-offering.json", - "sha256": "sha256:62a87e2b217ae731012a82d3c7e660e1430dbe0cfdbfe0711e16a049e4ec47d2" + "path": "metadata/evidence-offerings/nagdi-livestock-v1-offering.json", + "sha256": "sha256:26379f6e3780710b0cd9bb44081b0c674e5819086eaec49da12e6ab919c726be" }, { "media_type": "application/json", - "path": "metadata/evidence-offerings/nia-population-population-status-offering.json", - "sha256": "sha256:ce917bdb721637857697e7ce6f13dc2ddb4c4ed584a353da22cc253561cd2e42" + "path": "metadata/evidence-offerings/nagdi-voucher-v1-offering.json", + "sha256": "sha256:1cd41c0b7ad9c669dd5bc2668dfc64266b06dd6f6093bb4e386bc81109a59873" }, { "media_type": "application/json", - "path": "metadata/evidence-offerings/sipf-pensions-pension-case-offering.json", - "sha256": "sha256:75ba6b9f33fc855e83bd1459fe6ab229b8b624477c7f3d36c00ad97ca0abd930" + "path": "metadata/evidence-offerings/nia-child-benefit-v1-offering.json", + "sha256": "sha256:8829626e163fa2050505296b3b6d564f0348747ca7368d8418f834d6572e0d4c" }, { "media_type": "application/json", - "path": "metadata/evidence-offerings/solmara.child-benefit.authority-predicate-collection.json", - "sha256": "sha256:1b59dd02b2730d034b180123098768117eceaca0504e4c25846105e978956cfb" + "path": "metadata/evidence-offerings/nia-citizen-status-v1-offering.json", + "sha256": "sha256:1603eafad482dfc6ac8e7c9fe8523e3950d1b40ee836d43b9c360373555378c3" }, { "media_type": "application/json", - "path": "metadata/evidence-offerings/sro-social-household-poverty-offering.json", - "sha256": "sha256:b2593c8bfe333bcbffff3f5d91f9ce1930654ddc0db86209263b616e915d15c2" + "path": "metadata/evidence-offerings/sipf-pension-payment-v1-offering.json", + "sha256": "sha256:1b73e1397709a63fc9c7a5715797427e8c679ec4b6c4a770bf8be32f0c892ee7" + }, + { + "media_type": "application/json", + "path": "metadata/evidence-offerings/sipf-survivor-benefit-v1-offering.json", + "sha256": "sha256:07365e008affe512ea996059b85623e4026e7d225b9484d89059616ea09577fa" + }, + { + "media_type": "application/json", + "path": "metadata/evidence-offerings/sro-child-benefit-v1-offering.json", + "sha256": "sha256:078cfc456039f1ca832c5544eab3429f8477a9488d3a20b6ceb47da2403be97b" }, { "media_type": "application/yaml", "path": "metadata/metadata.yaml", - "sha256": "sha256:3961e5a3c7d19688039db461f8d8585e6b5d42a4c1c5a58cb759557829d30cc5" + "sha256": "sha256:4c765f6526a95a84d59485b94e1fd57f677df824b91ecd5a3b00651cf006d191" }, { "media_type": "application/json", "path": "metadata/ogc-records/items.json", - "sha256": "sha256:aeb387ef3c8821b1e9cd1b40b1720c080cb59e686aefa2c3654493e28cda0d33" + "sha256": "sha256:2b924e2f6fa8f95275ec0a175e165276866d75a4ab176b2c5a0453d3fae0cde7" }, { "media_type": "application/ld+json", "path": "metadata/policies.jsonld", - "sha256": "sha256:9f6b4698c3f2c2f3ba11ea79c64f7cc9c9809c1821c3cd65b35b563ad2537aaf" + "sha256": "sha256:4b4402dbd21c787c85d42fbad8919beeb170fd9abc3278c6974c2f9d758b5cba" + }, + { + "media_type": "application/ld+json", + "path": "metadata/policies/cra-child-benefit-v1-offering-policy.jsonld", + "sha256": "sha256:fd48f0c5c5801e1b1d0ba72322c1cbe801af46ca4232dbaa9c0ace3ca589ac08" + }, + { + "media_type": "application/ld+json", + "path": "metadata/policies/cra-citizen-record-v1-offering-policy.jsonld", + "sha256": "sha256:3580c537bba48557e4ab3f431f55f45cc94e2e558a3e9fc547c7bd255088f9bc" }, { "media_type": "application/ld+json", - "path": "metadata/policies/cra-birth-registration-offering-policy.jsonld", - "sha256": "sha256:5765930b8ba06eb475e1cbea73299bb218da725b0ad0d632d46d2772b0a557ee" + "path": "metadata/policies/cra-pension-death-v1-offering-policy.jsonld", + "sha256": "sha256:bb2ae74ceaa689b39edac3161e2e3f5d85fce0b90786202ce3424616b25943cc" }, { "media_type": "application/ld+json", - "path": "metadata/policies/cra-death-registration-offering-policy.jsonld", - "sha256": "sha256:91e8caf238fab79fcb995d2181d3c0d27486e2f0c8a865891f0196d653a850cd" + "path": "metadata/policies/mosd-child-benefit-v1-offering-policy.jsonld", + "sha256": "sha256:d209ff4a6c3a6a4f7b3d69f53fdcb7fe55bc68fdbd9f7180f2374a34c0b6c789" }, { "media_type": "application/ld+json", - "path": "metadata/policies/mosd-programme-beneficiary-enrollment-offering-policy.jsonld", - "sha256": "sha256:2f10bcafccfb352e80cd01d48acd0a5552ee26bc2ba42b599fe7fbef08a82d84" + "path": "metadata/policies/nagdi-livestock-v1-offering-policy.jsonld", + "sha256": "sha256:e6d2e623aeb6fb683e8181c10c65c725fb9574b890968519d6c44fbc0bff0b60" }, { "media_type": "application/ld+json", - "path": "metadata/policies/nagdi-agriculture-farmer-voucher-offering-policy.jsonld", - "sha256": "sha256:a1146d20b753132190483bac84b96a2d681e8e2c3ccf5f1b78d8506f4862b322" + "path": "metadata/policies/nagdi-voucher-v1-offering-policy.jsonld", + "sha256": "sha256:e3a854169bf138c8195c994121cbace6e4ed84e147b3e6017e590612e3f50f62" }, { "media_type": "application/ld+json", - "path": "metadata/policies/nagdi-agriculture-livestock-movement-offering-policy.jsonld", - "sha256": "sha256:6c3f849025434b9abc5be8644bfd29011bb519914b293e8f3445845ab53376ea" + "path": "metadata/policies/nia-child-benefit-v1-offering-policy.jsonld", + "sha256": "sha256:fd0c92c384820881bbabf8fee115825d1197563263f6eb815135c311d409f033" }, { "media_type": "application/ld+json", - "path": "metadata/policies/nia-population-population-status-offering-policy.jsonld", - "sha256": "sha256:2d3a0ff6df7f37513a0ec334e933ae27a3412db2a5e2fd569a1f6be5081bbb9d" + "path": "metadata/policies/nia-citizen-status-v1-offering-policy.jsonld", + "sha256": "sha256:f24586f80452144f90e3816d926921ce6ec211e78685992bcbd8c0bbe8e34ff1" }, { "media_type": "application/ld+json", - "path": "metadata/policies/sipf-pensions-pension-case-offering-policy.jsonld", - "sha256": "sha256:743025c16e905b278546623c1d2f268502b6c013b667c56ed8d46fd603bc36d9" + "path": "metadata/policies/sipf-pension-payment-v1-offering-policy.jsonld", + "sha256": "sha256:9dc670d33ae13a2e1063eea6e3ab6ce11e409912f3c2833ed67d4417e36971ed" }, { "media_type": "application/ld+json", - "path": "metadata/policies/solmara-child-benefit-authority-predicate-collection-policy.jsonld", - "sha256": "sha256:7b14a9bf40cdc5da8da1c3dae62c554c19eff14d8b0255984081216b651c57ca" + "path": "metadata/policies/sipf-survivor-benefit-v1-offering-policy.jsonld", + "sha256": "sha256:cd7fd749cb732565369d7dadd2dd06da4366c169545261dd1c8f60bc584f900a" }, { "media_type": "application/ld+json", - "path": "metadata/policies/sro-social-household-poverty-offering-policy.jsonld", - "sha256": "sha256:2b99e4de49c24e68fb5b203cc43709aa285faad8dd2f0a3760c59846ccc6ae63" + "path": "metadata/policies/sro-child-benefit-v1-offering-policy.jsonld", + "sha256": "sha256:47aa3d94c91cc97a63141cb67e59c017f2a4bcb4bcc11f31d42096d808c52542" }, { "media_type": "application/ld+json", @@ -143,7 +163,7 @@ } ], "catalog": "/metadata/catalog.json", - "catalog_id": "solmara-wave1-multi-authority", + "catalog_id": "solmara-authority-cells", "dcat": "/metadata/dcat.jsonld", "evidence_offerings": "/metadata/evidence-offerings.json", "manifest": "/metadata/metadata.yaml", diff --git a/metadata/public/metadata/metadata.yaml b/metadata/public/metadata/metadata.yaml index e613e67..219ad77 100644 --- a/metadata/public/metadata/metadata.yaml +++ b/metadata/public/metadata/metadata.yaml @@ -1,11 +1,11 @@ schema_version: registry-manifest/v1 catalog: - id: solmara-wave1-multi-authority + id: solmara-authority-cells base_url: https://metadata.solmara.registrystack.org title: - en: Solmara Lab Wave 1 Service Catalogue + en: Solmara authority-owned Evidence catalogue description: - en: One merged CPSV-AP metadata publish for Solmara Wave 1 registry and evidence services. + en: Six authority-owned Evidence cells, five governed Relay V2 services, and the programme applications that compose signed assertions. publisher: name: Solmara Interoperability Metadata Publisher iri: did:web:id.registrystack.org:solmara:authority:metadata @@ -50,385 +50,280 @@ authorities: authority_type: eli:PublicAuthority spatial: https://id.registrystack.org/solmara/jurisdiction/XS requirements: - - id: child-benefit-review-requirement - iri: https://id.registrystack.org/solmara/requirements/child-benefit-review - title: - en: Child benefit eligibility review - description: - en: Confirm registered birth, under-five age, household poverty status, and duplicate enrollment status. + - id: cra-child-benefit-v1 + iri: https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1 + title: {en: CRA child benefit evidence} + description: {en: Confirm registered birth and age under five from CRA's immutable birth extract.} evidence_type_lists: - - id: child-benefit-wave1-evidence - title: - en: Child benefit registry evidence - evidence_types: - - birth-registration-evidence - - population-status-evidence - - household-poverty-evidence - - beneficiary-enrollment-evidence - - id: pension-survivor-review-requirement - iri: https://id.registrystack.org/solmara/requirements/pension-survivor-review - title: - en: Pension stop and survivor benefit review - description: - en: Confirm death registration, active pension payment state, and survivor eligibility. + - id: cra-child-benefit-list + title: {en: CRA child benefit evidence} + evidence_types: [cra-child-benefit-v1] + - id: cra-pension-death-v1 + iri: https://id.registrystack.org/solmara/requirement/cra-pension-death/v1 + title: {en: CRA pension death status} + description: {en: Confirm death registration through CRA's governed Relay lookup.} evidence_type_lists: - - id: pension-wave1-evidence - title: - en: Pension and survivor registry evidence - evidence_types: - - death-registration-evidence - - pension-case-evidence - - id: nagdi-voucher-requirement - iri: https://id.registrystack.org/solmara/requirements/nagdi-voucher - title: - en: Farmer climate-smart voucher review - description: - en: Confirm farmer registration, data-use authorization, parcel, crop, climate-risk, and redemption predicates. + - id: cra-pension-death-list + title: {en: CRA death-status evidence} + evidence_types: [cra-death-status-v1] + - id: cra-citizen-record-v1 + iri: https://id.registrystack.org/solmara/requirement/cra-citizen-record/v1 + title: {en: CRA citizen record link} + description: {en: Confirm a linked civil record through CRA's governed Relay lookup.} evidence_type_lists: - - id: nagdi-voucher-evidence - title: - en: Farmer voucher evidence - evidence_types: - - farmer-voucher-evidence - - id: livestock-movement-requirement - iri: https://id.registrystack.org/solmara/requirements/livestock-movement - title: - en: Livestock movement control review - description: - en: Confirm herd registration, vaccination, species-aware quarantine, destination, and open permit predicates. + - id: cra-citizen-record-list + title: {en: CRA civil-record evidence} + evidence_types: [civil-record-linked-v1] + - id: nia-child-benefit-v1 + iri: https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1 + title: {en: NIA child benefit population status} + description: {en: Confirm active population status from NIA's immutable population extract.} evidence_type_lists: - - id: livestock-movement-evidence-list - title: - en: Livestock movement evidence - evidence_types: - - livestock-movement-evidence + - id: nia-child-benefit-list + title: {en: NIA population evidence} + evidence_types: [population-active-v1] + - id: nia-citizen-status-v1 + iri: https://id.registrystack.org/solmara/requirement/nia-citizen-status/v1 + title: {en: NIA citizen population status} + description: {en: Confirm active citizen population status from NIA's immutable population extract.} + evidence_type_lists: + - id: nia-citizen-status-list + title: {en: NIA citizen-status evidence} + evidence_types: [citizen-population-active-v1] + - id: sro-child-benefit-v1 + iri: https://id.registrystack.org/solmara/requirement/sro-child-benefit/v1 + title: {en: SRO poverty priority} + description: {en: Confirm household poverty priority from SRO's immutable poverty extract.} + evidence_type_lists: + - id: sro-child-benefit-list + title: {en: SRO poverty evidence} + evidence_types: [poverty-priority-v1] + - id: mosd-child-benefit-v1 + iri: https://id.registrystack.org/solmara/requirement/mosd-child-benefit/v1 + title: {en: MoSD programme enrolment status} + description: {en: Confirm that the person is not already enrolled through MoSD's governed Relay lookup.} + evidence_type_lists: + - id: mosd-child-benefit-list + title: {en: MoSD enrolment evidence} + evidence_types: [not-enrolled-v1] + - id: sipf-pension-payment-v1 + iri: https://id.registrystack.org/solmara/requirement/sipf-pension-payment/v1 + title: {en: SIPF pension payment status} + description: {en: Confirm active pension payment through SIPF's governed Relay lookup.} + evidence_type_lists: + - id: sipf-pension-payment-list + title: {en: SIPF pension payment evidence} + evidence_types: [pension-payment-active-v1] + - id: sipf-survivor-benefit-v1 + iri: https://id.registrystack.org/solmara/requirement/sipf-survivor-benefit/v1 + title: {en: SIPF survivor eligibility} + description: {en: Confirm survivor eligibility through SIPF's governed Relay lookup.} + evidence_type_lists: + - id: sipf-survivor-benefit-list + title: {en: SIPF survivor evidence} + evidence_types: [survivor-benefit-v1] + - id: nagdi-voucher-v1 + iri: https://id.registrystack.org/solmara/requirement/nagdi-voucher/v1 + title: {en: NAgDI climate-smart voucher} + description: {en: Confirm climate-smart voucher eligibility through NAgDI's governed Relay lookup.} + evidence_type_lists: + - id: nagdi-voucher-list + title: {en: NAgDI voucher evidence} + evidence_types: [climate-smart-voucher-v1] + - id: nagdi-livestock-v1 + iri: https://id.registrystack.org/solmara/requirement/nagdi-livestock/v1 + title: {en: NAgDI livestock movement} + description: {en: Confirm livestock movement eligibility through NAgDI's governed Relay lookup.} + evidence_type_lists: + - id: nagdi-livestock-list + title: {en: NAgDI livestock evidence} + evidence_types: [livestock-movement-v1] evidence_types: - - id: birth-registration-evidence - iri: https://id.registrystack.org/solmara/evidence-types/birth-registration - title: - en: Birth registration evidence - proves: - - child-benefit-review-requirement - information_concepts: - - ps:crvs/Birth - - id: population-status-evidence - iri: https://id.registrystack.org/solmara/evidence-types/population-status - title: - en: Population status evidence - proves: - - child-benefit-review-requirement - information_concepts: - - ps:Person - - id: household-poverty-evidence - iri: https://id.registrystack.org/solmara/evidence-types/household-poverty - title: - en: Household poverty evidence - proves: - - child-benefit-review-requirement + - id: cra-child-benefit-v1 + iri: https://id.registrystack.org/solmara/evidence-type/cra-child-benefit/v1 + title: {en: CRA child benefit assertion} + proves: [cra-child-benefit-v1] information_concepts: - - ps:Household - - ps:SocioEconomicProfile - - id: beneficiary-enrollment-evidence - iri: https://id.registrystack.org/solmara/evidence-types/beneficiary-enrollment - title: - en: Beneficiary enrollment evidence - proves: - - child-benefit-review-requirement + - https://id.registrystack.org/solmara/concept/birth-is-registered + - https://id.registrystack.org/solmara/concept/child-age-under-5 + - id: cra-death-status-v1 + iri: https://id.registrystack.org/solmara/evidence-type/cra-death-status/v1 + title: {en: CRA death-status assertion} + proves: [cra-pension-death-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/person-is-deceased] + - id: civil-record-linked-v1 + iri: https://id.registrystack.org/solmara/evidence-type/civil-record-linked/v1 + title: {en: CRA civil-record assertion} + proves: [cra-citizen-record-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/civil-record-linked] + - id: population-active-v1 + iri: https://id.registrystack.org/solmara/evidence-type/population-active/v1 + title: {en: NIA active-population assertion} + proves: [nia-child-benefit-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/population-record-active] + - id: citizen-population-active-v1 + iri: https://id.registrystack.org/solmara/evidence-type/citizen-population-active/v1 + title: {en: NIA active-citizen assertion} + proves: [nia-citizen-status-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/citizen-population-record-active] + - id: poverty-priority-v1 + iri: https://id.registrystack.org/solmara/evidence-type/poverty-priority/v1 + title: {en: SRO poverty-priority assertion} + proves: [sro-child-benefit-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/household-below-poverty-threshold] + - id: not-enrolled-v1 + iri: https://id.registrystack.org/solmara/evidence-type/not-enrolled/v1 + title: {en: MoSD not-enrolled assertion} + proves: [mosd-child-benefit-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/not-already-enrolled] + - id: pension-payment-active-v1 + iri: https://id.registrystack.org/solmara/evidence-type/pension-payment-active/v1 + title: {en: SIPF active-payment assertion} + proves: [sipf-pension-payment-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/pension-payment-active] + - id: survivor-benefit-v1 + iri: https://id.registrystack.org/solmara/evidence-type/survivor-benefit/v1 + title: {en: SIPF survivor-benefit assertion} + proves: [sipf-survivor-benefit-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/survivor-is-eligible] + - id: climate-smart-voucher-v1 + iri: https://id.registrystack.org/solmara/evidence-type/climate-smart-voucher/v1 + title: {en: NAgDI climate-smart voucher assertion} + proves: [nagdi-voucher-v1] information_concepts: - - ps:sp/Enrollment - - id: child-benefit-authority-predicate-collection-evidence - iri: https://id.registrystack.org/solmara/evidence-types/child-benefit-authority-predicate-collection - title: - en: Child benefit authority predicate collection - description: - en: A transient collection of source-owned child benefit predicates with no copied source rows or eligibility decision. - proves: - - child-benefit-review-requirement + - https://id.registrystack.org/solmara/concept/farmer-registered + - https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose + - https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher + - id: livestock-movement-v1 + iri: https://id.registrystack.org/solmara/evidence-type/livestock-movement/v1 + title: {en: NAgDI livestock movement assertion} + proves: [nagdi-livestock-v1] information_concepts: - - ps:Person - - id: death-registration-evidence - iri: https://id.registrystack.org/solmara/evidence-types/death-registration - title: - en: Death registration evidence - proves: - - pension-survivor-review-requirement - information_concepts: - - ps:crvs/Death - - id: pension-case-evidence - iri: https://id.registrystack.org/solmara/evidence-types/pension-case - title: - en: Pension case evidence - proves: - - pension-survivor-review-requirement - - id: farmer-voucher-evidence - iri: https://id.registrystack.org/solmara/evidence-types/farmer-voucher - title: - en: Farmer voucher evidence - proves: - - nagdi-voucher-requirement - information_concepts: - - ps:Farm - - id: livestock-movement-evidence - iri: https://id.registrystack.org/solmara/evidence-types/livestock-movement - title: - en: Livestock movement evidence - proves: - - livestock-movement-requirement + - https://id.registrystack.org/solmara/concept/registered-herd + - https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species + - https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit public_services: - id: child-benefit-review iri: https://id.registrystack.org/solmara/services/child-benefit-review - title: - en: Child benefit eligibility review - description: - en: MoSD review of child benefit eligibility using minimized registry evidence. + title: {en: Child benefit eligibility review} + description: {en: The programme composes four independently signed authority assertions into five reviewed concepts.} competent_authority: mosd-programme-mis jurisdiction: https://id.registrystack.org/solmara/jurisdiction/XS - holds_requirements: - - child-benefit-review-requirement - data_services: - - child-benefit-evidence-collector-api - - cra-notary-api - - nia-notary-api - - sro-notary-api - - programme-notary-api + holds_requirements: [cra-child-benefit-v1, nia-child-benefit-v1, sro-child-benefit-v1, mosd-child-benefit-v1] + data_services: [cra-evidence-api, nia-evidence-api, sro-evidence-api, mosd-programme-evidence-api] - id: pension-survivor-review iri: https://id.registrystack.org/solmara/services/pension-survivor-review - title: - en: Pension stop and survivor benefit review - description: - en: SIPF review for pension payment stop and survivor benefit eligibility. + title: {en: Pension stop and survivor benefit review} + description: {en: The programme composes CRA and SIPF signed assertions without exposing unrelated civil data.} competent_authority: sipf jurisdiction: https://id.registrystack.org/solmara/jurisdiction/XS - holds_requirements: - - pension-survivor-review-requirement - data_services: - - cra-notary-api - - sipf-notary-api - - id: nagdi-voucher-review - iri: https://id.registrystack.org/solmara/services/nagdi-voucher-review - title: - en: Farmer climate-smart voucher review - description: - en: NAgDI review of farmer eligibility for climate-smart input support. - competent_authority: nagdi + holds_requirements: [cra-pension-death-v1, sipf-pension-payment-v1, sipf-survivor-benefit-v1] + data_services: [cra-evidence-api, sipf-evidence-api] + - id: citizen-self-service + iri: https://id.registrystack.org/solmara/services/citizen-self-service + title: {en: Citizen self-service} + description: {en: The application composes separately signed CRA and NIA citizen assertions.} + competent_authority: nia jurisdiction: https://id.registrystack.org/solmara/jurisdiction/XS - holds_requirements: - - nagdi-voucher-requirement - data_services: - - nagdi-notary-api - - id: livestock-movement-control - iri: https://id.registrystack.org/solmara/services/livestock-movement-control - title: - en: Livestock movement control - description: - en: NAgDI review of livestock movement permit eligibility and quarantine controls. + holds_requirements: [cra-citizen-record-v1, nia-citizen-status-v1] + data_services: [cra-evidence-api, nia-evidence-api] + - id: agriculture-review + iri: https://id.registrystack.org/solmara/services/agriculture-review + title: {en: Agriculture voucher and livestock review} + description: {en: NAgDI applications consume independently signed voucher and livestock assertions.} competent_authority: nagdi jurisdiction: https://id.registrystack.org/solmara/jurisdiction/XS - holds_requirements: - - livestock-movement-requirement - data_services: - - nagdi-notary-api + holds_requirements: [nagdi-voucher-v1, nagdi-livestock-v1] + data_services: [nagdi-evidence-api] data_services: - - id: child-benefit-evidence-collector-api - iri: https://child-benefit-federator.solmara.registrystack.org/v1/evaluations - title: - en: Child benefit authority predicate collection API - description: - en: Returns application/json collections assembled from source-Notary predicates without composing an eligibility decision. - endpoint_url: https://child-benefit-federator.solmara.registrystack.org/v1/evaluations - endpoint_description: https://child-benefit-federator.solmara.registrystack.org/v1/claims - conforms_to: https://id.registrystack.org/solmara/contracts/authority-predicate-collection/v1 - - id: cra-notary-api - iri: https://cra-notary.solmara.registrystack.org/v1/evaluations - title: - en: CRA evidence API - endpoint_url: https://cra-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://cra-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 - - id: nia-notary-api - iri: https://nia-notary.solmara.registrystack.org/v1/evaluations - title: - en: NIA evidence API - endpoint_url: https://nia-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://nia-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 - - id: sro-notary-api - iri: https://sro-notary.solmara.registrystack.org/v1/evaluations - title: - en: SRO evidence API - endpoint_url: https://sro-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://sro-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 - - id: programme-notary-api - iri: https://programme-notary.solmara.registrystack.org/v1/evaluations - title: - en: Programme MIS evidence API - endpoint_url: https://programme-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://programme-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 - - id: sipf-notary-api - iri: https://sipf-notary.solmara.registrystack.org/v1/evaluations - title: - en: SIPF evidence API - endpoint_url: https://sipf-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://sipf-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 - - id: nagdi-notary-api - iri: https://nagdi-notary.solmara.registrystack.org/v1/evaluations - title: - en: NAgDI evidence API - endpoint_url: https://nagdi-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://nagdi-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 + - id: cra-evidence-api + iri: https://cra-evidence.solmara.registrystack.org/v1/evidence + title: {en: CRA Evidence cell} + endpoint_url: https://cra-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 + - id: nia-evidence-api + iri: https://nia-evidence.solmara.registrystack.org/v1/evidence + title: {en: NIA Evidence cell} + endpoint_url: https://nia-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 + - id: sro-evidence-api + iri: https://sro-evidence.solmara.registrystack.org/v1/evidence + title: {en: SRO Evidence cell} + endpoint_url: https://sro-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://sro-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 + - id: mosd-programme-evidence-api + iri: https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence + title: {en: MoSD Programme Evidence cell} + endpoint_url: https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 + - id: sipf-evidence-api + iri: https://sipf-evidence.solmara.registrystack.org/v1/evidence + title: {en: SIPF Evidence cell} + endpoint_url: https://sipf-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 + - id: nagdi-evidence-api + iri: https://nagdi-evidence.solmara.registrystack.org/v1/evidence + title: {en: NAgDI Evidence cell} + endpoint_url: https://nagdi-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 datasets: - id: cra-civil - title: - en: Civil registration + title: {en: CRA civil registration publication} + description: {en: CRA-owned immutable birth extract and governed civil-person Relay source.} access_rights: restricted entities: - name: civil_person - title: - en: Civil person - identifiers: - - name: uin - kind: uin - fields: - - name: uin - type: string - required: false - concepts: - - ps:Person.identifier - - name: birth_brn - type: string - required: false - concepts: - - ps:crvs/Birth.registrationNumber - - name: death_drn - type: string - required: false - concepts: - - ps:crvs/Death.registrationNumber - - name: deceased - type: boolean - required: true - evidence_offerings: - - id: cra-birth-registration-offering - iri: https://id.registrystack.org/solmara/evidence-offerings/cra-birth-registration - title: - en: CRA birth registration evidence offering - evidence_type: birth-registration-evidence - issuing_authority: - id: cra - iri: did:web:id.registrystack.org:solmara:authority:cra - name: Civil Registration Authority - country: XS - entity: civil_person - lookup_keys: - - uin - procedure_contexts: - - child-benefit-review - access: - kind: evidence-verification-api - conforms_to: https://spec.openapis.org/oas/v3.1.0 - endpoint_url: https://cra-notary.solmara.registrystack.org/v1/evaluations - discovery_url: https://cra-notary.solmara.registrystack.org/.well-known/evidence-service - ruleset: birth-is-registered - policy: - purpose: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - id: cra-death-registration-offering - iri: https://id.registrystack.org/solmara/evidence-offerings/cra-death-registration - title: - en: CRA death registration evidence offering - evidence_type: death-registration-evidence - issuing_authority: - id: cra - iri: did:web:id.registrystack.org:solmara:authority:cra - name: Civil Registration Authority - country: XS - entity: civil_person - lookup_keys: - - uin - procedure_contexts: - - pension-survivor-review - access: - kind: evidence-verification-api - conforms_to: https://spec.openapis.org/oas/v3.1.0 - endpoint_url: https://cra-notary.solmara.registrystack.org/v1/evaluations - discovery_url: https://cra-notary.solmara.registrystack.org/.well-known/evidence-service - ruleset: person-is-deceased - policy: - purpose: - - https://id.registrystack.org/solmara/purpose/pension-payment-review + identifiers: [{name: uin, kind: uin}] + fields: [{name: uin, type: string, required: true}] - id: nia-population - title: - en: Population register + title: {en: NIA population publication} + description: {en: NIA-owned immutable population extract and governed eSignet Relay source.} access_rights: restricted entities: - - name: person + - name: population_person + identifiers: [{name: uin, kind: uin}] + fields: [{name: uin, type: string, required: true}] - id: sro-social - title: - en: Social registry + title: {en: SRO poverty extract} + description: {en: SRO-owned immutable poverty publication.} access_rights: restricted entities: - - name: household + - name: poverty_record + identifiers: [{name: uin, kind: uin}] + fields: [{name: uin, type: string, required: true}] - id: mosd-programme - title: - en: Integrated beneficiary registry + title: {en: MoSD programme enrolment publication} + description: {en: MoSD-owned mutable SQLite source consulted through Relay V2.} access_rights: restricted entities: - - name: enrollment + - name: beneficiary_enrolment + identifiers: [{name: uin, kind: uin}] + fields: [{name: uin, type: string, required: true}] - id: sipf-pensions - title: - en: Pensions and social insurance + title: {en: SIPF pension and survivor publication} + description: {en: SIPF-owned mutable SQLite source consulted through Relay V2.} access_rights: restricted entities: - - name: pension_case + - name: pension_payment + identifiers: [{name: pensioner_uin, kind: uin}] + fields: [{name: pensioner_uin, type: string, required: true}] + - name: survivor_case + identifiers: [{name: spouse_uin, kind: uin}] + fields: [{name: spouse_uin, type: string, required: true}] - id: nagdi-agriculture - title: - en: Farmer and livestock registries + title: {en: NAgDI agriculture publication} + description: {en: NAgDI-owned mutable SQLite source consulted through Relay V2.} access_rights: restricted entities: - name: farmer_voucher + identifiers: [{name: farmer_id, kind: local}] + fields: [{name: farmer_id, type: string, required: true}] - name: livestock_movement -evaluation_profiles: - - id: birth-is-registered - ruleset: birth-is-registered-v1 - claim_id: birth-is-registered - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: population-record-active - ruleset: population-record-active-v1 - claim_id: population-record-active - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: child-age-under-5 - ruleset: child-age-under-5-v1 - claim_id: child-age-under-5 - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: household-below-poverty-threshold - ruleset: household-below-poverty-threshold-v1 - claim_id: household-below-poverty-threshold - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: not-already-enrolled - ruleset: not-already-enrolled-v1 - claim_id: not-already-enrolled - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: survivor-benefit-eligibility - ruleset: survivor-benefit-eligibility-v1 - claim_id: survivor-is-eligible - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: climate-smart-voucher-eligibility - ruleset: climate-smart-voucher-eligibility-v1 - claim_id: eligible-for-climate-smart-input-voucher - subject_id_type: farmer_id - max_source_observed_age_seconds: 86400 - - id: livestock-movement-permit - ruleset: livestock-movement-permit-v1 - claim_id: eligible-for-livestock-movement-permit - subject_id_type: farmer_id - max_source_observed_age_seconds: 86400 + identifiers: [{name: farmer_id, kind: local}] + fields: [{name: farmer_id, type: string, required: true}] diff --git a/metadata/public/metadata/ogc-records/items.json b/metadata/public/metadata/ogc-records/items.json index b7a46e9..9362d37 100644 --- a/metadata/public/metadata/ogc-records/items.json +++ b/metadata/public/metadata/ogc-records/items.json @@ -5,7 +5,7 @@ "id": "cra-civil", "properties": { "authority": "Civil Registration Authority", - "title": "Civil registration" + "title": "CRA civil registration publication" }, "type": "Feature" }, @@ -14,7 +14,7 @@ "id": "nia-population", "properties": { "authority": "National Identity Agency", - "title": "Population register" + "title": "NIA population publication" }, "type": "Feature" }, @@ -23,7 +23,7 @@ "id": "sro-social", "properties": { "authority": "Social Registry Office", - "title": "Social registry" + "title": "SRO poverty extract" }, "type": "Feature" }, @@ -32,7 +32,7 @@ "id": "mosd-programme", "properties": { "authority": "Ministry of Social Development Programme MIS", - "title": "Integrated beneficiary registry" + "title": "MoSD programme enrolment publication" }, "type": "Feature" }, @@ -41,7 +41,7 @@ "id": "sipf-pensions", "properties": { "authority": "Social Insurance and Pensions Fund", - "title": "Pensions and social insurance" + "title": "SIPF pension and survivor publication" }, "type": "Feature" }, @@ -50,7 +50,7 @@ "id": "nagdi-agriculture", "properties": { "authority": "National Agricultural Data Institute", - "title": "Farmer and livestock registries" + "title": "NAgDI agriculture publication" }, "type": "Feature" } diff --git a/metadata/public/metadata/policies.jsonld b/metadata/public/metadata/policies.jsonld index b355a54..e2d3bca 100644 --- a/metadata/public/metadata/policies.jsonld +++ b/metadata/public/metadata/policies.jsonld @@ -4,7 +4,7 @@ "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "cra-birth-registration-offering-policy", + "id": "cra-child-benefit-v1-offering-policy", "permission": [ { "action": "use", @@ -20,15 +20,15 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/cra-birth-registration", + "target": "https://id.registrystack.org/solmara/evidence-offerings/cra-child-benefit-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/cra-birth-registration-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/cra-child-benefit-v1-offering-policy" }, { "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "cra-death-registration-offering-policy", + "id": "cra-pension-death-v1-offering-policy", "permission": [ { "action": "use", @@ -44,15 +44,39 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/cra-death-registration", + "target": "https://id.registrystack.org/solmara/evidence-offerings/cra-pension-death-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/cra-death-registration-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/cra-pension-death-v1-offering-policy" }, { "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "nia-population-population-status-offering-policy", + "id": "cra-citizen-record-v1-offering-policy", + "permission": [ + { + "action": "use", + "constraint": [ + { + "leftOperand": "purpose", + "operator": "isAnyOf", + "rightOperand": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ] + } + ] + } + ], + "profile": "https://www.w3.org/TR/odrl-model/", + "target": "https://id.registrystack.org/solmara/evidence-offerings/cra-citizen-record-v1-offering", + "type": "odrl:Set", + "uid": "https://id.registrystack.org/solmara/policies/cra-citizen-record-v1-offering-policy" + }, + { + "@context": { + "odrl": "http://www.w3.org/ns/odrl/2/" + }, + "id": "nia-child-benefit-v1-offering-policy", "permission": [ { "action": "use", @@ -68,15 +92,39 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/nia-population-population-status-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/nia-child-benefit-v1-offering", + "type": "odrl:Set", + "uid": "https://id.registrystack.org/solmara/policies/nia-child-benefit-v1-offering-policy" + }, + { + "@context": { + "odrl": "http://www.w3.org/ns/odrl/2/" + }, + "id": "nia-citizen-status-v1-offering-policy", + "permission": [ + { + "action": "use", + "constraint": [ + { + "leftOperand": "purpose", + "operator": "isAnyOf", + "rightOperand": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ] + } + ] + } + ], + "profile": "https://www.w3.org/TR/odrl-model/", + "target": "https://id.registrystack.org/solmara/evidence-offerings/nia-citizen-status-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/nia-population-population-status-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/nia-citizen-status-v1-offering-policy" }, { "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "sro-social-household-poverty-offering-policy", + "id": "sro-child-benefit-v1-offering-policy", "permission": [ { "action": "use", @@ -92,15 +140,15 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/sro-social-household-poverty-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/sro-child-benefit-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/sro-social-household-poverty-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/sro-child-benefit-v1-offering-policy" }, { "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "mosd-programme-beneficiary-enrollment-offering-policy", + "id": "mosd-child-benefit-v1-offering-policy", "permission": [ { "action": "use", @@ -116,15 +164,15 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/mosd-programme-beneficiary-enrollment-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/mosd-child-benefit-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/mosd-programme-beneficiary-enrollment-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/mosd-child-benefit-v1-offering-policy" }, { "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "sipf-pensions-pension-case-offering-policy", + "id": "sipf-pension-payment-v1-offering-policy", "permission": [ { "action": "use", @@ -133,23 +181,22 @@ "leftOperand": "purpose", "operator": "isAnyOf", "rightOperand": [ - "https://id.registrystack.org/solmara/purpose/pension-payment-review", - "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" + "https://id.registrystack.org/solmara/purpose/pension-payment-review" ] } ] } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pensions-pension-case-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pension-payment-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/sipf-pensions-pension-case-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/sipf-pension-payment-v1-offering-policy" }, { "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "nagdi-agriculture-farmer-voucher-offering-policy", + "id": "sipf-survivor-benefit-v1-offering-policy", "permission": [ { "action": "use", @@ -158,22 +205,22 @@ "leftOperand": "purpose", "operator": "isAnyOf", "rightOperand": [ - "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" + "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" ] } ] } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-farmer-voucher-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/sipf-survivor-benefit-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/nagdi-agriculture-farmer-voucher-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/sipf-survivor-benefit-v1-offering-policy" }, { "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "nagdi-agriculture-livestock-movement-offering-policy", + "id": "nagdi-voucher-v1-offering-policy", "permission": [ { "action": "use", @@ -182,22 +229,22 @@ "leftOperand": "purpose", "operator": "isAnyOf", "rightOperand": [ - "https://id.registrystack.org/solmara/purpose/livestock-movement-control" + "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" ] } ] } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-livestock-movement-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-voucher-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/nagdi-agriculture-livestock-movement-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/nagdi-voucher-v1-offering-policy" }, { "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "solmara-child-benefit-authority-predicate-collection-policy", + "id": "nagdi-livestock-v1-offering-policy", "permission": [ { "action": "use", @@ -206,16 +253,16 @@ "leftOperand": "purpose", "operator": "isAnyOf", "rightOperand": [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" + "https://id.registrystack.org/solmara/purpose/livestock-movement-control" ] } ] } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/child-benefit-authority-predicate-collection", + "target": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-livestock-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/solmara-child-benefit-authority-predicate-collection-policy" + "uid": "https://id.registrystack.org/solmara/policies/nagdi-livestock-v1-offering-policy" } ] } diff --git a/metadata/public/metadata/policies/cra-birth-registration-offering-policy.jsonld b/metadata/public/metadata/policies/cra-child-benefit-v1-offering-policy.jsonld similarity index 73% rename from metadata/public/metadata/policies/cra-birth-registration-offering-policy.jsonld rename to metadata/public/metadata/policies/cra-child-benefit-v1-offering-policy.jsonld index e7f5d23..dc4a9d6 100644 --- a/metadata/public/metadata/policies/cra-birth-registration-offering-policy.jsonld +++ b/metadata/public/metadata/policies/cra-child-benefit-v1-offering-policy.jsonld @@ -2,7 +2,7 @@ "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "cra-birth-registration-offering-policy", + "id": "cra-child-benefit-v1-offering-policy", "permission": [ { "action": "use", @@ -18,7 +18,7 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/cra-birth-registration", + "target": "https://id.registrystack.org/solmara/evidence-offerings/cra-child-benefit-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/cra-birth-registration-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/cra-child-benefit-v1-offering-policy" } diff --git a/metadata/public/metadata/policies/cra-citizen-record-v1-offering-policy.jsonld b/metadata/public/metadata/policies/cra-citizen-record-v1-offering-policy.jsonld new file mode 100644 index 0000000..3ce686b --- /dev/null +++ b/metadata/public/metadata/policies/cra-citizen-record-v1-offering-policy.jsonld @@ -0,0 +1,24 @@ +{ + "@context": { + "odrl": "http://www.w3.org/ns/odrl/2/" + }, + "id": "cra-citizen-record-v1-offering-policy", + "permission": [ + { + "action": "use", + "constraint": [ + { + "leftOperand": "purpose", + "operator": "isAnyOf", + "rightOperand": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ] + } + ] + } + ], + "profile": "https://www.w3.org/TR/odrl-model/", + "target": "https://id.registrystack.org/solmara/evidence-offerings/cra-citizen-record-v1-offering", + "type": "odrl:Set", + "uid": "https://id.registrystack.org/solmara/policies/cra-citizen-record-v1-offering-policy" +} diff --git a/metadata/public/metadata/policies/cra-death-registration-offering-policy.jsonld b/metadata/public/metadata/policies/cra-pension-death-v1-offering-policy.jsonld similarity index 73% rename from metadata/public/metadata/policies/cra-death-registration-offering-policy.jsonld rename to metadata/public/metadata/policies/cra-pension-death-v1-offering-policy.jsonld index 71eea68..413be1b 100644 --- a/metadata/public/metadata/policies/cra-death-registration-offering-policy.jsonld +++ b/metadata/public/metadata/policies/cra-pension-death-v1-offering-policy.jsonld @@ -2,7 +2,7 @@ "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "cra-death-registration-offering-policy", + "id": "cra-pension-death-v1-offering-policy", "permission": [ { "action": "use", @@ -18,7 +18,7 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/cra-death-registration", + "target": "https://id.registrystack.org/solmara/evidence-offerings/cra-pension-death-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/cra-death-registration-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/cra-pension-death-v1-offering-policy" } diff --git a/metadata/public/metadata/policies/sro-social-household-poverty-offering-policy.jsonld b/metadata/public/metadata/policies/mosd-child-benefit-v1-offering-policy.jsonld similarity index 71% rename from metadata/public/metadata/policies/sro-social-household-poverty-offering-policy.jsonld rename to metadata/public/metadata/policies/mosd-child-benefit-v1-offering-policy.jsonld index b0abf5c..7a96382 100644 --- a/metadata/public/metadata/policies/sro-social-household-poverty-offering-policy.jsonld +++ b/metadata/public/metadata/policies/mosd-child-benefit-v1-offering-policy.jsonld @@ -2,7 +2,7 @@ "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "sro-social-household-poverty-offering-policy", + "id": "mosd-child-benefit-v1-offering-policy", "permission": [ { "action": "use", @@ -18,7 +18,7 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/sro-social-household-poverty-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/mosd-child-benefit-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/sro-social-household-poverty-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/mosd-child-benefit-v1-offering-policy" } diff --git a/metadata/public/metadata/policies/nagdi-agriculture-livestock-movement-offering-policy.jsonld b/metadata/public/metadata/policies/nagdi-livestock-v1-offering-policy.jsonld similarity index 69% rename from metadata/public/metadata/policies/nagdi-agriculture-livestock-movement-offering-policy.jsonld rename to metadata/public/metadata/policies/nagdi-livestock-v1-offering-policy.jsonld index 809e3ba..0ecf1a5 100644 --- a/metadata/public/metadata/policies/nagdi-agriculture-livestock-movement-offering-policy.jsonld +++ b/metadata/public/metadata/policies/nagdi-livestock-v1-offering-policy.jsonld @@ -2,7 +2,7 @@ "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "nagdi-agriculture-livestock-movement-offering-policy", + "id": "nagdi-livestock-v1-offering-policy", "permission": [ { "action": "use", @@ -18,7 +18,7 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-livestock-movement-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-livestock-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/nagdi-agriculture-livestock-movement-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/nagdi-livestock-v1-offering-policy" } diff --git a/metadata/public/metadata/policies/nagdi-agriculture-farmer-voucher-offering-policy.jsonld b/metadata/public/metadata/policies/nagdi-voucher-v1-offering-policy.jsonld similarity index 70% rename from metadata/public/metadata/policies/nagdi-agriculture-farmer-voucher-offering-policy.jsonld rename to metadata/public/metadata/policies/nagdi-voucher-v1-offering-policy.jsonld index 96f9226..5596c55 100644 --- a/metadata/public/metadata/policies/nagdi-agriculture-farmer-voucher-offering-policy.jsonld +++ b/metadata/public/metadata/policies/nagdi-voucher-v1-offering-policy.jsonld @@ -2,7 +2,7 @@ "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "nagdi-agriculture-farmer-voucher-offering-policy", + "id": "nagdi-voucher-v1-offering-policy", "permission": [ { "action": "use", @@ -18,7 +18,7 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-agriculture-farmer-voucher-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/nagdi-voucher-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/nagdi-agriculture-farmer-voucher-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/nagdi-voucher-v1-offering-policy" } diff --git a/metadata/public/metadata/policies/nia-population-population-status-offering-policy.jsonld b/metadata/public/metadata/policies/nia-child-benefit-v1-offering-policy.jsonld similarity index 69% rename from metadata/public/metadata/policies/nia-population-population-status-offering-policy.jsonld rename to metadata/public/metadata/policies/nia-child-benefit-v1-offering-policy.jsonld index 8190c7f..f694e6a 100644 --- a/metadata/public/metadata/policies/nia-population-population-status-offering-policy.jsonld +++ b/metadata/public/metadata/policies/nia-child-benefit-v1-offering-policy.jsonld @@ -2,7 +2,7 @@ "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "nia-population-population-status-offering-policy", + "id": "nia-child-benefit-v1-offering-policy", "permission": [ { "action": "use", @@ -18,7 +18,7 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/nia-population-population-status-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/nia-child-benefit-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/nia-population-population-status-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/nia-child-benefit-v1-offering-policy" } diff --git a/metadata/public/metadata/policies/nia-citizen-status-v1-offering-policy.jsonld b/metadata/public/metadata/policies/nia-citizen-status-v1-offering-policy.jsonld new file mode 100644 index 0000000..df1dbd7 --- /dev/null +++ b/metadata/public/metadata/policies/nia-citizen-status-v1-offering-policy.jsonld @@ -0,0 +1,24 @@ +{ + "@context": { + "odrl": "http://www.w3.org/ns/odrl/2/" + }, + "id": "nia-citizen-status-v1-offering-policy", + "permission": [ + { + "action": "use", + "constraint": [ + { + "leftOperand": "purpose", + "operator": "isAnyOf", + "rightOperand": [ + "https://id.registrystack.org/solmara/purpose/citizen-self-service" + ] + } + ] + } + ], + "profile": "https://www.w3.org/TR/odrl-model/", + "target": "https://id.registrystack.org/solmara/evidence-offerings/nia-citizen-status-v1-offering", + "type": "odrl:Set", + "uid": "https://id.registrystack.org/solmara/policies/nia-citizen-status-v1-offering-policy" +} diff --git a/metadata/public/metadata/policies/sipf-pensions-pension-case-offering-policy.jsonld b/metadata/public/metadata/policies/sipf-pension-payment-v1-offering-policy.jsonld similarity index 69% rename from metadata/public/metadata/policies/sipf-pensions-pension-case-offering-policy.jsonld rename to metadata/public/metadata/policies/sipf-pension-payment-v1-offering-policy.jsonld index ac79939..0161f51 100644 --- a/metadata/public/metadata/policies/sipf-pensions-pension-case-offering-policy.jsonld +++ b/metadata/public/metadata/policies/sipf-pension-payment-v1-offering-policy.jsonld @@ -2,7 +2,7 @@ "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "sipf-pensions-pension-case-offering-policy", + "id": "sipf-pension-payment-v1-offering-policy", "permission": [ { "action": "use", @@ -11,15 +11,14 @@ "leftOperand": "purpose", "operator": "isAnyOf", "rightOperand": [ - "https://id.registrystack.org/solmara/purpose/pension-payment-review", - "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" + "https://id.registrystack.org/solmara/purpose/pension-payment-review" ] } ] } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pensions-pension-case-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/sipf-pension-payment-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/sipf-pensions-pension-case-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/sipf-pension-payment-v1-offering-policy" } diff --git a/metadata/public/metadata/policies/sipf-survivor-benefit-v1-offering-policy.jsonld b/metadata/public/metadata/policies/sipf-survivor-benefit-v1-offering-policy.jsonld new file mode 100644 index 0000000..d4b75b1 --- /dev/null +++ b/metadata/public/metadata/policies/sipf-survivor-benefit-v1-offering-policy.jsonld @@ -0,0 +1,24 @@ +{ + "@context": { + "odrl": "http://www.w3.org/ns/odrl/2/" + }, + "id": "sipf-survivor-benefit-v1-offering-policy", + "permission": [ + { + "action": "use", + "constraint": [ + { + "leftOperand": "purpose", + "operator": "isAnyOf", + "rightOperand": [ + "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" + ] + } + ] + } + ], + "profile": "https://www.w3.org/TR/odrl-model/", + "target": "https://id.registrystack.org/solmara/evidence-offerings/sipf-survivor-benefit-v1-offering", + "type": "odrl:Set", + "uid": "https://id.registrystack.org/solmara/policies/sipf-survivor-benefit-v1-offering-policy" +} diff --git a/metadata/public/metadata/policies/solmara-child-benefit-authority-predicate-collection-policy.jsonld b/metadata/public/metadata/policies/solmara-child-benefit-authority-predicate-collection-policy.jsonld deleted file mode 100644 index faaf612..0000000 --- a/metadata/public/metadata/policies/solmara-child-benefit-authority-predicate-collection-policy.jsonld +++ /dev/null @@ -1,24 +0,0 @@ -{ - "@context": { - "odrl": "http://www.w3.org/ns/odrl/2/" - }, - "id": "solmara-child-benefit-authority-predicate-collection-policy", - "permission": [ - { - "action": "use", - "constraint": [ - { - "leftOperand": "purpose", - "operator": "isAnyOf", - "rightOperand": [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" - ] - } - ] - } - ], - "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/child-benefit-authority-predicate-collection", - "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/solmara-child-benefit-authority-predicate-collection-policy" -} diff --git a/metadata/public/metadata/policies/mosd-programme-beneficiary-enrollment-offering-policy.jsonld b/metadata/public/metadata/policies/sro-child-benefit-v1-offering-policy.jsonld similarity index 68% rename from metadata/public/metadata/policies/mosd-programme-beneficiary-enrollment-offering-policy.jsonld rename to metadata/public/metadata/policies/sro-child-benefit-v1-offering-policy.jsonld index 20e9063..e73b8b9 100644 --- a/metadata/public/metadata/policies/mosd-programme-beneficiary-enrollment-offering-policy.jsonld +++ b/metadata/public/metadata/policies/sro-child-benefit-v1-offering-policy.jsonld @@ -2,7 +2,7 @@ "@context": { "odrl": "http://www.w3.org/ns/odrl/2/" }, - "id": "mosd-programme-beneficiary-enrollment-offering-policy", + "id": "sro-child-benefit-v1-offering-policy", "permission": [ { "action": "use", @@ -18,7 +18,7 @@ } ], "profile": "https://www.w3.org/TR/odrl-model/", - "target": "https://id.registrystack.org/solmara/evidence-offerings/mosd-programme-beneficiary-enrollment-offering", + "target": "https://id.registrystack.org/solmara/evidence-offerings/sro-child-benefit-v1-offering", "type": "odrl:Set", - "uid": "https://id.registrystack.org/solmara/policies/mosd-programme-beneficiary-enrollment-offering-policy" + "uid": "https://id.registrystack.org/solmara/policies/sro-child-benefit-v1-offering-policy" } diff --git a/metadata/solmara-wave1.metadata.yaml b/metadata/solmara-wave1.metadata.yaml index e613e67..219ad77 100644 --- a/metadata/solmara-wave1.metadata.yaml +++ b/metadata/solmara-wave1.metadata.yaml @@ -1,11 +1,11 @@ schema_version: registry-manifest/v1 catalog: - id: solmara-wave1-multi-authority + id: solmara-authority-cells base_url: https://metadata.solmara.registrystack.org title: - en: Solmara Lab Wave 1 Service Catalogue + en: Solmara authority-owned Evidence catalogue description: - en: One merged CPSV-AP metadata publish for Solmara Wave 1 registry and evidence services. + en: Six authority-owned Evidence cells, five governed Relay V2 services, and the programme applications that compose signed assertions. publisher: name: Solmara Interoperability Metadata Publisher iri: did:web:id.registrystack.org:solmara:authority:metadata @@ -50,385 +50,280 @@ authorities: authority_type: eli:PublicAuthority spatial: https://id.registrystack.org/solmara/jurisdiction/XS requirements: - - id: child-benefit-review-requirement - iri: https://id.registrystack.org/solmara/requirements/child-benefit-review - title: - en: Child benefit eligibility review - description: - en: Confirm registered birth, under-five age, household poverty status, and duplicate enrollment status. + - id: cra-child-benefit-v1 + iri: https://id.registrystack.org/solmara/requirement/cra-child-benefit/v1 + title: {en: CRA child benefit evidence} + description: {en: Confirm registered birth and age under five from CRA's immutable birth extract.} evidence_type_lists: - - id: child-benefit-wave1-evidence - title: - en: Child benefit registry evidence - evidence_types: - - birth-registration-evidence - - population-status-evidence - - household-poverty-evidence - - beneficiary-enrollment-evidence - - id: pension-survivor-review-requirement - iri: https://id.registrystack.org/solmara/requirements/pension-survivor-review - title: - en: Pension stop and survivor benefit review - description: - en: Confirm death registration, active pension payment state, and survivor eligibility. + - id: cra-child-benefit-list + title: {en: CRA child benefit evidence} + evidence_types: [cra-child-benefit-v1] + - id: cra-pension-death-v1 + iri: https://id.registrystack.org/solmara/requirement/cra-pension-death/v1 + title: {en: CRA pension death status} + description: {en: Confirm death registration through CRA's governed Relay lookup.} evidence_type_lists: - - id: pension-wave1-evidence - title: - en: Pension and survivor registry evidence - evidence_types: - - death-registration-evidence - - pension-case-evidence - - id: nagdi-voucher-requirement - iri: https://id.registrystack.org/solmara/requirements/nagdi-voucher - title: - en: Farmer climate-smart voucher review - description: - en: Confirm farmer registration, data-use authorization, parcel, crop, climate-risk, and redemption predicates. + - id: cra-pension-death-list + title: {en: CRA death-status evidence} + evidence_types: [cra-death-status-v1] + - id: cra-citizen-record-v1 + iri: https://id.registrystack.org/solmara/requirement/cra-citizen-record/v1 + title: {en: CRA citizen record link} + description: {en: Confirm a linked civil record through CRA's governed Relay lookup.} evidence_type_lists: - - id: nagdi-voucher-evidence - title: - en: Farmer voucher evidence - evidence_types: - - farmer-voucher-evidence - - id: livestock-movement-requirement - iri: https://id.registrystack.org/solmara/requirements/livestock-movement - title: - en: Livestock movement control review - description: - en: Confirm herd registration, vaccination, species-aware quarantine, destination, and open permit predicates. + - id: cra-citizen-record-list + title: {en: CRA civil-record evidence} + evidence_types: [civil-record-linked-v1] + - id: nia-child-benefit-v1 + iri: https://id.registrystack.org/solmara/requirement/nia-child-benefit/v1 + title: {en: NIA child benefit population status} + description: {en: Confirm active population status from NIA's immutable population extract.} evidence_type_lists: - - id: livestock-movement-evidence-list - title: - en: Livestock movement evidence - evidence_types: - - livestock-movement-evidence + - id: nia-child-benefit-list + title: {en: NIA population evidence} + evidence_types: [population-active-v1] + - id: nia-citizen-status-v1 + iri: https://id.registrystack.org/solmara/requirement/nia-citizen-status/v1 + title: {en: NIA citizen population status} + description: {en: Confirm active citizen population status from NIA's immutable population extract.} + evidence_type_lists: + - id: nia-citizen-status-list + title: {en: NIA citizen-status evidence} + evidence_types: [citizen-population-active-v1] + - id: sro-child-benefit-v1 + iri: https://id.registrystack.org/solmara/requirement/sro-child-benefit/v1 + title: {en: SRO poverty priority} + description: {en: Confirm household poverty priority from SRO's immutable poverty extract.} + evidence_type_lists: + - id: sro-child-benefit-list + title: {en: SRO poverty evidence} + evidence_types: [poverty-priority-v1] + - id: mosd-child-benefit-v1 + iri: https://id.registrystack.org/solmara/requirement/mosd-child-benefit/v1 + title: {en: MoSD programme enrolment status} + description: {en: Confirm that the person is not already enrolled through MoSD's governed Relay lookup.} + evidence_type_lists: + - id: mosd-child-benefit-list + title: {en: MoSD enrolment evidence} + evidence_types: [not-enrolled-v1] + - id: sipf-pension-payment-v1 + iri: https://id.registrystack.org/solmara/requirement/sipf-pension-payment/v1 + title: {en: SIPF pension payment status} + description: {en: Confirm active pension payment through SIPF's governed Relay lookup.} + evidence_type_lists: + - id: sipf-pension-payment-list + title: {en: SIPF pension payment evidence} + evidence_types: [pension-payment-active-v1] + - id: sipf-survivor-benefit-v1 + iri: https://id.registrystack.org/solmara/requirement/sipf-survivor-benefit/v1 + title: {en: SIPF survivor eligibility} + description: {en: Confirm survivor eligibility through SIPF's governed Relay lookup.} + evidence_type_lists: + - id: sipf-survivor-benefit-list + title: {en: SIPF survivor evidence} + evidence_types: [survivor-benefit-v1] + - id: nagdi-voucher-v1 + iri: https://id.registrystack.org/solmara/requirement/nagdi-voucher/v1 + title: {en: NAgDI climate-smart voucher} + description: {en: Confirm climate-smart voucher eligibility through NAgDI's governed Relay lookup.} + evidence_type_lists: + - id: nagdi-voucher-list + title: {en: NAgDI voucher evidence} + evidence_types: [climate-smart-voucher-v1] + - id: nagdi-livestock-v1 + iri: https://id.registrystack.org/solmara/requirement/nagdi-livestock/v1 + title: {en: NAgDI livestock movement} + description: {en: Confirm livestock movement eligibility through NAgDI's governed Relay lookup.} + evidence_type_lists: + - id: nagdi-livestock-list + title: {en: NAgDI livestock evidence} + evidence_types: [livestock-movement-v1] evidence_types: - - id: birth-registration-evidence - iri: https://id.registrystack.org/solmara/evidence-types/birth-registration - title: - en: Birth registration evidence - proves: - - child-benefit-review-requirement - information_concepts: - - ps:crvs/Birth - - id: population-status-evidence - iri: https://id.registrystack.org/solmara/evidence-types/population-status - title: - en: Population status evidence - proves: - - child-benefit-review-requirement - information_concepts: - - ps:Person - - id: household-poverty-evidence - iri: https://id.registrystack.org/solmara/evidence-types/household-poverty - title: - en: Household poverty evidence - proves: - - child-benefit-review-requirement + - id: cra-child-benefit-v1 + iri: https://id.registrystack.org/solmara/evidence-type/cra-child-benefit/v1 + title: {en: CRA child benefit assertion} + proves: [cra-child-benefit-v1] information_concepts: - - ps:Household - - ps:SocioEconomicProfile - - id: beneficiary-enrollment-evidence - iri: https://id.registrystack.org/solmara/evidence-types/beneficiary-enrollment - title: - en: Beneficiary enrollment evidence - proves: - - child-benefit-review-requirement + - https://id.registrystack.org/solmara/concept/birth-is-registered + - https://id.registrystack.org/solmara/concept/child-age-under-5 + - id: cra-death-status-v1 + iri: https://id.registrystack.org/solmara/evidence-type/cra-death-status/v1 + title: {en: CRA death-status assertion} + proves: [cra-pension-death-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/person-is-deceased] + - id: civil-record-linked-v1 + iri: https://id.registrystack.org/solmara/evidence-type/civil-record-linked/v1 + title: {en: CRA civil-record assertion} + proves: [cra-citizen-record-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/civil-record-linked] + - id: population-active-v1 + iri: https://id.registrystack.org/solmara/evidence-type/population-active/v1 + title: {en: NIA active-population assertion} + proves: [nia-child-benefit-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/population-record-active] + - id: citizen-population-active-v1 + iri: https://id.registrystack.org/solmara/evidence-type/citizen-population-active/v1 + title: {en: NIA active-citizen assertion} + proves: [nia-citizen-status-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/citizen-population-record-active] + - id: poverty-priority-v1 + iri: https://id.registrystack.org/solmara/evidence-type/poverty-priority/v1 + title: {en: SRO poverty-priority assertion} + proves: [sro-child-benefit-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/household-below-poverty-threshold] + - id: not-enrolled-v1 + iri: https://id.registrystack.org/solmara/evidence-type/not-enrolled/v1 + title: {en: MoSD not-enrolled assertion} + proves: [mosd-child-benefit-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/not-already-enrolled] + - id: pension-payment-active-v1 + iri: https://id.registrystack.org/solmara/evidence-type/pension-payment-active/v1 + title: {en: SIPF active-payment assertion} + proves: [sipf-pension-payment-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/pension-payment-active] + - id: survivor-benefit-v1 + iri: https://id.registrystack.org/solmara/evidence-type/survivor-benefit/v1 + title: {en: SIPF survivor-benefit assertion} + proves: [sipf-survivor-benefit-v1] + information_concepts: [https://id.registrystack.org/solmara/concept/survivor-is-eligible] + - id: climate-smart-voucher-v1 + iri: https://id.registrystack.org/solmara/evidence-type/climate-smart-voucher/v1 + title: {en: NAgDI climate-smart voucher assertion} + proves: [nagdi-voucher-v1] information_concepts: - - ps:sp/Enrollment - - id: child-benefit-authority-predicate-collection-evidence - iri: https://id.registrystack.org/solmara/evidence-types/child-benefit-authority-predicate-collection - title: - en: Child benefit authority predicate collection - description: - en: A transient collection of source-owned child benefit predicates with no copied source rows or eligibility decision. - proves: - - child-benefit-review-requirement + - https://id.registrystack.org/solmara/concept/farmer-registered + - https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose + - https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher + - id: livestock-movement-v1 + iri: https://id.registrystack.org/solmara/evidence-type/livestock-movement/v1 + title: {en: NAgDI livestock movement assertion} + proves: [nagdi-livestock-v1] information_concepts: - - ps:Person - - id: death-registration-evidence - iri: https://id.registrystack.org/solmara/evidence-types/death-registration - title: - en: Death registration evidence - proves: - - pension-survivor-review-requirement - information_concepts: - - ps:crvs/Death - - id: pension-case-evidence - iri: https://id.registrystack.org/solmara/evidence-types/pension-case - title: - en: Pension case evidence - proves: - - pension-survivor-review-requirement - - id: farmer-voucher-evidence - iri: https://id.registrystack.org/solmara/evidence-types/farmer-voucher - title: - en: Farmer voucher evidence - proves: - - nagdi-voucher-requirement - information_concepts: - - ps:Farm - - id: livestock-movement-evidence - iri: https://id.registrystack.org/solmara/evidence-types/livestock-movement - title: - en: Livestock movement evidence - proves: - - livestock-movement-requirement + - https://id.registrystack.org/solmara/concept/registered-herd + - https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species + - https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit public_services: - id: child-benefit-review iri: https://id.registrystack.org/solmara/services/child-benefit-review - title: - en: Child benefit eligibility review - description: - en: MoSD review of child benefit eligibility using minimized registry evidence. + title: {en: Child benefit eligibility review} + description: {en: The programme composes four independently signed authority assertions into five reviewed concepts.} competent_authority: mosd-programme-mis jurisdiction: https://id.registrystack.org/solmara/jurisdiction/XS - holds_requirements: - - child-benefit-review-requirement - data_services: - - child-benefit-evidence-collector-api - - cra-notary-api - - nia-notary-api - - sro-notary-api - - programme-notary-api + holds_requirements: [cra-child-benefit-v1, nia-child-benefit-v1, sro-child-benefit-v1, mosd-child-benefit-v1] + data_services: [cra-evidence-api, nia-evidence-api, sro-evidence-api, mosd-programme-evidence-api] - id: pension-survivor-review iri: https://id.registrystack.org/solmara/services/pension-survivor-review - title: - en: Pension stop and survivor benefit review - description: - en: SIPF review for pension payment stop and survivor benefit eligibility. + title: {en: Pension stop and survivor benefit review} + description: {en: The programme composes CRA and SIPF signed assertions without exposing unrelated civil data.} competent_authority: sipf jurisdiction: https://id.registrystack.org/solmara/jurisdiction/XS - holds_requirements: - - pension-survivor-review-requirement - data_services: - - cra-notary-api - - sipf-notary-api - - id: nagdi-voucher-review - iri: https://id.registrystack.org/solmara/services/nagdi-voucher-review - title: - en: Farmer climate-smart voucher review - description: - en: NAgDI review of farmer eligibility for climate-smart input support. - competent_authority: nagdi + holds_requirements: [cra-pension-death-v1, sipf-pension-payment-v1, sipf-survivor-benefit-v1] + data_services: [cra-evidence-api, sipf-evidence-api] + - id: citizen-self-service + iri: https://id.registrystack.org/solmara/services/citizen-self-service + title: {en: Citizen self-service} + description: {en: The application composes separately signed CRA and NIA citizen assertions.} + competent_authority: nia jurisdiction: https://id.registrystack.org/solmara/jurisdiction/XS - holds_requirements: - - nagdi-voucher-requirement - data_services: - - nagdi-notary-api - - id: livestock-movement-control - iri: https://id.registrystack.org/solmara/services/livestock-movement-control - title: - en: Livestock movement control - description: - en: NAgDI review of livestock movement permit eligibility and quarantine controls. + holds_requirements: [cra-citizen-record-v1, nia-citizen-status-v1] + data_services: [cra-evidence-api, nia-evidence-api] + - id: agriculture-review + iri: https://id.registrystack.org/solmara/services/agriculture-review + title: {en: Agriculture voucher and livestock review} + description: {en: NAgDI applications consume independently signed voucher and livestock assertions.} competent_authority: nagdi jurisdiction: https://id.registrystack.org/solmara/jurisdiction/XS - holds_requirements: - - livestock-movement-requirement - data_services: - - nagdi-notary-api + holds_requirements: [nagdi-voucher-v1, nagdi-livestock-v1] + data_services: [nagdi-evidence-api] data_services: - - id: child-benefit-evidence-collector-api - iri: https://child-benefit-federator.solmara.registrystack.org/v1/evaluations - title: - en: Child benefit authority predicate collection API - description: - en: Returns application/json collections assembled from source-Notary predicates without composing an eligibility decision. - endpoint_url: https://child-benefit-federator.solmara.registrystack.org/v1/evaluations - endpoint_description: https://child-benefit-federator.solmara.registrystack.org/v1/claims - conforms_to: https://id.registrystack.org/solmara/contracts/authority-predicate-collection/v1 - - id: cra-notary-api - iri: https://cra-notary.solmara.registrystack.org/v1/evaluations - title: - en: CRA evidence API - endpoint_url: https://cra-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://cra-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 - - id: nia-notary-api - iri: https://nia-notary.solmara.registrystack.org/v1/evaluations - title: - en: NIA evidence API - endpoint_url: https://nia-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://nia-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 - - id: sro-notary-api - iri: https://sro-notary.solmara.registrystack.org/v1/evaluations - title: - en: SRO evidence API - endpoint_url: https://sro-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://sro-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 - - id: programme-notary-api - iri: https://programme-notary.solmara.registrystack.org/v1/evaluations - title: - en: Programme MIS evidence API - endpoint_url: https://programme-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://programme-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 - - id: sipf-notary-api - iri: https://sipf-notary.solmara.registrystack.org/v1/evaluations - title: - en: SIPF evidence API - endpoint_url: https://sipf-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://sipf-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 - - id: nagdi-notary-api - iri: https://nagdi-notary.solmara.registrystack.org/v1/evaluations - title: - en: NAgDI evidence API - endpoint_url: https://nagdi-notary.solmara.registrystack.org/v1/evaluations - endpoint_description: https://nagdi-notary.solmara.registrystack.org/openapi.json - conforms_to: https://id.registrystack.org/spec/registry-notary/evidence/v1 + - id: cra-evidence-api + iri: https://cra-evidence.solmara.registrystack.org/v1/evidence + title: {en: CRA Evidence cell} + endpoint_url: https://cra-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://cra-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 + - id: nia-evidence-api + iri: https://nia-evidence.solmara.registrystack.org/v1/evidence + title: {en: NIA Evidence cell} + endpoint_url: https://nia-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://nia-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 + - id: sro-evidence-api + iri: https://sro-evidence.solmara.registrystack.org/v1/evidence + title: {en: SRO Evidence cell} + endpoint_url: https://sro-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://sro-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 + - id: mosd-programme-evidence-api + iri: https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence + title: {en: MoSD Programme Evidence cell} + endpoint_url: https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://mosd-programme-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 + - id: sipf-evidence-api + iri: https://sipf-evidence.solmara.registrystack.org/v1/evidence + title: {en: SIPF Evidence cell} + endpoint_url: https://sipf-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://sipf-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 + - id: nagdi-evidence-api + iri: https://nagdi-evidence.solmara.registrystack.org/v1/evidence + title: {en: NAgDI Evidence cell} + endpoint_url: https://nagdi-evidence.solmara.registrystack.org/v1/evidence + endpoint_description: https://nagdi-evidence.solmara.registrystack.org/v1/evidence-definitions + conforms_to: https://id.registrystack.org/spec/registry-evidence/v1 datasets: - id: cra-civil - title: - en: Civil registration + title: {en: CRA civil registration publication} + description: {en: CRA-owned immutable birth extract and governed civil-person Relay source.} access_rights: restricted entities: - name: civil_person - title: - en: Civil person - identifiers: - - name: uin - kind: uin - fields: - - name: uin - type: string - required: false - concepts: - - ps:Person.identifier - - name: birth_brn - type: string - required: false - concepts: - - ps:crvs/Birth.registrationNumber - - name: death_drn - type: string - required: false - concepts: - - ps:crvs/Death.registrationNumber - - name: deceased - type: boolean - required: true - evidence_offerings: - - id: cra-birth-registration-offering - iri: https://id.registrystack.org/solmara/evidence-offerings/cra-birth-registration - title: - en: CRA birth registration evidence offering - evidence_type: birth-registration-evidence - issuing_authority: - id: cra - iri: did:web:id.registrystack.org:solmara:authority:cra - name: Civil Registration Authority - country: XS - entity: civil_person - lookup_keys: - - uin - procedure_contexts: - - child-benefit-review - access: - kind: evidence-verification-api - conforms_to: https://spec.openapis.org/oas/v3.1.0 - endpoint_url: https://cra-notary.solmara.registrystack.org/v1/evaluations - discovery_url: https://cra-notary.solmara.registrystack.org/.well-known/evidence-service - ruleset: birth-is-registered - policy: - purpose: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - id: cra-death-registration-offering - iri: https://id.registrystack.org/solmara/evidence-offerings/cra-death-registration - title: - en: CRA death registration evidence offering - evidence_type: death-registration-evidence - issuing_authority: - id: cra - iri: did:web:id.registrystack.org:solmara:authority:cra - name: Civil Registration Authority - country: XS - entity: civil_person - lookup_keys: - - uin - procedure_contexts: - - pension-survivor-review - access: - kind: evidence-verification-api - conforms_to: https://spec.openapis.org/oas/v3.1.0 - endpoint_url: https://cra-notary.solmara.registrystack.org/v1/evaluations - discovery_url: https://cra-notary.solmara.registrystack.org/.well-known/evidence-service - ruleset: person-is-deceased - policy: - purpose: - - https://id.registrystack.org/solmara/purpose/pension-payment-review + identifiers: [{name: uin, kind: uin}] + fields: [{name: uin, type: string, required: true}] - id: nia-population - title: - en: Population register + title: {en: NIA population publication} + description: {en: NIA-owned immutable population extract and governed eSignet Relay source.} access_rights: restricted entities: - - name: person + - name: population_person + identifiers: [{name: uin, kind: uin}] + fields: [{name: uin, type: string, required: true}] - id: sro-social - title: - en: Social registry + title: {en: SRO poverty extract} + description: {en: SRO-owned immutable poverty publication.} access_rights: restricted entities: - - name: household + - name: poverty_record + identifiers: [{name: uin, kind: uin}] + fields: [{name: uin, type: string, required: true}] - id: mosd-programme - title: - en: Integrated beneficiary registry + title: {en: MoSD programme enrolment publication} + description: {en: MoSD-owned mutable SQLite source consulted through Relay V2.} access_rights: restricted entities: - - name: enrollment + - name: beneficiary_enrolment + identifiers: [{name: uin, kind: uin}] + fields: [{name: uin, type: string, required: true}] - id: sipf-pensions - title: - en: Pensions and social insurance + title: {en: SIPF pension and survivor publication} + description: {en: SIPF-owned mutable SQLite source consulted through Relay V2.} access_rights: restricted entities: - - name: pension_case + - name: pension_payment + identifiers: [{name: pensioner_uin, kind: uin}] + fields: [{name: pensioner_uin, type: string, required: true}] + - name: survivor_case + identifiers: [{name: spouse_uin, kind: uin}] + fields: [{name: spouse_uin, type: string, required: true}] - id: nagdi-agriculture - title: - en: Farmer and livestock registries + title: {en: NAgDI agriculture publication} + description: {en: NAgDI-owned mutable SQLite source consulted through Relay V2.} access_rights: restricted entities: - name: farmer_voucher + identifiers: [{name: farmer_id, kind: local}] + fields: [{name: farmer_id, type: string, required: true}] - name: livestock_movement -evaluation_profiles: - - id: birth-is-registered - ruleset: birth-is-registered-v1 - claim_id: birth-is-registered - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: population-record-active - ruleset: population-record-active-v1 - claim_id: population-record-active - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: child-age-under-5 - ruleset: child-age-under-5-v1 - claim_id: child-age-under-5 - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: household-below-poverty-threshold - ruleset: household-below-poverty-threshold-v1 - claim_id: household-below-poverty-threshold - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: not-already-enrolled - ruleset: not-already-enrolled-v1 - claim_id: not-already-enrolled - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: survivor-benefit-eligibility - ruleset: survivor-benefit-eligibility-v1 - claim_id: survivor-is-eligible - subject_id_type: uin - max_source_observed_age_seconds: 86400 - - id: climate-smart-voucher-eligibility - ruleset: climate-smart-voucher-eligibility-v1 - claim_id: eligible-for-climate-smart-input-voucher - subject_id_type: farmer_id - max_source_observed_age_seconds: 86400 - - id: livestock-movement-permit - ruleset: livestock-movement-permit-v1 - claim_id: eligible-for-livestock-movement-permit - subject_id_type: farmer_id - max_source_observed_age_seconds: 86400 + identifiers: [{name: farmer_id, kind: local}] + fields: [{name: farmer_id, type: string, required: true}] diff --git a/perf/README.md b/perf/README.md deleted file mode 100644 index f28261c..0000000 --- a/perf/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# Solmara Lab Performance Harness - -This k6 harness exercises four live evaluation paths through the Solmara demo -stack: the Child Benefit Federator and the CRA, SIPF, and NAgDI authority-owned -Notaries. It assumes the lab has generated `.env` secrets and the local Docker -Compose topology is running. - -## Start the Lab - -```bash -just setup -just generate -just up -``` - -Wait for the standard smoke prerequisites to pass before collecting a baseline: - -```bash -just smoke -``` - -## Run with Local k6 - -```bash -set -a -. .env -set +a -mkdir -p output/perf/results output/perf/reports - -k6 run perf/k6/notary_relay_backed.js -``` - -The default profile is `smoke`. It intentionally uses a small VU count and a -short think time to validate routes without creating load. It is not a capacity -measurement. - -The generated `.env` supplies the required bearer tokens: - -- `CHILD_BENEFIT_FEDERATOR_TOKEN` -- `CRA_PENSION_CLIENT_TOKEN` -- `SIPF_PENSION_CLIENT_TOKEN` -- `NAGDI_NOTARY_TOKEN` - -The scenario uses these target URLs and local defaults: - -| Target | Environment variable | Default | -| --- | --- | --- | -| Child Benefit Federator | `CHILD_BENEFIT_FEDERATOR_URL` | `http://127.0.0.1:4321` | -| CRA Notary | `CRA_NOTARY_URL` | `http://127.0.0.1:4325` | -| SIPF Notary | `SIPF_NOTARY_URL` | `http://127.0.0.1:4322` | -| NAgDI Notary | `NAGDI_NOTARY_URL` | `http://127.0.0.1:4323` | - -Run a capacity baseline with an explicit arrival rate: - -```bash -REGISTRY_LAB_PROFILE=capacity \ -REGISTRY_LAB_DURATION=2m \ -REGISTRY_LAB_RATE=200 \ -REGISTRY_LAB_PRE_ALLOCATED_VUS=64 \ -REGISTRY_LAB_MAX_VUS=400 \ -k6 run perf/k6/notary_relay_backed.js -``` - -Run a breakpoint ramp: - -```bash -REGISTRY_LAB_PROFILE=breakpoint \ -REGISTRY_LAB_STAGES=1m:100,1m:200,1m:400,30s:0 \ -REGISTRY_LAB_PRE_ALLOCATED_VUS=64 \ -REGISTRY_LAB_MAX_VUS=400 \ -k6 run perf/k6/notary_relay_backed.js -``` - -## Run with Docker k6 - -Docker Desktop on macOS does not support `--network host` the same way Linux -does. Use `host.docker.internal` for loopback services: - -```bash -docker run --rm \ - --env-file .env \ - -e CHILD_BENEFIT_FEDERATOR_URL=http://host.docker.internal:4321 \ - -e CRA_NOTARY_URL=http://host.docker.internal:4325 \ - -e SIPF_NOTARY_URL=http://host.docker.internal:4322 \ - -e NAGDI_NOTARY_URL=http://host.docker.internal:4323 \ - -v "$PWD:/workspace" \ - -w /workspace \ - grafana/k6:0.57.0 run perf/k6/notary_relay_backed.js -``` - -## Profiles - -The scenario is profile-aware via `REGISTRY_LAB_PROFILE`: - -- `smoke`: `constant-vus`, defaults to `REGISTRY_LAB_VUS=4`, - `REGISTRY_LAB_DURATION=30s`, and `REGISTRY_LAB_THINK_TIME_SECONDS=0.1`. - This catches broken routes, auth drift, and basic latency regressions. -- `capacity`: `constant-arrival-rate`, defaults to the script's baseline target - rate and `REGISTRY_LAB_THINK_TIME_SECONDS=0`. Use this for comparable - requests-per-second baselines. -- `breakpoint`: `ramping-arrival-rate`, defaults to the script's ramp stages - and `REGISTRY_LAB_THINK_TIME_SECONDS=0`. Use this to find the first target - rate where latency or error thresholds fail. - -Common overrides: - -- `REGISTRY_LAB_PROFILE=smoke|capacity|breakpoint` -- `REGISTRY_LAB_DURATION=30s` -- `REGISTRY_LAB_VUS=4` -- `REGISTRY_LAB_RATE=200` -- `REGISTRY_LAB_PRE_ALLOCATED_VUS=64` -- `REGISTRY_LAB_MAX_VUS=400` -- `REGISTRY_LAB_START_RATE=0` -- `REGISTRY_LAB_THINK_TIME_SECONDS=0` -- `REGISTRY_LAB_STAGES=1m:100,1m:200,1m:400,30s:0` - -Keep hosted or shared environments opt-in; the default URLs are local loopback -ports. - -`notary_relay_backed` defaults to `200 req/s` for capacity runs and ramps from -`100` to `200` to `400 req/s` for breakpoint runs. - -## Reports - -The script writes: - -- `output/perf/results/.json` -- `output/perf/reports/.txt` - -The text summaries print the active profile, think time, rate/count gauges, -latency distributions (`avg`, `med`, `p90`, `p95`, `p99`, `max`), and -status-code counters. Status-specific counters are emitted as first-class -metrics because k6 does not include tag cardinality in the compact text summary. diff --git a/perf/k6/lib/common.js b/perf/k6/lib/common.js deleted file mode 100644 index 60e11cd..0000000 --- a/perf/k6/lib/common.js +++ /dev/null @@ -1,336 +0,0 @@ -import { Counter, Rate } from 'k6/metrics'; -import { sleep } from 'k6'; - -export const PURPOSE = 'https://id.registrystack.org/solmara/purpose/citizen-self-service'; -export const CHILD_BENEFIT_PURPOSE = 'https://id.registrystack.org/solmara/purpose/child-benefit-review'; -export const PENSION_PAYMENT_PURPOSE = 'https://id.registrystack.org/solmara/purpose/pension-payment-review'; -export const SURVIVOR_BENEFIT_PURPOSE = 'https://id.registrystack.org/solmara/purpose/survivor-benefit-determination'; -export const VOUCHER_PURPOSE = 'https://id.registrystack.org/solmara/purpose/voucher-eligibility-review'; -export const LIVESTOCK_PURPOSE = 'https://id.registrystack.org/solmara/purpose/livestock-movement-control'; -export const CLAIM_RESULT = 'application/vnd.registry-notary.claim-result+json'; -export const SD_JWT = 'application/dc+sd-jwt'; - -export const unexpectedStatus = new Counter('registry_lab_unexpected_status_total'); -export const checkFailureRate = new Rate('registry_lab_check_failure_rate'); -export const httpStatusTotal = new Counter('registry_lab_http_status_total'); -export const httpStatus200 = new Counter('registry_lab_http_status_200_total'); -export const httpStatus201 = new Counter('registry_lab_http_status_201_total'); -export const httpStatus204 = new Counter('registry_lab_http_status_204_total'); -export const httpStatus400 = new Counter('registry_lab_http_status_400_total'); -export const httpStatus401 = new Counter('registry_lab_http_status_401_total'); -export const httpStatus403 = new Counter('registry_lab_http_status_403_total'); -export const httpStatus404 = new Counter('registry_lab_http_status_404_total'); -export const httpStatus409 = new Counter('registry_lab_http_status_409_total'); -export const httpStatus422 = new Counter('registry_lab_http_status_422_total'); -export const httpStatus429 = new Counter('registry_lab_http_status_429_total'); -export const httpStatus500 = new Counter('registry_lab_http_status_500_total'); -export const httpStatus502 = new Counter('registry_lab_http_status_502_total'); -export const httpStatus503 = new Counter('registry_lab_http_status_503_total'); -export const httpStatus504 = new Counter('registry_lab_http_status_504_total'); -export const httpStatusOther = new Counter('registry_lab_http_status_other_total'); - -export function env(name, fallback = '') { - const value = __ENV[name]; - if (value === undefined || value === null || value === '') { - return fallback; - } - return value; -} - -export function requiredEnv(name) { - const value = env(name); - if (value === '') { - throw new Error(`missing required environment variable ${name}`); - } - return value; -} - -export function duration() { - return env('REGISTRY_LAB_DURATION', '30s'); -} - -export function profile() { - return env('REGISTRY_LAB_PROFILE', 'smoke'); -} - -export function vus(fallback = 4) { - return Number(env('REGISTRY_LAB_VUS', String(fallback))); -} - -export function rate(fallback = 8) { - return Number(env('REGISTRY_LAB_RATE', String(fallback))); -} - -export function preAllocatedVus(fallback = 8) { - return Number(env('REGISTRY_LAB_PRE_ALLOCATED_VUS', String(fallback))); -} - -export function maxVus(fallback = 32) { - return Number(env('REGISTRY_LAB_MAX_VUS', String(fallback))); -} - -export function thinkTimeSeconds() { - const fallback = profile() === 'smoke' ? '0.1' : '0'; - return Number(env('REGISTRY_LAB_THINK_TIME_SECONDS', fallback)); -} - -export function commonOptions(thresholds = {}) { - return { - discardResponseBodies: false, - summaryTrendStats: ['avg', 'min', 'med', 'p(90)', 'p(95)', 'p(99)', 'max'], - thresholds: { - checks: ['rate>=0.99'], - registry_lab_check_failure_rate: ['rate<0.01'], - registry_lab_unexpected_status_total: ['count==0'], - ...thresholds, - }, - }; -} - -export function sleepIfConfigured() { - const seconds = thinkTimeSeconds(); - if (seconds > 0) { - sleep(seconds); - } -} - -export function loopScenario(extra = {}) { - const { vusDefault, rateDefault, preAllocatedVusDefault, maxVusDefault, stages, ...scenario } = extra; - return { - executor: 'constant-vus', - vus: vus(vusDefault || 4), - duration: duration(), - gracefulStop: '10s', - ...scenario, - }; -} - -export function arrivalScenario(extra = {}) { - const { rateDefault, preAllocatedVusDefault, maxVusDefault, stages, ...scenario } = extra; - return { - executor: 'constant-arrival-rate', - rate: rate(rateDefault || 8), - timeUnit: '1s', - duration: duration(), - preAllocatedVUs: preAllocatedVus(preAllocatedVusDefault || 8), - maxVUs: maxVus(maxVusDefault || 32), - gracefulStop: '10s', - ...scenario, - }; -} - -export function rampingArrivalScenario(extra = {}) { - const { rateDefault, preAllocatedVusDefault, maxVusDefault, stages: defaultStages, ...scenario } = extra; - const stages = parseStages( - env('REGISTRY_LAB_STAGES'), - defaultStages || [ - { duration: '1m', target: rate(rateDefault || 8) }, - { duration: '1m', target: rate(rateDefault || 8) * 2 }, - { duration: '1m', target: rate(rateDefault || 8) * 4 }, - { duration: '30s', target: 0 }, - ], - ); - return { - executor: 'ramping-arrival-rate', - startRate: Number(env('REGISTRY_LAB_START_RATE', '0')), - timeUnit: '1s', - stages, - preAllocatedVUs: preAllocatedVus(preAllocatedVusDefault || 8), - maxVUs: maxVus(maxVusDefault || 32), - gracefulStop: '10s', - ...scenario, - }; -} - -export function profiledScenario(extra = {}) { - switch (profile()) { - case 'capacity': - return arrivalScenario(extra); - case 'breakpoint': - return rampingArrivalScenario(extra); - case 'smoke': - return loopScenario(extra); - default: - throw new Error(`unsupported REGISTRY_LAB_PROFILE ${profile()}`); - } -} - -function parseStages(value, fallback) { - if (!value) { - return fallback; - } - return value.split(',').map((stage) => { - const [durationValue, targetValue] = stage.split(':'); - if (!durationValue || !targetValue) { - throw new Error(`invalid REGISTRY_LAB_STAGES entry ${stage}; expected duration:target`); - } - return { - duration: durationValue, - target: Number(targetValue), - }; - }); -} - -export function bearerHeaders(token, purpose = PURPOSE, accept = 'application/json') { - return { - Authorization: `Bearer ${token}`, - Accept: accept, - 'Data-Purpose': purpose, - 'X-Request-Id': requestId(), - }; -} - -export function jsonHeaders(token, purpose = PURPOSE, accept = CLAIM_RESULT) { - return { - ...bearerHeaders(token, purpose, accept), - 'Content-Type': 'application/json', - }; -} - -export function requestId() { - return `solmara-lab-perf-${__VU}-${__ITER}-${Date.now()}`; -} - -export function target(subjectId, scheme = 'solmara_uin') { - return { - type: 'Person', - identifiers: [{ scheme, value: subjectId }], - }; -} - -export function evaluationPayload(subjectId, claim, disclosure = 'predicate', format = CLAIM_RESULT, scheme = 'solmara_uin') { - return JSON.stringify({ - target: target(subjectId, scheme), - claims: [claim], - disclosure, - format, - }); -} - -export function parseJson(response) { - try { - return response.json(); - } catch (_) { - return {}; - } -} - -export function recordStatus(ok, response, expected) { - recordHttpStatus(response); - checkFailureRate.add(!ok); - if (!ok) { - unexpectedStatus.add(1, { - status: String(response.status), - expected: Array.isArray(expected) ? expected.join(',') : String(expected), - }); - } -} - -export function recordHttpStatus(response) { - const status = Number(response.status); - httpStatusTotal.add(1, { status: String(status) }); - switch (status) { - case 200: - httpStatus200.add(1); - break; - case 201: - httpStatus201.add(1); - break; - case 204: - httpStatus204.add(1); - break; - case 400: - httpStatus400.add(1); - break; - case 401: - httpStatus401.add(1); - break; - case 403: - httpStatus403.add(1); - break; - case 404: - httpStatus404.add(1); - break; - case 409: - httpStatus409.add(1); - break; - case 422: - httpStatus422.add(1); - break; - case 429: - httpStatus429.add(1); - break; - case 500: - httpStatus500.add(1); - break; - case 502: - httpStatus502.add(1); - break; - case 503: - httpStatus503.add(1); - break; - case 504: - httpStatus504.add(1); - break; - default: - httpStatusOther.add(1); - break; - } -} - -function metricLine(name, metric) { - if (!metric || !metric.values) { - return `${name}: {}`; - } - const values = metric.values; - const fields = []; - for (const [label, key] of [ - ['count', 'count'], - ['rate', 'rate'], - ['value', 'value'], - ['avg', 'avg'], - ['min', 'min'], - ['med', 'med'], - ['p90', 'p(90)'], - ['p95', 'p(95)'], - ['p99', 'p(99)'], - ['max', 'max'], - ]) { - if (values[key] !== undefined) { - fields.push(`${label}=${formatMetricValue(values[key])}`); - } - } - if (fields.length === 0) { - return `${name}: ${JSON.stringify(values)}`; - } - return `${name}: ${fields.join(' ')}`; -} - -function formatMetricValue(value) { - if (typeof value !== 'number') { - return String(value); - } - if (Number.isInteger(value)) { - return String(value); - } - return value.toFixed(4); -} - -export function summaryFor(name) { - return (data) => { - const lines = [ - `${name} summary`, - `profile: ${profile()}`, - `think_time_seconds: ${thinkTimeSeconds().toFixed(3)}`, - ]; - const metricNames = Object.keys(data.metrics || {}).sort(); - for (const metricName of metricNames) { - lines.push(metricLine(metricName, data.metrics[metricName])); - } - return { - stdout: `${lines.join('\n')}\n`, - [`output/perf/results/${name}.json`]: JSON.stringify(data, null, 2), - [`output/perf/reports/${name}.txt`]: `${lines.join('\n')}\n`, - }; - }; -} diff --git a/perf/k6/notary_relay_backed.js b/perf/k6/notary_relay_backed.js deleted file mode 100644 index edd194a..0000000 --- a/perf/k6/notary_relay_backed.js +++ /dev/null @@ -1,110 +0,0 @@ -import http from 'k6/http'; -import { check } from 'k6'; -import { - CLAIM_RESULT, - CHILD_BENEFIT_PURPOSE, - PENSION_PAYMENT_PURPOSE, - VOUCHER_PURPOSE, - commonOptions, - env, - evaluationPayload, - jsonHeaders, - parseJson, - profiledScenario, - recordStatus, - requiredEnv, - sleepIfConfigured, - summaryFor, -} from './lib/common.js'; - -const childBenefitFederatorUrl = env('CHILD_BENEFIT_FEDERATOR_URL', 'http://127.0.0.1:4321'); -const craNotaryUrl = env('CRA_NOTARY_URL', 'http://127.0.0.1:4325'); -const sipfNotaryUrl = env('SIPF_NOTARY_URL', 'http://127.0.0.1:4322'); -const nagdiNotaryUrl = env('NAGDI_NOTARY_URL', 'http://127.0.0.1:4323'); -const childBenefitToken = requiredEnv('CHILD_BENEFIT_FEDERATOR_TOKEN'); -const craPensionToken = requiredEnv('CRA_PENSION_CLIENT_TOKEN'); -const sipfPensionToken = requiredEnv('SIPF_PENSION_CLIENT_TOKEN'); -const nagdiToken = requiredEnv('NAGDI_NOTARY_TOKEN'); - -const uinSubjects = ['2300010248', '2300091305', '2300036523', '2300073046']; -const farmerSubjects = ['FR-1001', 'FR-1002', 'FR-1003', 'FR-1004']; - -export const options = { - ...commonOptions({ - http_req_failed: ['rate<0.01'], - http_req_duration: ['p(95)<1500'], - }), - scenarios: { - notary_relay_backed: profiledScenario({ - rateDefault: 200, - preAllocatedVusDefault: 64, - maxVusDefault: 400, - stages: [ - { duration: '1m', target: 100 }, - { duration: '1m', target: 200 }, - { duration: '1m', target: 400 }, - { duration: '30s', target: 0 }, - ], - }), - }, -}; - -export default function () { - const cases = [ - { - name: 'child_benefit_review', - url: `${childBenefitFederatorUrl}/v1/evaluations`, - token: childBenefitToken, - subject: uinSubjects[(__VU + __ITER) % uinSubjects.length], - scheme: 'solmara_uin', - purpose: CHILD_BENEFIT_PURPOSE, - claim: 'birth-is-registered', - format: 'application/json', - }, - { - name: 'cra_death_registration_review', - url: `${craNotaryUrl}/v1/evaluations`, - token: craPensionToken, - subject: uinSubjects[(__VU + __ITER) % uinSubjects.length], - scheme: 'solmara_uin', - purpose: PENSION_PAYMENT_PURPOSE, - claim: 'person-is-deceased', - format: CLAIM_RESULT, - }, - { - name: 'sipf_pension_payment_review', - url: `${sipfNotaryUrl}/v1/evaluations`, - token: sipfPensionToken, - subject: uinSubjects[(__VU + __ITER) % uinSubjects.length], - scheme: 'solmara_uin', - purpose: PENSION_PAYMENT_PURPOSE, - claim: 'pension-payment-active', - format: CLAIM_RESULT, - }, - { - name: 'voucher_eligibility_review', - url: `${nagdiNotaryUrl}/v1/evaluations`, - token: nagdiToken, - subject: farmerSubjects[(__VU + __ITER) % farmerSubjects.length], - scheme: 'farmer_id', - purpose: VOUCHER_PURPOSE, - claim: 'eligible-for-climate-smart-input-voucher', - format: CLAIM_RESULT, - }, - ]; - const item = cases[(__VU + __ITER) % cases.length]; - const response = http.post( - item.url, - evaluationPayload(item.subject, item.claim, 'predicate', item.format, item.scheme), - { headers: jsonHeaders(item.token, item.purpose, item.format) }, - ); - const body = parseJson(response); - const ok = check(response, { - [`${item.name} returned 200`]: (r) => r.status === 200, - [`${item.name} returned results`]: () => Array.isArray(body.results) && body.results.length > 0, - }); - recordStatus(ok, response, 200); - sleepIfConfigured(); -} - -export const handleSummary = summaryFor('notary_relay_backed'); diff --git a/portal/AGENTS.md b/portal/AGENTS.md index ed39523..e386c3c 100644 --- a/portal/AGENTS.md +++ b/portal/AGENTS.md @@ -1,6 +1,6 @@ # portal - Glass Government Citizen Services Portal -This is the Phase 0 mock of the Glass Government citizen services portal for the Republic of Solmara (a fictional nation). It demonstrates the "evidence field" UX pattern: structured, authority-verified fields that show the citizen exactly what data was checked, which authority answered, and what was NOT disclosed. All authority calls are behind the `EvidenceProvider` seam (see `src/lib/providers/EvidenceProvider.ts`), which starts with a `MockEvidenceProvider` and will be wired to live Registry Notary endpoints in Phase 1 without a rewrite. +This is the Glass Government citizen services portal for the Republic of Solmara (a fictional nation). It demonstrates the "evidence field" UX pattern: structured, authority-verified fields that show the citizen exactly what was checked, which authority answered, and what was not disclosed. All authority calls stay behind the `EvidenceProvider` seam, with mock and live providers sharing the same renderer contract. The live provider consumes only the programme application's safe projection of independently verified authority assertions. ## Source-of-truth documentation diff --git a/portal/Dockerfile b/portal/Dockerfile index 6ffb014..321c742 100644 --- a/portal/Dockerfile +++ b/portal/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine AS build +FROM node@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 AS build WORKDIR /app RUN corepack enable @@ -9,7 +9,7 @@ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build -FROM node:22-alpine +FROM node@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 WORKDIR /app ENV NODE_ENV=production diff --git a/portal/e2e/portal.test.ts b/portal/e2e/portal.test.ts index c5ff1cb..5f2a073 100644 --- a/portal/e2e/portal.test.ts +++ b/portal/e2e/portal.test.ts @@ -61,20 +61,20 @@ test('child benefit delegated two-hop: the civil read resolves only after consen await expect(page.getByText('Child age under 5: yes')).toBeVisible({ timeout: 10_000 }); }); -// The footer audit log is an expandable drawer: collapsed to a few rows by default, +// The footer proof log is an expandable drawer: collapsed to a few rows by default, // click to expand and scroll the full proof history (older entries are not lost). -test('the proof audit log drawer expands to reveal full history', async ({ page }) => { +test('the proof log drawer expands to reveal full history', async ({ page }) => { await signIn(page); await page.getByTestId('card-farmer-voucher').click(); await expect(page.getByText('Registered farmer: yes')).toBeVisible({ timeout: 10_000 }); - const toggle = page.getByTestId('audit-log-toggle'); + const toggle = page.getByTestId('proof-log-toggle'); await expect(toggle).toHaveAttribute('aria-expanded', 'false'); await toggle.click(); await expect(toggle).toHaveAttribute('aria-expanded', 'true'); // The scroll region exists and can hold overflow (full history is reachable). - await expect(page.locator('#proof-audit-log')).toBeVisible(); + await expect(page.locator('#proof-log')).toBeVisible(); }); // DoD: every EvidenceField state is reachable on the mock via the state gallery. diff --git a/portal/src/lib/fields/EvidenceField.test.ts b/portal/src/lib/fields/EvidenceField.test.ts index 25a37d0..35ae0a5 100644 --- a/portal/src/lib/fields/EvidenceField.test.ts +++ b/portal/src/lib/fields/EvidenceField.test.ts @@ -17,7 +17,7 @@ const verifyField: Field = { label: 'Farmer registration', kind: 'verify', claim: 'farmer-registered', - notary: 'agri' + authority: 'agri' }; const fetchField: Field = { @@ -25,14 +25,14 @@ const fetchField: Field = { label: 'Date of birth', kind: 'fetch', claim: 'child-age-under-5', - notary: 'civil' + authority: 'civil' }; const decisionField: Field = { id: 'combined-eligibility', label: 'Combined eligibility', kind: 'decision', - notary: 'social', + authority: 'social', manual: true }; diff --git a/portal/src/lib/fields/authorities.ts b/portal/src/lib/fields/authorities.ts index b5c003c..950b903 100644 --- a/portal/src/lib/fields/authorities.ts +++ b/portal/src/lib/fields/authorities.ts @@ -1,65 +1,96 @@ -import type { NotaryId } from '$lib/types'; +import type { AuthorityId, EvidencePresentation, EvidenceSource } from '$lib/types'; export type SolmaraAuthority = { - id: NotaryId; + id: AuthorityId; label: string; serviceId: string; + issuer: string; + origin: string; }; // One Solmara authority source for portal labels, mock provenance, and live // trace copy. Service endpoints come from environment config; transport security // is a deployment concern and is not inferred from an evidence response. -export const SOLMARA_AUTHORITIES: Record = { +export const SOLMARA_AUTHORITIES: Record = { civil: { id: 'civil', label: 'Civil Registration Authority', - serviceId: 'cra-notary' + serviceId: 'cra-evidence', + issuer: 'did:web:id.registrystack.org:solmara:authority:cra', + origin: 'https://cra-evidence.solmara.registrystack.org' }, social: { id: 'social', label: 'Social Insurance and Pensions Fund', - serviceId: 'sipf-notary' + serviceId: 'sipf-evidence', + issuer: 'did:web:id.registrystack.org:solmara:authority:sipf', + origin: 'https://sipf-evidence.solmara.registrystack.org' }, agri: { id: 'agri', label: 'National Agricultural Data Institute', - serviceId: 'nagdi-notary' + serviceId: 'nagdi-evidence', + issuer: 'did:web:id.registrystack.org:solmara:authority:nagdi', + origin: 'https://nagdi-evidence.solmara.registrystack.org' }, certs: { id: 'certs', label: 'Civil Registration Authority', - serviceId: 'cra-notary' + serviceId: 'cra-evidence', + issuer: 'did:web:id.registrystack.org:solmara:authority:cra', + origin: 'https://cra-evidence.solmara.registrystack.org' }, childCivil: { id: 'childCivil', label: 'Civil Registration Authority', - serviceId: 'cra-notary' + serviceId: 'cra-evidence', + issuer: 'did:web:id.registrystack.org:solmara:authority:cra', + origin: 'https://cra-evidence.solmara.registrystack.org' }, population: { id: 'population', label: 'National Identity Agency', - serviceId: 'nia-notary' + serviceId: 'nia-evidence', + issuer: 'did:web:id.registrystack.org:solmara:authority:nia', + origin: 'https://nia-evidence.solmara.registrystack.org' }, socialRegistry: { id: 'socialRegistry', label: 'Social Registry Office', - serviceId: 'sro-notary' + serviceId: 'sro-evidence', + issuer: 'did:web:id.registrystack.org:solmara:authority:sro', + origin: 'https://sro-evidence.solmara.registrystack.org' }, programme: { id: 'programme', label: 'MoSD Programme MIS', - serviceId: 'programme-notary' + serviceId: 'mosd-programme-evidence', + issuer: 'did:web:id.registrystack.org:solmara:authority:mosd-programme-mis', + origin: 'https://mosd-programme-evidence.solmara.registrystack.org' } }; -export const AUTHORITY_NAMES: Record = Object.fromEntries( +export const AUTHORITY_NAMES: Record = Object.fromEntries( Object.entries(SOLMARA_AUTHORITIES).map(([id, authority]) => [id, authority.label]) -) as Record; +) as Record; -// A safe default so a wait still names *someone* if a result omits its notary. +// A safe default so a wait still names someone if a result omits its authority. const FALLBACK_AUTHORITY = 'the authority'; -export function authorityName(notary: NotaryId | undefined): string { - if (notary === undefined) return FALLBACK_AUTHORITY; - return AUTHORITY_NAMES[notary]; +export function authorityName(authority: AuthorityId | undefined): string { + if (authority === undefined) return FALLBACK_AUTHORITY; + return AUTHORITY_NAMES[authority]; +} + +export function evidencePresentation( + authority: AuthorityId, + source: EvidenceSource +): EvidencePresentation { + const definition = SOLMARA_AUTHORITIES[authority]; + return { + authority: definition.label, + issuer: definition.issuer, + serviceId: definition.serviceId, + source + }; } diff --git a/portal/src/lib/fields/index.ts b/portal/src/lib/fields/index.ts index dd92522..5f3699b 100644 --- a/portal/src/lib/fields/index.ts +++ b/portal/src/lib/fields/index.ts @@ -10,6 +10,6 @@ export { presentationFor, stampsOnEntry } from './states'; export type { Channel, StatusIcon as StatusIconName, StatePresentation } from './states'; // Authority and reason-code lookups, the single source of truth for how a -// NotaryId reads to a citizen and how a reason code maps to a human sentence. +// authority id reads to a citizen and how a reason code maps to a human sentence. export { authorityName, AUTHORITY_NAMES } from './authorities'; export { reasonSentence, REASON_CODES } from './reasonCodes'; diff --git a/portal/src/lib/forms/DenialBeat.svelte b/portal/src/lib/forms/DenialBeat.svelte index 579dd66..1c095c6 100644 --- a/portal/src/lib/forms/DenialBeat.svelte +++ b/portal/src/lib/forms/DenialBeat.svelte @@ -1,6 +1,6 @@ diff --git a/portal/src/lib/proof/ProofInspector.test.ts b/portal/src/lib/proof/ProofInspector.test.ts index bebc1c0..4bdb6a2 100644 --- a/portal/src/lib/proof/ProofInspector.test.ts +++ b/portal/src/lib/proof/ProofInspector.test.ts @@ -1,266 +1,66 @@ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/svelte'; +import { fireEvent, render, screen } from '@testing-library/svelte'; +import { describe, expect, it } from 'vitest'; import ProofInspector from './ProofInspector.svelte'; -import { CANNED_TRACES } from './canned-traces.js'; import type { ProofTrace } from '$lib/types'; -// Single trace with all depths populated -const verifiedTrace: ProofTrace = { - id: 'event-2', - seq: 2, +const trace: ProofTrace = { + id: 'proof-1', + seq: 1, fieldId: 'farmer-registered', authority: 'agri', - headline: 'Confirmed by NAgDI: Amina did not have to prove this herself', - answered: 'Agriculture answered: farmer-registered = true', - notDisclosed: 'Only the yes/no, no farm details or parcel coordinates', + headline: 'Confirmed by NAgDI', + answered: 'National Agricultural Data Institute answered: farmer-registered = true', + notDisclosed: 'source rows and selector values', status: 'ok', ts: '2026-06-21T12:04:09.000Z', - request: { - method: 'POST', - url: 'https://nagdi-notary.solmara.example/v1/evaluations', - body: { - claim: 'farmer-registered', - purpose: 'https://id.registrystack.org/solmara/purpose/voucher-eligibility-review', - relationship: 'self' - } - }, - response: { - status: 200, - body: { - registered: true, - source_authority: 'Agriculture', - as_of: '2026-05-01' - } - }, + purpose: 'voucher-eligibility-review', + resultState: 'verified', + responseStatus: 200, + presentations: [{ + authority: 'National Agricultural Data Institute', + issuer: 'did:web:id.registrystack.org:solmara:authority:nagdi', + serviceId: 'nagdi-evidence', + source: 'Relay lookup' + }], proof: { - signedBy: 'No credential issued; National Agricultural Data Institute returned a claim evaluation', - algorithm: 'Registry Notary claim-result response; no credential signature asserted', - issuerKey: 'Not applicable for claim-result evaluation', - holderBound: 'Not credential-bound; the portal selected the purpose and subject', - credential: 'Claim result only; no credential issued', - auditId: 'Not available in this canned gallery trace' - } -}; - -const denialTrace: ProofTrace = { - id: 'event-4', - seq: 4, - fieldId: 'person-is-deceased', - authority: 'civil', - headline: 'Denied by Civil Registry: subject mismatch, no data read', - answered: 'Civil answered: person-is-deceased = denied (subject_mismatch)', - notDisclosed: 'No data was read; the query was rejected before any registry access', - status: 'denied', - ts: '2026-06-21T12:04:15.000Z', - request: { - method: 'POST', - url: 'https://cra-notary.solmara.example/v1/evaluations', - body: { - claim: 'person-is-deceased', - purpose: 'https://id.registrystack.org/solmara/purpose/pension-payment-review', - relationship: 'self' - } - }, - response: { - status: 403, - body: { - error: 'subject_mismatch', - source_authority: 'Civil Registry', - message: 'Token subject does not match requested target' - } - } -}; - -const applicationEvidenceTrace: ProofTrace = { - ...verifiedTrace, - id: 'event-application-evidence', - authority: 'population', - proof: { - signedBy: 'National Identity Agency source-owned Notary', - algorithm: 'Ordinary JSON response; no application signature asserted', - issuerKey: 'Not applicable for an application evidence set', - holderBound: 'The application selected the purpose and subject', - credential: 'Minimized source-attributed predicate result', - auditId: 'evidence-set:cbe_test' + signedBy: 'National Agricultural Data Institute', + algorithm: 'Flattened JWS, ES256, verified server-side', + issuerKey: 'Authority JWKS', + holderBound: 'Reviewed request', + credential: 'Signed minimum-disclosure Evidence assertion' } }; describe('ProofInspector', () => { - describe('Depth 1 - always visible without expansion', () => { - it('renders the "Not disclosed:" line at depth 1 without any expansion', () => { - render(ProofInspector, { props: { traces: [verifiedTrace] } }); - - // The "Not disclosed:" label must be visible without expanding any accordion - const labels = screen.getAllByText('Not disclosed:'); - expect(labels.length).toBeGreaterThan(0); - - // The not-disclosed text itself is always present in the DOM - expect( - screen.getByText('Only the yes/no, no farm details or parcel coordinates') - ).toBeInTheDocument(); - }); - - it('renders the headline at depth 1', () => { - render(ProofInspector, { props: { traces: [verifiedTrace] } }); - expect( - screen.getByText('Confirmed by NAgDI: Amina did not have to prove this herself') - ).toBeInTheDocument(); - }); - - it('renders the answered line at depth 1', () => { - render(ProofInspector, { props: { traces: [verifiedTrace] } }); - expect( - screen.getByText('Agriculture answered: farmer-registered = true') - ).toBeInTheDocument(); - }); - - it('does not present canned evaluations or UserInfo as signed credentials', () => { - const serialized = JSON.stringify(CANNED_TRACES); - expect(serialized).not.toMatch(/SD-JWT|EdDSA\/Ed25519/); - expect(serialized).toContain('no credential signature asserted'); - expect(CANNED_TRACES.find((trace) => trace.fieldId === 'household-below-poverty-threshold')?.authority).toBe( - 'socialRegistry' - ); - }); - - it('renders "Not disclosed:" for all canned traces without expansion', () => { - render(ProofInspector, { props: { traces: CANNED_TRACES } }); - // Every trace has a not-disclosed line; count the labels - const labels = screen.getAllByText('Not disclosed:'); - expect(labels.length).toBe(CANNED_TRACES.length); - }); - - it('renders the denial trace "Not disclosed:" at depth 1', () => { - render(ProofInspector, { props: { traces: [denialTrace] } }); - expect( - screen.getByText('No data was read; the query was rejected before any registry access') - ).toBeInTheDocument(); - }); - }); - - describe('Bearer token redaction', () => { - it('never renders a real bearer token in the DOM', async () => { - const { container } = render(ProofInspector, { props: { traces: CANNED_TRACES } }); - - // Expand depth 2 for the verified trace - const expandBtns = screen.getAllByText(/Request and response/); - for (const btn of expandBtns) { - await fireEvent.click(btn); - } - - const html = container.innerHTML; - - // Must never contain a raw token value. Only redacted placeholders should - // be visible in the rendered request. - expect(html).not.toMatch(/Bearer [A-Za-z0-9+/=._-]{8,}/); - expect(html).not.toMatch(/x-api-key:\s+[A-Za-z0-9+/=._-]{8,}/); + it('renders the resident-facing result without exposing evidence internals', () => { + const { container } = render(ProofInspector, { traces: [trace] }); - // The redacted placeholder MUST be present after expanding - expect(html).toContain('(redacted)'); - }); - - it('renders redacted dots instead of a real token value', async () => { - const { container } = render(ProofInspector, { props: { traces: [verifiedTrace] } }); - - const expandBtn = screen.getByText(/Request and response/); - await fireEvent.click(expandBtn); - - // Should find the redacted dots and the "(redacted)" label - const redactedEl = container.querySelector('.redacted'); - expect(redactedEl).toBeInTheDocument(); - expect(container.innerHTML).toContain('(redacted)'); - }); + expect(screen.getByText('Confirmed by NAgDI')).toBeInTheDocument(); + expect(screen.getByText(/source rows and selector values/)).toBeInTheDocument(); + expect(container.textContent).not.toMatch(/requestNonce|subjects|protected|payload|signature|Bearer|FR-\d+/); }); - describe('Application evidence proof', () => { - it('shows the evidence artifact without presenting a synthetic SD-JWT credential', async () => { - render(ProofInspector, { props: { traces: [applicationEvidenceTrace] } }); + it('expands canonical authority, issuer, service, and exact source metadata only', async () => { + const { container } = render(ProofInspector, { traces: [trace] }); + await fireEvent.click(screen.getByRole('button', { name: /Authority evidence/ })); - await fireEvent.click(screen.getByText(/Request and response/)); - await fireEvent.click(screen.getByText(/Cryptographic proof/)); - - expect(screen.getByText('Minimized source-attributed predicate result')).toBeInTheDocument(); - expect( - screen.getByText('National Identity Agency source-owned Notary') - ).toBeInTheDocument(); - expect(screen.queryByText('Raw SD-JWT')).not.toBeInTheDocument(); - }); + expect(screen.getAllByText('National Agricultural Data Institute')).toHaveLength(2); + expect(screen.getByText('did:web:id.registrystack.org:solmara:authority:nagdi')).toBeInTheDocument(); + expect(screen.getByText('nagdi-evidence')).toBeInTheDocument(); + expect(screen.getByText('Relay lookup')).toBeInTheDocument(); + expect(container.textContent).not.toMatch(/raw wire|audit id|compact JWS|request body/i); }); - describe('Copy-as-curl', () => { - it('curl output contains $NOTARY_TOKEN placeholder, not a real token', async () => { - const writtenTexts: string[] = []; - const mockWriteText = vi.fn((text: string) => { - writtenTexts.push(text); - return Promise.resolve(); - }); - - Object.defineProperty(navigator, 'clipboard', { - value: { writeText: mockWriteText }, - writable: true, - configurable: true - }); - - render(ProofInspector, { props: { traces: [verifiedTrace] } }); - - // Expand depth 2 - const expandBtn = screen.getByText(/Request and response/); - await fireEvent.click(expandBtn); - - // Click the copy-as-curl button - const copyBtn = screen.getByLabelText('Copy request as curl command'); - await fireEvent.click(copyBtn); - - expect(mockWriteText).toHaveBeenCalledOnce(); - - const curlCmd = writtenTexts[0]; - - // Must contain the placeholder token variable - expect(curlCmd).toContain('$NOTARY_TOKEN'); - expect(curlCmd).toContain('x-api-key: $NOTARY_TOKEN'); - - // Must NOT contain any real token material - // Real tokens would be long base64 or JWT-format strings - expect(curlCmd).not.toMatch(/Bearer [A-Za-z0-9+/=._-]{8,}/); - expect(curlCmd).not.toMatch(/x-api-key:\s+[A-Za-z0-9+/=._-]{8,}/); - - // Must include the request method and URL - expect(curlCmd).toContain('POST'); - expect(curlCmd).toContain(verifiedTrace.request.url); - }); - }); - - describe('Connected state', () => { - it('does not show reconnecting pill when connected', () => { - render(ProofInspector, { props: { traces: [], connected: true } }); - expect(screen.queryByText(/Reconnecting to audit feed/i)).not.toBeInTheDocument(); - }); - - it('shows the reconnecting pill when disconnected', () => { - render(ProofInspector, { props: { traces: [], connected: false } }); - expect(screen.getByText(/Reconnecting to audit feed/i)).toBeInTheDocument(); - }); - }); - - describe('In-flight skeleton', () => { - it('renders in-flight traces at the top with heartbeat indicator', () => { - const inFlightTrace: ProofTrace = CANNED_TRACES.find( - (t) => t.status === 'in_flight' - )!; - render(ProofInspector, { props: { traces: [inFlightTrace] } }); - - // The heartbeat dot is rendered with aria-label "In flight" - expect(screen.getByLabelText('In flight')).toBeInTheDocument(); - }); + it('states when authorization prevented any authority call', async () => { + const denied = { ...trace, id: 'denied', status: 'denied' as const, resultState: 'error' as const, presentations: [] }; + render(ProofInspector, { traces: [denied] }); + await fireEvent.click(screen.getByRole('button', { name: /Authority evidence/ })); + expect(screen.getByText('No authority Evidence service was called.')).toBeInTheDocument(); }); - describe('All canned traces render', () => { - it('renders all five canned traces without error', () => { - const { container } = render(ProofInspector, { props: { traces: CANNED_TRACES } }); - // All 5 event ids should appear - for (const trace of CANNED_TRACES) { - expect(container.innerHTML).toContain(trace.id); - } - }); + it('shows reconnecting state without removing existing proofs', () => { + render(ProofInspector, { traces: [trace], connected: false }); + expect(screen.getByRole('status')).toHaveTextContent('Reconnecting to proof feed'); + expect(screen.getByText('Confirmed by NAgDI')).toBeInTheDocument(); }); }); diff --git a/portal/src/lib/proof/ProofTicker.svelte b/portal/src/lib/proof/ProofTicker.svelte index 6917fe7..4fe6b0b 100644 --- a/portal/src/lib/proof/ProofTicker.svelte +++ b/portal/src/lib/proof/ProofTicker.svelte @@ -39,8 +39,8 @@ } function authorityShort(trace: ProofTrace): string { - // The identity-binding trace has no Notary authority (eSignet is the identity - // provider, not one of the four Notaries), so name its issuer explicitly rather + // The identity-binding trace has no Evidence authority (eSignet is the identity + // provider, not one of the source authorities), so name its issuer explicitly rather // than falling through to a bare "Unknown". if (trace.id === IDENTITY_TRACE_ID) { return 'eSignet'; @@ -63,7 +63,7 @@ case 'programme': return 'Programme'; default: - return trace.authority ?? 'Unknown'; + return trace.status === 'denied' ? 'Portal' : trace.authority ?? 'Unattributed'; } } @@ -90,7 +90,7 @@ class="proof-ticker" role="log" aria-live="polite" - aria-label="Proof audit log" + aria-label="Proof log" aria-atomic="false" aria-relevant="additions" > diff --git a/portal/src/lib/proof/ProofTicker.test.ts b/portal/src/lib/proof/ProofTicker.test.ts index 532a095..e801783 100644 --- a/portal/src/lib/proof/ProofTicker.test.ts +++ b/portal/src/lib/proof/ProofTicker.test.ts @@ -17,9 +17,9 @@ describe('ProofTicker', () => { expect(ticker).toHaveAttribute('aria-live', 'polite'); }); - it('is labelled as the proof audit log', () => { + it('is labelled as the proof log', () => { render(ProofTicker, { props: { traces: [] } }); - expect(screen.getByRole('log', { name: /proof audit log/i })).toBeInTheDocument(); + expect(screen.getByRole('log', { name: /proof log/i })).toBeInTheDocument(); }); it('renders an entry for each trace', () => { @@ -59,14 +59,14 @@ describe('ProofTicker', () => { expect(screen.getAllByText('Agri').length).toBeGreaterThan(0); // Social Registry entries expect(screen.getAllByText('Social Registry').length).toBeGreaterThan(0); - // Civil entries - expect(screen.getAllByText('Civil').length).toBeGreaterThan(0); + // Portal authorization entry + expect(screen.getAllByText('Portal').length).toBeGreaterThan(0); }); it('labels the identity-binding trace as eSignet, never Unknown', () => { - // The identity trace carries no Notary `authority` (eSignet is not one of the - // four Notaries), so a naive authority lookup falls through to "Unknown". It - // must instead name its issuer so the very first audit line reads honestly. + // The identity trace carries no authority Evidence source (eSignet is the + // identity provider), so a naive lookup falls through to "Unknown". It + // must instead name its issuer so the first proof line reads honestly. render(ProofTicker, { props: { traces: [buildIdentityTrace('Elena Dela Cruz')] } }); expect(screen.queryByText('Unknown')).toBeNull(); expect(screen.getByText('eSignet')).toBeInTheDocument(); diff --git a/portal/src/lib/proof/canned-traces.ts b/portal/src/lib/proof/canned-traces.ts index 12d1352..be57298 100644 --- a/portal/src/lib/proof/canned-traces.ts +++ b/portal/src/lib/proof/canned-traces.ts @@ -1,181 +1,113 @@ -// Canned ProofTrace[] for the proof gallery demo and unit tests. -// Every ProofStatus and depth shape is represented. import type { ProofTrace } from '$lib/types'; export const CANNED_TRACES: ProofTrace[] = [ - // 1. In-flight (skeleton at top) { id: 'event-1', seq: 1, fieldId: 'farmer-registered', authority: 'agri', - headline: 'Checking NAgDI for farmer-registered status...', - answered: 'Agriculture answered: farmer-registered = (pending)', - notDisclosed: 'Only the yes/no result is checked, no farm details', + headline: 'Checking NAgDI for farmer registration status...', + answered: 'National Agricultural Data Institute response is pending', + notDisclosed: 'Only the reviewed answer is requested, no farm details', status: 'in_flight', ts: '2026-06-21T12:04:05.000Z', - request: { - method: 'POST', - url: 'https://nagdi-notary.solmara.example/v1/evaluations', - body: { - claim: 'farmer-registered', - purpose: 'https://id.registrystack.org/solmara/purpose/voucher-eligibility-review', - relationship: 'self' + purpose: 'voucher-eligibility-review', + resultState: 'in_flight', + presentations: [ + { + authority: 'National Agricultural Data Institute', + issuer: 'did:web:id.registrystack.org:solmara:authority:nagdi', + serviceId: 'nagdi-evidence', + source: 'Relay lookup' } - } + ] }, - - // 2. Verified (farmer registration) { id: 'event-2', seq: 2, fieldId: 'farmer-registered', authority: 'agri', - headline: - 'Confirmed by NAgDI: Amina did not have to prove this herself', - answered: 'Agriculture answered: farmer-registered = true', - notDisclosed: 'Only the yes/no, no farm details or parcel coordinates', + headline: 'Confirmed by NAgDI: the applicant did not have to prove this herself', + answered: 'National Agricultural Data Institute answered: farmer-registered = true', + notDisclosed: 'Only the yes/no answer, no farm details or parcel coordinates', status: 'ok', ts: '2026-06-21T12:04:09.000Z', - request: { - method: 'POST', - url: 'https://nagdi-notary.solmara.example/v1/evaluations', - body: { - claim: 'farmer-registered', - purpose: 'https://id.registrystack.org/solmara/purpose/voucher-eligibility-review', - relationship: 'self' + purpose: 'voucher-eligibility-review', + resultState: 'verified', + responseStatus: 200, + presentations: [ + { + authority: 'National Agricultural Data Institute', + issuer: 'did:web:id.registrystack.org:solmara:authority:nagdi', + serviceId: 'nagdi-evidence', + source: 'Relay lookup' } - }, - response: { - status: 200, - body: { - registered: true, - source_authority: 'Agriculture', - as_of: '2026-05-01' - } - }, + ], proof: { - signedBy: 'No credential issued; National Agricultural Data Institute returned a claim evaluation', - algorithm: 'Registry Notary claim-result response; no credential signature asserted', - issuerKey: 'Not applicable for claim-result evaluation', - holderBound: 'Not credential-bound; the portal selected the purpose and subject', - credential: 'Claim result only; no credential issued', - auditId: 'Not available in this canned gallery trace' + signedBy: 'National Agricultural Data Institute issued the returned Evidence', + algorithm: 'Verified Evidence assertion', + issuerKey: 'Authority Evidence JWKS', + holderBound: 'Audience-scoped to the portal request', + credential: 'Minimum-disclosure Evidence assertion' } }, - - // 3. Fetched (household composition) { id: 'event-3', seq: 3, fieldId: 'household-below-poverty-threshold', authority: 'socialRegistry', - headline: - 'Verified by Social Protection: household is below the programme threshold', - answered: 'Social answered: household-below-poverty-threshold = true', + headline: 'The household is below the reviewed programme threshold', + answered: 'Social Registry Office answered: household-below-poverty-threshold = true', notDisclosed: 'Predicate only, not poverty score or household roster', status: 'ok', ts: '2026-06-21T12:04:12.000Z', - request: { - method: 'POST', - url: 'https://sro-notary.solmara.example/v1/evaluations', - body: { - claim: 'household-below-poverty-threshold', - purpose: 'https://id.registrystack.org/solmara/purpose/child-benefit-review', - relationship: 'self' + purpose: 'child-benefit-review', + resultState: 'verified', + responseStatus: 200, + presentations: [ + { + authority: 'Social Registry Office', + issuer: 'did:web:id.registrystack.org:solmara:authority:sro', + serviceId: 'sro-evidence', + source: 'immutable extract' } - }, - response: { - status: 200, - body: { - satisfied: true, - source_authority: 'Social Registry Office', - as_of: '2026-04-15' - } - }, + ], proof: { - signedBy: 'Social Registry Office source-owned Notary', - algorithm: 'Authenticated claim-result evaluation; no credential signature asserted', - issuerKey: 'Not applicable for claim-result evaluation', - holderBound: 'Portal-selected purpose and subject', - credential: 'Minimized predicate claim result', - auditId: 'Not available in this canned gallery trace' + signedBy: 'Social Registry Office issued the returned Evidence', + algorithm: 'Verified Evidence assertion', + issuerKey: 'Authority Evidence JWKS', + holderBound: 'Audience-scoped to the portal request', + credential: 'Minimum-disclosure Evidence assertion' } }, - - // 4. Denial (cross-person attempt) { id: 'event-4', seq: 4, fieldId: 'person-is-deceased', - authority: 'civil', - headline: - 'Denied by Civil Registry: subject mismatch, no data read for 2300073046', - answered: 'Civil answered: person-is-deceased = denied (subject_mismatch)', - notDisclosed: - 'No data was read; the query was rejected before any registry access', + headline: 'Denied by the portal: request was not authorized, no data read', + answered: 'Portal authorization gate returned 403 not_authorized', + notDisclosed: 'No source was contacted', status: 'denied', ts: '2026-06-21T12:04:15.000Z', - request: { - method: 'POST', - url: 'https://cra-notary.solmara.example/v1/evaluations', - body: { - claim: 'person-is-deceased', - purpose: 'https://id.registrystack.org/solmara/purpose/pension-payment-review', - relationship: 'self' - } - }, - response: { - status: 403, - body: { - error: 'subject_mismatch', - source_authority: 'Civil Registry', - message: 'Token subject does not match requested target' - } - } + purpose: 'pension-payment-review', + resultState: 'error', + responseStatus: 403, + presentations: [] }, - - // 5. Identity-binding (pinned to bottom as the foundation) { id: 'event-0', seq: 0, fieldId: 'identity', - authority: 'civil', - headline: - 'Identity bound via eSignet: session linked to 2300018263 (Elena Dela Cruz)', - answered: 'Civil answered: identity = bound (eSignet UserInfo)', - notDisclosed: - 'Only name and national ID were shared; no other civil facts disclosed', + headline: 'Identity bound through eSignet', + answered: 'eSignet bound the signed-in portal session', + notDisclosed: 'No additional identity attributes were shared', status: 'ok', ts: '2026-06-21T12:03:58.000Z', - request: { - method: 'POST', - url: 'https://esignet.solmara.example/v1/userinfo', - body: { - claim: 'identity', - purpose: 'session_binding', - relationship: 'self' - } - }, - response: { - status: 200, - body: { - sub: '2300018263', - name: 'Elena Dela Cruz', - source_authority: 'Civil Registry via eSignet', - as_of: '2026-06-21' - } - }, - proof: { - signedBy: 'No credential issued; eSignet UserInfo bound the portal session', - algorithm: 'OIDC UserInfo response; no credential signature asserted', - issuerKey: 'Not applicable for UserInfo', - holderBound: 'Portal session bound to the configured UserInfo subject claim', - credential: 'OIDC session identity, not a verifiable credential', - auditId: 'session-binding:event-0' - } + purpose: 'session-binding', + resultState: 'prefilled', + responseStatus: 200, + presentations: [] } ]; -// The identity-binding entry is always pinned to the bottom. export const IDENTITY_TRACE_ID = 'event-0'; diff --git a/portal/src/lib/providers/authority-plan.ts b/portal/src/lib/providers/authority-plan.ts index 925b71c..4c2d498 100644 --- a/portal/src/lib/providers/authority-plan.ts +++ b/portal/src/lib/providers/authority-plan.ts @@ -1,5 +1,7 @@ import { PURPOSES } from '$lib/forms/descriptors'; import type { ScenarioResult } from '$lib/providers/mock/scenarios'; +import type { AuthorityId } from '$lib/types'; +import { SOLMARA_AUTHORITIES } from '$lib/fields/authorities'; export type AuthorityClient = | 'craPension' @@ -10,17 +12,19 @@ export type AuthorityClient = export type AuthorityPlan = { client: AuthorityClient; + authorityId: AuthorityId; authority: string; serviceId: string; claimId: string; purpose: string; + source: 'immutable extract' | 'Relay lookup'; scheme?: string; }; /** * Resolve the exact authority-owned claims used by a portal field. This is the * shared plan for live and mock providers, so the mock cannot present a - * portal-composed decision as if one Notary produced it. + * portal-composed decision as if one Evidence service produced it. */ export function authorityPlan( scenarioKey: string, @@ -28,24 +32,30 @@ export function authorityPlan( ): AuthorityPlan[] { const craPension: AuthorityPlan = { client: 'craPension', + authorityId: 'civil', authority: 'Civil Registration Authority', - serviceId: 'cra-notary', + serviceId: SOLMARA_AUTHORITIES.civil.serviceId, claimId: 'person-is-deceased', - purpose: PURPOSES.pensionPaymentReview + purpose: PURPOSES.pensionPaymentReview, + source: 'Relay lookup' }; const sipfPayment: AuthorityPlan = { client: 'sipfPension', + authorityId: 'social', authority: 'Social Insurance and Pensions Fund', - serviceId: 'sipf-notary', + serviceId: SOLMARA_AUTHORITIES.social.serviceId, claimId: 'pension-payment-active', - purpose: PURPOSES.pensionPaymentReview + purpose: PURPOSES.pensionPaymentReview, + source: 'Relay lookup' }; const sipfSurvivor: AuthorityPlan = { client: 'sipfPension', + authorityId: 'social', authority: 'Social Insurance and Pensions Fund', - serviceId: 'sipf-notary', + serviceId: SOLMARA_AUTHORITIES.social.serviceId, claimId: 'survivor-is-eligible', - purpose: PURPOSES.survivorBenefitDetermination + purpose: PURPOSES.survivorBenefitDetermination, + source: 'Relay lookup' }; if (scenarioKey === 'disability-determination') return [craPension, sipfPayment]; @@ -60,17 +70,21 @@ export function authorityPlan( return [ { client: 'craCitizen', + authorityId: 'civil', authority: 'Civil Registration Authority', - serviceId: 'cra-notary', + serviceId: SOLMARA_AUTHORITIES.civil.serviceId, claimId: 'civil-record-linked', - purpose: PURPOSES.citizenSelfService + purpose: PURPOSES.citizenSelfService, + source: 'Relay lookup' }, { client: 'niaCitizen', + authorityId: 'population', authority: 'National Identity Agency', - serviceId: 'nia-notary', + serviceId: SOLMARA_AUTHORITIES.population.serviceId, claimId: 'citizen-population-record-active', - purpose: PURPOSES.citizenSelfService + purpose: PURPOSES.citizenSelfService, + source: 'immutable extract' } ]; } @@ -78,15 +92,17 @@ export function authorityPlan( return [ { client: 'nagdi', + authorityId: 'agri', authority: 'National Agricultural Data Institute', - serviceId: 'nagdi-notary', + serviceId: SOLMARA_AUTHORITIES.agri.serviceId, claimId: scenario.claimId, purpose: scenario.purpose, + source: 'Relay lookup', scheme: 'farmer_id' } ]; } - if (scenario.notary === 'civil') return [craPension]; + if (scenario.authority === 'civil') return [craPension]; if (scenario.service === 'pension') return [sipfSurvivor]; throw new Error(`No authority route for scenario "${scenarioKey}"`); } diff --git a/portal/src/lib/providers/feeds.svelte.test.ts b/portal/src/lib/providers/feeds.svelte.test.ts index e1ecd14..a05fd7b 100644 --- a/portal/src/lib/providers/feeds.svelte.test.ts +++ b/portal/src/lib/providers/feeds.svelte.test.ts @@ -13,15 +13,8 @@ function trace(id: string): ProofTrace { notDisclosed: 'Not disclosed: any other civil record detail', status: 'ok', ts: '2026-06-22T12:00:00.000Z', - request: { - method: 'POST', - url: 'https://civil-notary.gov.solmara.example/v1/evaluations', - body: { claim: 'person-is-deceased' } - }, - response: { - status: 200, - body: { result: true } - } + resultState: 'verified', + presentations: [] }; } diff --git a/portal/src/lib/providers/live/index.test.ts b/portal/src/lib/providers/live/index.test.ts index afe612d..daa081f 100644 --- a/portal/src/lib/providers/live/index.test.ts +++ b/portal/src/lib/providers/live/index.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from 'vitest'; -import { PURPOSES } from '$lib/forms/descriptors'; import { LiveEvidenceProvider } from './index'; const ctx = { subject: '2300018263', delegatedTarget: '2300010248' }; @@ -15,7 +14,12 @@ function envelope(results: Array<{ claim_id: string; satisfied: boolean; value?: signed_evidence: [{ protected: 'e30', payload: 'e30', signature: 'c2ln' }] } }, - source_trace: [{ authority: 'Source authority', service_id: 'registry-evidence', status: 200 }], + presentation: { + authority: 'National Agricultural Data Institute', + issuer: 'did:web:id.registrystack.org:solmara:authority:nagdi', + provider: 'https://nagdi-evidence.solmara.registrystack.org', + source: 'Relay lookup' + }, ...extra } }), @@ -24,7 +28,7 @@ function envelope(results: Array<{ claim_id: string; satisfied: boolean; value?: } describe('LiveEvidenceProvider', () => { - it('routes reviewed fields through the scenario runner with an Evidence purpose code', async () => { + it('routes reviewed fields through the scenario runner without overriding its purpose', async () => { const fetcher = vi.fn(async () => envelope([{ claim_id: 'farmer-registered', satisfied: true }])) as unknown as typeof fetch; const provider = new LiveEvidenceProvider({ SCENARIO_RUNNER_URL: 'http://scenario-runner:8080' }, fetcher); const evaluation = await provider.evaluateDetailed( @@ -35,9 +39,9 @@ describe('LiveEvidenceProvider', () => { expect(fetcher).toHaveBeenCalledTimes(1); const [url, init] = vi.mocked(fetcher).mock.calls[0]; expect(String(url)).toContain('/v1/scenarios/farmer-climate-smart-voucher/steps/positive/run'); - expect(JSON.parse(String(init?.body))).toEqual({ config: { purpose_override: PURPOSES.voucherEligibilityReview } }); + expect(JSON.parse(String(init?.body))).toEqual({}); expect(evaluation.result.state).toBe('verified'); - expect(evaluation.proof.crypto.algorithm).toBe('Flattened JWS, EdDSA'); + expect(evaluation.proof.crypto.algorithm).toBe('Verified Evidence assertion'); expect(JSON.stringify(evaluation)).not.toContain('x-api-key'); }); diff --git a/portal/src/lib/providers/live/index.ts b/portal/src/lib/providers/live/index.ts index 4b82df0..c5326bb 100644 --- a/portal/src/lib/providers/live/index.ts +++ b/portal/src/lib/providers/live/index.ts @@ -1,6 +1,8 @@ import type { EvaluateContext, DetailedEvidenceProvider } from '$lib/providers/EvidenceProvider'; -import type { ClaimResult, Field, ProofTrace } from '$lib/types'; +import type { ClaimResult, EvidencePresentation, EvidenceSource, Field, ProofTrace } from '$lib/types'; import { SCENARIOS, type EvaluateOptions, type MockEvaluation } from '$lib/providers/mock'; +import { evidencePresentation, SOLMARA_AUTHORITIES } from '$lib/fields/authorities'; +import { authorityPlan, isApplicationOwnedPlan } from '$lib/providers/authority-plan'; export type LiveProviderEnv = { SCENARIO_RUNNER_URL?: string; @@ -15,12 +17,15 @@ type RunnerResult = { response_source?: { status?: number | null; body?: unknown; error?: string }; source_trace?: Dict[]; derived_decisions?: Record; + results?: Dict[]; + presentation?: unknown; + presentations?: unknown[]; }; type RunnerCall = { scenarioId: string; stepId: string }; /** * The live portal delegates scenario execution to the server-side scenario - * runner. That runner owns Mint authentication and calls Registry Evidence; + * runner. That runner owns Mint authentication and calls authority Evidence; * browser-controlled input can select only a reviewed portal field. */ export class LiveEvidenceProvider implements DetailedEvidenceProvider { @@ -46,22 +51,22 @@ export class LiveEvidenceProvider implements DetailedEvidenceProvider { const scenario = SCENARIOS[scenarioKey]; if (!scenario) throw new Error(`LiveEvidenceProvider: no scenario mapping for field "${field.id}"`); - if (scenarioKey === 'denial') return this.#blocked(field, scenario, 'subject_mismatch'); + if (scenarioKey === 'denial') return this.#blocked(field, scenario, 'not_authorized'); if (scenario.delegated && opts?.guardianLinkVerified !== true) { return this.#blocked(field, scenario, 'relationship_not_proven'); } const calls = runnerCalls(scenarioKey, scenario.service); - const responses = await Promise.all(calls.map((call) => this.#run(call, scenario.purpose))); + const responses = await Promise.all(calls.map((call) => this.#run(call))); const status = responses.find((response) => !isSuccess(response.response_source?.status))?.response_source?.status ?? 200; const results = responses.flatMap((response) => responseResults(response)); + const presentations = presentationsFor(scenarioKey, scenario, responses); const outcome = outcomeFor(scenarioKey, scenario.claimId, results, responses); const proofStatus = status === 403 ? 'denied' : !isSuccess(status) || !outcome.found ? 'error' : outcome.satisfied === false ? 'false' : 'ok'; const seq = ++this.#seq; const applicationOwned = scenario.applicationOwned === true; const responseBody = { results, - signed_evidence: responses.flatMap((response) => signedEvidence(response)), source_trace: responses.flatMap((response) => response.source_trace ?? []), ...(outcome.derivedDecisions ? { derived_decisions: outcome.derivedDecisions } : {}) }; @@ -78,7 +83,7 @@ export class LiveEvidenceProvider implements DetailedEvidenceProvider { result: { state: proofStatus === 'ok' ? scenario.state : proofStatus === 'false' ? 'false' : 'error', display, - ...(!applicationOwned ? { authority: scenario.notary } : {}), + ...(!applicationOwned ? { authority: scenario.authority } : {}), traceId: `event ${seq}` }, raw: { @@ -88,32 +93,44 @@ export class LiveEvidenceProvider implements DetailedEvidenceProvider { body: requestBody }, response: { status: typeof status === 'number' ? status : 503, body: responseBody } - } as unknown as MockEvaluation['raw'], + }, proof: { headline: scenario.headline, answered: outcome.found - ? `Registry Evidence answered: ${scenario.claimId} = ${String(outcome.value)}` - : 'Registry Evidence returned no usable value for this field', + ? `${presentations[0]?.authority ?? 'Authority Evidence'} answered: ${scenario.claimId} = ${String(outcome.value)}` + : 'The authority Evidence service returned no usable value for this field', notDisclosed: scenario.notDisclosed, status: proofStatus, - authority: applicationOwned ? undefined : scenario.notary, - crypto: evidenceProof(responses) + authority: applicationOwned ? undefined : scenario.authority, + purpose: scenario.purpose, + presentations, + crypto: evidenceProof(presentations, proofStatus) }, timing: { latencyMs: 0, staggerOrder: scenario.staggerOrder, slow: false } }; } - async #run(call: RunnerCall, purpose: string): Promise { + async #run(call: RunnerCall): Promise { const url = `${requiredRunnerUrl(this.#runnerUrl)}/v1/scenarios/${call.scenarioId}/steps/${call.stepId}/run`; const response = await this.#fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, - body: JSON.stringify({ config: { purpose_override: purpose } }) + body: JSON.stringify({}) }); const envelope = asDict(await response.json().catch(() => ({}))); + // RunnerResult is a bounded structural view over the already parsed object. const result = asDict(envelope.result) as RunnerResult; if (!response.ok && result.response_source === undefined) { - return { response_source: { status: response.status, body: { code: 'scenario_runner_unavailable' } } }; + return { + response_source: { + status: response.status, + body: { + type: 'https://registrystack.org/problems/evidence/service_unavailable', + title: 'Authority Evidence is unavailable', + status: response.status + } + } + }; } return result; } @@ -124,7 +141,13 @@ export class LiveEvidenceProvider implements DetailedEvidenceProvider { code: string ): MockEvaluation { const seq = ++this.#seq; - const body = { error: code, error_description: 'The portal stopped this request before source access.' }; + const body = { + type: `urn:solmara:portal:problem:${code}`, + title: 'Portal authorization denied the request', + status: 403, + detail: 'The portal stopped this request before source access.', + operation: `denial:event-${seq}` + }; return { result: { state: 'error', @@ -139,20 +162,21 @@ export class LiveEvidenceProvider implements DetailedEvidenceProvider { body: { field: field.id, purpose: scenario.purpose, disclosure: scenario.disclosure } }, response: { status: 403, body } - } as unknown as MockEvaluation['raw'], + }, proof: { - headline: 'Portal authorization stopped the request before Registry Evidence was called', + headline: 'Portal authorization stopped the request before authority Evidence was called', answered: `Portal authorization gate returned 403 ${code}`, notDisclosed: scenario.notDisclosed, status: 'denied', authority: undefined, + purpose: scenario.purpose, + presentations: [], crypto: { - signedBy: 'Portal authorization gate; Registry Evidence was not called', + signedBy: 'Portal authorization gate; authority Evidence was not called', algorithm: 'No evidence assertion was produced', issuerKey: 'Not applicable', holderBound: 'Portal session and server-selected subject', - credential: 'No credential or evidence assertion returned', - auditId: `denial:event-${seq}` + credential: 'No credential or evidence assertion returned' } }, timing: { latencyMs: 0, staggerOrder: scenario.staggerOrder, slow: false } @@ -180,17 +204,13 @@ function runnerCalls(scenarioKey: string, service: string): RunnerCall[] { } function responseResults(response: RunnerResult): Dict[] { + if (Array.isArray(response.results)) { + return response.results.map(asDict).filter((item) => Object.keys(item).length > 0); + } const body = asDict(response.response_source?.body); return Array.isArray(body.results) ? body.results.map(asDict).filter((item) => Object.keys(item).length > 0) : []; } -function signedEvidence(response: RunnerResult): unknown[] { - const body = asDict(response.response_source?.body); - const signed = body.signed_evidence; - if (Array.isArray(signed)) return signed; - return signed && typeof signed === 'object' ? [signed] : []; -} - function outcomeFor( scenarioKey: string, claimId: string, @@ -225,18 +245,72 @@ function outcomeFor( return { found: item !== undefined, satisfied: typeof item?.satisfied === 'boolean' ? item.satisfied : null, value }; } -function evidenceProof(responses: RunnerResult[]): NonNullable { - const assertions = responses.flatMap(signedEvidence); +function evidenceProof( + presentations: EvidencePresentation[], + status: ProofTrace['status'] +): NonNullable { + const authorities = [...new Set(presentations.map((item) => item.authority))]; + const returned = status === 'ok' || status === 'false'; return { - signedBy: assertions.length ? 'Registry Evidence signed the returned assertion set' : 'No signed assertion was returned', - algorithm: assertions.length ? 'Flattened JWS, EdDSA' : 'Not available', - issuerKey: '/.well-known/evidence/jwks.json', - holderBound: 'Mint requester identity, purpose, requirement, nonce, and selector', - credential: 'Registry Evidence assertion, not an application credential', - auditId: `${assertions.length} signed assertion${assertions.length === 1 ? '' : 's'}` + signedBy: returned && authorities.length ? `${authorities.join(' and ')} issued the returned Evidence` : 'No Evidence assertion was returned', + algorithm: returned ? 'Verified Evidence assertion' : 'Not available', + issuerKey: returned ? 'Authority Evidence JWKS' : 'Not applicable', + holderBound: 'Audience-scoped to the portal request', + credential: returned ? 'Minimum-disclosure Evidence assertion' : 'No Evidence assertion returned' }; } +function presentationsFor( + scenarioKey: string, + scenario: (typeof SCENARIOS)[string], + responses: RunnerResult[] +): EvidencePresentation[] { + const supplied = responses.flatMap((response) => { + const direct = response.presentations ?? (response.presentation ? [response.presentation] : []); + const fromTrace = response.source_trace ?? []; + const fromResults = responseResults(response).map((result) => result.presentation); + return [...direct, ...fromTrace, ...fromResults].map(parsePresentation).filter(isPresentation); + }); + if (supplied.length > 0) return uniquePresentations(supplied); + + const plan = scenario.service === 'childBenefit' ? [] : authorityPlan(scenarioKey, scenario); + if (isApplicationOwnedPlan(plan)) { + return plan.map((entry) => evidencePresentation(entry.authorityId, entry.source)); + } + return [evidencePresentation(scenario.authority, sourceForScenario(scenario))]; +} + +function parsePresentation(value: unknown): EvidencePresentation | null { + const item = asDict(value); + const source = item.source; + const match = Object.values(SOLMARA_AUTHORITIES).find( + (authority) => authority.label === item.authority && authority.issuer === item.issuer + ); + if (!match || (source !== 'immutable extract' && source !== 'Relay lookup')) return null; + return evidencePresentation(match.id, source); +} + +function isPresentation(value: EvidencePresentation | null): value is EvidencePresentation { + return value !== null; +} + +function uniquePresentations(items: EvidencePresentation[]): EvidencePresentation[] { + const seen = new Set(); + return items.filter((item) => { + const key = `${item.serviceId}:${item.source}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +function sourceForScenario(scenario: (typeof SCENARIOS)[string]): EvidenceSource { + if (scenario.service === 'childBenefit' && ['childCivil', 'population', 'socialRegistry'].includes(scenario.authority)) { + return 'immutable extract'; + } + return 'Relay lookup'; +} + function requiredRunnerUrl(value: string | undefined): string { if (!value) throw new Error('SCENARIO_RUNNER_URL is required for PORTAL_PROVIDER=live'); try { @@ -247,6 +321,7 @@ function requiredRunnerUrl(value: string | undefined): string { } function asDict(value: unknown): Dict { + // The runtime guard establishes the dictionary shape used by safe readers. return value && typeof value === 'object' && !Array.isArray(value) ? value as Dict : {}; } diff --git a/portal/src/lib/providers/mock/index.test.ts b/portal/src/lib/providers/mock/index.test.ts index 8a4a8a0..a0b1475 100644 --- a/portal/src/lib/providers/mock/index.test.ts +++ b/portal/src/lib/providers/mock/index.test.ts @@ -3,11 +3,12 @@ import type { EvaluateContext } from '$lib/providers/EvidenceProvider'; import type { Field } from '$lib/types'; import { MockEvidenceProvider, PERSONA } from './index'; import { - CHILD_BENEFIT_FORMAT, + decodeEvidencePayload, + EVIDENCE_RESPONSE_FORMAT, type RawApplicationRequest, type RawApplicationResponse, - type RawChildBenefitRequest, - type RawEvaluateRequest + type RawEvidenceRequest, + type RawEvidenceResponse } from './wire'; const provider = new MockEvidenceProvider(); @@ -18,277 +19,287 @@ function field(id: string): Field { } describe('MockEvidenceProvider.evaluate', () => { - it('returns a GREEN verified state for a true predicate', async () => { - const res = await provider.evaluate(field('registered-farmer'), ctx); - expect(res.state).toBe('verified'); - expect(res.authority).toBe('agri'); - expect(res.traceId).toMatch(/^event \d+$/); + it('returns a verified authority result for a true predicate', async () => { + const result = await provider.evaluate(field('registered-farmer'), ctx); + expect(result).toMatchObject({ state: 'verified', authority: 'agri' }); + expect(result.traceId).toMatch(/^event \d+$/); }); - it('returns a verified pension stop predicate', async () => { - const res = await provider.evaluate(field('disability-determination'), ctx); - expect(res.state).toBe('verified'); - expect(res.display).toContain('Pension payment should stop'); - expect(res.authority).toBeUndefined(); + it('keeps a portal-composed decision separate from authority assertions', async () => { + const evaluation = await provider.evaluateDetailed(field('combined-support-eligibility'), ctx); + expect(evaluation.result.state).toBe('verified'); + expect(evaluation.result.authority).toBeUndefined(); + expect(evaluation.proof.authority).toBeUndefined(); + expect(evaluation.raw.request.method).toBe('MULTI'); + + const request = evaluation.raw.request.body as RawApplicationRequest; + const response = evaluation.raw.response.body as RawApplicationResponse; + expect(request.requests.map((source) => source.service_id)).toEqual([ + 'cra-evidence', + 'sipf-evidence', + 'sipf-evidence' + ]); + expect(request.requests.map((source) => source.body.requirement)).toEqual([ + 'urn:solmara:requirement:person-is-deceased:v1', + 'urn:solmara:requirement:pension-payment-active:v1', + 'urn:solmara:requirement:survivor-is-eligible:v1' + ]); + expect(response.signed_evidence).toHaveLength(3); + expect(response.derived_decisions).toEqual({ 'survivor-benefit-eligible': true }); + }); + + it('returns the verified pension-stop decision composed from its two authority requirements', async () => { + const evaluation = await provider.evaluateDetailed(field('disability-determination'), ctx); + expect(evaluation.result.state).toBe('verified'); + expect(evaluation.result.display).toContain('Pension payment should stop'); + expect(evaluation.result.authority).toBeUndefined(); + + const request = evaluation.raw.request.body as RawApplicationRequest; + expect(request.requests.map((source) => source.body.requirement)).toEqual([ + 'urn:solmara:requirement:person-is-deceased:v1', + 'urn:solmara:requirement:pension-payment-active:v1' + ]); }); - it('returns a delegated child age predicate', async () => { - // date-of-birth is the legacy field key for the age predicate, so the - // guardian link must be proven for the value to be returned. - const ev = await provider.evaluateDetailed(field('date-of-birth'), ctx, { + it('returns the delegated child-age predicate as minimized signed Evidence', async () => { + const evaluation = await provider.evaluateDetailed(field('date-of-birth'), ctx, { guardianLinkVerified: true }); - expect(ev.result.state).toBe('verified'); - // The child application returns minimized predicates without a duplicate value field. - const view = ev.raw.response.body as { results: { satisfied: boolean }[] }; - expect(view.results[0].satisfied).toBe(true); + expect(evaluation.result).toMatchObject({ state: 'verified', authority: 'childCivil' }); + const assertion = decodeEvidencePayload(evaluation.raw.response.body as RawEvidenceResponse); + expect(assertion.supportedValues).toEqual([ + { providesValueFor: 'urn:solmara:concept:child-age-under-5', value: true } + ]); }); - it('returns the missing population source predicate', async () => { - const ev = await provider.evaluateDetailed(field('population-record-active'), ctx, { + it('returns the population predicate from NIA Evidence with canonical attribution', async () => { + const evaluation = await provider.evaluateDetailed(field('population-record-active'), ctx, { guardianLinkVerified: true }); - expect(ev.result).toMatchObject({ + expect(evaluation.result).toMatchObject({ state: 'verified', display: 'Population record active: yes', authority: 'population' }); - // The child application response is a predicate-only evidence set. - const view = ev.raw.response.body as { - results: { claim_id: string; satisfied: boolean }[]; - }; - expect(view.results[0]).toMatchObject({ - claim_id: 'population-record-active', - satisfied: true + const assertion = decodeEvidencePayload(evaluation.raw.response.body as RawEvidenceResponse); + expect(assertion).toMatchObject({ + supportsRequirement: 'urn:solmara:requirement:population-record-active:v1', + issuedBy: 'did:web:id.registrystack.org:solmara:authority:nia', + providedBy: 'https://nia-evidence.solmara.registrystack.org/' }); }); - it('returns a boolean citizen-record decision without inventing certificate facts', async () => { - const ev = await provider.evaluateDetailed(field('citizen-record-status'), ctx); - expect(ev.result.state).toBe('verified'); - expect(ev.result.authority).toBeUndefined(); - expect(ev.proof.authority).toBeUndefined(); - const view = ev.raw.response.body as RawApplicationResponse; - expect(view.results.map((result) => result.provenance.generated_by.service_id)).toEqual([ - 'cra-notary', - 'nia-notary' - ]); - expect(view.derived_decisions).toEqual({ 'citizen-self-service-ready': true }); - expect(JSON.stringify(ev)).not.toMatch(/certificate_id|issued_on|CSR-BIRTH/); - }); + it('keeps citizen-record readiness portal-owned without inventing certificate facts', async () => { + const evaluation = await provider.evaluateDetailed(field('citizen-record-status'), ctx); + expect(evaluation.result.state).toBe('verified'); + expect(evaluation.result.authority).toBeUndefined(); + expect(evaluation.proof.authority).toBeUndefined(); - it('mirrors live multi-authority requests instead of fabricating a Notary-owned decision', async () => { - const ev = await provider.evaluateDetailed(field('combined-support-eligibility'), ctx); - expect(ev.result.state).toBe('verified'); - expect(ev.result.authority).toBeUndefined(); - expect(ev.proof.authority).toBeUndefined(); - expect(ev.raw.request.method).toBe('MULTI'); - const request = ev.raw.request.body as RawApplicationRequest; - const view = ev.raw.response.body as RawApplicationResponse; - expect(request.requests.map((source) => source.service_id)).toEqual([ - 'cra-notary', - 'sipf-notary', - 'sipf-notary' - ]); - expect(request.requests.map((source) => source.body.claims[0])).toEqual([ - 'person-is-deceased', - 'pension-payment-active', - 'survivor-is-eligible' - ]); - expect(view.results.map((result) => result.claim_id)).toEqual([ - 'person-is-deceased', - 'pension-payment-active', - 'survivor-is-eligible' + const response = evaluation.raw.response.body as RawApplicationResponse; + expect(response.signed_evidence.map((source) => source.authority)).toEqual([ + 'Civil Registration Authority', + 'National Identity Agency' ]); - expect(view.derived_decisions).toEqual({ 'survivor-benefit-eligible': true }); - expect(JSON.stringify(ev)).not.toContain('support_band'); + expect(response.derived_decisions).toEqual({ 'citizen-self-service-ready': true }); + expect(JSON.stringify(evaluation)).not.toMatch(/certificate_id|issued_on|CSR-BIRTH/); }); }); -describe('denial beat (cross-person stranger)', () => { - it('produces a 403 subject_mismatch with NO source read', async () => { - const ev = await provider.evaluateDetailed(field('denial'), ctx); - expect(ev.result.state).toBe('error'); - expect(ev.result.reasonCode).toBe('subject_mismatch'); - // 403 shape, no 200 results body - expect(ev.raw.response.status).toBe(403); - expect(ev.raw.response.body).not.toHaveProperty('results'); - expect(ev.raw.response.body).toHaveProperty('error', 'subject_mismatch'); - // the proof status is a denial; the rail will bounce - expect(ev.proof.status).toBe('denied'); - expect(ev.result.authority).toBeUndefined(); - expect(ev.proof.authority).toBeUndefined(); - expect(ev.proof.headline).toBe( - 'Portal denied the cross-person request before any authority call' - ); - expect(ev.raw.request.url).toBe('solmara://citizen-portal/blocked-before-authority-call'); - expect(ev.proof.answered).toContain('before any authority call'); - expect(ev.proof.crypto).toMatchObject({ - signedBy: 'Portal authorization gate; no authority Notary called', - credential: 'No credential or evidence result returned' +describe('portal authorization gates', () => { + it('denies a cross-person request before authority Evidence is called', async () => { + const evaluation = await provider.evaluateDetailed(field('denial'), ctx); + expect(evaluation.raw.response).toMatchObject({ status: 403 }); + expect(evaluation.raw.response.body).toMatchObject({ + type: 'urn:solmara:portal:problem:not_authorized', + status: 403 + }); + expect(evaluation.raw.request.url).toBe('solmara://citizen-portal/blocked-before-authority-call'); + expect(evaluation.proof).toMatchObject({ + status: 'denied', + authority: undefined, + crypto: { + signedBy: 'Portal authorization gate; authority Evidence was not called', + credential: 'No credential or Evidence assertion returned' + } }); - // the request targeted the stranger, never the session subject - // Denial scenarios use a single authority request, never an application batch. - const target = (ev.raw.request.body as RawEvaluateRequest).target; - expect(target.identifiers[0].value).toBe(PERSONA.karim); }); - it('does not perform a source read (no source_count > 0 on a denial)', async () => { - const ev = await provider.evaluateDetailed(field('denial'), ctx); - // there is no 200 results body at all, so no source was read - expect(ev.raw.response.body).not.toHaveProperty('results'); - }); + it('denies a dependent read until the guardian link is proven', async () => { + const denied = await provider.evaluateDetailed(field('date-of-birth'), ctx); + expect(denied.raw.response.status).toBe(403); + expect(denied.raw.response.body).not.toHaveProperty('protected'); - it('preserves the configured disclosure on direct authority responses', async () => { - const ev = await provider.evaluateDetailed(field('voucher-eligibility'), ctx); - const view = ev.raw.response.body as { results: { disclosure: string }[] }; - expect(view.results[0].disclosure).toBe('decision'); + const allowed = await provider.evaluateDetailed(field('date-of-birth'), ctx, { + guardianLinkVerified: true + }); + expect(allowed.raw.response.status).toBe(200); + const request = allowed.raw.request.body as RawEvidenceRequest; + expect(request.requirement).toBe('urn:solmara:requirement:child-age-under-5:v1'); + expect(request.subjects[0].selector.values.solmara_uin).toBe(PERSONA.mateo); }); -}); -describe('delegated two-hop gate', () => { - it('denies a civil read before the guardian link is proven (no dependent read)', async () => { - const ev = await provider.evaluateDetailed(field('date-of-birth'), ctx, { + it('denies a dependent read when the guardian link is explicitly false', async () => { + const evaluation = await provider.evaluateDetailed(field('date-of-birth'), ctx, { guardianLinkVerified: false }); - expect(ev.result.state).toBe('error'); - expect(ev.raw.response.status).toBe(403); - expect(ev.raw.response.body).not.toHaveProperty('results'); - expect(ev.proof.status).toBe('denied'); - expect(ev.proof.crypto.signedBy).toBe('Portal authorization gate; no authority Notary called'); + expect(evaluation.result).toMatchObject({ + state: 'error', + reasonCode: 'relationship_not_proven' + }); + expect(evaluation.raw.response.status).toBe(403); + expect(evaluation.proof.crypto.signedBy).toBe( + 'Portal authorization gate; authority Evidence was not called' + ); }); - it('denies a delegated civil read when the guardian flag is omitted (deny by default)', async () => { - const ev = await provider.evaluateDetailed(field('date-of-birth'), ctx); - expect(ev.result.state).toBe('error'); - expect(ev.raw.response.status).toBe(403); - expect(ev.raw.response.body).not.toHaveProperty('results'); - expect(ev.proof.status).toBe('denied'); - }); - - it('authorizes the civil read once the guardian link is proven', async () => { - const ev = await provider.evaluateDetailed(field('date-of-birth'), ctx, { - guardianLinkVerified: true - }); - expect(ev.result.state).toBe('verified'); - expect(ev.raw.response.status).toBe(200); - // The portal gate authorizes the read before the clean federator request is built. - // Child scenarios use the application request variant of the provider union. - const body = ev.raw.request.body as RawChildBenefitRequest; - expect(body.on_behalf_of).toBeUndefined(); - expect(body.target.identifiers[0].value).toBe(PERSONA.mateo); - expect(body.variables).toEqual({ as_of_date: '2026-01-15' }); + it('retains the attempted selector only server-side on a cross-person denial', async () => { + const evaluation = await provider.evaluateDetailed(field('denial'), ctx); + const request = evaluation.raw.request.body as RawEvidenceRequest; + expect(evaluation.result.reasonCode).toBe('not_authorized'); + expect(request.subjects[0].selector.values.solmara_uin).toBe(PERSONA.karim); + expect(evaluation.raw.response.body).not.toHaveProperty('protected'); + expect(evaluation.raw.response.body).not.toHaveProperty('signed_evidence'); }); }); -describe('resilience states', () => { - it('flags a slow call (>= 6s budget) before it resolves verified', async () => { - const ev = await provider.evaluateDetailed(field('slow'), ctx); - expect(ev.timing.slow).toBe(true); - expect(ev.result.state).toBe('verified'); - }); +describe('authority Evidence wire contract', () => { + it('uses EvidenceRequest v1 keys and the /v1/evidence route', async () => { + const evaluation = await provider.evaluateDetailed(field('registered-farmer'), ctx); + const request = evaluation.raw.request.body as RawEvidenceRequest; - it('lands an upstream failure in ERROR with no source read', async () => { - const ev = await provider.evaluateDetailed(field('error'), ctx); - expect(ev.result.state).toBe('error'); - expect(ev.raw.response.status).toBe(503); - expect(ev.raw.response.body).not.toHaveProperty('results'); - expect(ev.proof.crypto).toMatchObject({ - signedBy: 'No claim result; Social Registry Office was unavailable', - credential: 'No credential or evidence result returned' + expect(evaluation.raw.request.url).toBe('https://nagdi-evidence.solmara.registrystack.org/v1/evidence'); + expect(Object.keys(request)).toEqual([ + 'requestNonce', + 'requirement', + 'purpose', + 'subjects' + ]); + expect(request.requestNonce).toMatch(/^[A-Za-z0-9_-]{43}$/); + expect(request.requirement).toBe('urn:solmara:requirement:farmer-registered:v1'); + expect(request.purpose).toBe('voucher-eligibility-review'); + expect(request.subjects[0]).toMatchObject({ + role: 'subject', + selector: { profile: 'solmara-farmer-v1' } }); }); - it('marks a fetched-but-old value as stale', async () => { - const ev = await provider.evaluateDetailed(field('stale'), ctx); - expect(ev.result.state).toBe('stale'); - const view = ev.raw.response.body as { results: { issued_at: string; expires_at: string }[] }; - expect(Date.parse(view.results[0].issued_at)).toBeLessThan(Date.parse(view.results[0].expires_at)); - expect(Date.parse(view.results[0].expires_at)).toBeLessThan(Date.now()); - }); + it('returns a flattened JWS carrying an Evidence assertion payload', async () => { + const evaluation = await provider.evaluateDetailed(field('registered-farmer'), ctx); + const response = evaluation.raw.response.body as RawEvidenceResponse; + expect(Object.keys(response)).toEqual(['protected', 'payload', 'signature']); + expect(response.signature).toMatch(/^[A-Za-z0-9_-]{86}$/); - it('never collapses an ambiguous match to false', async () => { - const ev = await provider.evaluateDetailed(field('ambiguous'), ctx); - expect(ev.result.state).toBe('ambiguous'); + const assertion = decodeEvidencePayload(response); + expect(assertion).toMatchObject({ + schema: 'registry.assertion-evidence/v1', + assuranceProfile: 'evidence-grade', + subjectBinding: 'audience-scoped', + type: 'Evidence', + supportsRequirement: 'urn:solmara:requirement:farmer-registered:v1', + purpose: 'voucher-eligibility-review' + }); + expect(assertion.supportedValues).toEqual([ + { providesValueFor: 'urn:solmara:concept:farmer-registered', value: true } + ]); + expect(EVIDENCE_RESPONSE_FORMAT).toBe('application/jose+json'); + expect(evaluation.proof.crypto).toMatchObject({ + algorithm: 'Flattened JWS, ES256', + issuerKey: '/.well-known/evidence/jwks.json', + credential: 'Signed minimum-disclosure Evidence assertion' + }); }); -}); -describe('structural match to the Notary OpenAPI', () => { - // The depth-2 bodies must carry the same key set as EvaluateRequest / - // EvaluationResponse -> ClaimResultView. We assert the required key sets here so - // a drift from the OpenAPI fails loudly. - it('request body matches EvaluateRequest key set', async () => { - const ev = await provider.evaluateDetailed(field('registered-farmer'), ctx); - // The agriculture scenario uses a single Registry Notary request. - const body = ev.raw.request.body as Record; - expect(Object.keys(body)).toEqual([ - 'claims', - 'purpose', - 'disclosure', - 'format', - 'relationship', - 'target' + it('encodes a reviewed decision object as an Evidence structured value', async () => { + const evaluation = await provider.evaluateDetailed(field('voucher-eligibility'), ctx); + const assertion = decodeEvidencePayload(evaluation.raw.response.body as RawEvidenceResponse); + expect(assertion.supportedValues).toEqual([ + { + providesValueFor: 'urn:solmara:concept:eligible-for-climate-smart-input-voucher', + value: { + form: 'reviewed-structured-value', + schema: 'urn:solmara:value-schema:eligible-for-climate-smart-input-voucher:v1', + fields: { eligible: true, voucher_tier: 'standard' } + } + } ]); - expect(body.claims).toEqual(['farmer-registered']); + expect(JSON.stringify(assertion)).not.toContain('parcel'); }); - it('200 response result matches the ClaimResultView required key set', async () => { - const ev = await provider.evaluateDetailed(field('registered-farmer'), ctx); - // The agriculture scenario uses the mock ClaimResultView response variant. - const view = (ev.raw.response.body as { results: Record[] }).results[0]; - // every required key from the OpenAPI ClaimResultView is present - for (const key of [ - 'evaluation_id', - 'claim_id', - 'claim_version', - 'subject_type', - 'target_ref', - 'value', - 'satisfied', - 'disclosure', - 'format', - 'issued_at', - 'expires_at', - 'provenance' - ]) { - expect(view).toHaveProperty(key); - } - // Provenance is asserted structurally after the required top-level key check. - const prov = view.provenance as Record; - expect(prov).toHaveProperty('schema_version', 'registry-notary-claim-provenance/v2'); - expect(prov).toHaveProperty('generated_by'); - expect(prov).toHaveProperty('used'); - expect(prov).toHaveProperty('derived_from'); + it('keeps authority Evidence service identity aligned with each legal issuer', async () => { + const evaluation = await provider.evaluateDetailed(field('combined-support-eligibility'), ctx); + const response = evaluation.raw.response.body as RawApplicationResponse; + expect(response.signed_evidence.map((source) => source.service_id)).toEqual([ + 'cra-evidence', + 'sipf-evidence', + 'sipf-evidence' + ]); + expect( + response.signed_evidence.map((source) => decodeEvidencePayload(source.assertion).issuedBy) + ).toEqual([ + 'did:web:id.registrystack.org:solmara:authority:cra', + 'did:web:id.registrystack.org:solmara:authority:sipf', + 'did:web:id.registrystack.org:solmara:authority:sipf' + ]); + expect( + response.signed_evidence.map((source) => decodeEvidencePayload(source.assertion).providedBy) + ).toEqual([ + 'https://cra-evidence.solmara.registrystack.org/', + 'https://sipf-evidence.solmara.registrystack.org/', + 'https://sipf-evidence.solmara.registrystack.org/' + ]); }); - it('uses the ordinary child application evidence request and source-attributed proof contract', async () => { - const ev = await provider.evaluateDetailed(field('date-of-birth'), ctx, { + it('uses the CRA Evidence endpoint and signed proof contract for child predicates', async () => { + const evaluation = await provider.evaluateDetailed(field('date-of-birth'), ctx, { guardianLinkVerified: true }); - expect(ev.raw.request).toEqual({ - method: 'POST', - url: 'https://child-benefit-federator.solmara.registrystack.org/v1/evaluations', - body: { - target: { - type: 'Person', - identifiers: [{ scheme: 'solmara_uin', value: PERSONA.mateo }] - }, - claims: ['child-age-under-5'], - disclosure: 'predicate', - format: CHILD_BENEFIT_FORMAT, - variables: { as_of_date: '2026-01-15' } - } - }); - expect(ev.proof.crypto).toMatchObject({ - signedBy: - 'Civil Registration Authority source result collected by child-benefit-federator', - algorithm: 'Ordinary JSON response; no application signature asserted', - issuerKey: 'Not applicable for an application evidence set', - holderBound: 'Purpose- and subject-bound child-benefit evidence request', - credential: 'Minimized source-attributed predicate result' + expect(evaluation.raw.request.url).toBe('https://cra-evidence.solmara.registrystack.org/v1/evidence'); + expect(evaluation.proof.crypto).toMatchObject({ + signedBy: 'Civil Registration Authority Evidence', + algorithm: 'Flattened JWS, ES256', + issuerKey: '/.well-known/evidence/jwks.json', + credential: 'Signed minimum-disclosure Evidence assertion' }); - expect(JSON.stringify(ev.proof.crypto)).not.toMatch( - /federated|federation|SD-JWT|notary:citizen/ + }); +}); + +describe('resilience states', () => { + it('flags a slow call without changing its eventual verified result', async () => { + const evaluation = await provider.evaluateDetailed(field('slow'), ctx); + expect(evaluation.timing.slow).toBe(true); + expect(evaluation.result.state).toBe('verified'); + }); + + it('scopes an unavailable Evidence service to the field', async () => { + const evaluation = await provider.evaluateDetailed(field('error'), ctx); + expect(evaluation.result.state).toBe('error'); + expect(evaluation.raw.response.status).toBe(503); + expect(evaluation.proof.crypto.signedBy).toBe( + 'No Evidence assertion; Social Registry Office was unavailable' ); }); + + it('never collapses an ambiguous source match to false', async () => { + const evaluation = await provider.evaluateDetailed(field('ambiguous'), ctx); + expect(evaluation.result.state).toBe('ambiguous'); + }); + + it('preserves stale assertion timestamps for the freshness warning', async () => { + const evaluation = await provider.evaluateDetailed(field('stale'), ctx); + const assertion = decodeEvidencePayload(evaluation.raw.response.body as RawEvidenceResponse); + expect(evaluation.result).toMatchObject({ state: 'stale', asOf: '2025-09-30' }); + expect(Date.parse(assertion.issuedAt)).toBeLessThan(Date.parse(assertion.validUntil)); + expect(Date.parse(assertion.validUntil)).toBeLessThan(Date.now()); + }); + + it('returns the current Evidence problem shape when the authority service is unavailable', async () => { + const evaluation = await provider.evaluateDetailed(field('error'), ctx); + expect(evaluation.result.reasonCode).toBe('service_unavailable'); + expect(evaluation.raw.response.body).toMatchObject({ + type: 'https://registrystack.org/problems/evidence/service_unavailable', + title: 'Authority Evidence is unavailable', + status: 503 + }); + expect(evaluation.raw.response.body).not.toHaveProperty('signature'); + }); }); diff --git a/portal/src/lib/providers/mock/index.ts b/portal/src/lib/providers/mock/index.ts index 744cfd2..5174a95 100644 --- a/portal/src/lib/providers/mock/index.ts +++ b/portal/src/lib/providers/mock/index.ts @@ -3,11 +3,11 @@ // per field/claim, with deterministic latency and a top-to-bottom stagger. // // The depth-2 request/response bodies inside each ProofTrace are built by ./wire -// from the owning service contract. Notary services keep their evaluation shape; -// child benefit uses its ordinary source-attributed JSON application shape. +// from the current authority Evidence request and signed-assertion contracts. import type { EvaluateContext, EvidenceProvider } from '$lib/providers/EvidenceProvider'; -import type { ClaimResult, Field, ProofStatus, ProofTrace } from '$lib/types'; +import type { ClaimResult, EvidencePresentation, EvidenceSource, Field, ProofStatus, ProofTrace } from '$lib/types'; +import { evidencePresentation } from '$lib/fields/authorities'; import { PERSONA, SCENARIOS, type ScenarioResult } from './scenarios'; import { authorityPlan, @@ -16,18 +16,16 @@ import { } from '$lib/providers/authority-plan'; import { authorityLabel, - buildClaimResultView, - buildChildBenefitRequest, - buildChildBenefitResponse, - buildEvaluationRequest, + buildEvidenceAssertion, + buildEvidenceRequest, buildRawRequest, buildRawResponse, - makeEvaluationId, - notaryUrl, + evidenceUrl, + makeOperationId, + signEvidence, type RawApplicationRequest, type RawApplicationResponse, - type RawProviderRequest, - type RawProviderResponse + type RawProviderRequest } from './wire'; // Optional knobs for delegated / denial selection the BFF passes via ctx-derived @@ -43,13 +41,13 @@ export type EvaluateOptions = { }; // The richer evaluate result the BFF consumes: the portal-facing ClaimResult plus -// the RAW (un-redacted) wire request/response and depth-1/3 proof material. The -// BFF redacts the raw bodies before teeing them to the feed. +// the raw server-only request/response and safe proof presentation. Raw values +// are never copied into the browser-facing ProofTrace. export type MockEvaluation = { result: ClaimResult; raw: { - request: { method: string; url: string; body: RawProviderRequest }; - response: { status: number; body: RawProviderResponse | DenialBody }; + request: { method: string; url: string; body: unknown }; + response: { status: number; body: unknown }; }; proof: { headline: string; @@ -57,13 +55,21 @@ export type MockEvaluation = { notDisclosed: string; status: ProofTrace['status']; authority: ProofTrace['authority']; + purpose: string; + presentations: EvidencePresentation[]; crypto: NonNullable; }; // deterministic timing for the UI stagger / SLOW threshold choreography. timing: { latencyMs: number; staggerOrder: number; slow: boolean }; }; -type DenialBody = { error: string; error_description: string }; +type DenialBody = { + type: string; + title: string; + status: number; + detail: string; + operation: string; +}; const SLOW_THRESHOLD_MS = 6000; @@ -84,68 +90,52 @@ export function resolveScenarioKey(field: Field, opts?: EvaluateOptions): string function resolveSubject(scenario: ScenarioResult, ctx: EvaluateContext, key: string): string { if (key === 'denial') return PERSONA.karim; if (scenario.subjectPersona) return PERSONA[scenario.subjectPersona]; - if (scenario.notary === 'agri') return PERSONA.aminaFarmer; + if (scenario.authority === 'agri') return PERSONA.aminaFarmer; if (scenario.delegated) return ctx.delegatedTarget ?? PERSONA.mateo; return ctx.subject; } function buildCrypto( scenario: ScenarioResult, - evaluationId: string, + _evaluationId: string, plan: AuthorityPlan[] ): NonNullable { - if (scenario.service === 'childBenefit') { - return { - signedBy: `${authorityLabel(scenario)} source result collected by child-benefit-federator`, - algorithm: 'Ordinary JSON response; no application signature asserted', - issuerKey: 'Not applicable for an application evidence set', - holderBound: 'Purpose- and subject-bound child-benefit evidence request', - credential: 'Minimized source-attributed predicate result', - auditId: `evidence-set:cbe_${evaluationId}` - }; - } if (isApplicationOwnedPlan(plan)) { const authorities = [...new Set(plan.map((authority) => authority.authority))]; return { - signedBy: `No credential issued; ${authorities.join(' and ')} returned separate claim evaluations`, - algorithm: 'Independent Registry Notary claim-result responses', - issuerKey: 'Not applicable for claim-result evaluation', - holderBound: 'Not credential-bound; the BFF selected the purpose and subject', - credential: 'Application decision only; no credential issued by the portal', - auditId: plan - .map((_authority, index) => `evaluation:${evaluationId}-${index + 1}`) - .join('; ') + signedBy: `${authorities.join(' and ')} returned separate authority Evidence assertions`, + algorithm: 'Independent flattened JWS assertions, ES256', + issuerKey: 'Each Evidence service publishes /.well-known/evidence/jwks.json', + holderBound: 'Audience-scoped to the portal requester, purpose, nonce, and subject binding', + credential: 'Signed minimum-disclosure assertions; the portal composed the decision', }; } return { - signedBy: `No credential issued; ${authorityLabel(scenario)} returned a claim result`, - algorithm: 'Registry Notary claim-result response', - issuerKey: 'Not applicable for claim-result evaluation', - holderBound: 'Not credential-bound; the BFF selected the purpose and subject', - credential: 'Claim result only; no credential issued', - auditId: `evaluation:${evaluationId}` + signedBy: `${authorityLabel(scenario)} Evidence`, + algorithm: 'Flattened JWS, ES256', + issuerKey: '/.well-known/evidence/jwks.json', + holderBound: 'Audience-scoped to the portal requester, purpose, nonce, and subject binding', + credential: 'Signed minimum-disclosure Evidence assertion' }; } -function blockedCrypto(evaluationId: string): NonNullable { +function blockedCrypto(_evaluationId: string): NonNullable { return { - signedBy: 'Portal authorization gate; no authority Notary called', - algorithm: 'No signature; request stopped before source access', + signedBy: 'Portal authorization gate; authority Evidence was not called', + algorithm: 'No Evidence assertion was produced', issuerKey: 'Not applicable', holderBound: 'Portal session actor and server-selected subject', - credential: 'No credential or evidence result returned', - auditId: `denial:${evaluationId}` + credential: 'No credential or Evidence assertion returned' }; } function unavailableCrypto(scenario: ScenarioResult): NonNullable { return { - signedBy: `No claim result; ${authorityLabel(scenario)} was unavailable`, + signedBy: `No Evidence assertion; ${authorityLabel(scenario)} was unavailable`, algorithm: 'No response proof available', issuerKey: 'Not applicable', holderBound: 'The BFF selected the purpose and subject', - credential: 'No credential or evidence result returned', - auditId: 'No evaluation identifier returned' + credential: 'No credential or Evidence assertion returned' }; } @@ -184,7 +174,7 @@ export class MockEvidenceProvider implements EvidenceProvider { const subject = resolveSubject(scenario, ctx, key); const seq = ++this.#seq; - const evaluationId = makeEvaluationId(seq); + const evaluationId = makeOperationId(seq); const issuedAt = scenario.state === 'stale' ? new Date(`${scenario.asOf}T00:00:00.000Z`) @@ -202,32 +192,22 @@ export class MockEvidenceProvider implements EvidenceProvider { ? buildApplicationExchange(scenario, plan, subject, evaluationId, issuedAt) : undefined; const rawRequest = - applicationExchange?.request ?? buildProviderRequest(scenario, subject, ctx.subject); + applicationExchange?.request ?? buildProviderRequest(scenario, subject); // Denial / error scenarios perform NO source read: there is no 200 body. if (scenario.httpStatus === 403) { - return this.#denied(field, scenario, ctx, key, scenario.denial?.code ?? 'subject_mismatch'); + return this.#denied(field, scenario, ctx, key, scenario.denial?.code ?? 'not_authorized'); } if (scenario.httpStatus === 503) { return this.#errored(field, scenario, ctx, key, rawRequest); } - const rawResponse = - applicationExchange?.response ?? - (scenario.service === 'childBenefit' - ? buildChildBenefitResponse( - scenario, - evaluationId, - issuedAt, - // The service branch above guarantees the child application request shape. - (rawRequest as ReturnType).target - ) - : buildRawResponse(scenario, evaluationId, issuedAt)); + const rawResponse = applicationExchange?.response ?? buildRawResponse(scenario, evaluationId, issuedAt); const result: ClaimResult = { state: scenario.state, display: scenario.display, - ...(!applicationOwned ? { authority: scenario.notary } : {}), + ...(!applicationOwned ? { authority: scenario.authority } : {}), asOf: scenario.asOf, ...(scenario.reasonCode ? { reasonCode: scenario.reasonCode } : {}), traceId: `event ${seq}` @@ -240,7 +220,7 @@ export class MockEvidenceProvider implements EvidenceProvider { method: applicationOwned ? 'MULTI' : 'POST', url: applicationOwned ? 'solmara://citizen-portal/application-composition' - : notaryUrl(scenario), + : evidenceUrl(scenario), body: rawRequest }, response: { status: scenario.httpStatus, body: rawResponse } @@ -250,7 +230,11 @@ export class MockEvidenceProvider implements EvidenceProvider { answered: scenario.answered, notDisclosed: scenario.notDisclosed, status: scenario.status, - authority: applicationOwned ? undefined : scenario.notary, + authority: applicationOwned ? undefined : scenario.authority, + purpose: scenario.purpose, + presentations: applicationOwned + ? plan.map((entry) => evidencePresentation(entry.authorityId, entry.source)) + : [evidencePresentation(scenario.authority, sourceForScenario(scenario))], crypto: buildCrypto(scenario, evaluationId, plan) }, timing: { @@ -270,16 +254,18 @@ export class MockEvidenceProvider implements EvidenceProvider { code: string ): MockEvaluation { const seq = ++this.#seq; - const evaluationId = makeEvaluationId(seq); + const evaluationId = makeOperationId(seq); // We still show the REQUEST the BFF attempted, with the stranger target, so the // inspector shows what was asked, then the 403 with no source read. The target // is redacted before it ever reaches the feed. const subject = scenario.denial ? PERSONA.karim : (scenario.subjectPersona ? PERSONA[scenario.subjectPersona] : PERSONA.mateo); - const rawRequest = buildProviderRequest(scenario, subject, _ctx.subject); + const rawRequest = buildProviderRequest(scenario, subject); const denialBody: DenialBody = { - error: code, - error_description: - scenario.denial?.message ?? 'requester is not authorized for this target' + type: `urn:solmara:portal:problem:${code}`, + title: 'Portal authorization denied the request', + status: 403, + detail: scenario.denial?.message ?? 'requester is not authorized for this target', + operation: evaluationId }; const result: ClaimResult = { state: 'error', @@ -309,6 +295,8 @@ export class MockEvidenceProvider implements EvidenceProvider { notDisclosed: scenario.notDisclosed, status: 'denied', authority: undefined, + purpose: scenario.purpose, + presentations: [], crypto: blockedCrypto(evaluationId) }, timing: { @@ -330,20 +318,23 @@ export class MockEvidenceProvider implements EvidenceProvider { ): MockEvaluation { const seq = this.#seq; // already incremented by the caller const errBody: DenialBody = { - error: 'upstream_unavailable', - error_description: `could not reach ${authorityLabel(scenario)}` + type: 'https://registrystack.org/problems/evidence/service_unavailable', + title: 'Authority Evidence is unavailable', + status: 503, + detail: `could not reach ${authorityLabel(scenario)}`, + operation: `unavailable:${seq}` }; const result: ClaimResult = { state: 'error', display: scenario.display, - authority: scenario.notary, + authority: scenario.authority, reasonCode: scenario.reasonCode, traceId: `event ${seq}` }; return { result, raw: { - request: { method: 'POST', url: notaryUrl(scenario), body: rawRequest }, + request: { method: 'POST', url: evidenceUrl(scenario), body: rawRequest }, response: { status: 503, body: errBody } }, proof: { @@ -351,7 +342,9 @@ export class MockEvidenceProvider implements EvidenceProvider { answered: scenario.answered, notDisclosed: scenario.notDisclosed, status: 'error', - authority: scenario.notary, + authority: scenario.authority, + purpose: scenario.purpose, + presentations: [evidencePresentation(scenario.authority, sourceForScenario(scenario))], crypto: unavailableCrypto(scenario) }, timing: { @@ -376,37 +369,19 @@ function buildApplicationExchange( purposes.length === 1 ? (purposes[0] ?? '') : 'application-composed-from-source-authorized-purposes', - disclosure: 'decision', - composition: 'application', - requests: plan.map((authority) => ({ + composition: 'portal-application', + requests: plan.map((authority, index) => ({ authority: authority.authority, service_id: authority.serviceId, - body: buildEvaluationRequest( + body: buildEvidenceRequest( authority.claimId, subject, authority.purpose, - authority.scheme ?? 'solmara_uin' + authority.scheme ?? 'solmara_uin', + index + 1 ) })) }; - const results = plan.map((authority, index) => - buildClaimResultView( - { - claimId: authority.claimId, - claimVersion: scenario.claimVersion, - serviceId: authority.serviceId, - subjectType: 'person', - satisfied: true, - value: true, - disclosure: 'predicate', - sourceCount: 1, - identifierScheme: authority.scheme ?? 'solmara_uin', - freshnessDays: scenario.freshnessDays - }, - `${evaluationId}-${index + 1}`, - issuedAt - ) - ); return { request, response: { @@ -415,12 +390,27 @@ function buildApplicationExchange( service_id: 'citizen-portal', decision: 'application_composed' }, - results, + signed_evidence: plan.map((authority, index) => ({ + authority: authority.authority, + service_id: authority.serviceId, + assertion: signEvidence( + buildEvidenceAssertion( + scenario, + `${evaluationId}-${index + 1}`, + issuedAt, + authority.claimId, + authority.serviceId, + true, + authority.authorityId + ), + authority.serviceId + ) + })), source_trace: plan.map((authority) => ({ authority: authority.authority, service_id: authority.serviceId, status: 200, - claims: [authority.claimId] + requirements: [`urn:solmara:requirement:${authority.claimId}:v1`] })), derived_decisions: { [scenario.claimId]: scenario.satisfied } } @@ -429,32 +419,25 @@ function buildApplicationExchange( function buildProviderRequest( scenario: ScenarioResult, - subject: string, - actorSubject: string + subject: string ): RawProviderRequest { - if (scenario.service === 'childBenefit') { - return buildChildBenefitRequest(scenario, subject); - } - return buildRawRequest(scenario, subject, { - actorIdHash: scenario.delegated ? hashActor(actorSubject) : undefined, - delegationRef: scenario.delegated ? 'rnref:v1:REL-1001-MOTHER' : undefined - }); -} - -// A keyed-hash placeholder for an actor id. Never the raw principal; matches the -// id_hash wire shape (hmac-sha256:). Deterministic for the mock. -function hashActor(subject: string): string { - let h = 0; - for (let i = 0; i < subject.length; i++) { - h = (h * 31 + subject.charCodeAt(i)) & 0xffffffff; - } - return `hmac-sha256:${(h >>> 0).toString(16).padStart(8, '0')}`; + return buildRawRequest(scenario, subject); } function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } +function sourceForScenario(scenario: ScenarioResult): EvidenceSource { + if ( + scenario.service === 'childBenefit' && + ['childCivil', 'population', 'socialRegistry'].includes(scenario.authority) + ) { + return 'immutable extract'; + } + return 'Relay lookup'; +} + export type { ProofStatus }; // Re-export the canned scenario keys so the BFF / state gallery can enumerate the diff --git a/portal/src/lib/providers/mock/scenarios.test.ts b/portal/src/lib/providers/mock/scenarios.test.ts index f568499..a823a3b 100644 --- a/portal/src/lib/providers/mock/scenarios.test.ts +++ b/portal/src/lib/providers/mock/scenarios.test.ts @@ -1,19 +1,23 @@ import { describe, expect, it } from 'vitest'; -import { AUTHORITY_LABEL, NOTARY_SERVICE_ID, SCENARIOS } from './scenarios'; +import { AUTHORITY_LABEL, EVIDENCE_SERVICE_ID, SCENARIOS } from './scenarios'; // The depth-1 proof feed always names the authority that answered. Every // "answered" line must open with that authority's canonical // label (the same label the field badge, ministry rail, and proof crypto -// "signedBy" already use for that notary id), never an operational short +// "signedBy" already use for that authority id), never an operational short // name or nickname. describe('SCENARIOS answered lines use the canonical authority label', () => { for (const [key, scenario] of Object.entries(SCENARIOS)) { - const label = AUTHORITY_LABEL[scenario.notary]; + const label = AUTHORITY_LABEL[scenario.authority]; it(`"${key}" attributes its answered line to its actual owner`, () => { expect( scenario.answered.startsWith( - scenario.applicationOwned ? 'Portal application answered:' : `${label} answered:` + scenario.status === 'denied' + ? 'Portal authorization gate answered:' + : scenario.applicationOwned + ? 'Portal application answered:' + : `${label} answered:` ) ).toBe(true); }); @@ -30,7 +34,7 @@ describe('pension purpose ownership', () => { }); describe('child-benefit source ownership', () => { - it('uses five predicates from four source-owned Notaries without a composed eligibility scenario', () => { + it('uses five predicates from four authority-owned Evidence services without a composed eligibility scenario', () => { const scenarios = [ SCENARIOS['birth-event-exists'], SCENARIOS['population-record-active'], @@ -46,16 +50,17 @@ describe('child-benefit source ownership', () => { 'household-below-poverty-threshold', 'not-already-enrolled' ]); - expect(new Set(scenarios.map((scenario) => scenario.notary))).toEqual( + expect(new Set(scenarios.map((scenario) => scenario.authority))).toEqual( new Set(['childCivil', 'population', 'socialRegistry', 'programme']) ); expect(SCENARIOS['eligible-for-child-benefit']).toBeUndefined(); - expect(scenarios.map((scenario) => NOTARY_SERVICE_ID[scenario.notary])).toEqual([ - 'cra-notary', - 'nia-notary', - 'cra-notary', - 'sro-notary', - 'programme-notary' + expect(scenarios.map((scenario) => EVIDENCE_SERVICE_ID[scenario.authority])).toEqual([ + 'cra-evidence', + 'nia-evidence', + 'cra-evidence', + 'sro-evidence', + 'mosd-programme-evidence' ]); + expect(new Set(scenarios.map((scenario) => EVIDENCE_SERVICE_ID[scenario.authority])).size).toBe(4); }); }); diff --git a/portal/src/lib/providers/mock/scenarios.ts b/portal/src/lib/providers/mock/scenarios.ts index 9d31f1b..c28054f 100644 --- a/portal/src/lib/providers/mock/scenarios.ts +++ b/portal/src/lib/providers/mock/scenarios.ts @@ -1,19 +1,18 @@ // Canned scenarios driving the Phase 0 mock. Each scenario is keyed by a stable // lookup id (field id, with a few delegated/denial variants) and carries enough // to build both a ClaimResult and a ProofTrace using the owning service's real -// wire contract: Registry Notary evaluations or the child federator bundle. +// wire contract: signed authority Evidence assertions or a portal composition. // // Volatile fields (evaluation_id, issued_at/expires_at, signatures, freshness) // are present but value-variable: they are stamped at evaluate() time, never // byte-matched. Everything else (key set, types, ordering) matches the OpenAPI. -import type { FieldState, NotaryId, ProofStatus } from '$lib/types'; +import type { AuthorityId, FieldState, ProofStatus } from '$lib/types'; import { SOLMARA_AUTHORITIES } from '$lib/fields/authorities'; import { PURPOSES } from '$lib/forms/descriptors'; -// Map a portal NotaryId to the human authority label the Notary returns and the -// proof inspector shows. Single canonical name per authority. -export const AUTHORITY_LABEL: Record = { +// Map a portal authority id to the human label shown by the proof inspector. +export const AUTHORITY_LABEL: Record = { civil: SOLMARA_AUTHORITIES.civil.label, social: SOLMARA_AUTHORITIES.social.label, agri: SOLMARA_AUTHORITIES.agri.label, @@ -24,8 +23,8 @@ export const AUTHORITY_LABEL: Record = { programme: SOLMARA_AUTHORITIES.programme.label }; -// Per-notary service id that appears in provenance.generated_by.service_id. -export const NOTARY_SERVICE_ID: Record = { +// Authority-owned Evidence service shown in request provenance. +export const EVIDENCE_SERVICE_ID: Record = { civil: SOLMARA_AUTHORITIES.civil.serviceId, social: SOLMARA_AUTHORITIES.social.serviceId, agri: SOLMARA_AUTHORITIES.agri.serviceId, @@ -36,27 +35,26 @@ export const NOTARY_SERVICE_ID: Record = { programme: SOLMARA_AUTHORITIES.programme.serviceId }; -// What the Notary sends back as source_authority / the proof "answered" line. -// The `disclosure` mirrors the EvaluateRequest.disclosure on the wire. +// The disclosure mode drives the minimized value represented by the assertion. export type ScenarioDisclosure = 'predicate' | 'value' | 'object' | 'decision'; // The depth-2 response value (the ClaimResultView.value). The runtime may return // any JSON value; we keep it as unknown so booleans, dates, and objects all fit. export type ScenarioResult = { // ---- routing / lookup ---- - notary: NotaryId; + authority: AuthorityId; service: 'childBenefit' | 'pension' | 'nagdi' | 'citizen'; claimId: string; // the wire claim id, e.g. 'farmer-registered' claimVersion: string; subjectPersona?: PersonaKey; applicationOwned?: boolean; - // ---- request shaping (EvaluateRequest) ---- + // ---- Authority Evidence request shaping ---- purpose: string; // declared purpose disclosure: ScenarioDisclosure; // delegated scenarios send on_behalf_of + relationship:guardian, and read a // dependent subject. Non-delegated scenarios are relationship:self. delegated?: boolean; - // ---- response / claim-result shaping (ClaimResultView) ---- + // ---- response / assertion projection ---- value: unknown; // boolean | string (date) | object summary satisfied: boolean | null; // null for plain value/object fetches subjectType: string; // 'person' | 'household' | 'holding' @@ -66,7 +64,7 @@ export type ScenarioResult = { state: FieldState; // resulting FieldState display: string; // the value/predicate sentence shown in the field reasonCode?: string; // e.g. 'VR-RED-02' - reasonCodes?: { code: string; authority: NotaryId; text: string }[]; // decisions + reasonCodes?: { code: string; authority: AuthorityId; text: string }[]; // decisions // ---- proof depth-1 copy ---- headline: string; // consequence-first answered: string; // "{Authority} answered: {claim} = {value}" @@ -74,7 +72,7 @@ export type ScenarioResult = { status: ProofStatus; // ---- denial / error shaping ---- httpStatus: number; // 200 normally; 403 denial; 503 error - denial?: { code: string; message: string }; // for the subject_mismatch beat + denial?: { code: string; message: string }; // for the not_authorized beat // ---- resilience flavor ---- // latencyMs is the deterministic delay; staggerOrder gives the top-to-bottom // stagger so fields land in a believable cascade, never all at once. @@ -105,7 +103,7 @@ export const SCENARIOS: Record = { // farmer-voucher // --------------------------------------------------------------------------- 'registered-farmer': { - notary: 'agri', + authority: 'agri', service: 'nagdi', claimId: 'farmer-registered', claimVersion: '2026-07', @@ -129,7 +127,7 @@ export const SCENARIOS: Record = { sourceCount: 1 }, 'farm-holding': { - notary: 'agri', + authority: 'agri', service: 'nagdi', claimId: 'data-use-authorized-for-purpose', claimVersion: '2026-07', @@ -153,7 +151,7 @@ export const SCENARIOS: Record = { sourceCount: 1 }, 'voucher-eligibility': { - notary: 'agri', + authority: 'agri', service: 'nagdi', claimId: 'eligible-for-climate-smart-input-voucher', claimVersion: '2026-07', @@ -185,7 +183,7 @@ export const SCENARIOS: Record = { // child-benefit (guardian gate, then five source-owned predicates) // --------------------------------------------------------------------------- 'caregiver-link': { - notary: 'childCivil', + authority: 'childCivil', service: 'childBenefit', claimId: 'birth-is-registered', claimVersion: '2026-07', @@ -211,7 +209,7 @@ export const SCENARIOS: Record = { // The source reads below are only authorized after the caregiver-link verify // above succeeds. The provider enforces this gate. 'birth-event-exists': { - notary: 'childCivil', + authority: 'childCivil', service: 'childBenefit', claimId: 'birth-is-registered', claimVersion: '2026-07', @@ -236,7 +234,7 @@ export const SCENARIOS: Record = { sourceCount: 1 }, 'population-record-active': { - notary: 'population', + authority: 'population', service: 'childBenefit', claimId: 'population-record-active', claimVersion: '2026-07', @@ -251,7 +249,7 @@ export const SCENARIOS: Record = { asOf: '2026-06-15', state: 'verified', display: 'Population record active: yes', - headline: 'Confirmed by the National Identity Agency through a source-owned Notary', + headline: 'Confirmed by National Identity Agency Evidence', answered: 'National Identity Agency answered: population-record-active = true', notDisclosed: 'Not disclosed: identity attributes or population register row', status: 'ok', @@ -261,7 +259,7 @@ export const SCENARIOS: Record = { sourceCount: 1 }, 'date-of-birth': { - notary: 'childCivil', + authority: 'childCivil', service: 'childBenefit', claimId: 'child-age-under-5', claimVersion: '2026-07', @@ -286,7 +284,7 @@ export const SCENARIOS: Record = { sourceCount: 1 }, 'household-composition': { - notary: 'socialRegistry', + authority: 'socialRegistry', service: 'childBenefit', claimId: 'household-below-poverty-threshold', claimVersion: '2026-07', @@ -300,7 +298,7 @@ export const SCENARIOS: Record = { asOf: '2026-05-09', state: 'verified', display: 'Household below threshold: yes', - headline: 'Confirmed by the Social Registry Office through its source-owned Notary', + headline: 'Confirmed by Social Registry Office Evidence', answered: 'Social Registry Office answered: household-below-poverty-threshold = true', notDisclosed: 'Not disclosed: poverty score or household roster', status: 'ok', @@ -310,7 +308,7 @@ export const SCENARIOS: Record = { sourceCount: 1 }, 'not-already-enrolled': { - notary: 'programme', + authority: 'programme', service: 'childBenefit', claimId: 'not-already-enrolled', claimVersion: '2026-07', @@ -339,7 +337,7 @@ export const SCENARIOS: Record = { // pension-survivor (multi-authority decision) // --------------------------------------------------------------------------- 'person-is-alive': { - notary: 'civil', + authority: 'civil', service: 'pension', claimId: 'person-is-deceased', claimVersion: '2026-07', @@ -363,7 +361,7 @@ export const SCENARIOS: Record = { sourceCount: 1 }, 'disability-determination': { - notary: 'social', + authority: 'social', service: 'pension', claimId: 'pension-payment-should-stop', claimVersion: '2026-07', @@ -388,7 +386,7 @@ export const SCENARIOS: Record = { sourceCount: 2 }, 'functioning-assessment': { - notary: 'social', + authority: 'social', service: 'pension', claimId: 'survivor-is-eligible', claimVersion: '2026-07', @@ -412,7 +410,7 @@ export const SCENARIOS: Record = { sourceCount: 1 }, 'pension-payment-active': { - notary: 'social', + authority: 'social', service: 'pension', claimId: 'pension-payment-active', claimVersion: '2026-07', @@ -436,7 +434,7 @@ export const SCENARIOS: Record = { sourceCount: 1 }, 'combined-support-eligibility': { - notary: 'social', + authority: 'social', service: 'pension', claimId: 'survivor-benefit-eligible', claimVersion: '2026-07', @@ -470,7 +468,7 @@ export const SCENARIOS: Record = { // citizen self-service gallery fixture // --------------------------------------------------------------------------- 'citizen-record-status': { - notary: 'certs', + authority: 'certs', service: 'citizen', claimId: 'citizen-self-service-ready', claimVersion: '2026-07', @@ -497,10 +495,10 @@ export const SCENARIOS: Record = { // --------------------------------------------------------------------------- // Denial beat (cross-person, stranger Karim 2300073046): a real denied - // evaluation, 403 subject_mismatch, NO source read. + // refusal, 403 not_authorized, NO source read. // --------------------------------------------------------------------------- denial: { - notary: 'civil', + authority: 'civil', service: 'pension', claimId: 'person-is-deceased', claimVersion: '2026-07', @@ -514,13 +512,13 @@ export const SCENARIOS: Record = { asOf: '2026-06-21', state: 'error', display: 'Denied: you cannot query this person', - reasonCode: 'subject_mismatch', + reasonCode: 'not_authorized', headline: 'Denied by Civil Registry before any record was read', - answered: 'Civil Registration Authority answered: 403 subject_mismatch, no data returned', + answered: 'Portal authorization gate answered: 403 not_authorized, no data returned', notDisclosed: 'Not disclosed: nothing, the boundary held and no source was read', status: 'denied', httpStatus: 403, - denial: { code: 'subject_mismatch', message: 'requester is not authorized for this target' }, + denial: { code: 'not_authorized', message: 'requester is not authorized for this target' }, latencyMs: 600, staggerOrder: 0, sourceCount: 0 @@ -532,7 +530,7 @@ export const SCENARIOS: Record = { // SLOW: a still-in-flight live call that crosses the ~6-8s SLOW threshold but // eventually resolves verified. The provider surfaces SLOW before VERIFIED. slow: { - notary: 'agri', + authority: 'agri', service: 'nagdi', claimId: 'farmer-registered', claimVersion: '2026-07', @@ -558,7 +556,7 @@ export const SCENARIOS: Record = { // ERROR: a hard failure (503). Scoped to the field, framed as minimization. No // source read, no value. error: { - notary: 'socialRegistry', + authority: 'socialRegistry', service: 'childBenefit', claimId: 'household-below-poverty-threshold', claimVersion: '2026-07', @@ -572,7 +570,7 @@ export const SCENARIOS: Record = { asOf: '2026-06-21', state: 'error', display: 'Could not reach Social Protection; other evidence is unaffected', - reasonCode: 'upstream_unavailable', + reasonCode: 'service_unavailable', headline: 'Could not reach Social Protection, the other authorities are unaffected', answered: 'Social Registry Office answered: 503, no data returned', notDisclosed: 'Not disclosed: nothing, there is no central lake so this failure is isolated', @@ -584,7 +582,7 @@ export const SCENARIOS: Record = { }, // STALE: fetched but older than the freshness rule (BLUE + AMBER flag). stale: { - notary: 'social', + authority: 'social', service: 'pension', claimId: 'survivor-is-eligible', claimVersion: '2026-07', @@ -609,7 +607,7 @@ export const SCENARIOS: Record = { }, // AMBIGUOUS: more than one record matched; never collapses to false. ambiguous: { - notary: 'civil', + authority: 'civil', service: 'pension', claimId: 'person-is-deceased', claimVersion: '2026-07', diff --git a/portal/src/lib/providers/mock/wire.ts b/portal/src/lib/providers/mock/wire.ts index 55f5c1b..7a64394 100644 --- a/portal/src/lib/providers/mock/wire.ts +++ b/portal/src/lib/providers/mock/wire.ts @@ -1,199 +1,72 @@ -// Wire-shape builders. Registry Notary services use EvaluateRequest and -// EvaluationResponse. Child benefit uses an ordinary application-owned JSON -// request and source-attributed evidence response. -// -// Volatile fields (evaluation_id, issued_at, expires_at) are stamped here from -// the passed clock/ids so they are present but value-variable. The live provider -// will emit the same shape; only the values differ. +// Authority Evidence wire-shape builders used by the portal mock. The request +// matches EvidenceRequest v1 and the response is a signed flattened JWS. The +// portal treats the encoded assertion as opaque transport data and projects the +// reviewed concept into its stable ClaimResult UI model. +import type { AuthorityId } from '$lib/types'; import type { ScenarioResult } from './scenarios'; -import { - AUTHORITY_LABEL, - NOTARY_SERVICE_ID -} from './scenarios'; +import { AUTHORITY_LABEL, EVIDENCE_SERVICE_ID } from './scenarios'; +import { SOLMARA_AUTHORITIES } from '$lib/fields/authorities'; -// A monotonic, demo-stable ULID-shaped id. Not a real ULID; deterministic so the -// mock is reproducible while still looking like the wire format. -export function makeEvaluationId(seed: number): string { +const REQUEST_NONCE_BASE = 'U29sbWFyYVJlZ2lzdHJ5RXZpZGVuY2VEZW1vMDAwMDA'; +const SIGNATURE = 'A'.repeat(86); + +export const EVIDENCE_RESPONSE_FORMAT = 'application/jose+json'; + +export function makeOperationId(seed: number): string { const base = '01HX7Y5F2WAJ7ZP0Q4M5K9E8N'; - const tail = (seed % 36).toString(36).toUpperCase(); - return `${base}${tail}`; + return `${base.slice(0, 25)}${(seed % 32).toString(32).toUpperCase()}`; } -// The full target/subject value the BFF holds server-side. This is the RAW shape -// the BFF builds and would send upstream; it is redacted before it ever reaches -// the proof feed. We expose it so the redaction test can prove the raw subject -// never appears in the streamed output. -export type RawEvaluateRequest = { - claims: string[]; - purpose: string; - disclosure: string; - format: string; - relationship: { type: string }; - target: { type: string; identifiers: { scheme: string; value: string }[] }; - on_behalf_of?: { actor: { type: string; id_hash: string }; delegation_ref: string }; +export type EvidenceSelector = { + profile: string; + values: Record; }; -export const CLAIM_RESULT_FORMAT = 'application/vnd.registry-notary.claim-result+json'; -export const CHILD_BENEFIT_FORMAT = 'application/json'; - -export type RawChildBenefitRequest = { - target: { type: 'Person'; identifiers: { scheme: 'solmara_uin'; value: string }[] }; - claims: string[]; - disclosure: 'predicate'; - format: typeof CHILD_BENEFIT_FORMAT; - variables?: { as_of_date: string }; - purpose?: never; - relationship?: never; - on_behalf_of?: never; +export type RawEvidenceRequest = { + requestNonce: string; + requirement: string; + purpose: string; + subjects: { role: string; selector: EvidenceSelector }[]; }; export type RawApplicationRequest = { purpose: string; - disclosure: 'decision'; - composition: 'application'; + composition: 'portal-application'; requests: { authority: string; service_id: string; - body: RawEvaluateRequest; + body: RawEvidenceRequest; }[]; }; -export type RawProviderRequest = - | RawEvaluateRequest - | RawChildBenefitRequest - | RawApplicationRequest; - -export function buildChildBenefitRequest( - scenario: ScenarioResult, - subject: string -): RawChildBenefitRequest { - return { - target: { - type: 'Person', - identifiers: [{ scheme: 'solmara_uin', value: subject }] - }, - claims: [scenario.claimId], - disclosure: 'predicate', - format: CHILD_BENEFIT_FORMAT, - ...(scenario.claimId === 'child-age-under-5' - ? { variables: { as_of_date: scenario.asOf } } - : {}) - }; -} - -export function buildEvaluationRequest( - claimId: string, - subject: string, - purpose: string, - scheme = 'solmara_uin' -): RawEvaluateRequest { - return { - claims: [claimId], - purpose, - disclosure: 'predicate', - format: CLAIM_RESULT_FORMAT, - relationship: { type: 'self' }, - target: { - type: 'Person', - identifiers: [{ scheme, value: subject }] - } - }; -} - -// Build the RAW EvaluateRequest the BFF would send. `subject` is the national id -// the BFF resolved server-side (never client-supplied). For delegated reads the -// subject is the dependent and actorIdHash binds the requesting guardian. -export function buildRawRequest( - scenario: ScenarioResult, - subject: string, - opts?: { actorIdHash?: string; delegationRef?: string } -): RawEvaluateRequest { - const req = buildEvaluationRequest( - scenario.claimId, - subject, - scenario.purpose, - subjectScheme(scenario) - ); - req.disclosure = scenario.disclosure; - req.relationship = { type: scenario.delegated ? 'guardian' : 'self' }; - if (scenario.delegated) { - req.on_behalf_of = { - actor: { - type: 'Person', - // Already a keyed hash placeholder; the redactor still re-hashes any raw - // id, but the wire shape carries id_hash, never a raw principal. - id_hash: opts?.actorIdHash ?? 'hmac-sha256:0000' - }, - delegation_ref: opts?.delegationRef ?? 'rnref:v1:caregiver-link' - }; - } - return req; -} - -// One ClaimResultView, structurally identical to the OpenAPI required key set and -// ordering. Volatile fields stamped from `clock`/`evaluationId`. -export type ClaimResultView = { - claim_id: string; - claim_version: string; - disclosure: string; - evaluation_id: string; - expires_at: string | null; - format: string; - issued_at: string; - provenance: { - derived_from: object[]; - generated_by: { - claim_id: string; - claim_version: string; - evaluation_id: string; - policy_hash?: string; - policy_id?: string; - policy_version?: string; - service_id: string; - type: 'claim_evaluation'; - }; - schema_version: 'registry-notary-claim-provenance/v2'; - used: { - relay_consultation_count: number; - }; - }; - satisfied: boolean | null; - subject_type: string; - target_ref: { - handle: string; - identifier_schemes?: string[]; - profile?: string; - type?: string; - }; - value: unknown; +export type RawProviderRequest = RawEvidenceRequest | RawApplicationRequest; + +export type EvidenceAssertion = { + schema: 'registry.assertion-evidence/v1'; + assuranceProfile: 'evidence-grade'; + subjectBinding: 'audience-scoped'; + requestNonce: string; + id: string; + type: 'Evidence'; + supportsRequirement: string; + isConformantTo: string; + issuedBy: string; + providedBy: string; + issuedAt: string; + observedAt: string; + validUntil: string; + purpose: string; + audience: string; + configurationRevision: string; + subjects: { role: string; binding: string }[]; + supportedValues: { providesValueFor: string; value: unknown }[]; }; -export type RawEvaluationResponse = { results: ClaimResultView[] }; - -export type ChildBenefitResult = { - claim_id: string; - claim_version: string; - format: string; - issued_at: string; - expires_at: string | null; - notary_service_id: string; - authority: string; - satisfied: boolean; - disclosure: 'predicate'; -}; - -export type RawChildBenefitResponse = { - schema_version: 'solmara-child-benefit-evidence/v1'; - evidence_set_id: string; - orchestration: { - service_id: 'child-benefit-federator'; - decision: 'not_composed'; - }; - purpose: string; - target: { type?: string; identifier_schemes: string[] }; - results: ChildBenefitResult[]; - source_trace: object[]; +export type RawEvidenceResponse = { + protected: string; + payload: string; + signature: string; }; export type RawApplicationResponse = { @@ -202,164 +75,160 @@ export type RawApplicationResponse = { service_id: 'citizen-portal'; decision: 'application_composed'; }; - results: ClaimResultView[]; + signed_evidence: { + authority: string; + service_id: string; + assertion: RawEvidenceResponse; + }[]; source_trace: { authority: string; service_id: string; status: number; - claims: string[]; + requirements: string[]; }[]; - derived_decisions?: Record; + derived_decisions: Record; }; -export type RawProviderResponse = - | RawEvaluationResponse - | RawChildBenefitResponse - | RawApplicationResponse; +export type RawProviderResponse = RawEvidenceResponse | RawApplicationResponse; -export type ClaimResultFixture = { - claimId: string; - claimVersion: string; - serviceId: string; - subjectType: string; - satisfied: boolean | null; - value: unknown; - disclosure: string; - sourceCount: number; - identifierScheme: string; - freshnessDays: number; -}; +export function requirementId(claimId: string): string { + return `urn:solmara:requirement:${claimId}:v1`; +} -export function buildChildBenefitResponse( - scenario: ScenarioResult, - evaluationId: string, - issuedAt: Date, - target: RawChildBenefitRequest['target'] -): RawChildBenefitResponse { - const serviceId = NOTARY_SERVICE_ID[scenario.notary]; +export function conceptId(claimId: string): string { + return `urn:solmara:concept:${claimId}`; +} + +export function buildEvidenceRequest( + claimId: string, + subject: string, + purpose: string, + scheme = 'solmara_uin', + seed = 0 +): RawEvidenceRequest { + void seed; return { - schema_version: 'solmara-child-benefit-evidence/v1', - evidence_set_id: `cbe_${evaluationId}`, - orchestration: { - service_id: 'child-benefit-federator', - decision: 'not_composed' - }, - purpose: scenario.purpose, - target: { - type: target.type, - identifier_schemes: target.identifiers.map((identifier) => identifier.scheme) - }, - results: [ - { - claim_id: scenario.claimId, - claim_version: scenario.claimVersion, - format: CLAIM_RESULT_FORMAT, - issued_at: issuedAt.toISOString(), - expires_at: new Date( - issuedAt.getTime() + scenario.freshnessDays * 24 * 60 * 60 * 1000 - ).toISOString(), - notary_service_id: serviceId, - authority: AUTHORITY_LABEL[scenario.notary], - satisfied: scenario.satisfied === true, - disclosure: 'predicate' - } - ], - source_trace: [ + requestNonce: requestNonce(0), + requirement: requirementId(claimId), + purpose, + subjects: [ { - authority: AUTHORITY_LABEL[scenario.notary], - service_id: serviceId, - claims: [scenario.claimId] + role: 'subject', + selector: { + profile: scheme === 'farmer_id' ? 'solmara-farmer-v1' : 'solmara-person-v1', + values: { [scheme]: subject } + } } ] }; } -// Build the RAW 200 EvaluationResponse. issuedAt is an ISO string; expiresAt is -// issuedAt + freshnessDays (can be in the past for the stale scenario). -export function buildRawResponse( +export function buildRawRequest( scenario: ScenarioResult, - evaluationId: string, - issuedAt: Date -): RawEvaluationResponse { + subject: string, + seed = 0 +): RawEvidenceRequest { + return buildEvidenceRequest( + scenario.claimId, + subject, + scenario.purpose, + subjectScheme(scenario), + seed + ); +} + +export function buildEvidenceAssertion( + scenario: ScenarioResult, + operationId: string, + issuedAt: Date, + claimId = scenario.claimId, + serviceId = EVIDENCE_SERVICE_ID[scenario.authority], + value: unknown = scenario.value, + issuerId: AuthorityId = scenario.authority +): EvidenceAssertion { + const requirement = requirementId(claimId); return { - results: [ - buildClaimResultView( - { - claimId: scenario.claimId, - claimVersion: scenario.claimVersion, - serviceId: NOTARY_SERVICE_ID[scenario.notary], - subjectType: scenario.subjectType, - satisfied: scenario.satisfied, - value: scenario.value, - disclosure: scenario.disclosure, - sourceCount: scenario.sourceCount, - identifierScheme: subjectScheme(scenario), - freshnessDays: scenario.freshnessDays - }, - evaluationId, - issuedAt - ) - ] + schema: 'registry.assertion-evidence/v1', + assuranceProfile: 'evidence-grade', + subjectBinding: 'audience-scoped', + requestNonce: requestNonce(0), + id: `urn:solmara:evidence:${operationId}`, + type: 'Evidence', + supportsRequirement: requirement, + isConformantTo: `urn:solmara:evidence-type:${claimId}:v1`, + issuedBy: SOLMARA_AUTHORITIES[issuerId].issuer, + providedBy: `${SOLMARA_AUTHORITIES[issuerId].origin}/`, + issuedAt: issuedAt.toISOString(), + observedAt: `${scenario.asOf}T00:00:00.000Z`, + validUntil: new Date( + issuedAt.getTime() + scenario.freshnessDays * 24 * 60 * 60 * 1000 + ).toISOString(), + purpose: scenario.purpose, + audience: 'urn:solmara:portal:citizen-services', + configurationRevision: `sha256:${'a'.repeat(64)}`, + subjects: [{ role: 'subject', binding: `urn:evidence:subject:v1_${'B'.repeat(43)}` }], + supportedValues: [{ providesValueFor: conceptId(claimId), value: publicValue(value, scenario) }] }; } -export function buildClaimResultView( - fixture: ClaimResultFixture, - evaluationId: string, - issuedAt: Date -): ClaimResultView { - const issued = issuedAt.toISOString(); - const expires = new Date( - issuedAt.getTime() + fixture.freshnessDays * 24 * 60 * 60 * 1000 - ).toISOString(); +export function signEvidence(assertion: EvidenceAssertion, serviceId: string): RawEvidenceResponse { return { - claim_id: fixture.claimId, - claim_version: fixture.claimVersion, - disclosure: fixture.disclosure, - evaluation_id: evaluationId, - expires_at: expires, - format: CLAIM_RESULT_FORMAT, - issued_at: issued, - provenance: { - derived_from: [], - generated_by: { - claim_id: fixture.claimId, - claim_version: fixture.claimVersion, - evaluation_id: evaluationId, - service_id: fixture.serviceId, - type: 'claim_evaluation' - }, - schema_version: 'registry-notary-claim-provenance/v2', - used: { - relay_consultation_count: fixture.sourceCount - } - }, - satisfied: fixture.satisfied, - subject_type: fixture.subjectType, - target_ref: { - handle: 'rnref:v1:target', - identifier_schemes: [fixture.identifierScheme], - profile: 'resident', - type: 'Person' - }, - value: fixture.value + protected: encode({ + alg: 'ES256', + kid: `${serviceId}-2026-01`, + typ: 'evidence+jws', + cty: 'application/evidence+json' + }), + payload: encode(assertion), + signature: SIGNATURE }; } -// The HTTP method + URL the proof inspector shows. URL carries the notary host -// but never a raw subject (subjects go in the body target, which is redacted). -export function notaryUrl(scenario: ScenarioResult): string { - if (scenario.service === 'childBenefit') { - return 'https://child-benefit-federator.solmara.registrystack.org/v1/evaluations'; - } - return `https://${NOTARY_SERVICE_ID[scenario.notary]}.solmara.registrystack.org/v1/evaluations`; +export function buildRawResponse( + scenario: ScenarioResult, + operationId: string, + issuedAt: Date +): RawEvidenceResponse { + const serviceId = EVIDENCE_SERVICE_ID[scenario.authority]; + return signEvidence( + buildEvidenceAssertion(scenario, operationId, issuedAt), + serviceId + ); +} + +export function evidenceUrl(scenario: ScenarioResult): string { + return `${SOLMARA_AUTHORITIES[scenario.authority].origin}/v1/evidence`; } -// The authority label the result attributes to (depth-1 answered line). export function authorityLabel(scenario: ScenarioResult): string { - return AUTHORITY_LABEL[scenario.notary]; + return AUTHORITY_LABEL[scenario.authority]; +} + +export function decodeEvidencePayload(response: RawEvidenceResponse): EvidenceAssertion { + return JSON.parse(Buffer.from(response.payload, 'base64url').toString('utf8')) as EvidenceAssertion; +} + +function requestNonce(seed: number): string { + const suffix = (seed % 64).toString(36).toUpperCase().padStart(2, '0'); + return `${REQUEST_NONCE_BASE.slice(0, 41)}${suffix}`; } function subjectScheme(scenario: ScenarioResult): string { - return scenario.notary === 'agri' ? 'farmer_id' : 'solmara_uin'; + return scenario.authority === 'agri' ? 'farmer_id' : 'solmara_uin'; +} + +function publicValue(value: unknown, scenario: ScenarioResult): unknown { + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { + return { + form: 'reviewed-structured-value', + schema: `urn:solmara:value-schema:${scenario.claimId}:v1`, + fields: value + }; + } + if (value === null) return scenario.state === 'ambiguous' ? 'multiple-matches' : 'not-available'; + return value; +} + +function encode(value: unknown): string { + return Buffer.from(JSON.stringify(value)).toString('base64url'); } diff --git a/portal/src/lib/rail/MinistryRail.svelte b/portal/src/lib/rail/MinistryRail.svelte index 5c965f0..45c8019 100644 --- a/portal/src/lib/rail/MinistryRail.svelte +++ b/portal/src/lib/rail/MinistryRail.svelte @@ -7,7 +7,7 @@ // // prefers-reduced-motion: all animation is suppressed; a numbered-sequence // list renders instead so the gating and fan-out story remains legible. - import type { RailEvent, NotaryId } from '$lib/types'; + import type { AuthorityId, RailEvent } from '$lib/types'; import { AUTHORITY_NAMES } from '$lib/fields/authorities'; type Props = { @@ -25,7 +25,7 @@ const ORBIT_R = 90; // Ministry nodes in clockwise order around the citizen seat. - type NodeId = NotaryId; + type NodeId = AuthorityId; type NodeDef = { id: NodeId; label: string; @@ -47,7 +47,7 @@ { id: 'certs', label: 'Certs', angle: 225, color: 'var(--color-ministry-civil)', glyph: 'Ce' } ]; - // NodeId -> NodeDef lookup. We use string keys so NotaryId events from the + // NodeId -> NodeDef lookup. We use string keys so authority events from the // wire can be looked up directly. const nodeMap = new Map(NODES.map((n) => [n.id, n])); @@ -87,7 +87,7 @@ // pairs: a request event whose id is not superseded by a sealed/denied event. type Packet = { id: string; - authority: NotaryId; + authority: AuthorityId; channel: string; // 'verify' | 'fetch' | 'denied' motion: 'pulse-target' | 'travel-stamp' | 'bounce'; }; @@ -112,7 +112,7 @@ ev.channel === 'verify' ? 'pulse-target' : ev.channel === 'fetch' ? 'travel-stamp' : 'bounce'; - packets.push({ id: ev.id, authority: ev.authority as NotaryId, channel: ev.channel, motion } satisfies Packet); + packets.push({ id: ev.id, authority: ev.authority as AuthorityId, channel: ev.channel, motion } satisfies Packet); } return packets; })() @@ -130,7 +130,7 @@ const sequenceSteps = $derived( events.map((ev, i): SequenceStep => { - const authLabel = AUTHORITY_NAMES[ev.authority as NotaryId] ?? ev.authority; + const authLabel = AUTHORITY_NAMES[ev.authority as AuthorityId] ?? ev.authority; const channelLabel = ev.channel === 'verify' ? 'verify' : ev.channel === 'fetch' ? 'fetch' : @@ -206,7 +206,7 @@ data-motion={packet.motion} data-channel={packet.channel} data-authority={packet.authority} - aria-label={`${packet.channel} packet to ${AUTHORITY_NAMES[packet.authority as NotaryId] ?? packet.authority}`} + aria-label={`${packet.channel} packet to ${AUTHORITY_NAMES[packet.authority as AuthorityId] ?? packet.authority}`} > {#if packet.motion === 'travel-stamp' || packet.motion === 'bounce'} diff --git a/portal/src/lib/server/bff.ts b/portal/src/lib/server/bff.ts index c39a31c..2172884 100644 --- a/portal/src/lib/server/bff.ts +++ b/portal/src/lib/server/bff.ts @@ -5,20 +5,19 @@ import type { MockEvaluation } from '$lib/providers/mock'; import type { ProofTrace, RailChannel, RailEvent } from '$lib/types'; import { proofFeed, railFeed } from '$lib/providers/feeds.svelte'; -import { redactRequest, redactResponse, scrubString } from './redact'; +import { scrubString } from './redact'; let traceSeq = 0; // Build a redacted ProofTrace from a full MockEvaluation. The depth-1 human copy // is allowlist-safe by construction (it never embeds a raw identifier; the mock // authors it). depth-2 bodies are run through the redactor. depth-3 crypto carries -// no raw identifier (dids, audit ids, algorithm). +// no raw identifier, internal event id, or cryptographic bytes. export function buildRedactedTrace( ev: MockEvaluation, opts?: { fieldId?: string } ): ProofTrace { const seq = ++traceSeq; - const redactedReq = redactRequest(ev.raw.request); const trace: ProofTrace = { id: ev.result.traceId, seq, @@ -30,36 +29,35 @@ export function buildRedactedTrace( notDisclosed: scrubString(ev.proof.notDisclosed), status: ev.proof.status, ts: new Date().toISOString(), - // depth 2: redacted wire bodies (allowlist only). - request: redactedReq, - response: redactResponse({ - status: ev.raw.response.status, - body: ev.raw.response.body as Record - }), - // depth 3: crypto. Present for resolved AND denied traces (the denial is still - // a signed, audited evaluation result). + purpose: ev.proof.purpose, + resultState: ev.result.state, + presentations: ev.proof.presentations, + responseStatus: ev.raw.response.status, proof: ev.proof.crypto }; return trace; } -// Map a proof status to a rail channel + phase. -function railFromStatus(ev: MockEvaluation): { channel: RailChannel; phase: RailEvent['phase'] } { - switch (ev.proof.status) { - case 'denied': - return { channel: 'denied', phase: 'denied' }; - case 'error': - return { channel: 'denied', phase: 'denied' }; +// Rail behavior is derived from the stable result state, never from a request +// body or upstream transport detail. +export function railFromStatus(ev: MockEvaluation): { channel: RailChannel; phase: RailEvent['phase'] } { + switch (ev.result.state) { + case 'fetched': + case 'stale': + return { channel: 'fetch', phase: 'sealed' }; + case 'verified': case 'false': - // a signed "no" is still a sealed verify, not a denial. + case 'recovered': return { channel: 'verify', phase: 'sealed' }; - case 'ok': { - const disclosure = (ev.raw.request.body.disclosure ?? '') as string; - const channel: RailChannel = disclosure === 'predicate' || disclosure === 'decision' ? 'verify' : 'fetch'; - return { channel, phase: 'sealed' }; - } - default: + case 'error': + case 'ambiguous': + return { channel: 'denied', phase: 'denied' }; + case 'in_flight': + case 'slow': return { channel: 'verify', phase: 'request' }; + case 'idle': + case 'prefilled': + return { channel: 'verify', phase: 'sealed' }; } } diff --git a/portal/src/lib/server/redact.test.ts b/portal/src/lib/server/redact.test.ts index 5f27e69..6a1a8fa 100644 --- a/portal/src/lib/server/redact.test.ts +++ b/portal/src/lib/server/redact.test.ts @@ -7,222 +7,54 @@ import { redactResponse, scrubString } from './redact'; -import { serializeTraceEvent } from './bff'; -import type { ProofTrace } from '$lib/types'; -// The load-bearing redaction test (spec 5.2 / 10): build a payload containing a -// fixture UIN, a person id, and fake API credentials; run it through the -// redactor AND the SSE serialization; assert NONE of the UIN, /CP-\d+/, or the -// credential strings survive, and that the allowlisted keys DO. - -const FAKE_BEARER = 'Bearer eyJhbGciOiJFZERTQSJ9.FAKE_TOKEN_PAYLOAD.FAKE_SIGNATURE_abc123'; -const FAKE_API_KEY = 'rn_api_FAKE_TOKEN_PAYLOAD_abc123456789'; - -// A raw request body shaped like the BFF's un-redacted EvaluateRequest, carrying -// the subject the BFF holds (2300010248) and a delegated dependent (2300036523). -const rawRequest = { - method: 'POST', - url: 'https://civil-notary.gov.solmara.example/v1/evaluations', - body: { - claims: [{ id: 'person-is-deceased', version: '2026-07' }], - purpose: 'https://id.registrystack.org/solmara/purpose/pension-payment-review', - disclosure: 'predicate', - format: 'application/vnd.registry-notary.claim-result+json', - relationship: { type: 'self' }, - target: { - type: 'Person', - identifiers: [{ scheme: 'solmara_uin', value: '2300010248' }] - }, - on_behalf_of: { - actor: { type: 'Person', id_hash: 'hmac-sha256:deadbeef' }, - delegation_ref: 'rnref:v1:REL-1001-MOTHER' - } - } -}; - -// A raw response that echoes a person id (CP-2001) in a target_ref handle, plus a -// freshness timestamp, plus the satisfied predicate. -const rawResponse = { - status: 200, - body: { - results: [ - { - claim_id: 'person-is-deceased', - claim_version: '2026-07', - disclosure: 'predicate', - evaluation_id: '01HX7Y5F2WAJ7ZP0Q4M5K9E8NC', - expires_at: '2026-05-25T12:00:00Z', - issued_at: '2026-05-24T12:00:00Z', - satisfied: true, - subject_type: 'person', - target_ref: { - handle: 'rnref:v1:CP-2001', - identifier_schemes: ['solmara_uin'], - profile: 'resident', - type: 'Person' - }, - value: true - } - ] - } -}; - -describe('scrubString', () => { - it('blanks UIN, CP, bearer, and x-api-key material', () => { - const dirty = `subject 2300010248 (CP-2001) with Authorization: ${FAKE_BEARER} and x-api-key: ${FAKE_API_KEY}`; +describe('portal proof redaction boundary', () => { + it('scrubs identifiers, credentials, private keys, and compact JWS values', () => { + const dirty = [ + '2300010248', + 'CP-2001', + 'FR-1001', + 'Bearer secret-token-123', + 'x-api-key: secret-key-123', + '-----BEGIN PRIVATE KEY-----\nsecret\n-----END PRIVATE KEY-----', + `${'a'.repeat(20)}.${'b'.repeat(20)}.${'c'.repeat(20)}` + ].join(' '); const clean = scrubString(dirty); - expect(clean).not.toContain('2300010248'); - expect(clean).not.toMatch(/CP-\d+/); - expect(clean).not.toContain('FAKE_TOKEN_PAYLOAD'); - expect(clean).toContain('Bearer •••••••• (redacted)'); - expect(clean).toContain('x-api-key: •••••••• (redacted)'); - }); -}); -describe('redactBody / request / response', () => { - it('drops identifier values but keeps the structural key set', () => { - const red = redactRequest(rawRequest); - const serialized = JSON.stringify(red); - // raw identifiers gone - expect(serialized).not.toContain('2300010248'); - // structural keys preserved (allowlisted wire shape) - expect(red.body).toHaveProperty('claims'); - expect(red.body).toHaveProperty('purpose', 'https://id.registrystack.org/solmara/purpose/pension-payment-review'); - expect(red.body).toHaveProperty('disclosure', 'predicate'); - expect(red.body).toHaveProperty('relationship'); - // target envelope kept structurally but identifiers stripped - expect(red.body).toHaveProperty('target'); - const target = red.body.target as { type?: string; identifiers?: unknown[] }; - expect(target.identifiers).toEqual([]); + expect(containsRawIdentifier(clean)).toBe(false); + expect(clean).not.toMatch(/2300010248|CP-2001|FR-1001|secret-token|secret-key/); + expect(clean).toContain('Bearer [redacted]'); + expect(clean).toContain('[private key redacted]'); + expect(clean).toContain('[JWS redacted]'); }); - it('redacts a response that echoes a person id in a handle', () => { - const red = redactResponse(rawResponse); - const serialized = JSON.stringify(red); - expect(serialized).not.toMatch(/CP-\d+/); - // allowlisted result keys survive - const result = (red.body.results as Record[])[0]; - expect(result).toHaveProperty('claim_id', 'person-is-deceased'); - expect(result).toHaveProperty('satisfied', true); - expect(result).toHaveProperty('issued_at', '2026-05-24T12:00:00Z'); - }); - - it('keeps child application source attribution without a target identifier', () => { - const red = redactResponse({ - status: 200, - body: { - schema_version: 'solmara-child-benefit-evidence/v1', - evidence_set_id: 'cbe_01TEST', - orchestration: { - service_id: 'child-benefit-federator', - decision: 'not_composed' - }, - target: { - type: 'Person', - identifier_schemes: ['solmara_uin'] - }, - results: [ - { - claim_id: 'population-record-active', - notary_service_id: 'nia-notary', - authority: 'National Identity Agency', - satisfied: true - } - ], - source_trace: [{ service_id: 'nia-notary', claims: ['population-record-active'] }] - } - }); - - expect(JSON.stringify(red)).not.toContain('2300010248'); - expect(red.body).toHaveProperty('evidence_set_id', 'cbe_01TEST'); - expect(red.body).toHaveProperty('orchestration.decision', 'not_composed'); - expect(red.body).toHaveProperty( - 'results.0.notary_service_id', - 'nia-notary' - ); - expect(red.body).toHaveProperty('source_trace.0.service_id', 'nia-notary'); - }); - - it('keeps the application-owned survivor decision value', () => { - const red = redactResponse({ - status: 200, - body: { - schema_version: 'solmara-portal-evidence/v1', - derived_decisions: { 'survivor-benefit-eligible': true } - } + it('never projects request or response bodies', () => { + const request = redactRequest({ + method: 'POST', + url: 'https://cra-evidence.example/v1/evidence?uin=2300010248', + body: { subjects: [{ selector: { values: { uin: '2300010248', farmer: 'FR-1001' } } }] } }); - - expect(red.body).toHaveProperty( - 'derived_decisions.survivor-benefit-eligible', - true - ); - }); -}); - -describe('SSE serialization is identifier-free end to end', () => { - it('produces an event frame with no UIN / CP / bearer material', () => { - const trace: ProofTrace = { - id: 'event 1', - seq: 1, - fieldId: 'person-is-deceased', - authority: 'civil', - headline: 'Confirmed by Civil Registry', - answered: 'Civil Registry answered: person-is-deceased = true', - notDisclosed: 'Not disclosed: any other civil record detail', - status: 'ok', - ts: '2026-06-21T10:00:00.000Z', - request: redactRequest(rawRequest), - response: redactResponse(rawResponse) - }; - const frame = serializeTraceEvent(trace); - - // the streamed bytes carry NONE of the secrets / identifiers - expect(frame).not.toContain('2300010248'); - expect(frame).not.toMatch(/CP-\d+/); - expect(frame).not.toContain('FAKE_TOKEN_PAYLOAD'); - expect(frame).not.toContain(FAKE_API_KEY); - expect(containsRawIdentifier(frame)).toBe(false); - - // but the allowlisted, structural content DID survive - expect(frame).toContain('person-is-deceased'); - expect(frame).toContain('https://id.registrystack.org/solmara/purpose/pension-payment-review'); - expect(frame).toContain('Not disclosed:'); - expect(frame.startsWith('event: trace\ndata: ')).toBe(true); - }); -}); - -describe('pickAllowedMeta', () => { - it('keeps only allowlisted keys and scrubs their values', () => { - const picked = pickAllowedMeta({ - claim: 'person-is-deceased', - purpose: 'https://id.registrystack.org/solmara/purpose/pension-payment-review', - disclosure: 'predicate', - authority: 'Civil Registry', - result: 'true', - freshness: '2026-05-24', - // these must be dropped - bearer: FAKE_BEARER, - api_key: FAKE_API_KEY, - target: '2300010248', - subject: 'CP-2001' - }); - expect(Object.keys(picked).sort()).toEqual( - ['authority', 'claim', 'disclosure', 'freshness', 'purpose', 'result'].sort() - ); - expect(JSON.stringify(picked)).not.toContain('2300010248'); - expect(JSON.stringify(picked)).not.toMatch(/CP-\d+/); - expect(JSON.stringify(picked)).not.toContain('FAKE_TOKEN_PAYLOAD'); - }); -}); - -describe('redactBody catches a leaked identifier inside an allowlisted value', () => { - it('scrubs a UIN embedded in an otherwise-allowed string value', () => { - const leaked = redactBody({ - purpose: 'lookup for 2300010248', - value: 'belongs to CP-2001' + const response = redactResponse({ status: 200, body: { protected: 'raw', payload: 'raw', signature: 'raw' } }); + + expect(request).toEqual({ method: 'POST', url: 'https://cra-evidence.example/v1/evidence?uin=[redacted]' }); + expect(response).toEqual({ status: 200 }); + expect(redactBody({ selector: 'FR-1001' })).toEqual({}); + expect(JSON.stringify({ request, response })).not.toMatch(/subjects|selector|protected|payload|signature|FR-1001/); + }); + + it('allowlists only safe presentation metadata and scrubs string values', () => { + expect(pickAllowedMeta({ + authority: 'Civil Registration Authority', + issuer: 'did:web:id.registrystack.org:solmara:authority:cra', + serviceId: 'cra-evidence', + source: 'Relay lookup', + selector: 'FR-1001', + token: 'secret' + })).toEqual({ + authority: 'Civil Registration Authority', + issuer: 'did:web:id.registrystack.org:solmara:authority:cra', + serviceId: 'cra-evidence', + source: 'Relay lookup' }); - const serialized = JSON.stringify(leaked); - expect(serialized).not.toContain('2300010248'); - expect(serialized).not.toMatch(/CP-\d+/); }); }); diff --git a/portal/src/lib/server/redact.ts b/portal/src/lib/server/redact.ts index 01dda87..37226ca 100644 --- a/portal/src/lib/server/redact.ts +++ b/portal/src/lib/server/redact.ts @@ -1,237 +1,83 @@ -// Allowlist redaction at the BFF boundary (spec 5.2 / 10). -// -// This is denylist-free by construction: nothing passes UNLESS its key is on an -// explicit allowlist. Everything else (bearer/x-api-key values, the request -// `target` / subject the BFF holds, any raw identifier echoed in a response) is -// dropped before anything reaches the proof feed. The structural WIRE SHAPE the -// proof inspector renders is preserved, but raw identifier values inside it are -// blanked so the inspector shows the derived self-attestation framing, never a -// caller-supplied target. -// -// Server-only: this module is imported by +server.ts routes and the BFF, never -// into a client bundle. +// Server boundary helpers. Raw bodies are intentionally not projectable into a +// ProofTrace. These functions retain only transport status and scrub defensive +// text fields before an SSE frame is serialized. -import type { ProofStatus } from '$lib/types'; +const SOLMARA_UIN_RE = /\b[2-9]\d{9}\b/g; +const CASE_REFERENCE_RE = /\bCP-\d+\b/g; +const FARMER_ID_RE = /\bFR-\d+\b/g; +const BEARER_RE = /Bearer\s+[A-Za-z0-9._~+/=-]+/gi; +const API_KEY_RE = /(x-api-key\s*[:=]\s*)[A-Za-z0-9._~+/=-]+/gi; +const PRIVATE_KEY_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g; +const COMPACT_JWS_RE = /\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g; -// --------------------------------------------------------------------------- -// The allowlist. Only these top-level proof keys are forwarded to the feed. -// (claim, purpose, disclosure, authority, result, freshness) plus the structural -// wire shape (request/response bodies, themselves re-redacted field-by-field). -// --------------------------------------------------------------------------- export const PROOF_ALLOWLIST = [ 'claim', 'purpose', 'disclosure', 'authority', + 'issuer', + 'serviceId', + 'source', 'result', 'freshness' ] as const; -// The allowlist of body keys that may keep their VALUES when serializing a wire -// request/response body to the feed. Anything not here is structurally kept (so -// the inspector still shows the key) but its value is blanked / hashed. -// -// Crucially: `target`, `requester`, `requester_ref`, `target_ref`, `identifiers`, -// `id_hash`, `value` (when it could echo an identifier) are NOT value-preserved -// at the identifier level: identifier strings inside them are blanked. -const BODY_VALUE_ALLOWLIST = new Set([ - 'claims', - 'id', - 'version', - 'purpose', - 'disclosure', - 'format', - 'relationship', - 'type', - 'on_behalf_of', - 'delegation_ref', - 'results', - 'evidence_set_id', - 'orchestration', - 'decision', - 'source_trace', - 'composition', - 'requests', - 'body', - 'status', - 'request_summary', - 'response_summary', - 'derived_decisions', - 'pension-payment-should-stop', - 'citizen-self-service-ready', - 'survivor-benefit-eligible', - 'claim_id', - 'claim_version', - 'evaluation_id', - 'subject_type', - 'satisfied', - 'value', - 'issued_at', - 'expires_at', - 'matching', - 'confidence', - 'method', - 'policy_id', - 'score', - 'provenance', - 'schema_version', - 'generated_by', - 'used', - 'derived_from', - 'policy_hash', - 'policy_version', - 'service_id', - 'notary_service_id', - 'authority', - 'source_count', - 'source_versions', - 'source_runtimes', - 'profile' -]); - -// Keys whose string values are STRUCTURAL identifier handles, not raw ids. They -// stay (handles are already opaque rnref:/did: tokens), but we still pass them -// through the identifier scrubber to catch any leaked raw id. -const HANDLE_KEYS = new Set(['handle', 'identifier_schemes', 'profile']); - -// Patterns for raw identifiers / secrets that must NEVER reach the feed. -// Allowlist design means we do not RELY on these to redact (the allowlist already -// drops unknown keys); they are a belt-and-braces scrub of any allowlisted value -// that could still embed a fixture id, plus the test's assertion surface. -const SOLMARA_UIN_RE = /\b[2-9]\d{9}\b/g; -const CP_RE = /CP-\d+/g; -const BEARER_RE = /Bearer\s+[A-Za-z0-9._~+/=-]+/gi; -const X_API_KEY_RE = /(x-api-key\s*[:=]\s*)[A-Za-z0-9._~+/=-]+/gi; - -// Replace any embedded raw identifier / bearer material inside a string value. export function scrubString(input: string): string { return input - .replace(BEARER_RE, 'Bearer •••••••• (redacted)') - .replace(X_API_KEY_RE, '$1•••••••• (redacted)') - .replace(SOLMARA_UIN_RE, '••••') - .replace(CP_RE, '••••'); + .replace(PRIVATE_KEY_RE, '[private key redacted]') + .replace(BEARER_RE, 'Bearer [redacted]') + .replace(API_KEY_RE, '$1[redacted]') + .replace(COMPACT_JWS_RE, '[JWS redacted]') + .replace(SOLMARA_UIN_RE, '[redacted]') + .replace(CASE_REFERENCE_RE, '[redacted]') + .replace(FARMER_ID_RE, '[redacted]'); } -type Json = unknown; - -// Recursively redact a wire body: keep the structure (keys + types), but blank -// the VALUES of any key not on BODY_VALUE_ALLOWLIST, and scrub identifier-shaped -// strings everywhere. The `target`/`requester` envelopes keep their `type` but -// drop the identifier arrays entirely (the inspector shows relationship:self and -// the ABSENCE of a caller-supplied target, which is the engineer-facing point). -export function redactBody(body: Json): Json { - return walk(body, true); -} - -function walk(node: Json, valuePreserved: boolean): Json { - if (typeof node === 'string') { - return valuePreserved ? scrubString(node) : '••••(redacted)'; - } - if (typeof node === 'number' || typeof node === 'boolean' || node === null) { - return valuePreserved ? node : null; - } - if (Array.isArray(node)) { - return node.map((item) => walk(item, valuePreserved)); - } - if (typeof node === 'object') { - const out: Record = {}; - for (const [key, val] of Object.entries(node as Record)) { - // `target` and `requester` envelopes carry the subject the BFF holds. Keep - // the key so the shape matches, keep `type`, but DROP identifier values. - if (key === 'target' || key === 'requester') { - out[key] = redactEntityEnvelope(val); - continue; - } - // identifier arrays anywhere are dropped to an empty, shape-preserving form. - if (key === 'identifiers') { - out[key] = []; - continue; - } - const childPreserved = - valuePreserved && - (BODY_VALUE_ALLOWLIST.has(key) || HANDLE_KEYS.has(key)); - out[key] = walk(val, childPreserved); - } - return out; - } - return null; -} - -// Keep { type } of an entity envelope but strip identifiers so no raw subject -// leaks. This is what makes the inspector show "relationship:self, no caller -// target". -function redactEntityEnvelope(node: Json): Json { - if (node === null || typeof node !== 'object' || Array.isArray(node)) { - return null; - } - const obj = node as Record; - const out: Record = {}; - if (typeof obj.type === 'string') out.type = scrubString(obj.type); - // identifiers deliberately dropped to an empty array (shape preserved). - out.identifiers = []; - return out; -} - -// The redacted depth-2 request shown to the feed. The body is structurally -// preserved and identifier values are scrubbed. export type RedactedRequest = { method: string; url: string; - body: Record; }; export type RedactedResponse = { status: number; - body: Record; }; -export function redactRequest(req: { +export function redactBody(_body: unknown): Record { + return {}; +} + +export function redactRequest(request: { method: string; url: string; - body: Json; + body?: unknown; }): RedactedRequest { - return { - method: req.method, - url: scrubString(req.url), - body: redactBody(req.body) as Record - }; + return { method: scrubString(request.method), url: scrubString(request.url) }; } -export function redactResponse(res: { +export function redactResponse(response: { status: number; - body: Json; + body?: unknown; }): RedactedResponse { - return { - status: res.status, - body: redactBody(res.body) as Record - }; + return { status: response.status }; } -// A small structural assertion used by the SSE serializer and the test: does a -// serialized string contain any raw identifier or bearer material? export function containsRawIdentifier(serialized: string): boolean { return ( /\b[2-9]\d{9}\b/.test(serialized) || - /CP-\d+/.test(serialized) || - /Bearer\s+[A-Za-z0-9._~+/=-]{8,}/.test(serialized) || - /x-api-key\s*[:=]\s*[A-Za-z0-9._~+/=-]{8,}/i.test(serialized) + /\bCP-\d+\b/.test(serialized) || + /\bFR-\d+\b/.test(serialized) || + /Bearer\s+[A-Za-z0-9._~+/=-]{8,}/i.test(serialized) || + /x-api-key\s*[:=]\s*[A-Za-z0-9._~+/=-]{8,}/i.test(serialized) || + /-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(serialized) || + /\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/.test(serialized) ); } -// Allowlist guard for a flat metadata object (claim/purpose/disclosure/authority/ -// result/freshness). Drops any key not on PROOF_ALLOWLIST. -export function pickAllowedMeta( - meta: Record -): Record { +export function pickAllowedMeta(meta: Record): Record { const allow = new Set(PROOF_ALLOWLIST); - const out: Record = {}; - for (const [k, v] of Object.entries(meta)) { - if (allow.has(k)) { - out[k] = typeof v === 'string' ? scrubString(v) : v; - } - } - return out; + return Object.fromEntries( + Object.entries(meta) + .filter(([key]) => allow.has(key)) + .map(([key, value]) => [key, typeof value === 'string' ? scrubString(value) : value]) + ); } - -// Re-exported type for callers that build a status alongside redacted bodies. -export type { ProofStatus }; diff --git a/portal/src/lib/types.ts b/portal/src/lib/types.ts index 62fbae5..995710f 100644 --- a/portal/src/lib/types.ts +++ b/portal/src/lib/types.ts @@ -1,6 +1,6 @@ // Provenance kinds -> field types (portal spec section 5.3, UX section 3). export type FieldKind = 'self' | 'verify' | 'fetch' | 'decision'; -export type NotaryId = +export type AuthorityId = | 'civil' | 'social' | 'agri' @@ -29,7 +29,7 @@ export type Field = { label: string; kind: FieldKind; claim?: string; // e.g. 'household-below-poverty-threshold' - notary?: NotaryId; + authority?: AuthorityId; purpose?: string; // declared purpose, shown in the proof inspector disclose?: string; // what is NOT disclosed (the minimization "money shot") selfPlaceholder?: string; // placeholder for kind:'self' inputs @@ -40,20 +40,18 @@ export type Field = { export type ServiceForm = { slug: string; title: string; - authorities: NotaryId[]; // seal glyphs shown on the card / form header + authorities: AuthorityId[]; // seal glyphs shown on the card / form header fields: Field[]; }; -// ---- Wire-facing shapes ---- -// ClaimResult and ProofTrace are the STABLE portal-facing projection. The raw -// Upstream request/response JSON lives inside ProofTrace.request.body / -// response.body and follows the owning service contract. Renderer and proof -// agents treat those bodies as opaque records and pretty-print them. +// ---- Portal-facing shapes ---- +// Raw requests, source rows, bearer tokens, and signed JWS segments remain on +// the server. The browser receives only this bounded presentation projection. export type ClaimResult = { state: FieldState; display: string; // value or predicate sentence shown in the field - authority?: NotaryId; + authority?: AuthorityId; asOf?: string; // freshness timestamp reasonCode?: string; // e.g. 'VR-RED-02' traceId: string; // links to the ProofTrace ('event N') @@ -61,28 +59,38 @@ export type ClaimResult = { export type ProofStatus = 'in_flight' | 'ok' | 'false' | 'denied' | 'error'; +export type EvidenceSource = 'immutable extract' | 'Relay lookup'; + +export type EvidencePresentation = { + authority: string; + issuer: string; + serviceId: string; + source: EvidenceSource; +}; + export type ProofTrace = { id: string; // 'event N' label seq: number; fieldId?: string; - authority?: NotaryId; + authority?: AuthorityId; // depth 1 - human headline: string; // consequence-first answered: string; // "{Authority} answered: {claim} = {value}" notDisclosed: string; // ALWAYS present status: ProofStatus; ts: string; - // depth 2 - wire (redacted; allowlist only). Body follows the owning service. - request: { method: string; url: string; body: Record }; - response?: { status: number; body: Record }; - // depth 3 - crypto + purpose?: string; + resultState: FieldState; + presentations: EvidencePresentation[]; + responseStatus?: number; + // Depth 2 is deliberately descriptive. Cryptographic bytes and internal event ids do + // not cross the BFF boundary. proof?: { signedBy: string; algorithm: string; issuerKey: string; holderBound: string; credential: string; - auditId: string; }; }; @@ -90,7 +98,7 @@ export type ProofTrace = { export type RailChannel = 'verify' | 'fetch' | 'denied'; export type RailEvent = { id: string; - authority: NotaryId; + authority: AuthorityId; channel: RailChannel; phase: 'request' | 'sealed' | 'denied'; ts: string; diff --git a/portal/src/routes/+layout.svelte b/portal/src/routes/+layout.svelte index ef4c1e6..6fcdbd0 100644 --- a/portal/src/routes/+layout.svelte +++ b/portal/src/routes/+layout.svelte @@ -8,9 +8,9 @@ let { children, data }: { children: import('svelte').Snippet; data: LayoutData } = $props(); - // The footer audit log is a drawer: collapsed to its most-recent rows by default, + // The footer proof log is a drawer: collapsed to its most-recent rows by default, // expanded (and scrollable) on demand so the full proof history is reachable. - let auditOpen = $state(false); + let proofLogOpen = $state(false); // One app-wide SSE connection to the redacted proof feed. Browser only (onMount // never runs during SSR), so EventSource is safe here. The rail, ticker, and @@ -51,24 +51,24 @@
- -
+
-
+
@@ -128,7 +128,7 @@ min-width: 0; } - /* The audit-log drawer, pinned to the foot of the viewport. Collapsed it shows its + /* The proof-log drawer, pinned to the foot of the viewport. Collapsed it shows its most-recent rows; opened it grows upward and the rows scroll, so the full history is reachable without the log ever dominating the page. */ .ticker-band { diff --git a/portal/src/routes/api/evaluate/+server.ts b/portal/src/routes/api/evaluate/+server.ts index 85c0de0..993adff 100644 --- a/portal/src/routes/api/evaluate/+server.ts +++ b/portal/src/routes/api/evaluate/+server.ts @@ -1,5 +1,4 @@ -// POST /api/evaluate : proxy a single field's claim to its Notary (Phase 0: the -// MockEvidenceProvider). +// POST /api/evaluate: request one reviewed Evidence field through the provider. // // Body: { slug, fieldId, scenarioKey?, delegated? } // - slug / fieldId identify a field in the server-side form catalogue. @@ -101,7 +100,7 @@ function resolveAllowedField( id: fieldId, label: 'Guardian link verified', kind: 'verify', - notary: 'social' + authority: 'social' }; } @@ -110,7 +109,7 @@ function resolveAllowedField( id: fieldId, label: 'Cross-person denial', kind: 'verify', - notary: 'civil' + authority: 'civil' }; } diff --git a/portal/src/routes/gallery/fields/+page.svelte b/portal/src/routes/gallery/fields/+page.svelte index c3a3c27..6b0fc08 100644 --- a/portal/src/routes/gallery/fields/+page.svelte +++ b/portal/src/routes/gallery/fields/+page.svelte @@ -16,27 +16,27 @@ label: 'Farmer registration', kind: 'verify', claim: 'farmer-registered', - notary: 'agri' + authority: 'agri' }; const fetchField: Field = { id: 'g-fetch', label: 'Child age under 5', kind: 'verify', claim: 'child-age-under-5', - notary: 'civil' + authority: 'civil' }; const householdField: Field = { id: 'g-household', label: 'Household below threshold', kind: 'verify', claim: 'household-below-poverty-threshold', - notary: 'social' + authority: 'social' }; const decisionField: Field = { id: 'g-decision', label: 'Combined eligibility', kind: 'decision', - notary: 'social', + authority: 'social', manual: true }; diff --git a/portal/src/routes/gallery/proof/+page.svelte b/portal/src/routes/gallery/proof/+page.svelte index 1a2d0f5..0ecf7e8 100644 --- a/portal/src/routes/gallery/proof/+page.svelte +++ b/portal/src/routes/gallery/proof/+page.svelte @@ -35,7 +35,7 @@ {connected ? 'Simulate: disconnect' : 'Simulate: reconnect'} - (Disconnected state shows the "reconnecting to audit feed" pill) + (Disconnected state shows the "reconnecting to proof feed" pill)
@@ -63,7 +63,7 @@
  • denied - event-4: denied (Civil, subject_mismatch - cross-person attempt) + event-4: denied (Civil, not_authorized - cross-person attempt)
  • identity @@ -79,7 +79,7 @@
  • -
    +

    Proof ticker (always present, ARIA live-region)

    diff --git a/portal/src/routes/proof/stream/server.test.ts b/portal/src/routes/proof/stream/server.test.ts index abec822..d1e0dd8 100644 --- a/portal/src/routes/proof/stream/server.test.ts +++ b/portal/src/routes/proof/stream/server.test.ts @@ -42,15 +42,8 @@ function trace(id: string): ProofTrace { notDisclosed: 'Not disclosed: any other civil record detail', status: 'ok', ts: '2026-06-22T12:00:00.000Z', - request: { - method: 'POST', - url: 'https://civil-notary.gov.solmara.example/v1/evaluations', - body: { claim: 'person-is-deceased' } - }, - response: { - status: 200, - body: { result: true } - } + resultState: 'verified', + presentations: [] }; } diff --git a/portal/src/routes/services/[slug]/+page.svelte b/portal/src/routes/services/[slug]/+page.svelte index 10b2a6c..6a4ee0b 100644 --- a/portal/src/routes/services/[slug]/+page.svelte +++ b/portal/src/routes/services/[slug]/+page.svelte @@ -20,11 +20,11 @@ let guardianVerified = $state(false); function inFlight(field: Field): ClaimResult { - return { state: 'in_flight', display: '', authority: field.notary, traceId: '' }; + return { state: 'in_flight', display: '', authority: field.authority, traceId: '' }; } function errored(field: Field): ClaimResult { - return { state: 'error', display: '', authority: field.notary, traceId: '' }; + return { state: 'error', display: '', authority: field.authority, traceId: '' }; } function sleep(ms: number): Promise { @@ -56,7 +56,7 @@ id: 'guardian-link-verified', label: 'Guardian link verified', kind: 'verify', - notary: 'social' + authority: 'social' }; const identityResult = $derived({ diff --git a/projects/cra-civil/.registry-stack-editor/manifest.json b/projects/cra-civil/.registry-stack-editor/manifest.json deleted file mode 100644 index 3603a8e..0000000 --- a/projects/cra-civil/.registry-stack-editor/manifest.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "format": "registry.stack.editor-manifest", - "version": 1, - "registryctl_version": "0.15.2", - "schemas": [ - { - "kind": "project", - "path": "schemas/project.schema.json", - "file_glob": "registry-stack.yaml", - "sha256": "sha256:9fd76663c866c52bb5a17a4cfea821049b7428c812f1666da9db0d43a00da144" - }, - { - "kind": "environment", - "path": "schemas/environment.schema.json", - "file_glob": "environments/*.yaml", - "sha256": "sha256:d16828629e001a30a1705af90bcc12a214be18cf5805a9833e65b03d631c9e9b" - }, - { - "kind": "integration", - "path": "schemas/integration.schema.json", - "file_glob": "integrations/*/integration.yaml", - "sha256": "sha256:2e45452f63c3c610ed14b79244bc9aa086d79d811ad0899223b36c160d449a79" - }, - { - "kind": "fixture", - "path": "schemas/fixture.schema.json", - "file_glob": "integrations/*/fixtures/*.yaml", - "sha256": "sha256:98c0ab21e35cecd143b51e4b5dd487de07ceea24e505bbe351d212e0628c8c8c" - }, - { - "kind": "entity", - "path": "schemas/entity.schema.json", - "file_glob": "entities/*.yaml", - "sha256": "sha256:4cbb31c8addeb54f8b57aaaff8f30c39e651a21a2d741a1b775ecb0d7c248342" - } - ] -} diff --git a/projects/cra-civil/.registry-stack-editor/schemas/entity.schema.json b/projects/cra-civil/.registry-stack-editor/schemas/entity.schema.json deleted file mode 100644 index ffc57c8..0000000 --- a/projects/cra-civil/.registry-stack-editor/schemas/entity.schema.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/entity.v1.json", - "title": "Registry Stack project entity v1", - "description": "Defines a bounded, materialized entity that Relay can query without coupling the project to a specific source product.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "primary_key", "schema", "materialization"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Entity authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the entity." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this entity definition.", "examples": [1] }, - "primary_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Field whose value uniquely identifies each materialized record.", "examples": ["person_id"] }, - "schema": {"x-registry-field": "property", "$ref": "#/$defs/objectSchema", "description": "Closed JSON object schema for materialized records." }, - "materialization": { - "x-registry-field": "property", - "description": "Authored resource, refresh, and bounded recovery-set retention applied while building entity generations.", - "type": "object", - "additionalProperties": false, - "required": ["max_records", "max_bytes", "refresh", "retain_generations"], - "properties": { - "max_records": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 100000000, "description": "Maximum number of records allowed in one generation.", "examples": [10000] }, - "max_bytes": { - "x-registry-field": "property", - "description": "Maximum encoded size of one generation, up to 1 GiB, as bytes or a KiB/MiB quantity.", - "examples": ["16MiB"], - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1073741824 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,5}|10[0-3][0-9]{4}|104[0-7][0-9]{3}|1048[0-4][0-9]{2}|10485[0-6][0-9]|104857[0-6])KiB|(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])MiB)$" } - ] - }, - "refresh": { - "x-registry-field": "property", - "description": "Refresh cadence as a canonical positive duration no greater than 30 days, or manual when an operator initiates every refresh.", - "examples": ["1h"], - "oneOf": [ - {"x-registry-field": "branch", "const": "manual" }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-4][0-9]{8}|25[0-8][0-9]{7}|259[0-1][0-9]{6}|2592000000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-4][0-9]{5}|25[0-8][0-9]{4}|259[0-1][0-9]{3}|2592000)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-2][0-9]{3}|43[0-1][0-9]{2}|43200)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-1][0-9]{1}|720)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30)d)$" } - ] - }, - "retain_generations": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16, "description": "Number of completed cache generations, including the active generation, retained as a bounded recovery set after successful publication. This does not make arbitrary retained generations selectable for rollback.", "examples": [2] } - } - } - }, - "examples": [ - { - "version": 1, - "id": "people", - "revision": 1, - "primary_key": "person_id", - "schema": { - "type": "object", - "additionalProperties": false, - "required": ["person_id"], - "properties": { "person_id": { "type": "string" } } - }, - "materialization": { - "max_records": 10000, - "max_bytes": "16MiB", - "refresh": "1h", - "retain_generations": 2 - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "propertyName": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$", "description": "Portable lowercase record field name." }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for a nullable field.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "items": false, - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "fieldSchema": { - "description": "Bounded schema keywords supported for one scalar materialized field.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "enum": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true }, - "const": {"x-registry-field": "property"}, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 65536 }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 } - } - }, - "objectSchema": { - "description": "Closed object schema whose declared required fields must appear in properties.", - "type": "object", - "additionalProperties": false, - "required": ["type", "additionalProperties", "required", "properties"], - "properties": { - "type": {"x-registry-field": "property", "const": "object" }, - "additionalProperties": {"x-registry-field": "property", "const": false }, - "required": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 256, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/propertyName" } - }, - "properties": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 256, - "propertyNames": {"x-registry-field": "property", "$ref": "#/$defs/propertyName" }, - "additionalProperties": {"x-registry-field": "property", "$ref": "#/$defs/fieldSchema" } - } - } - } - } -} diff --git a/projects/cra-civil/.registry-stack-editor/schemas/environment.schema.json b/projects/cra-civil/.registry-stack-editor/schemas/environment.schema.json deleted file mode 100644 index f04c70b..0000000 --- a/projects/cra-civil/.registry-stack-editor/schemas/environment.schema.json +++ /dev/null @@ -1,337 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/environment.v1.json", - "title": "Registry Stack project environment v1", - "description": "Binds a Registry Stack project to environment-specific services, sources, credentials, and deployment topology.", - "type": "object", - "additionalProperties": false, - "required": ["version", "deployment"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Environment authoring format version." }, - "integrations": {"x-registry-field": "property", "type": "object", "maxProperties": 16, "description": "Environment bindings keyed by authored integration identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/integration" } }, - "entities": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "description": "Environment-specific source bindings keyed by entity identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/entity" } }, - "issuance": { - "x-registry-field": "property", - "description": "Notary issuer and signing-key bindings for credential issuance.", - "type": "object", "additionalProperties": false, "required": ["issuer", "signing_key", "signing_kid", "generation"], - "properties": { "issuer": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1 }, "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" }, "algorithm": {"x-registry-field": "property", "enum": ["EdDSA", "ES256"], "default": "EdDSA", "description": "Credential issuer signing algorithm. Holder proof remains EdDSA with did:jwk." }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "callers": { - "x-registry-field": "property", - "description": "Notary API-key caller identities and the scopes each identity receives.", - "type": "object", "maxProperties": 64, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["api_key_fingerprint", "scopes"], - "properties": { "api_key_fingerprint": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } - } - }, - "relay": { - "x-registry-field": "property", - "description": "Public Relay identity and token-validation settings for a Relay deployment.", - "type": "object", "additionalProperties": false, - "required": ["origin", "issuer", "jwks_url", "audience", "allowed_clients"], - "properties": { "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "allowed_clients": {"x-registry-field": "sensitive_property", "type": "array", "maxItems": 64, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } }, "local_api_keys": {"x-registry-field": "property", "description": "Local-profile-only synthetic principals and scopes. Registryctl generates the raw keys and supplies only their fingerprints to Relay.", "type": "object", "additionalProperties": false, "required": ["match_principal", "no_match_principal", "scopes"], "properties": { "match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "no_match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } } } - }, - "notary_relay": { - "x-registry-field": "property", - "description": "Deployment-internal Relay connection, Notary workload identity, and token file used only when Notary consults Relay.", - "type": "object", "additionalProperties": false, "required": ["base_url", "workload_client_id", "token_file"], - "properties": { "base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/internalOrigin" }, "workload_client_id": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "token_file": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" } } - }, - "relay_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Relay-owned consultation PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Notary-owned PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_cel": { - "x-registry-field": "property", - "description": "Optional per-worker data/address-space ceiling for dedicated Notary CEL processes. The Notary default is 134217728 bytes; 1073741824 is the maximum emulation-compatible exception and remains a limit, not an allocation.", - "type": "object", "additionalProperties": false, "required": ["worker_memory_bytes"], - "properties": { - "worker_memory_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 33554432, "maximum": 1073741824 } - } - }, - "oid4vci": { - "x-registry-field": "property", - "description": "Explicit registry-backed holder-wallet OID4VCI binding for one authored Notary credential profile.", - "$ref": "#/$defs/oid4vci" - }, - "deployment": { - "x-registry-field": "property", - "description": "Products deployed in this environment and the operational profile they use.", - "type": "object", "additionalProperties": false, "required": ["profile"], - "properties": { "profile": {"x-registry-field": "property", "enum": ["local", "hosted_lab", "production", "evidence_grade"] }, "relay": {"x-registry-field": "property", "$ref": "#/$defs/service" }, "notary": {"x-registry-field": "property", "$ref": "#/$defs/service" } }, - "anyOf": [{"x-registry-field": "branch", "required": ["relay"] }, {"x-registry-field": "branch", "required": ["notary"] }] - } - }, - "allOf": [ - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["deployment"], "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } }, - "then": {"x-registry-field": "branch", "required": ["relay"] }, - "else": {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "required": ["relay"] } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_relay"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay", "notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["relay_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_cel"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["oid4vci"] }, - "then": { - "x-registry-field": "branch", - "required": ["notary_state"], - "properties": { - "deployment": {"x-registry-field": "property", "required": ["notary"] } - } - }, - "else": {"x-registry-field": "branch", "properties": { "callers": {"x-registry-field": "property", "minProperties": 1 } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["profile"], "properties": { "profile": {"x-registry-field": "property", "const": "local" } } } } }, - "else": { - "x-registry-field": "branch", - "properties": { - "relay": { - "x-registry-field": "property", - "not": {"x-registry-field": "branch", "required": ["local_api_keys"] }, - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - }, - "oid4vci": { - "x-registry-field": "property", - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorization_server": { - "x-registry-field": "property", - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - } - } - } - } - } - } - ], - "examples": [ - { - "version": 1, - "deployment": { - "profile": "local", - "notary": { "service": "notary" } - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token2048": { "type": "string", "minLength": 1, "maxLength": 2048, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII token, including full verification-method identifiers." }, - "secret": { "type": "object", "additionalProperties": false, "description": "Reference to a process-environment variable; secret values are never authored here.", "required": ["secret"], "properties": { "secret": {"x-registry-field": "secret_reference_property", "type": "string", "pattern": "^[A-Z_][A-Z0-9_]{0,127}$" } } }, - "origin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/?$", "description": "HTTPS origin without path, query, or fragment." }, - "httpsResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/[^?#]+$", "description": "Exact HTTPS resource without query or fragment." }, - "localLoopbackOrigin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/?$", "description": "HTTP IP-loopback origin; public Relay and issuer fields restrict it to the local profile, while internal Notary-to-Relay connections allow it in any profile." }, - "localLoopbackResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/[^?#]+$", "description": "HTTP IP-loopback resource accepted only with the local deployment profile." }, - "internalOrigin": { "description": "Deployment-internal HTTPS or HTTP IP-loopback origin.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayOrigin": { "description": "Relay or issuer origin using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayResource": { "description": "Relay authentication resource using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/httpsResource" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackResource" }] }, - "oid4vci": { - "description": "Closed OID4VCI trust, key, subject, wallet, and credential-profile binding.", - "type": "object", - "additionalProperties": false, - "required": ["public_base_url", "credential", "authorization_server", "client", "access_token", "sensitive_state_key", "subject", "redirect_uri", "allowed_wallet_origins"], - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "credential": { - "x-registry-field": "property", - "description": "Existing project service and credential profile exposed through OID4VCI.", - "type": "object", "additionalProperties": false, "required": ["service", "profile"], - "properties": { - "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "profile": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "authorization_server": { - "x-registry-field": "property", - "description": "Pinned eSignet issuer and exact endpoints used for login and token validation.", - "type": "object", "additionalProperties": false, "required": ["issuer", "jwks_url", "userinfo_url", "authorize_url", "token_url"], - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" } - } - }, - "client": { - "x-registry-field": "property", - "description": "eSignet relying-party identity and dedicated private-key reference.", - "type": "object", "additionalProperties": false, "required": ["id", "signing_key", "signing_kid"], - "properties": { - "id": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "access_token": { - "x-registry-field": "property", - "description": "Dedicated Notary access-token signing key and published key identifier.", - "type": "object", "additionalProperties": false, "required": ["signing_key", "signing_kid"], - "properties": { - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "sensitive_state_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "subject": { - "x-registry-field": "property", - "description": "Verified eSignet userinfo claim bound exactly to the credential subject identifier.", - "type": "object", "additionalProperties": false, "required": ["token_claim", "id_type"], - "properties": { - "token_claim": {"x-registry-field": "property", "$ref": "#/$defs/token256" }, - "id_type": {"x-registry-field": "property", "$ref": "#/$defs/token256" } - } - }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "allowed_wallet_origins": { - "x-registry-field": "sensitive_property", - "description": "Exact HTTPS browser origins admitted to the wallet-facing Notary routes.", - "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "$ref": "#/$defs/origin" } - }, - "tx_code": { - "x-registry-field": "property", - "description": "Transaction-code policy. Omit for the secure required-PIN default. Set required=false only for a bounded bearer-offer interoperability profile; the compiler fixes the offer lifetime at 300 seconds.", - "type": "object", "additionalProperties": false, - "properties": { "required": {"x-registry-field": "property", "type": "boolean", "default": true } } - } - } - }, - "privateCidrs": { - "description": "Explicit private network ranges this source may resolve to.", - "type": "array", "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "type": "string", "minLength": 3, "maxLength": 64 } - }, - "ca": { - "description": "Pinned certificate-authority file and its rotation generation.", - "type": "object", "additionalProperties": false, "required": ["file", "generation"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "mtls": { - "description": "Client certificate and private-key reference for mutual TLS.", - "type": "object", "additionalProperties": false, "required": ["certificate_file", "private_key", "generation"], - "properties": { "certificate_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "private_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "endpoint": { - "description": "Security-bound HTTPS endpoint with optional private-network, CA, and mTLS policy.", - "type": "object", "additionalProperties": false, "required": ["origin", "path", "generation"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//)(?!.*[?#]).+$" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } - } - }, - "credential": { - "description": "One supported source credential shape, containing references rather than values.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["username", "password", "generation"], "properties": { "username": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "password": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["token", "generation"], "properties": { "token": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["client_id", "client_secret", "generation"], "properties": { "client_id": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "client_secret": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["value", "generation"], "properties": { "value": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } } - ] - }, - "provider": { - "description": "Environment-specific physical provider for a materialized entity.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "csv" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "delimiter": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 }, "quote": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "project_file", "path", "sheet"], "properties": { "type": {"x-registry-field": "property", "const": "xlsx" }, "project_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "sheet": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "data_range": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "parquet" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "connection", "schema", "table"], "properties": { "type": {"x-registry-field": "property", "const": "postgres" }, "connection": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "schema": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" }, "table": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" } } } - ] - }, - "entity": { - "description": "Maps an authored entity to provider columns and immutable source-generation identifiers.", - "type": "object", "additionalProperties": false, "required": ["provider", "columns", "source_revision", "generation"], - "properties": { "provider": {"x-registry-field": "property", "$ref": "#/$defs/provider" }, "columns": {"x-registry-field": "property", "type": "object", "minProperties": 1, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } }, "source_revision": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "generation": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 } } - }, - "source": { - "description": "Runtime source connection policy for one authored integration.", - "type": "object", "additionalProperties": false, "required": ["origin"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "credential": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "oauth": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "jwks": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "rate": {"x-registry-field": "property", "type": "object", "additionalProperties": false, "required": ["per_minute", "burst"], "properties": { "per_minute": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 60000 }, "burst": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 1024 } } }, - "concurrency": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 64 }, - "timeout": { - "x-registry-field": "property", - "description": "Optional request timeout no greater than 20 seconds; generated bindings narrow it to the integration deadline.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - }, - "integration": { - "description": "Environment binding for an authored source integration.", - "type": "object", "additionalProperties": false, "required": ["source"], - "properties": { "source": {"x-registry-field": "property", "$ref": "#/$defs/source" } } - }, - "service": { "type": "object", "additionalProperties": false, "description": "Binds an authored product role to a deployed service identifier.", "required": ["service"], "properties": { "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } }, - "token256": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII protocol token." }, - "postgresIdentifier": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,62}$", "description": "Portable unquoted PostgreSQL schema or table identifier." }, - "absolutePath": { "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized absolute path without dot segments or duplicate separators." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized project-relative path without dot segments or duplicate separators." } - } -} diff --git a/projects/cra-civil/.registry-stack-editor/schemas/fixture.schema.json b/projects/cra-civil/.registry-stack-editor/schemas/fixture.schema.json deleted file mode 100644 index b853192..0000000 --- a/projects/cra-civil/.registry-stack-editor/schemas/fixture.schema.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/fixture.v1.json", - "title": "Registry Stack project integration fixture v1", - "description": "Defines one synthetic, deterministic integration scenario for offline adapter verification.", - "type": "object", - "additionalProperties": false, - "required": ["name", "classification", "input", "interactions", "expect"], - "properties": { - "name": {"x-registry-field": "public_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Human-readable scenario name shown in test output.", "examples": ["existing household matches"] }, - "classification": {"x-registry-field": "public_property", "const": "synthetic", "description": "Declares that fixture data is synthetic and safe for offline testing." }, - "request": { - "x-registry-field": "redacted_fixture_property", - "$ref": "#/$defs/governedRequest", - "description": "Optional independently authored synthetic Notary request used to prove the request-to-consultation binding before Relay access." - }, - "input": { - "x-registry-field": "redacted_fixture_property", - "description": "Typed consultation inputs supplied to the adapter under test.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "type": ["string", "boolean", "integer", "null"] - } - }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Named date values available to deterministic fixture interpolation.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "interactions": { - "x-registry-field": "redacted_fixture_property", - "description": "Ordered upstream request and response exchanges expected during execution.", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/interaction" } - }, - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/expectation", "description": "Observable adapter result required for the scenario to pass." } - }, - "examples": [ - { - "name": "existing household matches", - "classification": "synthetic", - "input": { "household_id": "HH-1001" }, - "interactions": [ - { - "expect": { "method": "GET", "path": "/households/HH-1001" }, - "respond": { "status": 200, "body": { "active": true } } - } - ], - "expect": { "outcome": "match", "outputs": { "active": true } } - } - ], - "$defs": { - "governedRequest": { - "description": "The same closed governed request shape accepted by project test --live.", - "type": "object", - "additionalProperties": false, - "required": ["target", "claims", "purpose"], - "properties": { - "target": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/governedTarget" }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies synthetic date variables to the governed evaluation request.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "claims": { - "x-registry-field": "redacted_fixture_property", - "description": "Lists the authored claims evaluated by this synthetic request witness.", - "type": "array", - "minItems": 1, - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedClaimRef" } - }, - "disclosure": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the disclosure mode requested from the authored claim policy." }, - "format": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the claim-result media type requested from the governed Notary path." }, - "purpose": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Names the authored service purpose exercised by this synthetic request witness." } - } - }, - "governedTarget": { - "description": "The synthetic subject target presented to the same governed request boundary as a live Notary evaluation.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 64, "description": "Names the authored target entity type used for claim evaluation." }, - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies an optional synthetic direct target identifier." }, - "identifiers": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored synthetic identifiers for consultation input mapping.", - "type": "array", - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedIdentifier" } - }, - "attributes": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored typed target attributes for consultation input mapping.", - "type": "object", - "maxProperties": 16, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": ["string", "boolean", "integer", "null"] } - } - } - }, - "governedIdentifier": { - "description": "One synthetic target identifier with an authored scheme and string value.", - "type": "object", - "additionalProperties": false, - "required": ["scheme", "value"], - "properties": { - "scheme": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 96, "description": "Names the authored identifier scheme selected by a consultation input mapping." }, - "value": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies the synthetic string value bound to this identifier scheme." } - } - }, - "governedClaimRef": { - "description": "A requested claim ID, optionally pinned to one authored claim version.", - "oneOf": [ - {"x-registry-field": "branch", "type": "string"}, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Names one authored claim requested by this fixture witness."}, - "version": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Pins the requested claim to one authored claim-policy version."} - } - } - ] - }, - "interaction": { - "description": "One expected upstream request paired with its synthetic response.", - "type": "object", - "additionalProperties": false, - "required": ["expect", "respond"], - "properties": { - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/request" }, - "respond": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/response" } - } - }, - "request": { - "description": "Exact HTTP request shape the adapter must produce.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "redacted_fixture_property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "query": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 64, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "oneOf": [ - {"x-registry-field": "branch", "type": ["string", "boolean", "integer", "null"] }, - { - "x-registry-field": "branch", - "type": "array", - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "type": ["string", "boolean", "integer", "null"] } - } - ] - } - }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - "response": { - "description": "Synthetic HTTP response or timeout returned to the adapter.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["status"], - "properties": { - "status": {"x-registry-field": "redacted_fixture_property", "type": "integer", "minimum": 100, "maximum": 599 }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["timeout"], - "properties": { - "timeout": { - "x-registry-field": "redacted_fixture_property", - "description": "Reviewed synthetic timeout intent no greater than 20 seconds; offline execution returns a timeout outcome without sleeping.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - ] - }, - "fixtureBody": { - "description": "Inline JSON, or an exact closed reference to a strict JSON file below the fixture bodies directory. The top-level file key is reserved for file references.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": { - "x-registry-field": "redacted_fixture_property", - "type": "string", - "minLength": 8, - "maxLength": 4096, - "pattern": "^bodies/(?!\\.\\.?/)(?!.*(?:/)\\.\\.?/)(?!.*//).+$" - } - } - }, - { - "x-registry-field": "branch", - "not": { - "x-registry-field": "branch", - "type": "object", - "required": ["file"] - } - } - ] - }, - "expectation": { - "description": "Expected normalized outcome, output values, claims, or error.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "outcome": {"x-registry-field": "redacted_fixture_property", "enum": ["match", "no_match", "ambiguous"] }, - "outputs": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "claims": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "error": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256 } - } - } - } -} diff --git a/projects/cra-civil/.registry-stack-editor/schemas/integration.schema.json b/projects/cra-civil/.registry-stack-editor/schemas/integration.schema.json deleted file mode 100644 index 9bb2ad0..0000000 --- a/projects/cra-civil/.registry-stack-editor/schemas/integration.schema.json +++ /dev/null @@ -1,514 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/integration.v1.json", - "title": "Registry Stack project integration v1", - "description": "Defines a product-neutral source adaptation contract using bounded HTTP, Rhai script, or exact snapshot capabilities.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "input", "capability", "outputs"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Integration authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the integration." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this integration contract.", "examples": [1] }, - "source": {"x-registry-field": "property", "$ref": "#/$defs/source", "description": "Optional source product metadata and transport policy; capabilities remain product-neutral." }, - "input": { - "x-registry-field": "property", - "description": "Typed selector and parameter inputs accepted by this integration.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/input" } - }, - "capability": {"x-registry-field": "property", "$ref": "#/$defs/capability", "description": "Exactly one bounded execution mechanism for the source adaptation." }, - "outputs": { - "x-registry-field": "property", - "description": "Named typed outputs, or a shorthand list of output names inferred by the authoring compiler.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": { - "x-registry-field": "map_key", - "allOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputName" }, - {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "enum": ["matched", "outcome"] } } - ] - }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/output" } - }, - { - "x-registry-field": "branch", - "type": "array", - "minItems": 1, - "maxItems": 64, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/inputName" } - } - ] - }, - "limits": {"x-registry-field": "property", "$ref": "#/$defs/limits", "description": "Optional tighter resource limits for one integration execution." }, - "not_applicable": {"x-registry-field": "property", "$ref": "#/$defs/notApplicable", "description": "Explicit rationale and request-fixture evidence for a normally required outcome that the source contract cannot produce." } - }, - "examples": [ - { - "version": 1, - "id": "household_lookup", - "revision": 1, - "input": { - "household_id": { "role": "selector", "type": "string" } - }, - "capability": { - "script": { "file": "adapter.rhai" } - }, - "outputs": { - "active": { "type": "boolean" } - } - } - ], - "$defs": { - "notApplicable": { - "description": "Closed declarations for outcomes that the source protocol, exact singleton endpoint, or local unique-key constraint proves impossible.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "ambiguity": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed source contract cannot produce more than one matching record." }, - "subject_mismatch": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed response contract contains no identifier comparable with a requested selector." } - } - }, - "notApplicableReason": { - "description": "A reviewable reason that names a real request fixture proving the source access shape used by the rationale.", - "type": "object", - "additionalProperties": false, - "required": ["rationale", "request_fixture"], - "properties": { - "rationale": {"x-registry-field": "property", "type": "string", "minLength": 24, "maxLength": 512, "description": "Why the named normally required outcome cannot occur for this source contract." }, - "request_fixture": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Fixture name containing the supporting bounded source request." } - } - }, - "stableId": { - "description": "Lowercase stable identifier used in project references.", - "type": "string", - "pattern": "^[a-z][a-z0-9._-]{0,95}$" - }, - "inputName": { - "description": "Portable lowercase name for an integration input or output.", - "type": "string", - "pattern": "^[a-z][a-z0-9_]{0,63}$" - }, - "relativePath": { - "description": "Project-relative path without traversal or duplicate separators.", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" - }, - "integrationResponseByteSize": { - "description": "Maximum bytes accepted from one source response, defaulting to 512 KiB and capped at 8 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "512KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 8388608 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|[1-7][0-9]{3}|80[0-9]{2}|81[0-8][0-9]|819[0-2])KiB|[1-8]MiB)$" } - ] - }, - "integrationRequestByteSize": { - "description": "Maximum aggregate author-controlled request bytes for one execution, defaulting to 64 KiB and capped at 1 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "64KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1048576 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])KiB|1MiB)$" } - ] - }, - "integrationSourceByteSize": { - "description": "Maximum aggregate source-response bytes for one execution, defaulting to 2 MiB and capped at 16 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "2MiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 16777216 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-5][0-9]{3}|16[0-2][0-9]{2}|163[0-7][0-9]|1638[0-4])KiB|(?:[1-9]|1[0-6])MiB)$" } - ] - }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for nullable values.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "input": { - "description": "Type, validation, and canonicalization rules for one authored input.", - "type": "object", - "additionalProperties": false, - "required": ["role", "type"], - "properties": { - "role": {"x-registry-field": "property", "enum": ["selector", "parameter"] }, - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "description": "Maximum Unicode scalar-value length. The 1024-character ceiling derives a conservative 4096-byte UTF-8 ceiling for one canonical input.", "type": "integer", "minimum": 1, "maximum": 1024 }, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 16384 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "canonicalization": {"x-registry-field": "property", "enum": ["identity", "ascii_lowercase"] } - } - }, - "versions": { - "description": "Source versions tested by the project and versions explicitly accepted as unverified.", - "type": "object", - "additionalProperties": false, - "properties": { - "tested": {"x-registry-field": "property", "$ref": "#/$defs/versionList" }, - "unverified": {"x-registry-field": "property", "$ref": "#/$defs/versionList" } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["tested"], "properties": { "tested": {"x-registry-field": "property", "minItems": 1 } } }, - {"x-registry-field": "branch", "required": ["unverified"], "properties": { "unverified": {"x-registry-field": "property", "minItems": 1 } } } - ] - }, - "versionList": { - "description": "Bounded list of source version labels.", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } - }, - "credential": { - "description": "Authentication mechanism expected by the source adapter, without environment secret values.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "enum": ["none", "basic", "static_bearer"] } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_header" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_query" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9._:~-]{0,95}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "request", "response_profile"], - "properties": { - "type": {"x-registry-field": "property", "const": "oauth2_client_credentials" }, - "request": {"x-registry-field": "property", "enum": ["form", "json"] }, - "response_profile": {"x-registry-field": "property", "const": "oauth2_bearer" }, - "scope": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 }, - "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "refresh_skew": { - "x-registry-field": "property", - "description": "Positive OAuth refresh interval below the generated token contract's 60-second minimum lifetime.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|[1-5][0-9]{4})ms|(?:[1-9]|[1-5][0-9])s)$" - } - } - } - ] - }, - "allowRule": { - "description": "One read-only upstream method and path pattern the adapter may call.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": { - "x-registry-field": "sensitive_property", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^/[^?#]*$" - }, - "semantics": {"x-registry-field": "property", "const": "read_only" } - } - }, - "source": { - "description": "Optional source metadata, authentication shape, allowlist, and protocol profile.", - "type": "object", - "additionalProperties": false, - "required": ["auth"], - "properties": { - "product": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "versions": {"x-registry-field": "property", "$ref": "#/$defs/versions" }, - "auth": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "allow": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/allowRule" } - }, - "request_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "format": {"x-registry-field": "property", "enum": ["json", "text"] }, - "max_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationResponseByteSize" } - } - }, - "protocol": {"x-registry-field": "property", "$ref": "#/$defs/protocol" } - } - }, - "protocol": { - "description": "Optional interoperable protocol profiles layered over the source transport.", - "type": "object", - "additionalProperties": false, - "properties": { - "signed_dci": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["profile", "path", "jwks_profile", "sender", "receiver", "registry_type", "record_type", "locale", "selectors"], - "properties": { - "profile": {"x-registry-field": "property", "const": "dci-search-v1" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^/[^?#]*$" }, - "jwks_profile": {"x-registry-field": "property", "const": "rsa-signing-jwks-v1" }, - "sender": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "receiver": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "registry_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "record_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "locale": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$" }, - "selectors": { - "x-registry-field": "property", - "type": "object", "minProperties": 1, "maxProperties": 8, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["field", "response_pointer"], - "properties": { - "field": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 160 }, - "response_pointer": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" } - } - } - } - } - } - }, - "minProperties": 1 - }, - "inputReference": { - "description": "Reference to a declared integration input.", - "type": "object", - "additionalProperties": false, - "required": ["input"], - "properties": { "input": {"x-registry-field": "property", "$ref": "#/$defs/inputName" } } - }, - "inputOrLiteral": { - "description": "Request value supplied from a declared input or an authored scalar literal.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputReference" }, - {"x-registry-field": "branch", "type": ["string", "boolean", "integer"] } - ] - }, - "httpRequest": { - "description": "Bounded read-only HTTP request template.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "semantics": {"x-registry-field": "property", "const": "read_only" }, - "query": {"x-registry-field": "property", "type": "object", "maxProperties": 64, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "headers": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "body": { "x-registry-field": "property" } - } - }, - "httpResponse": { - "description": "Rules for translating upstream status and body cardinality into normalized outcomes.", - "type": "object", - "additionalProperties": false, - "properties": { - "no_match": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "ambiguous": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "shape": { - "x-registry-field": "property", - "oneOf": [ - {"x-registry-field": "branch", "const": "singleton" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["records", "cardinality"], - "properties": { - "records": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" }, - "cardinality": {"x-registry-field": "property", "const": "probe_two" } - } - } - ] - } - } - }, - "capability": { - "description": "Closed choice among direct HTTP, Rhai script, and exact snapshot execution.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["http"], - "properties": { - "http": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["request"], - "properties": { - "request": {"x-registry-field": "property", "$ref": "#/$defs/httpRequest" }, - "response": {"x-registry-field": "property", "$ref": "#/$defs/httpResponse" } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["script"], - "properties": { - "script": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, - "modules": { - "x-registry-field": "sensitive_property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/relativePath" } - } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["snapshot"], - "properties": { - "snapshot": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["entity", "exact", "freshness"], - "properties": { - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "exact": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 8, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputReference" } - }, - "freshness": { - "x-registry-field": "property", - "type": "string", - "description": "Maximum accepted snapshot age as a canonical positive duration no greater than 31 days.", - "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-5][0-9]{8}|26[0-6][0-9]{7}|267[0-7][0-9]{6}|2678[0-3][0-9]{5}|2678400000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-5][0-9]{5}|26[0-6][0-9]{4}|267[0-7][0-9]{3}|2678[0-3][0-9]{2}|2678400)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-3][0-9]{3}|44[0-5][0-9]{2}|446[0-3][0-9]{1}|44640)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-3][0-9]{1}|74[0-3]|744)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30|31)d)$" - } - } - } - } - } - ] - }, - "output": { - "description": "Type and optional source pointer for one normalized integration output.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "x-registry-source": { - "x-registry-field": "property", - "type": "string", - "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" - } - } - }, - "limits": { - "description": "Per-execution call, byte, and deadline budgets.", - "type": "object", - "additionalProperties": false, - "properties": { - "calls": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16 }, - "request_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationRequestByteSize" }, - "source_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationSourceByteSize" }, - "deadline": { - "x-registry-field": "property", - "description": "Positive total integration deadline no greater than 20 seconds.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - } -} diff --git a/projects/cra-civil/.registry-stack-editor/schemas/project.schema.json b/projects/cra-civil/.registry-stack-editor/schemas/project.schema.json deleted file mode 100644 index 91b7ea7..0000000 --- a/projects/cra-civil/.registry-stack-editor/schemas/project.schema.json +++ /dev/null @@ -1,537 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/project.v1.json", - "title": "Registry Stack project v1", - "description": "Declares the product-neutral Registry Stack project graph: integrations, materialized entities, and Relay or Notary services.", - "type": "object", - "additionalProperties": false, - "required": ["version", "registry", "services"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Project authoring format version." }, - "starter": { - "x-registry-field": "property", - "description": "Immutable provenance for a workspace initialized from a Registry Stack starter. The digest excludes this content_digest field and covers the starter's authored project files.", - "type": "object", - "additionalProperties": false, - "required": ["id", "release", "content_digest"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Registry Stack starter identifier." }, - "release": {"x-registry-field": "property", "$ref": "#/$defs/token", "description": "Registry Stack release that supplied the starter." }, - "content_digest": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^sha256:[0-9a-f]{64}$", "description": "Digest of the initialized starter authoring content, used to report later workspace divergence." } - } - }, - "registry": { - "x-registry-field": "property", - "description": "Stable identity of the Registry Stack project.", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } - }, - "integrations": { - "x-registry-field": "property", - "description": "Source adaptation definitions keyed by project-local integration identifier.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "entities": { - "x-registry-field": "property", - "description": "Materialized entity definitions keyed by project-local entity identifier.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "services": { - "x-registry-field": "property", - "description": "Relay records APIs and Notary evidence services exposed by the project.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/service" } - } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["integrations"], "properties": { "integrations": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["entities"], "properties": { "entities": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["services"], "properties": { "services": {"x-registry-field": "property", "minProperties": 1 } } } - ], - "examples": [ - { - "version": 1, - "registry": { "id": "benefits-registry" }, - "integrations": { - "household_lookup": { "file": "integrations/household-lookup/integration.yaml" } - }, - "services": {} - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Non-empty token without commas, whitespace, or control characters." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Project-relative path without traversal or duplicate separators." }, - "text": { "type": "string", "minLength": 1, "maxLength": 2048, "description": "Bounded human-readable project metadata." }, - "scope": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Authorization scope token." }, - "recordsApi": { - "description": "Relay records API behavior, authorization, query bounds, and standards profiles.", - "type": "object", - "additionalProperties": false, - "required": ["scopes", "projection", "pagination", "standards"], - "properties": { - "scopes": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["metadata", "rows"], - "properties": { - "metadata": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "rows": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "evidence_verification": {"x-registry-field": "property", "$ref": "#/$defs/scope" } - } - }, - "purposes": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } }, - "projection": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 256, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "pagination": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["default_limit", "max_limit"], - "properties": { - "default_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 }, - "max_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 } - } - }, - "filters": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 256, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } - }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "relationships": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["kind", "target", "foreign_key"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["belongs_to", "has_many", "has_one"] }, - "target": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "foreign_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "concept_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - } - }, - "aggregates": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAggregate" } - }, - "attribute_release_profiles": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseProfiles" }, - "standards": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["ogc_features", "sp_dci"], - "properties": { - "ogc_features": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpatial" }] }, - "sp_dci": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpdci" }] } - } - } - } - }, - "recordAttributeReleaseProfiles": { - "description": "Purpose-bound, exact-one identity releases keyed by stable profile id. Generated Relay responses omit source metadata and are never cacheable.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAttributeReleaseProfile" } - }, - "recordAttributeReleaseProfile": { - "description": "One purpose-bound, minimized identity release compiled into the owning Relay entity.", - "type": "object", - "additionalProperties": false, - "required": ["version", "purpose", "release_scope", "subject", "release_conditions", "claims"], - "properties": { - "version": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "release_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "subject": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["source_field", "id_type"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "id_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" } - } - }, - "release_conditions": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["expression"], - "properties": { "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" } } - }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["required", "sensitivity"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" }, - "required": {"x-registry-field": "property", "type": "boolean" }, - "sensitivity": {"x-registry-field": "property", "enum": ["direct_identifier", "personal", "public", "pseudonymous"] } - }, - "oneOf": [ - {"x-registry-field": "branch", "required": ["source_field"] }, - {"x-registry-field": "branch", "required": ["expression"] } - ] - } - } - } - }, - "recordAttributeReleaseExpression": { - "description": "Bounded CEL evaluated only against the projected source object.", - "type": "object", - "additionalProperties": false, - "required": ["cel"], - "properties": { "cel": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 4096 } } - }, - "filterOperators": { "type": "array", "minItems": 1, "uniqueItems": true, "description": "Allowed operators for one queryable field.", "items": {"x-registry-field": "array_item", "enum": ["eq", "in", "gte", "lte", "between"] } }, - "fieldList": { "type": "array", "maxItems": 16, "uniqueItems": true, "description": "Bounded unique list of entity field identifiers.", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "recordAggregate": { - "description": "Governed aggregate definition with disclosure controls.", - "type": "object", - "additionalProperties": false, - "required": ["description", "disclosure_control"], - "properties": { - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "default_group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "dimensions": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateDimension" } }, - "indicators": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateIndicator" } }, - "allowed_filters": {"x-registry-field": "property", "type": "object", "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "temporal_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateAccess" }, - "spatial": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateSpatial" }, - "joins": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "measures": {"x-registry-field": "property", "type": "array", "minItems": 1, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateMeasure" } }, - "disclosure_control": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "min_group_size": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "suppression": {"x-registry-field": "property", "enum": ["omit", "mask", "null"] } - } - } - }, - "anyOf": [{"x-registry-field": "branch", "required": ["measures"] }, {"x-registry-field": "branch", "required": ["indicators"] }] - }, - "recordAggregateAccess": { - "description": "Authorization scopes and execution policy for one governed aggregate.", - "type": "object", - "additionalProperties": false, - "properties": { - "metadata_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_only_execution": {"x-registry-field": "property", "type": "boolean" } - } - }, - "recordAggregateSpatial": { - "description": "Administrative-area geometry join used to spatially group aggregate results.", - "type": "object", - "additionalProperties": false, - "required": ["mode", "dimension", "geometry_entity", "geometry_id_field", "geometry_field"], - "properties": { - "mode": {"x-registry-field": "property", "const": "admin_area" }, - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "dimension": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_id_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatial": { - "description": "OGC API Features collection metadata and bounded geometry mapping.", - "type": "object", - "additionalProperties": false, - "required": ["geometry"], - "properties": { - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "geometry": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialGeometry" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "datetime_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_bbox_degrees": {"x-registry-field": "property", "type": "number", "exclusiveMinimum": 0 }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatialGeometry": { - "description": "Supported point or encoded-geometry field mapping for spatial records.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "longitude_field", "latitude_field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "const": "point" }, - "longitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "latitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["geojson", "wkt", "wkb"] }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - } - ] - }, - "recordSpatialBbox": { - "description": "Record fields carrying the four coordinates of a bounding box.", - "type": "object", - "additionalProperties": false, - "required": ["min_x", "min_y", "max_x", "max_y"], - "properties": { - "min_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "min_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordSpdci": { - "description": "SP-DCI registry identity and field mappings for a records service.", - "type": "object", - "additionalProperties": false, - "required": ["registry", "registry_type", "record_type", "identifiers", "expression_fields"], - "properties": { - "registry": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "registry_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "record_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "identifiers": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "expression_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "response_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" } - } - }, - "recordFieldMap": { - "description": "Bounded mapping from standard field names to entity field identifiers.", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } - }, - "recordAggregateDimension": { - "description": "Named grouping dimension backed by one entity field.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "field"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "codelist": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateIndicator": { - "description": "SDMX-style aggregate indicator with units and display metadata.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "function", "column", "unit_measure"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "unit_measure": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "unit_mult": {"x-registry-field": "property", "type": "integer", "minimum": -2147483648, "maximum": 2147483647 }, - "decimals": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 4294967295 }, - "frequency": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "definition_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateMeasure": { - "description": "Named aggregate calculation over one entity column.", - "type": "object", - "additionalProperties": false, - "required": ["name", "function", "column"], - "properties": { - "name": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordAggregateFunction": { - "description": "Supported aggregate calculation function.", - "enum": ["count", "sum", "avg", "min", "max", "median", "count_distinct", "stddev"] - }, - "disclosureMode": { "enum": ["value", "predicate", "redacted"], "description": "Maximum detail a claim may disclose." }, - "disclosure": { - "description": "Fixed disclosure mode or a default constrained by explicitly allowed alternatives.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/disclosureMode" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["default", "allowed"], - "properties": { - "default": {"x-registry-field": "property", "$ref": "#/$defs/disclosureMode" }, - "allowed": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/disclosureMode" } } - } - } - ] - }, - "service": { - "description": "Closed choice between a Notary evidence service and a Relay records service.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/evidenceService" }, - {"x-registry-field": "branch", "$ref": "#/$defs/recordsService" } - ] - }, - "recordsService": { - "description": "Relay records API backed by one authored materialized entity.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "entity", "api"], - "properties": { - "kind": {"x-registry-field": "property", "const": "records_api" }, - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "owner": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "sensitivity": {"x-registry-field": "property", "enum": ["public", "internal", "personal", "confidential", "secret"] }, - "access_rights": {"x-registry-field": "property", "enum": ["public", "restricted", "non_public"] }, - "update_frequency": {"x-registry-field": "property", "enum": ["continuous", "daily", "weekly", "termly", "monthly", "quarterly", "annual", "irregular", "as_needed", "unknown"] }, - "conforms_to": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/text" } }, - "api": {"x-registry-field": "property", "$ref": "#/$defs/recordsApi" } - } - }, - "evidenceService": { - "description": "Notary evidence policy, consultations, claims, and registry-backed credential profiles. Source-free claims remain evaluation-only and cannot be selected by a credential profile.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "version", "purpose", "legal_basis", "consent", "access", "claims"], - "properties": { - "kind": {"x-registry-field": "property", "const": "evidence" }, - "version": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "subject_type": { - "x-registry-field": "property", - "description": "Subject category evaluated by this evidence service. Omission is normalized to person.", - "enum": ["person", "project"] - }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "legal_basis": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "consent": {"x-registry-field": "property", "enum": ["not_required", "required"] }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/access" }, - "variables": {"x-registry-field": "property", "$ref": "#/$defs/variables" }, - "consultations": {"x-registry-field": "property", "$ref": "#/$defs/consultations" }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["disclosure"], - "properties": { "output": {"x-registry-field": "property", "type": "string" }, "cel": {"x-registry-field": "property", "type": "string", "minLength": 1 }, "value": {"x-registry-field": "property", "$ref": "#/$defs/claimValue" }, "disclosure": {"x-registry-field": "property", "$ref": "#/$defs/disclosure" } }, - "oneOf": [{"x-registry-field": "branch", "required": ["output"] }, {"x-registry-field": "branch", "required": ["cel"] }] - } - }, - "credential_profiles": { - "x-registry-field": "property", - "description": "Credential profiles may select only claims backed by an exact Relay consultation. Source-free claim evaluation cannot be exposed as credential capability.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["format", "type", "validity", "claims"], - "properties": { - "format": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "validity": {"x-registry-field": "property", "type": "string", "pattern": "^[1-9][0-9]*(?:s|m|h)$" }, - "claims": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } } - } - } - } - } - }, - "access": { - "description": "Scopes a caller must hold to use an evidence service.", - "type": "object", "additionalProperties": false, "required": ["scopes"], - "properties": { "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } } } - }, - "variables": { - "description": "Typed request variables exposed to evidence evaluation.", - "type": "object", "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["from", "type"], "properties": { "from": {"x-registry-field": "property", "type": "string", "pattern": "^request\\.variables\\.[a-z][a-z0-9._-]{0,95}$" }, "type": {"x-registry-field": "property", "const": "date" } } } - }, - "consultations": { - "description": "Relay consultations and their closed bindings to target request identifiers or caller-supplied target attributes.", - "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["integration", "input"], "properties": { "integration": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, "input": {"x-registry-field": "property", "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/targetRequestMapping" } } } } - }, - "targetRequestMapping": { - "description": "Closed target request binding. Attribute values are bounded typed caller-supplied context, not authenticated identifiers.", - "type": "string", - "maxLength": 128, - "pattern": "^request\\.target\\.(?:id|identifiers\\.[A-Za-z][A-Za-z0-9._-]{0,95}|attributes\\.[a-z][a-z0-9_]{0,63})$" - }, - "claimValue": { - "description": "Explicit claim value type, nullability, and encoded-size bound for source-free evaluation. A source-free claim cannot be selected by a credential profile.", - "type": "object", "additionalProperties": false, "required": ["type"], - "properties": { "type": {"x-registry-field": "property", "enum": ["boolean", "integer", "string", "date"] }, "nullable": {"x-registry-field": "property", "type": "boolean" }, "max_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 } } - } - } -} diff --git a/projects/cra-civil/.vscode/extensions.json b/projects/cra-civil/.vscode/extensions.json deleted file mode 100644 index 6098b6e..0000000 --- a/projects/cra-civil/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "redhat.vscode-yaml" - ] -} diff --git a/projects/cra-civil/.vscode/settings.json b/projects/cra-civil/.vscode/settings.json deleted file mode 100644 index db92a93..0000000 --- a/projects/cra-civil/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "yaml.schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } -} diff --git a/projects/cra-civil/.zed/settings.json b/projects/cra-civil/.zed/settings.json deleted file mode 100644 index fac07ac..0000000 --- a/projects/cra-civil/.zed/settings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } - } - } - } - } -} diff --git a/projects/cra-civil/entities/civil_people.yaml b/projects/cra-civil/entities/civil_people.yaml deleted file mode 100644 index 7580f97..0000000 --- a/projects/cra-civil/entities/civil_people.yaml +++ /dev/null @@ -1,28 +0,0 @@ -version: 1 -id: civil_people -revision: 1 -primary_key: uin -schema: - type: object - additionalProperties: false - required: [uin, birth_date, birth_brn, deceased] - properties: - uin: - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - birth_date: - type: string - format: date - maxLength: 10 - birth_brn: - type: [string, "null"] - maxLength: 32 - deceased: - type: boolean -materialization: - max_records: 1000000 - max_bytes: 256MiB - refresh: 1m - retain_generations: 2 diff --git a/projects/cra-civil/environments/hosted.yaml b/projects/cra-civil/environments/hosted.yaml deleted file mode 100644 index 788a555..0000000 --- a/projects/cra-civil/environments/hosted.yaml +++ /dev/null @@ -1,25 +0,0 @@ -version: 1 -entities: - civil_people: - provider: - type: csv - path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv - header_row: 1 - columns: - uin: uin - birth_date: birth_date - birth_brn: birth_brn - deceased: deceased - source_revision: cra-civil-projection-v1 - generation: 2026-07-14 -relay: - origin: https://cra-relay.solmara.registrystack.org - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } -deployment: - profile: hosted_lab - relay: { service: cra-civil-relay } diff --git a/projects/cra-civil/environments/local.yaml b/projects/cra-civil/environments/local.yaml deleted file mode 100644 index 91d8096..0000000 --- a/projects/cra-civil/environments/local.yaml +++ /dev/null @@ -1,25 +0,0 @@ -version: 1 -entities: - civil_people: - provider: - type: csv - path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv - header_row: 1 - columns: - uin: uin - birth_date: birth_date - birth_brn: birth_brn - deceased: deceased - source_revision: cra-civil-projection-v1 - generation: 2026-07-14 -relay: - origin: http://127.0.0.1:8080 - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } -deployment: - profile: local - relay: { service: cra-civil-relay } diff --git a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/future-birth-date.yaml b/projects/cra-civil/integrations/civil-person-by-uin/fixtures/future-birth-date.yaml deleted file mode 100644 index a3e6d95..0000000 --- a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/future-birth-date.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: cra-civil-person-future-birth-date -classification: synthetic -input: { uin: "2300010248" } -variables: { as_of_date: 2026-07-14 } -interactions: - - expect: { method: GET, path: /snapshot } - respond: - status: 200 - body: - birth_date: 2027-03-14 - birth_brn: BRN-2027-0101-00001 - deceased: false -expect: - outcome: match - outputs: - birth_date: 2027-03-14 - birth_brn: BRN-2027-0101-00001 - deceased: false diff --git a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/match.yaml b/projects/cra-civil/integrations/civil-person-by-uin/fixtures/match.yaml deleted file mode 100644 index 3793df9..0000000 --- a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/match.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: cra-civil-person-match -classification: synthetic -input: { uin: "2300010248" } -variables: { as_of_date: 2026-07-14 } -interactions: - - expect: { method: GET, path: /snapshot } - respond: - status: 200 - body: - birth_date: 2022-03-14 - birth_brn: BRN-2022-0101-00001 - deceased: false -expect: - outcome: match - outputs: - birth_date: 2022-03-14 - birth_brn: BRN-2022-0101-00001 - deceased: false diff --git a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/no-match.yaml b/projects/cra-civil/integrations/civil-person-by-uin/fixtures/no-match.yaml deleted file mode 100644 index 0870844..0000000 --- a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/no-match.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: cra-civil-person-no-match -classification: synthetic -input: { uin: "9999999999" } -variables: { as_of_date: 2026-07-14 } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: [] } -expect: - outcome: no_match - outputs: {} diff --git a/projects/cra-civil/integrations/civil-person-by-uin/integration.yaml b/projects/cra-civil/integrations/civil-person-by-uin/integration.yaml deleted file mode 100644 index dd13213..0000000 --- a/projects/cra-civil/integrations/civil-person-by-uin/integration.yaml +++ /dev/null @@ -1,28 +0,0 @@ -version: 1 -id: solmara-cra-civil-person-by-uin -revision: 1 - -input: - uin: - role: selector - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - -capability: - snapshot: - entity: civil_people - exact: - uin: { input: uin } - freshness: 24h - -outputs: [birth_date, birth_brn, deceased] - -not_applicable: - ambiguity: - rationale: The exact UIN selector is the entity primary key, whose materialized unique-key constraint permits at most one civil record. - request_fixture: cra-civil-person-match - subject_mismatch: - rationale: The minimized output projection omits UIN, so it contains no identifier comparable with the requested UIN. - request_fixture: cra-civil-person-match diff --git a/projects/cra-civil/registry-stack.yaml b/projects/cra-civil/registry-stack.yaml deleted file mode 100644 index ee617d8..0000000 --- a/projects/cra-civil/registry-stack.yaml +++ /dev/null @@ -1,50 +0,0 @@ -version: 1 - -registry: - id: solmara-cra-civil - -integrations: - civil-person-by-uin: - file: integrations/civil-person-by-uin/integration.yaml - -entities: - civil_people: - file: entities/civil_people.yaml - -services: - cra-civil-records: - kind: records_api - entity: civil_people - title: CRA civil records - description: Purpose-limited civil facts used by the Solmara Evidence service. - owner: Civil Registration Authority - sensitivity: personal - access_rights: restricted - update_frequency: continuous - api: - scopes: - metadata: civil_people:metadata - rows: civil_people:rows - aggregate: civil_people:aggregate - evidence_verification: civil_people:evidence_verification - purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/pension-payment-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - projection: [uin, birth_date, birth_brn, deceased] - pagination: { default_limit: 2, max_limit: 2 } - filters: - uin: [eq] - standards: { ogc_features: false, sp_dci: false } - - cra-civil-consultation: - kind: consultation_api - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - legal_basis: child-benefit-act - consent: not_required - consultations: - civil: - integration: civil-person-by-uin - input: - uin: request.target.identifiers.solmara_uin diff --git a/projects/mosd-programme/.registry-stack-editor/manifest.json b/projects/mosd-programme/.registry-stack-editor/manifest.json deleted file mode 100644 index 3603a8e..0000000 --- a/projects/mosd-programme/.registry-stack-editor/manifest.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "format": "registry.stack.editor-manifest", - "version": 1, - "registryctl_version": "0.15.2", - "schemas": [ - { - "kind": "project", - "path": "schemas/project.schema.json", - "file_glob": "registry-stack.yaml", - "sha256": "sha256:9fd76663c866c52bb5a17a4cfea821049b7428c812f1666da9db0d43a00da144" - }, - { - "kind": "environment", - "path": "schemas/environment.schema.json", - "file_glob": "environments/*.yaml", - "sha256": "sha256:d16828629e001a30a1705af90bcc12a214be18cf5805a9833e65b03d631c9e9b" - }, - { - "kind": "integration", - "path": "schemas/integration.schema.json", - "file_glob": "integrations/*/integration.yaml", - "sha256": "sha256:2e45452f63c3c610ed14b79244bc9aa086d79d811ad0899223b36c160d449a79" - }, - { - "kind": "fixture", - "path": "schemas/fixture.schema.json", - "file_glob": "integrations/*/fixtures/*.yaml", - "sha256": "sha256:98c0ab21e35cecd143b51e4b5dd487de07ceea24e505bbe351d212e0628c8c8c" - }, - { - "kind": "entity", - "path": "schemas/entity.schema.json", - "file_glob": "entities/*.yaml", - "sha256": "sha256:4cbb31c8addeb54f8b57aaaff8f30c39e651a21a2d741a1b775ecb0d7c248342" - } - ] -} diff --git a/projects/mosd-programme/.registry-stack-editor/schemas/entity.schema.json b/projects/mosd-programme/.registry-stack-editor/schemas/entity.schema.json deleted file mode 100644 index ffc57c8..0000000 --- a/projects/mosd-programme/.registry-stack-editor/schemas/entity.schema.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/entity.v1.json", - "title": "Registry Stack project entity v1", - "description": "Defines a bounded, materialized entity that Relay can query without coupling the project to a specific source product.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "primary_key", "schema", "materialization"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Entity authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the entity." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this entity definition.", "examples": [1] }, - "primary_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Field whose value uniquely identifies each materialized record.", "examples": ["person_id"] }, - "schema": {"x-registry-field": "property", "$ref": "#/$defs/objectSchema", "description": "Closed JSON object schema for materialized records." }, - "materialization": { - "x-registry-field": "property", - "description": "Authored resource, refresh, and bounded recovery-set retention applied while building entity generations.", - "type": "object", - "additionalProperties": false, - "required": ["max_records", "max_bytes", "refresh", "retain_generations"], - "properties": { - "max_records": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 100000000, "description": "Maximum number of records allowed in one generation.", "examples": [10000] }, - "max_bytes": { - "x-registry-field": "property", - "description": "Maximum encoded size of one generation, up to 1 GiB, as bytes or a KiB/MiB quantity.", - "examples": ["16MiB"], - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1073741824 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,5}|10[0-3][0-9]{4}|104[0-7][0-9]{3}|1048[0-4][0-9]{2}|10485[0-6][0-9]|104857[0-6])KiB|(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])MiB)$" } - ] - }, - "refresh": { - "x-registry-field": "property", - "description": "Refresh cadence as a canonical positive duration no greater than 30 days, or manual when an operator initiates every refresh.", - "examples": ["1h"], - "oneOf": [ - {"x-registry-field": "branch", "const": "manual" }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-4][0-9]{8}|25[0-8][0-9]{7}|259[0-1][0-9]{6}|2592000000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-4][0-9]{5}|25[0-8][0-9]{4}|259[0-1][0-9]{3}|2592000)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-2][0-9]{3}|43[0-1][0-9]{2}|43200)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-1][0-9]{1}|720)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30)d)$" } - ] - }, - "retain_generations": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16, "description": "Number of completed cache generations, including the active generation, retained as a bounded recovery set after successful publication. This does not make arbitrary retained generations selectable for rollback.", "examples": [2] } - } - } - }, - "examples": [ - { - "version": 1, - "id": "people", - "revision": 1, - "primary_key": "person_id", - "schema": { - "type": "object", - "additionalProperties": false, - "required": ["person_id"], - "properties": { "person_id": { "type": "string" } } - }, - "materialization": { - "max_records": 10000, - "max_bytes": "16MiB", - "refresh": "1h", - "retain_generations": 2 - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "propertyName": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$", "description": "Portable lowercase record field name." }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for a nullable field.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "items": false, - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "fieldSchema": { - "description": "Bounded schema keywords supported for one scalar materialized field.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "enum": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true }, - "const": {"x-registry-field": "property"}, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 65536 }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 } - } - }, - "objectSchema": { - "description": "Closed object schema whose declared required fields must appear in properties.", - "type": "object", - "additionalProperties": false, - "required": ["type", "additionalProperties", "required", "properties"], - "properties": { - "type": {"x-registry-field": "property", "const": "object" }, - "additionalProperties": {"x-registry-field": "property", "const": false }, - "required": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 256, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/propertyName" } - }, - "properties": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 256, - "propertyNames": {"x-registry-field": "property", "$ref": "#/$defs/propertyName" }, - "additionalProperties": {"x-registry-field": "property", "$ref": "#/$defs/fieldSchema" } - } - } - } - } -} diff --git a/projects/mosd-programme/.registry-stack-editor/schemas/environment.schema.json b/projects/mosd-programme/.registry-stack-editor/schemas/environment.schema.json deleted file mode 100644 index f04c70b..0000000 --- a/projects/mosd-programme/.registry-stack-editor/schemas/environment.schema.json +++ /dev/null @@ -1,337 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/environment.v1.json", - "title": "Registry Stack project environment v1", - "description": "Binds a Registry Stack project to environment-specific services, sources, credentials, and deployment topology.", - "type": "object", - "additionalProperties": false, - "required": ["version", "deployment"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Environment authoring format version." }, - "integrations": {"x-registry-field": "property", "type": "object", "maxProperties": 16, "description": "Environment bindings keyed by authored integration identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/integration" } }, - "entities": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "description": "Environment-specific source bindings keyed by entity identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/entity" } }, - "issuance": { - "x-registry-field": "property", - "description": "Notary issuer and signing-key bindings for credential issuance.", - "type": "object", "additionalProperties": false, "required": ["issuer", "signing_key", "signing_kid", "generation"], - "properties": { "issuer": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1 }, "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" }, "algorithm": {"x-registry-field": "property", "enum": ["EdDSA", "ES256"], "default": "EdDSA", "description": "Credential issuer signing algorithm. Holder proof remains EdDSA with did:jwk." }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "callers": { - "x-registry-field": "property", - "description": "Notary API-key caller identities and the scopes each identity receives.", - "type": "object", "maxProperties": 64, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["api_key_fingerprint", "scopes"], - "properties": { "api_key_fingerprint": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } - } - }, - "relay": { - "x-registry-field": "property", - "description": "Public Relay identity and token-validation settings for a Relay deployment.", - "type": "object", "additionalProperties": false, - "required": ["origin", "issuer", "jwks_url", "audience", "allowed_clients"], - "properties": { "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "allowed_clients": {"x-registry-field": "sensitive_property", "type": "array", "maxItems": 64, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } }, "local_api_keys": {"x-registry-field": "property", "description": "Local-profile-only synthetic principals and scopes. Registryctl generates the raw keys and supplies only their fingerprints to Relay.", "type": "object", "additionalProperties": false, "required": ["match_principal", "no_match_principal", "scopes"], "properties": { "match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "no_match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } } } - }, - "notary_relay": { - "x-registry-field": "property", - "description": "Deployment-internal Relay connection, Notary workload identity, and token file used only when Notary consults Relay.", - "type": "object", "additionalProperties": false, "required": ["base_url", "workload_client_id", "token_file"], - "properties": { "base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/internalOrigin" }, "workload_client_id": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "token_file": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" } } - }, - "relay_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Relay-owned consultation PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Notary-owned PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_cel": { - "x-registry-field": "property", - "description": "Optional per-worker data/address-space ceiling for dedicated Notary CEL processes. The Notary default is 134217728 bytes; 1073741824 is the maximum emulation-compatible exception and remains a limit, not an allocation.", - "type": "object", "additionalProperties": false, "required": ["worker_memory_bytes"], - "properties": { - "worker_memory_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 33554432, "maximum": 1073741824 } - } - }, - "oid4vci": { - "x-registry-field": "property", - "description": "Explicit registry-backed holder-wallet OID4VCI binding for one authored Notary credential profile.", - "$ref": "#/$defs/oid4vci" - }, - "deployment": { - "x-registry-field": "property", - "description": "Products deployed in this environment and the operational profile they use.", - "type": "object", "additionalProperties": false, "required": ["profile"], - "properties": { "profile": {"x-registry-field": "property", "enum": ["local", "hosted_lab", "production", "evidence_grade"] }, "relay": {"x-registry-field": "property", "$ref": "#/$defs/service" }, "notary": {"x-registry-field": "property", "$ref": "#/$defs/service" } }, - "anyOf": [{"x-registry-field": "branch", "required": ["relay"] }, {"x-registry-field": "branch", "required": ["notary"] }] - } - }, - "allOf": [ - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["deployment"], "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } }, - "then": {"x-registry-field": "branch", "required": ["relay"] }, - "else": {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "required": ["relay"] } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_relay"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay", "notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["relay_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_cel"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["oid4vci"] }, - "then": { - "x-registry-field": "branch", - "required": ["notary_state"], - "properties": { - "deployment": {"x-registry-field": "property", "required": ["notary"] } - } - }, - "else": {"x-registry-field": "branch", "properties": { "callers": {"x-registry-field": "property", "minProperties": 1 } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["profile"], "properties": { "profile": {"x-registry-field": "property", "const": "local" } } } } }, - "else": { - "x-registry-field": "branch", - "properties": { - "relay": { - "x-registry-field": "property", - "not": {"x-registry-field": "branch", "required": ["local_api_keys"] }, - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - }, - "oid4vci": { - "x-registry-field": "property", - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorization_server": { - "x-registry-field": "property", - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - } - } - } - } - } - } - ], - "examples": [ - { - "version": 1, - "deployment": { - "profile": "local", - "notary": { "service": "notary" } - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token2048": { "type": "string", "minLength": 1, "maxLength": 2048, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII token, including full verification-method identifiers." }, - "secret": { "type": "object", "additionalProperties": false, "description": "Reference to a process-environment variable; secret values are never authored here.", "required": ["secret"], "properties": { "secret": {"x-registry-field": "secret_reference_property", "type": "string", "pattern": "^[A-Z_][A-Z0-9_]{0,127}$" } } }, - "origin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/?$", "description": "HTTPS origin without path, query, or fragment." }, - "httpsResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/[^?#]+$", "description": "Exact HTTPS resource without query or fragment." }, - "localLoopbackOrigin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/?$", "description": "HTTP IP-loopback origin; public Relay and issuer fields restrict it to the local profile, while internal Notary-to-Relay connections allow it in any profile." }, - "localLoopbackResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/[^?#]+$", "description": "HTTP IP-loopback resource accepted only with the local deployment profile." }, - "internalOrigin": { "description": "Deployment-internal HTTPS or HTTP IP-loopback origin.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayOrigin": { "description": "Relay or issuer origin using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayResource": { "description": "Relay authentication resource using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/httpsResource" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackResource" }] }, - "oid4vci": { - "description": "Closed OID4VCI trust, key, subject, wallet, and credential-profile binding.", - "type": "object", - "additionalProperties": false, - "required": ["public_base_url", "credential", "authorization_server", "client", "access_token", "sensitive_state_key", "subject", "redirect_uri", "allowed_wallet_origins"], - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "credential": { - "x-registry-field": "property", - "description": "Existing project service and credential profile exposed through OID4VCI.", - "type": "object", "additionalProperties": false, "required": ["service", "profile"], - "properties": { - "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "profile": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "authorization_server": { - "x-registry-field": "property", - "description": "Pinned eSignet issuer and exact endpoints used for login and token validation.", - "type": "object", "additionalProperties": false, "required": ["issuer", "jwks_url", "userinfo_url", "authorize_url", "token_url"], - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" } - } - }, - "client": { - "x-registry-field": "property", - "description": "eSignet relying-party identity and dedicated private-key reference.", - "type": "object", "additionalProperties": false, "required": ["id", "signing_key", "signing_kid"], - "properties": { - "id": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "access_token": { - "x-registry-field": "property", - "description": "Dedicated Notary access-token signing key and published key identifier.", - "type": "object", "additionalProperties": false, "required": ["signing_key", "signing_kid"], - "properties": { - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "sensitive_state_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "subject": { - "x-registry-field": "property", - "description": "Verified eSignet userinfo claim bound exactly to the credential subject identifier.", - "type": "object", "additionalProperties": false, "required": ["token_claim", "id_type"], - "properties": { - "token_claim": {"x-registry-field": "property", "$ref": "#/$defs/token256" }, - "id_type": {"x-registry-field": "property", "$ref": "#/$defs/token256" } - } - }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "allowed_wallet_origins": { - "x-registry-field": "sensitive_property", - "description": "Exact HTTPS browser origins admitted to the wallet-facing Notary routes.", - "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "$ref": "#/$defs/origin" } - }, - "tx_code": { - "x-registry-field": "property", - "description": "Transaction-code policy. Omit for the secure required-PIN default. Set required=false only for a bounded bearer-offer interoperability profile; the compiler fixes the offer lifetime at 300 seconds.", - "type": "object", "additionalProperties": false, - "properties": { "required": {"x-registry-field": "property", "type": "boolean", "default": true } } - } - } - }, - "privateCidrs": { - "description": "Explicit private network ranges this source may resolve to.", - "type": "array", "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "type": "string", "minLength": 3, "maxLength": 64 } - }, - "ca": { - "description": "Pinned certificate-authority file and its rotation generation.", - "type": "object", "additionalProperties": false, "required": ["file", "generation"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "mtls": { - "description": "Client certificate and private-key reference for mutual TLS.", - "type": "object", "additionalProperties": false, "required": ["certificate_file", "private_key", "generation"], - "properties": { "certificate_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "private_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "endpoint": { - "description": "Security-bound HTTPS endpoint with optional private-network, CA, and mTLS policy.", - "type": "object", "additionalProperties": false, "required": ["origin", "path", "generation"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//)(?!.*[?#]).+$" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } - } - }, - "credential": { - "description": "One supported source credential shape, containing references rather than values.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["username", "password", "generation"], "properties": { "username": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "password": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["token", "generation"], "properties": { "token": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["client_id", "client_secret", "generation"], "properties": { "client_id": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "client_secret": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["value", "generation"], "properties": { "value": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } } - ] - }, - "provider": { - "description": "Environment-specific physical provider for a materialized entity.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "csv" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "delimiter": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 }, "quote": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "project_file", "path", "sheet"], "properties": { "type": {"x-registry-field": "property", "const": "xlsx" }, "project_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "sheet": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "data_range": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "parquet" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "connection", "schema", "table"], "properties": { "type": {"x-registry-field": "property", "const": "postgres" }, "connection": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "schema": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" }, "table": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" } } } - ] - }, - "entity": { - "description": "Maps an authored entity to provider columns and immutable source-generation identifiers.", - "type": "object", "additionalProperties": false, "required": ["provider", "columns", "source_revision", "generation"], - "properties": { "provider": {"x-registry-field": "property", "$ref": "#/$defs/provider" }, "columns": {"x-registry-field": "property", "type": "object", "minProperties": 1, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } }, "source_revision": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "generation": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 } } - }, - "source": { - "description": "Runtime source connection policy for one authored integration.", - "type": "object", "additionalProperties": false, "required": ["origin"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "credential": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "oauth": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "jwks": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "rate": {"x-registry-field": "property", "type": "object", "additionalProperties": false, "required": ["per_minute", "burst"], "properties": { "per_minute": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 60000 }, "burst": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 1024 } } }, - "concurrency": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 64 }, - "timeout": { - "x-registry-field": "property", - "description": "Optional request timeout no greater than 20 seconds; generated bindings narrow it to the integration deadline.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - }, - "integration": { - "description": "Environment binding for an authored source integration.", - "type": "object", "additionalProperties": false, "required": ["source"], - "properties": { "source": {"x-registry-field": "property", "$ref": "#/$defs/source" } } - }, - "service": { "type": "object", "additionalProperties": false, "description": "Binds an authored product role to a deployed service identifier.", "required": ["service"], "properties": { "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } }, - "token256": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII protocol token." }, - "postgresIdentifier": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,62}$", "description": "Portable unquoted PostgreSQL schema or table identifier." }, - "absolutePath": { "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized absolute path without dot segments or duplicate separators." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized project-relative path without dot segments or duplicate separators." } - } -} diff --git a/projects/mosd-programme/.registry-stack-editor/schemas/fixture.schema.json b/projects/mosd-programme/.registry-stack-editor/schemas/fixture.schema.json deleted file mode 100644 index b853192..0000000 --- a/projects/mosd-programme/.registry-stack-editor/schemas/fixture.schema.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/fixture.v1.json", - "title": "Registry Stack project integration fixture v1", - "description": "Defines one synthetic, deterministic integration scenario for offline adapter verification.", - "type": "object", - "additionalProperties": false, - "required": ["name", "classification", "input", "interactions", "expect"], - "properties": { - "name": {"x-registry-field": "public_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Human-readable scenario name shown in test output.", "examples": ["existing household matches"] }, - "classification": {"x-registry-field": "public_property", "const": "synthetic", "description": "Declares that fixture data is synthetic and safe for offline testing." }, - "request": { - "x-registry-field": "redacted_fixture_property", - "$ref": "#/$defs/governedRequest", - "description": "Optional independently authored synthetic Notary request used to prove the request-to-consultation binding before Relay access." - }, - "input": { - "x-registry-field": "redacted_fixture_property", - "description": "Typed consultation inputs supplied to the adapter under test.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "type": ["string", "boolean", "integer", "null"] - } - }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Named date values available to deterministic fixture interpolation.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "interactions": { - "x-registry-field": "redacted_fixture_property", - "description": "Ordered upstream request and response exchanges expected during execution.", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/interaction" } - }, - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/expectation", "description": "Observable adapter result required for the scenario to pass." } - }, - "examples": [ - { - "name": "existing household matches", - "classification": "synthetic", - "input": { "household_id": "HH-1001" }, - "interactions": [ - { - "expect": { "method": "GET", "path": "/households/HH-1001" }, - "respond": { "status": 200, "body": { "active": true } } - } - ], - "expect": { "outcome": "match", "outputs": { "active": true } } - } - ], - "$defs": { - "governedRequest": { - "description": "The same closed governed request shape accepted by project test --live.", - "type": "object", - "additionalProperties": false, - "required": ["target", "claims", "purpose"], - "properties": { - "target": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/governedTarget" }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies synthetic date variables to the governed evaluation request.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "claims": { - "x-registry-field": "redacted_fixture_property", - "description": "Lists the authored claims evaluated by this synthetic request witness.", - "type": "array", - "minItems": 1, - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedClaimRef" } - }, - "disclosure": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the disclosure mode requested from the authored claim policy." }, - "format": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the claim-result media type requested from the governed Notary path." }, - "purpose": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Names the authored service purpose exercised by this synthetic request witness." } - } - }, - "governedTarget": { - "description": "The synthetic subject target presented to the same governed request boundary as a live Notary evaluation.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 64, "description": "Names the authored target entity type used for claim evaluation." }, - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies an optional synthetic direct target identifier." }, - "identifiers": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored synthetic identifiers for consultation input mapping.", - "type": "array", - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedIdentifier" } - }, - "attributes": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored typed target attributes for consultation input mapping.", - "type": "object", - "maxProperties": 16, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": ["string", "boolean", "integer", "null"] } - } - } - }, - "governedIdentifier": { - "description": "One synthetic target identifier with an authored scheme and string value.", - "type": "object", - "additionalProperties": false, - "required": ["scheme", "value"], - "properties": { - "scheme": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 96, "description": "Names the authored identifier scheme selected by a consultation input mapping." }, - "value": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies the synthetic string value bound to this identifier scheme." } - } - }, - "governedClaimRef": { - "description": "A requested claim ID, optionally pinned to one authored claim version.", - "oneOf": [ - {"x-registry-field": "branch", "type": "string"}, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Names one authored claim requested by this fixture witness."}, - "version": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Pins the requested claim to one authored claim-policy version."} - } - } - ] - }, - "interaction": { - "description": "One expected upstream request paired with its synthetic response.", - "type": "object", - "additionalProperties": false, - "required": ["expect", "respond"], - "properties": { - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/request" }, - "respond": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/response" } - } - }, - "request": { - "description": "Exact HTTP request shape the adapter must produce.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "redacted_fixture_property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "query": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 64, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "oneOf": [ - {"x-registry-field": "branch", "type": ["string", "boolean", "integer", "null"] }, - { - "x-registry-field": "branch", - "type": "array", - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "type": ["string", "boolean", "integer", "null"] } - } - ] - } - }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - "response": { - "description": "Synthetic HTTP response or timeout returned to the adapter.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["status"], - "properties": { - "status": {"x-registry-field": "redacted_fixture_property", "type": "integer", "minimum": 100, "maximum": 599 }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["timeout"], - "properties": { - "timeout": { - "x-registry-field": "redacted_fixture_property", - "description": "Reviewed synthetic timeout intent no greater than 20 seconds; offline execution returns a timeout outcome without sleeping.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - ] - }, - "fixtureBody": { - "description": "Inline JSON, or an exact closed reference to a strict JSON file below the fixture bodies directory. The top-level file key is reserved for file references.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": { - "x-registry-field": "redacted_fixture_property", - "type": "string", - "minLength": 8, - "maxLength": 4096, - "pattern": "^bodies/(?!\\.\\.?/)(?!.*(?:/)\\.\\.?/)(?!.*//).+$" - } - } - }, - { - "x-registry-field": "branch", - "not": { - "x-registry-field": "branch", - "type": "object", - "required": ["file"] - } - } - ] - }, - "expectation": { - "description": "Expected normalized outcome, output values, claims, or error.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "outcome": {"x-registry-field": "redacted_fixture_property", "enum": ["match", "no_match", "ambiguous"] }, - "outputs": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "claims": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "error": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256 } - } - } - } -} diff --git a/projects/mosd-programme/.registry-stack-editor/schemas/integration.schema.json b/projects/mosd-programme/.registry-stack-editor/schemas/integration.schema.json deleted file mode 100644 index 9bb2ad0..0000000 --- a/projects/mosd-programme/.registry-stack-editor/schemas/integration.schema.json +++ /dev/null @@ -1,514 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/integration.v1.json", - "title": "Registry Stack project integration v1", - "description": "Defines a product-neutral source adaptation contract using bounded HTTP, Rhai script, or exact snapshot capabilities.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "input", "capability", "outputs"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Integration authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the integration." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this integration contract.", "examples": [1] }, - "source": {"x-registry-field": "property", "$ref": "#/$defs/source", "description": "Optional source product metadata and transport policy; capabilities remain product-neutral." }, - "input": { - "x-registry-field": "property", - "description": "Typed selector and parameter inputs accepted by this integration.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/input" } - }, - "capability": {"x-registry-field": "property", "$ref": "#/$defs/capability", "description": "Exactly one bounded execution mechanism for the source adaptation." }, - "outputs": { - "x-registry-field": "property", - "description": "Named typed outputs, or a shorthand list of output names inferred by the authoring compiler.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": { - "x-registry-field": "map_key", - "allOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputName" }, - {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "enum": ["matched", "outcome"] } } - ] - }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/output" } - }, - { - "x-registry-field": "branch", - "type": "array", - "minItems": 1, - "maxItems": 64, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/inputName" } - } - ] - }, - "limits": {"x-registry-field": "property", "$ref": "#/$defs/limits", "description": "Optional tighter resource limits for one integration execution." }, - "not_applicable": {"x-registry-field": "property", "$ref": "#/$defs/notApplicable", "description": "Explicit rationale and request-fixture evidence for a normally required outcome that the source contract cannot produce." } - }, - "examples": [ - { - "version": 1, - "id": "household_lookup", - "revision": 1, - "input": { - "household_id": { "role": "selector", "type": "string" } - }, - "capability": { - "script": { "file": "adapter.rhai" } - }, - "outputs": { - "active": { "type": "boolean" } - } - } - ], - "$defs": { - "notApplicable": { - "description": "Closed declarations for outcomes that the source protocol, exact singleton endpoint, or local unique-key constraint proves impossible.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "ambiguity": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed source contract cannot produce more than one matching record." }, - "subject_mismatch": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed response contract contains no identifier comparable with a requested selector." } - } - }, - "notApplicableReason": { - "description": "A reviewable reason that names a real request fixture proving the source access shape used by the rationale.", - "type": "object", - "additionalProperties": false, - "required": ["rationale", "request_fixture"], - "properties": { - "rationale": {"x-registry-field": "property", "type": "string", "minLength": 24, "maxLength": 512, "description": "Why the named normally required outcome cannot occur for this source contract." }, - "request_fixture": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Fixture name containing the supporting bounded source request." } - } - }, - "stableId": { - "description": "Lowercase stable identifier used in project references.", - "type": "string", - "pattern": "^[a-z][a-z0-9._-]{0,95}$" - }, - "inputName": { - "description": "Portable lowercase name for an integration input or output.", - "type": "string", - "pattern": "^[a-z][a-z0-9_]{0,63}$" - }, - "relativePath": { - "description": "Project-relative path without traversal or duplicate separators.", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" - }, - "integrationResponseByteSize": { - "description": "Maximum bytes accepted from one source response, defaulting to 512 KiB and capped at 8 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "512KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 8388608 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|[1-7][0-9]{3}|80[0-9]{2}|81[0-8][0-9]|819[0-2])KiB|[1-8]MiB)$" } - ] - }, - "integrationRequestByteSize": { - "description": "Maximum aggregate author-controlled request bytes for one execution, defaulting to 64 KiB and capped at 1 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "64KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1048576 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])KiB|1MiB)$" } - ] - }, - "integrationSourceByteSize": { - "description": "Maximum aggregate source-response bytes for one execution, defaulting to 2 MiB and capped at 16 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "2MiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 16777216 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-5][0-9]{3}|16[0-2][0-9]{2}|163[0-7][0-9]|1638[0-4])KiB|(?:[1-9]|1[0-6])MiB)$" } - ] - }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for nullable values.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "input": { - "description": "Type, validation, and canonicalization rules for one authored input.", - "type": "object", - "additionalProperties": false, - "required": ["role", "type"], - "properties": { - "role": {"x-registry-field": "property", "enum": ["selector", "parameter"] }, - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "description": "Maximum Unicode scalar-value length. The 1024-character ceiling derives a conservative 4096-byte UTF-8 ceiling for one canonical input.", "type": "integer", "minimum": 1, "maximum": 1024 }, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 16384 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "canonicalization": {"x-registry-field": "property", "enum": ["identity", "ascii_lowercase"] } - } - }, - "versions": { - "description": "Source versions tested by the project and versions explicitly accepted as unverified.", - "type": "object", - "additionalProperties": false, - "properties": { - "tested": {"x-registry-field": "property", "$ref": "#/$defs/versionList" }, - "unverified": {"x-registry-field": "property", "$ref": "#/$defs/versionList" } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["tested"], "properties": { "tested": {"x-registry-field": "property", "minItems": 1 } } }, - {"x-registry-field": "branch", "required": ["unverified"], "properties": { "unverified": {"x-registry-field": "property", "minItems": 1 } } } - ] - }, - "versionList": { - "description": "Bounded list of source version labels.", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } - }, - "credential": { - "description": "Authentication mechanism expected by the source adapter, without environment secret values.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "enum": ["none", "basic", "static_bearer"] } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_header" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_query" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9._:~-]{0,95}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "request", "response_profile"], - "properties": { - "type": {"x-registry-field": "property", "const": "oauth2_client_credentials" }, - "request": {"x-registry-field": "property", "enum": ["form", "json"] }, - "response_profile": {"x-registry-field": "property", "const": "oauth2_bearer" }, - "scope": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 }, - "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "refresh_skew": { - "x-registry-field": "property", - "description": "Positive OAuth refresh interval below the generated token contract's 60-second minimum lifetime.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|[1-5][0-9]{4})ms|(?:[1-9]|[1-5][0-9])s)$" - } - } - } - ] - }, - "allowRule": { - "description": "One read-only upstream method and path pattern the adapter may call.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": { - "x-registry-field": "sensitive_property", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^/[^?#]*$" - }, - "semantics": {"x-registry-field": "property", "const": "read_only" } - } - }, - "source": { - "description": "Optional source metadata, authentication shape, allowlist, and protocol profile.", - "type": "object", - "additionalProperties": false, - "required": ["auth"], - "properties": { - "product": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "versions": {"x-registry-field": "property", "$ref": "#/$defs/versions" }, - "auth": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "allow": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/allowRule" } - }, - "request_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "format": {"x-registry-field": "property", "enum": ["json", "text"] }, - "max_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationResponseByteSize" } - } - }, - "protocol": {"x-registry-field": "property", "$ref": "#/$defs/protocol" } - } - }, - "protocol": { - "description": "Optional interoperable protocol profiles layered over the source transport.", - "type": "object", - "additionalProperties": false, - "properties": { - "signed_dci": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["profile", "path", "jwks_profile", "sender", "receiver", "registry_type", "record_type", "locale", "selectors"], - "properties": { - "profile": {"x-registry-field": "property", "const": "dci-search-v1" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^/[^?#]*$" }, - "jwks_profile": {"x-registry-field": "property", "const": "rsa-signing-jwks-v1" }, - "sender": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "receiver": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "registry_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "record_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "locale": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$" }, - "selectors": { - "x-registry-field": "property", - "type": "object", "minProperties": 1, "maxProperties": 8, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["field", "response_pointer"], - "properties": { - "field": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 160 }, - "response_pointer": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" } - } - } - } - } - } - }, - "minProperties": 1 - }, - "inputReference": { - "description": "Reference to a declared integration input.", - "type": "object", - "additionalProperties": false, - "required": ["input"], - "properties": { "input": {"x-registry-field": "property", "$ref": "#/$defs/inputName" } } - }, - "inputOrLiteral": { - "description": "Request value supplied from a declared input or an authored scalar literal.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputReference" }, - {"x-registry-field": "branch", "type": ["string", "boolean", "integer"] } - ] - }, - "httpRequest": { - "description": "Bounded read-only HTTP request template.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "semantics": {"x-registry-field": "property", "const": "read_only" }, - "query": {"x-registry-field": "property", "type": "object", "maxProperties": 64, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "headers": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "body": { "x-registry-field": "property" } - } - }, - "httpResponse": { - "description": "Rules for translating upstream status and body cardinality into normalized outcomes.", - "type": "object", - "additionalProperties": false, - "properties": { - "no_match": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "ambiguous": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "shape": { - "x-registry-field": "property", - "oneOf": [ - {"x-registry-field": "branch", "const": "singleton" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["records", "cardinality"], - "properties": { - "records": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" }, - "cardinality": {"x-registry-field": "property", "const": "probe_two" } - } - } - ] - } - } - }, - "capability": { - "description": "Closed choice among direct HTTP, Rhai script, and exact snapshot execution.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["http"], - "properties": { - "http": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["request"], - "properties": { - "request": {"x-registry-field": "property", "$ref": "#/$defs/httpRequest" }, - "response": {"x-registry-field": "property", "$ref": "#/$defs/httpResponse" } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["script"], - "properties": { - "script": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, - "modules": { - "x-registry-field": "sensitive_property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/relativePath" } - } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["snapshot"], - "properties": { - "snapshot": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["entity", "exact", "freshness"], - "properties": { - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "exact": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 8, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputReference" } - }, - "freshness": { - "x-registry-field": "property", - "type": "string", - "description": "Maximum accepted snapshot age as a canonical positive duration no greater than 31 days.", - "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-5][0-9]{8}|26[0-6][0-9]{7}|267[0-7][0-9]{6}|2678[0-3][0-9]{5}|2678400000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-5][0-9]{5}|26[0-6][0-9]{4}|267[0-7][0-9]{3}|2678[0-3][0-9]{2}|2678400)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-3][0-9]{3}|44[0-5][0-9]{2}|446[0-3][0-9]{1}|44640)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-3][0-9]{1}|74[0-3]|744)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30|31)d)$" - } - } - } - } - } - ] - }, - "output": { - "description": "Type and optional source pointer for one normalized integration output.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "x-registry-source": { - "x-registry-field": "property", - "type": "string", - "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" - } - } - }, - "limits": { - "description": "Per-execution call, byte, and deadline budgets.", - "type": "object", - "additionalProperties": false, - "properties": { - "calls": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16 }, - "request_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationRequestByteSize" }, - "source_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationSourceByteSize" }, - "deadline": { - "x-registry-field": "property", - "description": "Positive total integration deadline no greater than 20 seconds.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - } -} diff --git a/projects/mosd-programme/.registry-stack-editor/schemas/project.schema.json b/projects/mosd-programme/.registry-stack-editor/schemas/project.schema.json deleted file mode 100644 index 91b7ea7..0000000 --- a/projects/mosd-programme/.registry-stack-editor/schemas/project.schema.json +++ /dev/null @@ -1,537 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/project.v1.json", - "title": "Registry Stack project v1", - "description": "Declares the product-neutral Registry Stack project graph: integrations, materialized entities, and Relay or Notary services.", - "type": "object", - "additionalProperties": false, - "required": ["version", "registry", "services"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Project authoring format version." }, - "starter": { - "x-registry-field": "property", - "description": "Immutable provenance for a workspace initialized from a Registry Stack starter. The digest excludes this content_digest field and covers the starter's authored project files.", - "type": "object", - "additionalProperties": false, - "required": ["id", "release", "content_digest"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Registry Stack starter identifier." }, - "release": {"x-registry-field": "property", "$ref": "#/$defs/token", "description": "Registry Stack release that supplied the starter." }, - "content_digest": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^sha256:[0-9a-f]{64}$", "description": "Digest of the initialized starter authoring content, used to report later workspace divergence." } - } - }, - "registry": { - "x-registry-field": "property", - "description": "Stable identity of the Registry Stack project.", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } - }, - "integrations": { - "x-registry-field": "property", - "description": "Source adaptation definitions keyed by project-local integration identifier.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "entities": { - "x-registry-field": "property", - "description": "Materialized entity definitions keyed by project-local entity identifier.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "services": { - "x-registry-field": "property", - "description": "Relay records APIs and Notary evidence services exposed by the project.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/service" } - } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["integrations"], "properties": { "integrations": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["entities"], "properties": { "entities": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["services"], "properties": { "services": {"x-registry-field": "property", "minProperties": 1 } } } - ], - "examples": [ - { - "version": 1, - "registry": { "id": "benefits-registry" }, - "integrations": { - "household_lookup": { "file": "integrations/household-lookup/integration.yaml" } - }, - "services": {} - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Non-empty token without commas, whitespace, or control characters." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Project-relative path without traversal or duplicate separators." }, - "text": { "type": "string", "minLength": 1, "maxLength": 2048, "description": "Bounded human-readable project metadata." }, - "scope": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Authorization scope token." }, - "recordsApi": { - "description": "Relay records API behavior, authorization, query bounds, and standards profiles.", - "type": "object", - "additionalProperties": false, - "required": ["scopes", "projection", "pagination", "standards"], - "properties": { - "scopes": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["metadata", "rows"], - "properties": { - "metadata": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "rows": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "evidence_verification": {"x-registry-field": "property", "$ref": "#/$defs/scope" } - } - }, - "purposes": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } }, - "projection": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 256, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "pagination": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["default_limit", "max_limit"], - "properties": { - "default_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 }, - "max_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 } - } - }, - "filters": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 256, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } - }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "relationships": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["kind", "target", "foreign_key"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["belongs_to", "has_many", "has_one"] }, - "target": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "foreign_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "concept_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - } - }, - "aggregates": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAggregate" } - }, - "attribute_release_profiles": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseProfiles" }, - "standards": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["ogc_features", "sp_dci"], - "properties": { - "ogc_features": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpatial" }] }, - "sp_dci": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpdci" }] } - } - } - } - }, - "recordAttributeReleaseProfiles": { - "description": "Purpose-bound, exact-one identity releases keyed by stable profile id. Generated Relay responses omit source metadata and are never cacheable.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAttributeReleaseProfile" } - }, - "recordAttributeReleaseProfile": { - "description": "One purpose-bound, minimized identity release compiled into the owning Relay entity.", - "type": "object", - "additionalProperties": false, - "required": ["version", "purpose", "release_scope", "subject", "release_conditions", "claims"], - "properties": { - "version": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "release_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "subject": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["source_field", "id_type"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "id_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" } - } - }, - "release_conditions": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["expression"], - "properties": { "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" } } - }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["required", "sensitivity"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" }, - "required": {"x-registry-field": "property", "type": "boolean" }, - "sensitivity": {"x-registry-field": "property", "enum": ["direct_identifier", "personal", "public", "pseudonymous"] } - }, - "oneOf": [ - {"x-registry-field": "branch", "required": ["source_field"] }, - {"x-registry-field": "branch", "required": ["expression"] } - ] - } - } - } - }, - "recordAttributeReleaseExpression": { - "description": "Bounded CEL evaluated only against the projected source object.", - "type": "object", - "additionalProperties": false, - "required": ["cel"], - "properties": { "cel": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 4096 } } - }, - "filterOperators": { "type": "array", "minItems": 1, "uniqueItems": true, "description": "Allowed operators for one queryable field.", "items": {"x-registry-field": "array_item", "enum": ["eq", "in", "gte", "lte", "between"] } }, - "fieldList": { "type": "array", "maxItems": 16, "uniqueItems": true, "description": "Bounded unique list of entity field identifiers.", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "recordAggregate": { - "description": "Governed aggregate definition with disclosure controls.", - "type": "object", - "additionalProperties": false, - "required": ["description", "disclosure_control"], - "properties": { - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "default_group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "dimensions": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateDimension" } }, - "indicators": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateIndicator" } }, - "allowed_filters": {"x-registry-field": "property", "type": "object", "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "temporal_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateAccess" }, - "spatial": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateSpatial" }, - "joins": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "measures": {"x-registry-field": "property", "type": "array", "minItems": 1, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateMeasure" } }, - "disclosure_control": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "min_group_size": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "suppression": {"x-registry-field": "property", "enum": ["omit", "mask", "null"] } - } - } - }, - "anyOf": [{"x-registry-field": "branch", "required": ["measures"] }, {"x-registry-field": "branch", "required": ["indicators"] }] - }, - "recordAggregateAccess": { - "description": "Authorization scopes and execution policy for one governed aggregate.", - "type": "object", - "additionalProperties": false, - "properties": { - "metadata_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_only_execution": {"x-registry-field": "property", "type": "boolean" } - } - }, - "recordAggregateSpatial": { - "description": "Administrative-area geometry join used to spatially group aggregate results.", - "type": "object", - "additionalProperties": false, - "required": ["mode", "dimension", "geometry_entity", "geometry_id_field", "geometry_field"], - "properties": { - "mode": {"x-registry-field": "property", "const": "admin_area" }, - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "dimension": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_id_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatial": { - "description": "OGC API Features collection metadata and bounded geometry mapping.", - "type": "object", - "additionalProperties": false, - "required": ["geometry"], - "properties": { - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "geometry": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialGeometry" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "datetime_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_bbox_degrees": {"x-registry-field": "property", "type": "number", "exclusiveMinimum": 0 }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatialGeometry": { - "description": "Supported point or encoded-geometry field mapping for spatial records.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "longitude_field", "latitude_field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "const": "point" }, - "longitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "latitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["geojson", "wkt", "wkb"] }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - } - ] - }, - "recordSpatialBbox": { - "description": "Record fields carrying the four coordinates of a bounding box.", - "type": "object", - "additionalProperties": false, - "required": ["min_x", "min_y", "max_x", "max_y"], - "properties": { - "min_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "min_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordSpdci": { - "description": "SP-DCI registry identity and field mappings for a records service.", - "type": "object", - "additionalProperties": false, - "required": ["registry", "registry_type", "record_type", "identifiers", "expression_fields"], - "properties": { - "registry": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "registry_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "record_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "identifiers": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "expression_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "response_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" } - } - }, - "recordFieldMap": { - "description": "Bounded mapping from standard field names to entity field identifiers.", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } - }, - "recordAggregateDimension": { - "description": "Named grouping dimension backed by one entity field.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "field"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "codelist": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateIndicator": { - "description": "SDMX-style aggregate indicator with units and display metadata.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "function", "column", "unit_measure"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "unit_measure": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "unit_mult": {"x-registry-field": "property", "type": "integer", "minimum": -2147483648, "maximum": 2147483647 }, - "decimals": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 4294967295 }, - "frequency": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "definition_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateMeasure": { - "description": "Named aggregate calculation over one entity column.", - "type": "object", - "additionalProperties": false, - "required": ["name", "function", "column"], - "properties": { - "name": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordAggregateFunction": { - "description": "Supported aggregate calculation function.", - "enum": ["count", "sum", "avg", "min", "max", "median", "count_distinct", "stddev"] - }, - "disclosureMode": { "enum": ["value", "predicate", "redacted"], "description": "Maximum detail a claim may disclose." }, - "disclosure": { - "description": "Fixed disclosure mode or a default constrained by explicitly allowed alternatives.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/disclosureMode" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["default", "allowed"], - "properties": { - "default": {"x-registry-field": "property", "$ref": "#/$defs/disclosureMode" }, - "allowed": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/disclosureMode" } } - } - } - ] - }, - "service": { - "description": "Closed choice between a Notary evidence service and a Relay records service.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/evidenceService" }, - {"x-registry-field": "branch", "$ref": "#/$defs/recordsService" } - ] - }, - "recordsService": { - "description": "Relay records API backed by one authored materialized entity.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "entity", "api"], - "properties": { - "kind": {"x-registry-field": "property", "const": "records_api" }, - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "owner": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "sensitivity": {"x-registry-field": "property", "enum": ["public", "internal", "personal", "confidential", "secret"] }, - "access_rights": {"x-registry-field": "property", "enum": ["public", "restricted", "non_public"] }, - "update_frequency": {"x-registry-field": "property", "enum": ["continuous", "daily", "weekly", "termly", "monthly", "quarterly", "annual", "irregular", "as_needed", "unknown"] }, - "conforms_to": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/text" } }, - "api": {"x-registry-field": "property", "$ref": "#/$defs/recordsApi" } - } - }, - "evidenceService": { - "description": "Notary evidence policy, consultations, claims, and registry-backed credential profiles. Source-free claims remain evaluation-only and cannot be selected by a credential profile.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "version", "purpose", "legal_basis", "consent", "access", "claims"], - "properties": { - "kind": {"x-registry-field": "property", "const": "evidence" }, - "version": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "subject_type": { - "x-registry-field": "property", - "description": "Subject category evaluated by this evidence service. Omission is normalized to person.", - "enum": ["person", "project"] - }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "legal_basis": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "consent": {"x-registry-field": "property", "enum": ["not_required", "required"] }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/access" }, - "variables": {"x-registry-field": "property", "$ref": "#/$defs/variables" }, - "consultations": {"x-registry-field": "property", "$ref": "#/$defs/consultations" }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["disclosure"], - "properties": { "output": {"x-registry-field": "property", "type": "string" }, "cel": {"x-registry-field": "property", "type": "string", "minLength": 1 }, "value": {"x-registry-field": "property", "$ref": "#/$defs/claimValue" }, "disclosure": {"x-registry-field": "property", "$ref": "#/$defs/disclosure" } }, - "oneOf": [{"x-registry-field": "branch", "required": ["output"] }, {"x-registry-field": "branch", "required": ["cel"] }] - } - }, - "credential_profiles": { - "x-registry-field": "property", - "description": "Credential profiles may select only claims backed by an exact Relay consultation. Source-free claim evaluation cannot be exposed as credential capability.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["format", "type", "validity", "claims"], - "properties": { - "format": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "validity": {"x-registry-field": "property", "type": "string", "pattern": "^[1-9][0-9]*(?:s|m|h)$" }, - "claims": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } } - } - } - } - } - }, - "access": { - "description": "Scopes a caller must hold to use an evidence service.", - "type": "object", "additionalProperties": false, "required": ["scopes"], - "properties": { "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } } } - }, - "variables": { - "description": "Typed request variables exposed to evidence evaluation.", - "type": "object", "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["from", "type"], "properties": { "from": {"x-registry-field": "property", "type": "string", "pattern": "^request\\.variables\\.[a-z][a-z0-9._-]{0,95}$" }, "type": {"x-registry-field": "property", "const": "date" } } } - }, - "consultations": { - "description": "Relay consultations and their closed bindings to target request identifiers or caller-supplied target attributes.", - "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["integration", "input"], "properties": { "integration": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, "input": {"x-registry-field": "property", "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/targetRequestMapping" } } } } - }, - "targetRequestMapping": { - "description": "Closed target request binding. Attribute values are bounded typed caller-supplied context, not authenticated identifiers.", - "type": "string", - "maxLength": 128, - "pattern": "^request\\.target\\.(?:id|identifiers\\.[A-Za-z][A-Za-z0-9._-]{0,95}|attributes\\.[a-z][a-z0-9_]{0,63})$" - }, - "claimValue": { - "description": "Explicit claim value type, nullability, and encoded-size bound for source-free evaluation. A source-free claim cannot be selected by a credential profile.", - "type": "object", "additionalProperties": false, "required": ["type"], - "properties": { "type": {"x-registry-field": "property", "enum": ["boolean", "integer", "string", "date"] }, "nullable": {"x-registry-field": "property", "type": "boolean" }, "max_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 } } - } - } -} diff --git a/projects/mosd-programme/.vscode/extensions.json b/projects/mosd-programme/.vscode/extensions.json deleted file mode 100644 index 6098b6e..0000000 --- a/projects/mosd-programme/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "redhat.vscode-yaml" - ] -} diff --git a/projects/mosd-programme/.vscode/settings.json b/projects/mosd-programme/.vscode/settings.json deleted file mode 100644 index db92a93..0000000 --- a/projects/mosd-programme/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "yaml.schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } -} diff --git a/projects/mosd-programme/.zed/settings.json b/projects/mosd-programme/.zed/settings.json deleted file mode 100644 index fac07ac..0000000 --- a/projects/mosd-programme/.zed/settings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } - } - } - } - } -} diff --git a/projects/mosd-programme/entities/mosd_enroll.yaml b/projects/mosd-programme/entities/mosd_enroll.yaml deleted file mode 100644 index f4c8aba..0000000 --- a/projects/mosd-programme/entities/mosd_enroll.yaml +++ /dev/null @@ -1,20 +0,0 @@ -version: 1 -id: mosd_enroll -revision: 1 -primary_key: uin -schema: - type: object - additionalProperties: false - required: [uin, duplicate_flag] - properties: - uin: - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - duplicate_flag: { type: boolean } -materialization: - max_records: 1000000 - max_bytes: 64MiB - refresh: 1m - retain_generations: 2 diff --git a/projects/mosd-programme/environments/hosted.yaml b/projects/mosd-programme/environments/hosted.yaml deleted file mode 100644 index f74db15..0000000 --- a/projects/mosd-programme/environments/hosted.yaml +++ /dev/null @@ -1,26 +0,0 @@ -version: 1 - -entities: - mosd_enroll: - provider: - type: csv - path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv - header_row: 1 - columns: - uin: uin - duplicate_flag: duplicate_flag - source_revision: solmara-mosd-programme-projection-v1 - generation: "2026-07-14" - -relay: - origin: https://mosd-programme-relay.solmara.registrystack.org - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } - -deployment: - profile: hosted_lab - relay: { service: programme-mis-relay } diff --git a/projects/mosd-programme/environments/local.yaml b/projects/mosd-programme/environments/local.yaml deleted file mode 100644 index 41ae0d7..0000000 --- a/projects/mosd-programme/environments/local.yaml +++ /dev/null @@ -1,26 +0,0 @@ -version: 1 - -entities: - mosd_enroll: - provider: - type: csv - path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv - header_row: 1 - columns: - uin: uin - duplicate_flag: duplicate_flag - source_revision: solmara-mosd-programme-projection-v1 - generation: "2026-07-14" - -relay: - origin: http://127.0.0.1:8080 - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } - -deployment: - profile: local - relay: { service: programme-mis-relay } diff --git a/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/match.yaml b/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/match.yaml deleted file mode 100644 index 5230336..0000000 --- a/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: enrollment-match -classification: synthetic -input: { uin: "2300010248" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: { duplicate_flag: false } } -expect: - outcome: match - outputs: { duplicate_flag: false } diff --git a/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/no-match.yaml b/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/no-match.yaml deleted file mode 100644 index 20dd7ed..0000000 --- a/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/no-match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: enrollment-no-match -classification: synthetic -input: { uin: "2300999997" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: [] } -expect: - outcome: no_match - outputs: {} diff --git a/projects/mosd-programme/integrations/enrollment-by-uin/integration.yaml b/projects/mosd-programme/integrations/enrollment-by-uin/integration.yaml deleted file mode 100644 index 2b35969..0000000 --- a/projects/mosd-programme/integrations/enrollment-by-uin/integration.yaml +++ /dev/null @@ -1,28 +0,0 @@ -version: 1 -id: enrollment-by-uin -revision: 1 - -input: - uin: - role: selector - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - -capability: - snapshot: - entity: mosd_enroll - exact: - uin: { input: uin } - freshness: 24h - -outputs: [duplicate_flag] - -not_applicable: - ambiguity: - rationale: The exact UIN selector is the entity primary key, whose materialized unique-key constraint permits at most one enrollment projection. - request_fixture: enrollment-match - subject_mismatch: - rationale: The minimized output projection omits UIN, so it contains no identifier comparable with the requested UIN. - request_fixture: enrollment-match diff --git a/projects/mosd-programme/registry-stack.yaml b/projects/mosd-programme/registry-stack.yaml deleted file mode 100644 index ba9075b..0000000 --- a/projects/mosd-programme/registry-stack.yaml +++ /dev/null @@ -1,48 +0,0 @@ -version: 1 - -registry: - id: solmara-mosd-programme - -integrations: - enrollment-by-uin: - file: integrations/enrollment-by-uin/integration.yaml - -entities: - mosd_enroll: - file: entities/mosd_enroll.yaml - -services: - mosd-enrollment-records: - kind: records_api - entity: mosd_enroll - title: MOSD programme enrollment records - description: Purpose-limited enrollment facts used by the Solmara Evidence service. - owner: Ministry of Social Development - sensitivity: personal - access_rights: restricted - update_frequency: continuous - api: - scopes: - metadata: mosd_enroll:metadata - rows: mosd_enroll:rows - aggregate: mosd_enroll:aggregate - evidence_verification: mosd_enroll:evidence_verification - purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - projection: [uin, duplicate_flag] - pagination: { default_limit: 2, max_limit: 2 } - filters: - uin: [eq] - standards: { ogc_features: false, sp_dci: false } - - mosd-enrollment-consultation: - kind: consultation_api - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - legal_basis: child-benefit-act - consent: not_required - consultations: - enrollment: - integration: enrollment-by-uin - input: - uin: request.target.identifiers.solmara_uin diff --git a/projects/nagdi-agriculture/.registry-stack-editor/manifest.json b/projects/nagdi-agriculture/.registry-stack-editor/manifest.json deleted file mode 100644 index 3603a8e..0000000 --- a/projects/nagdi-agriculture/.registry-stack-editor/manifest.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "format": "registry.stack.editor-manifest", - "version": 1, - "registryctl_version": "0.15.2", - "schemas": [ - { - "kind": "project", - "path": "schemas/project.schema.json", - "file_glob": "registry-stack.yaml", - "sha256": "sha256:9fd76663c866c52bb5a17a4cfea821049b7428c812f1666da9db0d43a00da144" - }, - { - "kind": "environment", - "path": "schemas/environment.schema.json", - "file_glob": "environments/*.yaml", - "sha256": "sha256:d16828629e001a30a1705af90bcc12a214be18cf5805a9833e65b03d631c9e9b" - }, - { - "kind": "integration", - "path": "schemas/integration.schema.json", - "file_glob": "integrations/*/integration.yaml", - "sha256": "sha256:2e45452f63c3c610ed14b79244bc9aa086d79d811ad0899223b36c160d449a79" - }, - { - "kind": "fixture", - "path": "schemas/fixture.schema.json", - "file_glob": "integrations/*/fixtures/*.yaml", - "sha256": "sha256:98c0ab21e35cecd143b51e4b5dd487de07ceea24e505bbe351d212e0628c8c8c" - }, - { - "kind": "entity", - "path": "schemas/entity.schema.json", - "file_glob": "entities/*.yaml", - "sha256": "sha256:4cbb31c8addeb54f8b57aaaff8f30c39e651a21a2d741a1b775ecb0d7c248342" - } - ] -} diff --git a/projects/nagdi-agriculture/.registry-stack-editor/schemas/entity.schema.json b/projects/nagdi-agriculture/.registry-stack-editor/schemas/entity.schema.json deleted file mode 100644 index ffc57c8..0000000 --- a/projects/nagdi-agriculture/.registry-stack-editor/schemas/entity.schema.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/entity.v1.json", - "title": "Registry Stack project entity v1", - "description": "Defines a bounded, materialized entity that Relay can query without coupling the project to a specific source product.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "primary_key", "schema", "materialization"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Entity authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the entity." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this entity definition.", "examples": [1] }, - "primary_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Field whose value uniquely identifies each materialized record.", "examples": ["person_id"] }, - "schema": {"x-registry-field": "property", "$ref": "#/$defs/objectSchema", "description": "Closed JSON object schema for materialized records." }, - "materialization": { - "x-registry-field": "property", - "description": "Authored resource, refresh, and bounded recovery-set retention applied while building entity generations.", - "type": "object", - "additionalProperties": false, - "required": ["max_records", "max_bytes", "refresh", "retain_generations"], - "properties": { - "max_records": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 100000000, "description": "Maximum number of records allowed in one generation.", "examples": [10000] }, - "max_bytes": { - "x-registry-field": "property", - "description": "Maximum encoded size of one generation, up to 1 GiB, as bytes or a KiB/MiB quantity.", - "examples": ["16MiB"], - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1073741824 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,5}|10[0-3][0-9]{4}|104[0-7][0-9]{3}|1048[0-4][0-9]{2}|10485[0-6][0-9]|104857[0-6])KiB|(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])MiB)$" } - ] - }, - "refresh": { - "x-registry-field": "property", - "description": "Refresh cadence as a canonical positive duration no greater than 30 days, or manual when an operator initiates every refresh.", - "examples": ["1h"], - "oneOf": [ - {"x-registry-field": "branch", "const": "manual" }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-4][0-9]{8}|25[0-8][0-9]{7}|259[0-1][0-9]{6}|2592000000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-4][0-9]{5}|25[0-8][0-9]{4}|259[0-1][0-9]{3}|2592000)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-2][0-9]{3}|43[0-1][0-9]{2}|43200)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-1][0-9]{1}|720)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30)d)$" } - ] - }, - "retain_generations": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16, "description": "Number of completed cache generations, including the active generation, retained as a bounded recovery set after successful publication. This does not make arbitrary retained generations selectable for rollback.", "examples": [2] } - } - } - }, - "examples": [ - { - "version": 1, - "id": "people", - "revision": 1, - "primary_key": "person_id", - "schema": { - "type": "object", - "additionalProperties": false, - "required": ["person_id"], - "properties": { "person_id": { "type": "string" } } - }, - "materialization": { - "max_records": 10000, - "max_bytes": "16MiB", - "refresh": "1h", - "retain_generations": 2 - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "propertyName": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$", "description": "Portable lowercase record field name." }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for a nullable field.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "items": false, - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "fieldSchema": { - "description": "Bounded schema keywords supported for one scalar materialized field.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "enum": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true }, - "const": {"x-registry-field": "property"}, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 65536 }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 } - } - }, - "objectSchema": { - "description": "Closed object schema whose declared required fields must appear in properties.", - "type": "object", - "additionalProperties": false, - "required": ["type", "additionalProperties", "required", "properties"], - "properties": { - "type": {"x-registry-field": "property", "const": "object" }, - "additionalProperties": {"x-registry-field": "property", "const": false }, - "required": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 256, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/propertyName" } - }, - "properties": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 256, - "propertyNames": {"x-registry-field": "property", "$ref": "#/$defs/propertyName" }, - "additionalProperties": {"x-registry-field": "property", "$ref": "#/$defs/fieldSchema" } - } - } - } - } -} diff --git a/projects/nagdi-agriculture/.registry-stack-editor/schemas/environment.schema.json b/projects/nagdi-agriculture/.registry-stack-editor/schemas/environment.schema.json deleted file mode 100644 index f04c70b..0000000 --- a/projects/nagdi-agriculture/.registry-stack-editor/schemas/environment.schema.json +++ /dev/null @@ -1,337 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/environment.v1.json", - "title": "Registry Stack project environment v1", - "description": "Binds a Registry Stack project to environment-specific services, sources, credentials, and deployment topology.", - "type": "object", - "additionalProperties": false, - "required": ["version", "deployment"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Environment authoring format version." }, - "integrations": {"x-registry-field": "property", "type": "object", "maxProperties": 16, "description": "Environment bindings keyed by authored integration identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/integration" } }, - "entities": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "description": "Environment-specific source bindings keyed by entity identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/entity" } }, - "issuance": { - "x-registry-field": "property", - "description": "Notary issuer and signing-key bindings for credential issuance.", - "type": "object", "additionalProperties": false, "required": ["issuer", "signing_key", "signing_kid", "generation"], - "properties": { "issuer": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1 }, "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" }, "algorithm": {"x-registry-field": "property", "enum": ["EdDSA", "ES256"], "default": "EdDSA", "description": "Credential issuer signing algorithm. Holder proof remains EdDSA with did:jwk." }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "callers": { - "x-registry-field": "property", - "description": "Notary API-key caller identities and the scopes each identity receives.", - "type": "object", "maxProperties": 64, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["api_key_fingerprint", "scopes"], - "properties": { "api_key_fingerprint": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } - } - }, - "relay": { - "x-registry-field": "property", - "description": "Public Relay identity and token-validation settings for a Relay deployment.", - "type": "object", "additionalProperties": false, - "required": ["origin", "issuer", "jwks_url", "audience", "allowed_clients"], - "properties": { "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "allowed_clients": {"x-registry-field": "sensitive_property", "type": "array", "maxItems": 64, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } }, "local_api_keys": {"x-registry-field": "property", "description": "Local-profile-only synthetic principals and scopes. Registryctl generates the raw keys and supplies only their fingerprints to Relay.", "type": "object", "additionalProperties": false, "required": ["match_principal", "no_match_principal", "scopes"], "properties": { "match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "no_match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } } } - }, - "notary_relay": { - "x-registry-field": "property", - "description": "Deployment-internal Relay connection, Notary workload identity, and token file used only when Notary consults Relay.", - "type": "object", "additionalProperties": false, "required": ["base_url", "workload_client_id", "token_file"], - "properties": { "base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/internalOrigin" }, "workload_client_id": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "token_file": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" } } - }, - "relay_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Relay-owned consultation PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Notary-owned PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_cel": { - "x-registry-field": "property", - "description": "Optional per-worker data/address-space ceiling for dedicated Notary CEL processes. The Notary default is 134217728 bytes; 1073741824 is the maximum emulation-compatible exception and remains a limit, not an allocation.", - "type": "object", "additionalProperties": false, "required": ["worker_memory_bytes"], - "properties": { - "worker_memory_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 33554432, "maximum": 1073741824 } - } - }, - "oid4vci": { - "x-registry-field": "property", - "description": "Explicit registry-backed holder-wallet OID4VCI binding for one authored Notary credential profile.", - "$ref": "#/$defs/oid4vci" - }, - "deployment": { - "x-registry-field": "property", - "description": "Products deployed in this environment and the operational profile they use.", - "type": "object", "additionalProperties": false, "required": ["profile"], - "properties": { "profile": {"x-registry-field": "property", "enum": ["local", "hosted_lab", "production", "evidence_grade"] }, "relay": {"x-registry-field": "property", "$ref": "#/$defs/service" }, "notary": {"x-registry-field": "property", "$ref": "#/$defs/service" } }, - "anyOf": [{"x-registry-field": "branch", "required": ["relay"] }, {"x-registry-field": "branch", "required": ["notary"] }] - } - }, - "allOf": [ - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["deployment"], "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } }, - "then": {"x-registry-field": "branch", "required": ["relay"] }, - "else": {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "required": ["relay"] } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_relay"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay", "notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["relay_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_cel"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["oid4vci"] }, - "then": { - "x-registry-field": "branch", - "required": ["notary_state"], - "properties": { - "deployment": {"x-registry-field": "property", "required": ["notary"] } - } - }, - "else": {"x-registry-field": "branch", "properties": { "callers": {"x-registry-field": "property", "minProperties": 1 } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["profile"], "properties": { "profile": {"x-registry-field": "property", "const": "local" } } } } }, - "else": { - "x-registry-field": "branch", - "properties": { - "relay": { - "x-registry-field": "property", - "not": {"x-registry-field": "branch", "required": ["local_api_keys"] }, - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - }, - "oid4vci": { - "x-registry-field": "property", - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorization_server": { - "x-registry-field": "property", - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - } - } - } - } - } - } - ], - "examples": [ - { - "version": 1, - "deployment": { - "profile": "local", - "notary": { "service": "notary" } - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token2048": { "type": "string", "minLength": 1, "maxLength": 2048, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII token, including full verification-method identifiers." }, - "secret": { "type": "object", "additionalProperties": false, "description": "Reference to a process-environment variable; secret values are never authored here.", "required": ["secret"], "properties": { "secret": {"x-registry-field": "secret_reference_property", "type": "string", "pattern": "^[A-Z_][A-Z0-9_]{0,127}$" } } }, - "origin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/?$", "description": "HTTPS origin without path, query, or fragment." }, - "httpsResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/[^?#]+$", "description": "Exact HTTPS resource without query or fragment." }, - "localLoopbackOrigin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/?$", "description": "HTTP IP-loopback origin; public Relay and issuer fields restrict it to the local profile, while internal Notary-to-Relay connections allow it in any profile." }, - "localLoopbackResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/[^?#]+$", "description": "HTTP IP-loopback resource accepted only with the local deployment profile." }, - "internalOrigin": { "description": "Deployment-internal HTTPS or HTTP IP-loopback origin.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayOrigin": { "description": "Relay or issuer origin using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayResource": { "description": "Relay authentication resource using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/httpsResource" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackResource" }] }, - "oid4vci": { - "description": "Closed OID4VCI trust, key, subject, wallet, and credential-profile binding.", - "type": "object", - "additionalProperties": false, - "required": ["public_base_url", "credential", "authorization_server", "client", "access_token", "sensitive_state_key", "subject", "redirect_uri", "allowed_wallet_origins"], - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "credential": { - "x-registry-field": "property", - "description": "Existing project service and credential profile exposed through OID4VCI.", - "type": "object", "additionalProperties": false, "required": ["service", "profile"], - "properties": { - "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "profile": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "authorization_server": { - "x-registry-field": "property", - "description": "Pinned eSignet issuer and exact endpoints used for login and token validation.", - "type": "object", "additionalProperties": false, "required": ["issuer", "jwks_url", "userinfo_url", "authorize_url", "token_url"], - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" } - } - }, - "client": { - "x-registry-field": "property", - "description": "eSignet relying-party identity and dedicated private-key reference.", - "type": "object", "additionalProperties": false, "required": ["id", "signing_key", "signing_kid"], - "properties": { - "id": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "access_token": { - "x-registry-field": "property", - "description": "Dedicated Notary access-token signing key and published key identifier.", - "type": "object", "additionalProperties": false, "required": ["signing_key", "signing_kid"], - "properties": { - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "sensitive_state_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "subject": { - "x-registry-field": "property", - "description": "Verified eSignet userinfo claim bound exactly to the credential subject identifier.", - "type": "object", "additionalProperties": false, "required": ["token_claim", "id_type"], - "properties": { - "token_claim": {"x-registry-field": "property", "$ref": "#/$defs/token256" }, - "id_type": {"x-registry-field": "property", "$ref": "#/$defs/token256" } - } - }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "allowed_wallet_origins": { - "x-registry-field": "sensitive_property", - "description": "Exact HTTPS browser origins admitted to the wallet-facing Notary routes.", - "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "$ref": "#/$defs/origin" } - }, - "tx_code": { - "x-registry-field": "property", - "description": "Transaction-code policy. Omit for the secure required-PIN default. Set required=false only for a bounded bearer-offer interoperability profile; the compiler fixes the offer lifetime at 300 seconds.", - "type": "object", "additionalProperties": false, - "properties": { "required": {"x-registry-field": "property", "type": "boolean", "default": true } } - } - } - }, - "privateCidrs": { - "description": "Explicit private network ranges this source may resolve to.", - "type": "array", "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "type": "string", "minLength": 3, "maxLength": 64 } - }, - "ca": { - "description": "Pinned certificate-authority file and its rotation generation.", - "type": "object", "additionalProperties": false, "required": ["file", "generation"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "mtls": { - "description": "Client certificate and private-key reference for mutual TLS.", - "type": "object", "additionalProperties": false, "required": ["certificate_file", "private_key", "generation"], - "properties": { "certificate_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "private_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "endpoint": { - "description": "Security-bound HTTPS endpoint with optional private-network, CA, and mTLS policy.", - "type": "object", "additionalProperties": false, "required": ["origin", "path", "generation"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//)(?!.*[?#]).+$" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } - } - }, - "credential": { - "description": "One supported source credential shape, containing references rather than values.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["username", "password", "generation"], "properties": { "username": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "password": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["token", "generation"], "properties": { "token": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["client_id", "client_secret", "generation"], "properties": { "client_id": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "client_secret": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["value", "generation"], "properties": { "value": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } } - ] - }, - "provider": { - "description": "Environment-specific physical provider for a materialized entity.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "csv" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "delimiter": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 }, "quote": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "project_file", "path", "sheet"], "properties": { "type": {"x-registry-field": "property", "const": "xlsx" }, "project_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "sheet": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "data_range": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "parquet" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "connection", "schema", "table"], "properties": { "type": {"x-registry-field": "property", "const": "postgres" }, "connection": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "schema": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" }, "table": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" } } } - ] - }, - "entity": { - "description": "Maps an authored entity to provider columns and immutable source-generation identifiers.", - "type": "object", "additionalProperties": false, "required": ["provider", "columns", "source_revision", "generation"], - "properties": { "provider": {"x-registry-field": "property", "$ref": "#/$defs/provider" }, "columns": {"x-registry-field": "property", "type": "object", "minProperties": 1, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } }, "source_revision": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "generation": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 } } - }, - "source": { - "description": "Runtime source connection policy for one authored integration.", - "type": "object", "additionalProperties": false, "required": ["origin"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "credential": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "oauth": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "jwks": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "rate": {"x-registry-field": "property", "type": "object", "additionalProperties": false, "required": ["per_minute", "burst"], "properties": { "per_minute": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 60000 }, "burst": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 1024 } } }, - "concurrency": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 64 }, - "timeout": { - "x-registry-field": "property", - "description": "Optional request timeout no greater than 20 seconds; generated bindings narrow it to the integration deadline.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - }, - "integration": { - "description": "Environment binding for an authored source integration.", - "type": "object", "additionalProperties": false, "required": ["source"], - "properties": { "source": {"x-registry-field": "property", "$ref": "#/$defs/source" } } - }, - "service": { "type": "object", "additionalProperties": false, "description": "Binds an authored product role to a deployed service identifier.", "required": ["service"], "properties": { "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } }, - "token256": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII protocol token." }, - "postgresIdentifier": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,62}$", "description": "Portable unquoted PostgreSQL schema or table identifier." }, - "absolutePath": { "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized absolute path without dot segments or duplicate separators." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized project-relative path without dot segments or duplicate separators." } - } -} diff --git a/projects/nagdi-agriculture/.registry-stack-editor/schemas/fixture.schema.json b/projects/nagdi-agriculture/.registry-stack-editor/schemas/fixture.schema.json deleted file mode 100644 index b853192..0000000 --- a/projects/nagdi-agriculture/.registry-stack-editor/schemas/fixture.schema.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/fixture.v1.json", - "title": "Registry Stack project integration fixture v1", - "description": "Defines one synthetic, deterministic integration scenario for offline adapter verification.", - "type": "object", - "additionalProperties": false, - "required": ["name", "classification", "input", "interactions", "expect"], - "properties": { - "name": {"x-registry-field": "public_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Human-readable scenario name shown in test output.", "examples": ["existing household matches"] }, - "classification": {"x-registry-field": "public_property", "const": "synthetic", "description": "Declares that fixture data is synthetic and safe for offline testing." }, - "request": { - "x-registry-field": "redacted_fixture_property", - "$ref": "#/$defs/governedRequest", - "description": "Optional independently authored synthetic Notary request used to prove the request-to-consultation binding before Relay access." - }, - "input": { - "x-registry-field": "redacted_fixture_property", - "description": "Typed consultation inputs supplied to the adapter under test.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "type": ["string", "boolean", "integer", "null"] - } - }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Named date values available to deterministic fixture interpolation.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "interactions": { - "x-registry-field": "redacted_fixture_property", - "description": "Ordered upstream request and response exchanges expected during execution.", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/interaction" } - }, - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/expectation", "description": "Observable adapter result required for the scenario to pass." } - }, - "examples": [ - { - "name": "existing household matches", - "classification": "synthetic", - "input": { "household_id": "HH-1001" }, - "interactions": [ - { - "expect": { "method": "GET", "path": "/households/HH-1001" }, - "respond": { "status": 200, "body": { "active": true } } - } - ], - "expect": { "outcome": "match", "outputs": { "active": true } } - } - ], - "$defs": { - "governedRequest": { - "description": "The same closed governed request shape accepted by project test --live.", - "type": "object", - "additionalProperties": false, - "required": ["target", "claims", "purpose"], - "properties": { - "target": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/governedTarget" }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies synthetic date variables to the governed evaluation request.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "claims": { - "x-registry-field": "redacted_fixture_property", - "description": "Lists the authored claims evaluated by this synthetic request witness.", - "type": "array", - "minItems": 1, - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedClaimRef" } - }, - "disclosure": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the disclosure mode requested from the authored claim policy." }, - "format": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the claim-result media type requested from the governed Notary path." }, - "purpose": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Names the authored service purpose exercised by this synthetic request witness." } - } - }, - "governedTarget": { - "description": "The synthetic subject target presented to the same governed request boundary as a live Notary evaluation.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 64, "description": "Names the authored target entity type used for claim evaluation." }, - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies an optional synthetic direct target identifier." }, - "identifiers": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored synthetic identifiers for consultation input mapping.", - "type": "array", - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedIdentifier" } - }, - "attributes": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored typed target attributes for consultation input mapping.", - "type": "object", - "maxProperties": 16, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": ["string", "boolean", "integer", "null"] } - } - } - }, - "governedIdentifier": { - "description": "One synthetic target identifier with an authored scheme and string value.", - "type": "object", - "additionalProperties": false, - "required": ["scheme", "value"], - "properties": { - "scheme": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 96, "description": "Names the authored identifier scheme selected by a consultation input mapping." }, - "value": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies the synthetic string value bound to this identifier scheme." } - } - }, - "governedClaimRef": { - "description": "A requested claim ID, optionally pinned to one authored claim version.", - "oneOf": [ - {"x-registry-field": "branch", "type": "string"}, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Names one authored claim requested by this fixture witness."}, - "version": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Pins the requested claim to one authored claim-policy version."} - } - } - ] - }, - "interaction": { - "description": "One expected upstream request paired with its synthetic response.", - "type": "object", - "additionalProperties": false, - "required": ["expect", "respond"], - "properties": { - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/request" }, - "respond": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/response" } - } - }, - "request": { - "description": "Exact HTTP request shape the adapter must produce.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "redacted_fixture_property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "query": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 64, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "oneOf": [ - {"x-registry-field": "branch", "type": ["string", "boolean", "integer", "null"] }, - { - "x-registry-field": "branch", - "type": "array", - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "type": ["string", "boolean", "integer", "null"] } - } - ] - } - }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - "response": { - "description": "Synthetic HTTP response or timeout returned to the adapter.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["status"], - "properties": { - "status": {"x-registry-field": "redacted_fixture_property", "type": "integer", "minimum": 100, "maximum": 599 }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["timeout"], - "properties": { - "timeout": { - "x-registry-field": "redacted_fixture_property", - "description": "Reviewed synthetic timeout intent no greater than 20 seconds; offline execution returns a timeout outcome without sleeping.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - ] - }, - "fixtureBody": { - "description": "Inline JSON, or an exact closed reference to a strict JSON file below the fixture bodies directory. The top-level file key is reserved for file references.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": { - "x-registry-field": "redacted_fixture_property", - "type": "string", - "minLength": 8, - "maxLength": 4096, - "pattern": "^bodies/(?!\\.\\.?/)(?!.*(?:/)\\.\\.?/)(?!.*//).+$" - } - } - }, - { - "x-registry-field": "branch", - "not": { - "x-registry-field": "branch", - "type": "object", - "required": ["file"] - } - } - ] - }, - "expectation": { - "description": "Expected normalized outcome, output values, claims, or error.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "outcome": {"x-registry-field": "redacted_fixture_property", "enum": ["match", "no_match", "ambiguous"] }, - "outputs": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "claims": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "error": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256 } - } - } - } -} diff --git a/projects/nagdi-agriculture/.registry-stack-editor/schemas/integration.schema.json b/projects/nagdi-agriculture/.registry-stack-editor/schemas/integration.schema.json deleted file mode 100644 index 9bb2ad0..0000000 --- a/projects/nagdi-agriculture/.registry-stack-editor/schemas/integration.schema.json +++ /dev/null @@ -1,514 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/integration.v1.json", - "title": "Registry Stack project integration v1", - "description": "Defines a product-neutral source adaptation contract using bounded HTTP, Rhai script, or exact snapshot capabilities.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "input", "capability", "outputs"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Integration authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the integration." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this integration contract.", "examples": [1] }, - "source": {"x-registry-field": "property", "$ref": "#/$defs/source", "description": "Optional source product metadata and transport policy; capabilities remain product-neutral." }, - "input": { - "x-registry-field": "property", - "description": "Typed selector and parameter inputs accepted by this integration.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/input" } - }, - "capability": {"x-registry-field": "property", "$ref": "#/$defs/capability", "description": "Exactly one bounded execution mechanism for the source adaptation." }, - "outputs": { - "x-registry-field": "property", - "description": "Named typed outputs, or a shorthand list of output names inferred by the authoring compiler.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": { - "x-registry-field": "map_key", - "allOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputName" }, - {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "enum": ["matched", "outcome"] } } - ] - }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/output" } - }, - { - "x-registry-field": "branch", - "type": "array", - "minItems": 1, - "maxItems": 64, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/inputName" } - } - ] - }, - "limits": {"x-registry-field": "property", "$ref": "#/$defs/limits", "description": "Optional tighter resource limits for one integration execution." }, - "not_applicable": {"x-registry-field": "property", "$ref": "#/$defs/notApplicable", "description": "Explicit rationale and request-fixture evidence for a normally required outcome that the source contract cannot produce." } - }, - "examples": [ - { - "version": 1, - "id": "household_lookup", - "revision": 1, - "input": { - "household_id": { "role": "selector", "type": "string" } - }, - "capability": { - "script": { "file": "adapter.rhai" } - }, - "outputs": { - "active": { "type": "boolean" } - } - } - ], - "$defs": { - "notApplicable": { - "description": "Closed declarations for outcomes that the source protocol, exact singleton endpoint, or local unique-key constraint proves impossible.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "ambiguity": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed source contract cannot produce more than one matching record." }, - "subject_mismatch": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed response contract contains no identifier comparable with a requested selector." } - } - }, - "notApplicableReason": { - "description": "A reviewable reason that names a real request fixture proving the source access shape used by the rationale.", - "type": "object", - "additionalProperties": false, - "required": ["rationale", "request_fixture"], - "properties": { - "rationale": {"x-registry-field": "property", "type": "string", "minLength": 24, "maxLength": 512, "description": "Why the named normally required outcome cannot occur for this source contract." }, - "request_fixture": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Fixture name containing the supporting bounded source request." } - } - }, - "stableId": { - "description": "Lowercase stable identifier used in project references.", - "type": "string", - "pattern": "^[a-z][a-z0-9._-]{0,95}$" - }, - "inputName": { - "description": "Portable lowercase name for an integration input or output.", - "type": "string", - "pattern": "^[a-z][a-z0-9_]{0,63}$" - }, - "relativePath": { - "description": "Project-relative path without traversal or duplicate separators.", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" - }, - "integrationResponseByteSize": { - "description": "Maximum bytes accepted from one source response, defaulting to 512 KiB and capped at 8 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "512KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 8388608 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|[1-7][0-9]{3}|80[0-9]{2}|81[0-8][0-9]|819[0-2])KiB|[1-8]MiB)$" } - ] - }, - "integrationRequestByteSize": { - "description": "Maximum aggregate author-controlled request bytes for one execution, defaulting to 64 KiB and capped at 1 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "64KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1048576 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])KiB|1MiB)$" } - ] - }, - "integrationSourceByteSize": { - "description": "Maximum aggregate source-response bytes for one execution, defaulting to 2 MiB and capped at 16 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "2MiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 16777216 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-5][0-9]{3}|16[0-2][0-9]{2}|163[0-7][0-9]|1638[0-4])KiB|(?:[1-9]|1[0-6])MiB)$" } - ] - }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for nullable values.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "input": { - "description": "Type, validation, and canonicalization rules for one authored input.", - "type": "object", - "additionalProperties": false, - "required": ["role", "type"], - "properties": { - "role": {"x-registry-field": "property", "enum": ["selector", "parameter"] }, - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "description": "Maximum Unicode scalar-value length. The 1024-character ceiling derives a conservative 4096-byte UTF-8 ceiling for one canonical input.", "type": "integer", "minimum": 1, "maximum": 1024 }, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 16384 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "canonicalization": {"x-registry-field": "property", "enum": ["identity", "ascii_lowercase"] } - } - }, - "versions": { - "description": "Source versions tested by the project and versions explicitly accepted as unverified.", - "type": "object", - "additionalProperties": false, - "properties": { - "tested": {"x-registry-field": "property", "$ref": "#/$defs/versionList" }, - "unverified": {"x-registry-field": "property", "$ref": "#/$defs/versionList" } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["tested"], "properties": { "tested": {"x-registry-field": "property", "minItems": 1 } } }, - {"x-registry-field": "branch", "required": ["unverified"], "properties": { "unverified": {"x-registry-field": "property", "minItems": 1 } } } - ] - }, - "versionList": { - "description": "Bounded list of source version labels.", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } - }, - "credential": { - "description": "Authentication mechanism expected by the source adapter, without environment secret values.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "enum": ["none", "basic", "static_bearer"] } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_header" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_query" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9._:~-]{0,95}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "request", "response_profile"], - "properties": { - "type": {"x-registry-field": "property", "const": "oauth2_client_credentials" }, - "request": {"x-registry-field": "property", "enum": ["form", "json"] }, - "response_profile": {"x-registry-field": "property", "const": "oauth2_bearer" }, - "scope": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 }, - "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "refresh_skew": { - "x-registry-field": "property", - "description": "Positive OAuth refresh interval below the generated token contract's 60-second minimum lifetime.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|[1-5][0-9]{4})ms|(?:[1-9]|[1-5][0-9])s)$" - } - } - } - ] - }, - "allowRule": { - "description": "One read-only upstream method and path pattern the adapter may call.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": { - "x-registry-field": "sensitive_property", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^/[^?#]*$" - }, - "semantics": {"x-registry-field": "property", "const": "read_only" } - } - }, - "source": { - "description": "Optional source metadata, authentication shape, allowlist, and protocol profile.", - "type": "object", - "additionalProperties": false, - "required": ["auth"], - "properties": { - "product": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "versions": {"x-registry-field": "property", "$ref": "#/$defs/versions" }, - "auth": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "allow": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/allowRule" } - }, - "request_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "format": {"x-registry-field": "property", "enum": ["json", "text"] }, - "max_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationResponseByteSize" } - } - }, - "protocol": {"x-registry-field": "property", "$ref": "#/$defs/protocol" } - } - }, - "protocol": { - "description": "Optional interoperable protocol profiles layered over the source transport.", - "type": "object", - "additionalProperties": false, - "properties": { - "signed_dci": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["profile", "path", "jwks_profile", "sender", "receiver", "registry_type", "record_type", "locale", "selectors"], - "properties": { - "profile": {"x-registry-field": "property", "const": "dci-search-v1" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^/[^?#]*$" }, - "jwks_profile": {"x-registry-field": "property", "const": "rsa-signing-jwks-v1" }, - "sender": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "receiver": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "registry_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "record_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "locale": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$" }, - "selectors": { - "x-registry-field": "property", - "type": "object", "minProperties": 1, "maxProperties": 8, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["field", "response_pointer"], - "properties": { - "field": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 160 }, - "response_pointer": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" } - } - } - } - } - } - }, - "minProperties": 1 - }, - "inputReference": { - "description": "Reference to a declared integration input.", - "type": "object", - "additionalProperties": false, - "required": ["input"], - "properties": { "input": {"x-registry-field": "property", "$ref": "#/$defs/inputName" } } - }, - "inputOrLiteral": { - "description": "Request value supplied from a declared input or an authored scalar literal.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputReference" }, - {"x-registry-field": "branch", "type": ["string", "boolean", "integer"] } - ] - }, - "httpRequest": { - "description": "Bounded read-only HTTP request template.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "semantics": {"x-registry-field": "property", "const": "read_only" }, - "query": {"x-registry-field": "property", "type": "object", "maxProperties": 64, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "headers": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "body": { "x-registry-field": "property" } - } - }, - "httpResponse": { - "description": "Rules for translating upstream status and body cardinality into normalized outcomes.", - "type": "object", - "additionalProperties": false, - "properties": { - "no_match": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "ambiguous": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "shape": { - "x-registry-field": "property", - "oneOf": [ - {"x-registry-field": "branch", "const": "singleton" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["records", "cardinality"], - "properties": { - "records": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" }, - "cardinality": {"x-registry-field": "property", "const": "probe_two" } - } - } - ] - } - } - }, - "capability": { - "description": "Closed choice among direct HTTP, Rhai script, and exact snapshot execution.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["http"], - "properties": { - "http": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["request"], - "properties": { - "request": {"x-registry-field": "property", "$ref": "#/$defs/httpRequest" }, - "response": {"x-registry-field": "property", "$ref": "#/$defs/httpResponse" } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["script"], - "properties": { - "script": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, - "modules": { - "x-registry-field": "sensitive_property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/relativePath" } - } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["snapshot"], - "properties": { - "snapshot": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["entity", "exact", "freshness"], - "properties": { - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "exact": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 8, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputReference" } - }, - "freshness": { - "x-registry-field": "property", - "type": "string", - "description": "Maximum accepted snapshot age as a canonical positive duration no greater than 31 days.", - "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-5][0-9]{8}|26[0-6][0-9]{7}|267[0-7][0-9]{6}|2678[0-3][0-9]{5}|2678400000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-5][0-9]{5}|26[0-6][0-9]{4}|267[0-7][0-9]{3}|2678[0-3][0-9]{2}|2678400)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-3][0-9]{3}|44[0-5][0-9]{2}|446[0-3][0-9]{1}|44640)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-3][0-9]{1}|74[0-3]|744)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30|31)d)$" - } - } - } - } - } - ] - }, - "output": { - "description": "Type and optional source pointer for one normalized integration output.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "x-registry-source": { - "x-registry-field": "property", - "type": "string", - "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" - } - } - }, - "limits": { - "description": "Per-execution call, byte, and deadline budgets.", - "type": "object", - "additionalProperties": false, - "properties": { - "calls": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16 }, - "request_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationRequestByteSize" }, - "source_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationSourceByteSize" }, - "deadline": { - "x-registry-field": "property", - "description": "Positive total integration deadline no greater than 20 seconds.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - } -} diff --git a/projects/nagdi-agriculture/.registry-stack-editor/schemas/project.schema.json b/projects/nagdi-agriculture/.registry-stack-editor/schemas/project.schema.json deleted file mode 100644 index 91b7ea7..0000000 --- a/projects/nagdi-agriculture/.registry-stack-editor/schemas/project.schema.json +++ /dev/null @@ -1,537 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/project.v1.json", - "title": "Registry Stack project v1", - "description": "Declares the product-neutral Registry Stack project graph: integrations, materialized entities, and Relay or Notary services.", - "type": "object", - "additionalProperties": false, - "required": ["version", "registry", "services"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Project authoring format version." }, - "starter": { - "x-registry-field": "property", - "description": "Immutable provenance for a workspace initialized from a Registry Stack starter. The digest excludes this content_digest field and covers the starter's authored project files.", - "type": "object", - "additionalProperties": false, - "required": ["id", "release", "content_digest"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Registry Stack starter identifier." }, - "release": {"x-registry-field": "property", "$ref": "#/$defs/token", "description": "Registry Stack release that supplied the starter." }, - "content_digest": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^sha256:[0-9a-f]{64}$", "description": "Digest of the initialized starter authoring content, used to report later workspace divergence." } - } - }, - "registry": { - "x-registry-field": "property", - "description": "Stable identity of the Registry Stack project.", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } - }, - "integrations": { - "x-registry-field": "property", - "description": "Source adaptation definitions keyed by project-local integration identifier.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "entities": { - "x-registry-field": "property", - "description": "Materialized entity definitions keyed by project-local entity identifier.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "services": { - "x-registry-field": "property", - "description": "Relay records APIs and Notary evidence services exposed by the project.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/service" } - } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["integrations"], "properties": { "integrations": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["entities"], "properties": { "entities": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["services"], "properties": { "services": {"x-registry-field": "property", "minProperties": 1 } } } - ], - "examples": [ - { - "version": 1, - "registry": { "id": "benefits-registry" }, - "integrations": { - "household_lookup": { "file": "integrations/household-lookup/integration.yaml" } - }, - "services": {} - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Non-empty token without commas, whitespace, or control characters." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Project-relative path without traversal or duplicate separators." }, - "text": { "type": "string", "minLength": 1, "maxLength": 2048, "description": "Bounded human-readable project metadata." }, - "scope": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Authorization scope token." }, - "recordsApi": { - "description": "Relay records API behavior, authorization, query bounds, and standards profiles.", - "type": "object", - "additionalProperties": false, - "required": ["scopes", "projection", "pagination", "standards"], - "properties": { - "scopes": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["metadata", "rows"], - "properties": { - "metadata": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "rows": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "evidence_verification": {"x-registry-field": "property", "$ref": "#/$defs/scope" } - } - }, - "purposes": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } }, - "projection": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 256, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "pagination": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["default_limit", "max_limit"], - "properties": { - "default_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 }, - "max_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 } - } - }, - "filters": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 256, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } - }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "relationships": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["kind", "target", "foreign_key"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["belongs_to", "has_many", "has_one"] }, - "target": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "foreign_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "concept_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - } - }, - "aggregates": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAggregate" } - }, - "attribute_release_profiles": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseProfiles" }, - "standards": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["ogc_features", "sp_dci"], - "properties": { - "ogc_features": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpatial" }] }, - "sp_dci": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpdci" }] } - } - } - } - }, - "recordAttributeReleaseProfiles": { - "description": "Purpose-bound, exact-one identity releases keyed by stable profile id. Generated Relay responses omit source metadata and are never cacheable.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAttributeReleaseProfile" } - }, - "recordAttributeReleaseProfile": { - "description": "One purpose-bound, minimized identity release compiled into the owning Relay entity.", - "type": "object", - "additionalProperties": false, - "required": ["version", "purpose", "release_scope", "subject", "release_conditions", "claims"], - "properties": { - "version": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "release_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "subject": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["source_field", "id_type"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "id_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" } - } - }, - "release_conditions": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["expression"], - "properties": { "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" } } - }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["required", "sensitivity"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" }, - "required": {"x-registry-field": "property", "type": "boolean" }, - "sensitivity": {"x-registry-field": "property", "enum": ["direct_identifier", "personal", "public", "pseudonymous"] } - }, - "oneOf": [ - {"x-registry-field": "branch", "required": ["source_field"] }, - {"x-registry-field": "branch", "required": ["expression"] } - ] - } - } - } - }, - "recordAttributeReleaseExpression": { - "description": "Bounded CEL evaluated only against the projected source object.", - "type": "object", - "additionalProperties": false, - "required": ["cel"], - "properties": { "cel": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 4096 } } - }, - "filterOperators": { "type": "array", "minItems": 1, "uniqueItems": true, "description": "Allowed operators for one queryable field.", "items": {"x-registry-field": "array_item", "enum": ["eq", "in", "gte", "lte", "between"] } }, - "fieldList": { "type": "array", "maxItems": 16, "uniqueItems": true, "description": "Bounded unique list of entity field identifiers.", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "recordAggregate": { - "description": "Governed aggregate definition with disclosure controls.", - "type": "object", - "additionalProperties": false, - "required": ["description", "disclosure_control"], - "properties": { - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "default_group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "dimensions": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateDimension" } }, - "indicators": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateIndicator" } }, - "allowed_filters": {"x-registry-field": "property", "type": "object", "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "temporal_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateAccess" }, - "spatial": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateSpatial" }, - "joins": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "measures": {"x-registry-field": "property", "type": "array", "minItems": 1, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateMeasure" } }, - "disclosure_control": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "min_group_size": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "suppression": {"x-registry-field": "property", "enum": ["omit", "mask", "null"] } - } - } - }, - "anyOf": [{"x-registry-field": "branch", "required": ["measures"] }, {"x-registry-field": "branch", "required": ["indicators"] }] - }, - "recordAggregateAccess": { - "description": "Authorization scopes and execution policy for one governed aggregate.", - "type": "object", - "additionalProperties": false, - "properties": { - "metadata_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_only_execution": {"x-registry-field": "property", "type": "boolean" } - } - }, - "recordAggregateSpatial": { - "description": "Administrative-area geometry join used to spatially group aggregate results.", - "type": "object", - "additionalProperties": false, - "required": ["mode", "dimension", "geometry_entity", "geometry_id_field", "geometry_field"], - "properties": { - "mode": {"x-registry-field": "property", "const": "admin_area" }, - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "dimension": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_id_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatial": { - "description": "OGC API Features collection metadata and bounded geometry mapping.", - "type": "object", - "additionalProperties": false, - "required": ["geometry"], - "properties": { - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "geometry": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialGeometry" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "datetime_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_bbox_degrees": {"x-registry-field": "property", "type": "number", "exclusiveMinimum": 0 }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatialGeometry": { - "description": "Supported point or encoded-geometry field mapping for spatial records.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "longitude_field", "latitude_field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "const": "point" }, - "longitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "latitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["geojson", "wkt", "wkb"] }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - } - ] - }, - "recordSpatialBbox": { - "description": "Record fields carrying the four coordinates of a bounding box.", - "type": "object", - "additionalProperties": false, - "required": ["min_x", "min_y", "max_x", "max_y"], - "properties": { - "min_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "min_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordSpdci": { - "description": "SP-DCI registry identity and field mappings for a records service.", - "type": "object", - "additionalProperties": false, - "required": ["registry", "registry_type", "record_type", "identifiers", "expression_fields"], - "properties": { - "registry": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "registry_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "record_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "identifiers": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "expression_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "response_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" } - } - }, - "recordFieldMap": { - "description": "Bounded mapping from standard field names to entity field identifiers.", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } - }, - "recordAggregateDimension": { - "description": "Named grouping dimension backed by one entity field.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "field"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "codelist": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateIndicator": { - "description": "SDMX-style aggregate indicator with units and display metadata.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "function", "column", "unit_measure"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "unit_measure": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "unit_mult": {"x-registry-field": "property", "type": "integer", "minimum": -2147483648, "maximum": 2147483647 }, - "decimals": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 4294967295 }, - "frequency": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "definition_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateMeasure": { - "description": "Named aggregate calculation over one entity column.", - "type": "object", - "additionalProperties": false, - "required": ["name", "function", "column"], - "properties": { - "name": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordAggregateFunction": { - "description": "Supported aggregate calculation function.", - "enum": ["count", "sum", "avg", "min", "max", "median", "count_distinct", "stddev"] - }, - "disclosureMode": { "enum": ["value", "predicate", "redacted"], "description": "Maximum detail a claim may disclose." }, - "disclosure": { - "description": "Fixed disclosure mode or a default constrained by explicitly allowed alternatives.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/disclosureMode" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["default", "allowed"], - "properties": { - "default": {"x-registry-field": "property", "$ref": "#/$defs/disclosureMode" }, - "allowed": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/disclosureMode" } } - } - } - ] - }, - "service": { - "description": "Closed choice between a Notary evidence service and a Relay records service.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/evidenceService" }, - {"x-registry-field": "branch", "$ref": "#/$defs/recordsService" } - ] - }, - "recordsService": { - "description": "Relay records API backed by one authored materialized entity.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "entity", "api"], - "properties": { - "kind": {"x-registry-field": "property", "const": "records_api" }, - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "owner": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "sensitivity": {"x-registry-field": "property", "enum": ["public", "internal", "personal", "confidential", "secret"] }, - "access_rights": {"x-registry-field": "property", "enum": ["public", "restricted", "non_public"] }, - "update_frequency": {"x-registry-field": "property", "enum": ["continuous", "daily", "weekly", "termly", "monthly", "quarterly", "annual", "irregular", "as_needed", "unknown"] }, - "conforms_to": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/text" } }, - "api": {"x-registry-field": "property", "$ref": "#/$defs/recordsApi" } - } - }, - "evidenceService": { - "description": "Notary evidence policy, consultations, claims, and registry-backed credential profiles. Source-free claims remain evaluation-only and cannot be selected by a credential profile.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "version", "purpose", "legal_basis", "consent", "access", "claims"], - "properties": { - "kind": {"x-registry-field": "property", "const": "evidence" }, - "version": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "subject_type": { - "x-registry-field": "property", - "description": "Subject category evaluated by this evidence service. Omission is normalized to person.", - "enum": ["person", "project"] - }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "legal_basis": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "consent": {"x-registry-field": "property", "enum": ["not_required", "required"] }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/access" }, - "variables": {"x-registry-field": "property", "$ref": "#/$defs/variables" }, - "consultations": {"x-registry-field": "property", "$ref": "#/$defs/consultations" }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["disclosure"], - "properties": { "output": {"x-registry-field": "property", "type": "string" }, "cel": {"x-registry-field": "property", "type": "string", "minLength": 1 }, "value": {"x-registry-field": "property", "$ref": "#/$defs/claimValue" }, "disclosure": {"x-registry-field": "property", "$ref": "#/$defs/disclosure" } }, - "oneOf": [{"x-registry-field": "branch", "required": ["output"] }, {"x-registry-field": "branch", "required": ["cel"] }] - } - }, - "credential_profiles": { - "x-registry-field": "property", - "description": "Credential profiles may select only claims backed by an exact Relay consultation. Source-free claim evaluation cannot be exposed as credential capability.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["format", "type", "validity", "claims"], - "properties": { - "format": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "validity": {"x-registry-field": "property", "type": "string", "pattern": "^[1-9][0-9]*(?:s|m|h)$" }, - "claims": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } } - } - } - } - } - }, - "access": { - "description": "Scopes a caller must hold to use an evidence service.", - "type": "object", "additionalProperties": false, "required": ["scopes"], - "properties": { "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } } } - }, - "variables": { - "description": "Typed request variables exposed to evidence evaluation.", - "type": "object", "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["from", "type"], "properties": { "from": {"x-registry-field": "property", "type": "string", "pattern": "^request\\.variables\\.[a-z][a-z0-9._-]{0,95}$" }, "type": {"x-registry-field": "property", "const": "date" } } } - }, - "consultations": { - "description": "Relay consultations and their closed bindings to target request identifiers or caller-supplied target attributes.", - "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["integration", "input"], "properties": { "integration": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, "input": {"x-registry-field": "property", "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/targetRequestMapping" } } } } - }, - "targetRequestMapping": { - "description": "Closed target request binding. Attribute values are bounded typed caller-supplied context, not authenticated identifiers.", - "type": "string", - "maxLength": 128, - "pattern": "^request\\.target\\.(?:id|identifiers\\.[A-Za-z][A-Za-z0-9._-]{0,95}|attributes\\.[a-z][a-z0-9_]{0,63})$" - }, - "claimValue": { - "description": "Explicit claim value type, nullability, and encoded-size bound for source-free evaluation. A source-free claim cannot be selected by a credential profile.", - "type": "object", "additionalProperties": false, "required": ["type"], - "properties": { "type": {"x-registry-field": "property", "enum": ["boolean", "integer", "string", "date"] }, "nullable": {"x-registry-field": "property", "type": "boolean" }, "max_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 } } - } - } -} diff --git a/projects/nagdi-agriculture/.vscode/extensions.json b/projects/nagdi-agriculture/.vscode/extensions.json deleted file mode 100644 index 6098b6e..0000000 --- a/projects/nagdi-agriculture/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "redhat.vscode-yaml" - ] -} diff --git a/projects/nagdi-agriculture/.vscode/settings.json b/projects/nagdi-agriculture/.vscode/settings.json deleted file mode 100644 index db92a93..0000000 --- a/projects/nagdi-agriculture/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "yaml.schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } -} diff --git a/projects/nagdi-agriculture/.zed/settings.json b/projects/nagdi-agriculture/.zed/settings.json deleted file mode 100644 index fac07ac..0000000 --- a/projects/nagdi-agriculture/.zed/settings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } - } - } - } - } -} diff --git a/projects/nagdi-agriculture/entities/nagdi_farmer.yaml b/projects/nagdi-agriculture/entities/nagdi_farmer.yaml deleted file mode 100644 index e978185..0000000 --- a/projects/nagdi-agriculture/entities/nagdi_farmer.yaml +++ /dev/null @@ -1,36 +0,0 @@ -version: 1 -id: nagdi_farmer -revision: 1 -primary_key: farmer_id -schema: - type: object - additionalProperties: false - required: - - farmer_id - - farmer_registered - - data_use_authorized - - active_smallholder_farmer - - active_farm_parcel - - crop_declared_for_season - - district_climate_risk_active - - voucher_entitlement_current - - voucher_not_redeemed - properties: - farmer_id: - type: string - minLength: 7 - maxLength: 16 - pattern: "^FR-[0-9]+$" - farmer_registered: { type: boolean } - data_use_authorized: { type: boolean } - active_smallholder_farmer: { type: boolean } - active_farm_parcel: { type: boolean } - crop_declared_for_season: { type: boolean } - district_climate_risk_active: { type: boolean } - voucher_entitlement_current: { type: boolean } - voucher_not_redeemed: { type: boolean } -materialization: - max_records: 1000000 - max_bytes: 128MiB - refresh: 1m - retain_generations: 2 diff --git a/projects/nagdi-agriculture/entities/nagdi_herd.yaml b/projects/nagdi-agriculture/entities/nagdi_herd.yaml deleted file mode 100644 index 282ba81..0000000 --- a/projects/nagdi-agriculture/entities/nagdi_herd.yaml +++ /dev/null @@ -1,36 +0,0 @@ -version: 1 -id: nagdi_herd -revision: 1 -primary_key: herd_id -schema: - type: object - additionalProperties: false - required: - - herd_id - - farmer_id - - registered_herd - - herd_vaccination_current - - origin_district_not_quarantined_for_species - - destination_district_open - - no_conflicting_open_movement_permit - properties: - herd_id: - type: string - minLength: 11 - maxLength: 11 - pattern: "^HERD-[0-9]{6}$" - farmer_id: - type: string - minLength: 7 - maxLength: 16 - pattern: "^FR-[0-9]+$" - registered_herd: { type: boolean } - herd_vaccination_current: { type: boolean } - origin_district_not_quarantined_for_species: { type: boolean } - destination_district_open: { type: boolean } - no_conflicting_open_movement_permit: { type: boolean } -materialization: - max_records: 1000000 - max_bytes: 128MiB - refresh: 1m - retain_generations: 2 diff --git a/projects/nagdi-agriculture/environments/hosted.yaml b/projects/nagdi-agriculture/environments/hosted.yaml deleted file mode 100644 index c7a20c5..0000000 --- a/projects/nagdi-agriculture/environments/hosted.yaml +++ /dev/null @@ -1,48 +0,0 @@ -version: 1 - -entities: - nagdi_farmer: - provider: - type: csv - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - header_row: 1 - columns: - farmer_id: farmer_id - farmer_registered: farmer_registered - data_use_authorized: data_use_authorized - active_smallholder_farmer: active_smallholder_farmer - active_farm_parcel: active_farm_parcel - crop_declared_for_season: crop_declared_for_season - district_climate_risk_active: district_climate_risk_active - voucher_entitlement_current: voucher_entitlement_current - voucher_not_redeemed: voucher_not_redeemed - source_revision: nagdi-farmer-voucher-v1 - generation: "2026-07-14" - nagdi_herd: - provider: - type: csv - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - header_row: 1 - columns: - herd_id: herd_id - farmer_id: farmer_id - registered_herd: registered_herd - herd_vaccination_current: herd_vaccination_current - origin_district_not_quarantined_for_species: origin_district_not_quarantined_for_species - destination_district_open: destination_district_open - no_conflicting_open_movement_permit: no_conflicting_open_movement_permit - source_revision: nagdi-livestock-movement-v1 - generation: "2026-07-14" - -relay: - origin: https://nagdi-relay.solmara.registrystack.org - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } - -deployment: - profile: hosted_lab - relay: { service: nagdi-agriculture-relay } diff --git a/projects/nagdi-agriculture/environments/local.yaml b/projects/nagdi-agriculture/environments/local.yaml deleted file mode 100644 index dc47f2a..0000000 --- a/projects/nagdi-agriculture/environments/local.yaml +++ /dev/null @@ -1,48 +0,0 @@ -version: 1 - -entities: - nagdi_farmer: - provider: - type: csv - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - header_row: 1 - columns: - farmer_id: farmer_id - farmer_registered: farmer_registered - data_use_authorized: data_use_authorized - active_smallholder_farmer: active_smallholder_farmer - active_farm_parcel: active_farm_parcel - crop_declared_for_season: crop_declared_for_season - district_climate_risk_active: district_climate_risk_active - voucher_entitlement_current: voucher_entitlement_current - voucher_not_redeemed: voucher_not_redeemed - source_revision: nagdi-farmer-voucher-v1 - generation: "2026-07-14" - nagdi_herd: - provider: - type: csv - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - header_row: 1 - columns: - herd_id: herd_id - farmer_id: farmer_id - registered_herd: registered_herd - herd_vaccination_current: herd_vaccination_current - origin_district_not_quarantined_for_species: origin_district_not_quarantined_for_species - destination_district_open: destination_district_open - no_conflicting_open_movement_permit: no_conflicting_open_movement_permit - source_revision: nagdi-livestock-movement-v1 - generation: "2026-07-14" - -relay: - origin: http://127.0.0.1:8080 - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } - -deployment: - profile: local - relay: { service: nagdi-agriculture-relay } diff --git a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/match.yaml b/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/match.yaml deleted file mode 100644 index 0434561..0000000 --- a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/match.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: farmer-voucher-match -classification: synthetic -input: { farmer_id: FR-1001 } -interactions: - - expect: { method: GET, path: /snapshot } - respond: - status: 200 - body: - farmer_registered: true - data_use_authorized: true - active_smallholder_farmer: true - active_farm_parcel: true - crop_declared_for_season: true - district_climate_risk_active: true - voucher_entitlement_current: true - voucher_not_redeemed: true -expect: - outcome: match - outputs: - farmer_registered: true - data_use_authorized: true - active_smallholder_farmer: true - active_farm_parcel: true - crop_declared_for_season: true - district_climate_risk_active: true - voucher_entitlement_current: true - voucher_not_redeemed: true diff --git a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/no-match.yaml b/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/no-match.yaml deleted file mode 100644 index 2ae0097..0000000 --- a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/no-match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: farmer-voucher-no-match -classification: synthetic -input: { farmer_id: FR-9999 } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: [] } -expect: - outcome: no_match - outputs: {} diff --git a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/integration.yaml b/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/integration.yaml deleted file mode 100644 index 60253df..0000000 --- a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/integration.yaml +++ /dev/null @@ -1,36 +0,0 @@ -version: 1 -id: farmer-voucher-by-farmer -revision: 1 - -input: - farmer_id: - role: selector - type: string - minLength: 7 - maxLength: 16 - pattern: "^FR-[0-9]+$" - -capability: - snapshot: - entity: nagdi_farmer - exact: - farmer_id: { input: farmer_id } - freshness: 7d - -outputs: - - farmer_registered - - data_use_authorized - - active_smallholder_farmer - - active_farm_parcel - - crop_declared_for_season - - district_climate_risk_active - - voucher_entitlement_current - - voucher_not_redeemed - -not_applicable: - ambiguity: - rationale: The exact farmer identifier is the entity primary key, whose materialized unique-key constraint permits at most one voucher record. - request_fixture: farmer-voucher-match - subject_mismatch: - rationale: The minimized output projection omits farmer_id, so it contains no identifier comparable with the requested farmer identifier. - request_fixture: farmer-voucher-match diff --git a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/ambiguous.yaml b/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/ambiguous.yaml deleted file mode 100644 index 56c9694..0000000 --- a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/ambiguous.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: livestock-movement-ambiguous -classification: synthetic -input: { farmer_id: FR-8888 } -interactions: - - expect: { method: GET, path: /snapshot } - respond: - status: 200 - body: - - registered_herd: true - herd_vaccination_current: true - origin_district_not_quarantined_for_species: true - destination_district_open: true - no_conflicting_open_movement_permit: true - - registered_herd: false - herd_vaccination_current: false - origin_district_not_quarantined_for_species: false - destination_district_open: false - no_conflicting_open_movement_permit: false -expect: { outcome: ambiguous, outputs: {} } diff --git a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/match.yaml b/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/match.yaml deleted file mode 100644 index e4d51b0..0000000 --- a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/match.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: livestock-movement-match -classification: synthetic -input: { farmer_id: FR-1001 } -interactions: - - expect: { method: GET, path: /snapshot } - respond: - status: 200 - body: - registered_herd: true - herd_vaccination_current: true - origin_district_not_quarantined_for_species: true - destination_district_open: true - no_conflicting_open_movement_permit: true -expect: - outcome: match - outputs: - registered_herd: true - herd_vaccination_current: true - origin_district_not_quarantined_for_species: true - destination_district_open: true - no_conflicting_open_movement_permit: true diff --git a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/no-match.yaml b/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/no-match.yaml deleted file mode 100644 index a55e732..0000000 --- a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/no-match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: livestock-movement-no-match -classification: synthetic -input: { farmer_id: FR-9999 } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: [] } -expect: - outcome: no_match - outputs: {} diff --git a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/integration.yaml b/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/integration.yaml deleted file mode 100644 index 023a76b..0000000 --- a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/integration.yaml +++ /dev/null @@ -1,30 +0,0 @@ -version: 1 -id: livestock-movement-by-farmer -revision: 1 - -input: - farmer_id: - role: selector - type: string - minLength: 7 - maxLength: 16 - pattern: "^FR-[0-9]+$" - -capability: - snapshot: - entity: nagdi_herd - exact: - farmer_id: { input: farmer_id } - freshness: 7d - -outputs: - - registered_herd - - herd_vaccination_current - - origin_district_not_quarantined_for_species - - destination_district_open - - no_conflicting_open_movement_permit - -not_applicable: - subject_mismatch: - rationale: The minimized output projection omits farmer_id, so it contains no identifier comparable with the requested farmer identifier. - request_fixture: livestock-movement-match diff --git a/projects/nagdi-agriculture/registry-stack.yaml b/projects/nagdi-agriculture/registry-stack.yaml deleted file mode 100644 index 545691a..0000000 --- a/projects/nagdi-agriculture/registry-stack.yaml +++ /dev/null @@ -1,103 +0,0 @@ -version: 1 - -registry: - id: solmara-nagdi-agriculture - -integrations: - farmer-voucher-by-farmer: - file: integrations/farmer-voucher-by-farmer/integration.yaml - livestock-movement-by-farmer: - file: integrations/livestock-movement-by-farmer/integration.yaml - -entities: - nagdi_farmer: - file: entities/nagdi_farmer.yaml - nagdi_herd: - file: entities/nagdi_herd.yaml - -services: - nagdi-farmer-records: - kind: records_api - entity: nagdi_farmer - title: NAgDI farmer records - description: Purpose-limited farmer facts used by the Solmara Evidence service. - owner: National Agriculture Data and Digital Innovation Agency - sensitivity: personal - access_rights: restricted - update_frequency: continuous - api: - scopes: - metadata: nagdi_farmer:metadata - rows: nagdi_farmer:rows - aggregate: nagdi_farmer:aggregate - evidence_verification: nagdi_farmer:evidence_verification - purposes: - - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - projection: - - farmer_id - - farmer_registered - - data_use_authorized - - active_smallholder_farmer - - active_farm_parcel - - crop_declared_for_season - - district_climate_risk_active - - voucher_entitlement_current - - voucher_not_redeemed - pagination: { default_limit: 2, max_limit: 2 } - filters: - farmer_id: [eq] - standards: { ogc_features: false, sp_dci: false } - - nagdi-farmer-consultation: - kind: consultation_api - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - legal_basis: agricultural-data-act - consent: not_required - consultations: - farmer: - integration: farmer-voucher-by-farmer - input: - farmer_id: request.target.identifiers.farmer_id - - nagdi-herd-consultation: - kind: consultation_api - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control - legal_basis: agricultural-data-act - consent: not_required - consultations: - herd: - integration: livestock-movement-by-farmer - input: - farmer_id: request.target.identifiers.farmer_id - - nagdi-herd-records: - kind: records_api - entity: nagdi_herd - title: NAgDI herd records - description: Purpose-limited herd facts used by the Solmara Evidence service. - owner: National Agriculture Data and Digital Innovation Agency - sensitivity: personal - access_rights: restricted - update_frequency: continuous - api: - scopes: - metadata: nagdi_herd:metadata - rows: nagdi_herd:rows - aggregate: nagdi_herd:aggregate - evidence_verification: nagdi_herd:evidence_verification - purposes: - - https://id.registrystack.org/solmara/purpose/livestock-movement-control - projection: - - herd_id - - farmer_id - - registered_herd - - herd_vaccination_current - - origin_district_not_quarantined_for_species - - destination_district_open - - no_conflicting_open_movement_permit - pagination: { default_limit: 2, max_limit: 2 } - filters: - farmer_id: [eq] - standards: { ogc_features: false, sp_dci: false } diff --git a/projects/nia-population/.registry-stack-editor/manifest.json b/projects/nia-population/.registry-stack-editor/manifest.json deleted file mode 100644 index 3603a8e..0000000 --- a/projects/nia-population/.registry-stack-editor/manifest.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "format": "registry.stack.editor-manifest", - "version": 1, - "registryctl_version": "0.15.2", - "schemas": [ - { - "kind": "project", - "path": "schemas/project.schema.json", - "file_glob": "registry-stack.yaml", - "sha256": "sha256:9fd76663c866c52bb5a17a4cfea821049b7428c812f1666da9db0d43a00da144" - }, - { - "kind": "environment", - "path": "schemas/environment.schema.json", - "file_glob": "environments/*.yaml", - "sha256": "sha256:d16828629e001a30a1705af90bcc12a214be18cf5805a9833e65b03d631c9e9b" - }, - { - "kind": "integration", - "path": "schemas/integration.schema.json", - "file_glob": "integrations/*/integration.yaml", - "sha256": "sha256:2e45452f63c3c610ed14b79244bc9aa086d79d811ad0899223b36c160d449a79" - }, - { - "kind": "fixture", - "path": "schemas/fixture.schema.json", - "file_glob": "integrations/*/fixtures/*.yaml", - "sha256": "sha256:98c0ab21e35cecd143b51e4b5dd487de07ceea24e505bbe351d212e0628c8c8c" - }, - { - "kind": "entity", - "path": "schemas/entity.schema.json", - "file_glob": "entities/*.yaml", - "sha256": "sha256:4cbb31c8addeb54f8b57aaaff8f30c39e651a21a2d741a1b775ecb0d7c248342" - } - ] -} diff --git a/projects/nia-population/.registry-stack-editor/schemas/entity.schema.json b/projects/nia-population/.registry-stack-editor/schemas/entity.schema.json deleted file mode 100644 index ffc57c8..0000000 --- a/projects/nia-population/.registry-stack-editor/schemas/entity.schema.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/entity.v1.json", - "title": "Registry Stack project entity v1", - "description": "Defines a bounded, materialized entity that Relay can query without coupling the project to a specific source product.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "primary_key", "schema", "materialization"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Entity authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the entity." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this entity definition.", "examples": [1] }, - "primary_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Field whose value uniquely identifies each materialized record.", "examples": ["person_id"] }, - "schema": {"x-registry-field": "property", "$ref": "#/$defs/objectSchema", "description": "Closed JSON object schema for materialized records." }, - "materialization": { - "x-registry-field": "property", - "description": "Authored resource, refresh, and bounded recovery-set retention applied while building entity generations.", - "type": "object", - "additionalProperties": false, - "required": ["max_records", "max_bytes", "refresh", "retain_generations"], - "properties": { - "max_records": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 100000000, "description": "Maximum number of records allowed in one generation.", "examples": [10000] }, - "max_bytes": { - "x-registry-field": "property", - "description": "Maximum encoded size of one generation, up to 1 GiB, as bytes or a KiB/MiB quantity.", - "examples": ["16MiB"], - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1073741824 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,5}|10[0-3][0-9]{4}|104[0-7][0-9]{3}|1048[0-4][0-9]{2}|10485[0-6][0-9]|104857[0-6])KiB|(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])MiB)$" } - ] - }, - "refresh": { - "x-registry-field": "property", - "description": "Refresh cadence as a canonical positive duration no greater than 30 days, or manual when an operator initiates every refresh.", - "examples": ["1h"], - "oneOf": [ - {"x-registry-field": "branch", "const": "manual" }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-4][0-9]{8}|25[0-8][0-9]{7}|259[0-1][0-9]{6}|2592000000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-4][0-9]{5}|25[0-8][0-9]{4}|259[0-1][0-9]{3}|2592000)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-2][0-9]{3}|43[0-1][0-9]{2}|43200)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-1][0-9]{1}|720)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30)d)$" } - ] - }, - "retain_generations": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16, "description": "Number of completed cache generations, including the active generation, retained as a bounded recovery set after successful publication. This does not make arbitrary retained generations selectable for rollback.", "examples": [2] } - } - } - }, - "examples": [ - { - "version": 1, - "id": "people", - "revision": 1, - "primary_key": "person_id", - "schema": { - "type": "object", - "additionalProperties": false, - "required": ["person_id"], - "properties": { "person_id": { "type": "string" } } - }, - "materialization": { - "max_records": 10000, - "max_bytes": "16MiB", - "refresh": "1h", - "retain_generations": 2 - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "propertyName": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$", "description": "Portable lowercase record field name." }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for a nullable field.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "items": false, - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "fieldSchema": { - "description": "Bounded schema keywords supported for one scalar materialized field.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "enum": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true }, - "const": {"x-registry-field": "property"}, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 65536 }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 } - } - }, - "objectSchema": { - "description": "Closed object schema whose declared required fields must appear in properties.", - "type": "object", - "additionalProperties": false, - "required": ["type", "additionalProperties", "required", "properties"], - "properties": { - "type": {"x-registry-field": "property", "const": "object" }, - "additionalProperties": {"x-registry-field": "property", "const": false }, - "required": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 256, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/propertyName" } - }, - "properties": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 256, - "propertyNames": {"x-registry-field": "property", "$ref": "#/$defs/propertyName" }, - "additionalProperties": {"x-registry-field": "property", "$ref": "#/$defs/fieldSchema" } - } - } - } - } -} diff --git a/projects/nia-population/.registry-stack-editor/schemas/environment.schema.json b/projects/nia-population/.registry-stack-editor/schemas/environment.schema.json deleted file mode 100644 index f04c70b..0000000 --- a/projects/nia-population/.registry-stack-editor/schemas/environment.schema.json +++ /dev/null @@ -1,337 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/environment.v1.json", - "title": "Registry Stack project environment v1", - "description": "Binds a Registry Stack project to environment-specific services, sources, credentials, and deployment topology.", - "type": "object", - "additionalProperties": false, - "required": ["version", "deployment"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Environment authoring format version." }, - "integrations": {"x-registry-field": "property", "type": "object", "maxProperties": 16, "description": "Environment bindings keyed by authored integration identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/integration" } }, - "entities": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "description": "Environment-specific source bindings keyed by entity identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/entity" } }, - "issuance": { - "x-registry-field": "property", - "description": "Notary issuer and signing-key bindings for credential issuance.", - "type": "object", "additionalProperties": false, "required": ["issuer", "signing_key", "signing_kid", "generation"], - "properties": { "issuer": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1 }, "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" }, "algorithm": {"x-registry-field": "property", "enum": ["EdDSA", "ES256"], "default": "EdDSA", "description": "Credential issuer signing algorithm. Holder proof remains EdDSA with did:jwk." }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "callers": { - "x-registry-field": "property", - "description": "Notary API-key caller identities and the scopes each identity receives.", - "type": "object", "maxProperties": 64, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["api_key_fingerprint", "scopes"], - "properties": { "api_key_fingerprint": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } - } - }, - "relay": { - "x-registry-field": "property", - "description": "Public Relay identity and token-validation settings for a Relay deployment.", - "type": "object", "additionalProperties": false, - "required": ["origin", "issuer", "jwks_url", "audience", "allowed_clients"], - "properties": { "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "allowed_clients": {"x-registry-field": "sensitive_property", "type": "array", "maxItems": 64, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } }, "local_api_keys": {"x-registry-field": "property", "description": "Local-profile-only synthetic principals and scopes. Registryctl generates the raw keys and supplies only their fingerprints to Relay.", "type": "object", "additionalProperties": false, "required": ["match_principal", "no_match_principal", "scopes"], "properties": { "match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "no_match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } } } - }, - "notary_relay": { - "x-registry-field": "property", - "description": "Deployment-internal Relay connection, Notary workload identity, and token file used only when Notary consults Relay.", - "type": "object", "additionalProperties": false, "required": ["base_url", "workload_client_id", "token_file"], - "properties": { "base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/internalOrigin" }, "workload_client_id": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "token_file": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" } } - }, - "relay_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Relay-owned consultation PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Notary-owned PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_cel": { - "x-registry-field": "property", - "description": "Optional per-worker data/address-space ceiling for dedicated Notary CEL processes. The Notary default is 134217728 bytes; 1073741824 is the maximum emulation-compatible exception and remains a limit, not an allocation.", - "type": "object", "additionalProperties": false, "required": ["worker_memory_bytes"], - "properties": { - "worker_memory_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 33554432, "maximum": 1073741824 } - } - }, - "oid4vci": { - "x-registry-field": "property", - "description": "Explicit registry-backed holder-wallet OID4VCI binding for one authored Notary credential profile.", - "$ref": "#/$defs/oid4vci" - }, - "deployment": { - "x-registry-field": "property", - "description": "Products deployed in this environment and the operational profile they use.", - "type": "object", "additionalProperties": false, "required": ["profile"], - "properties": { "profile": {"x-registry-field": "property", "enum": ["local", "hosted_lab", "production", "evidence_grade"] }, "relay": {"x-registry-field": "property", "$ref": "#/$defs/service" }, "notary": {"x-registry-field": "property", "$ref": "#/$defs/service" } }, - "anyOf": [{"x-registry-field": "branch", "required": ["relay"] }, {"x-registry-field": "branch", "required": ["notary"] }] - } - }, - "allOf": [ - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["deployment"], "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } }, - "then": {"x-registry-field": "branch", "required": ["relay"] }, - "else": {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "required": ["relay"] } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_relay"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay", "notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["relay_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_cel"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["oid4vci"] }, - "then": { - "x-registry-field": "branch", - "required": ["notary_state"], - "properties": { - "deployment": {"x-registry-field": "property", "required": ["notary"] } - } - }, - "else": {"x-registry-field": "branch", "properties": { "callers": {"x-registry-field": "property", "minProperties": 1 } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["profile"], "properties": { "profile": {"x-registry-field": "property", "const": "local" } } } } }, - "else": { - "x-registry-field": "branch", - "properties": { - "relay": { - "x-registry-field": "property", - "not": {"x-registry-field": "branch", "required": ["local_api_keys"] }, - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - }, - "oid4vci": { - "x-registry-field": "property", - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorization_server": { - "x-registry-field": "property", - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - } - } - } - } - } - } - ], - "examples": [ - { - "version": 1, - "deployment": { - "profile": "local", - "notary": { "service": "notary" } - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token2048": { "type": "string", "minLength": 1, "maxLength": 2048, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII token, including full verification-method identifiers." }, - "secret": { "type": "object", "additionalProperties": false, "description": "Reference to a process-environment variable; secret values are never authored here.", "required": ["secret"], "properties": { "secret": {"x-registry-field": "secret_reference_property", "type": "string", "pattern": "^[A-Z_][A-Z0-9_]{0,127}$" } } }, - "origin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/?$", "description": "HTTPS origin without path, query, or fragment." }, - "httpsResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/[^?#]+$", "description": "Exact HTTPS resource without query or fragment." }, - "localLoopbackOrigin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/?$", "description": "HTTP IP-loopback origin; public Relay and issuer fields restrict it to the local profile, while internal Notary-to-Relay connections allow it in any profile." }, - "localLoopbackResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/[^?#]+$", "description": "HTTP IP-loopback resource accepted only with the local deployment profile." }, - "internalOrigin": { "description": "Deployment-internal HTTPS or HTTP IP-loopback origin.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayOrigin": { "description": "Relay or issuer origin using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayResource": { "description": "Relay authentication resource using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/httpsResource" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackResource" }] }, - "oid4vci": { - "description": "Closed OID4VCI trust, key, subject, wallet, and credential-profile binding.", - "type": "object", - "additionalProperties": false, - "required": ["public_base_url", "credential", "authorization_server", "client", "access_token", "sensitive_state_key", "subject", "redirect_uri", "allowed_wallet_origins"], - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "credential": { - "x-registry-field": "property", - "description": "Existing project service and credential profile exposed through OID4VCI.", - "type": "object", "additionalProperties": false, "required": ["service", "profile"], - "properties": { - "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "profile": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "authorization_server": { - "x-registry-field": "property", - "description": "Pinned eSignet issuer and exact endpoints used for login and token validation.", - "type": "object", "additionalProperties": false, "required": ["issuer", "jwks_url", "userinfo_url", "authorize_url", "token_url"], - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" } - } - }, - "client": { - "x-registry-field": "property", - "description": "eSignet relying-party identity and dedicated private-key reference.", - "type": "object", "additionalProperties": false, "required": ["id", "signing_key", "signing_kid"], - "properties": { - "id": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "access_token": { - "x-registry-field": "property", - "description": "Dedicated Notary access-token signing key and published key identifier.", - "type": "object", "additionalProperties": false, "required": ["signing_key", "signing_kid"], - "properties": { - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "sensitive_state_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "subject": { - "x-registry-field": "property", - "description": "Verified eSignet userinfo claim bound exactly to the credential subject identifier.", - "type": "object", "additionalProperties": false, "required": ["token_claim", "id_type"], - "properties": { - "token_claim": {"x-registry-field": "property", "$ref": "#/$defs/token256" }, - "id_type": {"x-registry-field": "property", "$ref": "#/$defs/token256" } - } - }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "allowed_wallet_origins": { - "x-registry-field": "sensitive_property", - "description": "Exact HTTPS browser origins admitted to the wallet-facing Notary routes.", - "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "$ref": "#/$defs/origin" } - }, - "tx_code": { - "x-registry-field": "property", - "description": "Transaction-code policy. Omit for the secure required-PIN default. Set required=false only for a bounded bearer-offer interoperability profile; the compiler fixes the offer lifetime at 300 seconds.", - "type": "object", "additionalProperties": false, - "properties": { "required": {"x-registry-field": "property", "type": "boolean", "default": true } } - } - } - }, - "privateCidrs": { - "description": "Explicit private network ranges this source may resolve to.", - "type": "array", "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "type": "string", "minLength": 3, "maxLength": 64 } - }, - "ca": { - "description": "Pinned certificate-authority file and its rotation generation.", - "type": "object", "additionalProperties": false, "required": ["file", "generation"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "mtls": { - "description": "Client certificate and private-key reference for mutual TLS.", - "type": "object", "additionalProperties": false, "required": ["certificate_file", "private_key", "generation"], - "properties": { "certificate_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "private_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "endpoint": { - "description": "Security-bound HTTPS endpoint with optional private-network, CA, and mTLS policy.", - "type": "object", "additionalProperties": false, "required": ["origin", "path", "generation"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//)(?!.*[?#]).+$" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } - } - }, - "credential": { - "description": "One supported source credential shape, containing references rather than values.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["username", "password", "generation"], "properties": { "username": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "password": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["token", "generation"], "properties": { "token": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["client_id", "client_secret", "generation"], "properties": { "client_id": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "client_secret": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["value", "generation"], "properties": { "value": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } } - ] - }, - "provider": { - "description": "Environment-specific physical provider for a materialized entity.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "csv" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "delimiter": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 }, "quote": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "project_file", "path", "sheet"], "properties": { "type": {"x-registry-field": "property", "const": "xlsx" }, "project_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "sheet": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "data_range": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "parquet" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "connection", "schema", "table"], "properties": { "type": {"x-registry-field": "property", "const": "postgres" }, "connection": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "schema": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" }, "table": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" } } } - ] - }, - "entity": { - "description": "Maps an authored entity to provider columns and immutable source-generation identifiers.", - "type": "object", "additionalProperties": false, "required": ["provider", "columns", "source_revision", "generation"], - "properties": { "provider": {"x-registry-field": "property", "$ref": "#/$defs/provider" }, "columns": {"x-registry-field": "property", "type": "object", "minProperties": 1, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } }, "source_revision": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "generation": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 } } - }, - "source": { - "description": "Runtime source connection policy for one authored integration.", - "type": "object", "additionalProperties": false, "required": ["origin"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "credential": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "oauth": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "jwks": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "rate": {"x-registry-field": "property", "type": "object", "additionalProperties": false, "required": ["per_minute", "burst"], "properties": { "per_minute": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 60000 }, "burst": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 1024 } } }, - "concurrency": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 64 }, - "timeout": { - "x-registry-field": "property", - "description": "Optional request timeout no greater than 20 seconds; generated bindings narrow it to the integration deadline.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - }, - "integration": { - "description": "Environment binding for an authored source integration.", - "type": "object", "additionalProperties": false, "required": ["source"], - "properties": { "source": {"x-registry-field": "property", "$ref": "#/$defs/source" } } - }, - "service": { "type": "object", "additionalProperties": false, "description": "Binds an authored product role to a deployed service identifier.", "required": ["service"], "properties": { "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } }, - "token256": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII protocol token." }, - "postgresIdentifier": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,62}$", "description": "Portable unquoted PostgreSQL schema or table identifier." }, - "absolutePath": { "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized absolute path without dot segments or duplicate separators." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized project-relative path without dot segments or duplicate separators." } - } -} diff --git a/projects/nia-population/.registry-stack-editor/schemas/fixture.schema.json b/projects/nia-population/.registry-stack-editor/schemas/fixture.schema.json deleted file mode 100644 index b853192..0000000 --- a/projects/nia-population/.registry-stack-editor/schemas/fixture.schema.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/fixture.v1.json", - "title": "Registry Stack project integration fixture v1", - "description": "Defines one synthetic, deterministic integration scenario for offline adapter verification.", - "type": "object", - "additionalProperties": false, - "required": ["name", "classification", "input", "interactions", "expect"], - "properties": { - "name": {"x-registry-field": "public_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Human-readable scenario name shown in test output.", "examples": ["existing household matches"] }, - "classification": {"x-registry-field": "public_property", "const": "synthetic", "description": "Declares that fixture data is synthetic and safe for offline testing." }, - "request": { - "x-registry-field": "redacted_fixture_property", - "$ref": "#/$defs/governedRequest", - "description": "Optional independently authored synthetic Notary request used to prove the request-to-consultation binding before Relay access." - }, - "input": { - "x-registry-field": "redacted_fixture_property", - "description": "Typed consultation inputs supplied to the adapter under test.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "type": ["string", "boolean", "integer", "null"] - } - }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Named date values available to deterministic fixture interpolation.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "interactions": { - "x-registry-field": "redacted_fixture_property", - "description": "Ordered upstream request and response exchanges expected during execution.", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/interaction" } - }, - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/expectation", "description": "Observable adapter result required for the scenario to pass." } - }, - "examples": [ - { - "name": "existing household matches", - "classification": "synthetic", - "input": { "household_id": "HH-1001" }, - "interactions": [ - { - "expect": { "method": "GET", "path": "/households/HH-1001" }, - "respond": { "status": 200, "body": { "active": true } } - } - ], - "expect": { "outcome": "match", "outputs": { "active": true } } - } - ], - "$defs": { - "governedRequest": { - "description": "The same closed governed request shape accepted by project test --live.", - "type": "object", - "additionalProperties": false, - "required": ["target", "claims", "purpose"], - "properties": { - "target": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/governedTarget" }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies synthetic date variables to the governed evaluation request.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "claims": { - "x-registry-field": "redacted_fixture_property", - "description": "Lists the authored claims evaluated by this synthetic request witness.", - "type": "array", - "minItems": 1, - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedClaimRef" } - }, - "disclosure": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the disclosure mode requested from the authored claim policy." }, - "format": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the claim-result media type requested from the governed Notary path." }, - "purpose": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Names the authored service purpose exercised by this synthetic request witness." } - } - }, - "governedTarget": { - "description": "The synthetic subject target presented to the same governed request boundary as a live Notary evaluation.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 64, "description": "Names the authored target entity type used for claim evaluation." }, - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies an optional synthetic direct target identifier." }, - "identifiers": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored synthetic identifiers for consultation input mapping.", - "type": "array", - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedIdentifier" } - }, - "attributes": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored typed target attributes for consultation input mapping.", - "type": "object", - "maxProperties": 16, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": ["string", "boolean", "integer", "null"] } - } - } - }, - "governedIdentifier": { - "description": "One synthetic target identifier with an authored scheme and string value.", - "type": "object", - "additionalProperties": false, - "required": ["scheme", "value"], - "properties": { - "scheme": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 96, "description": "Names the authored identifier scheme selected by a consultation input mapping." }, - "value": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies the synthetic string value bound to this identifier scheme." } - } - }, - "governedClaimRef": { - "description": "A requested claim ID, optionally pinned to one authored claim version.", - "oneOf": [ - {"x-registry-field": "branch", "type": "string"}, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Names one authored claim requested by this fixture witness."}, - "version": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Pins the requested claim to one authored claim-policy version."} - } - } - ] - }, - "interaction": { - "description": "One expected upstream request paired with its synthetic response.", - "type": "object", - "additionalProperties": false, - "required": ["expect", "respond"], - "properties": { - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/request" }, - "respond": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/response" } - } - }, - "request": { - "description": "Exact HTTP request shape the adapter must produce.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "redacted_fixture_property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "query": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 64, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "oneOf": [ - {"x-registry-field": "branch", "type": ["string", "boolean", "integer", "null"] }, - { - "x-registry-field": "branch", - "type": "array", - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "type": ["string", "boolean", "integer", "null"] } - } - ] - } - }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - "response": { - "description": "Synthetic HTTP response or timeout returned to the adapter.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["status"], - "properties": { - "status": {"x-registry-field": "redacted_fixture_property", "type": "integer", "minimum": 100, "maximum": 599 }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["timeout"], - "properties": { - "timeout": { - "x-registry-field": "redacted_fixture_property", - "description": "Reviewed synthetic timeout intent no greater than 20 seconds; offline execution returns a timeout outcome without sleeping.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - ] - }, - "fixtureBody": { - "description": "Inline JSON, or an exact closed reference to a strict JSON file below the fixture bodies directory. The top-level file key is reserved for file references.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": { - "x-registry-field": "redacted_fixture_property", - "type": "string", - "minLength": 8, - "maxLength": 4096, - "pattern": "^bodies/(?!\\.\\.?/)(?!.*(?:/)\\.\\.?/)(?!.*//).+$" - } - } - }, - { - "x-registry-field": "branch", - "not": { - "x-registry-field": "branch", - "type": "object", - "required": ["file"] - } - } - ] - }, - "expectation": { - "description": "Expected normalized outcome, output values, claims, or error.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "outcome": {"x-registry-field": "redacted_fixture_property", "enum": ["match", "no_match", "ambiguous"] }, - "outputs": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "claims": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "error": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256 } - } - } - } -} diff --git a/projects/nia-population/.registry-stack-editor/schemas/integration.schema.json b/projects/nia-population/.registry-stack-editor/schemas/integration.schema.json deleted file mode 100644 index 9bb2ad0..0000000 --- a/projects/nia-population/.registry-stack-editor/schemas/integration.schema.json +++ /dev/null @@ -1,514 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/integration.v1.json", - "title": "Registry Stack project integration v1", - "description": "Defines a product-neutral source adaptation contract using bounded HTTP, Rhai script, or exact snapshot capabilities.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "input", "capability", "outputs"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Integration authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the integration." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this integration contract.", "examples": [1] }, - "source": {"x-registry-field": "property", "$ref": "#/$defs/source", "description": "Optional source product metadata and transport policy; capabilities remain product-neutral." }, - "input": { - "x-registry-field": "property", - "description": "Typed selector and parameter inputs accepted by this integration.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/input" } - }, - "capability": {"x-registry-field": "property", "$ref": "#/$defs/capability", "description": "Exactly one bounded execution mechanism for the source adaptation." }, - "outputs": { - "x-registry-field": "property", - "description": "Named typed outputs, or a shorthand list of output names inferred by the authoring compiler.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": { - "x-registry-field": "map_key", - "allOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputName" }, - {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "enum": ["matched", "outcome"] } } - ] - }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/output" } - }, - { - "x-registry-field": "branch", - "type": "array", - "minItems": 1, - "maxItems": 64, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/inputName" } - } - ] - }, - "limits": {"x-registry-field": "property", "$ref": "#/$defs/limits", "description": "Optional tighter resource limits for one integration execution." }, - "not_applicable": {"x-registry-field": "property", "$ref": "#/$defs/notApplicable", "description": "Explicit rationale and request-fixture evidence for a normally required outcome that the source contract cannot produce." } - }, - "examples": [ - { - "version": 1, - "id": "household_lookup", - "revision": 1, - "input": { - "household_id": { "role": "selector", "type": "string" } - }, - "capability": { - "script": { "file": "adapter.rhai" } - }, - "outputs": { - "active": { "type": "boolean" } - } - } - ], - "$defs": { - "notApplicable": { - "description": "Closed declarations for outcomes that the source protocol, exact singleton endpoint, or local unique-key constraint proves impossible.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "ambiguity": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed source contract cannot produce more than one matching record." }, - "subject_mismatch": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed response contract contains no identifier comparable with a requested selector." } - } - }, - "notApplicableReason": { - "description": "A reviewable reason that names a real request fixture proving the source access shape used by the rationale.", - "type": "object", - "additionalProperties": false, - "required": ["rationale", "request_fixture"], - "properties": { - "rationale": {"x-registry-field": "property", "type": "string", "minLength": 24, "maxLength": 512, "description": "Why the named normally required outcome cannot occur for this source contract." }, - "request_fixture": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Fixture name containing the supporting bounded source request." } - } - }, - "stableId": { - "description": "Lowercase stable identifier used in project references.", - "type": "string", - "pattern": "^[a-z][a-z0-9._-]{0,95}$" - }, - "inputName": { - "description": "Portable lowercase name for an integration input or output.", - "type": "string", - "pattern": "^[a-z][a-z0-9_]{0,63}$" - }, - "relativePath": { - "description": "Project-relative path without traversal or duplicate separators.", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" - }, - "integrationResponseByteSize": { - "description": "Maximum bytes accepted from one source response, defaulting to 512 KiB and capped at 8 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "512KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 8388608 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|[1-7][0-9]{3}|80[0-9]{2}|81[0-8][0-9]|819[0-2])KiB|[1-8]MiB)$" } - ] - }, - "integrationRequestByteSize": { - "description": "Maximum aggregate author-controlled request bytes for one execution, defaulting to 64 KiB and capped at 1 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "64KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1048576 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])KiB|1MiB)$" } - ] - }, - "integrationSourceByteSize": { - "description": "Maximum aggregate source-response bytes for one execution, defaulting to 2 MiB and capped at 16 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "2MiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 16777216 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-5][0-9]{3}|16[0-2][0-9]{2}|163[0-7][0-9]|1638[0-4])KiB|(?:[1-9]|1[0-6])MiB)$" } - ] - }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for nullable values.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "input": { - "description": "Type, validation, and canonicalization rules for one authored input.", - "type": "object", - "additionalProperties": false, - "required": ["role", "type"], - "properties": { - "role": {"x-registry-field": "property", "enum": ["selector", "parameter"] }, - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "description": "Maximum Unicode scalar-value length. The 1024-character ceiling derives a conservative 4096-byte UTF-8 ceiling for one canonical input.", "type": "integer", "minimum": 1, "maximum": 1024 }, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 16384 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "canonicalization": {"x-registry-field": "property", "enum": ["identity", "ascii_lowercase"] } - } - }, - "versions": { - "description": "Source versions tested by the project and versions explicitly accepted as unverified.", - "type": "object", - "additionalProperties": false, - "properties": { - "tested": {"x-registry-field": "property", "$ref": "#/$defs/versionList" }, - "unverified": {"x-registry-field": "property", "$ref": "#/$defs/versionList" } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["tested"], "properties": { "tested": {"x-registry-field": "property", "minItems": 1 } } }, - {"x-registry-field": "branch", "required": ["unverified"], "properties": { "unverified": {"x-registry-field": "property", "minItems": 1 } } } - ] - }, - "versionList": { - "description": "Bounded list of source version labels.", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } - }, - "credential": { - "description": "Authentication mechanism expected by the source adapter, without environment secret values.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "enum": ["none", "basic", "static_bearer"] } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_header" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_query" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9._:~-]{0,95}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "request", "response_profile"], - "properties": { - "type": {"x-registry-field": "property", "const": "oauth2_client_credentials" }, - "request": {"x-registry-field": "property", "enum": ["form", "json"] }, - "response_profile": {"x-registry-field": "property", "const": "oauth2_bearer" }, - "scope": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 }, - "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "refresh_skew": { - "x-registry-field": "property", - "description": "Positive OAuth refresh interval below the generated token contract's 60-second minimum lifetime.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|[1-5][0-9]{4})ms|(?:[1-9]|[1-5][0-9])s)$" - } - } - } - ] - }, - "allowRule": { - "description": "One read-only upstream method and path pattern the adapter may call.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": { - "x-registry-field": "sensitive_property", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^/[^?#]*$" - }, - "semantics": {"x-registry-field": "property", "const": "read_only" } - } - }, - "source": { - "description": "Optional source metadata, authentication shape, allowlist, and protocol profile.", - "type": "object", - "additionalProperties": false, - "required": ["auth"], - "properties": { - "product": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "versions": {"x-registry-field": "property", "$ref": "#/$defs/versions" }, - "auth": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "allow": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/allowRule" } - }, - "request_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "format": {"x-registry-field": "property", "enum": ["json", "text"] }, - "max_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationResponseByteSize" } - } - }, - "protocol": {"x-registry-field": "property", "$ref": "#/$defs/protocol" } - } - }, - "protocol": { - "description": "Optional interoperable protocol profiles layered over the source transport.", - "type": "object", - "additionalProperties": false, - "properties": { - "signed_dci": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["profile", "path", "jwks_profile", "sender", "receiver", "registry_type", "record_type", "locale", "selectors"], - "properties": { - "profile": {"x-registry-field": "property", "const": "dci-search-v1" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^/[^?#]*$" }, - "jwks_profile": {"x-registry-field": "property", "const": "rsa-signing-jwks-v1" }, - "sender": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "receiver": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "registry_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "record_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "locale": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$" }, - "selectors": { - "x-registry-field": "property", - "type": "object", "minProperties": 1, "maxProperties": 8, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["field", "response_pointer"], - "properties": { - "field": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 160 }, - "response_pointer": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" } - } - } - } - } - } - }, - "minProperties": 1 - }, - "inputReference": { - "description": "Reference to a declared integration input.", - "type": "object", - "additionalProperties": false, - "required": ["input"], - "properties": { "input": {"x-registry-field": "property", "$ref": "#/$defs/inputName" } } - }, - "inputOrLiteral": { - "description": "Request value supplied from a declared input or an authored scalar literal.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputReference" }, - {"x-registry-field": "branch", "type": ["string", "boolean", "integer"] } - ] - }, - "httpRequest": { - "description": "Bounded read-only HTTP request template.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "semantics": {"x-registry-field": "property", "const": "read_only" }, - "query": {"x-registry-field": "property", "type": "object", "maxProperties": 64, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "headers": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "body": { "x-registry-field": "property" } - } - }, - "httpResponse": { - "description": "Rules for translating upstream status and body cardinality into normalized outcomes.", - "type": "object", - "additionalProperties": false, - "properties": { - "no_match": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "ambiguous": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "shape": { - "x-registry-field": "property", - "oneOf": [ - {"x-registry-field": "branch", "const": "singleton" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["records", "cardinality"], - "properties": { - "records": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" }, - "cardinality": {"x-registry-field": "property", "const": "probe_two" } - } - } - ] - } - } - }, - "capability": { - "description": "Closed choice among direct HTTP, Rhai script, and exact snapshot execution.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["http"], - "properties": { - "http": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["request"], - "properties": { - "request": {"x-registry-field": "property", "$ref": "#/$defs/httpRequest" }, - "response": {"x-registry-field": "property", "$ref": "#/$defs/httpResponse" } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["script"], - "properties": { - "script": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, - "modules": { - "x-registry-field": "sensitive_property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/relativePath" } - } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["snapshot"], - "properties": { - "snapshot": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["entity", "exact", "freshness"], - "properties": { - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "exact": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 8, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputReference" } - }, - "freshness": { - "x-registry-field": "property", - "type": "string", - "description": "Maximum accepted snapshot age as a canonical positive duration no greater than 31 days.", - "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-5][0-9]{8}|26[0-6][0-9]{7}|267[0-7][0-9]{6}|2678[0-3][0-9]{5}|2678400000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-5][0-9]{5}|26[0-6][0-9]{4}|267[0-7][0-9]{3}|2678[0-3][0-9]{2}|2678400)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-3][0-9]{3}|44[0-5][0-9]{2}|446[0-3][0-9]{1}|44640)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-3][0-9]{1}|74[0-3]|744)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30|31)d)$" - } - } - } - } - } - ] - }, - "output": { - "description": "Type and optional source pointer for one normalized integration output.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "x-registry-source": { - "x-registry-field": "property", - "type": "string", - "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" - } - } - }, - "limits": { - "description": "Per-execution call, byte, and deadline budgets.", - "type": "object", - "additionalProperties": false, - "properties": { - "calls": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16 }, - "request_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationRequestByteSize" }, - "source_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationSourceByteSize" }, - "deadline": { - "x-registry-field": "property", - "description": "Positive total integration deadline no greater than 20 seconds.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - } -} diff --git a/projects/nia-population/.registry-stack-editor/schemas/project.schema.json b/projects/nia-population/.registry-stack-editor/schemas/project.schema.json deleted file mode 100644 index 91b7ea7..0000000 --- a/projects/nia-population/.registry-stack-editor/schemas/project.schema.json +++ /dev/null @@ -1,537 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/project.v1.json", - "title": "Registry Stack project v1", - "description": "Declares the product-neutral Registry Stack project graph: integrations, materialized entities, and Relay or Notary services.", - "type": "object", - "additionalProperties": false, - "required": ["version", "registry", "services"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Project authoring format version." }, - "starter": { - "x-registry-field": "property", - "description": "Immutable provenance for a workspace initialized from a Registry Stack starter. The digest excludes this content_digest field and covers the starter's authored project files.", - "type": "object", - "additionalProperties": false, - "required": ["id", "release", "content_digest"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Registry Stack starter identifier." }, - "release": {"x-registry-field": "property", "$ref": "#/$defs/token", "description": "Registry Stack release that supplied the starter." }, - "content_digest": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^sha256:[0-9a-f]{64}$", "description": "Digest of the initialized starter authoring content, used to report later workspace divergence." } - } - }, - "registry": { - "x-registry-field": "property", - "description": "Stable identity of the Registry Stack project.", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } - }, - "integrations": { - "x-registry-field": "property", - "description": "Source adaptation definitions keyed by project-local integration identifier.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "entities": { - "x-registry-field": "property", - "description": "Materialized entity definitions keyed by project-local entity identifier.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "services": { - "x-registry-field": "property", - "description": "Relay records APIs and Notary evidence services exposed by the project.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/service" } - } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["integrations"], "properties": { "integrations": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["entities"], "properties": { "entities": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["services"], "properties": { "services": {"x-registry-field": "property", "minProperties": 1 } } } - ], - "examples": [ - { - "version": 1, - "registry": { "id": "benefits-registry" }, - "integrations": { - "household_lookup": { "file": "integrations/household-lookup/integration.yaml" } - }, - "services": {} - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Non-empty token without commas, whitespace, or control characters." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Project-relative path without traversal or duplicate separators." }, - "text": { "type": "string", "minLength": 1, "maxLength": 2048, "description": "Bounded human-readable project metadata." }, - "scope": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Authorization scope token." }, - "recordsApi": { - "description": "Relay records API behavior, authorization, query bounds, and standards profiles.", - "type": "object", - "additionalProperties": false, - "required": ["scopes", "projection", "pagination", "standards"], - "properties": { - "scopes": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["metadata", "rows"], - "properties": { - "metadata": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "rows": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "evidence_verification": {"x-registry-field": "property", "$ref": "#/$defs/scope" } - } - }, - "purposes": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } }, - "projection": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 256, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "pagination": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["default_limit", "max_limit"], - "properties": { - "default_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 }, - "max_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 } - } - }, - "filters": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 256, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } - }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "relationships": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["kind", "target", "foreign_key"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["belongs_to", "has_many", "has_one"] }, - "target": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "foreign_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "concept_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - } - }, - "aggregates": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAggregate" } - }, - "attribute_release_profiles": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseProfiles" }, - "standards": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["ogc_features", "sp_dci"], - "properties": { - "ogc_features": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpatial" }] }, - "sp_dci": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpdci" }] } - } - } - } - }, - "recordAttributeReleaseProfiles": { - "description": "Purpose-bound, exact-one identity releases keyed by stable profile id. Generated Relay responses omit source metadata and are never cacheable.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAttributeReleaseProfile" } - }, - "recordAttributeReleaseProfile": { - "description": "One purpose-bound, minimized identity release compiled into the owning Relay entity.", - "type": "object", - "additionalProperties": false, - "required": ["version", "purpose", "release_scope", "subject", "release_conditions", "claims"], - "properties": { - "version": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "release_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "subject": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["source_field", "id_type"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "id_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" } - } - }, - "release_conditions": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["expression"], - "properties": { "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" } } - }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["required", "sensitivity"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" }, - "required": {"x-registry-field": "property", "type": "boolean" }, - "sensitivity": {"x-registry-field": "property", "enum": ["direct_identifier", "personal", "public", "pseudonymous"] } - }, - "oneOf": [ - {"x-registry-field": "branch", "required": ["source_field"] }, - {"x-registry-field": "branch", "required": ["expression"] } - ] - } - } - } - }, - "recordAttributeReleaseExpression": { - "description": "Bounded CEL evaluated only against the projected source object.", - "type": "object", - "additionalProperties": false, - "required": ["cel"], - "properties": { "cel": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 4096 } } - }, - "filterOperators": { "type": "array", "minItems": 1, "uniqueItems": true, "description": "Allowed operators for one queryable field.", "items": {"x-registry-field": "array_item", "enum": ["eq", "in", "gte", "lte", "between"] } }, - "fieldList": { "type": "array", "maxItems": 16, "uniqueItems": true, "description": "Bounded unique list of entity field identifiers.", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "recordAggregate": { - "description": "Governed aggregate definition with disclosure controls.", - "type": "object", - "additionalProperties": false, - "required": ["description", "disclosure_control"], - "properties": { - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "default_group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "dimensions": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateDimension" } }, - "indicators": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateIndicator" } }, - "allowed_filters": {"x-registry-field": "property", "type": "object", "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "temporal_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateAccess" }, - "spatial": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateSpatial" }, - "joins": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "measures": {"x-registry-field": "property", "type": "array", "minItems": 1, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateMeasure" } }, - "disclosure_control": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "min_group_size": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "suppression": {"x-registry-field": "property", "enum": ["omit", "mask", "null"] } - } - } - }, - "anyOf": [{"x-registry-field": "branch", "required": ["measures"] }, {"x-registry-field": "branch", "required": ["indicators"] }] - }, - "recordAggregateAccess": { - "description": "Authorization scopes and execution policy for one governed aggregate.", - "type": "object", - "additionalProperties": false, - "properties": { - "metadata_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_only_execution": {"x-registry-field": "property", "type": "boolean" } - } - }, - "recordAggregateSpatial": { - "description": "Administrative-area geometry join used to spatially group aggregate results.", - "type": "object", - "additionalProperties": false, - "required": ["mode", "dimension", "geometry_entity", "geometry_id_field", "geometry_field"], - "properties": { - "mode": {"x-registry-field": "property", "const": "admin_area" }, - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "dimension": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_id_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatial": { - "description": "OGC API Features collection metadata and bounded geometry mapping.", - "type": "object", - "additionalProperties": false, - "required": ["geometry"], - "properties": { - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "geometry": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialGeometry" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "datetime_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_bbox_degrees": {"x-registry-field": "property", "type": "number", "exclusiveMinimum": 0 }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatialGeometry": { - "description": "Supported point or encoded-geometry field mapping for spatial records.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "longitude_field", "latitude_field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "const": "point" }, - "longitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "latitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["geojson", "wkt", "wkb"] }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - } - ] - }, - "recordSpatialBbox": { - "description": "Record fields carrying the four coordinates of a bounding box.", - "type": "object", - "additionalProperties": false, - "required": ["min_x", "min_y", "max_x", "max_y"], - "properties": { - "min_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "min_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordSpdci": { - "description": "SP-DCI registry identity and field mappings for a records service.", - "type": "object", - "additionalProperties": false, - "required": ["registry", "registry_type", "record_type", "identifiers", "expression_fields"], - "properties": { - "registry": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "registry_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "record_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "identifiers": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "expression_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "response_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" } - } - }, - "recordFieldMap": { - "description": "Bounded mapping from standard field names to entity field identifiers.", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } - }, - "recordAggregateDimension": { - "description": "Named grouping dimension backed by one entity field.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "field"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "codelist": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateIndicator": { - "description": "SDMX-style aggregate indicator with units and display metadata.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "function", "column", "unit_measure"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "unit_measure": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "unit_mult": {"x-registry-field": "property", "type": "integer", "minimum": -2147483648, "maximum": 2147483647 }, - "decimals": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 4294967295 }, - "frequency": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "definition_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateMeasure": { - "description": "Named aggregate calculation over one entity column.", - "type": "object", - "additionalProperties": false, - "required": ["name", "function", "column"], - "properties": { - "name": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordAggregateFunction": { - "description": "Supported aggregate calculation function.", - "enum": ["count", "sum", "avg", "min", "max", "median", "count_distinct", "stddev"] - }, - "disclosureMode": { "enum": ["value", "predicate", "redacted"], "description": "Maximum detail a claim may disclose." }, - "disclosure": { - "description": "Fixed disclosure mode or a default constrained by explicitly allowed alternatives.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/disclosureMode" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["default", "allowed"], - "properties": { - "default": {"x-registry-field": "property", "$ref": "#/$defs/disclosureMode" }, - "allowed": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/disclosureMode" } } - } - } - ] - }, - "service": { - "description": "Closed choice between a Notary evidence service and a Relay records service.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/evidenceService" }, - {"x-registry-field": "branch", "$ref": "#/$defs/recordsService" } - ] - }, - "recordsService": { - "description": "Relay records API backed by one authored materialized entity.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "entity", "api"], - "properties": { - "kind": {"x-registry-field": "property", "const": "records_api" }, - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "owner": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "sensitivity": {"x-registry-field": "property", "enum": ["public", "internal", "personal", "confidential", "secret"] }, - "access_rights": {"x-registry-field": "property", "enum": ["public", "restricted", "non_public"] }, - "update_frequency": {"x-registry-field": "property", "enum": ["continuous", "daily", "weekly", "termly", "monthly", "quarterly", "annual", "irregular", "as_needed", "unknown"] }, - "conforms_to": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/text" } }, - "api": {"x-registry-field": "property", "$ref": "#/$defs/recordsApi" } - } - }, - "evidenceService": { - "description": "Notary evidence policy, consultations, claims, and registry-backed credential profiles. Source-free claims remain evaluation-only and cannot be selected by a credential profile.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "version", "purpose", "legal_basis", "consent", "access", "claims"], - "properties": { - "kind": {"x-registry-field": "property", "const": "evidence" }, - "version": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "subject_type": { - "x-registry-field": "property", - "description": "Subject category evaluated by this evidence service. Omission is normalized to person.", - "enum": ["person", "project"] - }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "legal_basis": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "consent": {"x-registry-field": "property", "enum": ["not_required", "required"] }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/access" }, - "variables": {"x-registry-field": "property", "$ref": "#/$defs/variables" }, - "consultations": {"x-registry-field": "property", "$ref": "#/$defs/consultations" }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["disclosure"], - "properties": { "output": {"x-registry-field": "property", "type": "string" }, "cel": {"x-registry-field": "property", "type": "string", "minLength": 1 }, "value": {"x-registry-field": "property", "$ref": "#/$defs/claimValue" }, "disclosure": {"x-registry-field": "property", "$ref": "#/$defs/disclosure" } }, - "oneOf": [{"x-registry-field": "branch", "required": ["output"] }, {"x-registry-field": "branch", "required": ["cel"] }] - } - }, - "credential_profiles": { - "x-registry-field": "property", - "description": "Credential profiles may select only claims backed by an exact Relay consultation. Source-free claim evaluation cannot be exposed as credential capability.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["format", "type", "validity", "claims"], - "properties": { - "format": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "validity": {"x-registry-field": "property", "type": "string", "pattern": "^[1-9][0-9]*(?:s|m|h)$" }, - "claims": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } } - } - } - } - } - }, - "access": { - "description": "Scopes a caller must hold to use an evidence service.", - "type": "object", "additionalProperties": false, "required": ["scopes"], - "properties": { "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } } } - }, - "variables": { - "description": "Typed request variables exposed to evidence evaluation.", - "type": "object", "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["from", "type"], "properties": { "from": {"x-registry-field": "property", "type": "string", "pattern": "^request\\.variables\\.[a-z][a-z0-9._-]{0,95}$" }, "type": {"x-registry-field": "property", "const": "date" } } } - }, - "consultations": { - "description": "Relay consultations and their closed bindings to target request identifiers or caller-supplied target attributes.", - "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["integration", "input"], "properties": { "integration": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, "input": {"x-registry-field": "property", "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/targetRequestMapping" } } } } - }, - "targetRequestMapping": { - "description": "Closed target request binding. Attribute values are bounded typed caller-supplied context, not authenticated identifiers.", - "type": "string", - "maxLength": 128, - "pattern": "^request\\.target\\.(?:id|identifiers\\.[A-Za-z][A-Za-z0-9._-]{0,95}|attributes\\.[a-z][a-z0-9_]{0,63})$" - }, - "claimValue": { - "description": "Explicit claim value type, nullability, and encoded-size bound for source-free evaluation. A source-free claim cannot be selected by a credential profile.", - "type": "object", "additionalProperties": false, "required": ["type"], - "properties": { "type": {"x-registry-field": "property", "enum": ["boolean", "integer", "string", "date"] }, "nullable": {"x-registry-field": "property", "type": "boolean" }, "max_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 } } - } - } -} diff --git a/projects/nia-population/.vscode/extensions.json b/projects/nia-population/.vscode/extensions.json deleted file mode 100644 index 6098b6e..0000000 --- a/projects/nia-population/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "redhat.vscode-yaml" - ] -} diff --git a/projects/nia-population/.vscode/settings.json b/projects/nia-population/.vscode/settings.json deleted file mode 100644 index db92a93..0000000 --- a/projects/nia-population/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "yaml.schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } -} diff --git a/projects/nia-population/.zed/settings.json b/projects/nia-population/.zed/settings.json deleted file mode 100644 index fac07ac..0000000 --- a/projects/nia-population/.zed/settings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } - } - } - } - } -} diff --git a/projects/nia-population/entities/population.yaml b/projects/nia-population/entities/population.yaml deleted file mode 100644 index db0d1f6..0000000 --- a/projects/nia-population/entities/population.yaml +++ /dev/null @@ -1,48 +0,0 @@ -version: 1 -id: population -revision: 1 -primary_key: uin -schema: - type: object - additionalProperties: false - required: - - uin - - legacy_nid - - given_name - - family_name - - sex - - birth_date - - identity_status - - alive - properties: - uin: - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - legacy_nid: - type: [string, "null"] - maxLength: 32 - given_name: - type: string - maxLength: 128 - family_name: - type: string - maxLength: 128 - sex: - type: string - maxLength: 32 - birth_date: - type: string - format: date - maxLength: 10 - identity_status: - type: string - maxLength: 32 - alive: - type: boolean -materialization: - max_records: 10000000 - max_bytes: 512MiB - refresh: 1m - retain_generations: 2 diff --git a/projects/nia-population/environments/hosted.yaml b/projects/nia-population/environments/hosted.yaml deleted file mode 100644 index f7ad455..0000000 --- a/projects/nia-population/environments/hosted.yaml +++ /dev/null @@ -1,30 +0,0 @@ -version: 1 -entities: - population: - provider: - type: postgres - connection: { secret: SOLMARA_NIA_DATABASE_URL } - schema: public - table: population_person - columns: - uin: uin - legacy_nid: legacy_nid - given_name: given_name - family_name: family_name - sex: sex - birth_date: birth_date - identity_status: identity_status - alive: alive - source_revision: nia-population-person-v1 - generation: 2026-07-14 -relay: - origin: https://nia-relay.solmara.registrystack.org - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-esignet, solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } -deployment: - profile: hosted_lab - relay: { service: nia-population-relay } diff --git a/projects/nia-population/environments/local.yaml b/projects/nia-population/environments/local.yaml deleted file mode 100644 index 0742a5b..0000000 --- a/projects/nia-population/environments/local.yaml +++ /dev/null @@ -1,30 +0,0 @@ -version: 1 -entities: - population: - provider: - type: postgres - connection: { secret: SOLMARA_NIA_DATABASE_URL } - schema: public - table: population_person - columns: - uin: uin - legacy_nid: legacy_nid - given_name: given_name - family_name: family_name - sex: sex - birth_date: birth_date - identity_status: identity_status - alive: alive - source_revision: nia-population-person-v1 - generation: 2026-07-14 -relay: - origin: http://127.0.0.1:8080 - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-esignet, solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } -deployment: - profile: local - relay: { service: nia-population-relay } diff --git a/projects/nia-population/integrations/population-person-by-uin/fixtures/match.yaml b/projects/nia-population/integrations/population-person-by-uin/fixtures/match.yaml deleted file mode 100644 index 6a3a4e6..0000000 --- a/projects/nia-population/integrations/population-person-by-uin/fixtures/match.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: nia-population-person-match -classification: synthetic -input: { uin: "2300010248" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: - status: 200 - body: { identity_status: active, alive: true } -expect: - outcome: match - outputs: { identity_status: active, alive: true } diff --git a/projects/nia-population/integrations/population-person-by-uin/fixtures/no-match.yaml b/projects/nia-population/integrations/population-person-by-uin/fixtures/no-match.yaml deleted file mode 100644 index 08d453c..0000000 --- a/projects/nia-population/integrations/population-person-by-uin/fixtures/no-match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: nia-population-person-no-match -classification: synthetic -input: { uin: "9999999999" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: [] } -expect: - outcome: no_match - outputs: {} diff --git a/projects/nia-population/integrations/population-person-by-uin/integration.yaml b/projects/nia-population/integrations/population-person-by-uin/integration.yaml deleted file mode 100644 index c08cc90..0000000 --- a/projects/nia-population/integrations/population-person-by-uin/integration.yaml +++ /dev/null @@ -1,28 +0,0 @@ -version: 1 -id: solmara-nia-population-person-by-uin -revision: 1 - -input: - uin: - role: selector - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - -capability: - snapshot: - entity: population - exact: - uin: { input: uin } - freshness: 5m - -outputs: [identity_status, alive] - -not_applicable: - ambiguity: - rationale: The exact UIN selector is the entity primary key, whose materialized unique-key constraint permits at most one population record. - request_fixture: nia-population-person-match - subject_mismatch: - rationale: The minimized output projection omits UIN, so it contains no identifier comparable with the requested UIN. - request_fixture: nia-population-person-match diff --git a/projects/nia-population/registry-stack.yaml b/projects/nia-population/registry-stack.yaml deleted file mode 100644 index ea110a8..0000000 --- a/projects/nia-population/registry-stack.yaml +++ /dev/null @@ -1,97 +0,0 @@ -version: 1 - -registry: - id: solmara-nia-population - -integrations: - population-person-by-uin: - file: integrations/population-person-by-uin/integration.yaml - -entities: - population: - file: entities/population.yaml - -services: - nia-population-records: - kind: records_api - entity: population - title: NIA population records - description: Governed population attributes used by NIA-owned services. - owner: National Identity Agency - sensitivity: personal - access_rights: restricted - update_frequency: continuous - api: - scopes: - metadata: population:metadata - rows: population:rows - evidence_verification: population:evidence_verification - purposes: - - https://id.registrystack.org/solmara/purpose/esignet-identity-verification - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - projection: - - uin - - legacy_nid - - given_name - - family_name - - sex - - birth_date - - identity_status - - alive - pagination: { default_limit: 25, max_limit: 100 } - filters: - uin: [eq] - legacy_nid: [eq] - attribute_release_profiles: - solmara-nia-userinfo: - version: v1 - title: SolmaraID UserInfo - description: Minimal identity attributes for the Solmara eSignet authenticator. - purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - release_scope: population:identity_release - subject: - source_field: uin - id_type: national_id - release_conditions: - expression: - cel: "source.identity_status == 'active' && source.alive == true" - claims: - individual_id: - source_field: uin - required: true - sensitivity: direct_identifier - name: - expression: - cel: "source.given_name + ' ' + source.family_name" - required: true - sensitivity: direct_identifier - given_name: - source_field: given_name - required: true - sensitivity: direct_identifier - family_name: - source_field: family_name - required: true - sensitivity: direct_identifier - birthdate: - source_field: birth_date - required: true - sensitivity: personal - gender: - source_field: sex - required: false - sensitivity: personal - standards: { ogc_features: false, sp_dci: false } - - nia-population-consultation: - kind: consultation_api - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - legal_basis: child-benefit-act - consent: not_required - consultations: - population: - integration: population-person-by-uin - input: - uin: request.target.identifiers.solmara_uin diff --git a/projects/sipf-pensions/.registry-stack-editor/manifest.json b/projects/sipf-pensions/.registry-stack-editor/manifest.json deleted file mode 100644 index 3603a8e..0000000 --- a/projects/sipf-pensions/.registry-stack-editor/manifest.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "format": "registry.stack.editor-manifest", - "version": 1, - "registryctl_version": "0.15.2", - "schemas": [ - { - "kind": "project", - "path": "schemas/project.schema.json", - "file_glob": "registry-stack.yaml", - "sha256": "sha256:9fd76663c866c52bb5a17a4cfea821049b7428c812f1666da9db0d43a00da144" - }, - { - "kind": "environment", - "path": "schemas/environment.schema.json", - "file_glob": "environments/*.yaml", - "sha256": "sha256:d16828629e001a30a1705af90bcc12a214be18cf5805a9833e65b03d631c9e9b" - }, - { - "kind": "integration", - "path": "schemas/integration.schema.json", - "file_glob": "integrations/*/integration.yaml", - "sha256": "sha256:2e45452f63c3c610ed14b79244bc9aa086d79d811ad0899223b36c160d449a79" - }, - { - "kind": "fixture", - "path": "schemas/fixture.schema.json", - "file_glob": "integrations/*/fixtures/*.yaml", - "sha256": "sha256:98c0ab21e35cecd143b51e4b5dd487de07ceea24e505bbe351d212e0628c8c8c" - }, - { - "kind": "entity", - "path": "schemas/entity.schema.json", - "file_glob": "entities/*.yaml", - "sha256": "sha256:4cbb31c8addeb54f8b57aaaff8f30c39e651a21a2d741a1b775ecb0d7c248342" - } - ] -} diff --git a/projects/sipf-pensions/.registry-stack-editor/schemas/entity.schema.json b/projects/sipf-pensions/.registry-stack-editor/schemas/entity.schema.json deleted file mode 100644 index ffc57c8..0000000 --- a/projects/sipf-pensions/.registry-stack-editor/schemas/entity.schema.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/entity.v1.json", - "title": "Registry Stack project entity v1", - "description": "Defines a bounded, materialized entity that Relay can query without coupling the project to a specific source product.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "primary_key", "schema", "materialization"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Entity authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the entity." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this entity definition.", "examples": [1] }, - "primary_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Field whose value uniquely identifies each materialized record.", "examples": ["person_id"] }, - "schema": {"x-registry-field": "property", "$ref": "#/$defs/objectSchema", "description": "Closed JSON object schema for materialized records." }, - "materialization": { - "x-registry-field": "property", - "description": "Authored resource, refresh, and bounded recovery-set retention applied while building entity generations.", - "type": "object", - "additionalProperties": false, - "required": ["max_records", "max_bytes", "refresh", "retain_generations"], - "properties": { - "max_records": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 100000000, "description": "Maximum number of records allowed in one generation.", "examples": [10000] }, - "max_bytes": { - "x-registry-field": "property", - "description": "Maximum encoded size of one generation, up to 1 GiB, as bytes or a KiB/MiB quantity.", - "examples": ["16MiB"], - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1073741824 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,5}|10[0-3][0-9]{4}|104[0-7][0-9]{3}|1048[0-4][0-9]{2}|10485[0-6][0-9]|104857[0-6])KiB|(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])MiB)$" } - ] - }, - "refresh": { - "x-registry-field": "property", - "description": "Refresh cadence as a canonical positive duration no greater than 30 days, or manual when an operator initiates every refresh.", - "examples": ["1h"], - "oneOf": [ - {"x-registry-field": "branch", "const": "manual" }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-4][0-9]{8}|25[0-8][0-9]{7}|259[0-1][0-9]{6}|2592000000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-4][0-9]{5}|25[0-8][0-9]{4}|259[0-1][0-9]{3}|2592000)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-2][0-9]{3}|43[0-1][0-9]{2}|43200)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-1][0-9]{1}|720)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30)d)$" } - ] - }, - "retain_generations": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16, "description": "Number of completed cache generations, including the active generation, retained as a bounded recovery set after successful publication. This does not make arbitrary retained generations selectable for rollback.", "examples": [2] } - } - } - }, - "examples": [ - { - "version": 1, - "id": "people", - "revision": 1, - "primary_key": "person_id", - "schema": { - "type": "object", - "additionalProperties": false, - "required": ["person_id"], - "properties": { "person_id": { "type": "string" } } - }, - "materialization": { - "max_records": 10000, - "max_bytes": "16MiB", - "refresh": "1h", - "retain_generations": 2 - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "propertyName": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$", "description": "Portable lowercase record field name." }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for a nullable field.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "items": false, - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "fieldSchema": { - "description": "Bounded schema keywords supported for one scalar materialized field.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "enum": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true }, - "const": {"x-registry-field": "property"}, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 65536 }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 } - } - }, - "objectSchema": { - "description": "Closed object schema whose declared required fields must appear in properties.", - "type": "object", - "additionalProperties": false, - "required": ["type", "additionalProperties", "required", "properties"], - "properties": { - "type": {"x-registry-field": "property", "const": "object" }, - "additionalProperties": {"x-registry-field": "property", "const": false }, - "required": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 256, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/propertyName" } - }, - "properties": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 256, - "propertyNames": {"x-registry-field": "property", "$ref": "#/$defs/propertyName" }, - "additionalProperties": {"x-registry-field": "property", "$ref": "#/$defs/fieldSchema" } - } - } - } - } -} diff --git a/projects/sipf-pensions/.registry-stack-editor/schemas/environment.schema.json b/projects/sipf-pensions/.registry-stack-editor/schemas/environment.schema.json deleted file mode 100644 index f04c70b..0000000 --- a/projects/sipf-pensions/.registry-stack-editor/schemas/environment.schema.json +++ /dev/null @@ -1,337 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/environment.v1.json", - "title": "Registry Stack project environment v1", - "description": "Binds a Registry Stack project to environment-specific services, sources, credentials, and deployment topology.", - "type": "object", - "additionalProperties": false, - "required": ["version", "deployment"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Environment authoring format version." }, - "integrations": {"x-registry-field": "property", "type": "object", "maxProperties": 16, "description": "Environment bindings keyed by authored integration identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/integration" } }, - "entities": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "description": "Environment-specific source bindings keyed by entity identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/entity" } }, - "issuance": { - "x-registry-field": "property", - "description": "Notary issuer and signing-key bindings for credential issuance.", - "type": "object", "additionalProperties": false, "required": ["issuer", "signing_key", "signing_kid", "generation"], - "properties": { "issuer": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1 }, "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" }, "algorithm": {"x-registry-field": "property", "enum": ["EdDSA", "ES256"], "default": "EdDSA", "description": "Credential issuer signing algorithm. Holder proof remains EdDSA with did:jwk." }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "callers": { - "x-registry-field": "property", - "description": "Notary API-key caller identities and the scopes each identity receives.", - "type": "object", "maxProperties": 64, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["api_key_fingerprint", "scopes"], - "properties": { "api_key_fingerprint": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } - } - }, - "relay": { - "x-registry-field": "property", - "description": "Public Relay identity and token-validation settings for a Relay deployment.", - "type": "object", "additionalProperties": false, - "required": ["origin", "issuer", "jwks_url", "audience", "allowed_clients"], - "properties": { "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "allowed_clients": {"x-registry-field": "sensitive_property", "type": "array", "maxItems": 64, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } }, "local_api_keys": {"x-registry-field": "property", "description": "Local-profile-only synthetic principals and scopes. Registryctl generates the raw keys and supplies only their fingerprints to Relay.", "type": "object", "additionalProperties": false, "required": ["match_principal", "no_match_principal", "scopes"], "properties": { "match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "no_match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } } } - }, - "notary_relay": { - "x-registry-field": "property", - "description": "Deployment-internal Relay connection, Notary workload identity, and token file used only when Notary consults Relay.", - "type": "object", "additionalProperties": false, "required": ["base_url", "workload_client_id", "token_file"], - "properties": { "base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/internalOrigin" }, "workload_client_id": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "token_file": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" } } - }, - "relay_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Relay-owned consultation PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Notary-owned PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_cel": { - "x-registry-field": "property", - "description": "Optional per-worker data/address-space ceiling for dedicated Notary CEL processes. The Notary default is 134217728 bytes; 1073741824 is the maximum emulation-compatible exception and remains a limit, not an allocation.", - "type": "object", "additionalProperties": false, "required": ["worker_memory_bytes"], - "properties": { - "worker_memory_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 33554432, "maximum": 1073741824 } - } - }, - "oid4vci": { - "x-registry-field": "property", - "description": "Explicit registry-backed holder-wallet OID4VCI binding for one authored Notary credential profile.", - "$ref": "#/$defs/oid4vci" - }, - "deployment": { - "x-registry-field": "property", - "description": "Products deployed in this environment and the operational profile they use.", - "type": "object", "additionalProperties": false, "required": ["profile"], - "properties": { "profile": {"x-registry-field": "property", "enum": ["local", "hosted_lab", "production", "evidence_grade"] }, "relay": {"x-registry-field": "property", "$ref": "#/$defs/service" }, "notary": {"x-registry-field": "property", "$ref": "#/$defs/service" } }, - "anyOf": [{"x-registry-field": "branch", "required": ["relay"] }, {"x-registry-field": "branch", "required": ["notary"] }] - } - }, - "allOf": [ - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["deployment"], "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } }, - "then": {"x-registry-field": "branch", "required": ["relay"] }, - "else": {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "required": ["relay"] } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_relay"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay", "notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["relay_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_cel"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["oid4vci"] }, - "then": { - "x-registry-field": "branch", - "required": ["notary_state"], - "properties": { - "deployment": {"x-registry-field": "property", "required": ["notary"] } - } - }, - "else": {"x-registry-field": "branch", "properties": { "callers": {"x-registry-field": "property", "minProperties": 1 } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["profile"], "properties": { "profile": {"x-registry-field": "property", "const": "local" } } } } }, - "else": { - "x-registry-field": "branch", - "properties": { - "relay": { - "x-registry-field": "property", - "not": {"x-registry-field": "branch", "required": ["local_api_keys"] }, - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - }, - "oid4vci": { - "x-registry-field": "property", - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorization_server": { - "x-registry-field": "property", - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - } - } - } - } - } - } - ], - "examples": [ - { - "version": 1, - "deployment": { - "profile": "local", - "notary": { "service": "notary" } - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token2048": { "type": "string", "minLength": 1, "maxLength": 2048, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII token, including full verification-method identifiers." }, - "secret": { "type": "object", "additionalProperties": false, "description": "Reference to a process-environment variable; secret values are never authored here.", "required": ["secret"], "properties": { "secret": {"x-registry-field": "secret_reference_property", "type": "string", "pattern": "^[A-Z_][A-Z0-9_]{0,127}$" } } }, - "origin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/?$", "description": "HTTPS origin without path, query, or fragment." }, - "httpsResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/[^?#]+$", "description": "Exact HTTPS resource without query or fragment." }, - "localLoopbackOrigin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/?$", "description": "HTTP IP-loopback origin; public Relay and issuer fields restrict it to the local profile, while internal Notary-to-Relay connections allow it in any profile." }, - "localLoopbackResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/[^?#]+$", "description": "HTTP IP-loopback resource accepted only with the local deployment profile." }, - "internalOrigin": { "description": "Deployment-internal HTTPS or HTTP IP-loopback origin.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayOrigin": { "description": "Relay or issuer origin using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayResource": { "description": "Relay authentication resource using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/httpsResource" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackResource" }] }, - "oid4vci": { - "description": "Closed OID4VCI trust, key, subject, wallet, and credential-profile binding.", - "type": "object", - "additionalProperties": false, - "required": ["public_base_url", "credential", "authorization_server", "client", "access_token", "sensitive_state_key", "subject", "redirect_uri", "allowed_wallet_origins"], - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "credential": { - "x-registry-field": "property", - "description": "Existing project service and credential profile exposed through OID4VCI.", - "type": "object", "additionalProperties": false, "required": ["service", "profile"], - "properties": { - "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "profile": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "authorization_server": { - "x-registry-field": "property", - "description": "Pinned eSignet issuer and exact endpoints used for login and token validation.", - "type": "object", "additionalProperties": false, "required": ["issuer", "jwks_url", "userinfo_url", "authorize_url", "token_url"], - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" } - } - }, - "client": { - "x-registry-field": "property", - "description": "eSignet relying-party identity and dedicated private-key reference.", - "type": "object", "additionalProperties": false, "required": ["id", "signing_key", "signing_kid"], - "properties": { - "id": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "access_token": { - "x-registry-field": "property", - "description": "Dedicated Notary access-token signing key and published key identifier.", - "type": "object", "additionalProperties": false, "required": ["signing_key", "signing_kid"], - "properties": { - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "sensitive_state_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "subject": { - "x-registry-field": "property", - "description": "Verified eSignet userinfo claim bound exactly to the credential subject identifier.", - "type": "object", "additionalProperties": false, "required": ["token_claim", "id_type"], - "properties": { - "token_claim": {"x-registry-field": "property", "$ref": "#/$defs/token256" }, - "id_type": {"x-registry-field": "property", "$ref": "#/$defs/token256" } - } - }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "allowed_wallet_origins": { - "x-registry-field": "sensitive_property", - "description": "Exact HTTPS browser origins admitted to the wallet-facing Notary routes.", - "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "$ref": "#/$defs/origin" } - }, - "tx_code": { - "x-registry-field": "property", - "description": "Transaction-code policy. Omit for the secure required-PIN default. Set required=false only for a bounded bearer-offer interoperability profile; the compiler fixes the offer lifetime at 300 seconds.", - "type": "object", "additionalProperties": false, - "properties": { "required": {"x-registry-field": "property", "type": "boolean", "default": true } } - } - } - }, - "privateCidrs": { - "description": "Explicit private network ranges this source may resolve to.", - "type": "array", "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "type": "string", "minLength": 3, "maxLength": 64 } - }, - "ca": { - "description": "Pinned certificate-authority file and its rotation generation.", - "type": "object", "additionalProperties": false, "required": ["file", "generation"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "mtls": { - "description": "Client certificate and private-key reference for mutual TLS.", - "type": "object", "additionalProperties": false, "required": ["certificate_file", "private_key", "generation"], - "properties": { "certificate_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "private_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "endpoint": { - "description": "Security-bound HTTPS endpoint with optional private-network, CA, and mTLS policy.", - "type": "object", "additionalProperties": false, "required": ["origin", "path", "generation"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//)(?!.*[?#]).+$" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } - } - }, - "credential": { - "description": "One supported source credential shape, containing references rather than values.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["username", "password", "generation"], "properties": { "username": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "password": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["token", "generation"], "properties": { "token": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["client_id", "client_secret", "generation"], "properties": { "client_id": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "client_secret": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["value", "generation"], "properties": { "value": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } } - ] - }, - "provider": { - "description": "Environment-specific physical provider for a materialized entity.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "csv" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "delimiter": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 }, "quote": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "project_file", "path", "sheet"], "properties": { "type": {"x-registry-field": "property", "const": "xlsx" }, "project_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "sheet": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "data_range": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "parquet" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "connection", "schema", "table"], "properties": { "type": {"x-registry-field": "property", "const": "postgres" }, "connection": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "schema": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" }, "table": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" } } } - ] - }, - "entity": { - "description": "Maps an authored entity to provider columns and immutable source-generation identifiers.", - "type": "object", "additionalProperties": false, "required": ["provider", "columns", "source_revision", "generation"], - "properties": { "provider": {"x-registry-field": "property", "$ref": "#/$defs/provider" }, "columns": {"x-registry-field": "property", "type": "object", "minProperties": 1, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } }, "source_revision": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "generation": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 } } - }, - "source": { - "description": "Runtime source connection policy for one authored integration.", - "type": "object", "additionalProperties": false, "required": ["origin"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "credential": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "oauth": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "jwks": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "rate": {"x-registry-field": "property", "type": "object", "additionalProperties": false, "required": ["per_minute", "burst"], "properties": { "per_minute": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 60000 }, "burst": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 1024 } } }, - "concurrency": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 64 }, - "timeout": { - "x-registry-field": "property", - "description": "Optional request timeout no greater than 20 seconds; generated bindings narrow it to the integration deadline.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - }, - "integration": { - "description": "Environment binding for an authored source integration.", - "type": "object", "additionalProperties": false, "required": ["source"], - "properties": { "source": {"x-registry-field": "property", "$ref": "#/$defs/source" } } - }, - "service": { "type": "object", "additionalProperties": false, "description": "Binds an authored product role to a deployed service identifier.", "required": ["service"], "properties": { "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } }, - "token256": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII protocol token." }, - "postgresIdentifier": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,62}$", "description": "Portable unquoted PostgreSQL schema or table identifier." }, - "absolutePath": { "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized absolute path without dot segments or duplicate separators." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized project-relative path without dot segments or duplicate separators." } - } -} diff --git a/projects/sipf-pensions/.registry-stack-editor/schemas/fixture.schema.json b/projects/sipf-pensions/.registry-stack-editor/schemas/fixture.schema.json deleted file mode 100644 index b853192..0000000 --- a/projects/sipf-pensions/.registry-stack-editor/schemas/fixture.schema.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/fixture.v1.json", - "title": "Registry Stack project integration fixture v1", - "description": "Defines one synthetic, deterministic integration scenario for offline adapter verification.", - "type": "object", - "additionalProperties": false, - "required": ["name", "classification", "input", "interactions", "expect"], - "properties": { - "name": {"x-registry-field": "public_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Human-readable scenario name shown in test output.", "examples": ["existing household matches"] }, - "classification": {"x-registry-field": "public_property", "const": "synthetic", "description": "Declares that fixture data is synthetic and safe for offline testing." }, - "request": { - "x-registry-field": "redacted_fixture_property", - "$ref": "#/$defs/governedRequest", - "description": "Optional independently authored synthetic Notary request used to prove the request-to-consultation binding before Relay access." - }, - "input": { - "x-registry-field": "redacted_fixture_property", - "description": "Typed consultation inputs supplied to the adapter under test.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "type": ["string", "boolean", "integer", "null"] - } - }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Named date values available to deterministic fixture interpolation.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "interactions": { - "x-registry-field": "redacted_fixture_property", - "description": "Ordered upstream request and response exchanges expected during execution.", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/interaction" } - }, - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/expectation", "description": "Observable adapter result required for the scenario to pass." } - }, - "examples": [ - { - "name": "existing household matches", - "classification": "synthetic", - "input": { "household_id": "HH-1001" }, - "interactions": [ - { - "expect": { "method": "GET", "path": "/households/HH-1001" }, - "respond": { "status": 200, "body": { "active": true } } - } - ], - "expect": { "outcome": "match", "outputs": { "active": true } } - } - ], - "$defs": { - "governedRequest": { - "description": "The same closed governed request shape accepted by project test --live.", - "type": "object", - "additionalProperties": false, - "required": ["target", "claims", "purpose"], - "properties": { - "target": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/governedTarget" }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies synthetic date variables to the governed evaluation request.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "claims": { - "x-registry-field": "redacted_fixture_property", - "description": "Lists the authored claims evaluated by this synthetic request witness.", - "type": "array", - "minItems": 1, - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedClaimRef" } - }, - "disclosure": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the disclosure mode requested from the authored claim policy." }, - "format": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the claim-result media type requested from the governed Notary path." }, - "purpose": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Names the authored service purpose exercised by this synthetic request witness." } - } - }, - "governedTarget": { - "description": "The synthetic subject target presented to the same governed request boundary as a live Notary evaluation.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 64, "description": "Names the authored target entity type used for claim evaluation." }, - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies an optional synthetic direct target identifier." }, - "identifiers": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored synthetic identifiers for consultation input mapping.", - "type": "array", - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedIdentifier" } - }, - "attributes": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored typed target attributes for consultation input mapping.", - "type": "object", - "maxProperties": 16, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": ["string", "boolean", "integer", "null"] } - } - } - }, - "governedIdentifier": { - "description": "One synthetic target identifier with an authored scheme and string value.", - "type": "object", - "additionalProperties": false, - "required": ["scheme", "value"], - "properties": { - "scheme": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 96, "description": "Names the authored identifier scheme selected by a consultation input mapping." }, - "value": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies the synthetic string value bound to this identifier scheme." } - } - }, - "governedClaimRef": { - "description": "A requested claim ID, optionally pinned to one authored claim version.", - "oneOf": [ - {"x-registry-field": "branch", "type": "string"}, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Names one authored claim requested by this fixture witness."}, - "version": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Pins the requested claim to one authored claim-policy version."} - } - } - ] - }, - "interaction": { - "description": "One expected upstream request paired with its synthetic response.", - "type": "object", - "additionalProperties": false, - "required": ["expect", "respond"], - "properties": { - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/request" }, - "respond": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/response" } - } - }, - "request": { - "description": "Exact HTTP request shape the adapter must produce.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "redacted_fixture_property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "query": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 64, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "oneOf": [ - {"x-registry-field": "branch", "type": ["string", "boolean", "integer", "null"] }, - { - "x-registry-field": "branch", - "type": "array", - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "type": ["string", "boolean", "integer", "null"] } - } - ] - } - }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - "response": { - "description": "Synthetic HTTP response or timeout returned to the adapter.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["status"], - "properties": { - "status": {"x-registry-field": "redacted_fixture_property", "type": "integer", "minimum": 100, "maximum": 599 }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["timeout"], - "properties": { - "timeout": { - "x-registry-field": "redacted_fixture_property", - "description": "Reviewed synthetic timeout intent no greater than 20 seconds; offline execution returns a timeout outcome without sleeping.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - ] - }, - "fixtureBody": { - "description": "Inline JSON, or an exact closed reference to a strict JSON file below the fixture bodies directory. The top-level file key is reserved for file references.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": { - "x-registry-field": "redacted_fixture_property", - "type": "string", - "minLength": 8, - "maxLength": 4096, - "pattern": "^bodies/(?!\\.\\.?/)(?!.*(?:/)\\.\\.?/)(?!.*//).+$" - } - } - }, - { - "x-registry-field": "branch", - "not": { - "x-registry-field": "branch", - "type": "object", - "required": ["file"] - } - } - ] - }, - "expectation": { - "description": "Expected normalized outcome, output values, claims, or error.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "outcome": {"x-registry-field": "redacted_fixture_property", "enum": ["match", "no_match", "ambiguous"] }, - "outputs": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "claims": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "error": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256 } - } - } - } -} diff --git a/projects/sipf-pensions/.registry-stack-editor/schemas/integration.schema.json b/projects/sipf-pensions/.registry-stack-editor/schemas/integration.schema.json deleted file mode 100644 index 9bb2ad0..0000000 --- a/projects/sipf-pensions/.registry-stack-editor/schemas/integration.schema.json +++ /dev/null @@ -1,514 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/integration.v1.json", - "title": "Registry Stack project integration v1", - "description": "Defines a product-neutral source adaptation contract using bounded HTTP, Rhai script, or exact snapshot capabilities.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "input", "capability", "outputs"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Integration authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the integration." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this integration contract.", "examples": [1] }, - "source": {"x-registry-field": "property", "$ref": "#/$defs/source", "description": "Optional source product metadata and transport policy; capabilities remain product-neutral." }, - "input": { - "x-registry-field": "property", - "description": "Typed selector and parameter inputs accepted by this integration.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/input" } - }, - "capability": {"x-registry-field": "property", "$ref": "#/$defs/capability", "description": "Exactly one bounded execution mechanism for the source adaptation." }, - "outputs": { - "x-registry-field": "property", - "description": "Named typed outputs, or a shorthand list of output names inferred by the authoring compiler.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": { - "x-registry-field": "map_key", - "allOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputName" }, - {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "enum": ["matched", "outcome"] } } - ] - }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/output" } - }, - { - "x-registry-field": "branch", - "type": "array", - "minItems": 1, - "maxItems": 64, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/inputName" } - } - ] - }, - "limits": {"x-registry-field": "property", "$ref": "#/$defs/limits", "description": "Optional tighter resource limits for one integration execution." }, - "not_applicable": {"x-registry-field": "property", "$ref": "#/$defs/notApplicable", "description": "Explicit rationale and request-fixture evidence for a normally required outcome that the source contract cannot produce." } - }, - "examples": [ - { - "version": 1, - "id": "household_lookup", - "revision": 1, - "input": { - "household_id": { "role": "selector", "type": "string" } - }, - "capability": { - "script": { "file": "adapter.rhai" } - }, - "outputs": { - "active": { "type": "boolean" } - } - } - ], - "$defs": { - "notApplicable": { - "description": "Closed declarations for outcomes that the source protocol, exact singleton endpoint, or local unique-key constraint proves impossible.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "ambiguity": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed source contract cannot produce more than one matching record." }, - "subject_mismatch": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed response contract contains no identifier comparable with a requested selector." } - } - }, - "notApplicableReason": { - "description": "A reviewable reason that names a real request fixture proving the source access shape used by the rationale.", - "type": "object", - "additionalProperties": false, - "required": ["rationale", "request_fixture"], - "properties": { - "rationale": {"x-registry-field": "property", "type": "string", "minLength": 24, "maxLength": 512, "description": "Why the named normally required outcome cannot occur for this source contract." }, - "request_fixture": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Fixture name containing the supporting bounded source request." } - } - }, - "stableId": { - "description": "Lowercase stable identifier used in project references.", - "type": "string", - "pattern": "^[a-z][a-z0-9._-]{0,95}$" - }, - "inputName": { - "description": "Portable lowercase name for an integration input or output.", - "type": "string", - "pattern": "^[a-z][a-z0-9_]{0,63}$" - }, - "relativePath": { - "description": "Project-relative path without traversal or duplicate separators.", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" - }, - "integrationResponseByteSize": { - "description": "Maximum bytes accepted from one source response, defaulting to 512 KiB and capped at 8 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "512KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 8388608 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|[1-7][0-9]{3}|80[0-9]{2}|81[0-8][0-9]|819[0-2])KiB|[1-8]MiB)$" } - ] - }, - "integrationRequestByteSize": { - "description": "Maximum aggregate author-controlled request bytes for one execution, defaulting to 64 KiB and capped at 1 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "64KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1048576 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])KiB|1MiB)$" } - ] - }, - "integrationSourceByteSize": { - "description": "Maximum aggregate source-response bytes for one execution, defaulting to 2 MiB and capped at 16 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "2MiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 16777216 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-5][0-9]{3}|16[0-2][0-9]{2}|163[0-7][0-9]|1638[0-4])KiB|(?:[1-9]|1[0-6])MiB)$" } - ] - }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for nullable values.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "input": { - "description": "Type, validation, and canonicalization rules for one authored input.", - "type": "object", - "additionalProperties": false, - "required": ["role", "type"], - "properties": { - "role": {"x-registry-field": "property", "enum": ["selector", "parameter"] }, - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "description": "Maximum Unicode scalar-value length. The 1024-character ceiling derives a conservative 4096-byte UTF-8 ceiling for one canonical input.", "type": "integer", "minimum": 1, "maximum": 1024 }, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 16384 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "canonicalization": {"x-registry-field": "property", "enum": ["identity", "ascii_lowercase"] } - } - }, - "versions": { - "description": "Source versions tested by the project and versions explicitly accepted as unverified.", - "type": "object", - "additionalProperties": false, - "properties": { - "tested": {"x-registry-field": "property", "$ref": "#/$defs/versionList" }, - "unverified": {"x-registry-field": "property", "$ref": "#/$defs/versionList" } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["tested"], "properties": { "tested": {"x-registry-field": "property", "minItems": 1 } } }, - {"x-registry-field": "branch", "required": ["unverified"], "properties": { "unverified": {"x-registry-field": "property", "minItems": 1 } } } - ] - }, - "versionList": { - "description": "Bounded list of source version labels.", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } - }, - "credential": { - "description": "Authentication mechanism expected by the source adapter, without environment secret values.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "enum": ["none", "basic", "static_bearer"] } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_header" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_query" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9._:~-]{0,95}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "request", "response_profile"], - "properties": { - "type": {"x-registry-field": "property", "const": "oauth2_client_credentials" }, - "request": {"x-registry-field": "property", "enum": ["form", "json"] }, - "response_profile": {"x-registry-field": "property", "const": "oauth2_bearer" }, - "scope": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 }, - "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "refresh_skew": { - "x-registry-field": "property", - "description": "Positive OAuth refresh interval below the generated token contract's 60-second minimum lifetime.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|[1-5][0-9]{4})ms|(?:[1-9]|[1-5][0-9])s)$" - } - } - } - ] - }, - "allowRule": { - "description": "One read-only upstream method and path pattern the adapter may call.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": { - "x-registry-field": "sensitive_property", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^/[^?#]*$" - }, - "semantics": {"x-registry-field": "property", "const": "read_only" } - } - }, - "source": { - "description": "Optional source metadata, authentication shape, allowlist, and protocol profile.", - "type": "object", - "additionalProperties": false, - "required": ["auth"], - "properties": { - "product": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "versions": {"x-registry-field": "property", "$ref": "#/$defs/versions" }, - "auth": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "allow": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/allowRule" } - }, - "request_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "format": {"x-registry-field": "property", "enum": ["json", "text"] }, - "max_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationResponseByteSize" } - } - }, - "protocol": {"x-registry-field": "property", "$ref": "#/$defs/protocol" } - } - }, - "protocol": { - "description": "Optional interoperable protocol profiles layered over the source transport.", - "type": "object", - "additionalProperties": false, - "properties": { - "signed_dci": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["profile", "path", "jwks_profile", "sender", "receiver", "registry_type", "record_type", "locale", "selectors"], - "properties": { - "profile": {"x-registry-field": "property", "const": "dci-search-v1" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^/[^?#]*$" }, - "jwks_profile": {"x-registry-field": "property", "const": "rsa-signing-jwks-v1" }, - "sender": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "receiver": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "registry_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "record_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "locale": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$" }, - "selectors": { - "x-registry-field": "property", - "type": "object", "minProperties": 1, "maxProperties": 8, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["field", "response_pointer"], - "properties": { - "field": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 160 }, - "response_pointer": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" } - } - } - } - } - } - }, - "minProperties": 1 - }, - "inputReference": { - "description": "Reference to a declared integration input.", - "type": "object", - "additionalProperties": false, - "required": ["input"], - "properties": { "input": {"x-registry-field": "property", "$ref": "#/$defs/inputName" } } - }, - "inputOrLiteral": { - "description": "Request value supplied from a declared input or an authored scalar literal.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputReference" }, - {"x-registry-field": "branch", "type": ["string", "boolean", "integer"] } - ] - }, - "httpRequest": { - "description": "Bounded read-only HTTP request template.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "semantics": {"x-registry-field": "property", "const": "read_only" }, - "query": {"x-registry-field": "property", "type": "object", "maxProperties": 64, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "headers": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "body": { "x-registry-field": "property" } - } - }, - "httpResponse": { - "description": "Rules for translating upstream status and body cardinality into normalized outcomes.", - "type": "object", - "additionalProperties": false, - "properties": { - "no_match": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "ambiguous": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "shape": { - "x-registry-field": "property", - "oneOf": [ - {"x-registry-field": "branch", "const": "singleton" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["records", "cardinality"], - "properties": { - "records": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" }, - "cardinality": {"x-registry-field": "property", "const": "probe_two" } - } - } - ] - } - } - }, - "capability": { - "description": "Closed choice among direct HTTP, Rhai script, and exact snapshot execution.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["http"], - "properties": { - "http": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["request"], - "properties": { - "request": {"x-registry-field": "property", "$ref": "#/$defs/httpRequest" }, - "response": {"x-registry-field": "property", "$ref": "#/$defs/httpResponse" } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["script"], - "properties": { - "script": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, - "modules": { - "x-registry-field": "sensitive_property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/relativePath" } - } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["snapshot"], - "properties": { - "snapshot": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["entity", "exact", "freshness"], - "properties": { - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "exact": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 8, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputReference" } - }, - "freshness": { - "x-registry-field": "property", - "type": "string", - "description": "Maximum accepted snapshot age as a canonical positive duration no greater than 31 days.", - "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-5][0-9]{8}|26[0-6][0-9]{7}|267[0-7][0-9]{6}|2678[0-3][0-9]{5}|2678400000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-5][0-9]{5}|26[0-6][0-9]{4}|267[0-7][0-9]{3}|2678[0-3][0-9]{2}|2678400)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-3][0-9]{3}|44[0-5][0-9]{2}|446[0-3][0-9]{1}|44640)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-3][0-9]{1}|74[0-3]|744)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30|31)d)$" - } - } - } - } - } - ] - }, - "output": { - "description": "Type and optional source pointer for one normalized integration output.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "x-registry-source": { - "x-registry-field": "property", - "type": "string", - "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" - } - } - }, - "limits": { - "description": "Per-execution call, byte, and deadline budgets.", - "type": "object", - "additionalProperties": false, - "properties": { - "calls": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16 }, - "request_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationRequestByteSize" }, - "source_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationSourceByteSize" }, - "deadline": { - "x-registry-field": "property", - "description": "Positive total integration deadline no greater than 20 seconds.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - } -} diff --git a/projects/sipf-pensions/.registry-stack-editor/schemas/project.schema.json b/projects/sipf-pensions/.registry-stack-editor/schemas/project.schema.json deleted file mode 100644 index 91b7ea7..0000000 --- a/projects/sipf-pensions/.registry-stack-editor/schemas/project.schema.json +++ /dev/null @@ -1,537 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/project.v1.json", - "title": "Registry Stack project v1", - "description": "Declares the product-neutral Registry Stack project graph: integrations, materialized entities, and Relay or Notary services.", - "type": "object", - "additionalProperties": false, - "required": ["version", "registry", "services"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Project authoring format version." }, - "starter": { - "x-registry-field": "property", - "description": "Immutable provenance for a workspace initialized from a Registry Stack starter. The digest excludes this content_digest field and covers the starter's authored project files.", - "type": "object", - "additionalProperties": false, - "required": ["id", "release", "content_digest"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Registry Stack starter identifier." }, - "release": {"x-registry-field": "property", "$ref": "#/$defs/token", "description": "Registry Stack release that supplied the starter." }, - "content_digest": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^sha256:[0-9a-f]{64}$", "description": "Digest of the initialized starter authoring content, used to report later workspace divergence." } - } - }, - "registry": { - "x-registry-field": "property", - "description": "Stable identity of the Registry Stack project.", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } - }, - "integrations": { - "x-registry-field": "property", - "description": "Source adaptation definitions keyed by project-local integration identifier.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "entities": { - "x-registry-field": "property", - "description": "Materialized entity definitions keyed by project-local entity identifier.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "services": { - "x-registry-field": "property", - "description": "Relay records APIs and Notary evidence services exposed by the project.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/service" } - } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["integrations"], "properties": { "integrations": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["entities"], "properties": { "entities": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["services"], "properties": { "services": {"x-registry-field": "property", "minProperties": 1 } } } - ], - "examples": [ - { - "version": 1, - "registry": { "id": "benefits-registry" }, - "integrations": { - "household_lookup": { "file": "integrations/household-lookup/integration.yaml" } - }, - "services": {} - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Non-empty token without commas, whitespace, or control characters." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Project-relative path without traversal or duplicate separators." }, - "text": { "type": "string", "minLength": 1, "maxLength": 2048, "description": "Bounded human-readable project metadata." }, - "scope": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Authorization scope token." }, - "recordsApi": { - "description": "Relay records API behavior, authorization, query bounds, and standards profiles.", - "type": "object", - "additionalProperties": false, - "required": ["scopes", "projection", "pagination", "standards"], - "properties": { - "scopes": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["metadata", "rows"], - "properties": { - "metadata": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "rows": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "evidence_verification": {"x-registry-field": "property", "$ref": "#/$defs/scope" } - } - }, - "purposes": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } }, - "projection": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 256, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "pagination": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["default_limit", "max_limit"], - "properties": { - "default_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 }, - "max_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 } - } - }, - "filters": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 256, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } - }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "relationships": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["kind", "target", "foreign_key"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["belongs_to", "has_many", "has_one"] }, - "target": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "foreign_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "concept_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - } - }, - "aggregates": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAggregate" } - }, - "attribute_release_profiles": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseProfiles" }, - "standards": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["ogc_features", "sp_dci"], - "properties": { - "ogc_features": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpatial" }] }, - "sp_dci": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpdci" }] } - } - } - } - }, - "recordAttributeReleaseProfiles": { - "description": "Purpose-bound, exact-one identity releases keyed by stable profile id. Generated Relay responses omit source metadata and are never cacheable.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAttributeReleaseProfile" } - }, - "recordAttributeReleaseProfile": { - "description": "One purpose-bound, minimized identity release compiled into the owning Relay entity.", - "type": "object", - "additionalProperties": false, - "required": ["version", "purpose", "release_scope", "subject", "release_conditions", "claims"], - "properties": { - "version": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "release_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "subject": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["source_field", "id_type"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "id_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" } - } - }, - "release_conditions": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["expression"], - "properties": { "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" } } - }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["required", "sensitivity"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" }, - "required": {"x-registry-field": "property", "type": "boolean" }, - "sensitivity": {"x-registry-field": "property", "enum": ["direct_identifier", "personal", "public", "pseudonymous"] } - }, - "oneOf": [ - {"x-registry-field": "branch", "required": ["source_field"] }, - {"x-registry-field": "branch", "required": ["expression"] } - ] - } - } - } - }, - "recordAttributeReleaseExpression": { - "description": "Bounded CEL evaluated only against the projected source object.", - "type": "object", - "additionalProperties": false, - "required": ["cel"], - "properties": { "cel": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 4096 } } - }, - "filterOperators": { "type": "array", "minItems": 1, "uniqueItems": true, "description": "Allowed operators for one queryable field.", "items": {"x-registry-field": "array_item", "enum": ["eq", "in", "gte", "lte", "between"] } }, - "fieldList": { "type": "array", "maxItems": 16, "uniqueItems": true, "description": "Bounded unique list of entity field identifiers.", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "recordAggregate": { - "description": "Governed aggregate definition with disclosure controls.", - "type": "object", - "additionalProperties": false, - "required": ["description", "disclosure_control"], - "properties": { - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "default_group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "dimensions": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateDimension" } }, - "indicators": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateIndicator" } }, - "allowed_filters": {"x-registry-field": "property", "type": "object", "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "temporal_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateAccess" }, - "spatial": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateSpatial" }, - "joins": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "measures": {"x-registry-field": "property", "type": "array", "minItems": 1, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateMeasure" } }, - "disclosure_control": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "min_group_size": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "suppression": {"x-registry-field": "property", "enum": ["omit", "mask", "null"] } - } - } - }, - "anyOf": [{"x-registry-field": "branch", "required": ["measures"] }, {"x-registry-field": "branch", "required": ["indicators"] }] - }, - "recordAggregateAccess": { - "description": "Authorization scopes and execution policy for one governed aggregate.", - "type": "object", - "additionalProperties": false, - "properties": { - "metadata_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_only_execution": {"x-registry-field": "property", "type": "boolean" } - } - }, - "recordAggregateSpatial": { - "description": "Administrative-area geometry join used to spatially group aggregate results.", - "type": "object", - "additionalProperties": false, - "required": ["mode", "dimension", "geometry_entity", "geometry_id_field", "geometry_field"], - "properties": { - "mode": {"x-registry-field": "property", "const": "admin_area" }, - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "dimension": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_id_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatial": { - "description": "OGC API Features collection metadata and bounded geometry mapping.", - "type": "object", - "additionalProperties": false, - "required": ["geometry"], - "properties": { - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "geometry": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialGeometry" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "datetime_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_bbox_degrees": {"x-registry-field": "property", "type": "number", "exclusiveMinimum": 0 }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatialGeometry": { - "description": "Supported point or encoded-geometry field mapping for spatial records.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "longitude_field", "latitude_field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "const": "point" }, - "longitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "latitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["geojson", "wkt", "wkb"] }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - } - ] - }, - "recordSpatialBbox": { - "description": "Record fields carrying the four coordinates of a bounding box.", - "type": "object", - "additionalProperties": false, - "required": ["min_x", "min_y", "max_x", "max_y"], - "properties": { - "min_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "min_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordSpdci": { - "description": "SP-DCI registry identity and field mappings for a records service.", - "type": "object", - "additionalProperties": false, - "required": ["registry", "registry_type", "record_type", "identifiers", "expression_fields"], - "properties": { - "registry": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "registry_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "record_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "identifiers": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "expression_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "response_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" } - } - }, - "recordFieldMap": { - "description": "Bounded mapping from standard field names to entity field identifiers.", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } - }, - "recordAggregateDimension": { - "description": "Named grouping dimension backed by one entity field.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "field"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "codelist": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateIndicator": { - "description": "SDMX-style aggregate indicator with units and display metadata.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "function", "column", "unit_measure"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "unit_measure": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "unit_mult": {"x-registry-field": "property", "type": "integer", "minimum": -2147483648, "maximum": 2147483647 }, - "decimals": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 4294967295 }, - "frequency": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "definition_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateMeasure": { - "description": "Named aggregate calculation over one entity column.", - "type": "object", - "additionalProperties": false, - "required": ["name", "function", "column"], - "properties": { - "name": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordAggregateFunction": { - "description": "Supported aggregate calculation function.", - "enum": ["count", "sum", "avg", "min", "max", "median", "count_distinct", "stddev"] - }, - "disclosureMode": { "enum": ["value", "predicate", "redacted"], "description": "Maximum detail a claim may disclose." }, - "disclosure": { - "description": "Fixed disclosure mode or a default constrained by explicitly allowed alternatives.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/disclosureMode" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["default", "allowed"], - "properties": { - "default": {"x-registry-field": "property", "$ref": "#/$defs/disclosureMode" }, - "allowed": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/disclosureMode" } } - } - } - ] - }, - "service": { - "description": "Closed choice between a Notary evidence service and a Relay records service.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/evidenceService" }, - {"x-registry-field": "branch", "$ref": "#/$defs/recordsService" } - ] - }, - "recordsService": { - "description": "Relay records API backed by one authored materialized entity.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "entity", "api"], - "properties": { - "kind": {"x-registry-field": "property", "const": "records_api" }, - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "owner": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "sensitivity": {"x-registry-field": "property", "enum": ["public", "internal", "personal", "confidential", "secret"] }, - "access_rights": {"x-registry-field": "property", "enum": ["public", "restricted", "non_public"] }, - "update_frequency": {"x-registry-field": "property", "enum": ["continuous", "daily", "weekly", "termly", "monthly", "quarterly", "annual", "irregular", "as_needed", "unknown"] }, - "conforms_to": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/text" } }, - "api": {"x-registry-field": "property", "$ref": "#/$defs/recordsApi" } - } - }, - "evidenceService": { - "description": "Notary evidence policy, consultations, claims, and registry-backed credential profiles. Source-free claims remain evaluation-only and cannot be selected by a credential profile.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "version", "purpose", "legal_basis", "consent", "access", "claims"], - "properties": { - "kind": {"x-registry-field": "property", "const": "evidence" }, - "version": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "subject_type": { - "x-registry-field": "property", - "description": "Subject category evaluated by this evidence service. Omission is normalized to person.", - "enum": ["person", "project"] - }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "legal_basis": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "consent": {"x-registry-field": "property", "enum": ["not_required", "required"] }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/access" }, - "variables": {"x-registry-field": "property", "$ref": "#/$defs/variables" }, - "consultations": {"x-registry-field": "property", "$ref": "#/$defs/consultations" }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["disclosure"], - "properties": { "output": {"x-registry-field": "property", "type": "string" }, "cel": {"x-registry-field": "property", "type": "string", "minLength": 1 }, "value": {"x-registry-field": "property", "$ref": "#/$defs/claimValue" }, "disclosure": {"x-registry-field": "property", "$ref": "#/$defs/disclosure" } }, - "oneOf": [{"x-registry-field": "branch", "required": ["output"] }, {"x-registry-field": "branch", "required": ["cel"] }] - } - }, - "credential_profiles": { - "x-registry-field": "property", - "description": "Credential profiles may select only claims backed by an exact Relay consultation. Source-free claim evaluation cannot be exposed as credential capability.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["format", "type", "validity", "claims"], - "properties": { - "format": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "validity": {"x-registry-field": "property", "type": "string", "pattern": "^[1-9][0-9]*(?:s|m|h)$" }, - "claims": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } } - } - } - } - } - }, - "access": { - "description": "Scopes a caller must hold to use an evidence service.", - "type": "object", "additionalProperties": false, "required": ["scopes"], - "properties": { "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } } } - }, - "variables": { - "description": "Typed request variables exposed to evidence evaluation.", - "type": "object", "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["from", "type"], "properties": { "from": {"x-registry-field": "property", "type": "string", "pattern": "^request\\.variables\\.[a-z][a-z0-9._-]{0,95}$" }, "type": {"x-registry-field": "property", "const": "date" } } } - }, - "consultations": { - "description": "Relay consultations and their closed bindings to target request identifiers or caller-supplied target attributes.", - "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["integration", "input"], "properties": { "integration": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, "input": {"x-registry-field": "property", "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/targetRequestMapping" } } } } - }, - "targetRequestMapping": { - "description": "Closed target request binding. Attribute values are bounded typed caller-supplied context, not authenticated identifiers.", - "type": "string", - "maxLength": 128, - "pattern": "^request\\.target\\.(?:id|identifiers\\.[A-Za-z][A-Za-z0-9._-]{0,95}|attributes\\.[a-z][a-z0-9_]{0,63})$" - }, - "claimValue": { - "description": "Explicit claim value type, nullability, and encoded-size bound for source-free evaluation. A source-free claim cannot be selected by a credential profile.", - "type": "object", "additionalProperties": false, "required": ["type"], - "properties": { "type": {"x-registry-field": "property", "enum": ["boolean", "integer", "string", "date"] }, "nullable": {"x-registry-field": "property", "type": "boolean" }, "max_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 } } - } - } -} diff --git a/projects/sipf-pensions/.vscode/extensions.json b/projects/sipf-pensions/.vscode/extensions.json deleted file mode 100644 index 6098b6e..0000000 --- a/projects/sipf-pensions/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "redhat.vscode-yaml" - ] -} diff --git a/projects/sipf-pensions/.vscode/settings.json b/projects/sipf-pensions/.vscode/settings.json deleted file mode 100644 index db92a93..0000000 --- a/projects/sipf-pensions/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "yaml.schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } -} diff --git a/projects/sipf-pensions/.zed/settings.json b/projects/sipf-pensions/.zed/settings.json deleted file mode 100644 index fac07ac..0000000 --- a/projects/sipf-pensions/.zed/settings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } - } - } - } - } -} diff --git a/projects/sipf-pensions/entities/pension.yaml b/projects/sipf-pensions/entities/pension.yaml deleted file mode 100644 index 475f824..0000000 --- a/projects/sipf-pensions/entities/pension.yaml +++ /dev/null @@ -1,22 +0,0 @@ -version: 1 -id: pension -revision: 1 -primary_key: pensioner_uin -schema: - type: object - additionalProperties: false - required: [pensioner_uin, payment_status] - properties: - pensioner_uin: - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - payment_status: - type: string - maxLength: 32 -materialization: - max_records: 1000000 - max_bytes: 256MiB - refresh: 1m - retain_generations: 2 diff --git a/projects/sipf-pensions/entities/survivor.yaml b/projects/sipf-pensions/entities/survivor.yaml deleted file mode 100644 index 804f4de..0000000 --- a/projects/sipf-pensions/entities/survivor.yaml +++ /dev/null @@ -1,21 +0,0 @@ -version: 1 -id: survivor -revision: 1 -primary_key: spouse_uin -schema: - type: object - additionalProperties: false - required: [spouse_uin, survivor_eligible] - properties: - spouse_uin: - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - survivor_eligible: - type: boolean -materialization: - max_records: 1000000 - max_bytes: 256MiB - refresh: 1m - retain_generations: 2 diff --git a/projects/sipf-pensions/environments/hosted.yaml b/projects/sipf-pensions/environments/hosted.yaml deleted file mode 100644 index ae9a145..0000000 --- a/projects/sipf-pensions/environments/hosted.yaml +++ /dev/null @@ -1,35 +0,0 @@ -version: 1 -entities: - pension: - provider: - type: postgres - connection: { secret: SOLMARA_SIPF_DATABASE_URL } - schema: public - table: sipf_pension_payment - columns: - pensioner_uin: pensioner_uin - payment_status: payment_status - source_revision: sipf-pension-payment-projection-v1 - generation: 2026-07-14 - survivor: - provider: - type: postgres - connection: { secret: SOLMARA_SIPF_DATABASE_URL } - schema: public - table: sipf_survivor_benefit - columns: - spouse_uin: spouse_uin - survivor_eligible: survivor_eligible - source_revision: sipf-survivor-benefit-projection-v1 - generation: 2026-07-14 -relay: - origin: https://sipf-relay.solmara.registrystack.org - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } -deployment: - profile: hosted_lab - relay: { service: sipf-pensions-relay } diff --git a/projects/sipf-pensions/environments/local.yaml b/projects/sipf-pensions/environments/local.yaml deleted file mode 100644 index 7064b3c..0000000 --- a/projects/sipf-pensions/environments/local.yaml +++ /dev/null @@ -1,35 +0,0 @@ -version: 1 -entities: - pension: - provider: - type: postgres - connection: { secret: SOLMARA_SIPF_DATABASE_URL } - schema: public - table: sipf_pension_payment - columns: - pensioner_uin: pensioner_uin - payment_status: payment_status - source_revision: sipf-pension-payment-projection-v1 - generation: 2026-07-14 - survivor: - provider: - type: postgres - connection: { secret: SOLMARA_SIPF_DATABASE_URL } - schema: public - table: sipf_survivor_benefit - columns: - spouse_uin: spouse_uin - survivor_eligible: survivor_eligible - source_revision: sipf-survivor-benefit-projection-v1 - generation: 2026-07-14 -relay: - origin: http://127.0.0.1:8080 - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } -deployment: - profile: local - relay: { service: sipf-pensions-relay } diff --git a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/ambiguous.yaml b/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/ambiguous.yaml deleted file mode 100644 index 78d5378..0000000 --- a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/ambiguous.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: sipf-pension-case-ambiguous -classification: synthetic -input: { uin: "2300109568" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: - status: 200 - body: - - { payment_status: active } - - { payment_status: suspended } -expect: - outcome: ambiguous - outputs: {} diff --git a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/match.yaml b/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/match.yaml deleted file mode 100644 index 06f7c9f..0000000 --- a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: sipf-pension-case-match -classification: synthetic -input: { uin: "2300109568" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: { payment_status: active } } -expect: - outcome: match - outputs: { payment_status: active } diff --git a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/no-match.yaml b/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/no-match.yaml deleted file mode 100644 index 792dcad..0000000 --- a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/no-match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: sipf-pension-case-no-match -classification: synthetic -input: { uin: "9999999999" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: [] } -expect: - outcome: no_match - outputs: {} diff --git a/projects/sipf-pensions/integrations/pension-case-by-pensioner/integration.yaml b/projects/sipf-pensions/integrations/pension-case-by-pensioner/integration.yaml deleted file mode 100644 index e6b4256..0000000 --- a/projects/sipf-pensions/integrations/pension-case-by-pensioner/integration.yaml +++ /dev/null @@ -1,25 +0,0 @@ -version: 1 -id: solmara-sipf-pension-case-by-pensioner -revision: 1 - -input: - uin: - role: selector - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - -capability: - snapshot: - entity: pension - exact: - pensioner_uin: { input: uin } - freshness: 24h - -outputs: [payment_status] - -not_applicable: - subject_mismatch: - rationale: The minimized output projection omits pensioner_uin, so it contains no identifier comparable with the requested pensioner UIN. - request_fixture: sipf-pension-case-match diff --git a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/ambiguous.yaml b/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/ambiguous.yaml deleted file mode 100644 index 34858fd..0000000 --- a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/ambiguous.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: sipf-survivor-case-ambiguous -classification: synthetic -input: { uin: "2300118698" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: - status: 200 - body: - - { survivor_eligible: true } - - { survivor_eligible: false } -expect: - outcome: ambiguous - outputs: {} diff --git a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/match.yaml b/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/match.yaml deleted file mode 100644 index 093ad3d..0000000 --- a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: sipf-survivor-case-match -classification: synthetic -input: { uin: "2300118698" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: { survivor_eligible: true } } -expect: - outcome: match - outputs: { survivor_eligible: true } diff --git a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/no-match.yaml b/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/no-match.yaml deleted file mode 100644 index c07f8e7..0000000 --- a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/no-match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: sipf-survivor-case-no-match -classification: synthetic -input: { uin: "9999999999" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: [] } -expect: - outcome: no_match - outputs: {} diff --git a/projects/sipf-pensions/integrations/pension-case-by-survivor/integration.yaml b/projects/sipf-pensions/integrations/pension-case-by-survivor/integration.yaml deleted file mode 100644 index fc575d7..0000000 --- a/projects/sipf-pensions/integrations/pension-case-by-survivor/integration.yaml +++ /dev/null @@ -1,25 +0,0 @@ -version: 1 -id: solmara-sipf-pension-case-by-survivor -revision: 1 - -input: - uin: - role: selector - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - -capability: - snapshot: - entity: survivor - exact: - spouse_uin: { input: uin } - freshness: 24h - -outputs: [survivor_eligible] - -not_applicable: - subject_mismatch: - rationale: The minimized output projection omits spouse_uin, so it contains no identifier comparable with the requested survivor UIN. - request_fixture: sipf-survivor-case-match diff --git a/projects/sipf-pensions/registry-stack.yaml b/projects/sipf-pensions/registry-stack.yaml deleted file mode 100644 index 31bdcb7..0000000 --- a/projects/sipf-pensions/registry-stack.yaml +++ /dev/null @@ -1,87 +0,0 @@ -version: 1 - -registry: - id: solmara-sipf-pensions - -integrations: - pension-case-by-pensioner: - file: integrations/pension-case-by-pensioner/integration.yaml - pension-case-by-survivor: - file: integrations/pension-case-by-survivor/integration.yaml - -entities: - pension: - file: entities/pension.yaml - survivor: - file: entities/survivor.yaml - -services: - sipf-pension-records: - kind: records_api - entity: pension - title: SIPF pension payment records - description: Purpose-limited pension facts used by the Solmara Evidence service. - owner: Social Insurance and Pension Fund - sensitivity: personal - access_rights: restricted - update_frequency: continuous - api: - scopes: - metadata: pension:metadata - rows: pension:rows - aggregate: pension:aggregate - evidence_verification: pension:evidence_verification - purposes: - - https://id.registrystack.org/solmara/purpose/pension-payment-review - projection: [pensioner_uin, payment_status] - pagination: { default_limit: 2, max_limit: 2 } - filters: - pensioner_uin: [eq] - standards: { ogc_features: false, sp_dci: false } - - sipf-pension-consultation: - kind: consultation_api - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review - legal_basis: social-insurance-act - consent: not_required - consultations: - pension: - integration: pension-case-by-pensioner - input: - uin: request.target.identifiers.solmara_uin - - sipf-survivor-consultation: - kind: consultation_api - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - legal_basis: social-insurance-act - consent: not_required - consultations: - survivor: - integration: pension-case-by-survivor - input: - uin: request.target.identifiers.solmara_uin - - sipf-survivor-records: - kind: records_api - entity: survivor - title: SIPF survivor benefit records - description: Purpose-limited survivor facts used by the Solmara Evidence service. - owner: Social Insurance and Pension Fund - sensitivity: personal - access_rights: restricted - update_frequency: continuous - api: - scopes: - metadata: survivor:metadata - rows: survivor:rows - aggregate: survivor:aggregate - evidence_verification: survivor:evidence_verification - purposes: - - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - projection: [spouse_uin, survivor_eligible] - pagination: { default_limit: 2, max_limit: 2 } - filters: - spouse_uin: [eq] - standards: { ogc_features: false, sp_dci: false } diff --git a/projects/sro-social/.registry-stack-editor/manifest.json b/projects/sro-social/.registry-stack-editor/manifest.json deleted file mode 100644 index 3603a8e..0000000 --- a/projects/sro-social/.registry-stack-editor/manifest.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "format": "registry.stack.editor-manifest", - "version": 1, - "registryctl_version": "0.15.2", - "schemas": [ - { - "kind": "project", - "path": "schemas/project.schema.json", - "file_glob": "registry-stack.yaml", - "sha256": "sha256:9fd76663c866c52bb5a17a4cfea821049b7428c812f1666da9db0d43a00da144" - }, - { - "kind": "environment", - "path": "schemas/environment.schema.json", - "file_glob": "environments/*.yaml", - "sha256": "sha256:d16828629e001a30a1705af90bcc12a214be18cf5805a9833e65b03d631c9e9b" - }, - { - "kind": "integration", - "path": "schemas/integration.schema.json", - "file_glob": "integrations/*/integration.yaml", - "sha256": "sha256:2e45452f63c3c610ed14b79244bc9aa086d79d811ad0899223b36c160d449a79" - }, - { - "kind": "fixture", - "path": "schemas/fixture.schema.json", - "file_glob": "integrations/*/fixtures/*.yaml", - "sha256": "sha256:98c0ab21e35cecd143b51e4b5dd487de07ceea24e505bbe351d212e0628c8c8c" - }, - { - "kind": "entity", - "path": "schemas/entity.schema.json", - "file_glob": "entities/*.yaml", - "sha256": "sha256:4cbb31c8addeb54f8b57aaaff8f30c39e651a21a2d741a1b775ecb0d7c248342" - } - ] -} diff --git a/projects/sro-social/.registry-stack-editor/schemas/entity.schema.json b/projects/sro-social/.registry-stack-editor/schemas/entity.schema.json deleted file mode 100644 index ffc57c8..0000000 --- a/projects/sro-social/.registry-stack-editor/schemas/entity.schema.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/entity.v1.json", - "title": "Registry Stack project entity v1", - "description": "Defines a bounded, materialized entity that Relay can query without coupling the project to a specific source product.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "primary_key", "schema", "materialization"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Entity authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the entity." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this entity definition.", "examples": [1] }, - "primary_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Field whose value uniquely identifies each materialized record.", "examples": ["person_id"] }, - "schema": {"x-registry-field": "property", "$ref": "#/$defs/objectSchema", "description": "Closed JSON object schema for materialized records." }, - "materialization": { - "x-registry-field": "property", - "description": "Authored resource, refresh, and bounded recovery-set retention applied while building entity generations.", - "type": "object", - "additionalProperties": false, - "required": ["max_records", "max_bytes", "refresh", "retain_generations"], - "properties": { - "max_records": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 100000000, "description": "Maximum number of records allowed in one generation.", "examples": [10000] }, - "max_bytes": { - "x-registry-field": "property", - "description": "Maximum encoded size of one generation, up to 1 GiB, as bytes or a KiB/MiB quantity.", - "examples": ["16MiB"], - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1073741824 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,5}|10[0-3][0-9]{4}|104[0-7][0-9]{3}|1048[0-4][0-9]{2}|10485[0-6][0-9]|104857[0-6])KiB|(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])MiB)$" } - ] - }, - "refresh": { - "x-registry-field": "property", - "description": "Refresh cadence as a canonical positive duration no greater than 30 days, or manual when an operator initiates every refresh.", - "examples": ["1h"], - "oneOf": [ - {"x-registry-field": "branch", "const": "manual" }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-4][0-9]{8}|25[0-8][0-9]{7}|259[0-1][0-9]{6}|2592000000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-4][0-9]{5}|25[0-8][0-9]{4}|259[0-1][0-9]{3}|2592000)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-2][0-9]{3}|43[0-1][0-9]{2}|43200)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-1][0-9]{1}|720)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30)d)$" } - ] - }, - "retain_generations": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16, "description": "Number of completed cache generations, including the active generation, retained as a bounded recovery set after successful publication. This does not make arbitrary retained generations selectable for rollback.", "examples": [2] } - } - } - }, - "examples": [ - { - "version": 1, - "id": "people", - "revision": 1, - "primary_key": "person_id", - "schema": { - "type": "object", - "additionalProperties": false, - "required": ["person_id"], - "properties": { "person_id": { "type": "string" } } - }, - "materialization": { - "max_records": 10000, - "max_bytes": "16MiB", - "refresh": "1h", - "retain_generations": 2 - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "propertyName": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$", "description": "Portable lowercase record field name." }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for a nullable field.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "items": false, - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "fieldSchema": { - "description": "Bounded schema keywords supported for one scalar materialized field.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "enum": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true }, - "const": {"x-registry-field": "property"}, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 65536 }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 } - } - }, - "objectSchema": { - "description": "Closed object schema whose declared required fields must appear in properties.", - "type": "object", - "additionalProperties": false, - "required": ["type", "additionalProperties", "required", "properties"], - "properties": { - "type": {"x-registry-field": "property", "const": "object" }, - "additionalProperties": {"x-registry-field": "property", "const": false }, - "required": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 256, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/propertyName" } - }, - "properties": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 256, - "propertyNames": {"x-registry-field": "property", "$ref": "#/$defs/propertyName" }, - "additionalProperties": {"x-registry-field": "property", "$ref": "#/$defs/fieldSchema" } - } - } - } - } -} diff --git a/projects/sro-social/.registry-stack-editor/schemas/environment.schema.json b/projects/sro-social/.registry-stack-editor/schemas/environment.schema.json deleted file mode 100644 index f04c70b..0000000 --- a/projects/sro-social/.registry-stack-editor/schemas/environment.schema.json +++ /dev/null @@ -1,337 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/environment.v1.json", - "title": "Registry Stack project environment v1", - "description": "Binds a Registry Stack project to environment-specific services, sources, credentials, and deployment topology.", - "type": "object", - "additionalProperties": false, - "required": ["version", "deployment"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Environment authoring format version." }, - "integrations": {"x-registry-field": "property", "type": "object", "maxProperties": 16, "description": "Environment bindings keyed by authored integration identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/integration" } }, - "entities": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "description": "Environment-specific source bindings keyed by entity identifier.", "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/entity" } }, - "issuance": { - "x-registry-field": "property", - "description": "Notary issuer and signing-key bindings for credential issuance.", - "type": "object", "additionalProperties": false, "required": ["issuer", "signing_key", "signing_kid", "generation"], - "properties": { "issuer": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1 }, "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" }, "algorithm": {"x-registry-field": "property", "enum": ["EdDSA", "ES256"], "default": "EdDSA", "description": "Credential issuer signing algorithm. Holder proof remains EdDSA with did:jwk." }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "callers": { - "x-registry-field": "property", - "description": "Notary API-key caller identities and the scopes each identity receives.", - "type": "object", "maxProperties": 64, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["api_key_fingerprint", "scopes"], - "properties": { "api_key_fingerprint": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } - } - }, - "relay": { - "x-registry-field": "property", - "description": "Public Relay identity and token-validation settings for a Relay deployment.", - "type": "object", "additionalProperties": false, - "required": ["origin", "issuer", "jwks_url", "audience", "allowed_clients"], - "properties": { "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "allowed_clients": {"x-registry-field": "sensitive_property", "type": "array", "maxItems": 64, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } }, "local_api_keys": {"x-registry-field": "property", "description": "Local-profile-only synthetic principals and scopes. Registryctl generates the raw keys and supplies only their fingerprints to Relay.", "type": "object", "additionalProperties": false, "required": ["match_principal", "no_match_principal", "scopes"], "properties": { "match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "no_match_principal": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 128 } } } } } - }, - "notary_relay": { - "x-registry-field": "property", - "description": "Deployment-internal Relay connection, Notary workload identity, and token file used only when Notary consults Relay.", - "type": "object", "additionalProperties": false, "required": ["base_url", "workload_client_id", "token_file"], - "properties": { "base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/internalOrigin" }, "workload_client_id": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "token_file": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" } } - }, - "relay_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Relay-owned consultation PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_state": { - "x-registry-field": "property", - "description": "Optional deployment binding for Notary-owned PostgreSQL state transport trust.", - "type": "object", "additionalProperties": false, "required": ["postgresql"], - "properties": { - "postgresql": { - "x-registry-field": "property", - "type": "object", "additionalProperties": false, "required": ["root_certificate_path"], - "properties": { "root_certificate_path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } - } - } - }, - "notary_cel": { - "x-registry-field": "property", - "description": "Optional per-worker data/address-space ceiling for dedicated Notary CEL processes. The Notary default is 134217728 bytes; 1073741824 is the maximum emulation-compatible exception and remains a limit, not an allocation.", - "type": "object", "additionalProperties": false, "required": ["worker_memory_bytes"], - "properties": { - "worker_memory_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 33554432, "maximum": 1073741824 } - } - }, - "oid4vci": { - "x-registry-field": "property", - "description": "Explicit registry-backed holder-wallet OID4VCI binding for one authored Notary credential profile.", - "$ref": "#/$defs/oid4vci" - }, - "deployment": { - "x-registry-field": "property", - "description": "Products deployed in this environment and the operational profile they use.", - "type": "object", "additionalProperties": false, "required": ["profile"], - "properties": { "profile": {"x-registry-field": "property", "enum": ["local", "hosted_lab", "production", "evidence_grade"] }, "relay": {"x-registry-field": "property", "$ref": "#/$defs/service" }, "notary": {"x-registry-field": "property", "$ref": "#/$defs/service" } }, - "anyOf": [{"x-registry-field": "branch", "required": ["relay"] }, {"x-registry-field": "branch", "required": ["notary"] }] - } - }, - "allOf": [ - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["deployment"], "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } }, - "then": {"x-registry-field": "branch", "required": ["relay"] }, - "else": {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "required": ["relay"] } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_relay"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay", "notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["relay_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["relay"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_state"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["notary_cel"] }, - "then": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["notary"] } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "required": ["oid4vci"] }, - "then": { - "x-registry-field": "branch", - "required": ["notary_state"], - "properties": { - "deployment": {"x-registry-field": "property", "required": ["notary"] } - } - }, - "else": {"x-registry-field": "branch", "properties": { "callers": {"x-registry-field": "property", "minProperties": 1 } } } - }, - { - "x-registry-field": "branch", - "if": {"x-registry-field": "branch", "properties": { "deployment": {"x-registry-field": "property", "required": ["profile"], "properties": { "profile": {"x-registry-field": "property", "const": "local" } } } } }, - "else": { - "x-registry-field": "branch", - "properties": { - "relay": { - "x-registry-field": "property", - "not": {"x-registry-field": "branch", "required": ["local_api_keys"] }, - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - }, - "oid4vci": { - "x-registry-field": "property", - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorization_server": { - "x-registry-field": "property", - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/httpsResource" } - } - } - } - } - } - } - } - ], - "examples": [ - { - "version": 1, - "deployment": { - "profile": "local", - "notary": { "service": "notary" } - } - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token2048": { "type": "string", "minLength": 1, "maxLength": 2048, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII token, including full verification-method identifiers." }, - "secret": { "type": "object", "additionalProperties": false, "description": "Reference to a process-environment variable; secret values are never authored here.", "required": ["secret"], "properties": { "secret": {"x-registry-field": "secret_reference_property", "type": "string", "pattern": "^[A-Z_][A-Z0-9_]{0,127}$" } } }, - "origin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/?$", "description": "HTTPS origin without path, query, or fragment." }, - "httpsResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP][sS]://[^/?#]+/[^?#]+$", "description": "Exact HTTPS resource without query or fragment." }, - "localLoopbackOrigin": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/?$", "description": "HTTP IP-loopback origin; public Relay and issuer fields restrict it to the local profile, while internal Notary-to-Relay connections allow it in any profile." }, - "localLoopbackResource": { "type": "string", "format": "uri", "pattern": "^[hH][tT][tT][pP]://(?:127(?:\\.[0-9]{1,3}){3}|\\[::1\\])(?::[0-9]+)?/[^?#]+$", "description": "HTTP IP-loopback resource accepted only with the local deployment profile." }, - "internalOrigin": { "description": "Deployment-internal HTTPS or HTTP IP-loopback origin.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayOrigin": { "description": "Relay or issuer origin using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/origin" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackOrigin" }] }, - "relayResource": { "description": "Relay authentication resource using HTTPS, or HTTP IP-loopback under the local profile.", "anyOf": [{"x-registry-field": "branch", "$ref": "#/$defs/httpsResource" }, {"x-registry-field": "branch", "$ref": "#/$defs/localLoopbackResource" }] }, - "oid4vci": { - "description": "Closed OID4VCI trust, key, subject, wallet, and credential-profile binding.", - "type": "object", - "additionalProperties": false, - "required": ["public_base_url", "credential", "authorization_server", "client", "access_token", "sensitive_state_key", "subject", "redirect_uri", "allowed_wallet_origins"], - "properties": { - "public_base_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "credential": { - "x-registry-field": "property", - "description": "Existing project service and credential profile exposed through OID4VCI.", - "type": "object", "additionalProperties": false, "required": ["service", "profile"], - "properties": { - "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "profile": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "authorization_server": { - "x-registry-field": "property", - "description": "Pinned eSignet issuer and exact endpoints used for login and token validation.", - "type": "object", "additionalProperties": false, "required": ["issuer", "jwks_url", "userinfo_url", "authorize_url", "token_url"], - "properties": { - "issuer": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayOrigin" }, - "jwks_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "userinfo_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "authorize_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "token_url": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" } - } - }, - "client": { - "x-registry-field": "property", - "description": "eSignet relying-party identity and dedicated private-key reference.", - "type": "object", "additionalProperties": false, "required": ["id", "signing_key", "signing_kid"], - "properties": { - "id": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token256" }, - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "access_token": { - "x-registry-field": "property", - "description": "Dedicated Notary access-token signing key and published key identifier.", - "type": "object", "additionalProperties": false, "required": ["signing_key", "signing_kid"], - "properties": { - "signing_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "signing_kid": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/token2048" } - } - }, - "sensitive_state_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, - "subject": { - "x-registry-field": "property", - "description": "Verified eSignet userinfo claim bound exactly to the credential subject identifier.", - "type": "object", "additionalProperties": false, "required": ["token_claim", "id_type"], - "properties": { - "token_claim": {"x-registry-field": "property", "$ref": "#/$defs/token256" }, - "id_type": {"x-registry-field": "property", "$ref": "#/$defs/token256" } - } - }, - "redirect_uri": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relayResource" }, - "allowed_wallet_origins": { - "x-registry-field": "sensitive_property", - "description": "Exact HTTPS browser origins admitted to the wallet-facing Notary routes.", - "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "$ref": "#/$defs/origin" } - }, - "tx_code": { - "x-registry-field": "property", - "description": "Transaction-code policy. Omit for the secure required-PIN default. Set required=false only for a bounded bearer-offer interoperability profile; the compiler fixes the offer lifetime at 300 seconds.", - "type": "object", "additionalProperties": false, - "properties": { "required": {"x-registry-field": "property", "type": "boolean", "default": true } } - } - } - }, - "privateCidrs": { - "description": "Explicit private network ranges this source may resolve to.", - "type": "array", "maxItems": 16, "uniqueItems": true, - "items": {"x-registry-field": "sensitive_array_item", "type": "string", "minLength": 3, "maxLength": 64 } - }, - "ca": { - "description": "Pinned certificate-authority file and its rotation generation.", - "type": "object", "additionalProperties": false, "required": ["file", "generation"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "mtls": { - "description": "Client certificate and private-key reference for mutual TLS.", - "type": "object", "additionalProperties": false, "required": ["certificate_file", "private_key", "generation"], - "properties": { "certificate_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "private_key": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } - }, - "endpoint": { - "description": "Security-bound HTTPS endpoint with optional private-network, CA, and mTLS policy.", - "type": "object", "additionalProperties": false, "required": ["origin", "path", "generation"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//)(?!.*[?#]).+$" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } - } - }, - "credential": { - "description": "One supported source credential shape, containing references rather than values.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["username", "password", "generation"], "properties": { "username": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "password": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["token", "generation"], "properties": { "token": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["client_id", "client_secret", "generation"], "properties": { "client_id": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "client_secret": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["value", "generation"], "properties": { "value": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "generation": {"x-registry-field": "sensitive_property", "type": "integer", "minimum": 1, "maximum": 18446744073709551615 } } } - ] - }, - "provider": { - "description": "Environment-specific physical provider for a materialized entity.", - "oneOf": [ - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "csv" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "delimiter": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 }, "quote": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 255 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "project_file", "path", "sheet"], "properties": { "type": {"x-registry-field": "property", "const": "xlsx" }, "project_file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" }, "sheet": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, "header_row": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, "data_range": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "path"], "properties": { "type": {"x-registry-field": "property", "const": "parquet" }, "path": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/absolutePath" } } }, - {"x-registry-field": "branch", "type": "object", "additionalProperties": false, "required": ["type", "connection", "schema", "table"], "properties": { "type": {"x-registry-field": "property", "const": "postgres" }, "connection": {"x-registry-field": "secret_reference_property", "$ref": "#/$defs/secret" }, "schema": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" }, "table": {"x-registry-field": "property", "$ref": "#/$defs/postgresIdentifier" } } } - ] - }, - "entity": { - "description": "Maps an authored entity to provider columns and immutable source-generation identifiers.", - "type": "object", "additionalProperties": false, "required": ["provider", "columns", "source_revision", "generation"], - "properties": { "provider": {"x-registry-field": "property", "$ref": "#/$defs/provider" }, "columns": {"x-registry-field": "property", "type": "object", "minProperties": 1, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } }, "source_revision": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 }, "generation": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 256 } } - }, - "source": { - "description": "Runtime source connection policy for one authored integration.", - "type": "object", "additionalProperties": false, "required": ["origin"], - "properties": { - "origin": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/origin" }, - "allowed_private_cidrs": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/privateCidrs" }, - "ca": {"x-registry-field": "property", "$ref": "#/$defs/ca" }, - "mtls": {"x-registry-field": "property", "$ref": "#/$defs/mtls" }, - "credential": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "oauth": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "jwks": {"x-registry-field": "property", "$ref": "#/$defs/endpoint" }, - "rate": {"x-registry-field": "property", "type": "object", "additionalProperties": false, "required": ["per_minute", "burst"], "properties": { "per_minute": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 60000 }, "burst": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 1024 } } }, - "concurrency": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 64 }, - "timeout": { - "x-registry-field": "property", - "description": "Optional request timeout no greater than 20 seconds; generated bindings narrow it to the integration deadline.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - }, - "integration": { - "description": "Environment binding for an authored source integration.", - "type": "object", "additionalProperties": false, "required": ["source"], - "properties": { "source": {"x-registry-field": "property", "$ref": "#/$defs/source" } } - }, - "service": { "type": "object", "additionalProperties": false, "description": "Binds an authored product role to a deployed service identifier.", "required": ["service"], "properties": { "service": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } }, - "token256": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[!-~]+$", "description": "Bounded visible-ASCII protocol token." }, - "postgresIdentifier": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,62}$", "description": "Portable unquoted PostgreSQL schema or table identifier." }, - "absolutePath": { "type": "string", "minLength": 2, "maxLength": 4096, "pattern": "^/(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized absolute path without dot segments or duplicate separators." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Normalized project-relative path without dot segments or duplicate separators." } - } -} diff --git a/projects/sro-social/.registry-stack-editor/schemas/fixture.schema.json b/projects/sro-social/.registry-stack-editor/schemas/fixture.schema.json deleted file mode 100644 index b853192..0000000 --- a/projects/sro-social/.registry-stack-editor/schemas/fixture.schema.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/fixture.v1.json", - "title": "Registry Stack project integration fixture v1", - "description": "Defines one synthetic, deterministic integration scenario for offline adapter verification.", - "type": "object", - "additionalProperties": false, - "required": ["name", "classification", "input", "interactions", "expect"], - "properties": { - "name": {"x-registry-field": "public_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Human-readable scenario name shown in test output.", "examples": ["existing household matches"] }, - "classification": {"x-registry-field": "public_property", "const": "synthetic", "description": "Declares that fixture data is synthetic and safe for offline testing." }, - "request": { - "x-registry-field": "redacted_fixture_property", - "$ref": "#/$defs/governedRequest", - "description": "Optional independently authored synthetic Notary request used to prove the request-to-consultation binding before Relay access." - }, - "input": { - "x-registry-field": "redacted_fixture_property", - "description": "Typed consultation inputs supplied to the adapter under test.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "type": ["string", "boolean", "integer", "null"] - } - }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Named date values available to deterministic fixture interpolation.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "interactions": { - "x-registry-field": "redacted_fixture_property", - "description": "Ordered upstream request and response exchanges expected during execution.", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/interaction" } - }, - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/expectation", "description": "Observable adapter result required for the scenario to pass." } - }, - "examples": [ - { - "name": "existing household matches", - "classification": "synthetic", - "input": { "household_id": "HH-1001" }, - "interactions": [ - { - "expect": { "method": "GET", "path": "/households/HH-1001" }, - "respond": { "status": 200, "body": { "active": true } } - } - ], - "expect": { "outcome": "match", "outputs": { "active": true } } - } - ], - "$defs": { - "governedRequest": { - "description": "The same closed governed request shape accepted by project test --live.", - "type": "object", - "additionalProperties": false, - "required": ["target", "claims", "purpose"], - "properties": { - "target": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/governedTarget" }, - "variables": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies synthetic date variables to the governed evaluation request.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[a-z][a-z0-9._-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "format": "date" } - }, - "claims": { - "x-registry-field": "redacted_fixture_property", - "description": "Lists the authored claims evaluated by this synthetic request witness.", - "type": "array", - "minItems": 1, - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedClaimRef" } - }, - "disclosure": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the disclosure mode requested from the authored claim policy." }, - "format": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Selects the claim-result media type requested from the governed Notary path." }, - "purpose": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256, "description": "Names the authored service purpose exercised by this synthetic request witness." } - } - }, - "governedTarget": { - "description": "The synthetic subject target presented to the same governed request boundary as a live Notary evaluation.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 64, "description": "Names the authored target entity type used for claim evaluation." }, - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies an optional synthetic direct target identifier." }, - "identifiers": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored synthetic identifiers for consultation input mapping.", - "type": "array", - "maxItems": 16, - "items": {"x-registry-field": "redacted_fixture_array_item", "$ref": "#/$defs/governedIdentifier" } - }, - "attributes": { - "x-registry-field": "redacted_fixture_property", - "description": "Supplies independently authored typed target attributes for consultation input mapping.", - "type": "object", - "maxProperties": 16, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": ["string", "boolean", "integer", "null"] } - } - } - }, - "governedIdentifier": { - "description": "One synthetic target identifier with an authored scheme and string value.", - "type": "object", - "additionalProperties": false, - "required": ["scheme", "value"], - "properties": { - "scheme": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 96, "description": "Names the authored identifier scheme selected by a consultation input mapping." }, - "value": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Supplies the synthetic string value bound to this identifier scheme." } - } - }, - "governedClaimRef": { - "description": "A requested claim ID, optionally pinned to one authored claim version.", - "oneOf": [ - {"x-registry-field": "branch", "type": "string"}, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { - "id": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Names one authored claim requested by this fixture witness."}, - "version": {"x-registry-field": "redacted_fixture_property", "type": "string", "description": "Pins the requested claim to one authored claim-policy version."} - } - } - ] - }, - "interaction": { - "description": "One expected upstream request paired with its synthetic response.", - "type": "object", - "additionalProperties": false, - "required": ["expect", "respond"], - "properties": { - "expect": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/request" }, - "respond": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/response" } - } - }, - "request": { - "description": "Exact HTTP request shape the adapter must produce.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "redacted_fixture_property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "query": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 64, - "additionalProperties": { - "x-registry-field": "redacted_fixture_map_value", - "oneOf": [ - {"x-registry-field": "branch", "type": ["string", "boolean", "integer", "null"] }, - { - "x-registry-field": "branch", - "type": "array", - "maxItems": 64, - "items": {"x-registry-field": "redacted_fixture_array_item", "type": ["string", "boolean", "integer", "null"] } - } - ] - } - }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "redacted_fixture_map_key", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" }, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - "response": { - "description": "Synthetic HTTP response or timeout returned to the adapter.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["status"], - "properties": { - "status": {"x-registry-field": "redacted_fixture_property", "type": "integer", "minimum": 100, "maximum": 599 }, - "headers": { - "x-registry-field": "redacted_fixture_property", - "type": "object", - "maxProperties": 32, - "additionalProperties": {"x-registry-field": "redacted_fixture_map_value", "type": "string", "maxLength": 8192 } - }, - "body": {"x-registry-field": "redacted_fixture_property", "$ref": "#/$defs/fixtureBody" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["timeout"], - "properties": { - "timeout": { - "x-registry-field": "redacted_fixture_property", - "description": "Reviewed synthetic timeout intent no greater than 20 seconds; offline execution returns a timeout outcome without sleeping.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - ] - }, - "fixtureBody": { - "description": "Inline JSON, or an exact closed reference to a strict JSON file below the fixture bodies directory. The top-level file key is reserved for file references.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": { - "x-registry-field": "redacted_fixture_property", - "type": "string", - "minLength": 8, - "maxLength": 4096, - "pattern": "^bodies/(?!\\.\\.?/)(?!.*(?:/)\\.\\.?/)(?!.*//).+$" - } - } - }, - { - "x-registry-field": "branch", - "not": { - "x-registry-field": "branch", - "type": "object", - "required": ["file"] - } - } - ] - }, - "expectation": { - "description": "Expected normalized outcome, output values, claims, or error.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "outcome": {"x-registry-field": "redacted_fixture_property", "enum": ["match", "no_match", "ambiguous"] }, - "outputs": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "claims": {"x-registry-field": "redacted_fixture_property", "type": "object", "maxProperties": 64 }, - "error": {"x-registry-field": "redacted_fixture_property", "type": "string", "minLength": 1, "maxLength": 256 } - } - } - } -} diff --git a/projects/sro-social/.registry-stack-editor/schemas/integration.schema.json b/projects/sro-social/.registry-stack-editor/schemas/integration.schema.json deleted file mode 100644 index 9bb2ad0..0000000 --- a/projects/sro-social/.registry-stack-editor/schemas/integration.schema.json +++ /dev/null @@ -1,514 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/integration.v1.json", - "title": "Registry Stack project integration v1", - "description": "Defines a product-neutral source adaptation contract using bounded HTTP, Rhai script, or exact snapshot capabilities.", - "type": "object", - "additionalProperties": false, - "required": ["version", "id", "revision", "input", "capability", "outputs"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Integration authoring format version." }, - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Stable project-local identifier for the integration." }, - "revision": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295, "description": "Monotonically increasing revision of this integration contract.", "examples": [1] }, - "source": {"x-registry-field": "property", "$ref": "#/$defs/source", "description": "Optional source product metadata and transport policy; capabilities remain product-neutral." }, - "input": { - "x-registry-field": "property", - "description": "Typed selector and parameter inputs accepted by this integration.", - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/input" } - }, - "capability": {"x-registry-field": "property", "$ref": "#/$defs/capability", "description": "Exactly one bounded execution mechanism for the source adaptation." }, - "outputs": { - "x-registry-field": "property", - "description": "Named typed outputs, or a shorthand list of output names inferred by the authoring compiler.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": { - "x-registry-field": "map_key", - "allOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputName" }, - {"x-registry-field": "branch", "not": {"x-registry-field": "branch", "enum": ["matched", "outcome"] } } - ] - }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/output" } - }, - { - "x-registry-field": "branch", - "type": "array", - "minItems": 1, - "maxItems": 64, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/inputName" } - } - ] - }, - "limits": {"x-registry-field": "property", "$ref": "#/$defs/limits", "description": "Optional tighter resource limits for one integration execution." }, - "not_applicable": {"x-registry-field": "property", "$ref": "#/$defs/notApplicable", "description": "Explicit rationale and request-fixture evidence for a normally required outcome that the source contract cannot produce." } - }, - "examples": [ - { - "version": 1, - "id": "household_lookup", - "revision": 1, - "input": { - "household_id": { "role": "selector", "type": "string" } - }, - "capability": { - "script": { "file": "adapter.rhai" } - }, - "outputs": { - "active": { "type": "boolean" } - } - } - ], - "$defs": { - "notApplicable": { - "description": "Closed declarations for outcomes that the source protocol, exact singleton endpoint, or local unique-key constraint proves impossible.", - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "ambiguity": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed source contract cannot produce more than one matching record." }, - "subject_mismatch": {"x-registry-field": "property", "$ref": "#/$defs/notApplicableReason", "description": "Why the reviewed response contract contains no identifier comparable with a requested selector." } - } - }, - "notApplicableReason": { - "description": "A reviewable reason that names a real request fixture proving the source access shape used by the rationale.", - "type": "object", - "additionalProperties": false, - "required": ["rationale", "request_fixture"], - "properties": { - "rationale": {"x-registry-field": "property", "type": "string", "minLength": 24, "maxLength": 512, "description": "Why the named normally required outcome cannot occur for this source contract." }, - "request_fixture": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Fixture name containing the supporting bounded source request." } - } - }, - "stableId": { - "description": "Lowercase stable identifier used in project references.", - "type": "string", - "pattern": "^[a-z][a-z0-9._-]{0,95}$" - }, - "inputName": { - "description": "Portable lowercase name for an integration input or output.", - "type": "string", - "pattern": "^[a-z][a-z0-9_]{0,63}$" - }, - "relativePath": { - "description": "Project-relative path without traversal or duplicate separators.", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$" - }, - "integrationResponseByteSize": { - "description": "Maximum bytes accepted from one source response, defaulting to 512 KiB and capped at 8 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "512KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 8388608 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|[1-7][0-9]{3}|80[0-9]{2}|81[0-8][0-9]|819[0-2])KiB|[1-8]MiB)$" } - ] - }, - "integrationRequestByteSize": { - "description": "Maximum aggregate author-controlled request bytes for one execution, defaulting to 64 KiB and capped at 1 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "64KiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 1048576 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-4])KiB|1MiB)$" } - ] - }, - "integrationSourceByteSize": { - "description": "Maximum aggregate source-response bytes for one execution, defaulting to 2 MiB and capped at 16 MiB. Use a positive byte integer or canonical KiB/MiB value.", - "default": "2MiB", - "oneOf": [ - {"x-registry-field": "branch", "type": "integer", "minimum": 1, "maximum": 16777216 }, - {"x-registry-field": "branch", "type": "string", "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-5][0-9]{3}|16[0-2][0-9]{2}|163[0-7][0-9]|1638[0-4])KiB|(?:[1-9]|1[0-6])MiB)$" } - ] - }, - "scalarType": { - "description": "Supported scalar type, optionally paired with null for nullable values.", - "oneOf": [ - {"x-registry-field": "branch", "enum": ["string", "boolean", "integer"] }, - { - "x-registry-field": "branch", - "type": "array", - "prefixItems": [ - {"x-registry-field": "array_item", "enum": ["string", "boolean", "integer"] }, - {"x-registry-field": "array_item", "const": "null" } - ], - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } - ] - }, - "input": { - "description": "Type, validation, and canonicalization rules for one authored input.", - "type": "object", - "additionalProperties": false, - "required": ["role", "type"], - "properties": { - "role": {"x-registry-field": "property", "enum": ["selector", "parameter"] }, - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "description": "Maximum Unicode scalar-value length. The 1024-character ceiling derives a conservative 4096-byte UTF-8 ceiling for one canonical input.", "type": "integer", "minimum": 1, "maximum": 1024 }, - "minLength": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 16384 }, - "pattern": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, - "canonicalization": {"x-registry-field": "property", "enum": ["identity", "ascii_lowercase"] } - } - }, - "versions": { - "description": "Source versions tested by the project and versions explicitly accepted as unverified.", - "type": "object", - "additionalProperties": false, - "properties": { - "tested": {"x-registry-field": "property", "$ref": "#/$defs/versionList" }, - "unverified": {"x-registry-field": "property", "$ref": "#/$defs/versionList" } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["tested"], "properties": { "tested": {"x-registry-field": "property", "minItems": 1 } } }, - {"x-registry-field": "branch", "required": ["unverified"], "properties": { "unverified": {"x-registry-field": "property", "minItems": 1 } } } - ] - }, - "versionList": { - "description": "Bounded list of source version labels.", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "minLength": 1, "maxLength": 256 } - }, - "credential": { - "description": "Authentication mechanism expected by the source adapter, without environment secret values.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "enum": ["none", "basic", "static_bearer"] } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_header" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "name", "max_value_bytes"], - "properties": { - "type": {"x-registry-field": "property", "const": "api_key_query" }, - "name": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9._:~-]{0,95}$" }, - "max_value_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4096 } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["type", "request", "response_profile"], - "properties": { - "type": {"x-registry-field": "property", "const": "oauth2_client_credentials" }, - "request": {"x-registry-field": "property", "enum": ["form", "json"] }, - "response_profile": {"x-registry-field": "property", "const": "oauth2_bearer" }, - "scope": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 1024 }, - "audience": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "refresh_skew": { - "x-registry-field": "property", - "description": "Positive OAuth refresh interval below the generated token contract's 60-second minimum lifetime.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|[1-5][0-9]{4})ms|(?:[1-9]|[1-5][0-9])s)$" - } - } - } - ] - }, - "allowRule": { - "description": "One read-only upstream method and path pattern the adapter may call.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": { - "x-registry-field": "sensitive_property", - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^/[^?#]*$" - }, - "semantics": {"x-registry-field": "property", "const": "read_only" } - } - }, - "source": { - "description": "Optional source metadata, authentication shape, allowlist, and protocol profile.", - "type": "object", - "additionalProperties": false, - "required": ["auth"], - "properties": { - "product": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "versions": {"x-registry-field": "property", "$ref": "#/$defs/versions" }, - "auth": {"x-registry-field": "property", "$ref": "#/$defs/credential" }, - "allow": { - "x-registry-field": "property", - "type": "array", - "minItems": 1, - "maxItems": 16, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/allowRule" } - }, - "request_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response_headers": { - "x-registry-field": "property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$" } - }, - "response": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "format": {"x-registry-field": "property", "enum": ["json", "text"] }, - "max_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationResponseByteSize" } - } - }, - "protocol": {"x-registry-field": "property", "$ref": "#/$defs/protocol" } - } - }, - "protocol": { - "description": "Optional interoperable protocol profiles layered over the source transport.", - "type": "object", - "additionalProperties": false, - "properties": { - "signed_dci": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["profile", "path", "jwks_profile", "sender", "receiver", "registry_type", "record_type", "locale", "selectors"], - "properties": { - "profile": {"x-registry-field": "property", "const": "dci-search-v1" }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^/[^?#]*$" }, - "jwks_profile": {"x-registry-field": "property", "const": "rsa-signing-jwks-v1" }, - "sender": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "receiver": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 256 }, - "registry_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "record_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 128 }, - "locale": {"x-registry-field": "property", "type": "string", "pattern": "^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$" }, - "selectors": { - "x-registry-field": "property", - "type": "object", "minProperties": 1, "maxProperties": 8, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/inputName" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", "additionalProperties": false, "required": ["field", "response_pointer"], - "properties": { - "field": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 160 }, - "response_pointer": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" } - } - } - } - } - } - }, - "minProperties": 1 - }, - "inputReference": { - "description": "Reference to a declared integration input.", - "type": "object", - "additionalProperties": false, - "required": ["input"], - "properties": { "input": {"x-registry-field": "property", "$ref": "#/$defs/inputName" } } - }, - "inputOrLiteral": { - "description": "Request value supplied from a declared input or an authored scalar literal.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/inputReference" }, - {"x-registry-field": "branch", "type": ["string", "boolean", "integer"] } - ] - }, - "httpRequest": { - "description": "Bounded read-only HTTP request template.", - "type": "object", - "additionalProperties": false, - "required": ["method", "path"], - "properties": { - "method": {"x-registry-field": "property", "enum": ["GET", "POST"] }, - "path": {"x-registry-field": "sensitive_property", "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/[^?#]*$" }, - "semantics": {"x-registry-field": "property", "const": "read_only" }, - "query": {"x-registry-field": "property", "type": "object", "maxProperties": 64, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "headers": {"x-registry-field": "property", "type": "object", "maxProperties": 32, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputOrLiteral" } }, - "body": { "x-registry-field": "property" } - } - }, - "httpResponse": { - "description": "Rules for translating upstream status and body cardinality into normalized outcomes.", - "type": "object", - "additionalProperties": false, - "properties": { - "no_match": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "ambiguous": { - "x-registry-field": "property", - "type": "array", - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "type": "integer", "minimum": 100, "maximum": 599 } - }, - "shape": { - "x-registry-field": "property", - "oneOf": [ - {"x-registry-field": "branch", "const": "singleton" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["records", "cardinality"], - "properties": { - "records": {"x-registry-field": "property", "type": "string", "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" }, - "cardinality": {"x-registry-field": "property", "const": "probe_two" } - } - } - ] - } - } - }, - "capability": { - "description": "Closed choice among direct HTTP, Rhai script, and exact snapshot execution.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["http"], - "properties": { - "http": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["request"], - "properties": { - "request": {"x-registry-field": "property", "$ref": "#/$defs/httpRequest" }, - "response": {"x-registry-field": "property", "$ref": "#/$defs/httpResponse" } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["script"], - "properties": { - "script": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { - "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" }, - "modules": { - "x-registry-field": "sensitive_property", - "type": "array", - "maxItems": 32, - "uniqueItems": true, - "items": {"x-registry-field": "array_item", "$ref": "#/$defs/relativePath" } - } - } - } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["snapshot"], - "properties": { - "snapshot": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["entity", "exact", "freshness"], - "properties": { - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "exact": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 8, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/inputReference" } - }, - "freshness": { - "x-registry-field": "property", - "type": "string", - "description": "Maximum accepted snapshot age as a canonical positive duration no greater than 31 days.", - "pattern": "^(?:(?:[1-9][0-9]{0,8}|1[0-9]{9}|2[0-5][0-9]{8}|26[0-6][0-9]{7}|267[0-7][0-9]{6}|2678[0-3][0-9]{5}|2678400000)ms|(?:[1-9][0-9]{0,5}|1[0-9]{6}|2[0-5][0-9]{5}|26[0-6][0-9]{4}|267[0-7][0-9]{3}|2678[0-3][0-9]{2}|2678400)s|(?:[1-9][0-9]{0,3}|[1-3][0-9]{4}|4[0-3][0-9]{3}|44[0-5][0-9]{2}|446[0-3][0-9]{1}|44640)m|(?:[1-9][0-9]{0,1}|[1-6][0-9]{2}|7[0-3][0-9]{1}|74[0-3]|744)h|(?:[1-9][0-9]{0,0}|[1-2][0-9]{1}|30|31)d)$" - } - } - } - } - } - ] - }, - "output": { - "description": "Type and optional source pointer for one normalized integration output.", - "type": "object", - "additionalProperties": false, - "required": ["type"], - "properties": { - "type": {"x-registry-field": "property", "$ref": "#/$defs/scalarType" }, - "format": {"x-registry-field": "property", "const": "date" }, - "maxLength": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16384 }, - "minimum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "maximum": {"x-registry-field": "property", "type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807 }, - "x-registry-source": { - "x-registry-field": "property", - "type": "string", - "pattern": "^/(?:[^/~]|~[01])+(?:/(?:[^/~]|~[01])+)*$" - } - } - }, - "limits": { - "description": "Per-execution call, byte, and deadline budgets.", - "type": "object", - "additionalProperties": false, - "properties": { - "calls": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 16 }, - "request_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationRequestByteSize" }, - "source_bytes": {"x-registry-field": "property", "$ref": "#/$defs/integrationSourceByteSize" }, - "deadline": { - "x-registry-field": "property", - "description": "Positive total integration deadline no greater than 20 seconds.", - "type": "string", - "pattern": "^(?:(?:[1-9][0-9]{0,3}|1[0-9]{4}|20000)ms|(?:[1-9]|1[0-9]|20)s)$" - } - } - } - } -} diff --git a/projects/sro-social/.registry-stack-editor/schemas/project.schema.json b/projects/sro-social/.registry-stack-editor/schemas/project.schema.json deleted file mode 100644 index 91b7ea7..0000000 --- a/projects/sro-social/.registry-stack-editor/schemas/project.schema.json +++ /dev/null @@ -1,537 +0,0 @@ -{ - "x-registry-field": "root", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://registrystack.example/schemas/project-authoring/project.v1.json", - "title": "Registry Stack project v1", - "description": "Declares the product-neutral Registry Stack project graph: integrations, materialized entities, and Relay or Notary services.", - "type": "object", - "additionalProperties": false, - "required": ["version", "registry", "services"], - "properties": { - "version": {"x-registry-field": "public_property", "const": 1, "description": "Project authoring format version." }, - "starter": { - "x-registry-field": "property", - "description": "Immutable provenance for a workspace initialized from a Registry Stack starter. The digest excludes this content_digest field and covers the starter's authored project files.", - "type": "object", - "additionalProperties": false, - "required": ["id", "release", "content_digest"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId", "description": "Registry Stack starter identifier." }, - "release": {"x-registry-field": "property", "$ref": "#/$defs/token", "description": "Registry Stack release that supplied the starter." }, - "content_digest": {"x-registry-field": "sensitive_property", "type": "string", "pattern": "^sha256:[0-9a-f]{64}$", "description": "Digest of the initialized starter authoring content, used to report later workspace divergence." } - } - }, - "registry": { - "x-registry-field": "property", - "description": "Stable identity of the Registry Stack project.", - "type": "object", - "additionalProperties": false, - "required": ["id"], - "properties": { "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } } - }, - "integrations": { - "x-registry-field": "property", - "description": "Source adaptation definitions keyed by project-local integration identifier.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "entities": { - "x-registry-field": "property", - "description": "Materialized entity definitions keyed by project-local entity identifier.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["file"], - "properties": { "file": {"x-registry-field": "sensitive_property", "$ref": "#/$defs/relativePath" } } - } - }, - "services": { - "x-registry-field": "property", - "description": "Relay records APIs and Notary evidence services exposed by the project.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/service" } - } - }, - "anyOf": [ - {"x-registry-field": "branch", "required": ["integrations"], "properties": { "integrations": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["entities"], "properties": { "entities": {"x-registry-field": "property", "minProperties": 1 } } }, - {"x-registry-field": "branch", "required": ["services"], "properties": { "services": {"x-registry-field": "property", "minProperties": 1 } } } - ], - "examples": [ - { - "version": 1, - "registry": { "id": "benefits-registry" }, - "integrations": { - "household_lookup": { "file": "integrations/household-lookup/integration.yaml" } - }, - "services": {} - } - ], - "$defs": { - "stableId": { "type": "string", "pattern": "^[a-z][a-z0-9._-]{0,95}$", "description": "Lowercase stable identifier used in project references." }, - "token": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Non-empty token without commas, whitespace, or control characters." }, - "relativePath": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?/)(?!.*//).+$", "description": "Project-relative path without traversal or duplicate separators." }, - "text": { "type": "string", "minLength": 1, "maxLength": 2048, "description": "Bounded human-readable project metadata." }, - "scope": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$", "description": "Authorization scope token." }, - "recordsApi": { - "description": "Relay records API behavior, authorization, query bounds, and standards profiles.", - "type": "object", - "additionalProperties": false, - "required": ["scopes", "projection", "pagination", "standards"], - "properties": { - "scopes": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["metadata", "rows"], - "properties": { - "metadata": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "rows": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "evidence_verification": {"x-registry-field": "property", "$ref": "#/$defs/scope" } - } - }, - "purposes": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } }, - "projection": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 256, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "pagination": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["default_limit", "max_limit"], - "properties": { - "default_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 }, - "max_limit": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 10000 } - } - }, - "filters": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 256, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } - }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "relationships": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["kind", "target", "foreign_key"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["belongs_to", "has_many", "has_one"] }, - "target": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "foreign_key": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "concept_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - } - }, - "aggregates": { - "x-registry-field": "property", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAggregate" } - }, - "attribute_release_profiles": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseProfiles" }, - "standards": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["ogc_features", "sp_dci"], - "properties": { - "ogc_features": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpatial" }] }, - "sp_dci": {"x-registry-field": "property", "oneOf": [{"x-registry-field": "branch", "const": false }, {"x-registry-field": "branch", "$ref": "#/$defs/recordSpdci" }] } - } - } - } - }, - "recordAttributeReleaseProfiles": { - "description": "Purpose-bound, exact-one identity releases keyed by stable profile id. Generated Relay responses omit source metadata and are never cacheable.", - "type": "object", - "maxProperties": 16, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,95}$" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/recordAttributeReleaseProfile" } - }, - "recordAttributeReleaseProfile": { - "description": "One purpose-bound, minimized identity release compiled into the owning Relay entity.", - "type": "object", - "additionalProperties": false, - "required": ["version", "purpose", "release_scope", "subject", "release_conditions", "claims"], - "properties": { - "version": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "release_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "subject": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["source_field", "id_type"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "id_type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^,\\s\\u0000-\\u001F\\u007F]+$" } - } - }, - "release_conditions": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "required": ["expression"], - "properties": { "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" } } - }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["required", "sensitivity"], - "properties": { - "source_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "expression": {"x-registry-field": "property", "$ref": "#/$defs/recordAttributeReleaseExpression" }, - "required": {"x-registry-field": "property", "type": "boolean" }, - "sensitivity": {"x-registry-field": "property", "enum": ["direct_identifier", "personal", "public", "pseudonymous"] } - }, - "oneOf": [ - {"x-registry-field": "branch", "required": ["source_field"] }, - {"x-registry-field": "branch", "required": ["expression"] } - ] - } - } - } - }, - "recordAttributeReleaseExpression": { - "description": "Bounded CEL evaluated only against the projected source object.", - "type": "object", - "additionalProperties": false, - "required": ["cel"], - "properties": { "cel": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 4096 } } - }, - "filterOperators": { "type": "array", "minItems": 1, "uniqueItems": true, "description": "Allowed operators for one queryable field.", "items": {"x-registry-field": "array_item", "enum": ["eq", "in", "gte", "lte", "between"] } }, - "fieldList": { "type": "array", "maxItems": 16, "uniqueItems": true, "description": "Bounded unique list of entity field identifiers.", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } }, - "recordAggregate": { - "description": "Governed aggregate definition with disclosure controls.", - "type": "object", - "additionalProperties": false, - "required": ["description", "disclosure_control"], - "properties": { - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "default_group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "dimensions": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateDimension" } }, - "indicators": {"x-registry-field": "property", "type": "array", "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateIndicator" } }, - "allowed_filters": {"x-registry-field": "property", "type": "object", "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/filterOperators" } }, - "required_principal_filters": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "temporal_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateAccess" }, - "spatial": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateSpatial" }, - "joins": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "group_by": {"x-registry-field": "property", "$ref": "#/$defs/fieldList" }, - "measures": {"x-registry-field": "property", "type": "array", "minItems": 1, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/recordAggregateMeasure" } }, - "disclosure_control": { - "x-registry-field": "property", - "type": "object", - "additionalProperties": false, - "properties": { - "min_group_size": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "suppression": {"x-registry-field": "property", "enum": ["omit", "mask", "null"] } - } - } - }, - "anyOf": [{"x-registry-field": "branch", "required": ["measures"] }, {"x-registry-field": "branch", "required": ["indicators"] }] - }, - "recordAggregateAccess": { - "description": "Authorization scopes and execution policy for one governed aggregate.", - "type": "object", - "additionalProperties": false, - "properties": { - "metadata_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_scope": {"x-registry-field": "property", "$ref": "#/$defs/scope" }, - "aggregate_only_execution": {"x-registry-field": "property", "type": "boolean" } - } - }, - "recordAggregateSpatial": { - "description": "Administrative-area geometry join used to spatially group aggregate results.", - "type": "object", - "additionalProperties": false, - "required": ["mode", "dimension", "geometry_entity", "geometry_id_field", "geometry_field"], - "properties": { - "mode": {"x-registry-field": "property", "const": "admin_area" }, - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "dimension": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_id_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "geometry_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatial": { - "description": "OGC API Features collection metadata and bounded geometry mapping.", - "type": "object", - "additionalProperties": false, - "required": ["geometry"], - "properties": { - "collection_id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "geometry": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialGeometry" }, - "bbox_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordSpatialBbox" }, - "datetime_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_bbox_degrees": {"x-registry-field": "property", "type": "number", "exclusiveMinimum": 0 }, - "max_geometry_vertices": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 } - } - }, - "recordSpatialGeometry": { - "description": "Supported point or encoded-geometry field mapping for spatial records.", - "oneOf": [ - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "longitude_field", "latitude_field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "const": "point" }, - "longitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "latitude_field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["kind", "field", "crs"], - "properties": { - "kind": {"x-registry-field": "property", "enum": ["geojson", "wkt", "wkb"] }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "crs": {"x-registry-field": "property", "$ref": "#/$defs/token" } - } - } - ] - }, - "recordSpatialBbox": { - "description": "Record fields carrying the four coordinates of a bounding box.", - "type": "object", - "additionalProperties": false, - "required": ["min_x", "min_y", "max_x", "max_y"], - "properties": { - "min_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "min_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_x": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "max_y": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordSpdci": { - "description": "SP-DCI registry identity and field mappings for a records service.", - "type": "object", - "additionalProperties": false, - "required": ["registry", "registry_type", "record_type", "identifiers", "expression_fields"], - "properties": { - "registry": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "registry_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "record_type": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "identifiers": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "expression_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" }, - "response_fields": {"x-registry-field": "property", "$ref": "#/$defs/recordFieldMap" } - } - }, - "recordFieldMap": { - "description": "Bounded mapping from standard field names to entity field identifiers.", - "type": "object", - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/stableId" } - }, - "recordAggregateDimension": { - "description": "Named grouping dimension backed by one entity field.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "field"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "field": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "codelist": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateIndicator": { - "description": "SDMX-style aggregate indicator with units and display metadata.", - "type": "object", - "additionalProperties": false, - "required": ["id", "label", "function", "column", "unit_measure"], - "properties": { - "id": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "label": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "unit_measure": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "unit_mult": {"x-registry-field": "property", "type": "integer", "minimum": -2147483648, "maximum": 2147483647 }, - "decimals": {"x-registry-field": "property", "type": "integer", "minimum": 0, "maximum": 4294967295 }, - "frequency": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "definition_uri": {"x-registry-field": "property", "$ref": "#/$defs/text" } - } - }, - "recordAggregateMeasure": { - "description": "Named aggregate calculation over one entity column.", - "type": "object", - "additionalProperties": false, - "required": ["name", "function", "column"], - "properties": { - "name": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "function": {"x-registry-field": "property", "$ref": "#/$defs/recordAggregateFunction" }, - "column": {"x-registry-field": "property", "$ref": "#/$defs/stableId" } - } - }, - "recordAggregateFunction": { - "description": "Supported aggregate calculation function.", - "enum": ["count", "sum", "avg", "min", "max", "median", "count_distinct", "stddev"] - }, - "disclosureMode": { "enum": ["value", "predicate", "redacted"], "description": "Maximum detail a claim may disclose." }, - "disclosure": { - "description": "Fixed disclosure mode or a default constrained by explicitly allowed alternatives.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/disclosureMode" }, - { - "x-registry-field": "branch", - "type": "object", - "additionalProperties": false, - "required": ["default", "allowed"], - "properties": { - "default": {"x-registry-field": "property", "$ref": "#/$defs/disclosureMode" }, - "allowed": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/disclosureMode" } } - } - } - ] - }, - "service": { - "description": "Closed choice between a Notary evidence service and a Relay records service.", - "oneOf": [ - {"x-registry-field": "branch", "$ref": "#/$defs/evidenceService" }, - {"x-registry-field": "branch", "$ref": "#/$defs/recordsService" } - ] - }, - "recordsService": { - "description": "Relay records API backed by one authored materialized entity.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "entity", "api"], - "properties": { - "kind": {"x-registry-field": "property", "const": "records_api" }, - "entity": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, - "title": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "description": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "owner": {"x-registry-field": "property", "$ref": "#/$defs/text" }, - "sensitivity": {"x-registry-field": "property", "enum": ["public", "internal", "personal", "confidential", "secret"] }, - "access_rights": {"x-registry-field": "property", "enum": ["public", "restricted", "non_public"] }, - "update_frequency": {"x-registry-field": "property", "enum": ["continuous", "daily", "weekly", "termly", "monthly", "quarterly", "annual", "irregular", "as_needed", "unknown"] }, - "conforms_to": {"x-registry-field": "property", "type": "array", "maxItems": 32, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/text" } }, - "api": {"x-registry-field": "property", "$ref": "#/$defs/recordsApi" } - } - }, - "evidenceService": { - "description": "Notary evidence policy, consultations, claims, and registry-backed credential profiles. Source-free claims remain evaluation-only and cannot be selected by a credential profile.", - "type": "object", - "additionalProperties": false, - "required": ["kind", "version", "purpose", "legal_basis", "consent", "access", "claims"], - "properties": { - "kind": {"x-registry-field": "property", "const": "evidence" }, - "version": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 4294967295 }, - "subject_type": { - "x-registry-field": "property", - "description": "Subject category evaluated by this evidence service. Omission is normalized to person.", - "enum": ["person", "project"] - }, - "purpose": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "legal_basis": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "consent": {"x-registry-field": "property", "enum": ["not_required", "required"] }, - "access": {"x-registry-field": "property", "$ref": "#/$defs/access" }, - "variables": {"x-registry-field": "property", "$ref": "#/$defs/variables" }, - "consultations": {"x-registry-field": "property", "$ref": "#/$defs/consultations" }, - "claims": { - "x-registry-field": "property", - "type": "object", - "minProperties": 1, - "maxProperties": 64, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["disclosure"], - "properties": { "output": {"x-registry-field": "property", "type": "string" }, "cel": {"x-registry-field": "property", "type": "string", "minLength": 1 }, "value": {"x-registry-field": "property", "$ref": "#/$defs/claimValue" }, "disclosure": {"x-registry-field": "property", "$ref": "#/$defs/disclosure" } }, - "oneOf": [{"x-registry-field": "branch", "required": ["output"] }, {"x-registry-field": "branch", "required": ["cel"] }] - } - }, - "credential_profiles": { - "x-registry-field": "property", - "description": "Credential profiles may select only claims backed by an exact Relay consultation. Source-free claim evaluation cannot be exposed as credential capability.", - "type": "object", - "maxProperties": 32, - "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": { - "x-registry-field": "map_value", - "type": "object", - "additionalProperties": false, - "required": ["format", "type", "validity", "claims"], - "properties": { - "format": {"x-registry-field": "property", "$ref": "#/$defs/token" }, - "type": {"x-registry-field": "property", "type": "string", "minLength": 1, "maxLength": 2048 }, - "validity": {"x-registry-field": "property", "type": "string", "pattern": "^[1-9][0-9]*(?:s|m|h)$" }, - "claims": {"x-registry-field": "property", "type": "array", "minItems": 1, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/stableId" } } - } - } - } - } - }, - "access": { - "description": "Scopes a caller must hold to use an evidence service.", - "type": "object", "additionalProperties": false, "required": ["scopes"], - "properties": { "scopes": {"x-registry-field": "property", "type": "array", "minItems": 1, "maxItems": 16, "uniqueItems": true, "items": {"x-registry-field": "array_item", "$ref": "#/$defs/token" } } } - }, - "variables": { - "description": "Typed request variables exposed to evidence evaluation.", - "type": "object", "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["from", "type"], "properties": { "from": {"x-registry-field": "property", "type": "string", "pattern": "^request\\.variables\\.[a-z][a-z0-9._-]{0,95}$" }, "type": {"x-registry-field": "property", "const": "date" } } } - }, - "consultations": { - "description": "Relay consultations and their closed bindings to target request identifiers or caller-supplied target attributes.", - "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, - "additionalProperties": {"x-registry-field": "map_value", "type": "object", "additionalProperties": false, "required": ["integration", "input"], "properties": { "integration": {"x-registry-field": "property", "$ref": "#/$defs/stableId" }, "input": {"x-registry-field": "property", "type": "object", "minProperties": 1, "maxProperties": 16, "propertyNames": {"x-registry-field": "map_key", "$ref": "#/$defs/stableId" }, "additionalProperties": {"x-registry-field": "map_value", "$ref": "#/$defs/targetRequestMapping" } } } } - }, - "targetRequestMapping": { - "description": "Closed target request binding. Attribute values are bounded typed caller-supplied context, not authenticated identifiers.", - "type": "string", - "maxLength": 128, - "pattern": "^request\\.target\\.(?:id|identifiers\\.[A-Za-z][A-Za-z0-9._-]{0,95}|attributes\\.[a-z][a-z0-9_]{0,63})$" - }, - "claimValue": { - "description": "Explicit claim value type, nullability, and encoded-size bound for source-free evaluation. A source-free claim cannot be selected by a credential profile.", - "type": "object", "additionalProperties": false, "required": ["type"], - "properties": { "type": {"x-registry-field": "property", "enum": ["boolean", "integer", "string", "date"] }, "nullable": {"x-registry-field": "property", "type": "boolean" }, "max_bytes": {"x-registry-field": "property", "type": "integer", "minimum": 1, "maximum": 65536 } } - } - } -} diff --git a/projects/sro-social/.vscode/extensions.json b/projects/sro-social/.vscode/extensions.json deleted file mode 100644 index 6098b6e..0000000 --- a/projects/sro-social/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "redhat.vscode-yaml" - ] -} diff --git a/projects/sro-social/.vscode/settings.json b/projects/sro-social/.vscode/settings.json deleted file mode 100644 index db92a93..0000000 --- a/projects/sro-social/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "yaml.schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } -} diff --git a/projects/sro-social/.zed/settings.json b/projects/sro-social/.zed/settings.json deleted file mode 100644 index fac07ac..0000000 --- a/projects/sro-social/.zed/settings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "lsp": { - "yaml-language-server": { - "settings": { - "yaml": { - "schemas": { - "./.registry-stack-editor/schemas/entity.schema.json": "entities/*.yaml", - "./.registry-stack-editor/schemas/environment.schema.json": "environments/*.yaml", - "./.registry-stack-editor/schemas/fixture.schema.json": "integrations/*/fixtures/*.yaml", - "./.registry-stack-editor/schemas/integration.schema.json": "integrations/*/integration.yaml", - "./.registry-stack-editor/schemas/project.schema.json": "registry-stack.yaml" - } - } - } - } - } -} diff --git a/projects/sro-social/entities/sro_child.yaml b/projects/sro-social/entities/sro_child.yaml deleted file mode 100644 index bd5effa..0000000 --- a/projects/sro-social/entities/sro_child.yaml +++ /dev/null @@ -1,23 +0,0 @@ -version: 1 -id: sro_child -revision: 1 -primary_key: uin -schema: - type: object - additionalProperties: false - required: [uin, poverty_band] - properties: - uin: - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - poverty_band: - type: string - enum: [priority, standard, not_eligible] - maxLength: 16 -materialization: - max_records: 1000000 - max_bytes: 64MiB - refresh: 1m - retain_generations: 2 diff --git a/projects/sro-social/environments/hosted.yaml b/projects/sro-social/environments/hosted.yaml deleted file mode 100644 index c01d136..0000000 --- a/projects/sro-social/environments/hosted.yaml +++ /dev/null @@ -1,26 +0,0 @@ -version: 1 - -entities: - sro_child: - provider: - type: csv - path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv - header_row: 1 - columns: - uin: uin - poverty_band: poverty_band - source_revision: solmara-sro-child-benefit-projection-v1 - generation: "2026-07-14" - -relay: - origin: https://sro-relay.solmara.registrystack.org - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } - -deployment: - profile: hosted_lab - relay: { service: sro-social-relay } diff --git a/projects/sro-social/environments/local.yaml b/projects/sro-social/environments/local.yaml deleted file mode 100644 index 5b03e74..0000000 --- a/projects/sro-social/environments/local.yaml +++ /dev/null @@ -1,26 +0,0 @@ -version: 1 - -entities: - sro_child: - provider: - type: csv - path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv - header_row: 1 - columns: - uin: uin - poverty_band: poverty_band - source_revision: solmara-sro-child-benefit-projection-v1 - generation: "2026-07-14" - -relay: - origin: http://127.0.0.1:8080 - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json - audience: registry-relay - allowed_clients: [solmara-evidence] - consultation: { client_id: solmara-consultation, principal_id: solmara-consultation } -relay_state: { postgresql: { root_certificate_path: /etc/solmara/postgres/root.crt } } - -deployment: - profile: local - relay: { service: sro-social-relay } diff --git a/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/match.yaml b/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/match.yaml deleted file mode 100644 index e4b943a..0000000 --- a/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: child-benefit-household-match -classification: synthetic -input: { uin: "2300010248" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: { poverty_band: priority } } -expect: - outcome: match - outputs: { poverty_band: priority } diff --git a/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/no-match.yaml b/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/no-match.yaml deleted file mode 100644 index b1fa7ed..0000000 --- a/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/no-match.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: child-benefit-household-no-match -classification: synthetic -input: { uin: "2300999997" } -interactions: - - expect: { method: GET, path: /snapshot } - respond: { status: 200, body: [] } -expect: - outcome: no_match - outputs: {} diff --git a/projects/sro-social/integrations/child-benefit-household-by-uin/integration.yaml b/projects/sro-social/integrations/child-benefit-household-by-uin/integration.yaml deleted file mode 100644 index 60a9c0e..0000000 --- a/projects/sro-social/integrations/child-benefit-household-by-uin/integration.yaml +++ /dev/null @@ -1,28 +0,0 @@ -version: 1 -id: child-benefit-household-by-uin -revision: 1 - -input: - uin: - role: selector - type: string - minLength: 10 - maxLength: 10 - pattern: "^[0-9]{10}$" - -capability: - snapshot: - entity: sro_child - exact: - uin: { input: uin } - freshness: 31d - -outputs: [poverty_band] - -not_applicable: - ambiguity: - rationale: The exact UIN selector is the entity primary key, whose materialized unique-key constraint permits at most one active household projection. - request_fixture: child-benefit-household-match - subject_mismatch: - rationale: The minimized output projection omits UIN, so it contains no identifier comparable with the requested UIN. - request_fixture: child-benefit-household-match diff --git a/projects/sro-social/registry-stack.yaml b/projects/sro-social/registry-stack.yaml deleted file mode 100644 index eddf1cc..0000000 --- a/projects/sro-social/registry-stack.yaml +++ /dev/null @@ -1,48 +0,0 @@ -version: 1 - -registry: - id: solmara-sro-social - -integrations: - child-benefit-household-by-uin: - file: integrations/child-benefit-household-by-uin/integration.yaml - -entities: - sro_child: - file: entities/sro_child.yaml - -services: - sro-household-records: - kind: records_api - entity: sro_child - title: SRO household records - description: Purpose-limited household facts used by the Solmara Evidence service. - owner: Social Registry Office - sensitivity: personal - access_rights: restricted - update_frequency: continuous - api: - scopes: - metadata: sro_child:metadata - rows: sro_child:rows - aggregate: sro_child:aggregate - evidence_verification: sro_child:evidence_verification - purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - projection: [uin, poverty_band] - pagination: { default_limit: 2, max_limit: 2 } - filters: - uin: [eq] - standards: { ogc_features: false, sp_dci: false } - - sro-household-consultation: - kind: consultation_api - version: 1 - purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - legal_basis: child-benefit-act - consent: not_required - consultations: - household: - integration: child-benefit-household-by-uin - input: - uin: request.target.identifiers.solmara_uin diff --git a/relays/cra/codelists/record-lifecycle.yaml b/relays/cra/codelists/record-lifecycle.yaml new file mode 100644 index 0000000..cb66469 --- /dev/null +++ b/relays/cra/codelists/record-lifecycle.yaml @@ -0,0 +1,4 @@ +id: cra-record-lifecycle +version: "1" +status: reviewed +values: [active, deceased] diff --git a/relays/cra/expected-http.yaml b/relays/cra/expected-http.yaml new file mode 100644 index 0000000..ec3391b --- /dev/null +++ b/relays/cra/expected-http.yaml @@ -0,0 +1,42 @@ +schemaVersion: relay.registrystack.org/http-journey/v1alpha1 +registry: https://id.registrystack.org/solmara/registry/cra +authorizations: + death-ok: {principal: cra-pension-evidence, scopes: [solmara:relay:cra:death-by-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/pension-payment-review"}} + death-wrong-scope: {principal: cra-pension-evidence, scopes: [solmara:relay:cra:citizen-link-by-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/pension-payment-review"}} + death-wrong-purpose: {principal: cra-pension-evidence, scopes: [solmara:relay:cra:death-by-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/citizen-self-service"}} + citizen-ok: {principal: cra-citizen-evidence, scopes: [solmara:relay:cra:citizen-link-by-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/citizen-self-service"}} + citizen-wrong-scope: {principal: cra-citizen-evidence, scopes: [solmara:relay:cra:death-by-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/citizen-self-service"}} + citizen-wrong-purpose: {principal: cra-citizen-evidence, scopes: [solmara:relay:cra:citizen-link-by-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/pension-payment-review"}} +steps: + - {id: ready, request: {method: GET, path: /ready}, expect: {status: 200}} + - id: death-success + authorizationFixture: death-ok + request: {method: POST, path: /v2/resources/civil-person/lookups/death-by-uin, body: {uin: "2300109568"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [deceased], domainDataValues: {deceased: true}, absentEverywhere: [birthDateInternal, birthRegistrationNumberInternal, birth_date, birth_brn, linkedUin]} + - id: death-living-success + authorizationFixture: death-ok + request: {method: POST, path: /v2/resources/civil-person/lookups/death-by-uin, body: {uin: "2300010248"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [deceased], domainDataValues: {deceased: false}, absentEverywhere: [birthDateInternal, birthRegistrationNumberInternal, birth_date, birth_brn, linkedUin]} + - id: death-fields-minimum + authorizationFixture: death-ok + request: {method: POST, path: /v2/resources/civil-person/lookups/death-by-uin, query: {fields: deceased}, body: {uin: "2300109568"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [deceased], absentEverywhere: [linkedUin, birthDateInternal, birthRegistrationNumberInternal]} + - {id: death-wrong-scope, authorizationFixture: death-wrong-scope, request: {method: POST, path: /v2/resources/civil-person/lookups/death-by-uin, body: {uin: "2300109568"}}, expect: {status: 404, code: resource.not_found}} + - {id: death-wrong-purpose, authorizationFixture: death-wrong-purpose, request: {method: POST, path: /v2/resources/civil-person/lookups/death-by-uin, body: {uin: "2300109568"}}, expect: {status: 403, code: consultation.denied}} + - {id: death-malformed-selector, authorizationFixture: death-ok, request: {method: POST, path: /v2/resources/civil-person/lookups/death-by-uin, body: {uin: short}}, expect: {status: 400, code: consultation.invalid_request}} + - {id: death-no-match, authorizationFixture: death-ok, request: {method: POST, path: /v2/resources/civil-person/lookups/death-by-uin, body: {uin: "2399999999"}}, expect: {status: 404, code: consultation.unresolved, equivalenceClass: unresolved}} + - {id: death-invalid-row, authorizationFixture: death-ok, request: {method: POST, path: /v2/resources/civil-person/lookups/death-by-uin, body: {uin: "2300999999"}}, expect: {status: 503, code: source.unavailable}} + - id: citizen-success + authorizationFixture: citizen-ok + request: {method: POST, path: /v2/resources/civil-person/lookups/citizen-link-by-uin, body: {uin: "2300010248"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [linkedUin], domainDataValues: {linkedUin: "2300010248"}, absentEverywhere: [deceased, birthDateInternal, birthRegistrationNumberInternal, birth_date, birth_brn]} + - id: citizen-fields-minimum + authorizationFixture: citizen-ok + request: {method: POST, path: /v2/resources/civil-person/lookups/citizen-link-by-uin, query: {fields: linkedUin}, body: {uin: "2300010248"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [linkedUin], absentEverywhere: [deceased, birthDateInternal, birthRegistrationNumberInternal]} + - {id: citizen-wrong-scope, authorizationFixture: citizen-wrong-scope, request: {method: POST, path: /v2/resources/civil-person/lookups/citizen-link-by-uin, body: {uin: "2300010248"}}, expect: {status: 404, code: resource.not_found}} + - {id: citizen-wrong-purpose, authorizationFixture: citizen-wrong-purpose, request: {method: POST, path: /v2/resources/civil-person/lookups/citizen-link-by-uin, body: {uin: "2300010248"}}, expect: {status: 403, code: consultation.denied}} + - {id: citizen-malformed-selector, authorizationFixture: citizen-ok, request: {method: POST, path: /v2/resources/civil-person/lookups/citizen-link-by-uin, body: {uin: short}}, expect: {status: 400, code: consultation.invalid_request}} + - {id: citizen-no-match, authorizationFixture: citizen-ok, request: {method: POST, path: /v2/resources/civil-person/lookups/citizen-link-by-uin, body: {uin: "2399999999"}}, expect: {status: 404, code: consultation.unresolved, equivalenceClass: unresolved}} + - {id: citizen-invalid-row, authorizationFixture: citizen-ok, request: {method: POST, path: /v2/resources/civil-person/lookups/citizen-link-by-uin, body: {uin: "2300999999"}}, expect: {status: 503, code: source.unavailable}} + - {id: no-list-route, authorizationFixture: citizen-ok, request: {method: GET, path: /v2/resources/civil-person/records}, expect: {status: 404, routeAbsent: true}} diff --git a/relays/cra/fixture.sql b/relays/cra/fixture.sql new file mode 100644 index 0000000..a5f608c --- /dev/null +++ b/relays/cra/fixture.sql @@ -0,0 +1,25 @@ +PRAGMA user_version = 1; +CREATE TABLE civil_person_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + uin TEXT NOT NULL UNIQUE, + birth_date TEXT NOT NULL, + birth_brn TEXT, + deceased INTEGER NOT NULL CHECK (deceased IN (0, 1)) + ) STRICT; +CREATE VIEW relay_civil_person AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + uin, birth_date, birth_brn, deceased + FROM civil_person_source; +INSERT INTO civil_person_source VALUES +('CP-DEAD','rev-1','deceased','2026-07-04T09:00:00Z','2300109568','1944-02-01','BRN-1944-0301-00012',1), +('CP-LIVE','rev-2','active','2026-07-04T09:00:00Z','2300010248','2022-03-14','BRN-2022-0101-00001',0), +('CP-BAD','rev-3','deceased','invalid-date-time','2300999999','1950-01-01',NULL,1); +PRAGMA optimize; +-- relayctl's bundled SQLite enables STAT4 while the live Python publication does not. +-- Remove only that fixture-only schema entry so the fixture proves the published fingerprint. +PRAGMA writable_schema = ON; +DELETE FROM sqlite_schema WHERE name = 'sqlite_stat4'; +PRAGMA writable_schema = OFF; diff --git a/relays/cra/governance/classification-review-rationale.md b/relays/cra/governance/classification-review-rationale.md new file mode 100644 index 0000000..a6e562b --- /dev/null +++ b/relays/cra/governance/classification-review-rationale.md @@ -0,0 +1 @@ +CRA reviewed the inventory against the two exact-lookup disclosures. Death release contains only the current boolean deceased status for the exact queried UIN, including `false` where the person is living. Citizen link contains only the queried identifier as an existence-safe linked fact. Birth date, birth registration number, and unrelated civil data are not properties in either disclosure. diff --git a/relays/cra/governance/classification-review.yaml b/relays/cra/governance/classification-review.yaml new file mode 100644 index 0000000..3e66359 --- /dev/null +++ b/relays/cra/governance/classification-review.yaml @@ -0,0 +1,9 @@ +apiVersion: relay.registrystack.org/classification-review/v1 +kind: ClassificationReview +registryIdentifier: https://id.registrystack.org/solmara/registry/cra +classificationInventoryDigest: sha256:e58edad27369a9f4607a73275a02dda59b090ad3df72ff97ff7ec7237b1b7adc +method: manual +reviewer: did:web:id.registrystack.org:solmara:authority:cra +reviewDate: 2026-08-12 +status: reviewed +rationaleRef: governance/classification-review-rationale.md diff --git a/relays/cra/governance/identifier-lifecycle.yaml b/relays/cra/governance/identifier-lifecycle.yaml new file mode 100644 index 0000000..66a2f3f --- /dev/null +++ b/relays/cra/governance/identifier-lifecycle.yaml @@ -0,0 +1,2 @@ +status: reviewed +policy: CRA record identifiers are stable across revisions and never reassigned after retirement. diff --git a/relays/cra/governance/legal-basis.yaml b/relays/cra/governance/legal-basis.yaml new file mode 100644 index 0000000..849c7e9 --- /dev/null +++ b/relays/cra/governance/legal-basis.yaml @@ -0,0 +1,2 @@ +status: reviewed +legalBasis: Civil registration and pension administration laws authorize the two purpose-bound consultations. diff --git a/relays/cra/governance/processing.dpv.yaml b/relays/cra/governance/processing.dpv.yaml new file mode 100644 index 0000000..a7d3fa0 --- /dev/null +++ b/relays/cra/governance/processing.dpv.yaml @@ -0,0 +1,2 @@ +status: reviewed +profile: https://w3id.org/dpv/2.3 diff --git a/relays/cra/registry.yaml b/relays/cra/registry.yaml new file mode 100644 index 0000000..e71e266 --- /dev/null +++ b/relays/cra/registry.yaml @@ -0,0 +1,86 @@ +apiVersion: relay.registrystack.org/v2alpha1 +kind: RegistryContract +metadata: {id: solmara-cra-relay, version: "2026-08-12", title: CRA governed consultations} +registry: + registryIdentifier: https://id.registrystack.org/solmara/registry/cra + name: Civil Registration Authority Relay + authority: {identifier: "did:web:id.registrystack.org:solmara:authority:cra", name: Civil Registration Authority} + authoritativeScope: Current civil person link and death-registration facts held by CRA + baseUri: https://cra.gov.solmara.example/registry/ + identifierLifecyclePolicyRef: governance/identifier-lifecycle.yaml + alignmentTargets: [{name: govstack-digital-registries, version: 3.0.0-alpha.2, status: directional}] +governance: + controller: "did:web:id.registrystack.org:solmara:authority:cra" + publisher: "did:web:id.registrystack.org:solmara:authority:cra" + auditOwner: "did:web:id.registrystack.org:solmara:authority:cra" +semantics: {localVocabulary: https://id.registrystack.org/solmara/vocabulary/cra/} +classifications: + privacy: {scheme: https://w3id.org/dpv, version: "2.3"} + institutional: {scheme: https://id.registrystack.org/solmara/classification/cra, version: "1"} + handling: {scheme: https://id.registrystack.org/vocab/handling, version: "1"} + provenanceRef: governance/classification-review.yaml +sources: + cra: {kind: sqlite, profile: live-read-only, expectedSchemaFingerprint: "sha256:4aea7963a3de909459ac186f0f0630b7f7f2a6c8a53a6be67666e3422ed563fb"} +resources: + - id: civil-person + title: Civil person + description: Minimum CRA facts for exact, purpose-bound consultations. + semanticClass: local:CivilPerson + source: {source: cra, view: relay_civil_person} + classificationDefaults: {privacy: sensitive-personal, institutional: restricted, handling: restricted, status: reviewed} + sourceColumnClassifications: + record_id: {privacy: identifying} + record_revision: {privacy: non-personal} + lifecycle_state: {privacy: personal} + recorded_at: {privacy: personal-context} + uin: {privacy: identifying} + deceased: {privacy: sensitive-personal} + birth_date: {privacy: sensitive-personal} + birth_brn: {privacy: identifying} + recordContext: + recordIdentifier: {sourceColumn: record_id} + revisionIdentifier: {sourceColumn: record_revision} + lifecycleState: {sourceColumn: lifecycle_state, codelist: codelists/record-lifecycle.yaml} + recordedAt: {sourceColumn: recorded_at} + properties: + deceased: + {sourceColumn: deceased, type: boolean, sourceRequired: true, semanticTerm: local:deceased, label: Deceased, description: Whether CRA records the person as deceased, classification: {privacy: sensitive-personal}} + linkedUin: + {sourceColumn: uin, type: string, sourceRequired: true, semanticTerm: local:linkedUin, label: Linked UIN, description: The queried UIN exists in the CRA civil-person register, classification: {privacy: identifying}} + birthDateInternal: + {sourceColumn: birth_date, type: date, sourceRequired: true, semanticTerm: local:birthDateInternal, label: Internal birth date, description: Accounted source column excluded from every disclosure profile, classification: {privacy: sensitive-personal}} + birthRegistrationNumberInternal: + {sourceColumn: birth_brn, type: string, sourceRequired: false, semanticTerm: local:birthRegistrationNumberInternal, label: Internal birth registration number, description: Accounted source column excluded from every disclosure profile, classification: {privacy: identifying}} + disclosureProfiles: + death-fact: {properties: [deceased]} + citizen-link: {properties: [linkedUin]} + operations: + lookups: + - id: death-by-uin + requestBody: + maximumBytes: 128 + selectors: + uin: {sourceColumn: uin, type: string, minimumBytes: 10, maximumBytes: 10} + defaultAccessProfile: death-evidence + accessProfiles: + death-evidence: + access: + scope: solmara:relay:cra:death-by-uin + purpose: {claim: purpose, allowed: ["https://id.registrystack.org/solmara/purpose/pension-payment-review"]} + disclosureProfile: death-fact + - id: citizen-link-by-uin + requestBody: + maximumBytes: 96 + selectors: + uin: {sourceColumn: uin, type: string, minimumBytes: 10, maximumBytes: 10} + defaultAccessProfile: citizen-evidence + accessProfiles: + citizen-evidence: + access: + scope: solmara:relay:cra:citizen-link-by-uin + purpose: {claim: purpose, allowed: ["https://id.registrystack.org/solmara/purpose/citizen-self-service"]} + disclosureProfile: citizen-link + processingDescriptions: + - {id: death-consultation, operationRefs: [lookup:death-by-uin], purpose: "https://id.registrystack.org/solmara/purpose/pension-payment-review", recipientClass: authorized-evidence-service, legalBasisRef: governance/legal-basis.yaml, dpvProfileRef: governance/processing.dpv.yaml, safeguards: [exact-lookup, boolean-status-disclosure, minimum-disclosure, value-free-audit]} + - {id: citizen-link-consultation, operationRefs: [lookup:citizen-link-by-uin], purpose: "https://id.registrystack.org/solmara/purpose/citizen-self-service", recipientClass: authorized-evidence-service, legalBasisRef: governance/legal-basis.yaml, dpvProfileRef: governance/processing.dpv.yaml, safeguards: [exact-lookup, minimum-disclosure, value-free-audit]} +metadataVisibility: {service: public, resources: operation-bound, semantics: operation-bound, classifications: operator-only, processing: operation-bound} diff --git a/relays/cra/runtime.yaml b/relays/cra/runtime.yaml new file mode 100644 index 0000000..1424629 --- /dev/null +++ b/relays/cra/runtime.yaml @@ -0,0 +1,15 @@ +apiVersion: relay.registrystack.org/v2alpha1 +kind: RelayRuntime +server: {bind: "0.0.0.0:8080"} +packagePath: /etc/relay/cra/package +sources: {cra: {path: /var/lib/relay/source/cra.sqlite}} +authentication: + issuer: + id: solmara-mint + discoveryUrl: https://mint.solmara.registrystack.org/.well-known/openid-configuration + audience: solmara-runtime + tokenTypes: [at+jwt] + algorithms: [ES256] +audit: {sink: /var/lib/relay/audit/audit.jsonl, integrityKeyRef: secret:env/SOLMARA_RELAY_AUDIT_KEY} +limits: {requestTimeoutMilliseconds: 1500, concurrentQueries: 8} +quotas: {requestsPerMinute: 120, burst: 20} diff --git a/relays/mosd/codelists/record-lifecycle.yaml b/relays/mosd/codelists/record-lifecycle.yaml new file mode 100644 index 0000000..f7c9343 --- /dev/null +++ b/relays/mosd/codelists/record-lifecycle.yaml @@ -0,0 +1,4 @@ +id: mosd-record-lifecycle +version: "1" +status: reviewed +values: [active] diff --git a/relays/mosd/expected-http.yaml b/relays/mosd/expected-http.yaml new file mode 100644 index 0000000..c65d8ac --- /dev/null +++ b/relays/mosd/expected-http.yaml @@ -0,0 +1,21 @@ +schemaVersion: relay.registrystack.org/http-journey/v1alpha1 +registry: https://id.registrystack.org/solmara/registry/mosd-programme-mis +authorizations: + ok: {principal: mosd-child-benefit-evidence, scopes: [solmara:relay:mosd:by-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/child-benefit-review"}} + wrong-scope: {principal: mosd-child-benefit-evidence, scopes: [solmara:relay:mosd:other], claims: {purpose: "https://id.registrystack.org/solmara/purpose/child-benefit-review"}} + wrong-purpose: {principal: mosd-child-benefit-evidence, scopes: [solmara:relay:mosd:by-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/citizen-self-service"}} +steps: + - id: success + authorizationFixture: ok + request: {method: POST, path: /v2/resources/beneficiary-enrolment/lookups/by-uin, body: {uin: "2300010248"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [duplicateEnrolment], domainDataValues: {duplicateEnrolment: false}, absentEverywhere: [duplicate_flag]} + - id: fields-minimum + authorizationFixture: ok + request: {method: POST, path: /v2/resources/beneficiary-enrolment/lookups/by-uin, query: {fields: duplicateEnrolment}, body: {uin: "2300010248"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [duplicateEnrolment]} + - {id: wrong-scope, authorizationFixture: wrong-scope, request: {method: POST, path: /v2/resources/beneficiary-enrolment/lookups/by-uin, body: {uin: "2300010248"}}, expect: {status: 404, code: resource.not_found}} + - {id: wrong-purpose, authorizationFixture: wrong-purpose, request: {method: POST, path: /v2/resources/beneficiary-enrolment/lookups/by-uin, body: {uin: "2300010248"}}, expect: {status: 403, code: consultation.denied}} + - {id: malformed-selector, authorizationFixture: ok, request: {method: POST, path: /v2/resources/beneficiary-enrolment/lookups/by-uin, body: {uin: short}}, expect: {status: 400, code: consultation.invalid_request}} + - {id: no-match, authorizationFixture: ok, request: {method: POST, path: /v2/resources/beneficiary-enrolment/lookups/by-uin, body: {uin: "2399999999"}}, expect: {status: 404, code: consultation.unresolved, equivalenceClass: unresolved}} + - {id: invalid-row, authorizationFixture: ok, request: {method: POST, path: /v2/resources/beneficiary-enrolment/lookups/by-uin, body: {uin: "2300999999"}}, expect: {status: 503, code: source.unavailable}} + - {id: no-list-route, authorizationFixture: ok, request: {method: GET, path: /v2/resources/beneficiary-enrolment/records}, expect: {status: 404, routeAbsent: true}} diff --git a/relays/mosd/fixture.sql b/relays/mosd/fixture.sql new file mode 100644 index 0000000..6ef33de --- /dev/null +++ b/relays/mosd/fixture.sql @@ -0,0 +1,21 @@ +PRAGMA user_version = 1; +CREATE TABLE beneficiary_enrolment_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + uin TEXT NOT NULL UNIQUE, + duplicate_flag INTEGER NOT NULL CHECK (duplicate_flag IN (0, 1)) + ) STRICT; +CREATE VIEW relay_beneficiary_enrolment AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + uin, duplicate_flag + FROM beneficiary_enrolment_source; +INSERT INTO beneficiary_enrolment_source VALUES +('MOSD-1','rev-1','active','2026-07-04T09:00:00Z','2300010248',0), +('MOSD-BAD','rev-2','active','invalid-date-time','2300999999',1); +PRAGMA optimize; +-- Keep the fixture schema identical to the non-STAT4 live Python publication. +PRAGMA writable_schema = ON; +DELETE FROM sqlite_schema WHERE name = 'sqlite_stat4'; +PRAGMA writable_schema = OFF; diff --git a/relays/mosd/governance/classification-review-rationale.md b/relays/mosd/governance/classification-review-rationale.md new file mode 100644 index 0000000..4b21a77 --- /dev/null +++ b/relays/mosd/governance/classification-review-rationale.md @@ -0,0 +1 @@ +MoSD reviewed the inventory. The exact lookup releases only the duplicate-enrolment predicate required by child-benefit evidence and never publishes the selector or unrelated programme information. diff --git a/relays/mosd/governance/classification-review.yaml b/relays/mosd/governance/classification-review.yaml new file mode 100644 index 0000000..5df9fd2 --- /dev/null +++ b/relays/mosd/governance/classification-review.yaml @@ -0,0 +1,9 @@ +apiVersion: relay.registrystack.org/classification-review/v1 +kind: ClassificationReview +registryIdentifier: https://id.registrystack.org/solmara/registry/mosd-programme-mis +classificationInventoryDigest: sha256:d29a17afa121721d11c569168dba9e4561c0ee802a10c2435076a1edba97b2e2 +method: manual +reviewer: did:web:id.registrystack.org:solmara:authority:mosd-programme-mis +reviewDate: 2026-08-12 +status: reviewed +rationaleRef: governance/classification-review-rationale.md diff --git a/relays/mosd/governance/identifier-lifecycle.yaml b/relays/mosd/governance/identifier-lifecycle.yaml new file mode 100644 index 0000000..8df1d6b --- /dev/null +++ b/relays/mosd/governance/identifier-lifecycle.yaml @@ -0,0 +1,2 @@ +status: reviewed +policy: MoSD enrolment identifiers are stable across revisions and never reassigned after retirement. diff --git a/relays/mosd/governance/legal-basis.yaml b/relays/mosd/governance/legal-basis.yaml new file mode 100644 index 0000000..13c3378 --- /dev/null +++ b/relays/mosd/governance/legal-basis.yaml @@ -0,0 +1,2 @@ +status: reviewed +legalBasis: Child-benefit administration law authorizes the duplicate-enrolment consultation. diff --git a/relays/mosd/governance/processing.dpv.yaml b/relays/mosd/governance/processing.dpv.yaml new file mode 100644 index 0000000..a7d3fa0 --- /dev/null +++ b/relays/mosd/governance/processing.dpv.yaml @@ -0,0 +1,2 @@ +status: reviewed +profile: https://w3id.org/dpv/2.3 diff --git a/relays/mosd/registry.yaml b/relays/mosd/registry.yaml new file mode 100644 index 0000000..5b30418 --- /dev/null +++ b/relays/mosd/registry.yaml @@ -0,0 +1,50 @@ +apiVersion: relay.registrystack.org/v2alpha1 +kind: RegistryContract +metadata: {id: solmara-mosd-relay, version: "2026-08-12", title: MoSD beneficiary enrolment consultation} +registry: + registryIdentifier: https://id.registrystack.org/solmara/registry/mosd-programme-mis + name: Ministry of Social Development Programme MIS Relay + authority: {identifier: "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", name: Ministry of Social Development Programme MIS} + authoritativeScope: Current beneficiary enrolment facts held by the MoSD Programme MIS + baseUri: https://mosd.gov.solmara.example/registry/ + identifierLifecyclePolicyRef: governance/identifier-lifecycle.yaml + alignmentTargets: [{name: govstack-digital-registries, version: 3.0.0-alpha.2, status: directional}] +governance: {controller: "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", publisher: "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis", auditOwner: "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis"} +semantics: {localVocabulary: https://id.registrystack.org/solmara/vocabulary/mosd/} +classifications: + privacy: {scheme: https://w3id.org/dpv, version: "2.3"} + institutional: {scheme: https://id.registrystack.org/solmara/classification/mosd, version: "1"} + handling: {scheme: https://id.registrystack.org/vocab/handling, version: "1"} + provenanceRef: governance/classification-review.yaml +sources: + mosd: {kind: sqlite, profile: live-read-only, expectedSchemaFingerprint: "sha256:c7d2a7b176222fe8784fcb74adca48ffb3260afbcd0489f227621b446873e833"} +resources: + - id: beneficiary-enrolment + title: Beneficiary enrolment + description: Minimum duplicate-enrolment fact for child-benefit review. + semanticClass: local:BeneficiaryEnrolment + source: {source: mosd, view: relay_beneficiary_enrolment} + classificationDefaults: {privacy: sensitive-personal, institutional: restricted, handling: restricted, status: reviewed} + sourceColumnClassifications: {record_id: {privacy: identifying}, record_revision: {privacy: non-personal}, lifecycle_state: {privacy: personal}, recorded_at: {privacy: personal-context}, uin: {privacy: identifying}, duplicate_flag: {privacy: sensitive-personal}} + recordContext: + recordIdentifier: {sourceColumn: record_id} + revisionIdentifier: {sourceColumn: record_revision} + lifecycleState: {sourceColumn: lifecycle_state, codelist: codelists/record-lifecycle.yaml} + recordedAt: {sourceColumn: recorded_at} + properties: + duplicateEnrolment: {sourceColumn: duplicate_flag, type: boolean, sourceRequired: true, semanticTerm: local:duplicateEnrolment, label: Duplicate enrolment, description: Whether the person has a duplicate programme enrolment, classification: {privacy: sensitive-personal}} + disclosureProfiles: {child-benefit-evidence: {properties: [duplicateEnrolment]}} + operations: + lookups: + - id: by-uin + requestBody: {maximumBytes: 96, selectors: {uin: {sourceColumn: uin, type: string, minimumBytes: 10, maximumBytes: 10}}} + defaultAccessProfile: child-benefit-evidence + accessProfiles: + child-benefit-evidence: + access: + scope: solmara:relay:mosd:by-uin + purpose: {claim: purpose, allowed: ["https://id.registrystack.org/solmara/purpose/child-benefit-review"]} + disclosureProfile: child-benefit-evidence + processingDescriptions: + - {id: child-benefit-review, operationRefs: [lookup:by-uin], purpose: "https://id.registrystack.org/solmara/purpose/child-benefit-review", recipientClass: authorized-evidence-service, legalBasisRef: governance/legal-basis.yaml, dpvProfileRef: governance/processing.dpv.yaml, safeguards: [exact-lookup, minimum-disclosure, value-free-audit]} +metadataVisibility: {service: public, resources: operation-bound, semantics: operation-bound, classifications: operator-only, processing: operation-bound} diff --git a/relays/mosd/runtime.yaml b/relays/mosd/runtime.yaml new file mode 100644 index 0000000..0ed85bd --- /dev/null +++ b/relays/mosd/runtime.yaml @@ -0,0 +1,10 @@ +apiVersion: relay.registrystack.org/v2alpha1 +kind: RelayRuntime +server: {bind: "0.0.0.0:8080"} +packagePath: /etc/relay/mosd/package +sources: {mosd: {path: /var/lib/relay/source/mosd.sqlite}} +authentication: + issuer: {id: solmara-mint, discoveryUrl: https://mint.solmara.registrystack.org/.well-known/openid-configuration, audience: solmara-runtime, tokenTypes: [at+jwt], algorithms: [ES256]} +audit: {sink: /var/lib/relay/audit/audit.jsonl, integrityKeyRef: secret:env/SOLMARA_RELAY_AUDIT_KEY} +limits: {requestTimeoutMilliseconds: 1500, concurrentQueries: 8} +quotas: {requestsPerMinute: 120, burst: 20} diff --git a/relays/nagdi/codelists/record-lifecycle.yaml b/relays/nagdi/codelists/record-lifecycle.yaml new file mode 100644 index 0000000..0f3540e --- /dev/null +++ b/relays/nagdi/codelists/record-lifecycle.yaml @@ -0,0 +1,4 @@ +id: nagdi-record-lifecycle +version: "1" +status: reviewed +values: [active] diff --git a/relays/nagdi/expected-http.yaml b/relays/nagdi/expected-http.yaml new file mode 100644 index 0000000..42bb1fc --- /dev/null +++ b/relays/nagdi/expected-http.yaml @@ -0,0 +1,33 @@ +schemaVersion: relay.registrystack.org/http-journey/v1alpha1 +registry: https://id.registrystack.org/solmara/registry/nagdi +authorizations: + voucher-ok: {principal: nagdi-voucher-evidence, scopes: [solmara:relay:nagdi:voucher-by-farmer-id], claims: {purpose: "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"}} + voucher-wrong-scope: {principal: nagdi-voucher-evidence, scopes: [solmara:relay:nagdi:movement-by-farmer-id], claims: {purpose: "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"}} + voucher-wrong-purpose: {principal: nagdi-voucher-evidence, scopes: [solmara:relay:nagdi:voucher-by-farmer-id], claims: {purpose: "https://id.registrystack.org/solmara/purpose/livestock-movement-control"}} + livestock-ok: {principal: nagdi-livestock-evidence, scopes: [solmara:relay:nagdi:movement-by-farmer-id], claims: {purpose: "https://id.registrystack.org/solmara/purpose/livestock-movement-control"}} + livestock-wrong-scope: {principal: nagdi-livestock-evidence, scopes: [solmara:relay:nagdi:voucher-by-farmer-id], claims: {purpose: "https://id.registrystack.org/solmara/purpose/livestock-movement-control"}} + livestock-wrong-purpose: {principal: nagdi-livestock-evidence, scopes: [solmara:relay:nagdi:movement-by-farmer-id], claims: {purpose: "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"}} +steps: + - id: voucher-success + authorizationFixture: voucher-ok + request: {method: POST, path: /v2/resources/farmer/lookups/voucher-by-farmer-id, body: {farmerId: FR-1001}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [farmerRegistered, dataUseAuthorized, activeSmallholderFarmer, activeFarmParcel, cropDeclaredForSeason, districtClimateRiskActive, voucherEntitlementCurrent, voucherNotRedeemed], domainDataValues: {farmerRegistered: true, voucherNotRedeemed: true}, absentEverywhere: [farmerId, farmer_id]} + - {id: voucher-fields-minimum, authorizationFixture: voucher-ok, request: {method: POST, path: /v2/resources/farmer/lookups/voucher-by-farmer-id, query: {fields: "farmerRegistered,voucherNotRedeemed"}, body: {farmerId: FR-1001}}, expect: {status: 200, registryCoreRequired: true, domainDataKeys: [farmerRegistered, voucherNotRedeemed], absentEverywhere: [dataUseAuthorized, activeSmallholderFarmer, activeFarmParcel, cropDeclaredForSeason, districtClimateRiskActive, voucherEntitlementCurrent]}} + - {id: voucher-wrong-scope, authorizationFixture: voucher-wrong-scope, request: {method: POST, path: /v2/resources/farmer/lookups/voucher-by-farmer-id, body: {farmerId: FR-1001}}, expect: {status: 404, code: resource.not_found}} + - {id: voucher-wrong-purpose, authorizationFixture: voucher-wrong-purpose, request: {method: POST, path: /v2/resources/farmer/lookups/voucher-by-farmer-id, body: {farmerId: FR-1001}}, expect: {status: 403, code: consultation.denied}} + - {id: voucher-malformed-selector, authorizationFixture: voucher-ok, request: {method: POST, path: /v2/resources/farmer/lookups/voucher-by-farmer-id, body: {farmerId: bad}}, expect: {status: 400, code: consultation.invalid_request}} + - {id: voucher-no-match, authorizationFixture: voucher-ok, request: {method: POST, path: /v2/resources/farmer/lookups/voucher-by-farmer-id, body: {farmerId: FR-NONE}}, expect: {status: 404, code: consultation.unresolved, equivalenceClass: unresolved}} + - {id: voucher-invalid-row, authorizationFixture: voucher-ok, request: {method: POST, path: /v2/resources/farmer/lookups/voucher-by-farmer-id, body: {farmerId: FR-BAD01}}, expect: {status: 503, code: source.unavailable}} + - {id: voucher-no-list, authorizationFixture: voucher-ok, request: {method: GET, path: /v2/resources/farmer/records}, expect: {status: 404, routeAbsent: true}} + - id: livestock-success + authorizationFixture: livestock-ok + request: {method: POST, path: /v2/resources/livestock-herd/lookups/movement-by-farmer-id, body: {farmerId: FR-1001}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [herdId, registeredHerd, herdVaccinationCurrent, originDistrictNotQuarantinedForSpecies, destinationDistrictOpen, noConflictingOpenMovementPermit], domainDataValues: {herdId: HERD-000001, registeredHerd: true}, absentEverywhere: [farmerId, farmer_id]} + - {id: livestock-fields-minimum, authorizationFixture: livestock-ok, request: {method: POST, path: /v2/resources/livestock-herd/lookups/movement-by-farmer-id, query: {fields: "herdId,registeredHerd"}, body: {farmerId: FR-1001}}, expect: {status: 200, registryCoreRequired: true, domainDataKeys: [herdId, registeredHerd], absentEverywhere: [herdVaccinationCurrent, originDistrictNotQuarantinedForSpecies, destinationDistrictOpen, noConflictingOpenMovementPermit]}} + - {id: livestock-wrong-scope, authorizationFixture: livestock-wrong-scope, request: {method: POST, path: /v2/resources/livestock-herd/lookups/movement-by-farmer-id, body: {farmerId: FR-1001}}, expect: {status: 404, code: resource.not_found}} + - {id: livestock-wrong-purpose, authorizationFixture: livestock-wrong-purpose, request: {method: POST, path: /v2/resources/livestock-herd/lookups/movement-by-farmer-id, body: {farmerId: FR-1001}}, expect: {status: 403, code: consultation.denied}} + - {id: livestock-malformed-selector, authorizationFixture: livestock-ok, request: {method: POST, path: /v2/resources/livestock-herd/lookups/movement-by-farmer-id, body: {farmerId: bad}}, expect: {status: 400, code: consultation.invalid_request}} + - {id: livestock-no-match, authorizationFixture: livestock-ok, request: {method: POST, path: /v2/resources/livestock-herd/lookups/movement-by-farmer-id, body: {farmerId: FR-NONE}}, expect: {status: 404, code: consultation.unresolved, equivalenceClass: unresolved}} + - {id: livestock-ambiguous, authorizationFixture: livestock-ok, request: {method: POST, path: /v2/resources/livestock-herd/lookups/movement-by-farmer-id, body: {farmerId: FR-AMB1}}, expect: {status: 404, code: consultation.unresolved, equivalenceClass: unresolved}} + - {id: livestock-invalid-row, authorizationFixture: livestock-ok, request: {method: POST, path: /v2/resources/livestock-herd/lookups/movement-by-farmer-id, body: {farmerId: FR-BAD01}}, expect: {status: 503, code: source.unavailable}} + - {id: livestock-no-list, authorizationFixture: livestock-ok, request: {method: GET, path: /v2/resources/livestock-herd/records}, expect: {status: 404, routeAbsent: true}} diff --git a/relays/nagdi/fixture.sql b/relays/nagdi/fixture.sql new file mode 100644 index 0000000..c2b2476 --- /dev/null +++ b/relays/nagdi/fixture.sql @@ -0,0 +1,57 @@ +PRAGMA user_version = 1; +CREATE TABLE farmer_voucher_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + farmer_id TEXT NOT NULL UNIQUE, + farmer_registered INTEGER NOT NULL CHECK (farmer_registered IN (0, 1)), + data_use_authorized INTEGER NOT NULL CHECK (data_use_authorized IN (0, 1)), + active_smallholder_farmer INTEGER NOT NULL CHECK (active_smallholder_farmer IN (0, 1)), + active_farm_parcel INTEGER NOT NULL CHECK (active_farm_parcel IN (0, 1)), + crop_declared_for_season INTEGER NOT NULL CHECK (crop_declared_for_season IN (0, 1)), + district_climate_risk_active INTEGER NOT NULL CHECK (district_climate_risk_active IN (0, 1)), + voucher_entitlement_current INTEGER NOT NULL CHECK (voucher_entitlement_current IN (0, 1)), + voucher_not_redeemed INTEGER NOT NULL CHECK (voucher_not_redeemed IN (0, 1)) + ) STRICT; +CREATE TABLE livestock_movement_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + herd_id TEXT NOT NULL UNIQUE, + farmer_id TEXT NOT NULL, + registered_herd INTEGER NOT NULL CHECK (registered_herd IN (0, 1)), + herd_vaccination_current INTEGER NOT NULL CHECK (herd_vaccination_current IN (0, 1)), + origin_district_not_quarantined_for_species INTEGER NOT NULL CHECK (origin_district_not_quarantined_for_species IN (0, 1)), + destination_district_open INTEGER NOT NULL CHECK (destination_district_open IN (0, 1)), + no_conflicting_open_movement_permit INTEGER NOT NULL CHECK (no_conflicting_open_movement_permit IN (0, 1)) + ) STRICT; +CREATE VIEW relay_farmer_voucher AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + farmer_id, farmer_registered, data_use_authorized, + active_smallholder_farmer, active_farm_parcel, + crop_declared_for_season, district_climate_risk_active, + voucher_entitlement_current, voucher_not_redeemed + FROM farmer_voucher_source; +CREATE VIEW relay_livestock_movement AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + herd_id, farmer_id, registered_herd, + herd_vaccination_current, + origin_district_not_quarantined_for_species, + destination_district_open, + no_conflicting_open_movement_permit + FROM livestock_movement_source; +INSERT INTO farmer_voucher_source VALUES +('NAGDI-V-1','rev-1','active','2026-07-04T09:00:00Z','FR-1001',1,1,1,1,1,1,1,1), +('NAGDI-V-BAD','rev-2','active','invalid-date-time','FR-BAD01',1,1,1,1,1,1,1,1); +INSERT INTO livestock_movement_source VALUES +('NAGDI-M-1','rev-1','active','2026-07-04T09:00:00Z','HERD-000001','FR-1001',1,1,1,1,1), +('NAGDI-M-A1','rev-2','active','2026-07-04T09:00:00Z','HERD-AMB-001','FR-AMB1',1,1,1,1,1), +('NAGDI-M-A2','rev-3','active','2026-07-04T09:00:00Z','HERD-AMB-002','FR-AMB1',1,1,1,1,1), +('NAGDI-M-BAD','rev-4','active','invalid-date-time','HERD-BAD-001','FR-BAD01',1,1,1,1,1); +PRAGMA optimize; +-- Keep the fixture schema identical to the non-STAT4 live Python publication. +PRAGMA writable_schema = ON; +DELETE FROM sqlite_schema WHERE name = 'sqlite_stat4'; +PRAGMA writable_schema = OFF; diff --git a/relays/nagdi/governance/classification-review-rationale.md b/relays/nagdi/governance/classification-review-rationale.md new file mode 100644 index 0000000..8dc8a03 --- /dev/null +++ b/relays/nagdi/governance/classification-review-rationale.md @@ -0,0 +1 @@ +NAgDI reviewed separate voucher and livestock movement disclosures. Each operation releases only the closed predicate set required for its evidence decision. The farmer selector is not a property, and neither operation can select the other operation's disclosure profile. diff --git a/relays/nagdi/governance/classification-review.yaml b/relays/nagdi/governance/classification-review.yaml new file mode 100644 index 0000000..5577d29 --- /dev/null +++ b/relays/nagdi/governance/classification-review.yaml @@ -0,0 +1,9 @@ +apiVersion: relay.registrystack.org/classification-review/v1 +kind: ClassificationReview +registryIdentifier: https://id.registrystack.org/solmara/registry/nagdi +classificationInventoryDigest: sha256:533adfdf56fec71ce8805cc3ee4b2e4337352888e8f674941c02b5200a682d14 +method: manual +reviewer: did:web:id.registrystack.org:solmara:authority:nagdi +reviewDate: 2026-08-12 +status: reviewed +rationaleRef: governance/classification-review-rationale.md diff --git a/relays/nagdi/governance/identifier-lifecycle.yaml b/relays/nagdi/governance/identifier-lifecycle.yaml new file mode 100644 index 0000000..3dc5292 --- /dev/null +++ b/relays/nagdi/governance/identifier-lifecycle.yaml @@ -0,0 +1,2 @@ +status: reviewed +policy: NAgDI voucher and herd identifiers are stable across revisions and never reassigned after retirement. diff --git a/relays/nagdi/governance/legal-basis.yaml b/relays/nagdi/governance/legal-basis.yaml new file mode 100644 index 0000000..af23037 --- /dev/null +++ b/relays/nagdi/governance/legal-basis.yaml @@ -0,0 +1,2 @@ +status: reviewed +legalBasis: Agricultural data law authorizes the two purpose-bound evidence consultations. diff --git a/relays/nagdi/governance/processing.dpv.yaml b/relays/nagdi/governance/processing.dpv.yaml new file mode 100644 index 0000000..a7d3fa0 --- /dev/null +++ b/relays/nagdi/governance/processing.dpv.yaml @@ -0,0 +1,2 @@ +status: reviewed +profile: https://w3id.org/dpv/2.3 diff --git a/relays/nagdi/registry.yaml b/relays/nagdi/registry.yaml new file mode 100644 index 0000000..47cfc2a --- /dev/null +++ b/relays/nagdi/registry.yaml @@ -0,0 +1,104 @@ +apiVersion: relay.registrystack.org/v2alpha1 +kind: RegistryContract +metadata: {id: solmara-nagdi-relay, version: "2026-08-12", title: NAgDI agriculture consultations} +registry: + registryIdentifier: https://id.registrystack.org/solmara/registry/nagdi + name: National Agricultural Data Institute Relay + authority: {identifier: "did:web:id.registrystack.org:solmara:authority:nagdi", name: National Agricultural Data Institute} + authoritativeScope: Current farmer voucher and livestock movement facts held by NAgDI + baseUri: https://nagdi.gov.solmara.example/registry/ + identifierLifecyclePolicyRef: governance/identifier-lifecycle.yaml + alignmentTargets: [{name: govstack-digital-registries, version: 3.0.0-alpha.2, status: directional}] +governance: {controller: "did:web:id.registrystack.org:solmara:authority:nagdi", publisher: "did:web:id.registrystack.org:solmara:authority:nagdi", auditOwner: "did:web:id.registrystack.org:solmara:authority:nagdi"} +semantics: {localVocabulary: https://id.registrystack.org/solmara/vocabulary/nagdi/} +classifications: + privacy: {scheme: https://w3id.org/dpv, version: "2.3"} + institutional: {scheme: https://id.registrystack.org/solmara/classification/nagdi, version: "1"} + handling: {scheme: https://id.registrystack.org/vocab/handling, version: "1"} + provenanceRef: governance/classification-review.yaml +sources: + nagdi: {kind: sqlite, profile: live-read-only, expectedSchemaFingerprint: "sha256:f97ad1dbc22fbf9911228ea361c90babfaf1f2b46865e09a756a52fcf3e2d538"} +resources: + - id: farmer + title: Farmer voucher eligibility + description: Minimum governed farmer voucher predicate facts. + semanticClass: local:FarmerVoucher + source: {source: nagdi, view: relay_farmer_voucher} + classificationDefaults: {privacy: sensitive-personal, institutional: restricted, handling: restricted, status: reviewed} + sourceColumnClassifications: + record_id: {privacy: identifying} + record_revision: {privacy: non-personal} + lifecycle_state: {privacy: personal} + recorded_at: {privacy: personal-context} + farmer_id: {privacy: identifying} + farmer_registered: {privacy: sensitive-personal} + data_use_authorized: {privacy: sensitive-personal} + active_smallholder_farmer: {privacy: sensitive-personal} + active_farm_parcel: {privacy: sensitive-personal} + crop_declared_for_season: {privacy: sensitive-personal} + district_climate_risk_active: {privacy: sensitive-personal} + voucher_entitlement_current: {privacy: sensitive-personal} + voucher_not_redeemed: {privacy: sensitive-personal} + recordContext: {recordIdentifier: {sourceColumn: record_id}, revisionIdentifier: {sourceColumn: record_revision}, lifecycleState: {sourceColumn: lifecycle_state, codelist: codelists/record-lifecycle.yaml}, recordedAt: {sourceColumn: recorded_at}} + properties: + farmerRegistered: {sourceColumn: farmer_registered, type: boolean, sourceRequired: true, semanticTerm: local:farmerRegistered, label: Farmer registered, description: Farmer registration predicate, classification: {privacy: sensitive-personal}} + dataUseAuthorized: {sourceColumn: data_use_authorized, type: boolean, sourceRequired: true, semanticTerm: local:dataUseAuthorized, label: Data use authorized, description: Data-use authorization predicate, classification: {privacy: sensitive-personal}} + activeSmallholderFarmer: {sourceColumn: active_smallholder_farmer, type: boolean, sourceRequired: true, semanticTerm: local:activeSmallholderFarmer, label: Active smallholder farmer, description: Active smallholder predicate, classification: {privacy: sensitive-personal}} + activeFarmParcel: {sourceColumn: active_farm_parcel, type: boolean, sourceRequired: true, semanticTerm: local:activeFarmParcel, label: Active farm parcel, description: Active parcel predicate, classification: {privacy: sensitive-personal}} + cropDeclaredForSeason: {sourceColumn: crop_declared_for_season, type: boolean, sourceRequired: true, semanticTerm: local:cropDeclaredForSeason, label: Crop declared for season, description: Seasonal crop declaration predicate, classification: {privacy: sensitive-personal}} + districtClimateRiskActive: {sourceColumn: district_climate_risk_active, type: boolean, sourceRequired: true, semanticTerm: local:districtClimateRiskActive, label: District climate risk active, description: Climate-risk predicate, classification: {privacy: sensitive-personal}} + voucherEntitlementCurrent: {sourceColumn: voucher_entitlement_current, type: boolean, sourceRequired: true, semanticTerm: local:voucherEntitlementCurrent, label: Voucher entitlement current, description: Current entitlement predicate, classification: {privacy: sensitive-personal}} + voucherNotRedeemed: {sourceColumn: voucher_not_redeemed, type: boolean, sourceRequired: true, semanticTerm: local:voucherNotRedeemed, label: Voucher not redeemed, description: Voucher redemption predicate, classification: {privacy: sensitive-personal}} + disclosureProfiles: + voucher-evidence: {properties: [farmerRegistered, dataUseAuthorized, activeSmallholderFarmer, activeFarmParcel, cropDeclaredForSeason, districtClimateRiskActive, voucherEntitlementCurrent, voucherNotRedeemed]} + operations: + lookups: + - id: voucher-by-farmer-id + requestBody: {maximumBytes: 96, selectors: {farmerId: {sourceColumn: farmer_id, type: string, minimumBytes: 7, maximumBytes: 32}}} + defaultAccessProfile: voucher-evidence + accessProfiles: + voucher-evidence: + access: {scope: solmara:relay:nagdi:voucher-by-farmer-id, purpose: {claim: purpose, allowed: ["https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"]}} + disclosureProfile: voucher-evidence + processingDescriptions: + - {id: voucher-review, operationRefs: [lookup:voucher-by-farmer-id], purpose: "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review", recipientClass: authorized-evidence-service, legalBasisRef: governance/legal-basis.yaml, dpvProfileRef: governance/processing.dpv.yaml, safeguards: [exact-lookup, minimum-disclosure, value-free-audit]} + - id: livestock-herd + title: Livestock herd movement + description: Minimum governed livestock movement predicate facts. + semanticClass: local:LivestockHerd + source: {source: nagdi, view: relay_livestock_movement} + classificationDefaults: {privacy: sensitive-personal, institutional: restricted, handling: restricted, status: reviewed} + sourceColumnClassifications: + record_id: {privacy: identifying} + record_revision: {privacy: non-personal} + lifecycle_state: {privacy: personal} + recorded_at: {privacy: personal-context} + herd_id: {privacy: identifying} + farmer_id: {privacy: identifying} + registered_herd: {privacy: sensitive-personal} + herd_vaccination_current: {privacy: sensitive-personal} + origin_district_not_quarantined_for_species: {privacy: sensitive-personal} + destination_district_open: {privacy: sensitive-personal} + no_conflicting_open_movement_permit: {privacy: sensitive-personal} + recordContext: {recordIdentifier: {sourceColumn: record_id}, revisionIdentifier: {sourceColumn: record_revision}, lifecycleState: {sourceColumn: lifecycle_state, codelist: codelists/record-lifecycle.yaml}, recordedAt: {sourceColumn: recorded_at}} + properties: + herdId: {sourceColumn: herd_id, type: string, sourceRequired: true, semanticTerm: local:herdId, label: Herd ID, description: Registered herd identifier, classification: {privacy: identifying}} + registeredHerd: {sourceColumn: registered_herd, type: boolean, sourceRequired: true, semanticTerm: local:registeredHerd, label: Registered herd, description: Herd registration predicate, classification: {privacy: sensitive-personal}} + herdVaccinationCurrent: {sourceColumn: herd_vaccination_current, type: boolean, sourceRequired: true, semanticTerm: local:herdVaccinationCurrent, label: Herd vaccination current, description: Vaccination predicate, classification: {privacy: sensitive-personal}} + originDistrictNotQuarantinedForSpecies: {sourceColumn: origin_district_not_quarantined_for_species, type: boolean, sourceRequired: true, semanticTerm: local:originDistrictNotQuarantinedForSpecies, label: Origin not quarantined, description: Species quarantine predicate, classification: {privacy: sensitive-personal}} + destinationDistrictOpen: {sourceColumn: destination_district_open, type: boolean, sourceRequired: true, semanticTerm: local:destinationDistrictOpen, label: Destination district open, description: Destination predicate, classification: {privacy: sensitive-personal}} + noConflictingOpenMovementPermit: {sourceColumn: no_conflicting_open_movement_permit, type: boolean, sourceRequired: true, semanticTerm: local:noConflictingOpenMovementPermit, label: No conflicting permit, description: Conflicting permit predicate, classification: {privacy: sensitive-personal}} + disclosureProfiles: + livestock-evidence: {properties: [herdId, registeredHerd, herdVaccinationCurrent, originDistrictNotQuarantinedForSpecies, destinationDistrictOpen, noConflictingOpenMovementPermit]} + operations: + lookups: + - id: movement-by-farmer-id + requestBody: {maximumBytes: 96, selectors: {farmerId: {sourceColumn: farmer_id, type: string, minimumBytes: 7, maximumBytes: 32}}} + defaultAccessProfile: livestock-evidence + accessProfiles: + livestock-evidence: + access: {scope: solmara:relay:nagdi:movement-by-farmer-id, purpose: {claim: purpose, allowed: ["https://id.registrystack.org/solmara/purpose/livestock-movement-control"]}} + disclosureProfile: livestock-evidence + processingDescriptions: + - {id: livestock-movement-review, operationRefs: [lookup:movement-by-farmer-id], purpose: "https://id.registrystack.org/solmara/purpose/livestock-movement-control", recipientClass: authorized-evidence-service, legalBasisRef: governance/legal-basis.yaml, dpvProfileRef: governance/processing.dpv.yaml, safeguards: [exact-lookup, minimum-disclosure, value-free-audit]} +metadataVisibility: {service: public, resources: operation-bound, semantics: operation-bound, classifications: operator-only, processing: operation-bound} diff --git a/relays/nagdi/runtime.yaml b/relays/nagdi/runtime.yaml new file mode 100644 index 0000000..ab83f0d --- /dev/null +++ b/relays/nagdi/runtime.yaml @@ -0,0 +1,11 @@ +apiVersion: relay.registrystack.org/v2alpha1 +kind: RelayRuntime +server: {bind: "0.0.0.0:8080"} +packagePath: /etc/relay/nagdi/package +sources: {nagdi: {path: /var/lib/relay/source/nagdi.sqlite}} +authentication: + issuer: {id: solmara-mint, discoveryUrl: https://mint.solmara.registrystack.org/.well-known/openid-configuration, audience: solmara-runtime, tokenTypes: [at+jwt], algorithms: [ES256]} +audit: {sink: /var/lib/relay/audit/audit.jsonl, integrityKeyRef: secret:env/SOLMARA_RELAY_AUDIT_KEY} +cursor: {integrityKeyRef: secret:env/SOLMARA_RELAY_CURSOR_KEY, maximumAgeSeconds: 300} +limits: {requestTimeoutMilliseconds: 1500, concurrentQueries: 8} +quotas: {requestsPerMinute: 120, burst: 20} diff --git a/relays/nia/codelists/identity-status.yaml b/relays/nia/codelists/identity-status.yaml new file mode 100644 index 0000000..679cafd --- /dev/null +++ b/relays/nia/codelists/identity-status.yaml @@ -0,0 +1,4 @@ +id: nia-identity-status +version: "1" +status: reviewed +values: [active, deceased] diff --git a/relays/nia/codelists/record-lifecycle.yaml b/relays/nia/codelists/record-lifecycle.yaml new file mode 100644 index 0000000..1234470 --- /dev/null +++ b/relays/nia/codelists/record-lifecycle.yaml @@ -0,0 +1,4 @@ +id: nia-record-lifecycle +version: "1" +status: reviewed +values: [active, deceased] diff --git a/relays/nia/codelists/sex.yaml b/relays/nia/codelists/sex.yaml new file mode 100644 index 0000000..4a272e6 --- /dev/null +++ b/relays/nia/codelists/sex.yaml @@ -0,0 +1,4 @@ +id: nia-sex +version: "1" +status: reviewed +values: [female, male] diff --git a/relays/nia/expected-http.yaml b/relays/nia/expected-http.yaml new file mode 100644 index 0000000..bb01a29 --- /dev/null +++ b/relays/nia/expected-http.yaml @@ -0,0 +1,21 @@ +schemaVersion: relay.registrystack.org/http-journey/v1alpha1 +registry: https://id.registrystack.org/solmara/registry/nia +authorizations: + ok: {principal: nia-esignet, scopes: [solmara:relay:nia:esignet-userinfo], claims: {purpose: "https://id.registrystack.org/solmara/purpose/esignet-identity-verification"}} + wrong-scope: {principal: nia-esignet, scopes: [solmara:relay:nia:other], claims: {purpose: "https://id.registrystack.org/solmara/purpose/esignet-identity-verification"}} + wrong-purpose: {principal: nia-esignet, scopes: [solmara:relay:nia:esignet-userinfo], claims: {purpose: "https://id.registrystack.org/solmara/purpose/citizen-self-service"}} +steps: + - id: userinfo-success + authorizationFixture: ok + request: {method: POST, path: /v2/resources/population-person/lookups/esignet-userinfo, query: {fields: "individualId,givenName,familyName"}, body: {uin: "2300010248"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [individualId, givenName, familyName], domainDataValues: {individualId: "2300010248", givenName: Mateo, familyName: Santos}, absentEverywhere: [gender, birthdate, legacyIdentifierInternal, identityStatusInternal, legacy_nid, identity_status]} + - id: userinfo-fields-minimum + authorizationFixture: ok + request: {method: POST, path: /v2/resources/population-person/lookups/esignet-userinfo, query: {fields: individualId}, body: {uin: "2300010248"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [individualId], absentEverywhere: [givenName, familyName, gender, birthdate]} + - {id: wrong-scope, authorizationFixture: wrong-scope, request: {method: POST, path: /v2/resources/population-person/lookups/esignet-userinfo, query: {fields: individualId}, body: {uin: "2300010248"}}, expect: {status: 404, code: resource.not_found}} + - {id: wrong-purpose, authorizationFixture: wrong-purpose, request: {method: POST, path: /v2/resources/population-person/lookups/esignet-userinfo, query: {fields: individualId}, body: {uin: "2300010248"}}, expect: {status: 403, code: consultation.denied}} + - {id: malformed-selector, authorizationFixture: ok, request: {method: POST, path: /v2/resources/population-person/lookups/esignet-userinfo, query: {fields: individualId}, body: {uin: short}}, expect: {status: 400, code: consultation.invalid_request}} + - {id: no-match, authorizationFixture: ok, request: {method: POST, path: /v2/resources/population-person/lookups/esignet-userinfo, query: {fields: individualId}, body: {uin: "2399999999"}}, expect: {status: 404, code: consultation.unresolved, equivalenceClass: unresolved}} + - {id: invalid-row, authorizationFixture: ok, request: {method: POST, path: /v2/resources/population-person/lookups/esignet-userinfo, query: {fields: "individualId,birthdate"}, body: {uin: "2300999999"}}, expect: {status: 503, code: source.unavailable}} + - {id: no-list-route, authorizationFixture: ok, request: {method: GET, path: /v2/resources/population-person/records}, expect: {status: 404, routeAbsent: true}} diff --git a/relays/nia/fixture.sql b/relays/nia/fixture.sql new file mode 100644 index 0000000..6dd40bf --- /dev/null +++ b/relays/nia/fixture.sql @@ -0,0 +1,28 @@ +PRAGMA user_version = 1; +CREATE TABLE population_person_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + uin TEXT NOT NULL UNIQUE, + legacy_nid TEXT, + given_name TEXT NOT NULL, + family_name TEXT NOT NULL, + sex TEXT NOT NULL, + birth_date TEXT NOT NULL, + identity_status TEXT NOT NULL, + alive INTEGER NOT NULL CHECK (alive IN (0, 1)) + ) STRICT; +CREATE VIEW relay_population_person AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + uin, legacy_nid, given_name, family_name, sex, birth_date, + identity_status, alive + FROM population_person_source; +INSERT INTO population_person_source VALUES +('CP-1','rev-1','active','2026-07-04T09:00:00Z','2300010248','NID-1001','Mateo','Santos','male','2022-03-14','active',1), -- legacy_nid migration fixture +('CP-BAD','rev-2','active','2026-07-04T09:00:00Z','2300999999',NULL,'Bad','Row','unknown','not-a-date','active',1); +PRAGMA optimize; +-- Keep the fixture schema identical to the non-STAT4 live Python publication. +PRAGMA writable_schema = ON; +DELETE FROM sqlite_schema WHERE name = 'sqlite_stat4'; +PRAGMA writable_schema = OFF; diff --git a/relays/nia/governance/classification-review-rationale.md b/relays/nia/governance/classification-review-rationale.md new file mode 100644 index 0000000..03fb505 --- /dev/null +++ b/relays/nia/governance/classification-review-rationale.md @@ -0,0 +1 @@ +NIA reviewed the eSignet disclosure inventory. Every UserInfo property is restricted and identifying or sensitive personal data. The eSignet caller must project the user-consented subset through `fields`; legacy identifiers and identity-status source columns are not disclosable properties. diff --git a/relays/nia/governance/classification-review.yaml b/relays/nia/governance/classification-review.yaml new file mode 100644 index 0000000..6c208ea --- /dev/null +++ b/relays/nia/governance/classification-review.yaml @@ -0,0 +1,9 @@ +apiVersion: relay.registrystack.org/classification-review/v1 +kind: ClassificationReview +registryIdentifier: https://id.registrystack.org/solmara/registry/nia +classificationInventoryDigest: sha256:4337e9e1d8a3deed0bd3c30d8a4a79867a77d0c820beba0ffecf92225f6b460f +method: manual +reviewer: did:web:id.registrystack.org:solmara:authority:nia +reviewDate: 2026-08-12 +status: reviewed +rationaleRef: governance/classification-review-rationale.md diff --git a/relays/nia/governance/identifier-lifecycle.yaml b/relays/nia/governance/identifier-lifecycle.yaml new file mode 100644 index 0000000..76e64dc --- /dev/null +++ b/relays/nia/governance/identifier-lifecycle.yaml @@ -0,0 +1,2 @@ +status: reviewed +policy: NIA person record identifiers are stable across revisions and never reassigned after retirement. diff --git a/relays/nia/governance/legal-basis.yaml b/relays/nia/governance/legal-basis.yaml new file mode 100644 index 0000000..1057599 --- /dev/null +++ b/relays/nia/governance/legal-basis.yaml @@ -0,0 +1,2 @@ +status: reviewed +legalBasis: The identity law and explicit user consent authorize the eSignet UserInfo consultation. diff --git a/relays/nia/governance/processing.dpv.yaml b/relays/nia/governance/processing.dpv.yaml new file mode 100644 index 0000000..a7d3fa0 --- /dev/null +++ b/relays/nia/governance/processing.dpv.yaml @@ -0,0 +1,2 @@ +status: reviewed +profile: https://w3id.org/dpv/2.3 diff --git a/relays/nia/registry.yaml b/relays/nia/registry.yaml new file mode 100644 index 0000000..10fef48 --- /dev/null +++ b/relays/nia/registry.yaml @@ -0,0 +1,72 @@ +apiVersion: relay.registrystack.org/v2alpha1 +kind: RegistryContract +metadata: {id: solmara-nia-relay, version: "2026-08-12", title: NIA eSignet UserInfo consultation} +registry: + registryIdentifier: https://id.registrystack.org/solmara/registry/nia + name: National Identity Agency Relay + authority: {identifier: "did:web:id.registrystack.org:solmara:authority:nia", name: National Identity Agency} + authoritativeScope: Current population identity attributes held by NIA for consented eSignet UserInfo release + baseUri: https://nia.gov.solmara.example/registry/ + identifierLifecyclePolicyRef: governance/identifier-lifecycle.yaml + alignmentTargets: [{name: govstack-digital-registries, version: 3.0.0-alpha.2, status: directional}] +governance: {controller: "did:web:id.registrystack.org:solmara:authority:nia", publisher: "did:web:id.registrystack.org:solmara:authority:nia", auditOwner: "did:web:id.registrystack.org:solmara:authority:nia"} +semantics: {localVocabulary: https://id.registrystack.org/solmara/vocabulary/nia/} +classifications: + privacy: {scheme: https://w3id.org/dpv, version: "2.3"} + institutional: {scheme: https://id.registrystack.org/solmara/classification/nia, version: "1"} + handling: {scheme: https://id.registrystack.org/vocab/handling, version: "1"} + provenanceRef: governance/classification-review.yaml +sources: + nia: {kind: sqlite, profile: live-read-only, expectedSchemaFingerprint: "sha256:d91bc765476032de74670b1c4fce0c3d4f6c56f2b0116b36457fe5feb6027758"} +resources: + - id: population-person + title: Population person + description: Consent-projected eSignet UserInfo properties from NIA. + semanticClass: local:PopulationPerson + source: {source: nia, view: relay_population_person} + classificationDefaults: {privacy: sensitive-personal, institutional: restricted, handling: restricted, status: reviewed} + sourceColumnClassifications: + record_id: {privacy: identifying} + record_revision: {privacy: non-personal} + lifecycle_state: {privacy: personal} + recorded_at: {privacy: personal-context} + uin: {privacy: identifying} + legacy_nid: {privacy: identifying} + given_name: {privacy: identifying} + family_name: {privacy: identifying} + sex: {privacy: sensitive-personal} + birth_date: {privacy: sensitive-personal} + identity_status: {privacy: sensitive-personal} + alive: {privacy: sensitive-personal} + recordContext: + recordIdentifier: {sourceColumn: record_id} + revisionIdentifier: {sourceColumn: record_revision} + lifecycleState: {sourceColumn: lifecycle_state, codelist: codelists/record-lifecycle.yaml} + recordedAt: {sourceColumn: recorded_at} + properties: + individualId: {sourceColumn: uin, type: string, sourceRequired: true, semanticTerm: local:individualId, label: Individual ID, description: NIA subject identifier, classification: {privacy: identifying}} + givenName: {sourceColumn: given_name, type: string, sourceRequired: true, semanticTerm: local:givenName, label: Given name, description: Given name, classification: {privacy: identifying}} + familyName: {sourceColumn: family_name, type: string, sourceRequired: true, semanticTerm: local:familyName, label: Family name, description: Family name, classification: {privacy: identifying}} + gender: {sourceColumn: sex, type: controlled-code, codelist: codelists/sex.yaml, sourceRequired: true, semanticTerm: local:gender, label: Gender, description: Consented gender claim, classification: {privacy: sensitive-personal}} + birthdate: {sourceColumn: birth_date, type: date, sourceRequired: true, semanticTerm: local:birthdate, label: Birthdate, description: Consented date of birth claim, classification: {privacy: sensitive-personal}} + active: {sourceColumn: alive, type: boolean, sourceRequired: true, semanticTerm: local:active, label: Active, description: Whether the identity belongs to a living person, classification: {privacy: sensitive-personal}} + legacyIdentifierInternal: {sourceColumn: legacy_nid, type: string, sourceRequired: false, semanticTerm: local:legacyIdentifierInternal, label: Internal legacy identifier, description: Accounted source column excluded from the eSignet disclosure, classification: {privacy: identifying}} + identityStatusInternal: {sourceColumn: identity_status, type: controlled-code, codelist: codelists/identity-status.yaml, sourceRequired: true, semanticTerm: local:identityStatusInternal, label: Internal identity status, description: Accounted source column excluded from the eSignet disclosure, classification: {privacy: sensitive-personal}} + disclosureProfiles: + esignet-userinfo: {properties: [individualId, givenName, familyName, gender, birthdate, active]} + operations: + lookups: + - id: esignet-userinfo + requestBody: + maximumBytes: 96 + selectors: {uin: {sourceColumn: uin, type: string, minimumBytes: 10, maximumBytes: 10}} + defaultAccessProfile: esignet + accessProfiles: + esignet: + access: + scope: solmara:relay:nia:esignet-userinfo + purpose: {claim: purpose, allowed: ["https://id.registrystack.org/solmara/purpose/esignet-identity-verification"]} + disclosureProfile: esignet-userinfo + processingDescriptions: + - {id: esignet-userinfo, operationRefs: [lookup:esignet-userinfo], purpose: "https://id.registrystack.org/solmara/purpose/esignet-identity-verification", recipientClass: esignet, legalBasisRef: governance/legal-basis.yaml, dpvProfileRef: governance/processing.dpv.yaml, safeguards: [explicit-fields-projection, exact-lookup, minimum-disclosure, value-free-audit]} +metadataVisibility: {service: public, resources: operation-bound, semantics: operation-bound, classifications: operator-only, processing: operation-bound} diff --git a/relays/nia/runtime.yaml b/relays/nia/runtime.yaml new file mode 100644 index 0000000..99b9257 --- /dev/null +++ b/relays/nia/runtime.yaml @@ -0,0 +1,10 @@ +apiVersion: relay.registrystack.org/v2alpha1 +kind: RelayRuntime +server: {bind: "0.0.0.0:8080"} +packagePath: /etc/relay/nia/package +sources: {nia: {path: /var/lib/relay/source/nia.sqlite}} +authentication: + issuer: {id: solmara-mint, discoveryUrl: https://mint.solmara.registrystack.org/.well-known/openid-configuration, audience: solmara-runtime, tokenTypes: [at+jwt], algorithms: [ES256]} +audit: {sink: /var/lib/relay/audit/audit.jsonl, integrityKeyRef: secret:env/SOLMARA_RELAY_AUDIT_KEY} +limits: {requestTimeoutMilliseconds: 1500, concurrentQueries: 8} +quotas: {requestsPerMinute: 120, burst: 20} diff --git a/relays/sipf/codelists/payment-status.yaml b/relays/sipf/codelists/payment-status.yaml new file mode 100644 index 0000000..a4d1d83 --- /dev/null +++ b/relays/sipf/codelists/payment-status.yaml @@ -0,0 +1,4 @@ +id: sipf-payment-status +version: "1" +status: reviewed +values: [active, held] diff --git a/relays/sipf/codelists/record-lifecycle.yaml b/relays/sipf/codelists/record-lifecycle.yaml new file mode 100644 index 0000000..7482ec3 --- /dev/null +++ b/relays/sipf/codelists/record-lifecycle.yaml @@ -0,0 +1,4 @@ +id: sipf-record-lifecycle +version: "1" +status: reviewed +values: [active, in_payment, pending_proof_of_life] diff --git a/relays/sipf/expected-http.yaml b/relays/sipf/expected-http.yaml new file mode 100644 index 0000000..78ebc9e --- /dev/null +++ b/relays/sipf/expected-http.yaml @@ -0,0 +1,32 @@ +schemaVersion: relay.registrystack.org/http-journey/v1alpha1 +registry: https://id.registrystack.org/solmara/registry/sipf +authorizations: + pension-ok: {principal: sipf-pension-evidence, scopes: [solmara:relay:sipf:by-pensioner-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/pension-payment-review"}} + pension-wrong-scope: {principal: sipf-pension-evidence, scopes: [solmara:relay:sipf:by-spouse-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/pension-payment-review"}} + pension-wrong-purpose: {principal: sipf-pension-evidence, scopes: [solmara:relay:sipf:by-pensioner-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"}} + survivor-ok: {principal: sipf-survivor-evidence, scopes: [solmara:relay:sipf:by-spouse-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"}} + survivor-wrong-scope: {principal: sipf-survivor-evidence, scopes: [solmara:relay:sipf:by-pensioner-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"}} + survivor-wrong-purpose: {principal: sipf-survivor-evidence, scopes: [solmara:relay:sipf:by-spouse-uin], claims: {purpose: "https://id.registrystack.org/solmara/purpose/pension-payment-review"}} +steps: + - id: pension-success + authorizationFixture: pension-ok + request: {method: POST, path: /v2/resources/pension-payment/lookups/by-pensioner-uin, body: {pensionerUin: "2300109568"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [paymentStatus], domainDataValues: {paymentStatus: active}, absentEverywhere: [pensionerUin, pensioner_uin]} + - {id: pension-fields-minimum, authorizationFixture: pension-ok, request: {method: POST, path: /v2/resources/pension-payment/lookups/by-pensioner-uin, query: {fields: paymentStatus}, body: {pensionerUin: "2300109568"}}, expect: {status: 200, registryCoreRequired: true, domainDataKeys: [paymentStatus]}} + - {id: pension-wrong-scope, authorizationFixture: pension-wrong-scope, request: {method: POST, path: /v2/resources/pension-payment/lookups/by-pensioner-uin, body: {pensionerUin: "2300109568"}}, expect: {status: 404, code: resource.not_found}} + - {id: pension-wrong-purpose, authorizationFixture: pension-wrong-purpose, request: {method: POST, path: /v2/resources/pension-payment/lookups/by-pensioner-uin, body: {pensionerUin: "2300109568"}}, expect: {status: 403, code: consultation.denied}} + - {id: pension-malformed-selector, authorizationFixture: pension-ok, request: {method: POST, path: /v2/resources/pension-payment/lookups/by-pensioner-uin, body: {pensionerUin: short}}, expect: {status: 400, code: consultation.invalid_request}} + - {id: pension-no-match, authorizationFixture: pension-ok, request: {method: POST, path: /v2/resources/pension-payment/lookups/by-pensioner-uin, body: {pensionerUin: "2399999999"}}, expect: {status: 404, code: consultation.unresolved, equivalenceClass: unresolved}} + - {id: pension-invalid-row, authorizationFixture: pension-ok, request: {method: POST, path: /v2/resources/pension-payment/lookups/by-pensioner-uin, body: {pensionerUin: "2300999999"}}, expect: {status: 503, code: source.unavailable}} + - {id: pension-no-list, authorizationFixture: pension-ok, request: {method: GET, path: /v2/resources/pension-payment/records}, expect: {status: 404, routeAbsent: true}} + - id: survivor-success + authorizationFixture: survivor-ok + request: {method: POST, path: /v2/resources/survivor-case/lookups/by-spouse-uin, body: {spouseUin: "2300118698"}} + expect: {status: 200, registryCoreRequired: true, domainDataKeys: [survivorEligible], domainDataValues: {survivorEligible: true}, absentEverywhere: [spouseUin, spouse_uin]} + - {id: survivor-fields-minimum, authorizationFixture: survivor-ok, request: {method: POST, path: /v2/resources/survivor-case/lookups/by-spouse-uin, query: {fields: survivorEligible}, body: {spouseUin: "2300118698"}}, expect: {status: 200, registryCoreRequired: true, domainDataKeys: [survivorEligible]}} + - {id: survivor-wrong-scope, authorizationFixture: survivor-wrong-scope, request: {method: POST, path: /v2/resources/survivor-case/lookups/by-spouse-uin, body: {spouseUin: "2300118698"}}, expect: {status: 404, code: resource.not_found}} + - {id: survivor-wrong-purpose, authorizationFixture: survivor-wrong-purpose, request: {method: POST, path: /v2/resources/survivor-case/lookups/by-spouse-uin, body: {spouseUin: "2300118698"}}, expect: {status: 403, code: consultation.denied}} + - {id: survivor-malformed-selector, authorizationFixture: survivor-ok, request: {method: POST, path: /v2/resources/survivor-case/lookups/by-spouse-uin, body: {spouseUin: short}}, expect: {status: 400, code: consultation.invalid_request}} + - {id: survivor-no-match, authorizationFixture: survivor-ok, request: {method: POST, path: /v2/resources/survivor-case/lookups/by-spouse-uin, body: {spouseUin: "2399999999"}}, expect: {status: 404, code: consultation.unresolved, equivalenceClass: unresolved}} + - {id: survivor-invalid-row, authorizationFixture: survivor-ok, request: {method: POST, path: /v2/resources/survivor-case/lookups/by-spouse-uin, body: {spouseUin: "2300888888"}}, expect: {status: 503, code: source.unavailable}} + - {id: survivor-no-list, authorizationFixture: survivor-ok, request: {method: GET, path: /v2/resources/survivor-case/records}, expect: {status: 404, routeAbsent: true}} diff --git a/relays/sipf/fixture.sql b/relays/sipf/fixture.sql new file mode 100644 index 0000000..b66097f --- /dev/null +++ b/relays/sipf/fixture.sql @@ -0,0 +1,36 @@ +PRAGMA user_version = 1; +CREATE TABLE pension_case_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + pensioner_uin TEXT NOT NULL UNIQUE, + payment_status TEXT NOT NULL + ) STRICT; +CREATE TABLE survivor_case_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + spouse_uin TEXT NOT NULL UNIQUE, + survivor_eligible INTEGER NOT NULL CHECK (survivor_eligible IN (0, 1)) + ) STRICT; +CREATE VIEW relay_pension_payment AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + pensioner_uin, payment_status + FROM pension_case_source; +CREATE VIEW relay_survivor_case AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + spouse_uin, survivor_eligible + FROM survivor_case_source; +INSERT INTO pension_case_source VALUES +('SIPF-P-1','rev-1','in_payment','2026-07-04T09:00:00Z','2300109568','active'), +('SIPF-P-BAD','rev-2','in_payment','invalid-date-time','2300999999','active'); +INSERT INTO survivor_case_source VALUES +('SIPF-S-1','rev-1','active','2026-07-04T09:00:00Z','2300118698',1), +('SIPF-S-BAD','rev-2','active','invalid-date-time','2300888888',0); +PRAGMA optimize; +-- Keep the fixture schema identical to the non-STAT4 live Python publication. +PRAGMA writable_schema = ON; +DELETE FROM sqlite_schema WHERE name = 'sqlite_stat4'; +PRAGMA writable_schema = OFF; diff --git a/relays/sipf/governance/classification-review-rationale.md b/relays/sipf/governance/classification-review-rationale.md new file mode 100644 index 0000000..ffc70a8 --- /dev/null +++ b/relays/sipf/governance/classification-review-rationale.md @@ -0,0 +1 @@ +SIPF reviewed the two independent inventories. Pension consultation releases only current payment status; survivor consultation releases only the survivor-eligibility predicate. The UIN selectors and unrelated pension or family data are not properties. diff --git a/relays/sipf/governance/classification-review.yaml b/relays/sipf/governance/classification-review.yaml new file mode 100644 index 0000000..a6a088d --- /dev/null +++ b/relays/sipf/governance/classification-review.yaml @@ -0,0 +1,9 @@ +apiVersion: relay.registrystack.org/classification-review/v1 +kind: ClassificationReview +registryIdentifier: https://id.registrystack.org/solmara/registry/sipf +classificationInventoryDigest: sha256:08882398c5f65f3ccea706759a246e4eee5d4639f984af416a4f2a1b4eeaccd2 +method: manual +reviewer: did:web:id.registrystack.org:solmara:authority:sipf +reviewDate: 2026-08-12 +status: reviewed +rationaleRef: governance/classification-review-rationale.md diff --git a/relays/sipf/governance/identifier-lifecycle.yaml b/relays/sipf/governance/identifier-lifecycle.yaml new file mode 100644 index 0000000..70eadf4 --- /dev/null +++ b/relays/sipf/governance/identifier-lifecycle.yaml @@ -0,0 +1,2 @@ +status: reviewed +policy: SIPF case identifiers are stable across revisions and never reassigned after retirement. diff --git a/relays/sipf/governance/legal-basis.yaml b/relays/sipf/governance/legal-basis.yaml new file mode 100644 index 0000000..3d36c66 --- /dev/null +++ b/relays/sipf/governance/legal-basis.yaml @@ -0,0 +1,2 @@ +status: reviewed +legalBasis: Social insurance law authorizes purpose-bound pension and survivor consultations. diff --git a/relays/sipf/governance/processing.dpv.yaml b/relays/sipf/governance/processing.dpv.yaml new file mode 100644 index 0000000..a7d3fa0 --- /dev/null +++ b/relays/sipf/governance/processing.dpv.yaml @@ -0,0 +1,2 @@ +status: reviewed +profile: https://w3id.org/dpv/2.3 diff --git a/relays/sipf/registry.yaml b/relays/sipf/registry.yaml new file mode 100644 index 0000000..705e28c --- /dev/null +++ b/relays/sipf/registry.yaml @@ -0,0 +1,66 @@ +apiVersion: relay.registrystack.org/v2alpha1 +kind: RegistryContract +metadata: {id: solmara-sipf-relay, version: "2026-08-12", title: SIPF pension consultations} +registry: + registryIdentifier: https://id.registrystack.org/solmara/registry/sipf + name: Social Insurance and Pensions Fund Relay + authority: {identifier: "did:web:id.registrystack.org:solmara:authority:sipf", name: Social Insurance and Pensions Fund} + authoritativeScope: Current pension payment and survivor eligibility facts held by SIPF + baseUri: https://sipf.gov.solmara.example/registry/ + identifierLifecyclePolicyRef: governance/identifier-lifecycle.yaml + alignmentTargets: [{name: govstack-digital-registries, version: 3.0.0-alpha.2, status: directional}] +governance: {controller: "did:web:id.registrystack.org:solmara:authority:sipf", publisher: "did:web:id.registrystack.org:solmara:authority:sipf", auditOwner: "did:web:id.registrystack.org:solmara:authority:sipf"} +semantics: {localVocabulary: https://id.registrystack.org/solmara/vocabulary/sipf/} +classifications: + privacy: {scheme: https://w3id.org/dpv, version: "2.3"} + institutional: {scheme: https://id.registrystack.org/solmara/classification/sipf, version: "1"} + handling: {scheme: https://id.registrystack.org/vocab/handling, version: "1"} + provenanceRef: governance/classification-review.yaml +sources: + sipf: {kind: sqlite, profile: live-read-only, expectedSchemaFingerprint: "sha256:4298f8a9efcca5c1a2f58942f4a947afa3dd51e8767f54d1c557c60fb466a350"} +resources: + - id: pension-payment + title: Pension payment + description: Minimum pension payment status fact. + semanticClass: local:PensionPayment + source: {source: sipf, view: relay_pension_payment} + classificationDefaults: {privacy: sensitive-personal, institutional: restricted, handling: restricted, status: reviewed} + sourceColumnClassifications: {record_id: {privacy: identifying}, record_revision: {privacy: non-personal}, lifecycle_state: {privacy: personal}, recorded_at: {privacy: personal-context}, pensioner_uin: {privacy: identifying}, payment_status: {privacy: sensitive-personal}} + recordContext: {recordIdentifier: {sourceColumn: record_id}, revisionIdentifier: {sourceColumn: record_revision}, lifecycleState: {sourceColumn: lifecycle_state, codelist: codelists/record-lifecycle.yaml}, recordedAt: {sourceColumn: recorded_at}} + properties: + paymentStatus: {sourceColumn: payment_status, type: controlled-code, codelist: codelists/payment-status.yaml, sourceRequired: true, semanticTerm: local:paymentStatus, label: Payment status, description: Current pension payment status, classification: {privacy: sensitive-personal}} + disclosureProfiles: {pension-evidence: {properties: [paymentStatus]}} + operations: + lookups: + - id: by-pensioner-uin + requestBody: {maximumBytes: 96, selectors: {pensionerUin: {sourceColumn: pensioner_uin, type: string, minimumBytes: 10, maximumBytes: 10}}} + defaultAccessProfile: pension-evidence + accessProfiles: + pension-evidence: + access: {scope: solmara:relay:sipf:by-pensioner-uin, purpose: {claim: purpose, allowed: ["https://id.registrystack.org/solmara/purpose/pension-payment-review"]}} + disclosureProfile: pension-evidence + processingDescriptions: + - {id: pension-payment-review, operationRefs: [lookup:by-pensioner-uin], purpose: "https://id.registrystack.org/solmara/purpose/pension-payment-review", recipientClass: authorized-evidence-service, legalBasisRef: governance/legal-basis.yaml, dpvProfileRef: governance/processing.dpv.yaml, safeguards: [exact-lookup, minimum-disclosure, value-free-audit]} + - id: survivor-case + title: Survivor case + description: Minimum survivor eligibility fact. + semanticClass: local:SurvivorCase + source: {source: sipf, view: relay_survivor_case} + classificationDefaults: {privacy: sensitive-personal, institutional: restricted, handling: restricted, status: reviewed} + sourceColumnClassifications: {record_id: {privacy: identifying}, record_revision: {privacy: non-personal}, lifecycle_state: {privacy: personal}, recorded_at: {privacy: personal-context}, spouse_uin: {privacy: identifying}, survivor_eligible: {privacy: sensitive-personal}} + recordContext: {recordIdentifier: {sourceColumn: record_id}, revisionIdentifier: {sourceColumn: record_revision}, lifecycleState: {sourceColumn: lifecycle_state, codelist: codelists/record-lifecycle.yaml}, recordedAt: {sourceColumn: recorded_at}} + properties: + survivorEligible: {sourceColumn: survivor_eligible, type: boolean, sourceRequired: true, semanticTerm: local:survivorEligible, label: Survivor eligible, description: Current survivor benefit eligibility, classification: {privacy: sensitive-personal}} + disclosureProfiles: {survivor-evidence: {properties: [survivorEligible]}} + operations: + lookups: + - id: by-spouse-uin + requestBody: {maximumBytes: 96, selectors: {spouseUin: {sourceColumn: spouse_uin, type: string, minimumBytes: 10, maximumBytes: 10}}} + defaultAccessProfile: survivor-evidence + accessProfiles: + survivor-evidence: + access: {scope: solmara:relay:sipf:by-spouse-uin, purpose: {claim: purpose, allowed: ["https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"]}} + disclosureProfile: survivor-evidence + processingDescriptions: + - {id: survivor-benefit-review, operationRefs: [lookup:by-spouse-uin], purpose: "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination", recipientClass: authorized-evidence-service, legalBasisRef: governance/legal-basis.yaml, dpvProfileRef: governance/processing.dpv.yaml, safeguards: [exact-lookup, minimum-disclosure, value-free-audit]} +metadataVisibility: {service: public, resources: operation-bound, semantics: operation-bound, classifications: operator-only, processing: operation-bound} diff --git a/relays/sipf/runtime.yaml b/relays/sipf/runtime.yaml new file mode 100644 index 0000000..d0cd8d3 --- /dev/null +++ b/relays/sipf/runtime.yaml @@ -0,0 +1,11 @@ +apiVersion: relay.registrystack.org/v2alpha1 +kind: RelayRuntime +server: {bind: "0.0.0.0:8080"} +packagePath: /etc/relay/sipf/package +sources: {sipf: {path: /var/lib/relay/source/sipf.sqlite}} +authentication: + issuer: {id: solmara-mint, discoveryUrl: https://mint.solmara.registrystack.org/.well-known/openid-configuration, audience: solmara-runtime, tokenTypes: [at+jwt], algorithms: [ES256]} +audit: {sink: /var/lib/relay/audit/audit.jsonl, integrityKeyRef: secret:env/SOLMARA_RELAY_AUDIT_KEY} +cursor: {integrityKeyRef: secret:env/SOLMARA_RELAY_CURSOR_KEY, maximumAgeSeconds: 300} +limits: {requestTimeoutMilliseconds: 1500, concurrentQueries: 8} +quotas: {requestsPerMinute: 120, burst: 20} diff --git a/relays/test_relay_projects.py b/relays/test_relay_projects.py new file mode 100644 index 0000000..af92a97 --- /dev/null +++ b/relays/test_relay_projects.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import json +import unittest +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parent + +EXPECTED = { + "cra": { + "civil-person": { + "death-by-uin": ("cra-pension-evidence", "solmara:relay:cra:death-by-uin"), + "citizen-link-by-uin": ("cra-citizen-evidence", "solmara:relay:cra:citizen-link-by-uin"), + } + }, + "nia": {"population-person": {"esignet-userinfo": ("nia-esignet", "solmara:relay:nia:esignet-userinfo")}}, + "mosd": {"beneficiary-enrolment": {"by-uin": ("mosd-child-benefit-evidence", "solmara:relay:mosd:by-uin")}}, + "sipf": { + "pension-payment": {"by-pensioner-uin": ("sipf-pension-evidence", "solmara:relay:sipf:by-pensioner-uin")}, + "survivor-case": {"by-spouse-uin": ("sipf-survivor-evidence", "solmara:relay:sipf:by-spouse-uin")}, + }, + "nagdi": { + "farmer": {"voucher-by-farmer-id": ("nagdi-voucher-evidence", "solmara:relay:nagdi:voucher-by-farmer-id")}, + "livestock-herd": {"movement-by-farmer-id": ("nagdi-livestock-evidence", "solmara:relay:nagdi:movement-by-farmer-id")}, + }, +} + + +def load(path: Path) -> dict: + return yaml.safe_load(path.read_text()) + + +def lookup_map(contract: dict) -> dict[tuple[str, str], dict]: + result = {} + for resource in contract["resources"]: + operations = resource["operations"] + if operations.get("list") or operations.get("read") or operations.get("searches"): + raise AssertionError(f"{resource['id']} declares a forbidden enumerating operation") + for lookup in operations.get("lookups", []): + result[(resource["id"], lookup["id"])] = lookup + return result + + +class RelayProjectContracts(unittest.TestCase): + def test_exact_authority_owned_topology_and_operation_inventory(self) -> None: + self.assertEqual(set(EXPECTED), {path.name for path in ROOT.iterdir() if path.is_dir() and not path.name.startswith("__")}) + for authority, resources in EXPECTED.items(): + contract = load(ROOT / authority / "registry.yaml") + actual = lookup_map(contract) + expected = {(resource, lookup) for resource, lookups in resources.items() for lookup in lookups} + self.assertEqual(set(actual), expected) + for resource in contract["resources"]: + lookups = resource["operations"]["lookups"] + self.assertEqual(len(resource["disclosureProfiles"]), len(lookups)) + for lookup in lookups: + profiles = lookup["accessProfiles"] + self.assertEqual(len(profiles), 1) + self.assertIn(lookup["defaultAccessProfile"], profiles) + + def test_scopes_and_authorization_principals_are_operation_specific(self) -> None: + for authority, resources in EXPECTED.items(): + contract = load(ROOT / authority / "registry.yaml") + journey = load(ROOT / authority / "expected-http.yaml") + actual = lookup_map(contract) + for resource, lookups in resources.items(): + for lookup_id, (principal, scope) in lookups.items(): + lookup = actual[(resource, lookup_id)] + profile = next(iter(lookup["accessProfiles"].values())) + self.assertEqual(profile["access"]["scope"], scope) + matching = [fixture for fixture in journey["authorizations"].values() if fixture["principal"] == principal and scope in fixture["scopes"]] + self.assertTrue(matching, f"{authority}/{lookup_id} lacks its dedicated principal fixture") + + def test_runtime_is_container_bound_shared_mint_and_fail_closed_audit(self) -> None: + for authority in EXPECTED: + runtime = load(ROOT / authority / "runtime.yaml") + self.assertEqual(runtime["server"]["bind"], "0.0.0.0:8080") + self.assertEqual(runtime["packagePath"], f"/etc/relay/{authority}/package") + self.assertEqual(runtime["sources"][authority]["path"], f"/var/lib/relay/source/{authority}.sqlite") + issuer = runtime["authentication"]["issuer"] + self.assertEqual( + issuer["discoveryUrl"], + "https://mint.solmara.registrystack.org/.well-known/openid-configuration", + ) + self.assertEqual(issuer["audience"], "solmara-runtime") + self.assertEqual(issuer["algorithms"], ["ES256"]) + self.assertEqual(runtime["audit"]["sink"], "/var/lib/relay/audit/audit.jsonl") + self.assertTrue(runtime["audit"]["integrityKeyRef"].startswith("secret:env/")) + + def test_fixture_shorthand_maps_to_real_nested_selector_body(self) -> None: + for authority in EXPECTED: + journey = load(ROOT / authority / "expected-http.yaml") + for step in journey["steps"]: + request = step["request"] + if request["method"] != "POST": + continue + shorthand = request["body"] + self.assertNotIn("selectors", shorthand) + wire = json.loads(json.dumps({"selectors": shorthand}, separators=(",", ":"))) + self.assertEqual(wire, {"selectors": shorthand}) + + def test_each_lookup_has_behavioral_boundary_coverage(self) -> None: + for authority, resources in EXPECTED.items(): + steps = [step["id"] for step in load(ROOT / authority / "expected-http.yaml")["steps"]] + fixture_sql = (ROOT / authority / "fixture.sql").read_text() + lookup_count = sum(len(lookups) for lookups in resources.values()) + for lookups in resources.values(): + for lookup_id in lookups: + prefix = { + "death-by-uin": "death-", + "citizen-link-by-uin": "citizen-", + "esignet-userinfo": "userinfo-", + "by-uin": "", + "by-pensioner-uin": "pension-", + "by-spouse-uin": "survivor-", + "voucher-by-farmer-id": "voucher-", + "movement-by-farmer-id": "livestock-", + }[lookup_id] + for suffix in ("success", "fields-minimum", "wrong-scope", "wrong-purpose", "malformed-selector", "no-match", "invalid-row"): + self.assertTrue( + f"{prefix}{suffix}" in steps or (lookup_count == 1 and suffix in steps), + f"{authority}/{lookup_id} lacks {suffix}", + ) + self.assertTrue(any("no-list" in step for step in steps)) + # The publisher's UNIQUE selectors make ambiguity structurally impossible for + # every lookup except NAgDI livestock, whose two-row case is exercised over HTTP. + if authority == "nagdi": + self.assertIn("livestock-ambiguous", steps) + self.assertIn("UNIQUE", fixture_sql) + + def test_classification_reviews_bind_real_inventory_digests(self) -> None: + zero = "sha256:" + "0" * 64 + for authority in EXPECTED: + review = load(ROOT / authority / "governance" / "classification-review.yaml") + self.assertEqual(review["method"], "manual") + self.assertEqual(review["status"], "reviewed") + self.assertNotEqual(review["classificationInventoryDigest"], zero) + + def test_cra_birth_fields_are_never_disclosed(self) -> None: + contract = load(ROOT / "cra" / "registry.yaml") + resource = contract["resources"][0] + disclosed = {name for profile in resource["disclosureProfiles"].values() for name in profile["properties"]} + self.assertTrue({"birthDateInternal", "birthRegistrationNumberInternal"}.isdisjoint(disclosed)) + self.assertEqual(resource["disclosureProfiles"]["death-fact"]["properties"], ["deceased"]) + death = next(item for item in resource["operations"]["lookups"] if item["id"] == "death-by-uin") + self.assertEqual(set(death["requestBody"]["selectors"]), {"uin"}) + steps = {step["id"]: step for step in load(ROOT / "cra" / "expected-http.yaml")["steps"]} + self.assertEqual(steps["death-success"]["expect"]["domainDataValues"], {"deceased": True}) + self.assertEqual(steps["death-living-success"]["expect"]["domainDataValues"], {"deceased": False}) + + +if __name__ == "__main__": + unittest.main() diff --git a/requests/registry-lab/20 - Child Benefit/01 - Collect source predicates.bru b/requests/registry-lab/20 - Child Benefit/01 - Collect source predicates.bru index 70c4c35..5dfb415 100644 --- a/requests/registry-lab/20 - Child Benefit/01 - Collect source predicates.bru +++ b/requests/registry-lab/20 - Child Benefit/01 - Collect source predicates.bru @@ -12,12 +12,12 @@ post { headers { x-api-key: {{CHILD_BENEFIT_FEDERATOR_TOKEN}} - Data-Purpose: {{child_benefit_purpose}} Accept: application/json } body:json { { + "purpose": "child-benefit-review", "target": { "type": "Person", "identifiers": [ diff --git a/requests/registry-lab/30 - Pension Survivor/01 - Evaluate pension stop.bru b/requests/registry-lab/30 - Pension Survivor/01 - Evaluate pension stop.bru index 3b905b8..86ab2d1 100644 --- a/requests/registry-lab/30 - Pension Survivor/01 - Evaluate pension stop.bru +++ b/requests/registry-lab/30 - Pension Survivor/01 - Evaluate pension stop.bru @@ -1,36 +1,31 @@ meta { - name: 01 - Read CRA death predicate + name: 01 - Request CRA death assertion type: http seq: 1 } post { - url: {{cra_notary_url}}/v1/evaluations + url: {{cra_evidence_url}}/v1/evidence body: json auth: none } headers { - x-api-key: {{CRA_PENSION_CLIENT_TOKEN}} - Data-Purpose: {{pension_payment_purpose}} - Accept: application/vnd.registry-notary.claim-result+json + Authorization: Bearer {{EVIDENCE_ACCESS_TOKEN}} + Accept: application/jose+json } body:json { { - "target": { - "type": "Person", - "identifiers": [ - { - "scheme": "solmara_uin", - "value": "2300109568" - } - ] - }, - "claims": [ - "person-is-deceased" - ], - "disclosure": "predicate", - "format": "application/vnd.registry-notary.claim-result+json" + "requestNonce": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "requirement": "https://id.registrystack.org/solmara/requirement/cra-pension-death/v1", + "purpose": "pension-payment-review", + "subjects": [{ + "role": "subject", + "selector": { + "profile": "solmara-uin-v1", + "values": {"uin": "2300109568"} + } + }] } } diff --git a/requests/registry-lab/30 - Pension Survivor/02 - Read active pension payment.bru b/requests/registry-lab/30 - Pension Survivor/02 - Read active pension payment.bru index d6299fa..08484ef 100644 --- a/requests/registry-lab/30 - Pension Survivor/02 - Read active pension payment.bru +++ b/requests/registry-lab/30 - Pension Survivor/02 - Read active pension payment.bru @@ -1,36 +1,31 @@ meta { - name: 02 - Read SIPF payment predicate + name: 02 - Request SIPF payment assertion type: http seq: 2 } post { - url: {{sipf_notary_url}}/v1/evaluations + url: {{sipf_evidence_url}}/v1/evidence body: json auth: none } headers { - x-api-key: {{SIPF_PENSION_CLIENT_TOKEN}} - Data-Purpose: {{pension_payment_purpose}} - Accept: application/vnd.registry-notary.claim-result+json + Authorization: Bearer {{EVIDENCE_ACCESS_TOKEN}} + Accept: application/jose+json } body:json { { - "target": { - "type": "Person", - "identifiers": [ - { - "scheme": "solmara_uin", - "value": "2300109568" - } - ] - }, - "claims": [ - "pension-payment-active" - ], - "disclosure": "predicate", - "format": "application/vnd.registry-notary.claim-result+json" + "requestNonce": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "requirement": "https://id.registrystack.org/solmara/requirement/sipf-pension-payment/v1", + "purpose": "pension-payment-review", + "subjects": [{ + "role": "subject", + "selector": { + "profile": "solmara-uin-v1", + "values": {"uin": "2300109568"} + } + }] } } diff --git a/requests/registry-lab/30 - Pension Survivor/03 - Read survivor eligibility.bru b/requests/registry-lab/30 - Pension Survivor/03 - Read survivor eligibility.bru index af8a11a..7bc7c0e 100644 --- a/requests/registry-lab/30 - Pension Survivor/03 - Read survivor eligibility.bru +++ b/requests/registry-lab/30 - Pension Survivor/03 - Read survivor eligibility.bru @@ -1,36 +1,31 @@ meta { - name: 03 - Read SIPF survivor predicate + name: 03 - Request SIPF survivor assertion type: http seq: 3 } post { - url: {{sipf_notary_url}}/v1/evaluations + url: {{sipf_evidence_url}}/v1/evidence body: json auth: none } headers { - x-api-key: {{SIPF_PENSION_CLIENT_TOKEN}} - Data-Purpose: {{survivor_benefit_purpose}} - Accept: application/vnd.registry-notary.claim-result+json + Authorization: Bearer {{EVIDENCE_ACCESS_TOKEN}} + Accept: application/jose+json } body:json { { - "target": { - "type": "Person", - "identifiers": [ - { - "scheme": "solmara_uin", - "value": "2300118698" - } - ] - }, - "claims": [ - "survivor-is-eligible" - ], - "disclosure": "predicate", - "format": "application/vnd.registry-notary.claim-result+json" + "requestNonce": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", + "requirement": "https://id.registrystack.org/solmara/requirement/sipf-survivor-benefit/v1", + "purpose": "survivor-benefit-determination", + "subjects": [{ + "role": "subject", + "selector": { + "profile": "solmara-uin-v1", + "values": {"uin": "2300118698"} + } + }] } } diff --git a/requests/registry-lab/40 - NAgDI Voucher/01 - Voucher eligibility.bru b/requests/registry-lab/40 - NAgDI Voucher/01 - Voucher eligibility.bru index 667a514..b4937ae 100644 --- a/requests/registry-lab/40 - NAgDI Voucher/01 - Voucher eligibility.bru +++ b/requests/registry-lab/40 - NAgDI Voucher/01 - Voucher eligibility.bru @@ -5,32 +5,27 @@ meta { } post { - url: {{nagdi_notary_url}}/v1/evaluations + url: {{nagdi_evidence_url}}/v1/evidence body: json auth: none } headers { - x-api-key: {{NAGDI_NOTARY_TOKEN}} - Data-Purpose: {{voucher_purpose}} - Accept: application/vnd.registry-notary.claim-result+json + Authorization: Bearer {{EVIDENCE_ACCESS_TOKEN}} + Accept: application/jose+json } body:json { { - "target": { - "type": "Person", - "identifiers": [ - { - "scheme": "farmer_id", - "value": "FR-1001" - } - ] - }, - "claims": [ - "eligible-for-climate-smart-input-voucher" - ], - "disclosure": "predicate", - "format": "application/vnd.registry-notary.claim-result+json" + "requestNonce": "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", + "requirement": "https://id.registrystack.org/solmara/requirement/nagdi-voucher/v1", + "purpose": "voucher-eligibility-review", + "subjects": [{ + "role": "subject", + "selector": { + "profile": "farmer-reference-v1", + "values": {"farmer_id": "FR-1001"} + } + }] } } diff --git a/requests/registry-lab/40 - NAgDI Voucher/02 - Livestock movement control.bru b/requests/registry-lab/40 - NAgDI Voucher/02 - Livestock movement control.bru index 3319b6c..b5565df 100644 --- a/requests/registry-lab/40 - NAgDI Voucher/02 - Livestock movement control.bru +++ b/requests/registry-lab/40 - NAgDI Voucher/02 - Livestock movement control.bru @@ -5,32 +5,27 @@ meta { } post { - url: {{nagdi_notary_url}}/v1/evaluations + url: {{nagdi_evidence_url}}/v1/evidence body: json auth: none } headers { - x-api-key: {{NAGDI_NOTARY_TOKEN}} - Data-Purpose: {{livestock_purpose}} - Accept: application/vnd.registry-notary.claim-result+json + Authorization: Bearer {{EVIDENCE_ACCESS_TOKEN}} + Accept: application/jose+json } body:json { { - "target": { - "type": "Person", - "identifiers": [ - { - "scheme": "farmer_id", - "value": "FR-1001" - } - ] - }, - "claims": [ - "eligible-for-livestock-movement-permit" - ], - "disclosure": "predicate", - "format": "application/vnd.registry-notary.claim-result+json" + "requestNonce": "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", + "requirement": "https://id.registrystack.org/solmara/requirement/nagdi-livestock/v1", + "purpose": "livestock-movement-control", + "subjects": [{ + "role": "subject", + "selector": { + "profile": "farmer-reference-v1", + "values": {"farmer_id": "FR-1001"} + } + }] } } diff --git a/requests/registry-lab/50 - Relay V2/01 - CRA death lookup.bru b/requests/registry-lab/50 - Relay V2/01 - CRA death lookup.bru new file mode 100644 index 0000000..be902fd --- /dev/null +++ b/requests/registry-lab/50 - Relay V2/01 - CRA death lookup.bru @@ -0,0 +1,20 @@ +meta { + name: 01 - CRA death lookup + type: http + seq: 1 +} + +post { + url: {{cra_relay_url}}/v2/resources/civil-person/lookups/death-by-uin?fields=deceased + body: json + auth: none +} + +headers { + Authorization: Bearer {{CRA_PENSION_RELAY_TOKEN}} + Accept: application/json +} + +body:json { + {"selectors":{"uin":"2300109568"}} +} diff --git a/requests/registry-lab/50 - Relay V2/02 - NIA eSignet lookup.bru b/requests/registry-lab/50 - Relay V2/02 - NIA eSignet lookup.bru new file mode 100644 index 0000000..c2c5d0b --- /dev/null +++ b/requests/registry-lab/50 - Relay V2/02 - NIA eSignet lookup.bru @@ -0,0 +1,20 @@ +meta { + name: 02 - NIA eSignet lookup + type: http + seq: 2 +} + +post { + url: {{nia_relay_url}}/v2/resources/population-person/lookups/esignet-userinfo?fields=individualId,givenName,familyName,gender,birthdate,active + body: json + auth: none +} + +headers { + Authorization: Bearer {{NIA_ESIGNET_RELAY_TOKEN}} + Accept: application/json +} + +body:json { + {"selectors":{"uin":"2300010248"}} +} diff --git a/requests/registry-lab/50 - Relay V2/03 - MoSD enrolment lookup.bru b/requests/registry-lab/50 - Relay V2/03 - MoSD enrolment lookup.bru new file mode 100644 index 0000000..39e6c06 --- /dev/null +++ b/requests/registry-lab/50 - Relay V2/03 - MoSD enrolment lookup.bru @@ -0,0 +1,20 @@ +meta { + name: 03 - MoSD enrolment lookup + type: http + seq: 3 +} + +post { + url: {{mosd_relay_url}}/v2/resources/beneficiary-enrolment/lookups/by-uin?fields=duplicateEnrolment + body: json + auth: none +} + +headers { + Authorization: Bearer {{MOSD_CHILD_BENEFIT_RELAY_TOKEN}} + Accept: application/json +} + +body:json { + {"selectors":{"uin":"2300010248"}} +} diff --git a/requests/registry-lab/50 - Relay V2/04 - SIPF pension lookup.bru b/requests/registry-lab/50 - Relay V2/04 - SIPF pension lookup.bru new file mode 100644 index 0000000..0a6d09d --- /dev/null +++ b/requests/registry-lab/50 - Relay V2/04 - SIPF pension lookup.bru @@ -0,0 +1,20 @@ +meta { + name: 04 - SIPF pension lookup + type: http + seq: 4 +} + +post { + url: {{sipf_relay_url}}/v2/resources/pension-payment/lookups/by-pensioner-uin?fields=paymentStatus + body: json + auth: none +} + +headers { + Authorization: Bearer {{SIPF_PENSION_RELAY_TOKEN}} + Accept: application/json +} + +body:json { + {"selectors":{"pensionerUin":"2300109568"}} +} diff --git a/requests/registry-lab/50 - Relay V2/05 - NAgDI voucher lookup.bru b/requests/registry-lab/50 - Relay V2/05 - NAgDI voucher lookup.bru new file mode 100644 index 0000000..4eebc4b --- /dev/null +++ b/requests/registry-lab/50 - Relay V2/05 - NAgDI voucher lookup.bru @@ -0,0 +1,20 @@ +meta { + name: 05 - NAgDI voucher lookup + type: http + seq: 5 +} + +post { + url: {{nagdi_relay_url}}/v2/resources/farmer/lookups/voucher-by-farmer-id?fields=farmerRegistered,dataUseAuthorized,activeSmallholderFarmer,activeFarmParcel,cropDeclaredForSeason,districtClimateRiskActive,voucherEntitlementCurrent,voucherNotRedeemed + body: json + auth: none +} + +headers { + Authorization: Bearer {{NAGDI_VOUCHER_RELAY_TOKEN}} + Accept: application/json +} + +body:json { + {"selectors":{"farmerId":"FR-000001"}} +} diff --git a/requests/registry-lab/50 - Relay V2/06 - CRA citizen-link lookup.bru b/requests/registry-lab/50 - Relay V2/06 - CRA citizen-link lookup.bru new file mode 100644 index 0000000..f0bf1f5 --- /dev/null +++ b/requests/registry-lab/50 - Relay V2/06 - CRA citizen-link lookup.bru @@ -0,0 +1,20 @@ +meta { + name: 06 - CRA citizen-link lookup + type: http + seq: 6 +} + +post { + url: {{cra_relay_url}}/v2/resources/civil-person/lookups/citizen-link-by-uin?fields=linkedUin + body: json + auth: none +} + +headers { + Authorization: Bearer {{CRA_CITIZEN_RELAY_TOKEN}} + Accept: application/json +} + +body:json { + {"selectors":{"uin":"2300010248"}} +} diff --git a/requests/registry-lab/50 - Relay V2/07 - SIPF survivor lookup.bru b/requests/registry-lab/50 - Relay V2/07 - SIPF survivor lookup.bru new file mode 100644 index 0000000..42b8eb3 --- /dev/null +++ b/requests/registry-lab/50 - Relay V2/07 - SIPF survivor lookup.bru @@ -0,0 +1,20 @@ +meta { + name: 07 - SIPF survivor lookup + type: http + seq: 7 +} + +post { + url: {{sipf_relay_url}}/v2/resources/survivor-case/lookups/by-spouse-uin?fields=survivorEligible + body: json + auth: none +} + +headers { + Authorization: Bearer {{SIPF_SURVIVOR_RELAY_TOKEN}} + Accept: application/json +} + +body:json { + {"selectors":{"spouseUin":"2300118698"}} +} diff --git a/requests/registry-lab/50 - Relay V2/08 - NAgDI livestock lookup.bru b/requests/registry-lab/50 - Relay V2/08 - NAgDI livestock lookup.bru new file mode 100644 index 0000000..e24e1ec --- /dev/null +++ b/requests/registry-lab/50 - Relay V2/08 - NAgDI livestock lookup.bru @@ -0,0 +1,20 @@ +meta { + name: 08 - NAgDI livestock lookup + type: http + seq: 8 +} + +post { + url: {{nagdi_relay_url}}/v2/resources/livestock-herd/lookups/movement-by-farmer-id?fields=registeredHerd,herdVaccinationCurrent,originDistrictNotQuarantinedForSpecies,destinationDistrictOpen,noConflictingOpenMovementPermit + body: json + auth: none +} + +headers { + Authorization: Bearer {{NAGDI_LIVESTOCK_RELAY_TOKEN}} + Accept: application/json +} + +body:json { + {"selectors":{"farmerId":"FR-000002"}} +} diff --git a/requests/registry-lab/README.md b/requests/registry-lab/README.md index bb52fd3..9e4e65a 100644 --- a/requests/registry-lab/README.md +++ b/requests/registry-lab/README.md @@ -1,11 +1,27 @@ -# Solmara Lab API Workspace +# Solmara Lab API workspace -Wave 1 placeholders for the three Solmara stories: +The examples exercise the authority-owned reset: -1. Birth to child benefit. -2. Death to pension stop plus survivor benefit. -3. Farmer climate-smart voucher plus livestock movement control. +1. The child-benefit programme composes four independently signed assertions. +2. CRA and SIPF issue separate pension and survivor assertions. +3. NAgDI issues separate voucher and livestock assertions. +4. The Relay V2 folder demonstrates only the eight governed exact lookups used + by Evidence and the optional NIA eSignet profile. -Set the environment URLs and bearer tokens once compose or hosted deployment -generates the actual service endpoints. No partner-system or governed-ops -folders are part of this wave 1 surface. +Set `EVIDENCE_ACCESS_TOKEN` to a short-lived Registry Mint token for the +authority Evidence endpoints. Set `CHILD_BENEFIT_FEDERATOR_TOKEN` only when +calling the programme application. Purpose is part of each Evidence request or +programme request body. It is not an ambient HTTP header. + +Each Relay request uses a different placeholder token variable because Mint +fixes the scope and purpose claim per client. The example selectors identify +synthetic lab fixtures only. Relay responses remain no-store and disclose only +the explicitly requested governed fields. + +Every direct Evidence response is a flattened ES256 JWS. Verify it against the +JWKS of the authority host that received the request. The examples contain no +real-person selector, token value, source row, private audit data, or signing +key. + +The fixed request nonces are readable examples. Replace each one with 32 random +bytes encoded as unpadded base64url before using the collection outside the lab. diff --git a/requests/registry-lab/environments/Hosted Lab.bru b/requests/registry-lab/environments/Hosted Lab.bru index fa2b1ac..42df55b 100644 --- a/requests/registry-lab/environments/Hosted Lab.bru +++ b/requests/registry-lab/environments/Hosted Lab.bru @@ -1,16 +1,25 @@ vars { child_benefit_federator_url: https://child-benefit-federator.solmara.registrystack.org - cra_notary_url: https://cra-notary.solmara.registrystack.org - sipf_notary_url: https://sipf-notary.solmara.registrystack.org - nagdi_notary_url: https://nagdi-notary.solmara.registrystack.org + cra_evidence_url: https://cra-evidence.solmara.registrystack.org + nia_evidence_url: https://nia-evidence.solmara.registrystack.org + sro_evidence_url: https://sro-evidence.solmara.registrystack.org + mosd_programme_evidence_url: https://mosd-programme-evidence.solmara.registrystack.org + sipf_evidence_url: https://sipf-evidence.solmara.registrystack.org + nagdi_evidence_url: https://nagdi-evidence.solmara.registrystack.org + cra_relay_url: https://cra-relay.solmara.registrystack.org + nia_relay_url: https://nia-relay.solmara.registrystack.org + mosd_relay_url: https://mosd-programme-relay.solmara.registrystack.org + sipf_relay_url: https://sipf-relay.solmara.registrystack.org + nagdi_relay_url: https://nagdi-relay.solmara.registrystack.org metadata_url: https://metadata.solmara.registrystack.org - child_benefit_purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - pension_payment_purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review - survivor_benefit_purpose: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - voucher_purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - livestock_purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control CHILD_BENEFIT_FEDERATOR_TOKEN: - CRA_PENSION_CLIENT_TOKEN: - SIPF_PENSION_CLIENT_TOKEN: - NAGDI_NOTARY_TOKEN: + EVIDENCE_ACCESS_TOKEN: + CRA_PENSION_RELAY_TOKEN: + CRA_CITIZEN_RELAY_TOKEN: + NIA_ESIGNET_RELAY_TOKEN: + MOSD_CHILD_BENEFIT_RELAY_TOKEN: + SIPF_PENSION_RELAY_TOKEN: + SIPF_SURVIVOR_RELAY_TOKEN: + NAGDI_VOUCHER_RELAY_TOKEN: + NAGDI_LIVESTOCK_RELAY_TOKEN: } diff --git a/requests/registry-lab/environments/Local Compose.bru b/requests/registry-lab/environments/Local Compose.bru index e274201..b7a784f 100644 --- a/requests/registry-lab/environments/Local Compose.bru +++ b/requests/registry-lab/environments/Local Compose.bru @@ -1,16 +1,25 @@ vars { child_benefit_federator_url: http://127.0.0.1:4321 - cra_notary_url: http://127.0.0.1:4325 - sipf_notary_url: http://127.0.0.1:4322 - nagdi_notary_url: http://127.0.0.1:4323 + cra_evidence_url: https://localhost:4341/evidence/cra + nia_evidence_url: https://localhost:4341/evidence/nia + sro_evidence_url: https://localhost:4341/evidence/sro + mosd_programme_evidence_url: https://localhost:4341/evidence/mosd-programme + sipf_evidence_url: https://localhost:4341/evidence/sipf + nagdi_evidence_url: https://localhost:4341/evidence/nagdi + cra_relay_url: http://127.0.0.1:4311 + nia_relay_url: http://127.0.0.1:4312 + mosd_relay_url: http://127.0.0.1:4314 + sipf_relay_url: http://127.0.0.1:4315 + nagdi_relay_url: http://127.0.0.1:4316 metadata_url: http://127.0.0.1:4331 - child_benefit_purpose: https://id.registrystack.org/solmara/purpose/child-benefit-review - pension_payment_purpose: https://id.registrystack.org/solmara/purpose/pension-payment-review - survivor_benefit_purpose: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - voucher_purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - livestock_purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control CHILD_BENEFIT_FEDERATOR_TOKEN: - CRA_PENSION_CLIENT_TOKEN: - SIPF_PENSION_CLIENT_TOKEN: - NAGDI_NOTARY_TOKEN: + EVIDENCE_ACCESS_TOKEN: + CRA_PENSION_RELAY_TOKEN: + CRA_CITIZEN_RELAY_TOKEN: + NIA_ESIGNET_RELAY_TOKEN: + MOSD_CHILD_BENEFIT_RELAY_TOKEN: + SIPF_PENSION_RELAY_TOKEN: + SIPF_SURVIVOR_RELAY_TOKEN: + NAGDI_VOUCHER_RELAY_TOKEN: + NAGDI_LIVESTOCK_RELAY_TOKEN: } diff --git a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json deleted file mode 100644 index 1ec4b1f..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-civil-consultation.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:aa167d27cfe661509fb5b8e159dcf410ea4057cf223d8adfc33bcdefaf3e2818","id":"relay.cra-civil-consultation.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:cra-civil-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json deleted file mode 100644 index bfcbb9d..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-future-birth-date","variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-match","variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"cra-civil-person-no-match","variables":{"as_of_date":"2026-07-14"}}],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json deleted file mode 100644 index 9d92e3c..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-cra-civil-person-by-uin","operations":{},"outputs":{"birth_brn":{"from":"snapshot.record.birth_brn","max_bytes":128,"maximum":null,"minimum":null,"nullable":true,"type":"string"},"birth_date":{"from":"snapshot.record.birth_date","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"date"},"deceased":{"from":"snapshot.record.deceased","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json deleted file mode 100644 index 18ba42a..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json deleted file mode 100644 index e80b1e7..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:4348d96de834e21384de4e5d3f7e19ff10e299a7148c8d1df0739ee1d50199a3"],"minimization":["sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263"],"negative_security":["sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-cra-civil-person-by-uin","output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json b/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json deleted file mode 100644 index cb199e7..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:487bef77452bf86b610743eadc53f87429ef9ec66c06b9ee8d41e8a0e5ef6210","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-civil-consultation.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/cra-civil/relay-consultation/relay.yaml b/runtime/registry-projects/hosted/cra-civil/relay-consultation/relay.yaml deleted file mode 100644 index ce77c39..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay-consultation/relay.yaml +++ /dev/null @@ -1,175 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://cra-relay.solmara.registrystack.org - publisher: solmara-cra-civil - title: solmara-cra-civil governed Registry Relay -consultation: - artifacts: - evidence: - - class: negative_security - path: artifacts/evidence/civil-person-by-uin/negative-security.json - sha256: sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f - - class: conformance - path: artifacts/evidence/civil-person-by-uin/conformance.json - sha256: sha256:4348d96de834e21384de4e5d3f7e19ff10e299a7148c8d1df0739ee1d50199a3 - - class: minimization - path: artifacts/evidence/civil-person-by-uin/minimization.json - sha256: sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263 - integration_packs: - - hash: sha256:487bef77452bf86b610743eadc53f87429ef9ec66c06b9ee8d41e8a0e5ef6210 - path: artifacts/integration-packs/civil-person-by-uin.json - sha256: sha256:975ab5565da70354b1eccb3f2f5da123e69d4983c45bcf5955cf55df6d4123cf - private_bindings: - - hash: sha256:41e6b05846b019807b19590010bd812c5477d74228a55636b725548d21673bc4 - path: artifacts/private-bindings/cra-civil-consultation-civil.json - sha256: sha256:f5e2bd11c9c512a718bb0f82d8ba0d554b16f6da5ddf1d5f896bd536711824c5 - public_contracts: - - hash: sha256:7d81ecba1b399a2caa3f8b08f27b93626f2c8b2d51ec2305874a19af34cc852b - path: artifacts/consultation-contracts/cra-civil-consultation-civil.json - sha256: sha256:28effc0ce34a4c498ff7605cde0c59c59ecd199877bbd02fb73c49be31b73f48 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 5222333210205388 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 3104283851243516304 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited civil facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: civil_people:aggregate - evidence_verification_scope: civil_people:evidence_verification - metadata_scope: civil_people:metadata - read_scope: civil_people:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/pension-payment-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited civil facts used by the Solmara Evidence service. - fields: - - from: birth_brn - name: birth_brn - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: deceased - name: deceased - sensitive: true - - from: uin - name: uin - sensitive: true - name: civil_people - relationships: [] - spatial: null - table: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - title: CRA civil records - id: civil_people - owner: Civil Registration Authority - sensitivity: personal - tables: - - access: - aggregate_scope: civil_people:aggregate - metadata_scope: civil_people:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: birth_brn - nullable: true - sensitive: true - type: string - - name: birth_date - nullable: false - sensitive: true - type: date - - name: deceased - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv - type: file - title: CRA civil records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: cra-civil-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/cra-civil/relay/relay.yaml b/runtime/registry-projects/hosted/cra-civil/relay/relay.yaml deleted file mode 100644 index 24f67a0..0000000 --- a/runtime/registry-projects/hosted/cra-civil/relay/relay.yaml +++ /dev/null @@ -1,133 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-evidence - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://cra-relay.solmara.registrystack.org - publisher: solmara-cra-civil - title: solmara-cra-civil governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited civil facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: civil_people:aggregate - evidence_verification_scope: civil_people:evidence_verification - metadata_scope: civil_people:metadata - read_scope: civil_people:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/pension-payment-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited civil facts used by the Solmara Evidence service. - fields: - - from: birth_brn - name: birth_brn - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: deceased - name: deceased - sensitive: true - - from: uin - name: uin - sensitive: true - name: civil_people - relationships: [] - spatial: null - table: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - title: CRA civil records - id: civil_people - owner: Civil Registration Authority - sensitivity: personal - tables: - - access: - aggregate_scope: civil_people:aggregate - metadata_scope: civil_people:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: birth_brn - nullable: true - sensitive: true - type: string - - name: birth_date - nullable: false - sensitive: true - type: date - - name: deceased - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv - type: file - title: CRA civil records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: cra-civil-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json deleted file mode 100644 index c43521d..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-mosd-programme.mosd-enrollment-consultation.enrollment","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:e51568815beddf2e304dcc482daf67f7493e1cd23ff68847d22eb62c22cb6ba3","id":"relay.mosd-enrollment-consultation.enrollment","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:mosd-enrollment-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-mosd-programme.enrollment-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["duplicate_flag"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json deleted file mode 100644 index e91e1d3..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"duplicate_flag":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"duplicate_flag":false},"headers":{},"status":200}}}],"name":"enrollment-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"enrollment-no-match","variables":{}}],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json deleted file mode 100644 index 1d539e7..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"enrollment-by-uin","operations":{},"outputs":{"duplicate_flag":{"from":"snapshot.record.duplicate_flag","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json deleted file mode 100644 index 0b38f83..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json deleted file mode 100644 index 096dbe6..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-mosd-programme.enrollment-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:304b2cd16ce7d97127bfb0be3b60f7c25b2624ed25fbe0219bac09c067e6a1bb"],"minimization":["sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83"],"negative_security":["sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"enrollment-by-uin","output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json deleted file mode 100644 index a4bc8da..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:d82492e2f92fca8ce1d83208d892e2a18468ef6db6afa66d467df002336a6c92","id":"solmara-mosd-programme.enrollment-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"duplicate_flag":"duplicate_flag"}},"table_provider":"mosd_enroll__materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf"},"profile":{"id":"solmara-mosd-programme.mosd-enrollment-consultation.enrollment","version":"1"},"registry_instance":"solmara-mosd-programme","source_instance":"materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf","tenant":"solmara-mosd-programme"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/relay.yaml b/runtime/registry-projects/hosted/mosd-programme/relay-consultation/relay.yaml deleted file mode 100644 index 421266c..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/relay-consultation/relay.yaml +++ /dev/null @@ -1,159 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://mosd-programme-relay.solmara.registrystack.org - publisher: solmara-mosd-programme - title: solmara-mosd-programme governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/enrollment-by-uin/minimization.json - sha256: sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83 - - class: conformance - path: artifacts/evidence/enrollment-by-uin/conformance.json - sha256: sha256:304b2cd16ce7d97127bfb0be3b60f7c25b2624ed25fbe0219bac09c067e6a1bb - - class: negative_security - path: artifacts/evidence/enrollment-by-uin/negative-security.json - sha256: sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282 - integration_packs: - - hash: sha256:d82492e2f92fca8ce1d83208d892e2a18468ef6db6afa66d467df002336a6c92 - path: artifacts/integration-packs/enrollment-by-uin.json - sha256: sha256:3b4de6e7946d91a2af1e6f0cdbc3397e7c382e960ae829ec0ddbb030f8192c5d - private_bindings: - - hash: sha256:2d91cd9c15c0badbb565bd16a2be9f46ac77d0ad0c827a0eea895e20edf3a3a2 - path: artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json - sha256: sha256:f27344b5e2dec067577087b23dfd4cc031e5680f4871dc028222be875f81865a - public_contracts: - - hash: sha256:58e60de7fdae8b94a0cdf130f80c1daffb2622773aac4e60d3714f6547a7b2a5 - path: artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json - sha256: sha256:50bee564d51a566df7dcd634476a2bad80920ebfb0d88b535db85081c8558cab - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 9069287079765840165 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 891105660075992367 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited enrollment facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: mosd_enroll:aggregate - evidence_verification_scope: mosd_enroll:evidence_verification - metadata_scope: mosd_enroll:metadata - read_scope: mosd_enroll:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited enrollment facts used by the Solmara Evidence service. - fields: - - from: duplicate_flag - name: duplicate_flag - sensitive: true - - from: uin - name: uin - sensitive: true - name: mosd_enroll - relationships: [] - spatial: null - table: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - title: MOSD programme enrollment records - id: mosd_enroll - owner: Ministry of Social Development - sensitivity: personal - tables: - - access: - aggregate_scope: mosd_enroll:aggregate - metadata_scope: mosd_enroll:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: duplicate_flag - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv - type: file - title: MOSD programme enrollment records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: programme-mis-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/mosd-programme/relay/relay.yaml b/runtime/registry-projects/hosted/mosd-programme/relay/relay.yaml deleted file mode 100644 index 2cc24c5..0000000 --- a/runtime/registry-projects/hosted/mosd-programme/relay/relay.yaml +++ /dev/null @@ -1,117 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-evidence - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://mosd-programme-relay.solmara.registrystack.org - publisher: solmara-mosd-programme - title: solmara-mosd-programme governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited enrollment facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: mosd_enroll:aggregate - evidence_verification_scope: mosd_enroll:evidence_verification - metadata_scope: mosd_enroll:metadata - read_scope: mosd_enroll:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited enrollment facts used by the Solmara Evidence service. - fields: - - from: duplicate_flag - name: duplicate_flag - sensitive: true - - from: uin - name: uin - sensitive: true - name: mosd_enroll - relationships: [] - spatial: null - table: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - title: MOSD programme enrollment records - id: mosd_enroll - owner: Ministry of Social Development - sensitivity: personal - tables: - - access: - aggregate_scope: mosd_enroll:aggregate - metadata_scope: mosd_enroll:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: duplicate_flag - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv - type: file - title: MOSD programme enrollment records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: programme-mis-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json deleted file mode 100644 index 8378cbe..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.nagdi-farmer-consultation.farmer","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:20dc22dcff83f4f14e0402937e8def43c5f3255241783c35cdc6a6a7afbe3005","id":"relay.nagdi-farmer-consultation.farmer","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"],"required_scope":"registry:consult:nagdi-farmer-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["active_farm_parcel","active_smallholder_farmer","crop_declared_for_season","data_use_authorized","district_climate_risk_active","farmer_registered","voucher_entitlement_current","voucher_not_redeemed"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json deleted file mode 100644 index 45d152e..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.nagdi-herd-consultation.herd","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:7ed0ed7964797fe81e08fcda71fe90b6fcd5846611c3422bb8ce5f6d0b4fe970","id":"relay.nagdi-herd-consultation.herd","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/livestock-movement-control"],"required_scope":"registry:consult:nagdi-herd-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["destination_district_open","herd_vaccination_current","no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species","registered_herd"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json deleted file mode 100644 index 78620c0..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true},"headers":{},"status":200}}}],"name":"farmer-voucher-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"farmer-voucher-no-match","variables":{}}],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json deleted file mode 100644 index 59e808a..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"farmer-voucher-by-farmer","operations":{},"outputs":{"active_farm_parcel":{"from":"snapshot.record.active_farm_parcel","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"from":"snapshot.record.active_smallholder_farmer","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"crop_declared_for_season":{"from":"snapshot.record.crop_declared_for_season","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"data_use_authorized":{"from":"snapshot.record.data_use_authorized","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"district_climate_risk_active":{"from":"snapshot.record.district_climate_risk_active","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"farmer_registered":{"from":"snapshot.record.farmer_registered","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"from":"snapshot.record.voucher_entitlement_current","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"from":"snapshot.record.voucher_not_redeemed","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json deleted file mode 100644 index 7dfbf8c..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json deleted file mode 100644 index a7cd064..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"farmer_id":"FR-8888"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},{"destination_district_open":false,"herd_vaccination_current":false,"no_conflicting_open_movement_permit":false,"origin_district_not_quarantined_for_species":false,"registered_herd":false}],"headers":{},"status":200}}}],"name":"livestock-movement-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},"headers":{},"status":200}}}],"name":"livestock-movement-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"livestock-movement-no-match","variables":{}}],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json deleted file mode 100644 index 6768a09..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"livestock-movement-by-farmer","operations":{},"outputs":{"destination_district_open":{"from":"snapshot.record.destination_district_open","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"herd_vaccination_current":{"from":"snapshot.record.herd_vaccination_current","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"from":"snapshot.record.no_conflicting_open_movement_permit","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"from":"snapshot.record.origin_district_not_quarantined_for_species","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"registered_herd":{"from":"snapshot.record.registered_herd","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json deleted file mode 100644 index a823e60..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json deleted file mode 100644 index 13be5da..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:074f63c5eeddd3398a925e20c327d947da9b40239da0a03a13eae7efbf3ca48a"],"minimization":["sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b"],"negative_security":["sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"farmer-voucher-by-farmer","output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json deleted file mode 100644 index 1733ff4..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:f66511a1e088e0366836719d5011126c22599f4f4d570a24a1d7ecdb3a00dc15"],"minimization":["sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5"],"negative_security":["sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"livestock-movement-by-farmer","output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json deleted file mode 100644 index 55b939f..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:96e6941af8766505df657156db4249cc0504ec262992d8f802f3bbbd1e879e24","id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"active_farm_parcel":"active_farm_parcel","active_smallholder_farmer":"active_smallholder_farmer","crop_declared_for_season":"crop_declared_for_season","data_use_authorized":"data_use_authorized","district_climate_risk_active":"district_climate_risk_active","farmer_registered":"farmer_registered","voucher_entitlement_current":"voucher_entitlement_current","voucher_not_redeemed":"voucher_not_redeemed"}},"table_provider":"nagdi_farmer__materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7"},"profile":{"id":"solmara-nagdi-agriculture.nagdi-farmer-consultation.farmer","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json deleted file mode 100644 index 8c76bd0..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:1c7445affde1963d641ab519a4bc7d0ea83e3c718b055e7f9bd868f707530a4a","id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"destination_district_open":"destination_district_open","herd_vaccination_current":"herd_vaccination_current","no_conflicting_open_movement_permit":"no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species":"origin_district_not_quarantined_for_species","registered_herd":"registered_herd"}},"table_provider":"nagdi_herd__materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b"},"profile":{"id":"solmara-nagdi-agriculture.nagdi-herd-consultation.herd","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/relay.yaml b/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/relay.yaml deleted file mode 100644 index 5a284b2..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay-consultation/relay.yaml +++ /dev/null @@ -1,351 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nagdi-relay.solmara.registrystack.org - publisher: solmara-nagdi-agriculture - title: solmara-nagdi-agriculture governed Registry Relay -consultation: - artifacts: - evidence: - - class: conformance - path: artifacts/evidence/farmer-voucher-by-farmer/conformance.json - sha256: sha256:074f63c5eeddd3398a925e20c327d947da9b40239da0a03a13eae7efbf3ca48a - - class: minimization - path: artifacts/evidence/livestock-movement-by-farmer/minimization.json - sha256: sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5 - - class: negative_security - path: artifacts/evidence/farmer-voucher-by-farmer/negative-security.json - sha256: sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d - - class: negative_security - path: artifacts/evidence/livestock-movement-by-farmer/negative-security.json - sha256: sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce - - class: conformance - path: artifacts/evidence/livestock-movement-by-farmer/conformance.json - sha256: sha256:f66511a1e088e0366836719d5011126c22599f4f4d570a24a1d7ecdb3a00dc15 - - class: minimization - path: artifacts/evidence/farmer-voucher-by-farmer/minimization.json - sha256: sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b - integration_packs: - - hash: sha256:96e6941af8766505df657156db4249cc0504ec262992d8f802f3bbbd1e879e24 - path: artifacts/integration-packs/farmer-voucher-by-farmer.json - sha256: sha256:4ae71cf8b62ea65e9e8e6cd505288532c0b815f4c2c1b32bf678f43d1e03f73a - - hash: sha256:1c7445affde1963d641ab519a4bc7d0ea83e3c718b055e7f9bd868f707530a4a - path: artifacts/integration-packs/livestock-movement-by-farmer.json - sha256: sha256:1edb2e9b8c975e08cbd73f13b86aa73a261f6b68168272ebc9609c49f202b088 - private_bindings: - - hash: sha256:0b1df60778d3ee3367038e1c9a1e5a7cb1f757efa1dc1381431358263fe1a304 - path: artifacts/private-bindings/nagdi-farmer-consultation-farmer.json - sha256: sha256:6b87ded9da5ac89da7d3d87e3a7f01fa71ebb2fa94c39579bd3b7f7acfebb06c - - hash: sha256:b58bf7470fc6339907c00cc4f6fd36c50f8330b16e92802f562bc49663c6d2f9 - path: artifacts/private-bindings/nagdi-herd-consultation-herd.json - sha256: sha256:ad14fa29847e7bf583e8626be3879af2eab79199b6ba08b3ed139494c6b72855 - public_contracts: - - hash: sha256:4f7b86b90a4165ad82d0168d99b367f143743f753dbc027ac497c9f0e1c1eb43 - path: artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json - sha256: sha256:25f7a255324189ea717a24f40ac3c325c7167aebc256db01b1c847a2b1acdbcf - - hash: sha256:1fe461df217f93da31fbe20bcd6ad38ca752e091ca3fe20388153c3365d1b409 - path: artifacts/consultation-contracts/nagdi-herd-consultation-herd.json - sha256: sha256:21e8a0ed388b1b497d4ee2229ddba6f7acbc0b316b3541a8572a478c38c67393 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4554467022379962639 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 7450858971948805253 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited farmer facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: nagdi_farmer:aggregate - evidence_verification_scope: nagdi_farmer:evidence_verification - metadata_scope: nagdi_farmer:metadata - read_scope: nagdi_farmer:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: farmer_id - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited farmer facts used by the Solmara Evidence service. - fields: - - from: active_farm_parcel - name: active_farm_parcel - sensitive: true - - from: active_smallholder_farmer - name: active_smallholder_farmer - sensitive: true - - from: crop_declared_for_season - name: crop_declared_for_season - sensitive: true - - from: data_use_authorized - name: data_use_authorized - sensitive: true - - from: district_climate_risk_active - name: district_climate_risk_active - sensitive: true - - from: farmer_id - name: farmer_id - sensitive: true - - from: farmer_registered - name: farmer_registered - sensitive: true - - from: voucher_entitlement_current - name: voucher_entitlement_current - sensitive: true - - from: voucher_not_redeemed - name: voucher_not_redeemed - sensitive: true - name: nagdi_farmer - relationships: [] - spatial: null - table: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - title: NAgDI farmer records - id: nagdi_farmer - owner: National Agriculture Data and Digital Innovation Agency - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_farmer:aggregate - metadata_scope: nagdi_farmer:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - materialization: snapshot - primary_key: farmer_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: active_farm_parcel - nullable: false - sensitive: true - type: boolean - - name: active_smallholder_farmer - nullable: false - sensitive: true - type: boolean - - name: crop_declared_for_season - nullable: false - sensitive: true - type: boolean - - name: data_use_authorized - nullable: false - sensitive: true - type: boolean - - name: district_climate_risk_active - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: farmer_registered - nullable: false - sensitive: true - type: boolean - - name: voucher_entitlement_current - nullable: false - sensitive: true - type: boolean - - name: voucher_not_redeemed - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - type: file - title: NAgDI farmer records - update_frequency: continuous -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited herd facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: nagdi_herd:aggregate - evidence_verification_scope: nagdi_herd:evidence_verification - metadata_scope: nagdi_herd:metadata - read_scope: nagdi_herd:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: farmer_id - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/livestock-movement-control - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited herd facts used by the Solmara Evidence service. - fields: - - from: destination_district_open - name: destination_district_open - sensitive: true - - from: farmer_id - name: farmer_id - sensitive: true - - from: herd_id - name: herd_id - sensitive: true - - from: herd_vaccination_current - name: herd_vaccination_current - sensitive: true - - from: no_conflicting_open_movement_permit - name: no_conflicting_open_movement_permit - sensitive: true - - from: origin_district_not_quarantined_for_species - name: origin_district_not_quarantined_for_species - sensitive: true - - from: registered_herd - name: registered_herd - sensitive: true - name: nagdi_herd - relationships: [] - spatial: null - table: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - title: NAgDI herd records - id: nagdi_herd - owner: National Agriculture Data and Digital Innovation Agency - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_herd:aggregate - metadata_scope: nagdi_herd:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - materialization: snapshot - primary_key: herd_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: destination_district_open - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: herd_id - nullable: false - sensitive: true - type: string - - name: herd_vaccination_current - nullable: false - sensitive: true - type: boolean - - name: no_conflicting_open_movement_permit - nullable: false - sensitive: true - type: boolean - - name: origin_district_not_quarantined_for_species - nullable: false - sensitive: true - type: boolean - - name: registered_herd - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - type: file - title: NAgDI herd records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nagdi-agriculture-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml b/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml deleted file mode 100644 index 5262a3a..0000000 --- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml +++ /dev/null @@ -1,291 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-evidence - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nagdi-relay.solmara.registrystack.org - publisher: solmara-nagdi-agriculture - title: solmara-nagdi-agriculture governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited farmer facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: nagdi_farmer:aggregate - evidence_verification_scope: nagdi_farmer:evidence_verification - metadata_scope: nagdi_farmer:metadata - read_scope: nagdi_farmer:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: farmer_id - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited farmer facts used by the Solmara Evidence service. - fields: - - from: active_farm_parcel - name: active_farm_parcel - sensitive: true - - from: active_smallholder_farmer - name: active_smallholder_farmer - sensitive: true - - from: crop_declared_for_season - name: crop_declared_for_season - sensitive: true - - from: data_use_authorized - name: data_use_authorized - sensitive: true - - from: district_climate_risk_active - name: district_climate_risk_active - sensitive: true - - from: farmer_id - name: farmer_id - sensitive: true - - from: farmer_registered - name: farmer_registered - sensitive: true - - from: voucher_entitlement_current - name: voucher_entitlement_current - sensitive: true - - from: voucher_not_redeemed - name: voucher_not_redeemed - sensitive: true - name: nagdi_farmer - relationships: [] - spatial: null - table: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - title: NAgDI farmer records - id: nagdi_farmer - owner: National Agriculture Data and Digital Innovation Agency - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_farmer:aggregate - metadata_scope: nagdi_farmer:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - materialization: snapshot - primary_key: farmer_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: active_farm_parcel - nullable: false - sensitive: true - type: boolean - - name: active_smallholder_farmer - nullable: false - sensitive: true - type: boolean - - name: crop_declared_for_season - nullable: false - sensitive: true - type: boolean - - name: data_use_authorized - nullable: false - sensitive: true - type: boolean - - name: district_climate_risk_active - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: farmer_registered - nullable: false - sensitive: true - type: boolean - - name: voucher_entitlement_current - nullable: false - sensitive: true - type: boolean - - name: voucher_not_redeemed - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - type: file - title: NAgDI farmer records - update_frequency: continuous -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited herd facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: nagdi_herd:aggregate - evidence_verification_scope: nagdi_herd:evidence_verification - metadata_scope: nagdi_herd:metadata - read_scope: nagdi_herd:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: farmer_id - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/livestock-movement-control - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited herd facts used by the Solmara Evidence service. - fields: - - from: destination_district_open - name: destination_district_open - sensitive: true - - from: farmer_id - name: farmer_id - sensitive: true - - from: herd_id - name: herd_id - sensitive: true - - from: herd_vaccination_current - name: herd_vaccination_current - sensitive: true - - from: no_conflicting_open_movement_permit - name: no_conflicting_open_movement_permit - sensitive: true - - from: origin_district_not_quarantined_for_species - name: origin_district_not_quarantined_for_species - sensitive: true - - from: registered_herd - name: registered_herd - sensitive: true - name: nagdi_herd - relationships: [] - spatial: null - table: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - title: NAgDI herd records - id: nagdi_herd - owner: National Agriculture Data and Digital Innovation Agency - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_herd:aggregate - metadata_scope: nagdi_herd:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - materialization: snapshot - primary_key: herd_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: destination_district_open - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: herd_id - nullable: false - sensitive: true - type: string - - name: herd_vaccination_current - nullable: false - sensitive: true - type: boolean - - name: no_conflicting_open_movement_permit - nullable: false - sensitive: true - type: boolean - - name: origin_district_not_quarantined_for_species - nullable: false - sensitive: true - type: boolean - - name: registered_herd - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - type: file - title: NAgDI herd records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nagdi-agriculture-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json deleted file mode 100644 index 6614b16..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.nia-population-consultation.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:c962257eac077820b96cd886c1319dd1e741ef4b4bd0f81efbe6cfb8e9105fb2","id":"relay.nia-population-consultation.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:nia-population-consultation","workload":"solmara-esignet"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json deleted file mode 100644 index bf29091..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"alive":true,"identity_status":"active"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"alive":true,"identity_status":"active"},"headers":{},"status":200}}}],"name":"nia-population-person-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"nia-population-person-no-match","variables":{}}],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json deleted file mode 100644 index 223b391..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-nia-population-person-by-uin","operations":{},"outputs":{"alive":{"from":"snapshot.record.alive","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"identity_status":{"from":"snapshot.record.identity_status","max_bytes":128,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json deleted file mode 100644 index 53dcfa4..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json deleted file mode 100644 index 035dab4..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:4f346c55aef3cfc11d5e9db10ca32efd54eac449f16ad2e16a76fbc5bd97f171"],"minimization":["sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407"],"negative_security":["sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-nia-population-person-by-uin","output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":300000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json b/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json deleted file mode 100644 index 4369a15..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:e4c64f00a246783ba2afc3e47bb775d0d5f5c2794c72ea622f8e02bf96792c8d","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d"},"profile":{"id":"solmara-nia-population.nia-population-consultation.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d","tenant":"solmara-nia-population"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/nia-population/relay-consultation/relay.yaml b/runtime/registry-projects/hosted/nia-population/relay-consultation/relay.yaml deleted file mode 100644 index 6d68f2c..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay-consultation/relay.yaml +++ /dev/null @@ -1,248 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nia-relay.solmara.registrystack.org - publisher: solmara-nia-population - title: solmara-nia-population governed Registry Relay -consultation: - artifacts: - evidence: - - class: conformance - path: artifacts/evidence/population-person-by-uin/conformance.json - sha256: sha256:4f346c55aef3cfc11d5e9db10ca32efd54eac449f16ad2e16a76fbc5bd97f171 - - class: minimization - path: artifacts/evidence/population-person-by-uin/minimization.json - sha256: sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407 - - class: negative_security - path: artifacts/evidence/population-person-by-uin/negative-security.json - sha256: sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab - integration_packs: - - hash: sha256:e4c64f00a246783ba2afc3e47bb775d0d5f5c2794c72ea622f8e02bf96792c8d - path: artifacts/integration-packs/population-person-by-uin.json - sha256: sha256:aabbcc69617b62fbb2ad9d0cecdaa1d27f8020147d5fa8f9e9605bbf237a8f05 - private_bindings: - - hash: sha256:aa9e728f3b414efc56026b5cf4129ea1299869e20d93159929ed3f2593aa493e - path: artifacts/private-bindings/nia-population-consultation-population.json - sha256: sha256:be7924bf0dfd91d9cf734f6d72866bc686e35fcbfdf4e0732a7519cad96ad59c - public_contracts: - - hash: sha256:eb210bd4c1c1110a465fb00e4e3da3d91e8b2b4320243750a6d92729a7bb6add - path: artifacts/consultation-contracts/nia-population-consultation-population.json - sha256: sha256:92937e0c5bf6ce83e92a40b492551a59798d2f3600592eb307cce2393675865c - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 1016450306001272100 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 6539803953464966811 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Governed population attributes used by NIA-owned services. - entities: - - access: - aggregate_scope: population:aggregate - evidence_verification_scope: population:evidence_verification - metadata_scope: population:metadata - read_scope: population:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: legacy_nid - ops: - - eq - - field: uin - ops: - - eq - default_limit: 25 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/esignet-identity-verification - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 100 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: - - claims: - - expression: null - name: birthdate - required: true - sensitivity: personal - source_field: birth_date - - expression: null - name: family_name - required: true - sensitivity: direct_identifier - source_field: family_name - - expression: null - name: gender - required: false - sensitivity: personal - source_field: sex - - expression: null - name: given_name - required: true - sensitivity: direct_identifier - source_field: given_name - - expression: null - name: individual_id - required: true - sensitivity: direct_identifier - source_field: uin - - expression: - cel: source.given_name + ' ' + source.family_name - name: name - required: true - sensitivity: direct_identifier - source_field: null - description: Minimal identity attributes for the Solmara eSignet authenticator. - id: solmara-nia-userinfo - purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - release_conditions: - expression: - cel: source.identity_status == 'active' && source.alive == true - release_scope: population:identity_release - subject: - id_type: national_id - source_field: uin - title: SolmaraID UserInfo - version: v1 - description: Governed population attributes used by NIA-owned services. - fields: - - from: alive - name: alive - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: family_name - name: family_name - sensitive: true - - from: given_name - name: given_name - sensitive: true - - from: identity_status - name: identity_status - sensitive: true - - from: legacy_nid - name: legacy_nid - sensitive: true - - from: sex - name: sex - sensitive: true - - from: uin - name: uin - sensitive: true - name: population - relationships: [] - spatial: null - table: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - title: NIA population records - id: population - owner: National Identity Agency - sensitivity: personal - tables: - - access: - aggregate_scope: population:aggregate - metadata_scope: population:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 25 - max_limit: 100 - require_purpose_header: true - id: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: alive - nullable: false - sensitive: true - type: boolean - - name: birth_date - nullable: false - sensitive: true - type: date - - name: family_name - nullable: false - sensitive: true - type: string - - name: given_name - nullable: false - sensitive: true - type: string - - name: identity_status - nullable: false - sensitive: true - type: string - - name: legacy_nid - nullable: true - sensitive: true - type: string - - name: sex - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_NIA_DATABASE_URL - table: - name: population_person - schema: public - type: postgres - title: NIA population records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nia-population-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/nia-population/relay/relay.yaml b/runtime/registry-projects/hosted/nia-population/relay/relay.yaml deleted file mode 100644 index 560b5bb..0000000 --- a/runtime/registry-projects/hosted/nia-population/relay/relay.yaml +++ /dev/null @@ -1,207 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-esignet - - solmara-evidence - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://nia-relay.solmara.registrystack.org - publisher: solmara-nia-population - title: solmara-nia-population governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Governed population attributes used by NIA-owned services. - entities: - - access: - aggregate_scope: population:aggregate - evidence_verification_scope: population:evidence_verification - metadata_scope: population:metadata - read_scope: population:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: legacy_nid - ops: - - eq - - field: uin - ops: - - eq - default_limit: 25 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/esignet-identity-verification - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 100 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: - - claims: - - expression: null - name: birthdate - required: true - sensitivity: personal - source_field: birth_date - - expression: null - name: family_name - required: true - sensitivity: direct_identifier - source_field: family_name - - expression: null - name: gender - required: false - sensitivity: personal - source_field: sex - - expression: null - name: given_name - required: true - sensitivity: direct_identifier - source_field: given_name - - expression: null - name: individual_id - required: true - sensitivity: direct_identifier - source_field: uin - - expression: - cel: source.given_name + ' ' + source.family_name - name: name - required: true - sensitivity: direct_identifier - source_field: null - description: Minimal identity attributes for the Solmara eSignet authenticator. - id: solmara-nia-userinfo - purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - release_conditions: - expression: - cel: source.identity_status == 'active' && source.alive == true - release_scope: population:identity_release - subject: - id_type: national_id - source_field: uin - title: SolmaraID UserInfo - version: v1 - description: Governed population attributes used by NIA-owned services. - fields: - - from: alive - name: alive - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: family_name - name: family_name - sensitive: true - - from: given_name - name: given_name - sensitive: true - - from: identity_status - name: identity_status - sensitive: true - - from: legacy_nid - name: legacy_nid - sensitive: true - - from: sex - name: sex - sensitive: true - - from: uin - name: uin - sensitive: true - name: population - relationships: [] - spatial: null - table: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - title: NIA population records - id: population - owner: National Identity Agency - sensitivity: personal - tables: - - access: - aggregate_scope: population:aggregate - metadata_scope: population:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 25 - max_limit: 100 - require_purpose_header: true - id: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: alive - nullable: false - sensitive: true - type: boolean - - name: birth_date - nullable: false - sensitive: true - type: date - - name: family_name - nullable: false - sensitive: true - type: string - - name: given_name - nullable: false - sensitive: true - type: string - - name: identity_status - nullable: false - sensitive: true - type: string - - name: legacy_nid - nullable: true - sensitive: true - type: string - - name: sex - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_NIA_DATABASE_URL - table: - name: population_person - schema: public - type: postgres - title: NIA population records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: nia-population-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json deleted file mode 100644 index 5b5d627..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.sipf-pension-consultation.pension","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:39b4002812d2babf57a59302f2fc9680ac05e9439e57e74bc02b8d0dbcea3f0a","id":"relay.sipf-pension-consultation.pension","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:sipf-pension-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["payment_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json deleted file mode 100644 index 8e4fcd1..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.sipf-survivor-consultation.survivor","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:99c1d7d89c1d4abe175bd0693a123c881dff1775da7e78099a5158396de11d58","id":"relay.sipf-survivor-consultation.survivor","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"],"required_scope":"registry:consult:sipf-survivor-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["survivor_eligible"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json deleted file mode 100644 index d7d17b1..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"payment_status":"active"},{"payment_status":"suspended"}],"headers":{},"status":200}}}],"name":"sipf-pension-case-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"payment_status":"active"}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"payment_status":"active"},"headers":{},"status":200}}}],"name":"sipf-pension-case-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-pension-case-no-match","variables":{}}],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json deleted file mode 100644 index 27b99c4..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-sipf-pension-case-by-pensioner","operations":{},"outputs":{"payment_status":{"from":"snapshot.record.payment_status","max_bytes":128,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json deleted file mode 100644 index c62fd58..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json deleted file mode 100644 index 53ca6c1..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"survivor_eligible":true},{"survivor_eligible":false}],"headers":{},"status":200}}}],"name":"sipf-survivor-case-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"survivor_eligible":true}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"survivor_eligible":true},"headers":{},"status":200}}}],"name":"sipf-survivor-case-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-survivor-case-no-match","variables":{}}],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json deleted file mode 100644 index e246e22..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-sipf-pension-case-by-survivor","operations":{},"outputs":{"survivor_eligible":{"from":"snapshot.record.survivor_eligible","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json deleted file mode 100644 index 72f6ab7..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json deleted file mode 100644 index ded8a18..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:baac4ff492615cf355beaa403abaad465a23d3552fd6ed3682dce4898e829370"],"minimization":["sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660"],"negative_security":["sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-pensioner","output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json deleted file mode 100644 index 2cd8a30..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:7b0462b4a2d4592cd1aeb8cee70fb54b66a5d9cd7c0aae0d07ee4c160f56d84a"],"minimization":["sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f"],"negative_security":["sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-survivor","output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json deleted file mode 100644 index 94672b6..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:6fe615f0be4b84d28497b7c6111c5426094af8199955977b881e9ebc7c34daa7","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"pensioner_uin","physical_type":"utf8"}},"projection":{"payment_status":"payment_status"}},"table_provider":"pension__materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21"},"profile":{"id":"solmara-sipf-pensions.sipf-pension-consultation.pension","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json deleted file mode 100644 index b3c1c47..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:0aabcdecb0f1cc221e8d4949cdc613c1ecec9c396f0f85b22f24ca8810c92106","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"spouse_uin","physical_type":"utf8"}},"projection":{"survivor_eligible":"survivor_eligible"}},"table_provider":"survivor__materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539"},"profile":{"id":"solmara-sipf-pensions.sipf-survivor-consultation.survivor","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/relay.yaml b/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/relay.yaml deleted file mode 100644 index 3903746..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay-consultation/relay.yaml +++ /dev/null @@ -1,263 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sipf-relay.solmara.registrystack.org - publisher: solmara-sipf-pensions - title: solmara-sipf-pensions governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/pension-case-by-survivor/minimization.json - sha256: sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f - - class: negative_security - path: artifacts/evidence/pension-case-by-survivor/negative-security.json - sha256: sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7 - - class: minimization - path: artifacts/evidence/pension-case-by-pensioner/minimization.json - sha256: sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660 - - class: conformance - path: artifacts/evidence/pension-case-by-survivor/conformance.json - sha256: sha256:7b0462b4a2d4592cd1aeb8cee70fb54b66a5d9cd7c0aae0d07ee4c160f56d84a - - class: conformance - path: artifacts/evidence/pension-case-by-pensioner/conformance.json - sha256: sha256:baac4ff492615cf355beaa403abaad465a23d3552fd6ed3682dce4898e829370 - - class: negative_security - path: artifacts/evidence/pension-case-by-pensioner/negative-security.json - sha256: sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d - integration_packs: - - hash: sha256:6fe615f0be4b84d28497b7c6111c5426094af8199955977b881e9ebc7c34daa7 - path: artifacts/integration-packs/pension-case-by-pensioner.json - sha256: sha256:fc850bad2f6cec2f1b514c3005b92588c62c3f95cefc9d8419061d27cecd649a - - hash: sha256:0aabcdecb0f1cc221e8d4949cdc613c1ecec9c396f0f85b22f24ca8810c92106 - path: artifacts/integration-packs/pension-case-by-survivor.json - sha256: sha256:fac45aff1350bdfc5ec7f7e1741e02c02f74d5ac22505fa65d5b769a8a0697f8 - private_bindings: - - hash: sha256:4ed2016d95b43505d541bf2c91fa0ea952879457e5fce8c66fcd66a178ce57b4 - path: artifacts/private-bindings/sipf-pension-consultation-pension.json - sha256: sha256:872343722e004f2c420535877d15f50477726533c999bcd41d3529513654903d - - hash: sha256:4ff43cbb6fe5281a24a498a1cdb7bbd552ea3868c6aee27872df41fc11f37e2b - path: artifacts/private-bindings/sipf-survivor-consultation-survivor.json - sha256: sha256:f05668bc4724b8acabeadcf95eac837405da292196d1fb8d0e84d5f871bf683c - public_contracts: - - hash: sha256:67a23c52de56b492a69784ec0a57bde7f6772f5a8cbe3f7e450d5bf86a52fd13 - path: artifacts/consultation-contracts/sipf-pension-consultation-pension.json - sha256: sha256:46bdc7b1bf6fb6a2b83c5697bdc690831025b8da1c190ead0471769707c1f30a - - hash: sha256:0b57c5799c75c6a6717f35b045d76189a436aa124157faa070318a2f8a405253 - path: artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json - sha256: sha256:c0b833e6687c0af54df01acf34eda3e39eaef0ff42de2119bd5773e22e547b54 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4444991830243562918 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 6117019989203477116 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited pension facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: pension:aggregate - evidence_verification_scope: pension:evidence_verification - metadata_scope: pension:metadata - read_scope: pension:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: pensioner_uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/pension-payment-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited pension facts used by the Solmara Evidence service. - fields: - - from: payment_status - name: payment_status - sensitive: true - - from: pensioner_uin - name: pensioner_uin - sensitive: true - name: pension - relationships: [] - spatial: null - table: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - title: SIPF pension payment records - id: pension - owner: Social Insurance and Pension Fund - sensitivity: personal - tables: - - access: - aggregate_scope: pension:aggregate - metadata_scope: pension:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - materialization: snapshot - primary_key: pensioner_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: payment_status - nullable: false - sensitive: true - type: string - - name: pensioner_uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_pension_payment - schema: public - type: postgres - title: SIPF pension payment records - update_frequency: continuous -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited survivor facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: survivor:aggregate - evidence_verification_scope: survivor:evidence_verification - metadata_scope: survivor:metadata - read_scope: survivor:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: spouse_uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited survivor facts used by the Solmara Evidence service. - fields: - - from: spouse_uin - name: spouse_uin - sensitive: true - - from: survivor_eligible - name: survivor_eligible - sensitive: true - name: survivor - relationships: [] - spatial: null - table: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - title: SIPF survivor benefit records - id: survivor - owner: Social Insurance and Pension Fund - sensitivity: personal - tables: - - access: - aggregate_scope: survivor:aggregate - metadata_scope: survivor:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - materialization: snapshot - primary_key: spouse_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: spouse_uin - nullable: false - sensitive: true - type: string - - name: survivor_eligible - nullable: false - sensitive: true - type: boolean - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_survivor_benefit - schema: public - type: postgres - title: SIPF survivor benefit records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sipf-pensions-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/sipf-pensions/relay/relay.yaml b/runtime/registry-projects/hosted/sipf-pensions/relay/relay.yaml deleted file mode 100644 index a9bf6fd..0000000 --- a/runtime/registry-projects/hosted/sipf-pensions/relay/relay.yaml +++ /dev/null @@ -1,203 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-evidence - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sipf-relay.solmara.registrystack.org - publisher: solmara-sipf-pensions - title: solmara-sipf-pensions governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited pension facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: pension:aggregate - evidence_verification_scope: pension:evidence_verification - metadata_scope: pension:metadata - read_scope: pension:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: pensioner_uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/pension-payment-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited pension facts used by the Solmara Evidence service. - fields: - - from: payment_status - name: payment_status - sensitive: true - - from: pensioner_uin - name: pensioner_uin - sensitive: true - name: pension - relationships: [] - spatial: null - table: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - title: SIPF pension payment records - id: pension - owner: Social Insurance and Pension Fund - sensitivity: personal - tables: - - access: - aggregate_scope: pension:aggregate - metadata_scope: pension:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - materialization: snapshot - primary_key: pensioner_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: payment_status - nullable: false - sensitive: true - type: string - - name: pensioner_uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_pension_payment - schema: public - type: postgres - title: SIPF pension payment records - update_frequency: continuous -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited survivor facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: survivor:aggregate - evidence_verification_scope: survivor:evidence_verification - metadata_scope: survivor:metadata - read_scope: survivor:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: spouse_uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited survivor facts used by the Solmara Evidence service. - fields: - - from: spouse_uin - name: spouse_uin - sensitive: true - - from: survivor_eligible - name: survivor_eligible - sensitive: true - name: survivor - relationships: [] - spatial: null - table: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - title: SIPF survivor benefit records - id: survivor - owner: Social Insurance and Pension Fund - sensitivity: personal - tables: - - access: - aggregate_scope: survivor:aggregate - metadata_scope: survivor:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - materialization: snapshot - primary_key: spouse_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: spouse_uin - nullable: false - sensitive: true - type: string - - name: survivor_eligible - nullable: false - sensitive: true - type: boolean - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_survivor_benefit - schema: public - type: postgres - title: SIPF survivor benefit records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sipf-pensions-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json deleted file mode 100644 index 769fe52..0000000 --- a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sro-social.sro-household-consultation.household","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:f6d2a35ee331166669b6ae0eecd3ad084de203e4da00827a9368a9fb70f5f180","id":"relay.sro-household-consultation.household","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:sro-household-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sro-social.child-benefit-household-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["poverty_band"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":2678400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json deleted file mode 100644 index 7b8ebae..0000000 --- a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"poverty_band":"priority"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"poverty_band":"priority"},"headers":{},"status":200}}}],"name":"child-benefit-household-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"child-benefit-household-no-match","variables":{}}],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json deleted file mode 100644 index fc5378a..0000000 --- a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"child-benefit-household-by-uin","operations":{},"outputs":{"poverty_band":{"from":"snapshot.record.poverty_band","max_bytes":64,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json deleted file mode 100644 index 9037e0a..0000000 --- a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json deleted file mode 100644 index 257709e..0000000 --- a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sro-social.child-benefit-household-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:a4d045756396f4c69a9607b88163f42efcb7baf55f46d7049b1555e850ed0070"],"minimization":["sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0"],"negative_security":["sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"child-benefit-household-by-uin","output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":2678400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json b/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json deleted file mode 100644 index f598671..0000000 --- a/runtime/registry-projects/hosted/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:47547a81285fa1f655d534d521062e47f86bff6c2a74cb9b3637fd51f2943441","id":"solmara-sro-social.child-benefit-household-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"poverty_band":"poverty_band"}},"table_provider":"sro_child__materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d"},"profile":{"id":"solmara-sro-social.sro-household-consultation.household","version":"1"},"registry_instance":"solmara-sro-social","source_instance":"materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d","tenant":"solmara-sro-social"} \ No newline at end of file diff --git a/runtime/registry-projects/hosted/sro-social/relay-consultation/relay.yaml b/runtime/registry-projects/hosted/sro-social/relay-consultation/relay.yaml deleted file mode 100644 index 30c10dc..0000000 --- a/runtime/registry-projects/hosted/sro-social/relay-consultation/relay.yaml +++ /dev/null @@ -1,159 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sro-relay.solmara.registrystack.org - publisher: solmara-sro-social - title: solmara-sro-social governed Registry Relay -consultation: - artifacts: - evidence: - - class: negative_security - path: artifacts/evidence/child-benefit-household-by-uin/negative-security.json - sha256: sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95 - - class: minimization - path: artifacts/evidence/child-benefit-household-by-uin/minimization.json - sha256: sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0 - - class: conformance - path: artifacts/evidence/child-benefit-household-by-uin/conformance.json - sha256: sha256:a4d045756396f4c69a9607b88163f42efcb7baf55f46d7049b1555e850ed0070 - integration_packs: - - hash: sha256:47547a81285fa1f655d534d521062e47f86bff6c2a74cb9b3637fd51f2943441 - path: artifacts/integration-packs/child-benefit-household-by-uin.json - sha256: sha256:b1cbeacef1a39cb0d4cb03a4adf1061b91532e04acf92abf1e06f60c0b84f1f0 - private_bindings: - - hash: sha256:77a0bae801d725304c75e307f26f53753fb50f9cd72fd9f9bc501da37bfd1064 - path: artifacts/private-bindings/sro-household-consultation-household.json - sha256: sha256:2440ac8ae4c483d106e13af61a8b072509d2452b13a3a36f192b97421ece0bf1 - public_contracts: - - hash: sha256:2009a7b48ceb64aaf24a028af795a3e2fd399fad618db5fd6b1b65cbb8bb281e - path: artifacts/consultation-contracts/sro-household-consultation-household.json - sha256: sha256:662eb6af3e5c5308eedb27ba57575399d2377dbf65d2dd9041520ee21bd8088b - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 368637434756268187 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 2694379094216255332 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited household facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: sro_child:aggregate - evidence_verification_scope: sro_child:evidence_verification - metadata_scope: sro_child:metadata - read_scope: sro_child:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited household facts used by the Solmara Evidence service. - fields: - - from: poverty_band - name: poverty_band - sensitive: true - - from: uin - name: uin - sensitive: true - name: sro_child - relationships: [] - spatial: null - table: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - title: SRO household records - id: sro_child - owner: Social Registry Office - sensitivity: personal - tables: - - access: - aggregate_scope: sro_child:aggregate - metadata_scope: sro_child:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: poverty_band - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv - type: file - title: SRO household records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sro-social-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/hosted/sro-social/relay/relay.yaml b/runtime/registry-projects/hosted/sro-social/relay/relay.yaml deleted file mode 100644 index dee4b56..0000000 --- a/runtime/registry-projects/hosted/sro-social/relay/relay.yaml +++ /dev/null @@ -1,117 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: false - allowed_clients: - - solmara-evidence - audiences: - - registry-relay - issuer: https://workload-issuer.solmara.registrystack.org - jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json -catalog: - base_url: https://sro-relay.solmara.registrystack.org - publisher: solmara-sro-social - title: solmara-sro-social governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited household facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: sro_child:aggregate - evidence_verification_scope: sro_child:evidence_verification - metadata_scope: sro_child:metadata - read_scope: sro_child:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited household facts used by the Solmara Evidence service. - fields: - - from: poverty_band - name: poverty_band - sensitive: true - - from: uin - name: uin - sensitive: true - name: sro_child - relationships: [] - spatial: null - table: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - title: SRO household records - id: sro_child - owner: Social Registry Office - sensitivity: personal - tables: - - access: - aggregate_scope: sro_child:aggregate - metadata_scope: sro_child:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: poverty_band - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv - type: file - title: SRO household records - update_frequency: continuous -deployment: - profile: hosted_lab -instance: - environment: hosted - id: sro-social-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json deleted file mode 100644 index 1ec4b1f..0000000 --- a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/consultation-contracts/cra-civil-consultation-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.cra-civil-consultation.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:aa167d27cfe661509fb5b8e159dcf410ea4057cf223d8adfc33bcdefaf3e2818","id":"relay.cra-civil-consultation.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:cra-civil-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json deleted file mode 100644 index bfcbb9d..0000000 --- a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-future-birth-date","variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-match","variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"cra-civil-person-no-match","variables":{"as_of_date":"2026-07-14"}}],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json deleted file mode 100644 index 9d92e3c..0000000 --- a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-cra-civil-person-by-uin","operations":{},"outputs":{"birth_brn":{"from":"snapshot.record.birth_brn","max_bytes":128,"maximum":null,"minimum":null,"nullable":true,"type":"string"},"birth_date":{"from":"snapshot.record.birth_date","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"date"},"deceased":{"from":"snapshot.record.deceased","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json deleted file mode 100644 index 18ba42a..0000000 --- a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/evidence/civil-person-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json deleted file mode 100644 index e80b1e7..0000000 --- a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/integration-packs/civil-person-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:4348d96de834e21384de4e5d3f7e19ff10e299a7148c8d1df0739ee1d50199a3"],"minimization":["sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263"],"negative_security":["sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-cra-civil-person-by-uin","output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json b/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json deleted file mode 100644 index cb199e7..0000000 --- a/runtime/registry-projects/local/cra-civil/relay-consultation/artifacts/private-bindings/cra-civil-consultation-civil.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:487bef77452bf86b610743eadc53f87429ef9ec66c06b9ee8d41e8a0e5ef6210","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382"},"profile":{"id":"solmara-cra-civil.cra-civil-consultation.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/runtime/registry-projects/local/cra-civil/relay-consultation/relay.yaml b/runtime/registry-projects/local/cra-civil/relay-consultation/relay.yaml deleted file mode 100644 index da0511f..0000000 --- a/runtime/registry-projects/local/cra-civil/relay-consultation/relay.yaml +++ /dev/null @@ -1,175 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-cra-civil - title: solmara-cra-civil governed Registry Relay -consultation: - artifacts: - evidence: - - class: negative_security - path: artifacts/evidence/civil-person-by-uin/negative-security.json - sha256: sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f - - class: conformance - path: artifacts/evidence/civil-person-by-uin/conformance.json - sha256: sha256:4348d96de834e21384de4e5d3f7e19ff10e299a7148c8d1df0739ee1d50199a3 - - class: minimization - path: artifacts/evidence/civil-person-by-uin/minimization.json - sha256: sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263 - integration_packs: - - hash: sha256:487bef77452bf86b610743eadc53f87429ef9ec66c06b9ee8d41e8a0e5ef6210 - path: artifacts/integration-packs/civil-person-by-uin.json - sha256: sha256:975ab5565da70354b1eccb3f2f5da123e69d4983c45bcf5955cf55df6d4123cf - private_bindings: - - hash: sha256:41e6b05846b019807b19590010bd812c5477d74228a55636b725548d21673bc4 - path: artifacts/private-bindings/cra-civil-consultation-civil.json - sha256: sha256:f5e2bd11c9c512a718bb0f82d8ba0d554b16f6da5ddf1d5f896bd536711824c5 - public_contracts: - - hash: sha256:7d81ecba1b399a2caa3f8b08f27b93626f2c8b2d51ec2305874a19af34cc852b - path: artifacts/consultation-contracts/cra-civil-consultation-civil.json - sha256: sha256:28effc0ce34a4c498ff7605cde0c59c59ecd199877bbd02fb73c49be31b73f48 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 5222333210205388 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 3104283851243516304 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited civil facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: civil_people:aggregate - evidence_verification_scope: civil_people:evidence_verification - metadata_scope: civil_people:metadata - read_scope: civil_people:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/pension-payment-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited civil facts used by the Solmara Evidence service. - fields: - - from: birth_brn - name: birth_brn - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: deceased - name: deceased - sensitive: true - - from: uin - name: uin - sensitive: true - name: civil_people - relationships: [] - spatial: null - table: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - title: CRA civil records - id: civil_people - owner: Civil Registration Authority - sensitivity: personal - tables: - - access: - aggregate_scope: civil_people:aggregate - metadata_scope: civil_people:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: birth_brn - nullable: true - sensitive: true - type: string - - name: birth_date - nullable: false - sensitive: true - type: date - - name: deceased - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv - type: file - title: CRA civil records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: cra-civil-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/cra-civil/relay/relay.yaml b/runtime/registry-projects/local/cra-civil/relay/relay.yaml deleted file mode 100644 index 63ac593..0000000 --- a/runtime/registry-projects/local/cra-civil/relay/relay.yaml +++ /dev/null @@ -1,133 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-evidence - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-cra-civil - title: solmara-cra-civil governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited civil facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: civil_people:aggregate - evidence_verification_scope: civil_people:evidence_verification - metadata_scope: civil_people:metadata - read_scope: civil_people:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/pension-payment-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited civil facts used by the Solmara Evidence service. - fields: - - from: birth_brn - name: birth_brn - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: deceased - name: deceased - sensitive: true - - from: uin - name: uin - sensitive: true - name: civil_people - relationships: [] - spatial: null - table: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - title: CRA civil records - id: civil_people - owner: Civil Registration Authority - sensitivity: personal - tables: - - access: - aggregate_scope: civil_people:aggregate - metadata_scope: civil_people:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_f5d795774cce29f1dfb681aae7844653315f52188d39b841602bb758e7e53382 - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: birth_brn - nullable: true - sensitive: true - type: string - - name: birth_date - nullable: false - sensitive: true - type: date - - name: deceased - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv - type: file - title: CRA civil records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: cra-civil-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json deleted file mode 100644 index c43521d..0000000 --- a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-mosd-programme.mosd-enrollment-consultation.enrollment","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:e51568815beddf2e304dcc482daf67f7493e1cd23ff68847d22eb62c22cb6ba3","id":"relay.mosd-enrollment-consultation.enrollment","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:mosd-enrollment-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-mosd-programme.enrollment-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["duplicate_flag"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json deleted file mode 100644 index e91e1d3..0000000 --- a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"duplicate_flag":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"duplicate_flag":false},"headers":{},"status":200}}}],"name":"enrollment-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"enrollment-no-match","variables":{}}],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json deleted file mode 100644 index 1d539e7..0000000 --- a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"enrollment-by-uin","operations":{},"outputs":{"duplicate_flag":{"from":"snapshot.record.duplicate_flag","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json deleted file mode 100644 index 0b38f83..0000000 --- a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/evidence/enrollment-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json deleted file mode 100644 index 096dbe6..0000000 --- a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/integration-packs/enrollment-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-mosd-programme.enrollment-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:304b2cd16ce7d97127bfb0be3b60f7c25b2624ed25fbe0219bac09c067e6a1bb"],"minimization":["sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83"],"negative_security":["sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"enrollment-by-uin","output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json b/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json deleted file mode 100644 index a4bc8da..0000000 --- a/runtime/registry-projects/local/mosd-programme/relay-consultation/artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:d82492e2f92fca8ce1d83208d892e2a18468ef6db6afa66d467df002336a6c92","id":"solmara-mosd-programme.enrollment-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"duplicate_flag":"duplicate_flag"}},"table_provider":"mosd_enroll__materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf"},"profile":{"id":"solmara-mosd-programme.mosd-enrollment-consultation.enrollment","version":"1"},"registry_instance":"solmara-mosd-programme","source_instance":"materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf","tenant":"solmara-mosd-programme"} \ No newline at end of file diff --git a/runtime/registry-projects/local/mosd-programme/relay-consultation/relay.yaml b/runtime/registry-projects/local/mosd-programme/relay-consultation/relay.yaml deleted file mode 100644 index 61152d8..0000000 --- a/runtime/registry-projects/local/mosd-programme/relay-consultation/relay.yaml +++ /dev/null @@ -1,159 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-mosd-programme - title: solmara-mosd-programme governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/enrollment-by-uin/minimization.json - sha256: sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83 - - class: conformance - path: artifacts/evidence/enrollment-by-uin/conformance.json - sha256: sha256:304b2cd16ce7d97127bfb0be3b60f7c25b2624ed25fbe0219bac09c067e6a1bb - - class: negative_security - path: artifacts/evidence/enrollment-by-uin/negative-security.json - sha256: sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282 - integration_packs: - - hash: sha256:d82492e2f92fca8ce1d83208d892e2a18468ef6db6afa66d467df002336a6c92 - path: artifacts/integration-packs/enrollment-by-uin.json - sha256: sha256:3b4de6e7946d91a2af1e6f0cdbc3397e7c382e960ae829ec0ddbb030f8192c5d - private_bindings: - - hash: sha256:2d91cd9c15c0badbb565bd16a2be9f46ac77d0ad0c827a0eea895e20edf3a3a2 - path: artifacts/private-bindings/mosd-enrollment-consultation-enrollment.json - sha256: sha256:f27344b5e2dec067577087b23dfd4cc031e5680f4871dc028222be875f81865a - public_contracts: - - hash: sha256:58e60de7fdae8b94a0cdf130f80c1daffb2622773aac4e60d3714f6547a7b2a5 - path: artifacts/consultation-contracts/mosd-enrollment-consultation-enrollment.json - sha256: sha256:50bee564d51a566df7dcd634476a2bad80920ebfb0d88b535db85081c8558cab - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 9069287079765840165 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 891105660075992367 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited enrollment facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: mosd_enroll:aggregate - evidence_verification_scope: mosd_enroll:evidence_verification - metadata_scope: mosd_enroll:metadata - read_scope: mosd_enroll:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited enrollment facts used by the Solmara Evidence service. - fields: - - from: duplicate_flag - name: duplicate_flag - sensitive: true - - from: uin - name: uin - sensitive: true - name: mosd_enroll - relationships: [] - spatial: null - table: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - title: MOSD programme enrollment records - id: mosd_enroll - owner: Ministry of Social Development - sensitivity: personal - tables: - - access: - aggregate_scope: mosd_enroll:aggregate - metadata_scope: mosd_enroll:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: duplicate_flag - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv - type: file - title: MOSD programme enrollment records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: programme-mis-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/mosd-programme/relay/relay.yaml b/runtime/registry-projects/local/mosd-programme/relay/relay.yaml deleted file mode 100644 index 0c130a5..0000000 --- a/runtime/registry-projects/local/mosd-programme/relay/relay.yaml +++ /dev/null @@ -1,117 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-evidence - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-mosd-programme - title: solmara-mosd-programme governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited enrollment facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: mosd_enroll:aggregate - evidence_verification_scope: mosd_enroll:evidence_verification - metadata_scope: mosd_enroll:metadata - read_scope: mosd_enroll:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited enrollment facts used by the Solmara Evidence service. - fields: - - from: duplicate_flag - name: duplicate_flag - sensitive: true - - from: uin - name: uin - sensitive: true - name: mosd_enroll - relationships: [] - spatial: null - table: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - title: MOSD programme enrollment records - id: mosd_enroll - owner: Ministry of Social Development - sensitivity: personal - tables: - - access: - aggregate_scope: mosd_enroll:aggregate - metadata_scope: mosd_enroll:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: duplicate_flag - nullable: false - sensitive: true - type: boolean - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv - type: file - title: MOSD programme enrollment records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: programme-mis-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json deleted file mode 100644 index 8378cbe..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.nagdi-farmer-consultation.farmer","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:20dc22dcff83f4f14e0402937e8def43c5f3255241783c35cdc6a6a7afbe3005","id":"relay.nagdi-farmer-consultation.farmer","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"],"required_scope":"registry:consult:nagdi-farmer-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["active_farm_parcel","active_smallholder_farmer","crop_declared_for_season","data_use_authorized","district_climate_risk_active","farmer_registered","voucher_entitlement_current","voucher_not_redeemed"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json deleted file mode 100644 index 45d152e..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/consultation-contracts/nagdi-herd-consultation-herd.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.nagdi-herd-consultation.herd","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:7ed0ed7964797fe81e08fcda71fe90b6fcd5846611c3422bb8ce5f6d0b4fe970","id":"relay.nagdi-herd-consultation.herd","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/livestock-movement-control"],"required_scope":"registry:consult:nagdi-herd-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["destination_district_open","herd_vaccination_current","no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species","registered_herd"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json deleted file mode 100644 index 78620c0..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true},"headers":{},"status":200}}}],"name":"farmer-voucher-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"farmer-voucher-no-match","variables":{}}],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json deleted file mode 100644 index 59e808a..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"farmer-voucher-by-farmer","operations":{},"outputs":{"active_farm_parcel":{"from":"snapshot.record.active_farm_parcel","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"from":"snapshot.record.active_smallholder_farmer","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"crop_declared_for_season":{"from":"snapshot.record.crop_declared_for_season","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"data_use_authorized":{"from":"snapshot.record.data_use_authorized","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"district_climate_risk_active":{"from":"snapshot.record.district_climate_risk_active","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"farmer_registered":{"from":"snapshot.record.farmer_registered","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"from":"snapshot.record.voucher_entitlement_current","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"from":"snapshot.record.voucher_not_redeemed","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json deleted file mode 100644 index 7dfbf8c..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json deleted file mode 100644 index a7cd064..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"farmer_id":"FR-8888"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},{"destination_district_open":false,"herd_vaccination_current":false,"no_conflicting_open_movement_permit":false,"origin_district_not_quarantined_for_species":false,"registered_herd":false}],"headers":{},"status":200}}}],"name":"livestock-movement-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},"headers":{},"status":200}}}],"name":"livestock-movement-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"livestock-movement-no-match","variables":{}}],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json deleted file mode 100644 index 6768a09..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"livestock-movement-by-farmer","operations":{},"outputs":{"destination_district_open":{"from":"snapshot.record.destination_district_open","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"herd_vaccination_current":{"from":"snapshot.record.herd_vaccination_current","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"from":"snapshot.record.no_conflicting_open_movement_permit","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"from":"snapshot.record.origin_district_not_quarantined_for_species","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"registered_herd":{"from":"snapshot.record.registered_herd","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json deleted file mode 100644 index a823e60..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/evidence/livestock-movement-by-farmer/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json deleted file mode 100644 index 13be5da..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/farmer-voucher-by-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:074f63c5eeddd3398a925e20c327d947da9b40239da0a03a13eae7efbf3ca48a"],"minimization":["sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b"],"negative_security":["sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"farmer-voucher-by-farmer","output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json deleted file mode 100644 index 1733ff4..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/integration-packs/livestock-movement-by-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:f66511a1e088e0366836719d5011126c22599f4f4d570a24a1d7ecdb3a00dc15"],"minimization":["sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5"],"negative_security":["sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"livestock-movement-by-farmer","output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json deleted file mode 100644 index 55b939f..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-farmer-consultation-farmer.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:96e6941af8766505df657156db4249cc0504ec262992d8f802f3bbbd1e879e24","id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"active_farm_parcel":"active_farm_parcel","active_smallholder_farmer":"active_smallholder_farmer","crop_declared_for_season":"crop_declared_for_season","data_use_authorized":"data_use_authorized","district_climate_risk_active":"district_climate_risk_active","farmer_registered":"farmer_registered","voucher_entitlement_current":"voucher_entitlement_current","voucher_not_redeemed":"voucher_not_redeemed"}},"table_provider":"nagdi_farmer__materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7"},"profile":{"id":"solmara-nagdi-agriculture.nagdi-farmer-consultation.farmer","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json deleted file mode 100644 index 8c76bd0..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/artifacts/private-bindings/nagdi-herd-consultation-herd.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:1c7445affde1963d641ab519a4bc7d0ea83e3c718b055e7f9bd868f707530a4a","id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"destination_district_open":"destination_district_open","herd_vaccination_current":"herd_vaccination_current","no_conflicting_open_movement_permit":"no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species":"origin_district_not_quarantined_for_species","registered_herd":"registered_herd"}},"table_provider":"nagdi_herd__materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b"},"profile":{"id":"solmara-nagdi-agriculture.nagdi-herd-consultation.herd","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/relay.yaml b/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/relay.yaml deleted file mode 100644 index 520e854..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay-consultation/relay.yaml +++ /dev/null @@ -1,351 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-nagdi-agriculture - title: solmara-nagdi-agriculture governed Registry Relay -consultation: - artifacts: - evidence: - - class: conformance - path: artifacts/evidence/farmer-voucher-by-farmer/conformance.json - sha256: sha256:074f63c5eeddd3398a925e20c327d947da9b40239da0a03a13eae7efbf3ca48a - - class: minimization - path: artifacts/evidence/livestock-movement-by-farmer/minimization.json - sha256: sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5 - - class: negative_security - path: artifacts/evidence/farmer-voucher-by-farmer/negative-security.json - sha256: sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d - - class: negative_security - path: artifacts/evidence/livestock-movement-by-farmer/negative-security.json - sha256: sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce - - class: conformance - path: artifacts/evidence/livestock-movement-by-farmer/conformance.json - sha256: sha256:f66511a1e088e0366836719d5011126c22599f4f4d570a24a1d7ecdb3a00dc15 - - class: minimization - path: artifacts/evidence/farmer-voucher-by-farmer/minimization.json - sha256: sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b - integration_packs: - - hash: sha256:96e6941af8766505df657156db4249cc0504ec262992d8f802f3bbbd1e879e24 - path: artifacts/integration-packs/farmer-voucher-by-farmer.json - sha256: sha256:4ae71cf8b62ea65e9e8e6cd505288532c0b815f4c2c1b32bf678f43d1e03f73a - - hash: sha256:1c7445affde1963d641ab519a4bc7d0ea83e3c718b055e7f9bd868f707530a4a - path: artifacts/integration-packs/livestock-movement-by-farmer.json - sha256: sha256:1edb2e9b8c975e08cbd73f13b86aa73a261f6b68168272ebc9609c49f202b088 - private_bindings: - - hash: sha256:0b1df60778d3ee3367038e1c9a1e5a7cb1f757efa1dc1381431358263fe1a304 - path: artifacts/private-bindings/nagdi-farmer-consultation-farmer.json - sha256: sha256:6b87ded9da5ac89da7d3d87e3a7f01fa71ebb2fa94c39579bd3b7f7acfebb06c - - hash: sha256:b58bf7470fc6339907c00cc4f6fd36c50f8330b16e92802f562bc49663c6d2f9 - path: artifacts/private-bindings/nagdi-herd-consultation-herd.json - sha256: sha256:ad14fa29847e7bf583e8626be3879af2eab79199b6ba08b3ed139494c6b72855 - public_contracts: - - hash: sha256:4f7b86b90a4165ad82d0168d99b367f143743f753dbc027ac497c9f0e1c1eb43 - path: artifacts/consultation-contracts/nagdi-farmer-consultation-farmer.json - sha256: sha256:25f7a255324189ea717a24f40ac3c325c7167aebc256db01b1c847a2b1acdbcf - - hash: sha256:1fe461df217f93da31fbe20bcd6ad38ca752e091ca3fe20388153c3365d1b409 - path: artifacts/consultation-contracts/nagdi-herd-consultation-herd.json - sha256: sha256:21e8a0ed388b1b497d4ee2229ddba6f7acbc0b316b3541a8572a478c38c67393 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4554467022379962639 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 7450858971948805253 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited farmer facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: nagdi_farmer:aggregate - evidence_verification_scope: nagdi_farmer:evidence_verification - metadata_scope: nagdi_farmer:metadata - read_scope: nagdi_farmer:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: farmer_id - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited farmer facts used by the Solmara Evidence service. - fields: - - from: active_farm_parcel - name: active_farm_parcel - sensitive: true - - from: active_smallholder_farmer - name: active_smallholder_farmer - sensitive: true - - from: crop_declared_for_season - name: crop_declared_for_season - sensitive: true - - from: data_use_authorized - name: data_use_authorized - sensitive: true - - from: district_climate_risk_active - name: district_climate_risk_active - sensitive: true - - from: farmer_id - name: farmer_id - sensitive: true - - from: farmer_registered - name: farmer_registered - sensitive: true - - from: voucher_entitlement_current - name: voucher_entitlement_current - sensitive: true - - from: voucher_not_redeemed - name: voucher_not_redeemed - sensitive: true - name: nagdi_farmer - relationships: [] - spatial: null - table: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - title: NAgDI farmer records - id: nagdi_farmer - owner: National Agriculture Data and Digital Innovation Agency - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_farmer:aggregate - metadata_scope: nagdi_farmer:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - materialization: snapshot - primary_key: farmer_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: active_farm_parcel - nullable: false - sensitive: true - type: boolean - - name: active_smallholder_farmer - nullable: false - sensitive: true - type: boolean - - name: crop_declared_for_season - nullable: false - sensitive: true - type: boolean - - name: data_use_authorized - nullable: false - sensitive: true - type: boolean - - name: district_climate_risk_active - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: farmer_registered - nullable: false - sensitive: true - type: boolean - - name: voucher_entitlement_current - nullable: false - sensitive: true - type: boolean - - name: voucher_not_redeemed - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - type: file - title: NAgDI farmer records - update_frequency: continuous -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited herd facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: nagdi_herd:aggregate - evidence_verification_scope: nagdi_herd:evidence_verification - metadata_scope: nagdi_herd:metadata - read_scope: nagdi_herd:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: farmer_id - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/livestock-movement-control - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited herd facts used by the Solmara Evidence service. - fields: - - from: destination_district_open - name: destination_district_open - sensitive: true - - from: farmer_id - name: farmer_id - sensitive: true - - from: herd_id - name: herd_id - sensitive: true - - from: herd_vaccination_current - name: herd_vaccination_current - sensitive: true - - from: no_conflicting_open_movement_permit - name: no_conflicting_open_movement_permit - sensitive: true - - from: origin_district_not_quarantined_for_species - name: origin_district_not_quarantined_for_species - sensitive: true - - from: registered_herd - name: registered_herd - sensitive: true - name: nagdi_herd - relationships: [] - spatial: null - table: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - title: NAgDI herd records - id: nagdi_herd - owner: National Agriculture Data and Digital Innovation Agency - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_herd:aggregate - metadata_scope: nagdi_herd:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - materialization: snapshot - primary_key: herd_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: destination_district_open - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: herd_id - nullable: false - sensitive: true - type: string - - name: herd_vaccination_current - nullable: false - sensitive: true - type: boolean - - name: no_conflicting_open_movement_permit - nullable: false - sensitive: true - type: boolean - - name: origin_district_not_quarantined_for_species - nullable: false - sensitive: true - type: boolean - - name: registered_herd - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - type: file - title: NAgDI herd records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: nagdi-agriculture-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/nagdi-agriculture/relay/relay.yaml b/runtime/registry-projects/local/nagdi-agriculture/relay/relay.yaml deleted file mode 100644 index 439b7ba..0000000 --- a/runtime/registry-projects/local/nagdi-agriculture/relay/relay.yaml +++ /dev/null @@ -1,291 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-evidence - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-nagdi-agriculture - title: solmara-nagdi-agriculture governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited farmer facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: nagdi_farmer:aggregate - evidence_verification_scope: nagdi_farmer:evidence_verification - metadata_scope: nagdi_farmer:metadata - read_scope: nagdi_farmer:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: farmer_id - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/voucher-eligibility-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited farmer facts used by the Solmara Evidence service. - fields: - - from: active_farm_parcel - name: active_farm_parcel - sensitive: true - - from: active_smallholder_farmer - name: active_smallholder_farmer - sensitive: true - - from: crop_declared_for_season - name: crop_declared_for_season - sensitive: true - - from: data_use_authorized - name: data_use_authorized - sensitive: true - - from: district_climate_risk_active - name: district_climate_risk_active - sensitive: true - - from: farmer_id - name: farmer_id - sensitive: true - - from: farmer_registered - name: farmer_registered - sensitive: true - - from: voucher_entitlement_current - name: voucher_entitlement_current - sensitive: true - - from: voucher_not_redeemed - name: voucher_not_redeemed - sensitive: true - name: nagdi_farmer - relationships: [] - spatial: null - table: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - title: NAgDI farmer records - id: nagdi_farmer - owner: National Agriculture Data and Digital Innovation Agency - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_farmer:aggregate - metadata_scope: nagdi_farmer:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 - materialization: snapshot - primary_key: farmer_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: active_farm_parcel - nullable: false - sensitive: true - type: boolean - - name: active_smallholder_farmer - nullable: false - sensitive: true - type: boolean - - name: crop_declared_for_season - nullable: false - sensitive: true - type: boolean - - name: data_use_authorized - nullable: false - sensitive: true - type: boolean - - name: district_climate_risk_active - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: farmer_registered - nullable: false - sensitive: true - type: boolean - - name: voucher_entitlement_current - nullable: false - sensitive: true - type: boolean - - name: voucher_not_redeemed - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv - type: file - title: NAgDI farmer records - update_frequency: continuous -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited herd facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: nagdi_herd:aggregate - evidence_verification_scope: nagdi_herd:evidence_verification - metadata_scope: nagdi_herd:metadata - read_scope: nagdi_herd:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: farmer_id - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/livestock-movement-control - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited herd facts used by the Solmara Evidence service. - fields: - - from: destination_district_open - name: destination_district_open - sensitive: true - - from: farmer_id - name: farmer_id - sensitive: true - - from: herd_id - name: herd_id - sensitive: true - - from: herd_vaccination_current - name: herd_vaccination_current - sensitive: true - - from: no_conflicting_open_movement_permit - name: no_conflicting_open_movement_permit - sensitive: true - - from: origin_district_not_quarantined_for_species - name: origin_district_not_quarantined_for_species - sensitive: true - - from: registered_herd - name: registered_herd - sensitive: true - name: nagdi_herd - relationships: [] - spatial: null - table: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - title: NAgDI herd records - id: nagdi_herd - owner: National Agriculture Data and Digital Innovation Agency - sensitivity: personal - tables: - - access: - aggregate_scope: nagdi_herd:aggregate - metadata_scope: nagdi_herd:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b - materialization: snapshot - primary_key: herd_id - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: destination_district_open - nullable: false - sensitive: true - type: boolean - - name: farmer_id - nullable: false - sensitive: true - type: string - - name: herd_id - nullable: false - sensitive: true - type: string - - name: herd_vaccination_current - nullable: false - sensitive: true - type: boolean - - name: no_conflicting_open_movement_permit - nullable: false - sensitive: true - type: boolean - - name: origin_district_not_quarantined_for_species - nullable: false - sensitive: true - type: boolean - - name: registered_herd - nullable: false - sensitive: true - type: boolean - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv - type: file - title: NAgDI herd records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: nagdi-agriculture-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json deleted file mode 100644 index 6614b16..0000000 --- a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/consultation-contracts/nia-population-consultation-population.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.nia-population-consultation.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:c962257eac077820b96cd886c1319dd1e741ef4b4bd0f81efbe6cfb8e9105fb2","id":"relay.nia-population-consultation.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:nia-population-consultation","workload":"solmara-esignet"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json deleted file mode 100644 index bf29091..0000000 --- a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"alive":true,"identity_status":"active"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"alive":true,"identity_status":"active"},"headers":{},"status":200}}}],"name":"nia-population-person-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"nia-population-person-no-match","variables":{}}],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json deleted file mode 100644 index 223b391..0000000 --- a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-nia-population-person-by-uin","operations":{},"outputs":{"alive":{"from":"snapshot.record.alive","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"identity_status":{"from":"snapshot.record.identity_status","max_bytes":128,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json deleted file mode 100644 index 53dcfa4..0000000 --- a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/evidence/population-person-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json deleted file mode 100644 index 035dab4..0000000 --- a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/integration-packs/population-person-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:4f346c55aef3cfc11d5e9db10ca32efd54eac449f16ad2e16a76fbc5bd97f171"],"minimization":["sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407"],"negative_security":["sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-nia-population-person-by-uin","output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":300000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json b/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json deleted file mode 100644 index 4369a15..0000000 --- a/runtime/registry-projects/local/nia-population/relay-consultation/artifacts/private-bindings/nia-population-consultation-population.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:e4c64f00a246783ba2afc3e47bb775d0d5f5c2794c72ea622f8e02bf96792c8d","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d"},"profile":{"id":"solmara-nia-population.nia-population-consultation.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d","tenant":"solmara-nia-population"} \ No newline at end of file diff --git a/runtime/registry-projects/local/nia-population/relay-consultation/relay.yaml b/runtime/registry-projects/local/nia-population/relay-consultation/relay.yaml deleted file mode 100644 index 8a705b0..0000000 --- a/runtime/registry-projects/local/nia-population/relay-consultation/relay.yaml +++ /dev/null @@ -1,248 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-nia-population - title: solmara-nia-population governed Registry Relay -consultation: - artifacts: - evidence: - - class: conformance - path: artifacts/evidence/population-person-by-uin/conformance.json - sha256: sha256:4f346c55aef3cfc11d5e9db10ca32efd54eac449f16ad2e16a76fbc5bd97f171 - - class: minimization - path: artifacts/evidence/population-person-by-uin/minimization.json - sha256: sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407 - - class: negative_security - path: artifacts/evidence/population-person-by-uin/negative-security.json - sha256: sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab - integration_packs: - - hash: sha256:e4c64f00a246783ba2afc3e47bb775d0d5f5c2794c72ea622f8e02bf96792c8d - path: artifacts/integration-packs/population-person-by-uin.json - sha256: sha256:aabbcc69617b62fbb2ad9d0cecdaa1d27f8020147d5fa8f9e9605bbf237a8f05 - private_bindings: - - hash: sha256:aa9e728f3b414efc56026b5cf4129ea1299869e20d93159929ed3f2593aa493e - path: artifacts/private-bindings/nia-population-consultation-population.json - sha256: sha256:be7924bf0dfd91d9cf734f6d72866bc686e35fcbfdf4e0732a7519cad96ad59c - public_contracts: - - hash: sha256:eb210bd4c1c1110a465fb00e4e3da3d91e8b2b4320243750a6d92729a7bb6add - path: artifacts/consultation-contracts/nia-population-consultation-population.json - sha256: sha256:92937e0c5bf6ce83e92a40b492551a59798d2f3600592eb307cce2393675865c - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 1016450306001272100 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 6539803953464966811 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Governed population attributes used by NIA-owned services. - entities: - - access: - aggregate_scope: population:aggregate - evidence_verification_scope: population:evidence_verification - metadata_scope: population:metadata - read_scope: population:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: legacy_nid - ops: - - eq - - field: uin - ops: - - eq - default_limit: 25 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/esignet-identity-verification - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 100 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: - - claims: - - expression: null - name: birthdate - required: true - sensitivity: personal - source_field: birth_date - - expression: null - name: family_name - required: true - sensitivity: direct_identifier - source_field: family_name - - expression: null - name: gender - required: false - sensitivity: personal - source_field: sex - - expression: null - name: given_name - required: true - sensitivity: direct_identifier - source_field: given_name - - expression: null - name: individual_id - required: true - sensitivity: direct_identifier - source_field: uin - - expression: - cel: source.given_name + ' ' + source.family_name - name: name - required: true - sensitivity: direct_identifier - source_field: null - description: Minimal identity attributes for the Solmara eSignet authenticator. - id: solmara-nia-userinfo - purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - release_conditions: - expression: - cel: source.identity_status == 'active' && source.alive == true - release_scope: population:identity_release - subject: - id_type: national_id - source_field: uin - title: SolmaraID UserInfo - version: v1 - description: Governed population attributes used by NIA-owned services. - fields: - - from: alive - name: alive - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: family_name - name: family_name - sensitive: true - - from: given_name - name: given_name - sensitive: true - - from: identity_status - name: identity_status - sensitive: true - - from: legacy_nid - name: legacy_nid - sensitive: true - - from: sex - name: sex - sensitive: true - - from: uin - name: uin - sensitive: true - name: population - relationships: [] - spatial: null - table: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - title: NIA population records - id: population - owner: National Identity Agency - sensitivity: personal - tables: - - access: - aggregate_scope: population:aggregate - metadata_scope: population:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 25 - max_limit: 100 - require_purpose_header: true - id: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: alive - nullable: false - sensitive: true - type: boolean - - name: birth_date - nullable: false - sensitive: true - type: date - - name: family_name - nullable: false - sensitive: true - type: string - - name: given_name - nullable: false - sensitive: true - type: string - - name: identity_status - nullable: false - sensitive: true - type: string - - name: legacy_nid - nullable: true - sensitive: true - type: string - - name: sex - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_NIA_DATABASE_URL - table: - name: population_person - schema: public - type: postgres - title: NIA population records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: nia-population-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/nia-population/relay/relay.yaml b/runtime/registry-projects/local/nia-population/relay/relay.yaml deleted file mode 100644 index 4c0df3e..0000000 --- a/runtime/registry-projects/local/nia-population/relay/relay.yaml +++ /dev/null @@ -1,207 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-esignet - - solmara-evidence - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-nia-population - title: solmara-nia-population governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Governed population attributes used by NIA-owned services. - entities: - - access: - aggregate_scope: population:aggregate - evidence_verification_scope: population:evidence_verification - metadata_scope: population:metadata - read_scope: population:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: legacy_nid - ops: - - eq - - field: uin - ops: - - eq - default_limit: 25 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/esignet-identity-verification - - https://id.registrystack.org/solmara/purpose/child-benefit-review - - https://id.registrystack.org/solmara/purpose/citizen-self-service - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 100 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: - - claims: - - expression: null - name: birthdate - required: true - sensitivity: personal - source_field: birth_date - - expression: null - name: family_name - required: true - sensitivity: direct_identifier - source_field: family_name - - expression: null - name: gender - required: false - sensitivity: personal - source_field: sex - - expression: null - name: given_name - required: true - sensitivity: direct_identifier - source_field: given_name - - expression: null - name: individual_id - required: true - sensitivity: direct_identifier - source_field: uin - - expression: - cel: source.given_name + ' ' + source.family_name - name: name - required: true - sensitivity: direct_identifier - source_field: null - description: Minimal identity attributes for the Solmara eSignet authenticator. - id: solmara-nia-userinfo - purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification - release_conditions: - expression: - cel: source.identity_status == 'active' && source.alive == true - release_scope: population:identity_release - subject: - id_type: national_id - source_field: uin - title: SolmaraID UserInfo - version: v1 - description: Governed population attributes used by NIA-owned services. - fields: - - from: alive - name: alive - sensitive: true - - from: birth_date - name: birth_date - sensitive: true - - from: family_name - name: family_name - sensitive: true - - from: given_name - name: given_name - sensitive: true - - from: identity_status - name: identity_status - sensitive: true - - from: legacy_nid - name: legacy_nid - sensitive: true - - from: sex - name: sex - sensitive: true - - from: uin - name: uin - sensitive: true - name: population - relationships: [] - spatial: null - table: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - title: NIA population records - id: population - owner: National Identity Agency - sensitivity: personal - tables: - - access: - aggregate_scope: population:aggregate - metadata_scope: population:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 25 - max_limit: 100 - require_purpose_header: true - id: materialization_aae79577068250722b57ed81fc12d038a35dc36e179ee2d7efb168fd9942f05d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: alive - nullable: false - sensitive: true - type: boolean - - name: birth_date - nullable: false - sensitive: true - type: date - - name: family_name - nullable: false - sensitive: true - type: string - - name: given_name - nullable: false - sensitive: true - type: string - - name: identity_status - nullable: false - sensitive: true - type: string - - name: legacy_nid - nullable: true - sensitive: true - type: string - - name: sex - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_NIA_DATABASE_URL - table: - name: population_person - schema: public - type: postgres - title: NIA population records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: nia-population-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json deleted file mode 100644 index 5b5d627..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-pension-consultation-pension.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.sipf-pension-consultation.pension","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:39b4002812d2babf57a59302f2fc9680ac05e9439e57e74bc02b8d0dbcea3f0a","id":"relay.sipf-pension-consultation.pension","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:sipf-pension-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["payment_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json deleted file mode 100644 index 8e4fcd1..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.sipf-survivor-consultation.survivor","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:99c1d7d89c1d4abe175bd0693a123c881dff1775da7e78099a5158396de11d58","id":"relay.sipf-survivor-consultation.survivor","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"],"required_scope":"registry:consult:sipf-survivor-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["survivor_eligible"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json deleted file mode 100644 index d7d17b1..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"payment_status":"active"},{"payment_status":"suspended"}],"headers":{},"status":200}}}],"name":"sipf-pension-case-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"payment_status":"active"}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"payment_status":"active"},"headers":{},"status":200}}}],"name":"sipf-pension-case-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-pension-case-no-match","variables":{}}],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json deleted file mode 100644 index 27b99c4..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-sipf-pension-case-by-pensioner","operations":{},"outputs":{"payment_status":{"from":"snapshot.record.payment_status","max_bytes":128,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json deleted file mode 100644 index c62fd58..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-pensioner/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json deleted file mode 100644 index 53ca6c1..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"survivor_eligible":true},{"survivor_eligible":false}],"headers":{},"status":200}}}],"name":"sipf-survivor-case-ambiguous","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"survivor_eligible":true}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"survivor_eligible":true},"headers":{},"status":200}}}],"name":"sipf-survivor-case-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-survivor-case-no-match","variables":{}}],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json deleted file mode 100644 index e246e22..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"solmara-sipf-pension-case-by-survivor","operations":{},"outputs":{"survivor_eligible":{"from":"snapshot.record.survivor_eligible","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json deleted file mode 100644 index 72f6ab7..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/evidence/pension-case-by-survivor/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json deleted file mode 100644 index ded8a18..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-pensioner.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:baac4ff492615cf355beaa403abaad465a23d3552fd6ed3682dce4898e829370"],"minimization":["sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660"],"negative_security":["sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-pensioner","output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json deleted file mode 100644 index 2cd8a30..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/integration-packs/pension-case-by-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:7b0462b4a2d4592cd1aeb8cee70fb54b66a5d9cd7c0aae0d07ee4c160f56d84a"],"minimization":["sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f"],"negative_security":["sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"solmara-sipf-pension-case-by-survivor","output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json deleted file mode 100644 index 94672b6..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-pension-consultation-pension.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:6fe615f0be4b84d28497b7c6111c5426094af8199955977b881e9ebc7c34daa7","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"pensioner_uin","physical_type":"utf8"}},"projection":{"payment_status":"payment_status"}},"table_provider":"pension__materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21"},"profile":{"id":"solmara-sipf-pensions.sipf-pension-consultation.pension","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json b/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json deleted file mode 100644 index b3c1c47..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/artifacts/private-bindings/sipf-survivor-consultation-survivor.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:0aabcdecb0f1cc221e8d4949cdc613c1ecec9c396f0f85b22f24ca8810c92106","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"spouse_uin","physical_type":"utf8"}},"projection":{"survivor_eligible":"survivor_eligible"}},"table_provider":"survivor__materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539"},"profile":{"id":"solmara-sipf-pensions.sipf-survivor-consultation.survivor","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sipf-pensions/relay-consultation/relay.yaml b/runtime/registry-projects/local/sipf-pensions/relay-consultation/relay.yaml deleted file mode 100644 index c3338b5..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay-consultation/relay.yaml +++ /dev/null @@ -1,263 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-sipf-pensions - title: solmara-sipf-pensions governed Registry Relay -consultation: - artifacts: - evidence: - - class: minimization - path: artifacts/evidence/pension-case-by-survivor/minimization.json - sha256: sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f - - class: negative_security - path: artifacts/evidence/pension-case-by-survivor/negative-security.json - sha256: sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7 - - class: minimization - path: artifacts/evidence/pension-case-by-pensioner/minimization.json - sha256: sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660 - - class: conformance - path: artifacts/evidence/pension-case-by-survivor/conformance.json - sha256: sha256:7b0462b4a2d4592cd1aeb8cee70fb54b66a5d9cd7c0aae0d07ee4c160f56d84a - - class: conformance - path: artifacts/evidence/pension-case-by-pensioner/conformance.json - sha256: sha256:baac4ff492615cf355beaa403abaad465a23d3552fd6ed3682dce4898e829370 - - class: negative_security - path: artifacts/evidence/pension-case-by-pensioner/negative-security.json - sha256: sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d - integration_packs: - - hash: sha256:6fe615f0be4b84d28497b7c6111c5426094af8199955977b881e9ebc7c34daa7 - path: artifacts/integration-packs/pension-case-by-pensioner.json - sha256: sha256:fc850bad2f6cec2f1b514c3005b92588c62c3f95cefc9d8419061d27cecd649a - - hash: sha256:0aabcdecb0f1cc221e8d4949cdc613c1ecec9c396f0f85b22f24ca8810c92106 - path: artifacts/integration-packs/pension-case-by-survivor.json - sha256: sha256:fac45aff1350bdfc5ec7f7e1741e02c02f74d5ac22505fa65d5b769a8a0697f8 - private_bindings: - - hash: sha256:4ed2016d95b43505d541bf2c91fa0ea952879457e5fce8c66fcd66a178ce57b4 - path: artifacts/private-bindings/sipf-pension-consultation-pension.json - sha256: sha256:872343722e004f2c420535877d15f50477726533c999bcd41d3529513654903d - - hash: sha256:4ff43cbb6fe5281a24a498a1cdb7bbd552ea3868c6aee27872df41fc11f37e2b - path: artifacts/private-bindings/sipf-survivor-consultation-survivor.json - sha256: sha256:f05668bc4724b8acabeadcf95eac837405da292196d1fb8d0e84d5f871bf683c - public_contracts: - - hash: sha256:67a23c52de56b492a69784ec0a57bde7f6772f5a8cbe3f7e450d5bf86a52fd13 - path: artifacts/consultation-contracts/sipf-pension-consultation-pension.json - sha256: sha256:46bdc7b1bf6fb6a2b83c5697bdc690831025b8da1c190ead0471769707c1f30a - - hash: sha256:0b57c5799c75c6a6717f35b045d76189a436aa124157faa070318a2f8a405253 - path: artifacts/consultation-contracts/sipf-survivor-consultation-survivor.json - sha256: sha256:c0b833e6687c0af54df01acf34eda3e39eaef0ff42de2119bd5773e22e547b54 - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 4444991830243562918 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 6117019989203477116 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited pension facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: pension:aggregate - evidence_verification_scope: pension:evidence_verification - metadata_scope: pension:metadata - read_scope: pension:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: pensioner_uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/pension-payment-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited pension facts used by the Solmara Evidence service. - fields: - - from: payment_status - name: payment_status - sensitive: true - - from: pensioner_uin - name: pensioner_uin - sensitive: true - name: pension - relationships: [] - spatial: null - table: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - title: SIPF pension payment records - id: pension - owner: Social Insurance and Pension Fund - sensitivity: personal - tables: - - access: - aggregate_scope: pension:aggregate - metadata_scope: pension:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - materialization: snapshot - primary_key: pensioner_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: payment_status - nullable: false - sensitive: true - type: string - - name: pensioner_uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_pension_payment - schema: public - type: postgres - title: SIPF pension payment records - update_frequency: continuous -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited survivor facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: survivor:aggregate - evidence_verification_scope: survivor:evidence_verification - metadata_scope: survivor:metadata - read_scope: survivor:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: spouse_uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited survivor facts used by the Solmara Evidence service. - fields: - - from: spouse_uin - name: spouse_uin - sensitive: true - - from: survivor_eligible - name: survivor_eligible - sensitive: true - name: survivor - relationships: [] - spatial: null - table: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - title: SIPF survivor benefit records - id: survivor - owner: Social Insurance and Pension Fund - sensitivity: personal - tables: - - access: - aggregate_scope: survivor:aggregate - metadata_scope: survivor:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - materialization: snapshot - primary_key: spouse_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: spouse_uin - nullable: false - sensitive: true - type: string - - name: survivor_eligible - nullable: false - sensitive: true - type: boolean - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_survivor_benefit - schema: public - type: postgres - title: SIPF survivor benefit records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: sipf-pensions-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/sipf-pensions/relay/relay.yaml b/runtime/registry-projects/local/sipf-pensions/relay/relay.yaml deleted file mode 100644 index b96a6cf..0000000 --- a/runtime/registry-projects/local/sipf-pensions/relay/relay.yaml +++ /dev/null @@ -1,203 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-evidence - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-sipf-pensions - title: solmara-sipf-pensions governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited pension facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: pension:aggregate - evidence_verification_scope: pension:evidence_verification - metadata_scope: pension:metadata - read_scope: pension:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: pensioner_uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/pension-payment-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited pension facts used by the Solmara Evidence service. - fields: - - from: payment_status - name: payment_status - sensitive: true - - from: pensioner_uin - name: pensioner_uin - sensitive: true - name: pension - relationships: [] - spatial: null - table: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - title: SIPF pension payment records - id: pension - owner: Social Insurance and Pension Fund - sensitivity: personal - tables: - - access: - aggregate_scope: pension:aggregate - metadata_scope: pension:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_e378f482aa5f7ec6ca19d81f76802de996046719bcf2976f033a9ff5c8dedd21 - materialization: snapshot - primary_key: pensioner_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: payment_status - nullable: false - sensitive: true - type: string - - name: pensioner_uin - nullable: false - sensitive: true - type: string - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_pension_payment - schema: public - type: postgres - title: SIPF pension payment records - update_frequency: continuous -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited survivor facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: survivor:aggregate - evidence_verification_scope: survivor:evidence_verification - metadata_scope: survivor:metadata - read_scope: survivor:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: spouse_uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/survivor-benefit-determination - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited survivor facts used by the Solmara Evidence service. - fields: - - from: spouse_uin - name: spouse_uin - sensitive: true - - from: survivor_eligible - name: survivor_eligible - sensitive: true - name: survivor - relationships: [] - spatial: null - table: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - title: SIPF survivor benefit records - id: survivor - owner: Social Insurance and Pension Fund - sensitivity: personal - tables: - - access: - aggregate_scope: survivor:aggregate - metadata_scope: survivor:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_c346e9a9e571c9cff9cc50780a950c7e58e5cd048091b530157c5cb540d9d539 - materialization: snapshot - primary_key: spouse_uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: spouse_uin - nullable: false - sensitive: true - type: string - - name: survivor_eligible - nullable: false - sensitive: true - type: boolean - strict: true - source: - connection_env: SOLMARA_SIPF_DATABASE_URL - table: - name: sipf_survivor_benefit - schema: public - type: postgres - title: SIPF survivor benefit records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: sipf-pensions-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json deleted file mode 100644 index 769fe52..0000000 --- a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/consultation-contracts/sro-household-consultation-household.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sro-social.sro-household-consultation.household","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:f6d2a35ee331166669b6ae0eecd3ad084de203e4da00827a9368a9fb70f5f180","id":"relay.sro-household-consultation.household","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:sro-household-consultation","workload":"solmara-evidence"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sro-social.child-benefit-household-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["poverty_band"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":2678400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json deleted file mode 100644 index 7b8ebae..0000000 --- a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/conformance.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"error":null,"outcome":"match","outputs":{"poverty_band":"priority"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"poverty_band":"priority"},"headers":{},"status":200}}}],"name":"child-benefit-household-match","variables":{}},{"classification":"synthetic","expect":{"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"child-benefit-household-no-match","variables":{}}],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json deleted file mode 100644 index fc5378a..0000000 --- a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/minimization.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"minimization","integration":"child-benefit-household-by-uin","operations":{},"outputs":{"poverty_band":{"from":"snapshot.record.poverty_band","max_bytes":64,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json deleted file mode 100644 index 9037e0a..0000000 --- a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/evidence/child-benefit-household-by-uin/negative-security.json +++ /dev/null @@ -1 +0,0 @@ -{"class":"negative_security","fixtures":[],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json deleted file mode 100644 index 257709e..0000000 --- a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/integration-packs/child-benefit-household-by-uin.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"solmara-sro-social.child-benefit-household-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:a4d045756396f4c69a9607b88163f42efcb7baf55f46d7049b1555e850ed0070"],"minimization":["sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0"],"negative_security":["sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"child-benefit-household-by-uin","output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":2678400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json b/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json deleted file mode 100644 index f598671..0000000 --- a/runtime/registry-projects/local/sro-social/relay-consultation/artifacts/private-bindings/sro-household-consultation-household.json +++ /dev/null @@ -1 +0,0 @@ -{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:47547a81285fa1f655d534d521062e47f86bff6c2a74cb9b3637fd51f2943441","id":"solmara-sro-social.child-benefit-household-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"poverty_band":"poverty_band"}},"table_provider":"sro_child__materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d"},"profile":{"id":"solmara-sro-social.sro-household-consultation.household","version":"1"},"registry_instance":"solmara-sro-social","source_instance":"materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d","tenant":"solmara-sro-social"} \ No newline at end of file diff --git a/runtime/registry-projects/local/sro-social/relay-consultation/relay.yaml b/runtime/registry-projects/local/sro-social/relay-consultation/relay.yaml deleted file mode 100644 index 185e879..0000000 --- a/runtime/registry-projects/local/sro-social/relay-consultation/relay.yaml +++ /dev/null @@ -1,159 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-consultation - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-sro-social - title: solmara-sro-social governed Registry Relay -consultation: - artifacts: - evidence: - - class: negative_security - path: artifacts/evidence/child-benefit-household-by-uin/negative-security.json - sha256: sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95 - - class: minimization - path: artifacts/evidence/child-benefit-household-by-uin/minimization.json - sha256: sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0 - - class: conformance - path: artifacts/evidence/child-benefit-household-by-uin/conformance.json - sha256: sha256:a4d045756396f4c69a9607b88163f42efcb7baf55f46d7049b1555e850ed0070 - integration_packs: - - hash: sha256:47547a81285fa1f655d534d521062e47f86bff6c2a74cb9b3637fd51f2943441 - path: artifacts/integration-packs/child-benefit-household-by-uin.json - sha256: sha256:b1cbeacef1a39cb0d4cb03a4adf1061b91532e04acf92abf1e06f60c0b84f1f0 - private_bindings: - - hash: sha256:77a0bae801d725304c75e307f26f53753fb50f9cd72fd9f9bc501da37bfd1064 - path: artifacts/private-bindings/sro-household-consultation-household.json - sha256: sha256:2440ac8ae4c483d106e13af61a8b072509d2452b13a3a36f192b97421ece0bf1 - public_contracts: - - hash: sha256:2009a7b48ceb64aaf24a028af795a3e2fd399fad618db5fd6b1b65cbb8bb281e - path: artifacts/consultation-contracts/sro-household-consultation-household.json - sha256: sha256:662eb6af3e5c5308eedb27ba57575399d2377dbf65d2dd9041520ee21bd8088b - rhai_scripts: [] - audit_pseudonym_materials: - - key_id: epoch-1 - source: - name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 - provider: environment - authorized_workload: - audience: registry-relay - client_claim_selector: azp - client_value: solmara-consultation - principal_id: solmara-consultation - source_credentials: [] - state_plane: - audit_pseudonym_keyring_lock_key: 368637434756268187 - chain_key_epoch_id: project-consultation-chain-1 - database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL - root_certificate_path: /etc/solmara/postgres/root.crt - serving_fence_lock_key: 2694379094216255332 -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited household facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: sro_child:aggregate - evidence_verification_scope: sro_child:evidence_verification - metadata_scope: sro_child:metadata - read_scope: sro_child:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited household facts used by the Solmara Evidence service. - fields: - - from: poverty_band - name: poverty_band - sensitive: true - - from: uin - name: uin - sensitive: true - name: sro_child - relationships: [] - spatial: null - table: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - title: SRO household records - id: sro_child - owner: Social Registry Office - sensitivity: personal - tables: - - access: - aggregate_scope: sro_child:aggregate - metadata_scope: sro_child:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: poverty_band - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv - type: file - title: SRO household records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: sro-social-relay-consultation -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/runtime/registry-projects/local/sro-social/relay/relay.yaml b/runtime/registry-projects/local/sro-social/relay/relay.yaml deleted file mode 100644 index 9e2d8c0..0000000 --- a/runtime/registry-projects/local/sro-social/relay/relay.yaml +++ /dev/null @@ -1,117 +0,0 @@ -audit: - hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET - path: /var/lib/registry/audit/audit.jsonl - sink: file -auth: - mode: oidc - oidc: - allow_dev_insecure_fetch_urls: true - allowed_clients: - - solmara-evidence - audiences: - - registry-relay - issuer: http://127.0.0.1:8090 - jwks_url: http://127.0.0.1:8090/.well-known/jwks.json -catalog: - base_url: http://127.0.0.1:8080 - publisher: solmara-sro-social - title: solmara-sro-social governed Registry Relay -datasets: -- access_rights: restricted - aggregates: [] - conforms_to: [] - defaults: - materialization: snapshot - refresh: - interval: 1m - mode: interval - description: Purpose-limited household facts used by the Solmara Evidence service. - entities: - - access: - aggregate_scope: sro_child:aggregate - evidence_verification_scope: sro_child:evidence_verification - metadata_scope: sro_child:metadata - read_scope: sro_child:rows - aggregates: [] - api: - allowed_expansions: [] - allowed_filters: - - field: uin - ops: - - eq - default_limit: 2 - governed_policy: - allowed_assurance: [] - permitted_jurisdictions: [] - permitted_purposes: - - https://id.registrystack.org/solmara/purpose/child-benefit-review - redaction_fields: [] - require_consent: false - require_legal_basis: false - trusted_context: {} - max_limit: 2 - require_purpose_header: true - required_filter_bindings: [] - required_filters: [] - attribute_release_profiles: [] - description: Purpose-limited household facts used by the Solmara Evidence service. - fields: - - from: poverty_band - name: poverty_band - sensitive: true - - from: uin - name: uin - sensitive: true - name: sro_child - relationships: [] - spatial: null - table: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - title: SRO household records - id: sro_child - owner: Social Registry Office - sensitivity: personal - tables: - - access: - aggregate_scope: sro_child:aggregate - metadata_scope: sro_child:metadata - aggregates: [] - api: - allowed_filters: [] - default_limit: 2 - max_limit: 2 - require_purpose_header: true - id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d - materialization: snapshot - primary_key: uin - refresh: - interval: 1m - mode: interval - schema: - fields: - - name: poverty_band - nullable: false - sensitive: true - type: string - - name: uin - nullable: false - sensitive: true - type: string - strict: true - source: - format: - csv: - delimiter: null - header_row: 1 - quote: null - path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv - type: file - title: SRO household records - update_frequency: continuous -deployment: - profile: local -instance: - environment: local - id: sro-social-relay -server: - bind: 0.0.0.0:8080 -standards: {} diff --git a/scenario-runner/Dockerfile b/scenario-runner/Dockerfile index 411d024..9f72ba0 100644 --- a/scenario-runner/Dockerfile +++ b/scenario-runner/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-alpine +FROM python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df WORKDIR /app ENV PYTHONUNBUFFERED=1 @@ -9,7 +9,7 @@ ENV SCENARIO_RUNNER_PORT=8080 # Runtime dependencies (currently just cryptography, for holder proof signing) # are declared in the repo-root uv.lock. Export and install the exact locked # versions rather than letting pip resolve them independently. -COPY --from=ghcr.io/astral-sh/uv:0.11.16 /uv /usr/local/bin/uv +COPY --from=ghcr.io/astral-sh/uv@sha256:440fd6477af86a2f1b38080c539f1672cd22acb1b1a47e321dba5158ab08864d /uv /usr/local/bin/uv COPY pyproject.toml uv.lock ./ RUN uv export --frozen --no-dev --no-hashes -o requirements.txt \ && pip install --no-cache-dir -r requirements.txt \ @@ -17,7 +17,6 @@ RUN uv export --frozen --no-dev --no-hashes -o requirements.txt \ COPY scenarios ./scenarios COPY scenario-runner ./scenario-runner -COPY scripts/relay_workload_identity_agent.py ./scripts/relay_workload_identity_agent.py EXPOSE 8080 CMD ["python", "scenario-runner/server.py"] diff --git a/scenario-runner/child_benefit_federator.py b/scenario-runner/child_benefit_federator.py index f08548f..c1151ea 100644 --- a/scenario-runner/child_benefit_federator.py +++ b/scenario-runner/child_benefit_federator.py @@ -17,7 +17,7 @@ from urllib.parse import urlparse from scenarios.common import PURPOSES, evidence_body, evidence_headers, http_json, normalized_evidence_result -from scenarios.service_config import requirement_id, service_token, service_url +from scenarios.service_config import authority_service_id, requirement_config, requirement_id, service_token, service_url API_VERSION = "solmara-child-benefit-evidence/v2" @@ -26,10 +26,10 @@ CHILD_PURPOSE = PURPOSES["child_benefit"] MAX_REQUEST_BODY_BYTES = 64 * 1024 SOURCE_ROUTES: tuple[dict[str, Any], ...] = ( - {"client_id": "cra-child-benefit", "authority": "Civil Registration Authority", "claims": ("birth-is-registered", "child-age-under-5")}, - {"client_id": "nia-child-benefit", "authority": "National Identity Agency", "claims": ("population-record-active",)}, - {"client_id": "sro-child-benefit", "authority": "Social Registry Office", "claims": ("household-below-poverty-threshold",)}, - {"client_id": "programme-child-benefit", "authority": "MoSD Programme MIS", "claims": ("not-already-enrolled",)}, + {"client_id": "cra-child-benefit", "claims": ("birth-is-registered", "child-age-under-5")}, + {"client_id": "nia-child-benefit", "claims": ("population-record-active",)}, + {"client_id": "sro-child-benefit", "claims": ("household-below-poverty-threshold",)}, + {"client_id": "programme-child-benefit", "claims": ("not-already-enrolled",)}, ) CLAIM_ROUTES = {claim: route for route in SOURCE_ROUTES for claim in route["claims"]} @@ -45,7 +45,7 @@ def do_GET(self) -> None: if path == "/v1/claims": if not self.require_token(): return - self.write_json({"schema_version": API_VERSION, "claims": [{"claim_id": claim, "authority": route["authority"]} for claim, route in CLAIM_ROUTES.items()]}) + self.write_json({"schema_version": API_VERSION, "claims": [{"claim_id": claim, "authority": requirement_config(route["client_id"])["name"]} for claim, route in CLAIM_ROUTES.items()]}) return self.write_problem(HTTPStatus.NOT_FOUND, "not_found", "No such application route.") @@ -55,13 +55,13 @@ def do_POST(self) -> None: return if not self.require_token(): return - purpose = self.headers.get("Data-Purpose", "") - if purpose != CHILD_PURPOSE: - self.write_problem(HTTPStatus.FORBIDDEN, "purpose_not_permitted", "Only child-benefit-review is permitted.") - return body = self.read_body() if body is None: return + purpose = body.get("purpose") + if purpose != CHILD_PURPOSE: + self.write_problem(HTTPStatus.FORBIDDEN, "purpose_not_permitted", "Only child-benefit-review is permitted.") + return subject = subject_id(body) claims = requested_claims(body) if not subject or not claims or len(claims) != len(set(claims)) or any(claim not in CLAIM_ROUTES for claim in claims): @@ -81,17 +81,22 @@ def do_POST(self) -> None: url = service_url(route["client_id"]) headers = evidence_headers(token) request = evidence_body(subject, requirement_id(route["client_id"]), purpose) - response = normalized_evidence_result(http_json("POST", url, headers, request)) + response = normalized_evidence_result( + http_json("POST", url, headers, request), + request=request, + service_id=route["client_id"], + ) if response.status is None or not 200 <= response.status < 300: - self.write_problem(HTTPStatus.BAD_GATEWAY, "evidence_unavailable", f"{route['authority']} evidence was unavailable.") + self.write_problem(HTTPStatus.BAD_GATEWAY, "evidence_unavailable", "A required Evidence assertion was unavailable.") return returned = {item["claim_id"]: item for item in response.body.get("results", [])} if any(claim not in returned for claim in requested): - self.write_problem(HTTPStatus.BAD_GATEWAY, "evidence_incomplete", f"{route['authority']} omitted a requested concept.") + self.write_problem(HTTPStatus.BAD_GATEWAY, "evidence_unavailable", "A required Evidence assertion was unavailable.") return - results.extend({**returned[claim], "authority": route["authority"]} for claim in requested) + presentation = response.body["presentation"] + results.extend({**returned[claim], "presentation": presentation} for claim in requested) signed_evidence.append(response.body.get("signed_evidence")) - trace.append({"authority": route["authority"], "service_id": "registry-evidence", "requirement": requirement_id(route["client_id"]), "status": response.status}) + trace.append({"authority": presentation["authority"], "service_id": authority_service_id(route["client_id"]), "issuer": presentation["issuer"], "provider": presentation["provider"], "source": presentation["source"], "status": response.status}) self.write_json({"schema_version": API_VERSION, "orchestration": {"service_id": FEDERATOR_SERVICE_ID, "decision": "not_composed"}, "purpose": purpose, "target": {"type": "Person", "binding": "withheld"}, "results": results, "signed_evidence": signed_evidence, "source_trace": trace}) def require_token(self) -> bool: diff --git a/scenario-runner/test_server.py b/scenario-runner/test_server.py index 7d7250d..8fd649f 100644 --- a/scenario-runner/test_server.py +++ b/scenario-runner/test_server.py @@ -14,35 +14,91 @@ import urllib.error import urllib.parse import urllib.request +import uuid from http.server import ThreadingHTTPServer from pathlib import Path from typing import Any +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature + ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scenario-runner")) import child_benefit_federator # noqa: E402 from server import ScenarioRunnerHandler # noqa: E402 -from scenarios import child_benefit, citizen, common, farmer_voucher, pension_survivor # noqa: E402 +from scenarios import child_benefit, citizen, common, pension_survivor # noqa: E402 from scenarios.common import StepHttpResult # noqa: E402 +from scenarios import service_config # noqa: E402 def b64url_decode(segment: str) -> bytes: return base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4)) -def signed_evidence(values: list[tuple[str, Any]]) -> dict[str, str]: +AUTHORITY_KEYS = {authority: ec.generate_private_key(ec.SECP256R1()) for authority in service_config.AUTHORITY_DIRECTORY} + + +def public_jwk(private_key) -> dict[str, str]: + numbers = private_key.public_key().public_numbers() + jwk = { + "kty": "EC", + "alg": "ES256", + "crv": "P-256", + "x": common.b64url_nopad(numbers.x.to_bytes(32, "big")), + "y": common.b64url_nopad(numbers.y.to_bytes(32, "big")), + } + jwk["kid"] = common._jwk_thumbprint(jwk) + return jwk + + +def install_authority_keys() -> None: + common._JWKS_CACHE.clear() + for authority, private_key in AUTHORITY_KEYS.items(): + service_id = next(key for key, value in service_config.REQUIREMENT_DIRECTORY.items() if value["authority"] == authority) + url = service_config.service_url(service_id, "/.well-known/evidence/jwks.json") + common._JWKS_CACHE[url] = (time.monotonic() + 300, (public_jwk(private_key),)) + + +def signed_evidence(service_id: str, request: dict[str, Any], values: list[tuple[str, Any]], *, private_key=None, claims: dict[str, Any] | None = None) -> dict[str, str]: + config = service_config.requirement_config(service_id) + private_key = private_key or AUTHORITY_KEYS[config["authority"]] + jwk = public_jwk(private_key) + now = datetime_now = time.time() payload = { + "schema": "registry.assertion-evidence/v1", + "assuranceProfile": "production", + "subjectBinding": "audience-scoped", + "requestNonce": request["requestNonce"], + "id": f"urn:uuid:{uuid.uuid4()}", + "type": "Evidence", + "supportsRequirement": request["requirement"], + "isConformantTo": config["evidence_type"], + "issuedBy": config["issuer"], + "providedBy": config["provider"], + "issuedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(datetime_now)), + "observedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(datetime_now)), + "validUntil": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now + 300)), + "purpose": request["purpose"], + "audience": common.EVIDENCE_AUDIENCE, + "configurationRevision": f"sha256:{'a' * 64}", + "subjects": [{"role": "subject", "binding": f"urn:evidence:subject:v1_{'A' * 43}"}], "supportedValues": [ {"providesValueFor": f"https://id.registrystack.org/solmara/concept/{name}", "value": value} for name, value in values - ] + ], } + payload.update(claims or {}) + protected = common.b64url_nopad(json.dumps({**common.EVIDENCE_JWS_HEADER, "kid": jwk["kid"]}, separators=(",", ":")).encode()) + payload_segment = common.b64url_nopad(json.dumps(payload, separators=(",", ":")).encode()) + der_signature = private_key.sign(f"{protected}.{payload_segment}".encode(), ec.ECDSA(hashes.SHA256())) + r, s = decode_dss_signature(der_signature) return { - "protected": common.b64url_nopad(b"{}"), - "payload": common.b64url_nopad(json.dumps(payload).encode()), - "signature": common.b64url_nopad(b"test-signature"), + "protected": protected, + "payload": payload_segment, + "signature": common.b64url_nopad(r.to_bytes(32, "big") + s.to_bytes(32, "big")), } @@ -78,8 +134,8 @@ def test_detail_previews_evidence_requirements(self) -> None: self.assertEqual(preview["method"], "MULTI") self.assertEqual(len(preview["requests"]), 2) self.assertTrue(all(item["url"].endswith("/v1/evidence") for item in preview["requests"])) - requirements = {item["body"]["requirement"] for item in preview["requests"]} - self.assertEqual(requirements, {citizen.requirement_id(client) for client in citizen.CLIENTS}) + self.assertTrue(all("body" not in item for item in preview["requests"])) + self.assertNotIn("2300018263", json.dumps(preview)) def test_missing_mint_credentials_is_stable_and_safe(self) -> None: first = self.post_json("/v1/scenarios/citizen-self-service/steps/positive/run", {}) @@ -90,13 +146,14 @@ def test_missing_mint_credentials_is_stable_and_safe(self) -> None: self.assertIn("Bearer [runtime token hidden]", serialized) self.assertNotIn("Bearer runtime-token", serialized) - def test_purpose_overrides_reach_evidence_request_bodies(self) -> None: + def test_purpose_overrides_are_not_exposed_in_request_traces(self) -> None: purpose = "pension-payment-review" payload = self.post_json( "/v1/scenarios/farmer-climate-smart-voucher/steps/positive/run", {"config": {"purpose_override": purpose}}, ) - self.assertEqual(payload["result"]["request_source"]["body"]["purpose"], purpose) + self.assertNotIn("body", payload["result"]["request_source"]) + self.assertNotIn(purpose, json.dumps(payload["result"]["request_source"])) def test_denial_steps_ignore_purpose_overrides(self) -> None: citizen_result = self.post_json( @@ -108,7 +165,7 @@ def test_denial_steps_ignore_purpose_overrides(self) -> None: {"config": {"purpose_override": common.PURPOSES["livestock"]}}, )["result"] self.assertEqual(citizen_result["request_source"]["purpose"], "unsupported-demo-purpose") - self.assertEqual(farmer_result["request_source"]["body"]["purpose"], common.PURPOSES["voucher"]) + self.assertNotIn("body", farmer_result["request_source"]) def test_unknown_scenario_returns_404(self) -> None: with self.assertRaises(urllib.error.HTTPError) as raised: @@ -132,8 +189,12 @@ def post_json(self, path: str, body: dict[str, Any]) -> dict[str, Any]: class MintAndEvidenceContractTest(unittest.TestCase): + def setUp(self) -> None: + install_authority_keys() + def tearDown(self) -> None: common._TOKEN_CACHE = ("", 0.0) + common._JWKS_CACHE.clear() for name in ( "SOLMARA_EVIDENCE_ACCESS_TOKEN", "SOLMARA_MINT_URL", @@ -151,21 +212,205 @@ def test_evidence_body_uses_bounded_selector_and_unique_nonce(self) -> None: self.assertNotEqual(first["requestNonce"], second["requestNonce"]) def test_signed_evidence_is_preserved_and_normalized(self) -> None: - signed = signed_evidence([("person-is-deceased", True), ("pension-payment-active", False)]) - result = common.normalized_evidence_result(StepHttpResult(200, signed, {"content-type": common.EVIDENCE_JWS_MEDIA_TYPE})) - self.assertEqual([entry["satisfied"] for entry in result.body["results"]], [True, False]) + request = common.evidence_body("2300109568", service_config.requirement_id("cra-pension"), common.PURPOSES["pension_payment"]) + signed = signed_evidence("cra-pension", request, [("person-is-deceased", True)]) + result = common.normalized_evidence_result(StepHttpResult(200, signed, {"content-type": common.EVIDENCE_JWS_MEDIA_TYPE}), request=request, service_id="cra-pension") + self.assertEqual([entry["satisfied"] for entry in result.body["results"]], [True]) self.assertEqual(result.body["signed_evidence"], signed) + self.assertEqual(result.body["presentation"]["source"], "Relay lookup") + + def test_safe_projection_omits_jws_and_assertion_payloads(self) -> None: + presentation = { + "authority": "Civil Registration Authority", + "issuer": "did:web:id.registrystack.org:solmara:authority:cra", + "provider": "https://id.registrystack.org/solmara/evidence/cra", + "source": "Relay lookup", + } + projection = common.safe_evidence_projection( + StepHttpResult( + 200, + { + "results": [ + { + "claim_id": "person-is-deceased", + "concept_id": "https://id.registrystack.org/solmara/concept/person-is-deceased", + "satisfied": True, + "value": True, + "presentation": presentation, + } + ], + "presentation": presentation, + "assertion": {"secret": "must-not-leave-server"}, + "signed_evidence": {"payload": "must-not-leave-server"}, + }, + {}, + ) + ) + self.assertEqual(projection["presentations"], [presentation]) + self.assertEqual(projection["results"][0]["value"], True) + rendered = json.dumps(projection) + self.assertNotIn("must-not-leave-server", rendered) + self.assertNotIn("assertion", rendered) + self.assertNotIn("signed_evidence", rendered) + + def test_safe_projection_attributes_single_authority_results(self) -> None: + presentation = { + "authority": "National Agricultural Data Institute", + "issuer": "did:web:id.registrystack.org:solmara:authority:nagdi", + "provider": "https://id.registrystack.org/solmara/evidence/nagdi", + "source": "Relay lookup", + } + projection = common.safe_evidence_projection( + StepHttpResult( + 200, + { + "results": [ + { + "claim_id": "farmer-registered", + "concept_id": "https://id.registrystack.org/solmara/concept/farmer-registered", + "satisfied": True, + "value": True, + } + ], + "presentation": presentation, + }, + {}, + ) + ) + self.assertEqual(projection["results"][0]["presentation"], presentation) + + def test_child_scenario_deduplicates_authority_presentations(self) -> None: + presentation = { + "authority": "Civil Registration Authority", + "issuer": "did:web:id.registrystack.org:solmara:authority:cra", + "provider": "https://id.registrystack.org/solmara/evidence/cra", + "source": "immutable extract", + } + original = child_benefit.http_json + child_benefit.http_json = lambda *args, **kwargs: StepHttpResult( + 200, + { + "results": [ + {"claim_id": "birth-is-registered", "satisfied": True, "presentation": presentation}, + {"claim_id": "child-age-under-5", "satisfied": True, "presentation": presentation}, + ], + "source_trace": [], + }, + {}, + ) + try: + os.environ[service_config.service_token_env(child_benefit.SERVICE_ID)] = "collector-token" + result = child_benefit.run_step({}, "positive") + finally: + child_benefit.http_json = original + os.environ.pop(service_config.service_token_env(child_benefit.SERVICE_ID), None) + self.assertEqual(result["presentations"], [presentation]) + + def test_cross_authority_key_is_rejected(self) -> None: + request = common.evidence_body("2300010248", service_config.requirement_id("cra-child-benefit"), common.PURPOSES["child_benefit"]) + signed = signed_evidence("cra-child-benefit", request, [("birth-is-registered", True), ("child-age-under-5", True)], private_key=AUTHORITY_KEYS["nia"]) + result = common.normalized_evidence_result(StepHttpResult(200, signed, {"content-type": common.EVIDENCE_JWS_MEDIA_TYPE}), request=request, service_id="cra-child-benefit") + self.assertEqual(result.status, 502) + self.assertEqual(common.source_response(result), {"status": 502, "code": "assertion_verification_failed"}) + + def test_signature_tamper_is_rejected(self) -> None: + request = common.evidence_body("2300010248", service_config.requirement_id("nia-child-benefit"), common.PURPOSES["child_benefit"]) + signed = signed_evidence("nia-child-benefit", request, [("population-record-active", True)]) + signed["signature"] = ("A" if signed["signature"][0] != "A" else "B") + signed["signature"][1:] + result = common.normalized_evidence_result(StepHttpResult(200, signed, {"content-type": common.EVIDENCE_JWS_MEDIA_TYPE}), request=request, service_id="nia-child-benefit") + self.assertEqual(result.status, 502) + self.assertNotIn("signature", json.dumps(common.source_response(result))) + + def test_claim_mismatch_is_rejected(self) -> None: + request = common.evidence_body("2300010248", service_config.requirement_id("sro-child-benefit"), common.PURPOSES["child_benefit"]) + signed = signed_evidence("sro-child-benefit", request, [("household-below-poverty-threshold", True)], claims={"requestNonce": "B" * 43}) + result = common.normalized_evidence_result(StepHttpResult(200, signed, {"content-type": common.EVIDENCE_JWS_MEDIA_TYPE}), request=request, service_id="sro-child-benefit") + self.assertEqual(result.status, 502) + self.assertNotIn(request["requestNonce"], json.dumps(result.body)) + + def test_relay_assertion_above_five_minutes_is_rejected(self) -> None: + request = common.evidence_body("2300109568", service_config.requirement_id("cra-pension"), common.PURPOSES["pension_payment"]) + issued = int(time.time()) + claims = { + "issuedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(issued)), + "observedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(issued)), + "validUntil": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(issued + 301)), + } + signed = signed_evidence("cra-pension", request, [("person-is-deceased", True)], claims=claims) + result = common.normalized_evidence_result(StepHttpResult(200, signed, {"content-type": common.EVIDENCE_JWS_MEDIA_TYPE}), request=request, service_id="cra-pension") + self.assertEqual(result.status, 502) + + def test_request_and_problem_traces_are_value_free(self) -> None: + selector = "2300010248" + request = common.evidence_body(selector, service_config.requirement_id("cra-child-benefit"), common.PURPOSES["child_benefit"]) + trace = common.request_source("POST", f"https://cra.example/v1/evidence?uin={selector}", {"Authorization": "Bearer secret", "X-Selector": selector}, request) + response = common.source_response(StepHttpResult(403, {"detail": selector, "source": "private"}, {}, "canary-error")) + rendered = json.dumps({"request": trace, "response": response}) + self.assertNotIn(selector, rendered) + self.assertNotIn("private", rendered) + self.assertNotIn("canary-error", rendered) + + +class AuthorityRoutingTest(unittest.TestCase): + def tearDown(self) -> None: + common._TOKEN_CACHE = ("", 0.0) + for config in service_config.AUTHORITY_DIRECTORY.values(): + os.environ.pop(config["env"], None) + for name in ( + "SOLMARA_EVIDENCE_URL", + "SOLMARA_MINT_URL", + "SOLMARA_MINT_ASSERTION_AUDIENCE", + "SOLMARA_EVIDENCE_CLIENT_ID", + "SOLMARA_EVIDENCE_CLIENT_KEY", + ): + os.environ.pop(name, None) + + def test_all_requirement_aliases_route_to_their_authority_cell(self) -> None: + expected_hosts = { + "cra": "cra-evidence.solmara.registrystack.org", + "nia": "nia-evidence.solmara.registrystack.org", + "sro": "sro-evidence.solmara.registrystack.org", + "mosd-programme": "mosd-programme-evidence.solmara.registrystack.org", + "sipf": "sipf-evidence.solmara.registrystack.org", + "nagdi": "nagdi-evidence.solmara.registrystack.org", + } + self.assertEqual(len(service_config.REQUIREMENT_DIRECTORY), 11) + for service_id, route in service_config.REQUIREMENT_DIRECTORY.items(): + with self.subTest(service_id=service_id): + self.assertEqual(urllib.parse.urlsplit(service_config.service_url(service_id)).hostname, expected_hosts[route["authority"]]) + self.assertEqual(service_config.authority_service_id(service_id), f"{route['authority']}-evidence") + + def test_source_labels_match_the_requirement_acquisition_path(self) -> None: + immutable = {"cra-child-benefit", "nia-child-benefit", "nia-citizen", "sro-child-benefit"} + self.assertEqual( + {service_id for service_id, route in service_config.REQUIREMENT_DIRECTORY.items() if route["source"] == "immutable extract"}, + immutable, + ) + self.assertTrue(all(route["maximum_validity_seconds"] == (3600 if service_id in immutable else 300) for service_id, route in service_config.REQUIREMENT_DIRECTORY.items())) + + def test_authority_override_is_local_and_singleton_setting_is_ignored(self) -> None: + os.environ["SOLMARA_CRA_EVIDENCE_URL"] = "http://cra-evidence:8080" + os.environ["SOLMARA_EVIDENCE_URL"] = "https://must-not-be-used.example" + self.assertEqual(service_config.service_url("cra-citizen"), "http://cra-evidence:8080/v1/evidence") + self.assertEqual(service_config.service_url("nia-citizen"), "https://nia-evidence.solmara.registrystack.org/v1/evidence") def test_private_key_jwt_and_mint_token_request(self) -> None: try: - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import ec, utils except ModuleNotFoundError: self.skipTest("cryptography is not installed") - private_key = Ed25519PrivateKey.generate() - raw = private_key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption()) - jwk = {"kty": "OKP", "crv": "Ed25519", "kid": "scenario-client-1", "d": common.b64url_nopad(raw)} + private_key = ec.generate_private_key(ec.SECP256R1()) + numbers = private_key.private_numbers() + public = numbers.public_numbers + jwk = { + "kty": "EC", "crv": "P-256", "alg": "ES256", + "kid": "A" * 43, + "x": common.b64url_nopad(public.x.to_bytes(32, "big")), + "y": common.b64url_nopad(public.y.to_bytes(32, "big")), + "d": common.b64url_nopad(numbers.private_value.to_bytes(32, "big")), + } captured: dict[str, Any] = {} class Response: @@ -207,22 +452,54 @@ def fake_urlopen(request, **kwargs): header_segment, claims_segment, signature_segment = assertion.split(".") header = json.loads(b64url_decode(header_segment)) claims = json.loads(b64url_decode(claims_segment)) - self.assertEqual(header, {"alg": "EdDSA", "typ": "JWT", "kid": "scenario-client-1"}) + self.assertEqual(header, {"alg": "ES256", "typ": "JWT", "kid": "A" * 43}) self.assertEqual(claims["iss"], "solmara-scenario-runner") self.assertEqual(claims["sub"], "solmara-scenario-runner") self.assertEqual( claims["aud"], "https://mint.evidence.solmara.invalid/token" ) self.assertEqual(claims["exp"] - claims["iat"], 120) - private_key.public_key().verify(b64url_decode(signature_segment), f"{header_segment}.{claims_segment}".encode()) + signature = b64url_decode(signature_segment) + self.assertEqual(len(signature), 64) + der_signature = utils.encode_dss_signature( + int.from_bytes(signature[:32], "big"), + int.from_bytes(signature[32:], "big"), + ) + private_key.public_key().verify( + der_signature, + f"{header_segment}.{claims_segment}".encode(), + ec.ECDSA(hashes.SHA256()), + ) + + def test_invalid_private_key_fails_closed_before_mint_request(self) -> None: + original = urllib.request.urlopen + + def fail_if_called(*_args, **_kwargs): + raise AssertionError("Mint must not be called with an invalid assertion key") + + with tempfile.TemporaryDirectory() as directory: + key_path = Path(directory) / "client.jwk" + key_path.write_text('{"kty":"EC","crv":"P-256","alg":"ES256","kid":"invalid"}') + os.environ.update( + SOLMARA_MINT_URL="https://localhost:4341", + SOLMARA_EVIDENCE_CLIENT_ID="solmara-scenario-runner", + SOLMARA_EVIDENCE_CLIENT_KEY=str(key_path), + ) + urllib.request.urlopen = fail_if_called + try: + self.assertEqual(common.evidence_access_token(), "") + finally: + urllib.request.urlopen = original class EvidenceScenarioTest(unittest.TestCase): def setUp(self) -> None: os.environ["SOLMARA_EVIDENCE_ACCESS_TOKEN"] = "runtime-token" + install_authority_keys() def tearDown(self) -> None: os.environ.pop("SOLMARA_EVIDENCE_ACCESS_TOKEN", None) + common._JWKS_CACHE.clear() def test_pension_combines_two_signed_requirements_in_the_application(self) -> None: calls: list[dict[str, Any]] = [] @@ -231,8 +508,9 @@ def test_pension_combines_two_signed_requirements_in_the_application(self) -> No def fake_http_json(method, url, headers, body=None, timeout=8.0): calls.append(body) requirement = body["requirement"] - concept = "person-is-deceased" if "cra-pension-death" in requirement else "pension-payment-active" - return StepHttpResult(200, signed_evidence([(concept, True)]), {}) + service_id = "cra-pension" if "cra-pension-death" in requirement else "sipf-pension" + concept = "person-is-deceased" if service_id == "cra-pension" else "pension-payment-active" + return StepHttpResult(200, signed_evidence(service_id, body, [(concept, True)]), {"content-type": common.EVIDENCE_JWS_MEDIA_TYPE}) pension_survivor.http_json = fake_http_json try: @@ -241,8 +519,8 @@ def fake_http_json(method, url, headers, body=None, timeout=8.0): pension_survivor.http_json = original self.assertEqual(len(calls), 2) self.assertTrue(result["derived_decisions"]["pension-payment-should-stop"]) - self.assertEqual({trace["service_id"] for trace in result["source_trace"]}, {"registry-evidence"}) - self.assertEqual(len(result["response_source"]["body"]["signed_evidence"]), 2) + self.assertEqual({trace["service_id"] for trace in result["source_trace"]}, {"cra-evidence", "sipf-evidence"}) + self.assertEqual(result["response_source"], {"status": 200, "code": "ok"}) def test_citizen_requests_cra_and_nia_requirements(self) -> None: calls: list[str] = [] @@ -250,8 +528,9 @@ def test_citizen_requests_cra_and_nia_requirements(self) -> None: def fake_http_json(method, url, headers, body=None, timeout=8.0): calls.append(body["requirement"]) - concept = "civil-record-linked" if "cra-citizen" in body["requirement"] else "citizen-population-record-active" - return StepHttpResult(200, signed_evidence([(concept, True)]), {}) + service_id = "cra-citizen" if "cra-citizen" in body["requirement"] else "nia-citizen" + concept = "civil-record-linked" if service_id == "cra-citizen" else "citizen-population-record-active" + return StepHttpResult(200, signed_evidence(service_id, body, [(concept, True)]), {"content-type": common.EVIDENCE_JWS_MEDIA_TYPE}) citizen.http_json = fake_http_json try: @@ -278,12 +557,20 @@ def tearDownClass(cls) -> None: cls.server.server_close() os.environ.pop(child_benefit_federator.FEDERATOR_TOKEN_ENV, None) + def setUp(self) -> None: + os.environ["SOLMARA_EVIDENCE_ACCESS_TOKEN"] = "runtime-token" + install_authority_keys() + + def tearDown(self) -> None: + os.environ.pop("SOLMARA_EVIDENCE_ACCESS_TOKEN", None) + common._JWKS_CACHE.clear() + def request(self, path: str, *, token: str = "collector-token", body: dict[str, Any] | None = None): headers = {"x-api-key": token} data = None method = "GET" if body is not None: - headers.update({"Content-Type": "application/json", "Data-Purpose": common.PURPOSES["child_benefit"]}) + headers.update({"Content-Type": "application/json"}) data = json.dumps(body).encode() method = "POST" return urllib.request.urlopen( @@ -304,13 +591,48 @@ def test_requires_local_application_auth(self) -> None: raised.exception.close() def test_rejects_duplicate_or_unknown_concepts_before_evidence(self) -> None: - base = {"target": {"identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}]}} + base = {"purpose": common.PURPOSES["child_benefit"], "target": {"identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}]}} for claims in (["birth-is-registered", "birth-is-registered"], ["not-a-concept"]): with self.subTest(claims=claims), self.assertRaises(urllib.error.HTTPError) as raised: self.request("/v1/evaluations", body={**base, "claims": claims}) self.assertEqual(raised.exception.code, 400) raised.exception.close() + def test_composes_only_four_independently_verified_assertions(self) -> None: + original = child_benefit_federator.http_json + + def fake_http_json(method, url, headers, body=None, timeout=8.0): + config = service_config.config_for_requirement(body["requirement"]) + values = [(concept, True) for concept in config["concepts"]] + return StepHttpResult(200, signed_evidence(config["service_id"], body, values), {"content-type": common.EVIDENCE_JWS_MEDIA_TYPE}) + + child_benefit_federator.http_json = fake_http_json + try: + body = { + "purpose": common.PURPOSES["child_benefit"], + "target": {"identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}]}, + "claims": child_benefit.CLAIMS, + } + with self.request("/v1/evaluations", body=body) as response: + payload = json.loads(response.read()) + finally: + child_benefit_federator.http_json = original + self.assertEqual(len(payload["signed_evidence"]), 4) + self.assertEqual({item["presentation"]["source"] for item in payload["results"]}, {"immutable extract", "Relay lookup"}) + self.assertEqual({item["service_id"] for item in payload["source_trace"]}, {"cra-evidence", "nia-evidence", "sro-evidence", "mosd-programme-evidence"}) + self.assertNotIn("requirement", json.dumps(payload["source_trace"])) + + def test_purpose_comes_from_json_body_not_header(self) -> None: + body = { + "purpose": "unsupported-purpose", + "target": {"identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}]}, + "claims": child_benefit.CLAIMS, + } + with self.assertRaises(urllib.error.HTTPError) as raised: + self.request("/v1/evaluations", body=body) + self.assertEqual(raised.exception.code, 403) + raised.exception.close() + if __name__ == "__main__": unittest.main() diff --git a/scenarios/child_benefit.py b/scenarios/child_benefit.py index a35b353..cadee1d 100644 --- a/scenarios/child_benefit.py +++ b/scenarios/child_benefit.py @@ -19,7 +19,7 @@ DUPLICATE_CONTROL = "2300054788" CLAIMS = ["birth-is-registered", "population-record-active", "child-age-under-5", "household-below-poverty-threshold", "not-already-enrolled"] FRIENDLY = { - "positive": {"met": ("Mateo's signed source evidence is ready.", "Evidence evaluated five reviewed requirements without copying authority rows.")}, + "positive": {"met": ("Mateo's signed source evidence is ready.", "The programme composed five concepts from four authority requirements without copying source rows.")}, "deceased-control": {"unmet": ("Rejected, exactly as designed.", "The civil evidence says the child is not active for this review.")}, "poverty-control": {"unmet": ("Rejected: the household is above the threshold.", "Only the reviewed poverty predicate was disclosed.")}, "unregistered-control": {"unmet": ("Registration comes first.", "No registered-birth evidence was asserted.")}, @@ -68,16 +68,36 @@ def _request(config: dict[str, Any], step_id: str, *, send: bool) -> dict[str, A token = service_token(SERVICE_ID) if send else "" purpose = "unsupported-demo-purpose" if step_id == "purpose-denial" else str(config.get("purpose_override") or PURPOSES["child_benefit"]) url = service_url(SERVICE_ID, "/v1/claims" if step_id == "discover" else "/v1/evaluations") - headers = {"x-api-key": token, "Accept": "application/json", "Data-Purpose": purpose} - body = None if step_id == "discover" else {"target": {"type": "Person", "identifiers": [{"scheme": "solmara_uin", "value": subject}]}, "claims": CLAIMS, "disclosure": "predicate", "format": "application/json", "variables": {"as_of_date": CHILD_BENEFIT_AS_OF_DATE}} + headers = {"x-api-key": token, "Accept": "application/json"} + body = None if step_id == "discover" else {"purpose": purpose, "target": {"type": "Person", "identifiers": [{"scheme": "solmara_uin", "value": subject}]}, "claims": CLAIMS, "disclosure": "predicate", "format": "application/json", "variables": {"as_of_date": CHILD_BENEFIT_AS_OF_DATE}} request = request_source("GET" if step_id == "discover" else "POST", url, headers, body) + if body is not None: + request["purpose"] = purpose if not send: return {"request_source": request} if not token: return missing_runtime_token(step_id, SERVICE_NAME, service_token_env(SERVICE_ID), request) result = http_json("GET" if step_id == "discover" else "POST", url, headers, body) response_body = result.body if isinstance(result.body, dict) else {} - return {"step_id": step_id, "friendly": friendly_result(step_id, result, FRIENDLY), "request_source": request, "response_source": source_response(result), "source_trace": response_body.get("source_trace", [])} + results = [] + for item in response_body.get("results", []): + if not isinstance(item, dict) or not isinstance(item.get("satisfied"), bool): + continue + safe = { + "claim_id": item.get("claim_id"), + "concept_id": item.get("concept_id"), + "satisfied": item["satisfied"], + "value": item["satisfied"], + } + if isinstance(item.get("presentation"), dict): + safe["presentation"] = item["presentation"] + results.append(safe) + presentations = [] + for item in results: + presentation = item.get("presentation") + if isinstance(presentation, dict) and presentation not in presentations: + presentations.append(presentation) + return {"step_id": step_id, "friendly": friendly_result(step_id, result, FRIENDLY), "request_source": request, "response_source": source_response(result), "source_trace": response_body.get("source_trace", []), "results": results, "presentations": presentations} def request_purpose(config: dict[str, Any], step_id: str) -> str: diff --git a/scenarios/citizen.py b/scenarios/citizen.py index 41ceb0c..feade1b 100644 --- a/scenarios/citizen.py +++ b/scenarios/citizen.py @@ -5,15 +5,14 @@ from typing import Any -from .common import PURPOSES, StepHttpResult, evidence_body, evidence_headers, friendly_result, http_json, missing_runtime_token, normalized_evidence_result, request_source, source_response, standard_error_result -from .service_config import requirement_id, service_token, service_token_env, service_url +from .common import PURPOSES, StepHttpResult, evidence_body, evidence_headers, friendly_result, http_json, missing_runtime_token, normalized_evidence_result, request_source, safe_evidence_projection, source_response, standard_error_result +from .service_config import authority_service_id, requirement_config, requirement_id, service_token, service_token_env, service_url SCENARIO_ID = "citizen-self-service" SERVICE_NAME = "Registry Evidence" POSITIVE_SUBJECT = "2300018263" CLIENTS = ("cra-citizen", "nia-citizen") -AUTHORITY_NAMES = {"cra-citizen": "Civil Registration Authority", "nia-citizen": "National Identity Agency"} FRIENDLY = {"positive": {"met": ("Elena's signed status evidence is ready.", "CRA and NIA released separate reviewed concept values.")}, "purpose-denial": {"refused": ("Refused, exactly as designed.", "No grant permits that purpose.")}} @@ -40,9 +39,10 @@ def _request(config: dict[str, Any], step_id: str, *, send: bool) -> dict[str, A return {"request_source": preview} if not token: return missing_runtime_token(step_id, SERVICE_NAME, service_token_env("cra-citizen"), preview) - responses = [(item, normalized_evidence_result(http_json(item["method"], item["url"], item["headers"], item["body"]))) for item in requests] + responses = [(item, normalized_evidence_result(http_json(item["method"], item["url"], item["headers"], item["body"]), request=item["body"], service_id=item["client_id"])) for item in requests] aggregate = _aggregate(responses) - return {"step_id": step_id, "friendly": friendly_result(step_id, aggregate, FRIENDLY), "request_source": preview, "request_sources": [item["source"] for item, _ in responses], "response_source": source_response(aggregate), "source_trace": [{"authority": AUTHORITY_NAMES[item["client_id"]], "service_id": "registry-evidence", "status": response.status} for item, response in responses]} + projection = safe_evidence_projection(aggregate) + return {"step_id": step_id, "friendly": friendly_result(step_id, aggregate, FRIENDLY), "request_source": preview, "request_sources": [item["source"] for item, _ in responses], "response_source": source_response(aggregate), "source_trace": [_source_trace(item["client_id"], response) for item, response in responses], **projection} def _requests(step_id: str, subject: str, purpose: str, token: str) -> list[dict[str, Any]]: @@ -60,12 +60,26 @@ def _requests(step_id: str, subject: str, purpose: str, token: str) -> list[dict def _preview(requests: list[dict[str, Any]], purpose: str) -> dict[str, Any]: - return requests[0]["source"] if len(requests) == 1 else {"method": "MULTI", "url": "solmara://registry-evidence", "purpose": purpose, "requests": [item["source"] for item in requests]} + return requests[0]["source"] if len(requests) == 1 else {"method": "MULTI", "url": "solmara://authority-evidence", "purpose": purpose, "requests": [item["source"] for item in requests]} + + +def _source_trace(service_id: str, response: StepHttpResult) -> dict[str, Any]: + config = requirement_config(service_id) + return {"authority": config["name"], "service_id": authority_service_id(service_id), "issuer": config["issuer"], "provider": config["provider"], "source": config["source"], "status": response.status} def _aggregate(responses: list[tuple[dict[str, Any], StepHttpResult]]) -> StepHttpResult: failed = next((response for _, response in responses if response.status is None or not 200 <= response.status < 300), None) if failed: return failed - results = [entry for _, response in responses for entry in response.body.get("results", [])] - return StepHttpResult(200, {"results": results, "signed_evidence": [response.body.get("signed_evidence") for _, response in responses]}, {"content-type": "application/json"}) + results = [] + presentations = [] + for _, response in responses: + presentation = response.body.get("presentation") + if isinstance(presentation, dict): + presentations.append(presentation) + results.extend( + {**entry, "presentation": presentation} + for entry in response.body.get("results", []) + ) + return StepHttpResult(200, {"results": results, "presentations": presentations}, {"content-type": "application/json"}) diff --git a/scenarios/common.py b/scenarios/common.py index 775cb4f..bbc377d 100644 --- a/scenarios/common.py +++ b/scenarios/common.py @@ -4,8 +4,10 @@ from __future__ import annotations import base64 +import hashlib import json import os +import re import secrets import ssl import threading @@ -15,11 +17,23 @@ import urllib.request import uuid from dataclasses import dataclass +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any from urllib.parse import urljoin EVIDENCE_JWS_MEDIA_TYPE = "application/jose+json" +EVIDENCE_JWKS_MEDIA_TYPE = "application/jwk-set+json" +EVIDENCE_JWS_HEADER = { + "alg": "ES256", + "typ": "evidence+jws", + "cty": "application/evidence+json", +} +EVIDENCE_AUDIENCE = "https://id.registrystack.org/solmara/audience/demo-client" +MAX_JWKS_BYTES = 64 * 1024 +MAX_JWKS_KEYS = 8 +JWKS_CACHE_SECONDS = 60 +ASSERTION_CLOCK_SKEW_SECONDS = 30 CHILD_BENEFIT_AS_OF_DATE = "2026-07-14" PURPOSES = { "child_benefit": "child-benefit-review", @@ -32,6 +46,10 @@ _TOKEN_LOCK = threading.Lock() _TOKEN_CACHE: tuple[str, float] = ("", 0.0) +_JWKS_LOCK = threading.Lock() +_JWKS_CACHE: dict[str, tuple[float, tuple[dict[str, str], ...]]] = {} +_B64URL_PATTERN = re.compile(r"^[A-Za-z0-9_-]+$") +_KID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{43}$") @dataclass @@ -40,6 +58,7 @@ class StepHttpResult: body: Any headers: dict[str, str] error: str = "" + request: Any | None = None def joined_url(base: str, path: str) -> str: @@ -60,23 +79,25 @@ def request_source( headers: dict[str, str], body: Any | None = None, ) -> dict[str, Any]: + parsed_url = urllib.parse.urlsplit(url) source: dict[str, Any] = { "method": method, - "url": url, + "url": parsed_url._replace(query="", fragment="").geturl(), "headers": redact_headers(headers), } - if body is not None: - source["body"] = body return source def redact_headers(headers: dict[str, str]) -> dict[str, str]: - redacted = dict(headers) - for key in redacted: - if key.lower() == "authorization": + redacted: dict[str, str] = {} + for key, value in headers.items(): + lowered = key.lower() + if lowered == "authorization": redacted[key] = "Bearer [runtime token hidden]" - elif key.lower() == "x-api-key": + elif lowered == "x-api-key": redacted[key] = "[runtime token hidden]" + elif lowered in {"accept", "content-type"}: + redacted[key] = value return redacted @@ -104,45 +125,77 @@ def http_json( response.status, parse_body(response.read()), {key.lower(): value for key, value in response.headers.items()}, + request=body, ) except urllib.error.HTTPError as error: return StepHttpResult( error.code, parse_body(error.read()), {key.lower(): value for key, value in error.headers.items()}, + request=body, ) except Exception as error: # the guided UI reports a value-free class only - return StepHttpResult(None, {}, {}, error.__class__.__name__) + return StepHttpResult(None, {}, {}, error.__class__.__name__, body) def parse_body(raw: bytes) -> Any: if not raw: return {} try: - return json.loads(raw.decode("utf-8")) - except (UnicodeDecodeError, json.JSONDecodeError): + return json.loads(raw.decode("utf-8"), object_pairs_hook=_closed_json_object) + except (UnicodeDecodeError, json.JSONDecodeError, ValueError): return raw.decode("utf-8", errors="replace") +def _closed_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + value: dict[str, Any] = {} + for key, member in pairs: + if key in value: + raise ValueError("duplicate JSON member") + value[key] = member + return value + + def source_response(result: StepHttpResult) -> dict[str, Any]: - return { - "status": result.status, - "headers": { - key: value - for key, value in result.headers.items() - if key in {"content-type", "www-authenticate"} - }, - "body": result.body, - "error": result.error, - } + if result.status is None: + code = "transport_unavailable" + elif 200 <= result.status < 300: + code = "ok" + elif result.status == 502 and isinstance(result.body, dict) and result.body.get("code") == "evidence.invalid_response": + code = "assertion_verification_failed" + elif 400 <= result.status < 500: + code = "request_refused" + else: + code = "service_unavailable" + return {"status": result.status, "code": code} def _client_assertion(client_id: str, key_path: str, audience: str) -> str: - from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - - jwk = json.loads(Path(key_path).read_text()) + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import ec, utils + + jwk = json.loads(Path(key_path).read_text(), object_pairs_hook=_closed_json_object) + if ( + not isinstance(jwk, dict) + or jwk.get("kty") != "EC" + or jwk.get("crv") != "P-256" + or jwk.get("alg") != "ES256" + or not isinstance(jwk.get("kid"), str) + or not _KID_PATTERN.fullmatch(jwk["kid"]) + ): + raise ValueError("invalid client assertion key") + scalar = b64url_decode(jwk.get("d", "")) + if len(scalar) != 32: + raise ValueError("invalid client assertion key") + private_key = ec.derive_private_key(int.from_bytes(scalar, "big"), ec.SECP256R1()) + numbers = private_key.public_key().public_numbers() + if ( + b64url_nopad(numbers.x.to_bytes(32, "big")) != jwk.get("x") + or b64url_nopad(numbers.y.to_bytes(32, "big")) != jwk.get("y") + ): + raise ValueError("invalid client assertion key") now = int(time.time()) - header = {"alg": "EdDSA", "typ": "JWT", "kid": jwk["kid"]} + header = {"alg": "ES256", "typ": "JWT", "kid": jwk["kid"]} claims = { "iss": client_id, "sub": client_id, @@ -155,8 +208,9 @@ def _client_assertion(client_id: str, key_path: str, audience: str) -> str: b64url_nopad(json.dumps(value, separators=(",", ":")).encode()) for value in (header, claims) ) - private_key = Ed25519PrivateKey.from_private_bytes(b64url_decode(jwk["d"])) - signature = private_key.sign(signing_input.encode("ascii")) + der_signature = private_key.sign(signing_input.encode("ascii"), ec.ECDSA(hashes.SHA256())) + r_value, s_value = utils.decode_dss_signature(der_signature) + signature = r_value.to_bytes(32, "big") + s_value.to_bytes(32, "big") return f"{signing_input}.{b64url_nopad(signature)}" @@ -176,9 +230,12 @@ def evidence_access_token() -> str: if _TOKEN_CACHE[0] and _TOKEN_CACHE[1] > time.time() + 10: return _TOKEN_CACHE[0] token_url = joined_url(mint_url, "/token") - assertion = _client_assertion( - client_id, key_path, assertion_audience or token_url - ) + try: + assertion = _client_assertion( + client_id, key_path, assertion_audience or token_url + ) + except (OSError, ValueError, TypeError, json.JSONDecodeError): + return "" form = urllib.parse.urlencode( { "grant_type": "client_credentials", @@ -236,34 +293,282 @@ def evidence_body( } +def _invalid_evidence_result() -> StepHttpResult: + return StepHttpResult( + 502, + { + "code": "evidence.invalid_response", + "detail": "The Evidence assertion could not be verified.", + }, + {}, + ) + + +def _decode_segment(value: Any) -> bytes: + if not isinstance(value, str) or not value or not _B64URL_PATTERN.fullmatch(value): + raise ValueError("invalid JWS segment") + decoded = b64url_decode(value) + if b64url_nopad(decoded) != value: + raise ValueError("noncanonical JWS segment") + return decoded + + +def _json_segment(value: Any) -> dict[str, Any]: + decoded = _decode_segment(value) + parsed = json.loads(decoded, object_pairs_hook=_closed_json_object) + if not isinstance(parsed, dict): + raise ValueError("JWS segment is not an object") + return parsed + + +def _jwk_thumbprint(jwk: dict[str, Any]) -> str: + canonical = json.dumps( + {"crv": jwk["crv"], "kty": jwk["kty"], "x": jwk["x"], "y": jwk["y"]}, + separators=(",", ":"), + sort_keys=True, + ).encode() + return b64url_nopad(hashlib.sha256(canonical).digest()) + + +class _NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + return None + + +def _fetch_jwks(url: str) -> tuple[dict[str, str], ...]: + request = urllib.request.Request( + url, + headers={"Accept": EVIDENCE_JWKS_MEDIA_TYPE}, + method="GET", + ) + handlers: list[Any] = [_NoRedirect()] + context = tls_context() + if context is not None: + handlers.append(urllib.request.HTTPSHandler(context=context)) + opener = urllib.request.build_opener(*handlers) + with opener.open(request, timeout=5) as response: + if response.status != 200 or response.headers.get_content_type() != EVIDENCE_JWKS_MEDIA_TYPE: + raise ValueError("invalid JWKS response") + raw = response.read(MAX_JWKS_BYTES + 1) + if len(raw) > MAX_JWKS_BYTES: + raise ValueError("JWKS response is too large") + document = json.loads(raw, object_pairs_hook=_closed_json_object) + if not isinstance(document, dict) or set(document) != {"keys"}: + raise ValueError("invalid JWKS document") + keys = document["keys"] + if not isinstance(keys, list) or not 1 <= len(keys) <= MAX_JWKS_KEYS: + raise ValueError("invalid JWKS key count") + validated: list[dict[str, str]] = [] + seen: set[str] = set() + expected_members = {"kty", "kid", "alg", "crv", "x", "y"} + for key in keys: + if not isinstance(key, dict) or set(key) != expected_members: + raise ValueError("invalid JWK") + if key.get("kty") != "EC" or key.get("crv") != "P-256" or key.get("alg") != "ES256": + raise ValueError("invalid JWK type") + if not all(isinstance(key.get(member), str) for member in expected_members): + raise ValueError("invalid JWK member") + if len(_decode_segment(key["x"])) != 32 or len(_decode_segment(key["y"])) != 32: + raise ValueError("invalid JWK coordinate") + if not _KID_PATTERN.fullmatch(key["kid"]) or _jwk_thumbprint(key) != key["kid"]: + raise ValueError("invalid JWK kid") + if key["kid"] in seen: + raise ValueError("duplicate JWK kid") + seen.add(key["kid"]) + validated.append(key) + return tuple(validated) + + +def _authority_jwks(service_id: str) -> tuple[dict[str, str], ...]: + from .service_config import service_url + + url = service_url(service_id, "/.well-known/evidence/jwks.json") + now = time.monotonic() + with _JWKS_LOCK: + cached = _JWKS_CACHE.get(url) + if cached and cached[0] > now: + return cached[1] + keys = _fetch_jwks(url) + with _JWKS_LOCK: + _JWKS_CACHE[url] = (time.monotonic() + JWKS_CACHE_SECONDS, keys) + return keys + + +def _verify_es256(protected: str, payload: str, signature: str, jwk: dict[str, str]) -> None: + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature + + raw_signature = _decode_segment(signature) + if len(raw_signature) != 64: + raise ValueError("invalid ES256 signature") + x = int.from_bytes(_decode_segment(jwk["x"]), "big") + y = int.from_bytes(_decode_segment(jwk["y"]), "big") + public_key = ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key() + der_signature = encode_dss_signature( + int.from_bytes(raw_signature[:32], "big"), + int.from_bytes(raw_signature[32:], "big"), + ) + public_key.verify( + der_signature, + f"{protected}.{payload}".encode("ascii"), + ec.ECDSA(hashes.SHA256()), + ) + + +def _parse_evidence_time(value: Any) -> datetime: + if not isinstance(value, str) or not value: + raise ValueError("invalid Evidence time") + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + if parsed.tzinfo is None: + raise ValueError("Evidence time has no timezone") + return parsed.astimezone(timezone.utc) + + +def _valid_uri(value: Any) -> bool: + return isinstance(value, str) and 1 <= len(value) <= 512 and bool(urllib.parse.urlsplit(value).scheme) + + +def _validate_evidence_payload(payload: dict[str, Any], request: dict[str, Any], config: dict[str, Any]) -> None: + required = { + "schema", "assuranceProfile", "subjectBinding", "requestNonce", "id", "type", + "supportsRequirement", "isConformantTo", "issuedBy", "providedBy", "issuedAt", + "observedAt", "validUntil", "purpose", "audience", "configurationRevision", + "subjects", "supportedValues", + } + if set(payload) != required: + raise ValueError("invalid Evidence members") + expected_audience = os.environ.get("SOLMARA_EVIDENCE_AUDIENCE", EVIDENCE_AUDIENCE) + if ( + payload.get("schema") != "registry.assertion-evidence/v1" + or payload.get("assuranceProfile") != "production" + or payload.get("subjectBinding") != "audience-scoped" + or payload.get("type") != "Evidence" + or payload.get("supportsRequirement") != request.get("requirement") + or payload.get("isConformantTo") != config["evidence_type"] + or payload.get("purpose") != request.get("purpose") + or payload.get("requestNonce") != request.get("requestNonce") + or payload.get("issuedBy") != config["issuer"] + or payload.get("providedBy") != config["provider"] + or payload.get("audience") != expected_audience + ): + raise ValueError("Evidence policy mismatch") + for member in ("id", "supportsRequirement", "isConformantTo", "issuedBy", "providedBy", "audience"): + if not _valid_uri(payload.get(member)): + raise ValueError("invalid Evidence URI") + revision = payload.get("configurationRevision") + if not isinstance(revision, str) or not re.fullmatch(r"sha256:[a-f0-9]{64}", revision): + raise ValueError("invalid configuration revision") + purpose = payload.get("purpose") + if not isinstance(purpose, str) or not re.fullmatch(r"[a-z][a-z0-9._:-]{0,127}", purpose): + raise ValueError("invalid Evidence purpose") + nonce = payload.get("requestNonce") + if not isinstance(nonce, str) or not re.fullmatch(r"[A-Za-z0-9_-]{43}", nonce): + raise ValueError("invalid Evidence nonce") + subjects = payload.get("subjects") + if not isinstance(subjects, list) or len(subjects) != 1: + raise ValueError("invalid Evidence subjects") + subject_pairs: set[tuple[str, str]] = set() + for subject in subjects: + if not isinstance(subject, dict) or set(subject) != {"role", "binding"}: + raise ValueError("invalid Evidence subject") + role, binding = subject.get("role"), subject.get("binding") + if not isinstance(role, str) or not re.fullmatch(r"[a-z][a-z0-9._-]{0,63}", role): + raise ValueError("invalid Evidence subject role") + if role != "subject": + raise ValueError("unexpected Evidence subject role") + if not isinstance(binding, str) or not re.fullmatch(r"urn:evidence:subject:v[1-9][0-9]*_[A-Za-z0-9_-]{43}", binding): + raise ValueError("invalid Evidence subject binding") + if (role, binding) in subject_pairs: + raise ValueError("duplicate Evidence subject") + subject_pairs.add((role, binding)) + supported = payload.get("supportedValues") + if not isinstance(supported, list) or not 1 <= len(supported) <= 16: + raise ValueError("invalid supported values") + concepts: list[str] = [] + for entry in supported: + if not isinstance(entry, dict) or set(entry) != {"providesValueFor", "value"}: + raise ValueError("invalid supported value") + concept = entry.get("providesValueFor") + if not _valid_uri(concept) or not isinstance(entry.get("value"), bool): + raise ValueError("invalid supported value") + concepts.append(str(concept)) + expected_concepts = tuple( + f"https://id.registrystack.org/solmara/concept/{concept}" + for concept in config["concepts"] + ) + if tuple(concepts) != expected_concepts or len(concepts) != len(set(concepts)): + raise ValueError("Evidence output mismatch") + issued = _parse_evidence_time(payload.get("issuedAt")) + observed = _parse_evidence_time(payload.get("observedAt")) + valid_until = _parse_evidence_time(payload.get("validUntil")) + now = datetime.now(timezone.utc) + skew = timedelta(seconds=ASSERTION_CLOCK_SKEW_SECONDS) + if ( + issued < observed + or valid_until <= observed + or valid_until <= issued + or valid_until - issued > timedelta(seconds=config["maximum_validity_seconds"]) + or issued > now + skew + or observed > now + skew + or now >= valid_until + skew + ): + raise ValueError("invalid Evidence validity") + + def decoded_evidence_payload(body: Any) -> dict[str, Any] | None: - if not isinstance(body, dict) or not isinstance(body.get("payload"), str): + if not isinstance(body, dict) or set(body) != {"protected", "payload", "signature"}: return None try: - payload = json.loads(b64url_decode(body["payload"])) + return _json_segment(body["payload"]) except (ValueError, UnicodeDecodeError, json.JSONDecodeError): return None - return payload if isinstance(payload, dict) else None -def normalized_evidence_result(result: StepHttpResult) -> StepHttpResult: - """Keep the signed JWS and add the old UI's small predicate summary.""" +def normalized_evidence_result( + result: StepHttpResult, + *, + request: dict[str, Any] | None = None, + service_id: str | None = None, +) -> StepHttpResult: + """Verify a signed assertion, then add the UI's small predicate summary.""" if result.status is None or not 200 <= result.status < 300: return result - payload = decoded_evidence_payload(result.body) - if payload is None: - return StepHttpResult( - 502, - {"code": "evidence.invalid_response", "detail": "Evidence returned an invalid signed assertion."}, - {}, - ) - supported = payload.get("supportedValues", []) + retained_request = request if request is not None else result.request + if not isinstance(retained_request, dict): + return _invalid_evidence_result() + try: + from .service_config import config_for_requirement, requirement_config + + config = requirement_config(service_id) if service_id else config_for_requirement(str(retained_request.get("requirement", ""))) + if retained_request.get("requirement") != config["requirement"]: + raise ValueError("request requirement mismatch") + content_type = result.headers.get("content-type", "").strip().lower() + if content_type != EVIDENCE_JWS_MEDIA_TYPE: + raise ValueError("invalid Evidence media type") + body = result.body + if not isinstance(body, dict) or set(body) != {"protected", "payload", "signature"}: + raise ValueError("invalid flattened JWS") + protected = _json_segment(body["protected"]) + if set(protected) != {"alg", "kid", "typ", "cty"}: + raise ValueError("invalid protected header") + if {key: protected.get(key) for key in EVIDENCE_JWS_HEADER} != EVIDENCE_JWS_HEADER: + raise ValueError("invalid Evidence protected header") + kid = protected.get("kid") + if not isinstance(kid, str) or not _KID_PATTERN.fullmatch(kid): + raise ValueError("invalid Evidence kid") + key = next((candidate for candidate in _authority_jwks(config["service_id"]) if candidate["kid"] == kid), None) + if key is None: + raise ValueError("untrusted Evidence kid") + _verify_es256(body["protected"], body["payload"], body["signature"], key) + payload = _json_segment(body["payload"]) + _validate_evidence_payload(payload, retained_request, config) + except Exception: + return _invalid_evidence_result() results = [] - for entry in supported if isinstance(supported, list) else []: - if not isinstance(entry, dict): - continue - concept = str(entry.get("providesValueFor", "")) - value = entry.get("value") + for entry in payload["supportedValues"]: + concept = entry["providesValueFor"] + value = entry["value"] results.append( { "claim_id": concept.rsplit("/", 1)[-1], @@ -272,14 +577,69 @@ def normalized_evidence_result(result: StepHttpResult) -> StepHttpResult: "value": value, } ) + presentation = { + "authority": config["name"], + "issuer": config["issuer"], + "provider": config["provider"], + "source": config["source"], + } return StepHttpResult( result.status, - {"results": results, "assertion": payload, "signed_evidence": result.body}, + {"results": results, "assertion": payload, "signed_evidence": result.body, "presentation": presentation}, result.headers, result.error, ) +def safe_evidence_projection(result: StepHttpResult) -> dict[str, Any]: + """Return only verified concepts and public authority attribution for a UI.""" + if result.status is None or not 200 <= result.status < 300: + return {"results": [], "presentations": []} + body = result.body if isinstance(result.body, dict) else {} + presentation = body.get("presentation") + presentations = body.get("presentations") + if not isinstance(presentations, list): + presentations = [presentation] if isinstance(presentation, dict) else [] + safe_presentations = [ + { + "authority": item.get("authority"), + "issuer": item.get("issuer"), + "provider": item.get("provider"), + "source": item.get("source"), + } + for item in presentations + if isinstance(item, dict) + and set(item) == {"authority", "issuer", "provider", "source"} + ] + safe_results: list[dict[str, Any]] = [] + for item in body.get("results", []): + if not isinstance(item, dict): + continue + value = item.get("value") + if not isinstance(value, bool): + continue + safe = { + "claim_id": item.get("claim_id"), + "concept_id": item.get("concept_id"), + "satisfied": value, + "value": value, + } + item_presentation = item.get("presentation") + if item_presentation is None and len(safe_presentations) == 1: + item_presentation = safe_presentations[0] + if ( + isinstance(item_presentation, dict) + and set(item_presentation) + == {"authority", "issuer", "provider", "source"} + ): + safe["presentation"] = { + key: item_presentation[key] + for key in ("authority", "issuer", "provider", "source") + } + safe_results.append(safe) + return {"results": safe_results, "presentations": safe_presentations} + + def friendly_result( step_id: str, result: StepHttpResult, @@ -295,7 +655,7 @@ def friendly_result( facts.append({"label": "Evidence values", "value": f"{len(results) - len(unmet)} of {len(results)} true"}) if result.status is None: return {"title": "No response from the service.", "message": "Check that Mint and Evidence are running.", "status": "needs_attention", "facts": facts} - if result.status >= 400 and "refused" in copy.get(step_id, {}): + if 400 <= result.status < 500 and "refused" in copy.get(step_id, {}): title, message = copy[step_id]["refused"] return {"title": title, "message": message, "status": "done", "facts": facts} if 200 <= result.status < 300: @@ -309,7 +669,7 @@ def friendly_result( return {"title": title, "message": message, "status": "done", "facts": facts} return { "title": "Request needs attention.", - "message": str(body.get("detail") or body.get("title") or body.get("code") or "Evidence refused the request."), + "message": "Evidence could not complete the request.", "status": "needs_attention", "facts": facts, } @@ -327,7 +687,7 @@ def missing_runtime_token( "facts": [{"label": "Required setting", "value": token_env}], }, "request_source": request, - "response_source": {"note": "No Mint access token was available."}, + "response_source": {"status": None, "code": "not_configured"}, } diff --git a/scenarios/farmer_voucher.py b/scenarios/farmer_voucher.py index 5382959..ec6c3f9 100644 --- a/scenarios/farmer_voucher.py +++ b/scenarios/farmer_voucher.py @@ -5,8 +5,8 @@ from typing import Any -from .common import PURPOSES, evidence_body, evidence_headers, friendly_result, http_json, missing_runtime_token, normalized_evidence_result, request_source, source_response, standard_error_result -from .service_config import requirement_id, service_token, service_token_env, service_url +from .common import PURPOSES, evidence_body, evidence_headers, friendly_result, http_json, missing_runtime_token, normalized_evidence_result, request_source, safe_evidence_projection, source_response, standard_error_result +from .service_config import authority_service_id, requirement_config, requirement_id, service_token, service_token_env, service_url SCENARIO_ID = "farmer-climate-smart-voucher" @@ -51,8 +51,11 @@ def _request(config: dict[str, Any], step_id: str, *, send: bool) -> dict[str, A if not token: return missing_runtime_token(step_id, SERVICE_NAME, service_token_env(client), request) raw = http_json("GET" if step_id == "discover" else "POST", url, headers, body) - result = raw if step_id == "discover" else normalized_evidence_result(raw) - return {"step_id": step_id, "friendly": friendly_result(step_id, result, FRIENDLY), "request_source": request, "response_source": source_response(result)} + result = raw if step_id == "discover" else normalized_evidence_result(raw, request=body, service_id=client) + config = requirement_config(client) + trace = {"authority": config["name"], "service_id": authority_service_id(client), "issuer": config["issuer"], "provider": config["provider"], "source": config["source"], "status": result.status} + projection = safe_evidence_projection(result) + return {"step_id": step_id, "friendly": friendly_result(step_id, result, FRIENDLY), "request_source": request, "response_source": source_response(result), "source_trace": [trace], **projection} def request_purpose(config: dict[str, Any], step_id: str) -> str: diff --git a/scenarios/pension_survivor.py b/scenarios/pension_survivor.py index f309e2c..83f2731 100644 --- a/scenarios/pension_survivor.py +++ b/scenarios/pension_survivor.py @@ -5,8 +5,8 @@ from typing import Any -from .common import PURPOSES, StepHttpResult, evidence_body, evidence_headers, friendly_result, http_json, missing_runtime_token, normalized_evidence_result, request_source, source_response, standard_error_result -from .service_config import requirement_id, service_token, service_token_env, service_url +from .common import PURPOSES, StepHttpResult, evidence_body, evidence_headers, friendly_result, http_json, missing_runtime_token, normalized_evidence_result, request_source, safe_evidence_projection, source_response, standard_error_result +from .service_config import authority_service_id, requirement_config, requirement_id, service_token, service_token_env, service_url SCENARIO_ID = "death-to-pension-survivor" @@ -15,7 +15,6 @@ SURVIVING_SPOUSE = "2300118698" STALE_CONTROL = "2300127827" DISSOLVED_MARRIAGE_CONTROL = "2300146081" -AUTHORITY_NAMES = {"cra-pension": "Civil Registration Authority", "sipf-pension": "Social Insurance and Pensions Fund", "sipf-survivor": "Social Insurance and Pensions Fund"} FRIENDLY = { "stop-payment": {"met": ("The pension stops.", "The application combined two signed values while each authority kept its source row.")}, "survivor-benefit": {"met": ("Yes. The surviving spouse can be offered the benefit.", "SIPF returned only the reviewed survivor concept.")}, @@ -44,14 +43,14 @@ def _request(config: dict[str, Any], step_id: str, *, send: bool) -> dict[str, A token = service_token("cra-pension") if send else "" purpose = request_purpose(config, step_id) requests = _requests(step_id, subject or "", purpose, token) - preview = requests[0]["source"] if len(requests) == 1 else {"method": "MULTI", "url": "solmara://registry-evidence", "purpose": purpose, "requests": [item["source"] for item in requests]} + preview = requests[0]["source"] if len(requests) == 1 else {"method": "MULTI", "url": "solmara://authority-evidence", "purpose": purpose, "requests": [item["source"] for item in requests]} if not send: return {"request_source": preview} if not token: return missing_runtime_token(step_id, SERVICE_NAME, service_token_env("cra-pension"), preview) - responses = [(item, normalized_evidence_result(http_json(item["method"], item["url"], item["headers"], item["body"]))) for item in requests] + responses = [(item, normalized_evidence_result(http_json(item["method"], item["url"], item["headers"], item["body"]), request=item["body"], service_id=item["client_id"])) for item in requests] aggregate = _aggregate(responses) - payload: dict[str, Any] = {"step_id": step_id, "friendly": friendly_result(step_id, aggregate, FRIENDLY), "request_source": preview, "request_sources": [item["source"] for item, _ in responses], "response_source": source_response(aggregate), "source_trace": [{"authority": AUTHORITY_NAMES.get(item["client_id"], "Registry Evidence"), "service_id": "registry-evidence", "status": response.status} for item, response in responses]} + payload: dict[str, Any] = {"step_id": step_id, "friendly": friendly_result(step_id, aggregate, FRIENDLY), "request_source": preview, "request_sources": [item["source"] for item, _ in responses], "response_source": source_response(aggregate), "source_trace": [_source_trace(item["client_id"], response) for item, response in responses], **safe_evidence_projection(aggregate)} if step_id in {"stop-payment", "stale-control"} and aggregate.status == 200: values = {item.get("claim_id"): item.get("satisfied") for item in aggregate.body.get("results", [])} payload["derived_decisions"] = {"pension-payment-should-stop": values.get("person-is-deceased") is True and values.get("pension-payment-active") is True, "owner": "pension-review-application"} @@ -80,11 +79,25 @@ def _aggregate(responses: list[tuple[dict[str, Any], StepHttpResult]]) -> StepHt failed = next((response for _, response in responses if response.status is None or not 200 <= response.status < 300), None) if failed: return failed - results = [entry for _, response in responses for entry in response.body.get("results", [])] - return StepHttpResult(200, {"results": results, "signed_evidence": [response.body.get("signed_evidence") for _, response in responses]}, {"content-type": "application/json"}) + results = [] + presentations = [] + for _, response in responses: + presentation = response.body.get("presentation") + if isinstance(presentation, dict): + presentations.append(presentation) + results.extend( + {**entry, "presentation": presentation} + for entry in response.body.get("results", []) + ) + return StepHttpResult(200, {"results": results, "presentations": presentations}, {"content-type": "application/json"}) def request_purpose(config: dict[str, Any], step_id: str) -> str: if isinstance(config.get("purpose_override"), str): return config["purpose_override"] return PURPOSES["survivor_benefit"] if step_id in {"survivor-benefit", "dissolved-control"} else PURPOSES["pension_payment"] + + +def _source_trace(service_id: str, response: StepHttpResult) -> dict[str, Any]: + config = requirement_config(service_id) + return {"authority": config["name"], "service_id": authority_service_id(service_id), "issuer": config["issuer"], "provider": config["provider"], "source": config["source"], "status": response.status} diff --git a/scenarios/service_config.py b/scenarios/service_config.py index 574b363..3bb1624 100644 --- a/scenarios/service_config.py +++ b/scenarios/service_config.py @@ -1,28 +1,95 @@ #!/usr/bin/env python3 -"""One Evidence endpoint with requirement-specific policy identifiers.""" +"""Authority-owned Registry Evidence endpoints and requirement identifiers.""" from __future__ import annotations import os +from typing import Any from .common import evidence_access_token, joined_url BASE = "https://id.registrystack.org/solmara/requirement" -REQUIREMENTS = { - "cra-child-benefit": f"{BASE}/cra-child-benefit/v1", - "nia-child-benefit": f"{BASE}/nia-child-benefit/v1", - "sro-child-benefit": f"{BASE}/sro-child-benefit/v1", - "programme-child-benefit": f"{BASE}/mosd-child-benefit/v1", - "cra-pension": f"{BASE}/cra-pension-death/v1", - "sipf-pension": f"{BASE}/sipf-pension-payment/v1", - "sipf-survivor": f"{BASE}/sipf-survivor-benefit/v1", - "cra-citizen": f"{BASE}/cra-citizen-record/v1", - "nia-citizen": f"{BASE}/nia-citizen-status/v1", - "nagdi-voucher": f"{BASE}/nagdi-voucher/v1", - "nagdi-livestock": f"{BASE}/nagdi-livestock/v1", +PROVIDER_BASE = "https://id.registrystack.org/solmara/evidence" +EVIDENCE_TYPE_BASE = "https://id.registrystack.org/solmara/evidence-type" +AUTHORITY_DID_BASE = "did:web:id.registrystack.org:solmara:authority" +AUTHORITY_DIRECTORY: dict[str, dict[str, str]] = { + "cra": { + "name": "Civil Registration Authority", + "base_url": "https://cra-evidence.solmara.registrystack.org", + "env": "SOLMARA_CRA_EVIDENCE_URL", + "issuer": f"{AUTHORITY_DID_BASE}:cra", + "provider": f"{PROVIDER_BASE}/cra", + }, + "nia": { + "name": "National Identity Agency", + "base_url": "https://nia-evidence.solmara.registrystack.org", + "env": "SOLMARA_NIA_EVIDENCE_URL", + "issuer": f"{AUTHORITY_DID_BASE}:nia", + "provider": f"{PROVIDER_BASE}/nia", + }, + "sro": { + "name": "Social Registry Office", + "base_url": "https://sro-evidence.solmara.registrystack.org", + "env": "SOLMARA_SRO_EVIDENCE_URL", + "issuer": f"{AUTHORITY_DID_BASE}:sro", + "provider": f"{PROVIDER_BASE}/sro", + }, + "mosd-programme": { + "name": "Ministry of Social Development Programme MIS", + "base_url": "https://mosd-programme-evidence.solmara.registrystack.org", + "env": "SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL", + "issuer": f"{AUTHORITY_DID_BASE}:mosd-programme-mis", + "provider": f"{PROVIDER_BASE}/mosd-programme", + }, + "sipf": { + "name": "Social Insurance and Pensions Fund", + "base_url": "https://sipf-evidence.solmara.registrystack.org", + "env": "SOLMARA_SIPF_EVIDENCE_URL", + "issuer": f"{AUTHORITY_DID_BASE}:sipf", + "provider": f"{PROVIDER_BASE}/sipf", + }, + "nagdi": { + "name": "National Agricultural Data Institute", + "base_url": "https://nagdi-evidence.solmara.registrystack.org", + "env": "SOLMARA_NAGDI_EVIDENCE_URL", + "issuer": f"{AUTHORITY_DID_BASE}:nagdi", + "provider": f"{PROVIDER_BASE}/nagdi", + }, } +REQUIREMENT_DIRECTORY: dict[str, dict[str, Any]] = { + "cra-child-benefit": {"authority": "cra", "requirement": f"{BASE}/cra-child-benefit/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/cra-child-benefit/v1", "source": "immutable extract", "maximum_validity_seconds": 3600, "concepts": ("birth-is-registered", "child-age-under-5")}, + "nia-child-benefit": {"authority": "nia", "requirement": f"{BASE}/nia-child-benefit/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/population-active/v1", "source": "immutable extract", "maximum_validity_seconds": 3600, "concepts": ("population-record-active",)}, + "sro-child-benefit": {"authority": "sro", "requirement": f"{BASE}/sro-child-benefit/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/poverty-priority/v1", "source": "immutable extract", "maximum_validity_seconds": 3600, "concepts": ("household-below-poverty-threshold",)}, + "programme-child-benefit": {"authority": "mosd-programme", "requirement": f"{BASE}/mosd-child-benefit/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/not-enrolled/v1", "source": "Relay lookup", "maximum_validity_seconds": 300, "concepts": ("not-already-enrolled",)}, + "cra-pension": {"authority": "cra", "requirement": f"{BASE}/cra-pension-death/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/cra-death-status/v1", "source": "Relay lookup", "maximum_validity_seconds": 300, "concepts": ("person-is-deceased",)}, + "sipf-pension": {"authority": "sipf", "requirement": f"{BASE}/sipf-pension-payment/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/pension-payment-active/v1", "source": "Relay lookup", "maximum_validity_seconds": 300, "concepts": ("pension-payment-active",)}, + "sipf-survivor": {"authority": "sipf", "requirement": f"{BASE}/sipf-survivor-benefit/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/survivor-benefit/v1", "source": "Relay lookup", "maximum_validity_seconds": 300, "concepts": ("survivor-is-eligible",)}, + "cra-citizen": {"authority": "cra", "requirement": f"{BASE}/cra-citizen-record/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/civil-record-linked/v1", "source": "Relay lookup", "maximum_validity_seconds": 300, "concepts": ("civil-record-linked",)}, + "nia-citizen": {"authority": "nia", "requirement": f"{BASE}/nia-citizen-status/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/citizen-population-active/v1", "source": "immutable extract", "maximum_validity_seconds": 3600, "concepts": ("citizen-population-record-active",)}, + "nagdi-voucher": {"authority": "nagdi", "requirement": f"{BASE}/nagdi-voucher/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/climate-smart-voucher/v1", "source": "Relay lookup", "maximum_validity_seconds": 300, "concepts": ("farmer-registered", "data-use-authorized-for-purpose", "eligible-for-climate-smart-input-voucher")}, + "nagdi-livestock": {"authority": "nagdi", "requirement": f"{BASE}/nagdi-livestock/v1", "evidence_type": f"{EVIDENCE_TYPE_BASE}/livestock-movement/v1", "source": "Relay lookup", "maximum_validity_seconds": 300, "concepts": ("registered-herd", "origin-district-not-quarantined-for-species", "eligible-for-livestock-movement-permit")}, +} +REQUIREMENTS = {key: value["requirement"] for key, value in REQUIREMENT_DIRECTORY.items()} +_REQUIREMENT_ALIASES = {value: key for key, value in REQUIREMENTS.items()} + + +def requirement_config(service_id: str) -> dict[str, Any]: + try: + requirement = REQUIREMENT_DIRECTORY[service_id] + authority = AUTHORITY_DIRECTORY[requirement["authority"]] + except KeyError as error: + raise ValueError(f"unknown Evidence requirement: {service_id}") from error + return {**requirement, **authority, "service_id": service_id} + + +def config_for_requirement(requirement: str) -> dict[str, Any]: + try: + return requirement_config(_REQUIREMENT_ALIASES[requirement]) + except KeyError as error: + raise ValueError("unknown Evidence requirement") from error + def service_url(service_id: str, path: str = "/v1/evidence") -> str: if service_id == "child-benefit-federator": @@ -30,15 +97,14 @@ def service_url(service_id: str, path: str = "/v1/evidence") -> str: os.environ.get("CHILD_BENEFIT_FEDERATOR_URL", "http://127.0.0.1:4321"), path, ) - return joined_url( - os.environ.get("SOLMARA_EVIDENCE_URL", "https://evidence.solmara.invalid"), - path, - ) + config = requirement_config(service_id) + return joined_url(os.environ.get(config["env"], config["base_url"]), path) def service_token(service_id: str) -> str: if service_id == "child-benefit-federator": return os.environ.get("CHILD_BENEFIT_FEDERATOR_TOKEN", "") + requirement_config(service_id) return evidence_access_token() @@ -51,11 +117,8 @@ def service_token_env(service_id: str) -> str: def requirement_id(service_id: str) -> str: - try: - return REQUIREMENTS[service_id] - except KeyError as error: - raise ValueError(f"unknown Evidence requirement: {service_id}") from error + return str(requirement_config(service_id)["requirement"]) def authority_service_id(service_id: str) -> str: - return "registry-evidence" + return f"{requirement_config(service_id)['authority']}-evidence" diff --git a/scripts/build-registry-stack-runtime.sh b/scripts/build-registry-stack-runtime.sh index 4c031fb..52d6a5a 100755 --- a/scripts/build-registry-stack-runtime.sh +++ b/scripts/build-registry-stack-runtime.sh @@ -8,88 +8,62 @@ set -a . "$root/versions.env" set +a -source_ref=${REGISTRY_STACK_SOURCE_REF:?missing REGISTRY_STACK_SOURCE_REF} +"$root/scripts/check-registry-stack-release-pin.py" --require-public + +version=${REGISTRY_STACK_REQUIRED_VERSION:?missing REGISTRY_STACK_REQUIRED_VERSION} source_commit=${REGISTRY_STACK_SOURCE_COMMIT:?missing REGISTRY_STACK_SOURCE_COMMIT} +relay_digest=${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?missing REGISTRY_STACK_RELEASE_RELAY_DIGEST} relay_image=${REGISTRY_RELAY_IMAGE:?missing REGISTRY_RELAY_IMAGE} +relayctl_image=${REGISTRY_RELAYCTL_IMAGE:?missing REGISTRY_RELAYCTL_IMAGE} evidence_image=${SOLMARA_EVIDENCE_IMAGE:?missing SOLMARA_EVIDENCE_IMAGE} mint_image=${SOLMARA_MINT_IMAGE:?missing SOLMARA_MINT_IMAGE} -source_dir=${REGISTRY_STACK_SOURCE_DIR:-"$root/../registry-stack"} -if [ "$source_ref" != "main" ]; then - echo "REGISTRY_STACK_SOURCE_REF must be main" >&2 - exit 1 -fi -case "$source_commit" in - *[!0-9a-f]*) - echo "REGISTRY_STACK_SOURCE_COMMIT must be exactly 40 lowercase hex characters" >&2 - exit 1 - ;; -esac -if [ "${#source_commit}" -ne 40 ]; then - echo "REGISTRY_STACK_SOURCE_COMMIT must be exactly 40 lowercase hex characters" >&2 +expected_relay="ghcr.io/registrystack/relay@sha256:$relay_digest" +if [ "$relay_image" != "$expected_relay" ]; then + echo "REGISTRY_RELAY_IMAGE must bind the published Relay digest" >&2 exit 1 fi -if [ ! -d "$source_dir/.git" ]; then - echo "REGISTRY_STACK_SOURCE_DIR must name a Registry Stack checkout" >&2 +observed_relay=$(docker buildx imagetools inspect "ghcr.io/registrystack/relay:v$version" --format '{{.Manifest.Digest}}') +if [ "$observed_relay" != "sha256:$relay_digest" ]; then + echo "published Relay tag does not match the required digest" >&2 exit 1 fi -if ! git -C "$source_dir" cat-file -e "$source_commit^{commit}"; then - echo "Registry Stack source commit is unavailable in $source_dir" >&2 - exit 1 -fi -resolved_ref=$(git -C "$source_dir" rev-parse "$source_ref^{commit}") -remote_main=$(git -C "$source_dir" rev-parse "refs/remotes/origin/$source_ref^{commit}" 2>/dev/null || true) -if [ "$resolved_ref" != "$source_commit" ] || [ "$remote_main" != "$source_commit" ]; then - echo "Registry Stack main does not match $source_commit; fetch origin/main and update versions.env intentionally" >&2 + +registry_stack_platform=${REGISTRY_STACK_PLATFORM:-linux/amd64} +if [ "$registry_stack_platform" != "linux/amd64" ]; then + echo "published Registry Stack runtime assets require linux/amd64" >&2 exit 1 fi +platform_args="--platform $registry_stack_platform" -all_current=true -for image in "$relay_image" "$evidence_image" "$mint_image"; do - revision=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$image" 2>/dev/null || true) - if [ "$revision" != "$source_commit" ]; then - all_current=false +build_release_binary() { + target=$1 + image=$2 + current_revision=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$image" 2>/dev/null || true) + current_version=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$image" 2>/dev/null || true) + current_architecture=$(docker image inspect --format '{{.Architecture}}' "$image" 2>/dev/null || true) + if [ "$current_revision" = "$source_commit" ] && [ "$current_version" = "$version" ] && [ "$current_architecture" = "amd64" ]; then + echo "$target image already matches Registry Stack v$version" + return fi -done -if [ "$all_current" = true ]; then - echo "Registry Stack runtime images already match $source_commit" - exit 0 -fi -temporary=$(mktemp -d "${TMPDIR:-/tmp}/solmara-registry-stack.XXXXXX") -worktree="$temporary/source" -cleanup() { - git -C "$source_dir" worktree remove --force "$worktree" >/dev/null 2>&1 || true - rm -rf "$temporary" -} -trap cleanup EXIT HUP INT TERM -git -C "$source_dir" worktree add --detach "$worktree" "$source_commit" >/dev/null - -platform_args="" -if [ -n "${REGISTRY_STACK_PLATFORM:-}" ]; then - platform_args="--platform $REGISTRY_STACK_PLATFORM" -fi - -# shellcheck disable=SC2086 -docker buildx build --load $platform_args \ - --label "org.opencontainers.image.revision=$source_commit" \ - --label "org.opencontainers.image.ref.name=$source_ref" \ - --tag "$relay_image" \ - --file "$root/docker/registry-stack-runtime/Dockerfile" \ - --target relay \ - "$worktree" - -for target in evidence mint; do - case "$target" in - evidence) image=$evidence_image ;; - mint) image=$mint_image ;; - esac # shellcheck disable=SC2086 docker buildx build --load $platform_args \ + --build-arg "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL=$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL" \ + --build-arg "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256=$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256" \ + --build-arg "REGISTRY_STACK_RELEASE_MINT_ASSET_URL=$REGISTRY_STACK_RELEASE_MINT_ASSET_URL" \ + --build-arg "REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256=$REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256" \ + --build-arg "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" \ + --build-arg "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256=$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" \ + --label "org.opencontainers.image.source=https://github.com/registrystack/registry-stack" \ --label "org.opencontainers.image.revision=$source_commit" \ - --label "org.opencontainers.image.ref.name=$source_ref" \ + --label "org.opencontainers.image.version=$version" \ --tag "$image" \ - --file "$worktree/docker/Dockerfile" \ + --file "$root/docker/registry-stack-release-binary/Dockerfile" \ --target "$target" \ - "$worktree" -done + "$root" +} + +build_release_binary evidence "$evidence_image" +build_release_binary mint "$mint_image" +build_release_binary relayctl "$relayctl_image" diff --git a/scripts/build-relay-runtime.sh b/scripts/build-relay-runtime.sh deleted file mode 100755 index 050cac9..0000000 --- a/scripts/build-relay-runtime.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env sh -set -eu - -root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -exec "$root/scripts/build-registry-stack-runtime.sh" diff --git a/scripts/check-config-secrets.py b/scripts/check-config-secrets.py index 3efa6de..59e9b1a 100755 --- a/scripts/check-config-secrets.py +++ b/scripts/check-config-secrets.py @@ -11,8 +11,7 @@ SCAN_DIRS = [ "ministries", "metadata", - "projects", - "runtime/registry-projects", + "relays", "evidence", "config/hosted-relay-bundles", "compose.yaml", @@ -27,6 +26,7 @@ "private_jwk_env:", "hash_secret_env:", "secret:file/", + "secret:env/", "POSTGRES_PASSWORD:", ) WORKLOAD_TOKEN_VOLUME = re.compile( diff --git a/scripts/check-coolify-compose.sh b/scripts/check-coolify-compose.sh index c84da3b..ea5f484 100755 --- a/scripts/check-coolify-compose.sh +++ b/scripts/check-coolify-compose.sh @@ -1,31 +1,63 @@ #!/usr/bin/env sh set -eu -if [ ! -f .env ]; then - echo ".env is missing; run 'just gen-secrets' first" >&2 - exit 1 -fi +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +test_digest=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +test_image="ghcr.io/registrystack/solmara-test@sha256:$test_digest" -compose_project_name="$(python3 scripts/compose_project_name.py)" -for compose in $(find . -maxdepth 1 -name 'compose.coolify*.yaml' -print | sort); do - COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-$compose_project_name}" \ - SOLMARA_RELAY_IMAGE="${SOLMARA_RELAY_IMAGE:-ghcr.io/registrystack/solmara-lab-relay@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - SOLMARA_NOTARY_IMAGE="${SOLMARA_NOTARY_IMAGE:-ghcr.io/registrystack/solmara-lab-notary@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - SOLMARA_POSTGRES_IMAGE="${SOLMARA_POSTGRES_IMAGE:-ghcr.io/registrystack/solmara-lab-postgres@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - SOLMARA_STATIC_METADATA_IMAGE="${SOLMARA_STATIC_METADATA_IMAGE:-ghcr.io/registrystack/solmara-lab-static-metadata@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - SOLMARA_HOME_IMAGE="${SOLMARA_HOME_IMAGE:-ghcr.io/registrystack/solmara-lab-home@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - SOLMARA_PORTAL_IMAGE="${SOLMARA_PORTAL_IMAGE:-ghcr.io/registrystack/solmara-lab-portal@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - SOLMARA_SCENARIO_RUNNER_IMAGE="${SOLMARA_SCENARIO_RUNNER_IMAGE:-ghcr.io/registrystack/solmara-lab-scenario-runner@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - SOLMARA_ESIGNET_RELAY_IMAGE="${SOLMARA_ESIGNET_RELAY_IMAGE:-ghcr.io/registrystack/solmara-lab-esignet-relay@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - SOLMARA_ESIGNET_SEED_IMAGE="${SOLMARA_ESIGNET_SEED_IMAGE:-ghcr.io/registrystack/solmara-lab-esignet-seed@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - SOLMARA_ESIGNET_POSTGRES_IMAGE="${SOLMARA_ESIGNET_POSTGRES_IMAGE:-ghcr.io/registrystack/solmara-lab-esignet-postgres@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - SOLMARA_ESIGNET_UI_IMAGE="${SOLMARA_ESIGNET_UI_IMAGE:-ghcr.io/registrystack/solmara-lab-esignet-ui@sha256:0000000000000000000000000000000000000000000000000000000000000000}" \ - CONFIG_REPO_REF="${CONFIG_REPO_REF:-local-compose-check}" \ - WALT_DB_PASSWORD="${WALT_DB_PASSWORD:-local-compose-check}" \ - WALT_AUTH_ENCRYPTION_KEY="${WALT_AUTH_ENCRYPTION_KEY:-local-compose-check}" \ - WALT_AUTH_SIGN_KEY="${WALT_AUTH_SIGN_KEY:-local-compose-check}" \ - WALT_AUTH_TOKEN_KEY="${WALT_AUTH_TOKEN_KEY:-local-compose-check}" \ - WALT_KTOR_SIGNING_KEY="${WALT_KTOR_SIGNING_KEY:-local-compose-check}" \ - WALT_KTOR_VERIFICATION_KEY="${WALT_KTOR_VERIFICATION_KEY:-local-compose-check}" \ - docker compose --env-file versions.env --env-file .env -f "$compose" config >/dev/null +check_compose() { + REGISTRY_STACK_RELEASE_RELAY_DIGEST="$test_digest" \ + SOLMARA_EVIDENCE_IMAGE="$test_image" \ + SOLMARA_MINT_IMAGE="$test_image" \ + SOLMARA_STATIC_METADATA_IMAGE="$test_image" \ + SOLMARA_SCENARIO_RUNNER_IMAGE="$test_image" \ + SOLMARA_AUTHORITY_PROVISIONER_IMAGE="$test_image" \ + SOLMARA_TRANSIT_SIGNER_IMAGE="$test_image" \ + SOLMARA_HOME_IMAGE="$test_image" \ + SOLMARA_PORTAL_IMAGE="$test_image" \ + SOLMARA_ESIGNET_RELAY_IMAGE="$test_image" \ + SOLMARA_ESIGNET_SEED_IMAGE="$test_image" \ + SOLMARA_ESIGNET_POSTGRES_IMAGE="$test_image" \ + SOLMARA_ESIGNET_UI_IMAGE="$test_image" \ + ESIGNET_REDIS_IMAGE="redis@sha256:b99ffd0554dc8d300230b9d1b9f2a129a6abf595bf8589883beb980ed1feae3d" \ + SOLMARA_ESIGNET_POSTGRES_PASSWORD=test \ + ESIGNET_DEMO_OTP=111111 \ + REGISTRY_ESIGNET_KYC_TOKEN_SECRET=test \ + REGISTRY_ESIGNET_PSUT_SECRET=test \ + REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD=test \ + PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64=test \ + NIA_ESIGNET_CLIENT_PRIVATE_JWK='{"kty":"RSA"}' \ + SOLMARA_DEMO_CLIENT_PRIVATE_JWK='{"kty":"EC"}' \ + CHILD_BENEFIT_FEDERATOR_TOKEN=test \ + CRA_RELAY_AUDIT_KEY=test NIA_RELAY_AUDIT_KEY=test MOSD_RELAY_AUDIT_KEY=test \ + SIPF_RELAY_AUDIT_KEY=test SIPF_RELAY_CURSOR_KEY=test \ + NAGDI_RELAY_AUDIT_KEY=test NAGDI_RELAY_CURSOR_KEY=test \ + MINT_SIGNING_JWK=test MINT_SIGNING_PUBLIC_JWK=test MINT_AUDIT_HMAC_KEY=test \ + CRA_EVIDENCE_SIGNING_JWK=test CRA_EVIDENCE_SIGNING_PUBLIC_JWK=test CRA_EVIDENCE_AUDIT_HMAC_KEY=test CRA_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + NIA_EVIDENCE_SIGNING_JWK=test NIA_EVIDENCE_SIGNING_PUBLIC_JWK=test NIA_EVIDENCE_AUDIT_HMAC_KEY=test NIA_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + SRO_EVIDENCE_SIGNING_JWK=test SRO_EVIDENCE_SIGNING_PUBLIC_JWK=test SRO_EVIDENCE_AUDIT_HMAC_KEY=test SRO_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + MOSD_EVIDENCE_SIGNING_JWK=test MOSD_EVIDENCE_SIGNING_PUBLIC_JWK=test MOSD_EVIDENCE_AUDIT_HMAC_KEY=test MOSD_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + SIPF_EVIDENCE_SIGNING_JWK=test SIPF_EVIDENCE_SIGNING_PUBLIC_JWK=test SIPF_EVIDENCE_AUDIT_HMAC_KEY=test SIPF_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + NAGDI_EVIDENCE_SIGNING_JWK=test NAGDI_EVIDENCE_SIGNING_PUBLIC_JWK=test NAGDI_EVIDENCE_AUDIT_HMAC_KEY=test NAGDI_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + CRA_PENSION_EVIDENCE_CLIENT_PRIVATE_JWK=test CRA_CITIZEN_EVIDENCE_CLIENT_PRIVATE_JWK=test \ + MOSD_CHILD_BENEFIT_EVIDENCE_CLIENT_PRIVATE_JWK=test \ + SIPF_PENSION_EVIDENCE_CLIENT_PRIVATE_JWK=test SIPF_SURVIVOR_EVIDENCE_CLIENT_PRIVATE_JWK=test \ + NAGDI_VOUCHER_EVIDENCE_CLIENT_PRIVATE_JWK=test NAGDI_LIVESTOCK_EVIDENCE_CLIENT_PRIVATE_JWK=test \ + CRA_PENSION_EVIDENCE_CLIENT_PUBLIC_JWK=test CRA_CITIZEN_EVIDENCE_CLIENT_PUBLIC_JWK=test \ + MOSD_CHILD_BENEFIT_EVIDENCE_CLIENT_PUBLIC_JWK=test \ + SIPF_PENSION_EVIDENCE_CLIENT_PUBLIC_JWK=test SIPF_SURVIVOR_EVIDENCE_CLIENT_PUBLIC_JWK=test \ + NAGDI_VOUCHER_EVIDENCE_CLIENT_PUBLIC_JWK=test NAGDI_LIVESTOCK_EVIDENCE_CLIENT_PUBLIC_JWK=test \ + NIA_ESIGNET_CLIENT_PUBLIC_JWK=test SOLMARA_DEMO_CLIENT_PUBLIC_JWK=test \ + docker compose "$@" config >/dev/null +} + +for compose in "$root"/compose.coolify*.yaml; do + if [ "$(basename "$compose")" = "compose.coolify.esignet.yaml" ]; then + continue + fi + check_compose -f "$compose" done + +# eSignet is intentionally an overlay: validate that it augments the same +# Portal service rather than accidentally declaring a second Portal deployment. +check_compose -f "$root/compose.coolify.yaml" -f "$root/compose.coolify.esignet.yaml" diff --git a/scripts/check-evidence-cells.sh b/scripts/check-evidence-cells.sh new file mode 100755 index 0000000..2d1a360 --- /dev/null +++ b/scripts/check-evidence-cells.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +project_name=${COMPOSE_PROJECT_NAME:-$(python3 "$root/scripts/compose_project_name.py")} + +compose() { + COMPOSE_PROJECT_NAME="$project_name" docker compose \ + --env-file "$root/versions.env" \ + --env-file "$root/.env" \ + -f "$root/compose.yaml" "$@" +} + +for cell in cra nia sro mosd-programme sipf nagdi; do + compose exec -T "$cell-evidence" evidence check \ + --runtime "/etc/registry-evidence/$cell/runtime.yaml" + for fixture in "$root/runtime/evidence-cells/cells/$cell/bundle/fixtures/"*.yaml; do + compose exec -T "$cell-evidence" evidence evaluate \ + --runtime "/etc/registry-evidence/$cell/runtime.yaml" \ + --fixture "fixtures/$(basename "$fixture")" + done +done + +printf '%s\n' 'evidence-check: six authority cells and all eleven requirement fixtures passed' diff --git a/scripts/check-evidence-runtime.py b/scripts/check-evidence-runtime.py deleted file mode 100755 index d41a31c..0000000 --- a/scripts/check-evidence-runtime.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 -"""Validate the authored Evidence bundle and paired Mint configuration.""" - -from __future__ import annotations - -import shutil -import stat -import subprocess -import sys -import tempfile -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -LOCAL = ROOT / "config/evidence/local" - - -def main() -> int: - version = versions()["REGISTRYCTL_VERSION"] - require_tool_version("evidencectl", version) - require_tool_version("mint", version) - required = ( - LOCAL / "evidence/audit-hmac-key", - LOCAL / "evidence/signing-ed25519-private-jwk", - LOCAL / "evidence/subject-binding-hmac-key", - LOCAL / "mint/audit-hmac-key", - LOCAL / "mint/signing.jwk", - LOCAL / "mint/clients/solmara-demo.yaml", - LOCAL / "tls/ca.crt", - LOCAL / "tls/gateway.crt", - ) - missing = [path for path in required if not path.is_file()] - if missing: - names = ", ".join(str(path.relative_to(ROOT)) for path in missing) - raise SystemExit(f"missing generated Evidence material: {names}; run `just gen-secrets`") - - with tempfile.TemporaryDirectory(prefix="solmara-evidence-check-") as directory: - stage = Path(directory) - evidence_project = stage / "evidence" - shutil.copytree(ROOT / "evidence", evidence_project) - make_immutable(evidence_project / "bundle") - audit = stage / "audit" - audit.mkdir(mode=0o700) - evidence_secrets = stage / "evidence-secrets" - evidence_secrets.mkdir(mode=0o700) - for name in ( - "audit-hmac-key", - "signing-ed25519-private-jwk", - "subject-binding-hmac-key", - ): - shutil.copy2(LOCAL / "evidence" / name, evidence_secrets / name) - for authority in ("cra", "nia", "sro", "programme", "sipf", "nagdi"): - token = evidence_secrets / f"{authority}-relay-token" - token.write_text("fixture-only-token") - token.chmod(0o600) - staged_ca = stage / "ca.crt" - shutil.copy2(LOCAL / "tls/ca.crt", staged_ca) - staged_ca.chmod(staged_ca.stat().st_mode & ~stat.S_IWUSR) - - runtime = (evidence_project / "runtime.yaml").read_text() - replacements = { - "/etc/registry-evidence/bundle": str(evidence_project / "bundle"), - "bindHost: 172.29.0.10": "bindHost: 127.0.0.1", - "/run/secrets/registry-evidence": str(evidence_secrets), - "/var/lib/registry-evidence/audit/evidence.jsonl": str(audit / "evidence.jsonl"), - "/etc/registry-evidence/tls/lab-ca.crt": str(staged_ca), - } - for old, new in replacements.items(): - runtime = runtime.replace(old, new) - staged_runtime = evidence_project / "runtime.yaml" - staged_runtime.write_text(runtime) - staged_runtime.chmod(staged_runtime.stat().st_mode & ~stat.S_IWUSR) - - mint_config = (ROOT / "evidence/mint.yaml").read_text() - mint_replacements = { - "/run/secrets/registry-mint/": f"{LOCAL / 'mint'}/", - "/var/lib/registry-mint/audit/mint.jsonl": str(audit / "mint.jsonl"), - "/etc/registry-mint/clients": str(LOCAL / "mint/clients"), - } - for old, new in mint_replacements.items(): - mint_config = mint_config.replace(old, new) - staged_mint = stage / "mint.yaml" - staged_mint.write_text(mint_config) - - subprocess.run(["mint", "check", "--config", str(staged_mint)], check=True) - subprocess.run( - ["evidencectl", "fixtures", "run", "--project", str(evidence_project)], - check=True, - ) - return 0 - - -def versions() -> dict[str, str]: - return { - key: value - for line in (ROOT / "versions.env").read_text().splitlines() - if line and not line.startswith("#") and "=" in line - for key, value in [line.split("=", 1)] - } - - -def require_tool_version(command: str, expected: str) -> None: - path = shutil.which(command) - if not path: - raise SystemExit(f"{command} is missing; install Registry Stack {expected} tools from the pinned source checkout") - output = subprocess.run([path, "--version"], check=True, capture_output=True, text=True).stdout.strip() - actual = output.rsplit(" ", 1)[-1] - if actual != expected: - raise SystemExit(f"{command} {actual} does not match pinned Registry Stack {expected}") - - -def make_immutable(root: Path) -> None: - for path in [root, *root.rglob("*")]: - mode = path.stat().st_mode - path.chmod(mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)) - - -if __name__ == "__main__": - try: - raise SystemExit(main()) - except subprocess.CalledProcessError as error: - raise SystemExit(error.returncode) from error diff --git a/scripts/check-hosted-compose.sh b/scripts/check-hosted-compose.sh new file mode 100755 index 0000000..370cedb --- /dev/null +++ b/scripts/check-hosted-compose.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +test_digest=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +test_image="ghcr.io/registrystack/solmara-compose-check@sha256:$test_digest" + +REGISTRY_STACK_RELEASE_RELAY_DIGEST="$test_digest" \ +SOLMARA_EVIDENCE_IMAGE="$test_image" \ +SOLMARA_MINT_IMAGE="$test_image" \ +SOLMARA_STATIC_METADATA_IMAGE="$test_image" \ +SOLMARA_SCENARIO_RUNNER_IMAGE="$test_image" \ +SOLMARA_HOME_IMAGE="$test_image" \ +SOLMARA_PORTAL_IMAGE="$test_image" \ +docker compose \ + --env-file "$root/versions.env" \ + --env-file "$root/.env" \ + -f "$root/compose.yaml" \ + -f "$root/compose.hosted.yaml" \ + config >/dev/null diff --git a/scripts/check-hosted-relay-bundles.py b/scripts/check-hosted-relay-bundles.py deleted file mode 100755 index 735019e..0000000 --- a/scripts/check-hosted-relay-bundles.py +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/env python3 -"""Verify committed hosted Relay bundles and their generated source closure.""" - -from __future__ import annotations - -import copy -import json -import subprocess -from pathlib import Path - -import yaml - - -ROOT = Path(__file__).resolve().parents[1] -PROJECTS = ( - "cra-civil", - "nia-population", - "sro-social", - "mosd-programme", - "sipf-pensions", - "nagdi-agriculture", -) -BUNDLE_VARIANTS = ( - ("public", "relay.yaml", "", False), - ("consultation", "relay-consultation.yaml", "consultation", True), -) -BUNDLE_ROOT = ROOT / "config" / "hosted-relay-bundles" -CONTAINER_ROOT = Path("/etc/solmara/hosted-relay-bundles") -ANTIROLLBACK_PATH = ( - "/var/lib/registry-relay/cache/config-bundle-antirollback.json" -) - - -def fail(message: str) -> None: - raise SystemExit(f"check-hosted-relay-bundles: {message}") - - -def registryctl_path() -> str: - result = subprocess.run( - [str(ROOT / "scripts" / "registryctl-pinned.sh"), "path"], - check=True, - stdout=subprocess.PIPE, - text=True, - ) - return result.stdout.strip() - - -def expected_bundle_sequence() -> int: - for raw_line in (ROOT / "versions.env").read_text(encoding="utf-8").splitlines(): - if raw_line.startswith("SOLMARA_RELAY_BUNDLE_SEQUENCE="): - raw_sequence = raw_line.split("=", 1)[1] - break - else: - fail("versions.env must set SOLMARA_RELAY_BUNDLE_SEQUENCE") - try: - sequence = int(raw_sequence) - except ValueError: - fail("SOLMARA_RELAY_BUNDLE_SEQUENCE must be a positive integer") - if sequence < 1: - fail("SOLMARA_RELAY_BUNDLE_SEQUENCE must be a positive integer") - return sequence - - -def load_yaml(path: Path) -> dict[str, object]: - value = yaml.safe_load(path.read_text(encoding="utf-8")) - if not isinstance(value, dict): - fail(f"{path.relative_to(ROOT)} must contain a mapping") - return value - - -def regular_file_tree(root: Path) -> dict[str, bytes]: - if not root.is_dir(): - fail(f"missing artifact directory {root}") - files: dict[str, bytes] = {} - for path in sorted(root.rglob("*")): - relative = path.relative_to(root).as_posix() - if path.is_symlink(): - fail(f"artifact tree contains symlink {relative}") - if path.is_dir(): - continue - if not path.is_file(): - fail(f"artifact tree contains non-file {relative}") - files[relative] = path.read_bytes() - return files - - -def verify_artifact_closure( - project: str, bundled_artifacts: Path, source_artifacts: Path -) -> None: - bundled_files = regular_file_tree(bundled_artifacts) - source_files = regular_file_tree(source_artifacts) - if bundled_files.keys() != source_files.keys(): - fail(f"{project} signed artifact paths differ from compiler output") - for relative, bundled_content in bundled_files.items(): - if bundled_content != source_files[relative]: - fail( - f"{project} signed artifact {relative} differs from compiler output" - ) - - -def main() -> int: - registryctl = registryctl_path() - expected_sequence = expected_bundle_sequence() - for project in PROJECTS: - for variant, source_name, output_subdirectory, include_artifacts in BUNDLE_VARIANTS: - project_dir = BUNDLE_ROOT / project - container_dir = CONTAINER_ROOT / project - if output_subdirectory: - project_dir /= output_subdirectory - container_dir /= output_subdirectory - bootstrap_path = project_dir / "bootstrap.yaml" - anchor_path = project_dir / "trust-anchor.json" - seed_path = project_dir / "antirollback-seed.json" - bundle_dir = project_dir / "bundle" - bundle_config_path = bundle_dir / "config" / "relay.yaml" - source_config_path = ( - ROOT - / "runtime" - / "registry-projects" - / "hosted" - / project - / "relay" - / source_name - ) - bundled_artifacts = bundle_dir / "config" / "artifacts" - source_artifacts = source_config_path.parent / "artifacts" - for required in ( - bootstrap_path, - anchor_path, - seed_path, - bundle_dir / "manifest.json", - bundle_dir / "manifest.sig.json", - bundle_config_path, - source_config_path, - ): - if not required.is_file(): - fail(f"missing {required.relative_to(ROOT)}") - - bootstrap = load_yaml(bootstrap_path) - bundled = load_yaml(bundle_config_path) - source = load_yaml(source_config_path) - if variant == "public" and "consultation" in source: - fail(f"{project} public Relay config contains consultation authority") - if variant == "consultation" and "consultation" not in source: - fail(f"{project} consultation Relay config omits consultation authority") - if bootstrap != bundled: - fail( - f"{project} {variant} bootstrap config differs from signed config" - ) - - expected_trust = { - "trust_anchor_path": str(container_dir / "trust-anchor.json"), - "bundle_path": str(container_dir / "bundle"), - "antirollback_state_path": ANTIROLLBACK_PATH, - } - if bundled.get("config_trust") != expected_trust: - fail( - f"{project} {variant} config trust paths are not deployment-bound" - ) - unsigned_projection = copy.deepcopy(bundled) - unsigned_projection.pop("config_trust", None) - if unsigned_projection != source: - fail(f"{project} {variant} signed config differs from compiler output") - if include_artifacts: - verify_artifact_closure( - f"{project} {variant}", bundled_artifacts, source_artifacts - ) - elif bundled_artifacts.exists(): - fail(f"{project} public bundle contains private consultation artifacts") - - anchor = json.loads(anchor_path.read_text(encoding="utf-8")) - manifest = json.loads( - (bundle_dir / "manifest.json").read_text(encoding="utf-8") - ) - instance_id = source["instance"]["id"] - stream_suffix = "" if variant == "public" else "-consultation" - stream_id = f"solmara-hosted-{project}{stream_suffix}" - expected_binding = { - "product": "registry-relay", - "environment": "hosted", - "stream_id": stream_id, - "instance_id": instance_id, - } - for key, expected in expected_binding.items(): - if anchor.get(key) != expected or manifest.get(key) != expected: - fail(f"{project} {variant} has an incorrect {key} binding") - if manifest.get("sequence") != expected_sequence: - fail( - f"{project} {variant} bundle sequence must match " - "SOLMARA_RELAY_BUNDLE_SEQUENCE" - ) - if any( - "d" in signer.get("jwk", {}) for signer in anchor.get("signers", []) - ): - fail(f"{project} {variant} trust anchor contains private key material") - seed = json.loads(seed_path.read_text(encoding="utf-8")) - expected_seed = { - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": stream_id, - }, - "last_sequence": 0, - "last_config_hash": f"sha256:{'0' * 64}", - } - if seed != expected_seed: - fail( - f"{project} {variant} anti-rollback seed is not " - "the sequence-zero baseline" - ) - - subprocess.run( - [ - registryctl, - "bundle", - "verify", - "--bundle-dir", - str(bundle_dir), - "--anchor-path", - str(anchor_path), - ], - check=True, - stdout=subprocess.DEVNULL, - ) - print("check-hosted-relay-bundles: ok") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check-image-pins.py b/scripts/check-image-pins.py index 96e79cb..5fe284a 100755 --- a/scripts/check-image-pins.py +++ b/scripts/check-image-pins.py @@ -8,14 +8,19 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] -LINE_RE = re.compile(r"^([A-Z0-9_]+)=([^#\s]+)$") +LINE_RE = re.compile(r"^([A-Z0-9_]+)=([^#\s]*)$") PIN_RE = re.compile(r"^[^#\s]+@sha256:[0-9a-f]{64}$") SOURCE_IMAGE_KEYS = { "REGISTRY_RELAY_IMAGE", "SOLMARA_EVIDENCE_IMAGE", "SOLMARA_MINT_IMAGE", } -PINNED_IMAGE_KEYS = {"VOLUME_INIT_IMAGE", "EVIDENCE_GATEWAY_IMAGE"} +PINNED_IMAGE_KEYS = { + "VOLUME_INIT_IMAGE", "EVIDENCE_GATEWAY_IMAGE", "PYTHON_STATIC_IMAGE", + "NODE_BUILD_IMAGE", "UV_BUILD_IMAGE", + "ESIGNET_REDIS_IMAGE", "ESIGNET_BASE_IMAGE", "ESIGNET_UI_IMAGE", + "ESIGNET_POSTGRES_IMAGE", +} def main() -> int: @@ -60,6 +65,14 @@ def main() -> int: if count == 0: failures.append(f"compose files: expected a required {key} reference") + for deployment in (ROOT / "compose.hosted.yaml", *sorted(ROOT.glob("compose.coolify*.yaml"))): + if not deployment.exists(): + continue + text = deployment.read_text(encoding="utf-8") + for nonexistent in ("ghcr.io/registrystack/evidence", "ghcr.io/registrystack/mint"): + if nonexistent in text: + failures.append(f"{deployment.name}: {nonexistent} is not a published Registry Stack image") + if failures: for failure in failures: print(failure, file=sys.stderr) diff --git a/scripts/check-local-transit-providers.py b/scripts/check-local-transit-providers.py new file mode 100755 index 0000000..a603710 --- /dev/null +++ b/scripts/check-local-transit-providers.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Require all external local Transit proxies before starting runtimes.""" + +from __future__ import annotations + +import stat +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PROVIDERS = ("mint", "cra", "nia", "sro", "mosd-programme", "sipf", "nagdi") + + +def failures(root: Path = ROOT) -> list[str]: + result = [] + for provider in PROVIDERS: + socket = root / "config/evidence/local/cells" / provider / "transit/transit-proxy.sock" + try: + mode = socket.stat().st_mode + except FileNotFoundError: + result.append(f"{provider}: Transit proxy socket is missing") + continue + if not stat.S_ISSOCK(mode): + result.append(f"{provider}: Transit provider path is not a Unix socket") + return result + + +def main() -> int: + found = failures() + if found: + print("\n".join(found), file=sys.stderr) + print("Start the seven external signer providers; private keys are never mounted into Evidence or Mint.", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check-registry-stack-release-pin.py b/scripts/check-registry-stack-release-pin.py new file mode 100755 index 0000000..49f1f77 --- /dev/null +++ b/scripts/check-registry-stack-release-pin.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Validate the coherent immutable Registry Stack release pins.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DIGEST = re.compile(r"^[0-9a-f]{64}$") +HTTPS = re.compile(r"^https://[^\s]+$") +REQUIRED_VERSION = "0.20.1" + + +def read_versions(path: Path) -> dict[str, str]: + result: dict[str, str] = {} + for raw in path.read_text(encoding="utf-8").splitlines(): + if raw and not raw.startswith("#") and "=" in raw: + key, value = raw.split("=", 1) + result[key] = value + return result + + +def validate(values: dict[str, str], *, require_public: bool) -> list[str]: + failures: list[str] = [] + if values.get("REGISTRY_STACK_REQUIRED_VERSION") != REQUIRED_VERSION: + failures.append(f"REGISTRY_STACK_REQUIRED_VERSION must be {REQUIRED_VERSION}") + source_ref = values.get("REGISTRY_STACK_SOURCE_REF", "") + expected_source_ref = f"v{REQUIRED_VERSION}" + if source_ref and source_ref != expected_source_ref: + failures.append(f"REGISTRY_STACK_SOURCE_REF must be {expected_source_ref}") + if require_public and not source_ref: + failures.append(f"REGISTRY_STACK_SOURCE_REF must bind the published {expected_source_ref} tag") + source_commit = values.get("REGISTRY_STACK_SOURCE_COMMIT", "") + if source_commit and not re.fullmatch(r"[0-9a-f]{40}", source_commit): + failures.append("REGISTRY_STACK_SOURCE_COMMIT must be 40 lowercase hex characters") + if require_public and not source_commit: + failures.append(f"REGISTRY_STACK_SOURCE_COMMIT is not final; v{REQUIRED_VERSION} promotion is blocked") + for key in ( + "REGISTRY_STACK_RELEASE_RELAY_DIGEST", + "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256", + "REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256", + "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256", + ): + value = values.get(key, "") + if value and not DIGEST.fullmatch(value): + failures.append(f"{key} must be 64 lowercase hex characters") + if require_public and not value: + failures.append(f"{key} is not published; v{REQUIRED_VERSION} promotion is blocked") + asset_names = { + "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL": f"evidence-v{REQUIRED_VERSION}-linux-amd64", + "REGISTRY_STACK_RELEASE_MINT_ASSET_URL": f"mint-v{REQUIRED_VERSION}-linux-amd64", + "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL": f"relayctl-v{REQUIRED_VERSION}-linux-amd64", + } + release_base = f"https://github.com/registrystack/registry-stack/releases/download/v{REQUIRED_VERSION}/" + for key, asset_name in asset_names.items(): + asset_url = values.get(key, "") + if asset_url and not HTTPS.fullmatch(asset_url): + failures.append(f"{key} must be an exact HTTPS URL") + elif asset_url and asset_url != release_base + asset_name: + failures.append(f"{key} must bind the exact v{REQUIRED_VERSION} linux-amd64 asset") + if require_public and not asset_url: + failures.append(f"{key} is not published; v{REQUIRED_VERSION} promotion is blocked") + if values.get("ESIGNET_AUTHENTICATOR_VERSION") != "0.2.0": + failures.append("ESIGNET_AUTHENTICATOR_VERSION must be 0.2.0") + release_url = "https://github.com/jeremi/esignet-relay-authenticator/releases/tag/v0.2.0" + if values.get("ESIGNET_AUTHENTICATOR_RELEASE_URL") != release_url: + failures.append("ESIGNET_AUTHENTICATOR_RELEASE_URL must bind the exact v0.2.0 release") + jar_url = values.get("ESIGNET_AUTHENTICATOR_JAR_URL", "") + jar_sha = values.get("ESIGNET_AUTHENTICATOR_JAR_SHA256", "") + if jar_url and not HTTPS.fullmatch(jar_url): + failures.append("ESIGNET_AUTHENTICATOR_JAR_URL must be an exact HTTPS URL") + if jar_sha and not DIGEST.fullmatch(jar_sha): + failures.append("ESIGNET_AUTHENTICATOR_JAR_SHA256 must be 64 lowercase hex characters") + expected_base = "https://github.com/jeremi/esignet-relay-authenticator/releases/download/v0.2.0/" + if jar_url and jar_url != expected_base + "esignet-relay-authenticator-0.2.0.jar": + failures.append("ESIGNET_AUTHENTICATOR_JAR_URL must bind the exact v0.2.0 release asset") + if values.get("ESIGNET_AUTHENTICATOR_CHECKSUM_URL", "") != expected_base + "esignet-relay-authenticator-0.2.0.jar.sha256": + failures.append("ESIGNET_AUTHENTICATOR_CHECKSUM_URL must bind the exact v0.2.0 checksum asset") + if require_public and (not jar_url or not jar_sha): + failures.append("eSignet authenticator v0.2.0 JAR URL/checksum is not published; promotion is blocked") + return failures + + +def main() -> int: + failures = validate(read_versions(ROOT / "versions.env"), require_public="--require-public" in sys.argv[1:]) + if failures: + print("\n".join(failures), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check-relay-projects.sh b/scripts/check-relay-projects.sh new file mode 100755 index 0000000..06b8635 --- /dev/null +++ b/scripts/check-relay-projects.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +set -a +. "$root/versions.env" +set +a + +cd "$root/generator" +uv run python -c 'from pathlib import Path; from solmara_lab.publisher import publish_relay_sources; publish_relay_sources(Path(".."))' + +temporary_root=$(mktemp -d "$root/output/relay-check.XXXXXX") +cleanup() { + rm -rf -- "$temporary_root" +} +trap cleanup EXIT HUP INT TERM + +cd "$root" +for authority in cra nia mosd sipf nagdi; do + project="relays/$authority" + database="$root/output/sqlite/relay/$authority.sqlite" + generated="$temporary_root/$authority-generated" + package="$temporary_root/$authority-package" + docker run --rm \ + --platform linux/amd64 \ + --user "$(id -u):$(id -g)" \ + --volume "$root:/workspace" \ + --volume "$database:/var/lib/relay/source/$authority.sqlite:ro" \ + --workdir /workspace \ + "$REGISTRY_RELAYCTL_IMAGE" \ + --json check "$project" --production >/dev/null + docker run --rm \ + --platform linux/amd64 \ + --user "$(id -u):$(id -g)" \ + --volume "$root:/workspace" \ + --volume "$database:/var/lib/relay/source/$authority.sqlite:ro" \ + --workdir /workspace \ + "$REGISTRY_RELAYCTL_IMAGE" \ + --json generate "$project" --output "${generated#"$root/"}" >/dev/null + docker run --rm \ + --platform linux/amd64 \ + --user "$(id -u):$(id -g)" \ + --volume "$root:/workspace" \ + --volume "$database:/var/lib/relay/source/$authority.sqlite:ro" \ + --workdir /workspace \ + "$REGISTRY_RELAYCTL_IMAGE" \ + --json test "$project" >/dev/null + docker run --rm \ + --platform linux/amd64 \ + --user "$(id -u):$(id -g)" \ + --volume "$root:/workspace" \ + --volume "$database:/var/lib/relay/source/$authority.sqlite:ro" \ + --workdir /workspace \ + "$REGISTRY_RELAYCTL_IMAGE" \ + --json package "$project" --output "${package#"$root/"}" >/dev/null +done + +printf '%s\n' 'relay-check: five production Relay projects passed check, generate, test, and package' diff --git a/scripts/check-release-pins.py b/scripts/check-release-pins.py deleted file mode 100755 index 0120e3d..0000000 --- a/scripts/check-release-pins.py +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env python3 -"""Verify Registry Stack release inputs match a published release tag.""" - -from __future__ import annotations - -import os -import re -import subprocess -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -IMAGE_KEYS = { - "REGISTRY_RELAY_IMAGE": "ghcr.io/registrystack/registry-relay", - "REGISTRY_NOTARY_IMAGE": "ghcr.io/registrystack/registry-notary", -} -REGISTRY_STACK_REMOTE = "https://github.com/registrystack/registry-stack.git" -PIN_RE = re.compile(r"^(?P[^@\s]+)@(?Psha256:[0-9a-f]{64})$") -DIGEST_RE = re.compile(r"^Digest:\s+(sha256:[0-9a-f]{64})$", re.MULTILINE) -TAG_RE = re.compile( - r"^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)" - r"(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" -) -COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") - - -def main(argv: list[str]) -> int: - if len(argv) != 2 or not argv[1]: - print("usage: check-release-pins.py ", file=sys.stderr) - return 2 - - tag = argv[1] - if not TAG_RE.fullmatch(tag): - print( - "check-release-pins: tag must match " - "vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-PRERELEASE", - file=sys.stderr, - ) - return 2 - - versions = read_versions(ROOT / "versions.env") - failures: list[str] = [] - release_version = tag.removeprefix("v") - - registryctl_version = versions.get("REGISTRYCTL_VERSION") - if registryctl_version != release_version: - failures.append( - "REGISTRYCTL_VERSION from versions.env " - f"is {registryctl_version or 'missing'}, expected {release_version}" - ) - - source_ref = versions.get("REGISTRY_STACK_SOURCE_REF") - if source_ref != tag: - failures.append( - "REGISTRY_STACK_SOURCE_REF from versions.env " - f"is {source_ref or 'missing'}, expected {tag}" - ) - source_commit = versions.get("REGISTRY_STACK_SOURCE_COMMIT") - if not source_commit or not COMMIT_RE.fullmatch(source_commit): - failures.append( - "REGISTRY_STACK_SOURCE_COMMIT must be exactly 40 lowercase hex characters" - ) - - for key in IMAGE_KEYS: - pinned = versions.get(key) - override = os.environ.get(key) - if pinned and override and override != pinned: - failures.append(f"{key} environment override must match versions.env") - - if failures: - for failure in failures: - print(f"check-release-pins: {failure}", file=sys.stderr) - return 1 - - try: - tag_commit = resolve_tag_commit(tag) - except (RuntimeError, subprocess.CalledProcessError): - print( - f"check-release-pins: could not resolve Registry Stack tag {tag}", - file=sys.stderr, - ) - return 1 - if source_commit != tag_commit: - print( - "check-release-pins: REGISTRY_STACK_SOURCE_COMMIT from versions.env " - f"is {source_commit}, but {tag} resolves to {tag_commit}", - file=sys.stderr, - ) - return 1 - - for key, image in IMAGE_KEYS.items(): - pinned = versions.get(key) - if not pinned: - failures.append(f"{key} is missing from versions.env") - continue - match = PIN_RE.match(pinned) - if not match: - failures.append(f"{key} must be image@sha256:") - continue - if match.group("image") != image: - failures.append(f"{key} points at {match.group('image')}, expected {image}") - continue - release_digest = inspect_tag_digest(f"{image}:{tag}") - if release_digest != match.group("digest"): - failures.append( - f"{key} from versions.env pins {match.group('digest')}, " - f"but {image}:{tag} resolves to {release_digest}" - ) - - if failures: - for failure in failures: - print(f"check-release-pins: {failure}", file=sys.stderr) - return 1 - - print(f"check-release-pins: Registry Stack release inputs match {tag}") - return 0 - - -def read_versions(path: Path) -> dict[str, str]: - values: dict[str, str] = {} - for raw in path.read_text().splitlines(): - line = raw.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, value = line.split("=", 1) - values[key] = value - return values - - -def inspect_tag_digest(ref: str) -> str: - result = subprocess.run( - ["docker", "buildx", "imagetools", "inspect", ref], - cwd=ROOT, - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - match = DIGEST_RE.search(result.stdout) - if not match: - raise RuntimeError(f"could not find digest in `docker buildx imagetools inspect {ref}` output") - return match.group(1) - - -def resolve_tag_commit(tag: str) -> str: - direct_ref = f"refs/tags/{tag}" - peeled_ref = f"{direct_ref}^{{}}" - result = subprocess.run( - ["git", "ls-remote", REGISTRY_STACK_REMOTE, direct_ref, peeled_ref], - cwd=ROOT, - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - refs: dict[str, str] = {} - for raw_line in result.stdout.splitlines(): - fields = raw_line.split() - if len(fields) != 2 or fields[1] not in {direct_ref, peeled_ref}: - raise RuntimeError("Registry Stack tag lookup returned an invalid ref") - commit, ref = fields - if ref in refs or not COMMIT_RE.fullmatch(commit): - raise RuntimeError("Registry Stack tag lookup returned an invalid commit") - refs[ref] = commit - - commit = refs.get(peeled_ref) or refs.get(direct_ref) - if not commit: - raise RuntimeError("Registry Stack tag lookup returned no matching tag") - return commit - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv)) diff --git a/scripts/check-runtime-topology.py b/scripts/check-runtime-topology.py new file mode 100755 index 0000000..29400f6 --- /dev/null +++ b/scripts/check-runtime-topology.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Fail closed when the active deployment regresses to retired topology.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +ACTIVE = [ + ROOT / "compose.yaml", + ROOT / "compose.hosted.yaml", + ROOT / "compose.esignet.yaml", + ROOT / "justfile", + ROOT / "config/evidence/Caddyfile", + ROOT / "config/walt/Caddyfile", + ROOT / "config/walt/README.md", + ROOT / "config/walt/registration-defaults.conf", + *sorted(ROOT.glob("compose.coolify*.yaml")), +] +FORBIDDEN = { + "retired database": re.compile(r"(?i)\bpostgres(?:ql)?\b"), + "retired workload agent": re.compile(r"(?i)workload[-_ ](?:identity[-_ ])?agent"), + "retired Notary": re.compile(r"(?i)\bnotary\b"), + "retired data-purpose": re.compile(r"(?i)data[-_]purpose"), + "retired authoring CLI": re.compile(r"\bregistryctl\b"), + "retired dataset route": re.compile(r"/v1/datasets\b"), +} + + +def failures(paths: list[Path] = ACTIVE) -> list[str]: + found: list[str] = [] + for path in paths: + text = path.read_text(encoding="utf-8") + # eSignet is the only surviving stateful third-party component. Its + # database is deliberately isolated from Registry Stack runtime state. + suppress_database = path.name in {"compose.esignet.yaml", "compose.coolify.esignet.yaml"} + for label, pattern in FORBIDDEN.items(): + if suppress_database and label == "retired database": + continue + for match in pattern.finditer(text): + line = text.count("\n", 0, match.start()) + 1 + found.append(f"{path.name}:{line}: {label}") + return found + + +def main() -> int: + found = failures() + if found: + print("\n".join(found), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check-signer-public-keys.py b/scripts/check-signer-public-keys.py new file mode 100755 index 0000000..fc476a1 --- /dev/null +++ b/scripts/check-signer-public-keys.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Verify every generated provider public JWK matches its operator private JWK.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PROVIDERS = ("mint", "cra", "nia", "sro", "mosd-programme", "sipf", "nagdi") +PUBLIC_MEMBERS = ("alg", "crv", "kid", "kty", "x", "y") + + +def failures(root: Path = ROOT) -> list[str]: + result: list[str] = [] + for provider in PROVIDERS: + private_path = root / "config/evidence/local/cells" / provider / "secrets/signing.jwk" + public_root = ( + root / "runtime/evidence-cells/mint/public-keys" + if provider == "mint" + else root / "runtime/evidence-cells/cells" / provider / "bundle/public-keys" + ) + try: + private = json.loads(private_path.read_text(encoding="utf-8")) + public_path = public_root / f"{private['kid']}.jwk.json" + public = json.loads(public_path.read_text(encoding="utf-8")) + except (FileNotFoundError, KeyError, json.JSONDecodeError): + result.append(f"{provider}: signer material is missing or invalid") + continue + if set(public) != set(PUBLIC_MEMBERS) or any(public.get(name) != private.get(name) for name in PUBLIC_MEMBERS): + result.append(f"{provider}: generated public key does not match operator signer") + return result + + +def main() -> int: + found = failures() + if found: + print("\n".join(found), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/contract-generation-proof.py b/scripts/contract-generation-proof.py deleted file mode 100755 index a6d898c..0000000 --- a/scripts/contract-generation-proof.py +++ /dev/null @@ -1,607 +0,0 @@ -#!/usr/bin/env python3 -"""Prove a compiler-generated SRO Relay/Notary blue-green transition. - -The proof is intentionally bounded to one authority. It compiles two complete -generations with the pinned registryctl, rejects the mixed pair during Notary -activation, and then activates the complete successor generation. -""" - -from __future__ import annotations - -import json -import os -import re -import shlex -import shutil -import subprocess -import sys -import tempfile -import time -import urllib.error -import urllib.request -from pathlib import Path -from typing import Any, Callable, Mapping, NoReturn, Sequence - -import yaml - - -ROOT = Path(__file__).resolve().parents[1] -PROJECT = ROOT / "projects" / "sro-social" -PURPOSE = "https://id.registrystack.org/solmara/purpose/child-benefit-review" -CLAIM_ID = "household-below-poverty-threshold" -BLUE_SUBJECT = "2300027390" -GREEN_SUBJECT = "2300018263" -RESULT_FORMAT = "application/vnd.registry-notary.claim-result+json" -MIXED_GENERATION_PROBLEM_CODE = "notary.relay.profile_mismatch" -SENSITIVE_ENV_MARKERS = ("TOKEN", "PASSWORD", "SECRET", "JWK") -MAX_DIAGNOSTIC_LINES = 12 -MAX_DIAGNOSTIC_LINE_BYTES = 256 -MAX_DIAGNOSTIC_BYTES = 4096 -ANSI_ESCAPE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]") -REGISTRY_RELAY_IMAGE = re.compile( - r"ghcr\.io/registrystack/registry-relay@sha256:[0-9a-f]{64}" -) - - -class ProofFailure(RuntimeError): - """A bounded proof assertion failed.""" - - -def read_env(path: Path) -> dict[str, str]: - values: dict[str, str] = {} - for raw_line in path.read_text(encoding="utf-8").splitlines(): - line = raw_line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - name, raw_value = line.split("=", 1) - parts = shlex.split(raw_value, posix=True) - if len(parts) > 1: - raise ProofFailure(f"{path.name} contains an invalid value for {name.strip()}") - values[name.strip()] = parts[0] if parts else "" - return values - - -def relay_image(environment: Mapping[str, str]) -> str: - image = environment.get("REGISTRY_RELAY_IMAGE", "") - if REGISTRY_RELAY_IMAGE.fullmatch(image) is None: - raise ProofFailure( - "REGISTRY_RELAY_IMAGE must pin the canonical Relay release by digest" - ) - return image - - -def diagnostic_environment(environment: Mapping[str, str] | None) -> dict[str, str]: - return dict(os.environ if environment is None else environment) - - -def bounded_redacted_output( - output: str, environment: Mapping[str, str] | None -) -> str: - redacted = output - values = [BLUE_SUBJECT, GREEN_SUBJECT] - values.extend( - value - for name, value in diagnostic_environment(environment).items() - if value and any(marker in name for marker in SENSITIVE_ENV_MARKERS) - ) - for value in sorted(set(values), key=len, reverse=True): - redacted = redacted.replace(value, "[redacted]") - redacted = ANSI_ESCAPE.sub("", redacted) - lines = [ - "".join( - character - if character.isprintable() or character == "\t" - else "?" - for character in line - ) - for line in redacted.splitlines() - ] - if not lines: - return "(no command output captured)" - if len(lines) > MAX_DIAGNOSTIC_LINES: - lines = [*lines[:6], "... output lines omitted ...", *lines[-5:]] - bounded_lines = [] - for line in lines: - encoded = line.encode("utf-8") - if len(encoded) > MAX_DIAGNOSTIC_LINE_BYTES: - line = encoded[:MAX_DIAGNOSTIC_LINE_BYTES].decode("utf-8", errors="ignore") - line += "..." - bounded_lines.append(line) - bounded = "\n".join(bounded_lines) - encoded = bounded.encode("utf-8") - if len(encoded) > MAX_DIAGNOSTIC_BYTES: - bounded = encoded[:MAX_DIAGNOSTIC_BYTES].decode("utf-8", errors="ignore") - return bounded - - -def command_failure( - executable: str, - returncode: int, - output: str, - environment: Mapping[str, str] | None, -) -> ProofFailure: - diagnostic = bounded_redacted_output(output, environment) - return ProofFailure( - f"{Path(executable).name} command failed with exit {returncode}\n" - f"command output (redacted and bounded):\n{diagnostic}" - ) - - -def run( - arguments: Sequence[str], - *, - environment: Mapping[str, str] | None = None, - timeout: int = 240, - check: bool = True, -) -> subprocess.CompletedProcess[str]: - result = subprocess.run( - list(arguments), - cwd=ROOT, - env=dict(environment) if environment is not None else None, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - timeout=timeout, - check=False, - ) - if check and result.returncode != 0: - raise command_failure(arguments[0], result.returncode, result.stdout, environment) - return result - - -def preserve_cleanup_failure( - cleanup: Callable[[], subprocess.CompletedProcess[str]], - *, - environment: Mapping[str, str] | None, - primary_failure_active: bool, -) -> None: - failure: ProofFailure | None = None - try: - result = cleanup() - except subprocess.TimeoutExpired: - failure = ProofFailure("docker Compose cleanup timed out") - except OSError: - failure = ProofFailure("docker Compose cleanup could not start") - else: - if result.returncode != 0: - failure = command_failure( - "docker compose cleanup", - result.returncode, - result.stdout, - environment, - ) - if failure is None: - return - if primary_failure_active: - print( - f"contract-generation-proof: secondary cleanup failure: {failure}", - file=sys.stderr, - ) - return - raise failure - - -def raise_mixed_notary_timeout( - container_name: str, - timeout_error: subprocess.TimeoutExpired, - *, - environment: Mapping[str, str] | None, - emergency_cleanup: Callable[[], subprocess.CompletedProcess[str]] | None = None, -) -> NoReturn: - cleanup = emergency_cleanup or ( - lambda: run( - ["docker", "rm", "--force", container_name], - environment=environment, - timeout=30, - check=False, - ) - ) - preserve_cleanup_failure( - cleanup, - environment=environment, - primary_failure_active=True, - ) - raise ProofFailure("mixed-generation Notary unexpectedly kept serving") from timeout_error - - -def make_successor(project: Path) -> None: - integration = project / "integrations" / "child-benefit-household-by-uin" / "integration.yaml" - document = yaml.safe_load(integration.read_text(encoding="utf-8")) - if document.get("id") != "child-benefit-household-by-uin" or document.get("revision") != 1: - raise ProofFailure("the SRO integration no longer has the expected blue revision") - document["revision"] = 2 - integration.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8") - - -def make_runtime_readable(root: Path) -> None: - root.chmod(0o755) - for path in root.rglob("*"): - path.chmod(0o755 if path.is_dir() else 0o644) - - -def build_generation(registryctl: Path, source: Path, destination: Path) -> str: - run( - [ - str(registryctl), - "build", - "--project-dir", - str(source), - "--environment", - "local", - ] - ) - private = source / ".registry-stack" / "build" / "local" / "private" - relay = destination / "relay" - notary = destination / "notary" - shutil.copytree(private / "relay" / "config", relay) - notary.mkdir(parents=True) - shutil.copy2(private / "notary" / "config" / "notary.yaml", notary / "notary.yaml") - make_runtime_readable(relay) - make_runtime_readable(notary) - document = yaml.safe_load((notary / "notary.yaml").read_text(encoding="utf-8")) - return document["evidence"]["claims"][0]["evidence_mode"]["consultations"]["household"]["profile"]["contract_hash"] - - -def write_override(path: Path, generation: Path, *, relay: bool, notary: bool) -> None: - services: dict[str, Any] = {} - relay_mounts = [ - f"{generation / 'relay'}:/etc/registry-relay:ro", - ] - notary_mounts = [ - f"{generation / 'notary' / 'notary.yaml'}:/etc/registry-notary/notary.yaml:ro", - ] - if relay: - services["sro-relay-state-bootstrap"] = {"volumes": relay_mounts} - services["sro-social-relay-consultation"] = {"volumes": relay_mounts} - if notary: - services["sro-notary-state-install"] = {"volumes": notary_mounts} - services["sro-notary"] = {"volumes": notary_mounts} - path.write_text(yaml.safe_dump({"services": services}, sort_keys=False), encoding="utf-8") - - -def compose_command(project_name: str, override: Path | None = None) -> list[str]: - command = [ - "docker", - "compose", - "--project-name", - project_name, - "--env-file", - str(ROOT / "versions.env"), - "--env-file", - str(ROOT / ".env"), - "--file", - str(ROOT / "compose.yaml"), - ] - if override is not None: - command.extend(["--file", str(override)]) - return command - - -def start_generation( - compose: Sequence[str], environment: Mapping[str, str] -) -> None: - result = run( - [ - *compose, - "up", - "--detach", - "--wait", - "--wait-timeout", - "180", - "sro-notary", - ], - environment=environment, - timeout=300, - check=False, - ) - if result.returncode == 0: - return - logs = run( - [ - *compose, - "logs", - "--no-color", - "postgres", - "registry-postgresql-bootstrap", - "sro-relay-state-bootstrap", - "sro-social-relay-consultation", - ], - environment=environment, - timeout=30, - check=False, - ) - raise command_failure( - "docker", - result.returncode, - f"{result.stdout}\nfailed service logs:\n{logs.stdout}", - environment, - ) - - -def wait_for_ready(url: str, timeout: int = 120) -> None: - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - try: - with urllib.request.urlopen(f"{url}/ready", timeout=2) as response: - if response.status in (200, 204): - return - except (OSError, urllib.error.URLError): - pass - time.sleep(1) - raise ProofFailure("SRO Notary did not become ready") - - -def shared_notary_url(compose: Sequence[str], environment: Mapping[str, str]) -> str: - result = run( - [*compose, "port", "sro-social-relay-consultation", "8081"], - environment=environment, - ) - address = result.stdout.strip().splitlines()[-1] - match = re.search(r":([0-9]+)$", address) - if match is None: - raise ProofFailure( - "could not resolve the SRO Notary port shared by its consultation Relay" - ) - return f"http://127.0.0.1:{match.group(1)}" - - -def evaluate(url: str, token: str, subject: str) -> dict[str, Any]: - body = json.dumps( - { - "target": { - "type": "Person", - "identifiers": [{"scheme": "solmara_uin", "value": subject}], - }, - "claims": [CLAIM_ID], - "disclosure": "predicate", - "format": RESULT_FORMAT, - }, - separators=(",", ":"), - ).encode("utf-8") - request = urllib.request.Request( - f"{url}/v1/evaluations", - data=body, - method="POST", - headers={ - "Accept": RESULT_FORMAT, - "Content-Type": "application/json", - "Data-Purpose": PURPOSE, - "x-api-key": token, - }, - ) - try: - with urllib.request.urlopen(request, timeout=30) as response: - payload = json.loads(response.read().decode("utf-8")) - status = response.status - except urllib.error.HTTPError as error: - error.read() - raise ProofFailure(f"SRO evaluation returned HTTP {error.code}") from error - if status != 200 or not successful_evaluation(payload, subject): - raise ProofFailure("SRO evaluation did not return the expected minimized predicate") - return payload - - -def successful_evaluation(payload: Any, subject: str) -> bool: - if not isinstance(payload, dict) or subject in json.dumps(payload, sort_keys=True): - return False - results = payload.get("results") - if not isinstance(results, list) or len(results) != 1 or not isinstance(results[0], dict): - return False - result = results[0] - return ( - result.get("claim_id") == CLAIM_ID - and result.get("disclosure") == "predicate" - and (result.get("value") is True or result.get("satisfied") is True) - ) - - -def relay_activity_counts( - compose: Sequence[str], environment: Mapping[str, str] -) -> tuple[int, int]: - state_epoch = environment.get("REGISTRY_RELAY_STATE_EPOCH", "") - if re.fullmatch(r"[a-z0-9_]+", state_epoch) is None: - raise ProofFailure("REGISTRY_RELAY_STATE_EPOCH is missing or invalid") - sql = ( - "SELECT (SELECT count(*) FROM relay_state_private.consultation_completion_intent)," - "(SELECT count(*) FROM relay_state_private.dispatch_permit WHERE dispatched_at IS NOT NULL);" - ) - user = environment.get("SOLMARA_POSTGRES_USER", "solmara_registry") - result = run( - [ - *compose, - "exec", - "--no-TTY", - "postgres", - "psql", - "--username", - user, - "--dbname", - f"solmara_relay_sro_consultation_{state_epoch}", - "--tuples-only", - "--no-align", - "--command", - sql, - ], - environment=environment, - ) - match = re.fullmatch(r"\s*([0-9]+)\|([0-9]+)\s*", result.stdout) - if match is None: - raise ProofFailure("could not read the SRO Relay execution counters") - return int(match.group(1)), int(match.group(2)) - - -def sensitive_patterns(environment: Mapping[str, str]) -> dict[str, bytes]: - patterns = { - "blue synthetic subject": BLUE_SUBJECT.encode("utf-8"), - "successor synthetic subject": GREEN_SUBJECT.encode("utf-8"), - } - for name, value in environment.items(): - if len(value) >= 16 and any(marker in name for marker in SENSITIVE_ENV_MARKERS): - patterns[f"credential from {name}"] = value.encode("utf-8") - return patterns - - -def scan_paths(paths: Sequence[Path], patterns: Mapping[str, bytes]) -> None: - for root in paths: - candidates = [root] if root.is_file() else sorted(path for path in root.rglob("*") if path.is_file()) - for path in candidates: - data = path.read_bytes() - for description, pattern in patterns.items(): - if pattern in data: - raise ProofFailure(f"{description} appeared in captured artifact {path.name}") - - -def main() -> int: - if not (ROOT / ".env").is_file(): - raise ProofFailure(".env is missing; run `just generate` first") - environment = read_env(ROOT / "versions.env") - environment.update(read_env(ROOT / ".env")) - environment.update(os.environ) - environment.update( - { - "REGISTRY_RELAY_IMAGE": relay_image(environment), - "SOLMARA_POSTGRES_PORT": "0", - "SOLMARA_SRO_RELAY_PORT": "0", - "SOLMARA_SRO_NOTARY_PORT": "0", - } - ) - token = environment.get("SRO_CHILD_BENEFIT_CLIENT_TOKEN", "") - if not token: - raise ProofFailure("generated SRO client credential is missing") - - registryctl_result = run([str(ROOT / "scripts" / "registryctl-pinned.sh"), "path"]) - registryctl = Path(registryctl_result.stdout.strip()) - project_name = f"solmara-contract-proof-{os.getpid()}" - - with tempfile.TemporaryDirectory(prefix="solmara-contract-proof-") as temporary: - workspace = Path(temporary) - blue_project = workspace / "projects" / "blue" - green_project = workspace / "projects" / "green" - shutil.copytree(PROJECT, blue_project) - shutil.copytree(PROJECT, green_project) - make_successor(green_project) - - print("contract-generation-proof: compiling blue and successor generations") - blue = workspace / "generations" / "blue" - green = workspace / "generations" / "green" - blue_hash = build_generation(registryctl, blue_project, blue) - green_hash = build_generation(registryctl, green_project, green) - if blue_hash == green_hash: - raise ProofFailure("the harmless successor did not move the consultation contract hash") - - blue_override = workspace / "blue.compose.yaml" - mixed_override = workspace / "mixed.compose.yaml" - green_override = workspace / "green.compose.yaml" - write_override(blue_override, blue, relay=True, notary=True) - write_override(mixed_override, green, relay=False, notary=True) - write_override(green_override, green, relay=True, notary=True) - blue_compose = compose_command(project_name, blue_override) - mixed_compose = compose_command(project_name, mixed_override) - green_compose = compose_command(project_name, green_override) - evidence = workspace / "evidence" - evidence.mkdir() - - try: - print("contract-generation-proof: starting the complete blue generation") - start_generation(blue_compose, environment) - blue_url = shared_notary_url(blue_compose, environment) - wait_for_ready(blue_url) - blue_response = evaluate(blue_url, token, BLUE_SUBJECT) - (evidence / "blue-response.json").write_text( - json.dumps(blue_response, sort_keys=True), encoding="utf-8" - ) - before_mixed = relay_activity_counts(blue_compose, environment) - if before_mixed[0] < 1: - raise ProofFailure("the blue generation did not execute its Relay consultation") - - print("contract-generation-proof: rejecting the mixed blue Relay / successor Notary") - run([*blue_compose, "stop", "sro-notary"], environment=environment) - run( - [ - *mixed_compose, - "run", - "--rm", - "--no-deps", - "sro-notary-state-install", - ], - environment=environment, - ) - mixed_name = f"{project_name}-mixed-notary" - try: - mixed = run( - [ - *mixed_compose, - "run", - "--rm", - "--no-deps", - "--name", - mixed_name, - "sro-notary", - ], - environment=environment, - timeout=45, - check=False, - ) - except subprocess.TimeoutExpired as error: - raise_mixed_notary_timeout( - mixed_name, - error, - environment=environment, - ) - (evidence / "mixed-notary.log").write_text(mixed.stdout, encoding="utf-8") - if mixed.returncode == 0: - raise ProofFailure("mixed-generation Notary unexpectedly activated") - expected_failure = f"ERROR {MIXED_GENERATION_PROBLEM_CODE}:" - if expected_failure not in mixed.stdout: - diagnostic = bounded_redacted_output(mixed.stdout, environment) - raise ProofFailure( - "mixed-generation Notary failed for an unexpected reason\n" - f"command output (redacted and bounded):\n{diagnostic}" - ) - after_mixed = relay_activity_counts(blue_compose, environment) - if after_mixed != before_mixed: - raise ProofFailure("mixed-generation activation reached Relay execute or source dispatch") - - blue_logs = run([*blue_compose, "logs", "--no-color"], environment=environment) - (evidence / "blue-services.log").write_text(blue_logs.stdout, encoding="utf-8") - run([*blue_compose, "down", "--remove-orphans"], environment=environment) - - print("contract-generation-proof: activating the complete successor generation") - start_generation(green_compose, environment) - green_url = shared_notary_url(green_compose, environment) - wait_for_ready(green_url) - green_response = evaluate(green_url, token, GREEN_SUBJECT) - (evidence / "green-response.json").write_text( - json.dumps(green_response, sort_keys=True), encoding="utf-8" - ) - after_green = relay_activity_counts(green_compose, environment) - if after_green[0] <= after_mixed[0]: - raise ProofFailure("the complete successor did not execute its Relay consultation") - (evidence / "green-services.log").write_text( - run([*green_compose, "logs", "--no-color"], environment=environment).stdout, - encoding="utf-8", - ) - - scan_paths([blue, green, evidence], sensitive_patterns(environment)) - finally: - preserve_cleanup_failure( - lambda: run( - [*green_compose, "down", "--volumes", "--remove-orphans"], - environment=environment, - timeout=120, - check=False, - ), - environment=environment, - primary_failure_active=sys.exc_info()[0] is not None, - ) - - print("contract-generation-proof: blue success, mixed rejection, and successor success passed") - return 0 - - -if __name__ == "__main__": - try: - raise SystemExit(main()) - except ProofFailure as error: - print(f"contract-generation-proof: {error}", file=sys.stderr) - raise SystemExit(1) from error diff --git a/scripts/gen-secrets.py b/scripts/gen-secrets.py index 2ec6a1b..c4ab270 100755 --- a/scripts/gen-secrets.py +++ b/scripts/gen-secrets.py @@ -1,374 +1,237 @@ #!/usr/bin/env python3 -"""Generate local .env credentials for Solmara Lab.""" +"""Create ignored local operator keys and the Compose environment.""" from __future__ import annotations -import argparse import base64 +import hashlib import json import secrets import shlex import subprocess from pathlib import Path +from typing import Callable + +from cryptography.hazmat.primitives.asymmetric import ec, rsa from compose_project_name import compose_project_name ROOT = Path(__file__).resolve().parents[1] -POSTGRES_SSL_DIR = ROOT / "config" / "postgres" / "ssl" -EVIDENCE_LOCAL_DIR = ROOT / "config" / "evidence" / "local" - -JWK_KIDS = { - "CRA_RELAY_WORKLOAD_JWK": "solmara-cra-relay-workload-key-1", - "NIA_RELAY_WORKLOAD_JWK": "solmara-nia-relay-workload-key-1", - "NIA_ESIGNET_RELAY_WORKLOAD_JWK": "solmara-nia-esignet-relay-workload-key-1", - "SRO_RELAY_WORKLOAD_JWK": "solmara-sro-relay-workload-key-1", - "PROGRAMME_RELAY_WORKLOAD_JWK": "solmara-programme-relay-workload-key-1", - "SIPF_RELAY_WORKLOAD_JWK": "solmara-sipf-relay-workload-key-1", - "NAGDI_RELAY_WORKLOAD_JWK": "solmara-nagdi-relay-workload-key-1", -} - -def raw_key() -> str: - return secrets.token_urlsafe(32) +LOCAL = ROOT / "config/evidence/local" +RANDOM_ENV_KEYS = ( + "CRA_RELAY_AUDIT_KEY", + "NIA_RELAY_AUDIT_KEY", + "MOSD_RELAY_AUDIT_KEY", + "SIPF_RELAY_AUDIT_KEY", + "SIPF_RELAY_CURSOR_KEY", + "NAGDI_RELAY_AUDIT_KEY", + "NAGDI_RELAY_CURSOR_KEY", + "CHILD_BENEFIT_FEDERATOR_TOKEN", + "PORTAL_SESSION_SECRET", + "SOLMARA_ESIGNET_POSTGRES_PASSWORD", + "REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD", + "REGISTRY_ESIGNET_KYC_TOKEN_SECRET", + "REGISTRY_ESIGNET_PSUT_SECRET", +) def b64url(raw: bytes) -> str: return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") -def local_ed25519_jwk(kid: str) -> str: - private_der = subprocess.run( - ["openssl", "genpkey", "-algorithm", "ED25519", "-outform", "DER"], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ).stdout - public_der = subprocess.run( - ["openssl", "pkey", "-inform", "DER", "-pubout", "-outform", "DER"], - input=private_der, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ).stdout - private_seed = private_der[-32:] - public_key = public_der[-32:] +def raw_key() -> str: + return secrets.token_urlsafe(32) + + +def p256_jwk() -> str: + private = ec.generate_private_key(ec.SECP256R1()).private_numbers() + public = private.public_numbers jwk = { - "kty": "OKP", - "crv": "Ed25519", - "kid": kid, - "alg": "EdDSA", - "x": b64url(public_key), - "d": b64url(private_seed), + "kty": "EC", "crv": "P-256", "alg": "ES256", + "x": b64url(public.x.to_bytes(32, "big")), + "y": b64url(public.y.to_bytes(32, "big")), + "d": b64url(private.private_value.to_bytes(32, "big")), } + thumbprint = {key: jwk[key] for key in ("crv", "kty", "x", "y")} + jwk["kid"] = b64url(hashlib.sha256(json.dumps(thumbprint, separators=(",", ":"), sort_keys=True).encode()).digest()) return json.dumps(jwk, separators=(",", ":"), sort_keys=True) -def public_jwk(private_jwk: str) -> dict[str, str]: - jwk = json.loads(private_jwk) - return {key: jwk[key] for key in ("kty", "crv", "kid", "alg", "x")} +def rsa_jwk() -> str: + private = rsa.generate_private_key(public_exponent=65537, key_size=2048).private_numbers() + public = private.public_numbers + + def encode(number: int) -> str: + return b64url(number.to_bytes((number.bit_length() + 7) // 8, "big")) + + jwk = { + "kty": "RSA", "alg": "RS256", "n": encode(public.n), "e": encode(public.e), + "d": encode(private.d), "p": encode(private.p), "q": encode(private.q), + "dp": encode(private.dmp1), "dq": encode(private.dmq1), "qi": encode(private.iqmp), + } + thumbprint = {key: jwk[key] for key in ("e", "kty", "n")} + jwk["kid"] = b64url(hashlib.sha256(json.dumps(thumbprint, separators=(",", ":"), sort_keys=True).encode()).digest()) + return json.dumps(jwk, separators=(",", ":"), sort_keys=True) def write_private(path: Path, value: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(value.rstrip("\n") + "\n") + path.write_text(value.rstrip("\n") + "\n", encoding="utf-8") path.chmod(0o600) -def ensure_evidence_material() -> None: - evidence_dir = EVIDENCE_LOCAL_DIR / "evidence" - mint_dir = EVIDENCE_LOCAL_DIR / "mint" - tls_dir = EVIDENCE_LOCAL_DIR / "tls" - for directory in (evidence_dir, mint_dir / "clients", tls_dir): - directory.mkdir(parents=True, exist_ok=True) - - private_paths = ( - evidence_dir / "audit-hmac-key", - evidence_dir / "subject-binding-hmac-key", - evidence_dir / "signing-ed25519-private-jwk", - mint_dir / "signing.jwk", - mint_dir / "audit-hmac-key", - mint_dir / "client-private.jwk", - tls_dir / "ca.key", - tls_dir / "gateway.key", - ) - public_paths = ( - mint_dir / "clients" / "solmara-demo.yaml", - tls_dir / "ca.crt", - tls_dir / "gateway.crt", - ) - material_paths = (*private_paths, *public_paths) - present = tuple(path for path in material_paths if path.is_file()) - if len(present) == len(material_paths): - for path in private_paths: - path.chmod(0o600) - for path in public_paths: - path.chmod(0o644) - return - if present: - missing = ", ".join( - str(path.relative_to(EVIDENCE_LOCAL_DIR)) - for path in material_paths - if not path.is_file() - ) - raise SystemExit( - "incomplete local Evidence material: " - f"{missing}; run `just reset`, remove config/evidence/local/evidence, " - "config/evidence/local/mint, and config/evidence/local/tls, then rerun " - "`just gen-secrets`" - ) - - write_private(evidence_dir / "audit-hmac-key", raw_key()) - write_private(evidence_dir / "subject-binding-hmac-key", raw_key()) - write_private( - evidence_dir / "signing-ed25519-private-jwk", - local_ed25519_jwk("solmara-evidence-signing-key-1"), - ) - - mint_signing_jwk = local_ed25519_jwk("solmara-mint-signing-key-1") - client_jwk = local_ed25519_jwk("solmara-demo-client-key-1") - write_private(mint_dir / "signing.jwk", mint_signing_jwk) - write_private(mint_dir / "audit-hmac-key", raw_key()) - write_private(mint_dir / "client-private.jwk", client_jwk) - client = { - "clientId": "solmara-demo", - "principal": "https://id.registrystack.org/solmara/principal/demo-client", - "evidenceAudience": "https://id.registrystack.org/solmara/audience/demo-client", - "requesterTags": ["solmara-demo"], - "keys": [public_jwk(client_jwk)], +def create_once(path: Path, factory) -> None: + if not path.exists(): + write_private(path, factory()) + + +def load_environment(path: Path) -> dict[str, str]: + if not path.exists(): + return {} + values: dict[str, str] = {} + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if "=" not in line: + raise ValueError("generated environment contains a malformed entry") + key, encoded = line.split("=", 1) + if key in values: + raise ValueError("generated environment contains a duplicate entry") + try: + parsed = shlex.split(encoded, posix=True) + except ValueError as exc: + raise ValueError( + "generated environment contains a malformed value" + ) from exc + if len(parsed) != 1 or not parsed[0]: + raise ValueError(f"generated environment value is invalid for {key}") + values[key] = parsed[0] + return values + + +def create_environment_value( + existing: dict[str, str], key: str, factory: Callable[[], str] +) -> str: + value = existing.get(key) + if value is not None: + if not value: + raise ValueError(f"generated environment value is invalid for {key}") + return value + return factory() + + +def compose_environment_values( + existing: dict[str, str], operator_values: dict[str, str] +) -> dict[str, str]: + values = { + key: create_environment_value(existing, key, raw_key) + for key in RANDOM_ENV_KEYS } - client_path = mint_dir / "clients" / "solmara-demo.yaml" - client_path.write_text(json.dumps(client, indent=2, sort_keys=True) + "\n") - client_path.chmod(0o644) - - ca_certificate = tls_dir / "ca.crt" - ca_private_key = tls_dir / "ca.key" - certificate = tls_dir / "gateway.crt" - private_key = tls_dir / "gateway.key" - certificate_request = tls_dir / "gateway.csr" - ca_serial = tls_dir / "ca.srl" - for path in ( - ca_certificate, - ca_private_key, - certificate, - private_key, - certificate_request, - ca_serial, - ): - path.unlink(missing_ok=True) - san_names = [ - "localhost", - "mint.evidence.solmara.invalid", - "evidence.solmara.invalid", - "cra-relay.evidence.solmara.invalid", - "nia-relay.evidence.solmara.invalid", - "sro-relay.evidence.solmara.invalid", - "programme-relay.evidence.solmara.invalid", - "sipf-relay.evidence.solmara.invalid", - "nagdi-relay.evidence.solmara.invalid", - ] - subprocess.run( - [ - "openssl", - "req", - "-x509", - "-newkey", - "rsa:2048", - "-nodes", - "-days", - "3650", - "-subj", - "/CN=Solmara Lab Evidence Development CA", - "-addext", - "basicConstraints=critical,CA:TRUE", - "-addext", - "keyUsage=critical,keyCertSign,cRLSign", - "-keyout", - str(ca_private_key), - "-out", - str(ca_certificate), - ], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - subprocess.run( - [ - "openssl", - "req", - "-new", - "-newkey", - "rsa:2048", - "-nodes", - "-subj", - "/CN=evidence.solmara.invalid", - "-addext", - "subjectAltName=" + ",".join(f"DNS:{name}" for name in san_names), - "-addext", - "basicConstraints=critical,CA:FALSE", - "-addext", - "keyUsage=critical,digitalSignature,keyEncipherment", - "-addext", - "extendedKeyUsage=serverAuth", - "-keyout", - str(private_key), - "-out", - str(certificate_request), - ], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + values["PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64"] = create_environment_value( + existing, "PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64", rsa_private_key_b64 ) - subprocess.run( - [ - "openssl", - "x509", - "-req", - "-in", - str(certificate_request), - "-CA", - str(ca_certificate), - "-CAkey", - str(ca_private_key), - "-CAcreateserial", - "-days", - "3650", - "-copy_extensions", - "copy", - "-out", - str(certificate), - ], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - certificate_request.unlink() - ca_serial.unlink(missing_ok=True) - ca_private_key.chmod(0o600) - private_key.chmod(0o600) - ca_certificate.chmod(0o644) - certificate.chmod(0o644) - - -def local_rsa_private_key_b64() -> str: - private_pem = subprocess.run( - ["openssl", "genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048"], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ).stdout - return base64.b64encode(private_pem).decode("ascii") - - -def ensure_postgres_tls() -> None: - POSTGRES_SSL_DIR.mkdir(parents=True, exist_ok=True) - key_path = POSTGRES_SSL_DIR / "server.key" - cert_path = POSTGRES_SSL_DIR / "server.crt" - for path in (key_path, cert_path): - path.unlink(missing_ok=True) - subprocess.run( - [ - "openssl", - "req", - "-x509", - "-newkey", - "rsa:2048", - "-nodes", - "-days", - "365", - "-subj", - "/CN=postgres", - "-addext", - "subjectAltName=DNS:postgres,IP:127.0.0.1", - "-keyout", - str(key_path), - "-out", - str(cert_path), - ], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + values.update( + { + "COMPOSE_PROJECT_NAME": compose_project_name(ROOT), + "PORTAL_AUTH_PROVIDER": "mock", + "PORTAL_ESIGNET_CLIENT_ID": "solmara-portal", + "PORTAL_ESIGNET_CLIENT_KEY_ID": "solmara-portal-key-1", + **operator_values, + } ) - key_path.chmod(0o600) - cert_path.chmod(0o644) + return values -def env_line(key: str, value: str) -> str: - return f"{key}={shlex.quote(value)}" +def ensure_client_identifier(path: Path, client_id: str) -> None: + """Write an exact public identifier while preserving unrelated material.""" - -def write_env_file(output: Path, values: dict[str, str], header: str) -> None: - lines = [header, *[env_line(key, values[key]) for key in sorted(values)]] - output.write_text("\n".join(lines) + "\n") - output.chmod(0o600) + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists(): + current = path.read_text(encoding="utf-8") + if current == client_id: + path.chmod(0o600) + return + # Migrate the previous generator's single trailing newline only. Any + # other value is operator-owned divergence and must fail closed. + if current != f"{client_id}\n": + raise ValueError(f"client identifier does not match {path.name}") + path.write_text(client_id, encoding="utf-8") + path.chmod(0o600) -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.parse_args(argv) - - ensure_postgres_tls() - ensure_evidence_material() - postgres_user = "solmara_registry" - postgres_password = raw_key() - postgres_db = "solmara_lab" - nia_source_password = raw_key() - sipf_source_password = raw_key() - values: dict[str, str] = { - "COMPOSE_PROJECT_NAME": compose_project_name(ROOT), - "CRA_RELAY_AUDIT_HASH_SECRET": raw_key(), - "NIA_RELAY_AUDIT_HASH_SECRET": raw_key(), - "SRO_RELAY_AUDIT_HASH_SECRET": raw_key(), - "PROGRAMME_RELAY_AUDIT_HASH_SECRET": raw_key(), - "SIPF_RELAY_AUDIT_HASH_SECRET": raw_key(), - "NAGDI_RELAY_AUDIT_HASH_SECRET": raw_key(), - "REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD": raw_key(), - "REGISTRY_ESIGNET_KYC_TOKEN_SECRET": raw_key(), - "REGISTRY_ESIGNET_PSUT_SECRET": raw_key(), - "PORTAL_SESSION_SECRET": raw_key(), - "PORTAL_AUTH_PROVIDER": "mock", - "PORTAL_ESIGNET_CLIENT_ID": "solmara-portal", - "PORTAL_ESIGNET_CLIENT_KEY_ID": "solmara-portal-key-1", - "PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64": local_rsa_private_key_b64(), - "PORTAL_ESIGNET_ISSUER": "http://127.0.0.1:4308", - "PORTAL_ESIGNET_AUTHORIZATION_ENDPOINT": "http://127.0.0.1:4309/authorize", - "PORTAL_ESIGNET_TOKEN_ENDPOINT": "http://esignet:8088/v1/esignet/oauth/v2/token", - "PORTAL_ESIGNET_CLIENT_ASSERTION_AUDIENCE": "http://127.0.0.1:4308/v1/esignet/oauth/v2/token", - "PORTAL_ESIGNET_USERINFO_ENDPOINT": "http://esignet:8088/v1/esignet/oidc/userinfo", - "PORTAL_ESIGNET_REDIRECT_URI": "http://127.0.0.1:4300/auth/callback", - "PORTAL_ESIGNET_SCOPE": "openid profile", - "PORTAL_ESIGNET_SUBJECT_CLAIM": "individual_id", - "SOLMARA_ESIGNET_PUBLIC_BASE_URL": "http://127.0.0.1:4308", - "SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL": "http://127.0.0.1:4309", - "SOLMARA_POSTGRES_USER": postgres_user, - "SOLMARA_POSTGRES_PASSWORD": postgres_password, - "SOLMARA_POSTGRES_DB": postgres_db, - "NIA_SOURCE_POSTGRES_READER_PASSWORD": nia_source_password, - "SIPF_SOURCE_POSTGRES_READER_PASSWORD": sipf_source_password, - "SOLMARA_NIA_DATABASE_URL": f"postgres://solmara_source_nia_reader:{nia_source_password}@postgres:5432/{postgres_db}?sslmode=require", - "SOLMARA_SIPF_DATABASE_URL": f"postgres://solmara_source_sipf_reader:{sipf_source_password}@postgres:5432/{postgres_db}?sslmode=require", - "SOLMARA_ESIGNET_POSTGRES_PASSWORD": raw_key(), - "CHILD_BENEFIT_FEDERATOR_TOKEN": raw_key(), - "CHILD_BENEFIT_FEDERATOR_URL": "https://localhost:4341/child-benefit/", - # Host-side scenario smokes use the gateway's localhost certificate SAN. - # Compose services override these with the internal gateway hostnames. - "SOLMARA_EVIDENCE_URL": "https://localhost:4341", - "SOLMARA_MINT_URL": "https://localhost:4341", - "SOLMARA_MINT_ASSERTION_AUDIENCE": "https://mint.evidence.solmara.invalid/token", - "SOLMARA_EVIDENCE_CLIENT_ID": "solmara-demo", - "SOLMARA_EVIDENCE_CLIENT_KEY": str( - ROOT / "config/evidence/local/mint/client-private.jwk" - ), - "SOLMARA_EVIDENCE_CA_BUNDLE": str( - ROOT / "config/evidence/local/tls/ca.crt" - ), +def ensure_operator_material() -> dict[str, str]: + cells = { + "cra": ("cra-pension-evidence", "cra-citizen-evidence"), + "nia": (), "sro": (), + "mosd-programme": ("mosd-child-benefit-evidence",), + "sipf": ("sipf-pension-evidence", "sipf-survivor-evidence"), + "nagdi": ("nagdi-voucher-evidence", "nagdi-livestock-evidence"), } + for cell, clients in cells.items(): + secret_root = LOCAL / "cells" / cell / "secrets" + (LOCAL / "cells" / cell / "transit").mkdir(parents=True, exist_ok=True) + create_once(secret_root / "signing.jwk", p256_jwk) + create_once(secret_root / "audit-hmac-key", raw_key) + create_once(secret_root / "subject-binding-hmac-key", raw_key) + for client in clients: + create_once(secret_root / f"{client}-client-key", p256_jwk) + ensure_client_identifier(secret_root / f"{client}-client-id", client) + + mint = LOCAL / "cells" / "mint" + for directory in (mint / "secrets", mint / "clients", mint / "transit"): + directory.mkdir(parents=True, exist_ok=True) + create_once(mint / "secrets/signing.jwk", p256_jwk) + create_once(mint / "secrets/audit-hmac-key", raw_key) + create_once(mint / "clients/nia-esignet-rsa-client-key", rsa_jwk) + create_once(mint / "clients/solmara-demo-client-key", p256_jwk) + return { + "NIA_ESIGNET_CLIENT_PRIVATE_JWK": (mint / "clients/nia-esignet-rsa-client-key").read_text().strip(), + "SOLMARA_EVIDENCE_CLIENT_KEY": str(mint / "clients/solmara-demo-client-key"), + } + + +def ensure_tls() -> None: + tls = LOCAL / "tls" + tls.mkdir(parents=True, exist_ok=True) + ca_key, ca_crt = tls / "ca.key", tls / "ca.crt" + key, crt, csr = tls / "gateway.key", tls / "gateway.crt", tls / "gateway.csr" + sans = [ + "localhost", "mint.solmara.registrystack.org", "evidence.solmara.invalid", + "cra-relay.solmara.registrystack.org", "mosd-programme-relay.solmara.registrystack.org", + "sipf-relay.solmara.registrystack.org", "nagdi-relay.solmara.registrystack.org", + ] + if all(path.exists() for path in (ca_key, ca_crt, key, crt)): + certificate = subprocess.run( + ["openssl", "x509", "-in", str(crt), "-noout", "-ext", "subjectAltName"], + check=True, text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + ).stdout + if all(f"DNS:{name}" in certificate for name in sans): + return + for path in (ca_key, ca_crt, key, crt, csr, tls / "ca.srl"): + path.unlink(missing_ok=True) + subprocess.run(["openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "3650", "-subj", "/CN=Solmara Lab CA", "-keyout", str(ca_key), "-out", str(ca_crt)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(["openssl", "req", "-new", "-newkey", "rsa:2048", "-nodes", "-subj", "/CN=localhost", "-addext", "subjectAltName=" + ",".join(f"DNS:{name}" for name in sans), "-keyout", str(key), "-out", str(csr)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(["openssl", "x509", "-req", "-in", str(csr), "-CA", str(ca_crt), "-CAkey", str(ca_key), "-CAcreateserial", "-days", "3650", "-copy_extensions", "copy", "-out", str(crt)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + csr.unlink() + (tls / "ca.srl").unlink(missing_ok=True) + ca_key.chmod(0o600) + key.chmod(0o600) + ca_crt.chmod(0o644) + crt.chmod(0o644) + + +def rsa_private_key_b64() -> str: + return base64.b64encode(subprocess.run(["openssl", "genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048"], check=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout).decode() - for name, kid in JWK_KIDS.items(): - values[name] = local_ed25519_jwk(kid) +def main() -> int: + operator_values = ensure_operator_material() + ensure_tls() output = ROOT / ".env" - write_env_file( - output, values, "# Generated by scripts/gen-secrets.py. Do not commit." - ) + values = compose_environment_values(load_environment(output), operator_values) + output.write_text("# Generated by scripts/gen-secrets.py. Do not commit.\n" + "\n".join(f"{key}={shlex.quote(value)}" for key, value in sorted(values.items())) + "\n", encoding="utf-8") + output.chmod(0o600) print(f"Wrote {output}") return 0 diff --git a/scripts/generate-hosted-relay-bundles.py b/scripts/generate-hosted-relay-bundles.py deleted file mode 100755 index ef47073..0000000 --- a/scripts/generate-hosted-relay-bundles.py +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/env python3 -"""Generate signed, instance-bound Relay bundles for hosted Solmara.""" - -from __future__ import annotations - -import argparse -import json -import shutil -import subprocess -import tempfile -from pathlib import Path - -import yaml - - -ROOT = Path(__file__).resolve().parents[1] -PROJECTS = ( - "cra-civil", - "nia-population", - "sro-social", - "mosd-programme", - "sipf-pensions", - "nagdi-agriculture", -) -BUNDLE_VARIANTS = ( - ("public", "relay.yaml", "", False), - ("consultation", "relay-consultation.yaml", "consultation", True), -) -CONTAINER_ROOT = Path("/etc/solmara/hosted-relay-bundles") -ANTIROLLBACK_PATH = ( - "/var/lib/registry-relay/cache/config-bundle-antirollback.json" -) - - -def run(*args: str) -> None: - subprocess.run(args, check=True) - - -def registryctl_path() -> str: - result = subprocess.run( - [str(ROOT / "scripts" / "registryctl-pinned.sh"), "path"], - check=True, - stdout=subprocess.PIPE, - text=True, - ) - return result.stdout.strip() - - -def configured_bundle_sequence() -> int: - for raw_line in (ROOT / "versions.env").read_text(encoding="utf-8").splitlines(): - if raw_line.startswith("SOLMARA_RELAY_BUNDLE_SEQUENCE="): - raw_sequence = raw_line.split("=", 1)[1] - break - else: - raise SystemExit("versions.env must set SOLMARA_RELAY_BUNDLE_SEQUENCE") - try: - sequence = int(raw_sequence) - except ValueError: - raise SystemExit( - "SOLMARA_RELAY_BUNDLE_SEQUENCE must be a positive integer" - ) from None - if sequence < 1: - raise SystemExit("SOLMARA_RELAY_BUNDLE_SEQUENCE must be a positive integer") - return sequence - - -def validate_private_jwk_reference(value: str) -> str: - if value.startswith("op://"): - if not value.removeprefix("op://").strip() or any( - character in value for character in ("\r", "\n", "\0") - ): - raise SystemExit("invalid 1Password private JWK reference") - return value - - key_path = Path(value) - if not key_path.is_file(): - raise SystemExit(f"missing signing key: {key_path}") - private_jwk = json.loads(key_path.read_text(encoding="utf-8")) - if "d" not in private_jwk: - raise SystemExit("expected a private JWK or op:// secret reference") - return value - - -def validate_public_jwk(path: Path) -> Path: - if not path.is_file(): - raise SystemExit(f"missing signing key: {path}") - public_jwk = json.loads(path.read_text(encoding="utf-8")) - if "d" in public_jwk: - raise SystemExit("expected a public-only JWK") - return path - - -def write_governed_config( - source: Path, - destination: Path, - container_dir: Path, -) -> dict[str, object]: - config = yaml.safe_load(source.read_text(encoding="utf-8")) - config["config_trust"] = { - "trust_anchor_path": str(container_dir / "trust-anchor.json"), - "bundle_path": str(container_dir / "bundle"), - "antirollback_state_path": ANTIROLLBACK_PATH, - } - destination.write_text( - yaml.safe_dump(config, sort_keys=False), - encoding="utf-8", - ) - return config - - -def generate_bundle( - registryctl: str, - project: str, - variant: str, - source_name: str, - output_subdirectory: str, - include_artifacts: bool, - private_jwk: str, - public_jwk: Path, - sequence: int, - output_root: Path, -) -> None: - runtime_dir = ROOT / "runtime" / "registry-projects" / "hosted" / project / "relay" - source_config = runtime_dir / source_name - if not source_config.is_file(): - raise SystemExit(f"missing hosted {variant} Relay config: {source_config}") - - source_document = yaml.safe_load(source_config.read_text(encoding="utf-8")) - if variant == "public" and "consultation" in source_document: - raise SystemExit(f"{project} public Relay config contains consultation authority") - if variant == "consultation" and "consultation" not in source_document: - raise SystemExit(f"{project} consultation Relay config omits consultation authority") - instance_id = source_document["instance"]["id"] - stream_suffix = "" if variant == "public" else "-consultation" - stream_id = f"solmara-hosted-{project}{stream_suffix}" - project_output = output_root / project - container_dir = CONTAINER_ROOT / project - if output_subdirectory: - project_output /= output_subdirectory - container_dir /= output_subdirectory - - with tempfile.TemporaryDirectory( - prefix=f"solmara-{project}-{variant}-bundle-" - ) as temporary: - staging = Path(temporary) - input_config = staging / "input" / "config" - input_config.mkdir(parents=True) - governed_config = input_config / "relay.yaml" - write_governed_config(source_config, governed_config, container_dir) - artifacts = runtime_dir / "artifacts" - if include_artifacts: - if not artifacts.is_dir(): - raise SystemExit(f"missing hosted consultation artifacts: {artifacts}") - shutil.copytree(artifacts, input_config / "artifacts") - - bundle_dir = staging / "bundle" - run( - registryctl, - "bundle", - "sign", - "--input", - str(staging / "input"), - "--key", - private_jwk, - "--product", - "registry-relay", - "--environment", - "hosted", - "--stream-id", - stream_id, - "--instance-id", - instance_id, - "--sequence", - str(sequence), - "--bundle-id", - f"{stream_id}-sequence-{sequence}", - "--out", - str(bundle_dir), - ) - - anchor = staging / "trust-anchor.json" - run( - registryctl, - "anchor", - "init", - "--anchor-path", - str(anchor), - "--product", - "registry-relay", - "--environment", - "hosted", - "--stream-id", - stream_id, - "--instance-id", - instance_id, - ) - run( - registryctl, - "anchor", - "add-key", - "--anchor-path", - str(anchor), - "--jwk-path", - str(public_jwk), - ) - run( - registryctl, - "bundle", - "verify", - "--bundle-dir", - str(bundle_dir), - "--anchor-path", - str(anchor), - ) - - project_output.mkdir(parents=True) - shutil.copy2(governed_config, project_output / "bootstrap.yaml") - shutil.copy2(anchor, project_output / "trust-anchor.json") - shutil.copytree(bundle_dir, project_output / "bundle") - seed = { - "key": { - "product": "registry-relay", - "environment": "hosted", - "stream_id": stream_id, - }, - "last_sequence": 0, - "last_config_hash": f"sha256:{'0' * 64}", - } - (project_output / "antirollback-seed.json").write_text( - json.dumps(seed, indent=2) + "\n", - encoding="utf-8", - ) - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--private-jwk", - required=True, - help="Private JWK path or op:// secret reference", - ) - parser.add_argument("--public-jwk", type=Path, required=True) - parser.add_argument("--sequence", type=int) - parser.add_argument("--out", type=Path, required=True) - args = parser.parse_args() - - configured_sequence = configured_bundle_sequence() - sequence = args.sequence if args.sequence is not None else configured_sequence - if sequence < 1: - raise SystemExit("--sequence must be positive") - if sequence != configured_sequence: - raise SystemExit( - "--sequence must match SOLMARA_RELAY_BUNDLE_SEQUENCE in versions.env" - ) - private_jwk = validate_private_jwk_reference(args.private_jwk) - public_jwk = validate_public_jwk(args.public_jwk) - if args.out.exists(): - raise SystemExit(f"output path must not exist: {args.out}") - - registryctl = registryctl_path() - args.out.mkdir(parents=True) - for project in PROJECTS: - for variant in BUNDLE_VARIANTS: - generate_bundle( - registryctl, - project, - *variant, - private_jwk, - public_jwk, - sequence, - args.out, - ) - for path in args.out.rglob("*"): - path.chmod(0o755 if path.is_dir() else 0o644) - print(f"Wrote {args.out}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/hosted-authority-rollout.py b/scripts/hosted-authority-rollout.py new file mode 100755 index 0000000..b1fddad --- /dev/null +++ b/scripts/hosted-authority-rollout.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Print the non-destructive authority-cell rollout operation for one phase.""" + +from __future__ import annotations + +import argparse + +PHASES = { + "side-by-side": "start authority-cell services with public routes disabled", + "switch": "enable authority-cell public routes after health and evidence checks", + "disable": "stop superseded services; retain every superseded volume", +} + + +def operation(phase: str) -> dict[str, str]: + if phase not in PHASES: + raise ValueError("unknown rollout phase") + return { + "phase": phase, + "action": PHASES[phase], + "volumePolicy": "retain", + "destructiveCommand": "none", + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("phase", choices=tuple(PHASES)) + args = parser.parse_args() + plan = operation(args.phase) + for key, value in plan.items(): + print(f"{key}={value}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/hosted-image-manifest.py b/scripts/hosted-image-manifest.py new file mode 100755 index 0000000..6bd1a91 --- /dev/null +++ b/scripts/hosted-image-manifest.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Build and validate the sanitized image handoff for hosted Solmara deployments.""" + +from __future__ import annotations + +import argparse +import os +import re +import sys +import tempfile +from collections.abc import Mapping, Sequence +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +IMAGE_REPOSITORIES = ( + ("SOLMARA_EVIDENCE_IMAGE", "solmara-lab-evidence"), + ("SOLMARA_MINT_IMAGE", "solmara-lab-mint"), + ( + "SOLMARA_AUTHORITY_PROVISIONER_IMAGE", + "solmara-lab-authority-provisioner", + ), + ("SOLMARA_TRANSIT_SIGNER_IMAGE", "solmara-lab-transit-signer"), + ("SOLMARA_STATIC_METADATA_IMAGE", "solmara-lab-static-metadata"), + ("SOLMARA_SCENARIO_RUNNER_IMAGE", "solmara-lab-scenario-runner"), + ("SOLMARA_HOME_IMAGE", "solmara-lab-home"), + ("SOLMARA_PORTAL_IMAGE", "solmara-lab-portal"), + ("SOLMARA_ESIGNET_RELAY_IMAGE", "solmara-lab-esignet-relay"), + ("SOLMARA_ESIGNET_POSTGRES_IMAGE", "solmara-lab-esignet-postgres"), + ("SOLMARA_ESIGNET_UI_IMAGE", "solmara-lab-esignet-ui"), + ("SOLMARA_ESIGNET_SEED_IMAGE", "solmara-lab-esignet-seed"), +) +EXPECTED_KEYS = tuple(key for key, _repository in IMAGE_REPOSITORIES) +EXPECTED_REPOSITORIES = dict(IMAGE_REPOSITORIES) +MANIFEST_LINE_RE = re.compile(r"^([A-Z][A-Z0-9_]*)=([^\s#]+)$") +HOSTED_IMAGE_LINE_RE = re.compile( + r"^\s*image:\s*\$\{(SOLMARA_[A-Z0-9_]+_IMAGE):\?[^}]+\}\s*(?:#.*)?$" +) +DIGEST_RE = re.compile(r"^[0-9a-f]{64}$") + + +class ManifestError(ValueError): + """A hosted image handoff violates its closed contract.""" + + +def expected_reference(key: str, digest: str) -> str: + repository = EXPECTED_REPOSITORIES[key] + return f"ghcr.io/registrystack/{repository}@sha256:{digest}" + + +def validate_reference(key: str, value: str) -> None: + prefix = expected_reference(key, "") + if not value.startswith(prefix): + raise ManifestError( + f"{key} must reference {prefix}<64 lowercase hex>" + ) + digest = value.removeprefix(prefix) + if DIGEST_RE.fullmatch(digest) is None: + raise ManifestError( + f"{key} must be a full ghcr.io image@sha256:<64 lowercase hex> reference" + ) + + +def parse_manifest(text: str) -> dict[str, str]: + if not text.endswith("\n"): + raise ManifestError("manifest must end with one newline") + if "\r" in text: + raise ManifestError("manifest must use LF line endings") + + values: dict[str, str] = {} + keys: list[str] = [] + for line_number, line in enumerate(text[:-1].split("\n"), start=1): + match = MANIFEST_LINE_RE.fullmatch(line) + if match is None: + raise ManifestError( + f"line {line_number} must contain exactly NAME=image@sha256:<64 hex>" + ) + key, value = match.groups() + if key in values: + raise ManifestError(f"line {line_number} duplicates {key}") + if key not in EXPECTED_REPOSITORIES: + raise ManifestError(f"line {line_number} contains unexpected key {key}") + validate_reference(key, value) + values[key] = value + keys.append(key) + + missing = [key for key in EXPECTED_KEYS if key not in values] + if missing: + raise ManifestError(f"manifest is missing {', '.join(missing)}") + if tuple(keys) != EXPECTED_KEYS: + raise ManifestError("manifest keys are not in canonical order") + return values + + +def render_manifest(environment: Mapping[str, str]) -> str: + missing = [key for key in EXPECTED_KEYS if not environment.get(key)] + if missing: + raise ManifestError(f"environment is missing {', '.join(missing)}") + + text = "".join(f"{key}={environment[key]}\n" for key in EXPECTED_KEYS) + parse_manifest(text) + return text + + +def read_manifest(path: Path) -> str: + return path.read_bytes().decode("utf-8") + + +def hosted_compose_paths(root: Path) -> tuple[Path, ...]: + return (root / "compose.hosted.yaml", *sorted(root.glob("compose.coolify*.yaml"))) + + +def validate_hosted_compose_inventory(root: Path) -> None: + paths = hosted_compose_paths(root) + missing_files = [path.name for path in paths if not path.is_file()] + if missing_files: + raise ManifestError(f"hosted Compose file is missing: {', '.join(missing_files)}") + + observed: set[str] = set() + malformed: list[str] = [] + for path in paths: + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + if "image:" not in line or "${SOLMARA_" not in line: + continue + match = HOSTED_IMAGE_LINE_RE.fullmatch(line) + if match is None: + malformed.append(f"{path.name}:{line_number}") + continue + observed.add(match.group(1)) + + if malformed: + raise ManifestError( + "hosted SOLMARA image references must be required variables: " + + ", ".join(malformed) + ) + + expected = set(EXPECTED_KEYS) + if observed != expected: + missing = sorted(expected - observed) + unexpected = sorted(observed - expected) + detail: list[str] = [] + if missing: + detail.append(f"missing {', '.join(missing)}") + if unexpected: + detail.append(f"unexpected {', '.join(unexpected)}") + raise ManifestError("hosted Compose image inventory mismatch: " + "; ".join(detail)) + + +def write_manifest(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=path.parent, + prefix=f".{path.name}.", + delete=False, + ) as temporary: + temporary.write(text) + temporary.flush() + temporary_path = Path(temporary.name) + temporary_path.chmod(0o644) + temporary_path.replace(path) + + +def parser() -> argparse.ArgumentParser: + argument_parser = argparse.ArgumentParser(description=__doc__) + argument_parser.add_argument( + "--compose-root", + type=Path, + default=ROOT, + help="repository root containing hosted Compose files", + ) + subparsers = argument_parser.add_subparsers(dest="command", required=True) + write = subparsers.add_parser("write", help="write a canonical manifest from the environment") + write.add_argument("--output", type=Path, required=True) + validate = subparsers.add_parser("validate", help="validate an existing canonical manifest") + validate.add_argument("--manifest", type=Path, required=True) + subparsers.add_parser("inventory", help="validate the hosted Compose image inventory") + return argument_parser + + +def main(argv: Sequence[str] | None = None) -> int: + arguments = parser().parse_args(argv) + try: + validate_hosted_compose_inventory(arguments.compose_root) + if arguments.command == "write": + text = render_manifest(os.environ) + write_manifest(arguments.output, text) + parse_manifest(read_manifest(arguments.output)) + elif arguments.command == "validate": + parse_manifest(read_manifest(arguments.manifest)) + except (ManifestError, OSError, UnicodeError) as error: + print(f"hosted image manifest: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/hosted-runtime-assets.py b/scripts/hosted-runtime-assets.py new file mode 100755 index 0000000..7893f9f --- /dev/null +++ b/scripts/hosted-runtime-assets.py @@ -0,0 +1,485 @@ +#!/usr/bin/env python3 +"""Assemble the closed, secret-free hosted authority provisioning payload.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shutil +import stat +import subprocess +import sys +import tempfile +from pathlib import Path, PurePosixPath + +AUTHORITIES = ("cra", "nia", "mosd", "sipf", "nagdi") +EVIDENCE_CELLS = ("cra", "nia", "sro", "mosd-programme", "sipf", "nagdi") +GENERATOR_FILES = frozenset({"__init__.py", "generate.py", "publisher.py"}) +RELAY_FILES = { + "cra": frozenset( + { + "codelists/record-lifecycle.yaml", + "expected-http.yaml", + "fixture.sql", + "governance/classification-review-rationale.md", + "governance/classification-review.yaml", + "governance/identifier-lifecycle.yaml", + "governance/legal-basis.yaml", + "governance/processing.dpv.yaml", + "registry.yaml", + "runtime.yaml", + } + ), + "nia": frozenset( + { + "codelists/identity-status.yaml", + "codelists/record-lifecycle.yaml", + "codelists/sex.yaml", + "expected-http.yaml", + "fixture.sql", + "governance/classification-review-rationale.md", + "governance/classification-review.yaml", + "governance/identifier-lifecycle.yaml", + "governance/legal-basis.yaml", + "governance/processing.dpv.yaml", + "registry.yaml", + "runtime.yaml", + } + ), + "mosd": frozenset( + { + "codelists/record-lifecycle.yaml", + "expected-http.yaml", + "fixture.sql", + "governance/classification-review-rationale.md", + "governance/classification-review.yaml", + "governance/identifier-lifecycle.yaml", + "governance/legal-basis.yaml", + "governance/processing.dpv.yaml", + "registry.yaml", + "runtime.yaml", + } + ), + "sipf": frozenset( + { + "codelists/payment-status.yaml", + "codelists/record-lifecycle.yaml", + "expected-http.yaml", + "fixture.sql", + "governance/classification-review-rationale.md", + "governance/classification-review.yaml", + "governance/identifier-lifecycle.yaml", + "governance/legal-basis.yaml", + "governance/processing.dpv.yaml", + "registry.yaml", + "runtime.yaml", + } + ), + "nagdi": frozenset( + { + "codelists/record-lifecycle.yaml", + "expected-http.yaml", + "fixture.sql", + "governance/classification-review-rationale.md", + "governance/classification-review.yaml", + "governance/identifier-lifecycle.yaml", + "governance/legal-basis.yaml", + "governance/processing.dpv.yaml", + "registry.yaml", + "runtime.yaml", + } + ), +} +EVIDENCE_FILES = { + "cra": frozenset( + { + "bundle/adapters/birth-extract.rhai", + "bundle/adapters/relay-extract.rhai", + "bundle/adapters/relay-prepare.rhai", + "bundle/derivations/child-benefit.rhai", + "bundle/derivations/deceased.rhai", + "bundle/derivations/linked.rhai", + "bundle/evidence.yaml", + "bundle/fixtures/child-benefit.yaml", + "bundle/fixtures/citizen.yaml", + "bundle/fixtures/pension.yaml", + "bundle/queries/birth-evidence.sql", + "bundle/schemas/birth-facts.schema.yaml", + "bundle/schemas/birth-response.schema.yaml", + "bundle/schemas/deceased-facts.schema.yaml", + "bundle/schemas/deceased-response.schema.yaml", + "bundle/schemas/linked-facts.schema.yaml", + "bundle/schemas/linked-response.schema.yaml", + "bundle/schemas/relay-adapter-parameters.schema.yaml", + "runtime.yaml", + } + ), + "nia": frozenset( + { + "bundle/adapters/sqlite-extract.rhai", + "bundle/derivations/population-active.rhai", + "bundle/evidence.yaml", + "bundle/fixtures/child-benefit.yaml", + "bundle/fixtures/citizen.yaml", + "bundle/queries/population-evidence.sql", + "bundle/schemas/population-facts.schema.yaml", + "bundle/schemas/population-response.schema.yaml", + "runtime.yaml", + } + ), + "sro": frozenset( + { + "bundle/adapters/sqlite-extract.rhai", + "bundle/derivations/poverty-priority.rhai", + "bundle/evidence.yaml", + "bundle/fixtures/child-benefit.yaml", + "bundle/queries/poverty-evidence.sql", + "bundle/schemas/poverty-facts.schema.yaml", + "bundle/schemas/poverty-response.schema.yaml", + "runtime.yaml", + } + ), + "mosd-programme": frozenset( + { + "bundle/adapters/relay-extract.rhai", + "bundle/adapters/relay-prepare.rhai", + "bundle/derivations/not-enrolled.rhai", + "bundle/evidence.yaml", + "bundle/fixtures/child-benefit.yaml", + "bundle/schemas/facts.schema.yaml", + "bundle/schemas/relay-adapter-parameters.schema.yaml", + "bundle/schemas/response.schema.yaml", + "runtime.yaml", + } + ), + "sipf": frozenset( + { + "bundle/adapters/relay-extract.rhai", + "bundle/adapters/relay-prepare.rhai", + "bundle/derivations/pension-active.rhai", + "bundle/derivations/survivor-eligible.rhai", + "bundle/evidence.yaml", + "bundle/fixtures/pension.yaml", + "bundle/fixtures/survivor.yaml", + "bundle/schemas/pension-facts.schema.yaml", + "bundle/schemas/pension-response.schema.yaml", + "bundle/schemas/relay-adapter-parameters.schema.yaml", + "bundle/schemas/survivor-facts.schema.yaml", + "bundle/schemas/survivor-response.schema.yaml", + "runtime.yaml", + } + ), + "nagdi": frozenset( + { + "bundle/adapters/relay-extract.rhai", + "bundle/adapters/relay-prepare.rhai", + "bundle/derivations/livestock.rhai", + "bundle/derivations/voucher.rhai", + "bundle/evidence.yaml", + "bundle/fixtures/livestock.yaml", + "bundle/fixtures/voucher.yaml", + "bundle/schemas/livestock-facts.schema.yaml", + "bundle/schemas/livestock-response.schema.yaml", + "bundle/schemas/relay-adapter-parameters.schema.yaml", + "bundle/schemas/voucher-facts.schema.yaml", + "bundle/schemas/voucher-response.schema.yaml", + "runtime.yaml", + } + ), +} +MANIFEST_NAME = "manifest.json" +MANIFEST_KEYS = frozenset({"format", "files"}) +SHA256_LENGTH = 64 +SAFE_PATH_COMPONENT = re.compile(r"^[A-Za-z0-9._-]+$") +FORBIDDEN_DIRECTORY_NAMES = frozenset( + { + ".cache", + ".pytest_cache", + ".ruff_cache", + "__pycache__", + "audit", + "audits", + "extracts", + "secrets", + } +) +FORBIDDEN_SUFFIXES = ( + ".db", + ".jwk", + ".jwk.json", + ".jsonl", + ".key", + ".log", + ".p12", + ".pem", + ".pfx", + ".pyc", + ".sqlite-journal", + ".sqlite-shm", + ".sqlite-wal", +) + + +class AssetBuildError(RuntimeError): + """Raised when the immutable provisioning payload cannot be trusted.""" + + +def _digest(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def _walk_files(root: Path) -> set[str]: + if root.is_symlink() or not root.is_dir(): + raise AssetBuildError("asset source is not a regular directory") + files: set[str] = set() + for current, directories, names in os.walk(root, followlinks=False): + current_path = Path(current) + for name in directories: + path = current_path / name + if path.is_symlink(): + raise AssetBuildError("asset source contains a symbolic link") + for name in names: + path = current_path / name + metadata = path.lstat() + if stat.S_ISLNK(metadata.st_mode): + raise AssetBuildError("asset source contains a symbolic link") + if not stat.S_ISREG(metadata.st_mode): + raise AssetBuildError("asset source contains a non-regular file") + files.add(path.relative_to(root).as_posix()) + return files + + +def _validate_exact_tree(root: Path, expected: frozenset[str]) -> None: + if _walk_files(root) != set(expected): + raise AssetBuildError("asset source inventory is not allowed") + + +def _copy_exact_tree(source: Path, destination: Path, files: frozenset[str]) -> None: + if destination.exists(): + raise AssetBuildError("asset destination is not empty") + for relative in sorted(files): + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source / relative, target) + + +def _path_is_sensitive(relative: str, *, allow_seed: bool = False) -> bool: + path = PurePosixPath(relative) + lowered = tuple(part.lower() for part in path.parts) + if any(part in FORBIDDEN_DIRECTORY_NAMES for part in lowered[:-1]): + return True + name = lowered[-1] + if "secret" in name or "private" in name: + return True + if name.endswith(FORBIDDEN_SUFFIXES): + return True + return name.endswith(".sqlite") and not allow_seed + + +def _validate_generated_tree(root: Path, *, allow_seed: bool = False) -> None: + files = _walk_files(root) + if not files: + raise AssetBuildError("generated asset tree is empty") + for relative in files: + if _path_is_sensitive(relative, allow_seed=allow_seed): + raise AssetBuildError("generated asset tree contains a forbidden artifact") + + +def _manifest(root: Path) -> dict[str, object]: + files: dict[str, str] = {} + for relative in sorted(_walk_files(root)): + if relative != MANIFEST_NAME: + files[relative] = _digest(root / relative) + if not files: + raise AssetBuildError("asset payload is empty") + return {"format": 1, "files": files} + + +def _manifest_bytes(manifest: dict[str, object]) -> bytes: + return (json.dumps(manifest, indent=2, sort_keys=True) + "\n").encode("utf-8") + + +def _strict_json(data: bytes) -> object: + def object_pairs(pairs: list[tuple[str, object]]) -> dict[str, object]: + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + raise AssetBuildError("asset manifest contains duplicate keys") + result[key] = value + return result + + try: + return json.loads(data.decode("utf-8"), object_pairs_hook=object_pairs) + except (UnicodeError, json.JSONDecodeError): + raise AssetBuildError("asset manifest is invalid") from None + + +def verify_manifest(root: Path) -> None: + """Verify the exact, canonical manifest and every payload file.""" + + if root.is_symlink() or not root.is_dir(): + raise AssetBuildError("asset payload is unavailable") + manifest_path = root / MANIFEST_NAME + try: + raw = manifest_path.read_bytes() + manifest = _strict_json(raw) + if not isinstance(manifest, dict) or set(manifest) != MANIFEST_KEYS: + raise AssetBuildError("asset manifest shape is invalid") + if manifest.get("format") != 1 or isinstance(manifest.get("format"), bool): + raise AssetBuildError("asset manifest format is invalid") + files = manifest.get("files") + if not isinstance(files, dict) or not files: + raise AssetBuildError("asset manifest inventory is invalid") + for relative, expected_digest in files.items(): + if ( + not isinstance(relative, str) + or not relative + or PurePosixPath(relative).is_absolute() + or ".." in PurePosixPath(relative).parts + or relative != PurePosixPath(relative).as_posix() + or any( + not SAFE_PATH_COMPONENT.fullmatch(component) + for component in PurePosixPath(relative).parts + ) + or relative == MANIFEST_NAME + or not isinstance(expected_digest, str) + or len(expected_digest) != SHA256_LENGTH + or any( + character not in "0123456789abcdef" for character in expected_digest + ) + ): + raise AssetBuildError("asset manifest entry is invalid") + observed = _manifest(root) + if manifest != observed or raw != _manifest_bytes(observed): + raise AssetBuildError("asset manifest verification failed") + except AssetBuildError: + raise + except OSError: + raise AssetBuildError("asset manifest verification failed") from None + + +def _validate_sources(root: Path) -> None: + _validate_exact_tree(root / "generator" / "solmara_lab", GENERATOR_FILES) + for authority in AUTHORITIES: + _validate_exact_tree(root / "relays" / authority, RELAY_FILES[authority]) + for cell in EVIDENCE_CELLS: + _validate_exact_tree(root / "evidence" / "cells" / cell, EVIDENCE_FILES[cell]) + mint = root / "evidence" / "mint.yaml" + if mint.is_symlink() or not mint.is_file(): + raise AssetBuildError("Mint template is unavailable") + + +def _relay_environment() -> dict[str, str]: + return {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"} + + +def _prepare_packaging_project(project: Path, authority: str, seed: Path) -> None: + shutil.copyfile(seed, project / "source.sqlite") + runtime = project / "runtime.yaml" + hosted_path = f"/var/lib/relay/source/{authority}.sqlite" + content = runtime.read_text(encoding="utf-8") + if content.count(hosted_path) != 1: + raise AssetBuildError("Relay runtime source binding is unexpected") + runtime.write_text(content.replace(hosted_path, "source.sqlite"), encoding="utf-8") + + +def build(root: Path, output: Path, relayctl: Path) -> None: + """Build fresh Relay packages/sources and copy authored templates.""" + + root = root.absolute() + output = output.absolute() + relayctl = relayctl.absolute() + if output.exists() or output.is_symlink(): + raise AssetBuildError("asset destination already exists") + if not relayctl.is_file() or relayctl.is_symlink(): + raise AssetBuildError("relayctl is unavailable") + _validate_sources(root) + + sys.dont_write_bytecode = True + sys.path.insert(0, str(root / "generator")) + from solmara_lab import publisher # pylint: disable=import-outside-toplevel + + with tempfile.TemporaryDirectory(prefix="solmara-hosted-assets-") as temporary: + staging = Path(temporary) / "assets" + staging.mkdir() + + published = publisher.publish_relay_sources(staging) + for authority in AUTHORITIES: + project = Path(temporary) / "projects" / authority + _copy_exact_tree( + root / "relays" / authority, project, RELAY_FILES[authority] + ) + _prepare_packaging_project(project, authority, published[authority]) + package = staging / "relays" / authority / "package" + package.parent.mkdir(parents=True, exist_ok=True) + subprocess.run( + [ + str(relayctl), + "--json", + "package", + str(project), + "--output", + str(package), + ], + cwd=root, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=_relay_environment(), + ) + _validate_generated_tree(package) + runtime = staging / "relays" / authority / "runtime.yaml" + shutil.copyfile(root / "relays" / authority / "runtime.yaml", runtime) + seed = staging / "relays" / authority / "source" / f"{authority}.sqlite" + seed.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(published[authority], seed) + + for cell in EVIDENCE_CELLS: + _copy_exact_tree( + root / "evidence" / "cells" / cell, + staging / "evidence" / "cells" / cell, + EVIDENCE_FILES[cell], + ) + mint = staging / "mint" / "mint.yaml" + mint.parent.mkdir(parents=True) + shutil.copyfile(root / "evidence" / "mint.yaml", mint) + _copy_exact_tree( + root / "generator" / "solmara_lab", + staging / "generator" / "solmara_lab", + GENERATOR_FILES, + ) + + manifest = _manifest(staging) + (staging / MANIFEST_NAME).write_bytes(_manifest_bytes(manifest)) + verify_manifest(staging) + shutil.copytree(staging, output) + verify_manifest(output) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("build", choices=("build",)) + parser.add_argument("--root", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--relayctl", required=True, type=Path) + args = parser.parse_args() + try: + build(args.root, args.output, args.relayctl) + # This is the security redaction boundary. Neither dependency failures nor + # relayctl diagnostics may disclose paths, configuration, or source values. + except Exception: # noqa: BLE001 + print("hosted runtime asset build failed", file=sys.stderr) + return 1 + print("hosted runtime assets ready") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/hosted-transit-signer.py b/scripts/hosted-transit-signer.py new file mode 100644 index 0000000..08f6f87 --- /dev/null +++ b/scripts/hosted-transit-signer.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python3 +"""Confine one hosted signing secret to one Transit proxy process.""" + +from __future__ import annotations + +import argparse +import base64 +import errno +import hashlib +import json +import os +import socket +import stat +import sys +import tempfile +from pathlib import Path + +from cryptography.hazmat.primitives.asymmetric import ec + +MAX_SECRET_BYTES = 16 * 1024 +SECRET_PATH = Path("/run/secrets/signing.jwk") +PUBLIC_PATH = Path("/run/secrets/signing-public.jwk") +SOCKET_PATH = Path("/transit/transit-proxy.sock") +STAGING_ROOT = Path("/tmp") +ALLOWED_KEY_NAMES = frozenset( + { + "solmara-mint", + "solmara-evidence-cra", + "solmara-evidence-nia", + "solmara-evidence-sro", + "solmara-evidence-mosd-programme", + "solmara-evidence-sipf", + "solmara-evidence-nagdi", + } +) +GENERIC_ERROR = "hosted Transit signer could not start" + + +class SignerError(Exception): + """A value-free signer configuration refusal.""" + + +class QuietArgumentParser(argparse.ArgumentParser): + """Keep rejected configuration values out of diagnostics.""" + + def error(self, message: str) -> None: + del message + raise SignerError("invalid arguments") + + +def _directory_flags() -> int: + flags = os.O_RDONLY + if hasattr(os, "O_DIRECTORY"): + flags |= os.O_DIRECTORY + if hasattr(os, "O_CLOEXEC"): + flags |= os.O_CLOEXEC + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + return flags + + +def _file_flags() -> int: + flags = os.O_RDONLY + if hasattr(os, "O_CLOEXEC"): + flags |= os.O_CLOEXEC + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + return flags + + +def _directory_is_confined(metadata: os.stat_result) -> bool: + if not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid not in {0, os.geteuid()}: + return False + if metadata.st_mode & 0o022 == 0: + return True + # Root-owned sticky directories such as /tmp prevent unprivileged callers + # from replacing a child they do not own. Every following component is + # still opened relative to a pinned descriptor and must be owner-controlled. + return metadata.st_uid == 0 and metadata.st_mode & stat.S_ISVTX != 0 + + +def _open_confined_secret(path: Path) -> int: + """Open an absolute secret without following any path component.""" + + if not path.is_absolute() or path.name in {"", ".", ".."}: + raise SignerError("invalid secret") + components = path.parts[1:] + if not components or any(component in {"", ".", ".."} for component in components): + raise SignerError("invalid secret") + + directory = os.open("/", _directory_flags()) + try: + for component in components[:-1]: + next_directory = os.open(component, _directory_flags(), dir_fd=directory) + os.close(directory) + directory = next_directory + metadata = os.fstat(directory) + if not _directory_is_confined(metadata): + raise SignerError("invalid secret") + return os.open(components[-1], _file_flags(), dir_fd=directory) + except OSError as error: + raise SignerError("invalid secret") from error + finally: + os.close(directory) + + +def _read_secret(path: Path) -> bytearray: + descriptor = _open_confined_secret(path) + try: + before = os.fstat(descriptor) + if ( + not stat.S_ISREG(before.st_mode) + or before.st_uid not in {0, os.geteuid()} + or before.st_mode & 0o022 + or before.st_nlink != 1 + or before.st_size <= 0 + or before.st_size > MAX_SECRET_BYTES + ): + raise SignerError("invalid secret") + + value = bytearray() + while len(value) <= MAX_SECRET_BYTES: + chunk = os.read(descriptor, min(4096, MAX_SECRET_BYTES + 1 - len(value))) + if not chunk: + break + value.extend(chunk) + after = os.fstat(descriptor) + if ( + len(value) > MAX_SECRET_BYTES + or len(value) != before.st_size + or ( + before.st_dev, + before.st_ino, + before.st_uid, + before.st_mode, + before.st_size, + ) + != (after.st_dev, after.st_ino, after.st_uid, after.st_mode, after.st_size) + ): + raise SignerError("invalid secret") + return value + except OSError as error: + raise SignerError("invalid secret") from error + finally: + os.close(descriptor) + + +def _stage_secret(source: Path, staging_root: Path = STAGING_ROOT) -> Path: + value = _read_secret(source) + try: + directory = Path(tempfile.mkdtemp(prefix="solmara-transit-", dir=staging_root)) + directory.chmod(0o700) + metadata = directory.lstat() + if ( + not stat.S_ISDIR(metadata.st_mode) + or metadata.st_uid != os.geteuid() + or stat.S_IMODE(metadata.st_mode) != 0o700 + ): + raise SignerError("invalid staging area") + + destination = directory / "signing.jwk" + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + if hasattr(os, "O_CLOEXEC"): + flags |= os.O_CLOEXEC + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + descriptor = os.open(destination, flags, 0o600) + try: + written = 0 + while written < len(value): + written += os.write(descriptor, value[written:]) + os.fchmod(descriptor, 0o600) + metadata = os.fstat(descriptor) + if ( + not stat.S_ISREG(metadata.st_mode) + or metadata.st_uid != os.geteuid() + or stat.S_IMODE(metadata.st_mode) != 0o600 + or metadata.st_size != len(value) + ): + raise SignerError("invalid staging area") + finally: + os.close(descriptor) + return destination + except OSError as error: + raise SignerError("invalid staging area") from error + finally: + value[:] = b"\0" * len(value) + + +def _b64url(raw: bytes) -> str: + return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") + + +def _b64decode(value: str) -> bytes: + return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) + + +def _verify_public_match(private_path: Path, public_path: Path) -> None: + private_bytes = _read_secret(private_path) + public_bytes = _read_secret(public_path) + try: + private = json.loads(private_bytes.decode("utf-8")) + public = json.loads(public_bytes.decode("utf-8")) + private_keys = {"kty", "crv", "alg", "x", "y", "d", "kid"} + public_keys = {"kty", "crv", "alg", "x", "y", "kid"} + if ( + not isinstance(private, dict) + or not isinstance(public, dict) + or set(private) != private_keys + or set(public) != public_keys + or private.get("kty") != "EC" + or private.get("crv") != "P-256" + or private.get("alg") != "ES256" + ): + raise SignerError("invalid key pair") + scalar = int.from_bytes(_b64decode(private["d"]), "big") + numbers = ( + ec.derive_private_key(scalar, ec.SECP256R1()).public_key().public_numbers() + ) + expected = { + "kty": "EC", + "crv": "P-256", + "alg": "ES256", + "x": _b64url(numbers.x.to_bytes(32, "big")), + "y": _b64url(numbers.y.to_bytes(32, "big")), + } + thumbprint = {key: expected[key] for key in ("crv", "kty", "x", "y")} + expected["kid"] = _b64url( + hashlib.sha256( + json.dumps(thumbprint, separators=(",", ":"), sort_keys=True).encode() + ).digest() + ) + if public != expected or any(private[key] != expected[key] for key in expected): + raise SignerError("invalid key pair") + except SignerError: + raise + except (UnicodeError, json.JSONDecodeError, KeyError, TypeError, ValueError): + raise SignerError("invalid key pair") from None + finally: + private_bytes[:] = b"\0" * len(private_bytes) + public_bytes[:] = b"\0" * len(public_bytes) + + +def _socket_identity(path: Path) -> tuple[int, int, int, int]: + try: + metadata = path.lstat() + except OSError as error: + raise SignerError("invalid socket") from error + if ( + not stat.S_ISSOCK(metadata.st_mode) + or metadata.st_uid != os.geteuid() + or stat.S_IMODE(metadata.st_mode) & 0o077 + or metadata.st_nlink != 1 + ): + raise SignerError("invalid socket") + return metadata.st_dev, metadata.st_ino, metadata.st_uid, metadata.st_mode + + +def _remove_stale_socket(path: Path) -> None: + identity = _socket_identity(path) + try: + with socket.socket(socket.AF_UNIX) as client: + client.settimeout(0.25) + client.connect(str(path)) + except (ConnectionRefusedError, FileNotFoundError): + pass + except OSError as error: + if error.errno not in {errno.ECONNREFUSED, errno.ENOENT}: + raise SignerError("invalid socket") from error + else: + raise SignerError("invalid socket") + + if not path.exists() and not path.is_symlink(): + return + if _socket_identity(path) != identity: + raise SignerError("invalid socket") + try: + path.unlink() + except OSError as error: + raise SignerError("invalid socket") from error + + +def _validate_socket(path: Path) -> None: + if path != SOCKET_PATH: + raise SignerError("invalid socket") + try: + metadata = path.parent.lstat() + except OSError as error: + raise SignerError("invalid socket") from error + if ( + not stat.S_ISDIR(metadata.st_mode) + or stat.S_ISLNK(metadata.st_mode) + or metadata.st_uid != os.geteuid() + or metadata.st_mode & 0o022 + ): + raise SignerError("invalid socket") + if path.exists() or path.is_symlink(): + _remove_stale_socket(path) + + +def _validate_proxy(path: Path) -> None: + if not path.is_absolute() or path.is_symlink(): + raise SignerError("invalid proxy") + try: + metadata = path.stat() + except OSError as error: + raise SignerError("invalid proxy") from error + if ( + not stat.S_ISREG(metadata.st_mode) + or metadata.st_uid not in {0, os.geteuid()} + or metadata.st_mode & 0o022 + ): + raise SignerError("invalid proxy") + + +def exec_signer( + private_jwk: Path, + public_jwk: Path, + socket_path: Path, + key_name: str, + proxy: Path, +) -> None: + if ( + private_jwk != SECRET_PATH + or public_jwk != PUBLIC_PATH + or key_name not in ALLOWED_KEY_NAMES + ): + raise SignerError("invalid signer configuration") + _verify_public_match(private_jwk, public_jwk) + _validate_socket(socket_path) + _validate_proxy(proxy) + staged = _stage_secret(private_jwk) + arguments = [ + sys.executable, + str(proxy), + "--private-jwk", + str(staged), + "--socket", + str(socket_path), + "--key-name", + key_name, + ] + environment = { + "LANG": "C.UTF-8", + "PYTHONDONTWRITEBYTECODE": "1", + "PYTHONUNBUFFERED": "1", + } + os.execve(sys.executable, arguments, environment) + + +def main() -> int: + parser = QuietArgumentParser(add_help=False) + parser.add_argument("--private-jwk", required=True, type=Path) + parser.add_argument("--public-jwk", required=True, type=Path) + parser.add_argument("--socket", required=True, type=Path) + parser.add_argument("--key-name", required=True) + try: + arguments = parser.parse_args() + proxy = Path(__file__).resolve().with_name("local-transit-proxy.py") + os.umask(0o077) + exec_signer( + arguments.private_jwk, + arguments.public_jwk, + arguments.socket, + arguments.key_name, + proxy, + ) + except (OSError, SignerError, ValueError): + print(GENERIC_ERROR, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/init-notary-postgresql.sh b/scripts/init-notary-postgresql.sh deleted file mode 100755 index c41ceb0..0000000 --- a/scripts/init-notary-postgresql.sh +++ /dev/null @@ -1,360 +0,0 @@ -#!/usr/bin/env sh -set -eu - -# PostgreSQL runs this file while initializing a fresh cluster and Compose -# reruns it as an idempotent bootstrap before schema installation. Each -# allowlisted authority receives separate Relay consultation and Notary -# databases. Passwords arrive through the deployment secret store and are -# never printed. - -relay_state_epoch=${REGISTRY_RELAY_STATE_EPOCH:-v015} -case "$relay_state_epoch" in - "" | *[!a-z0-9_]*) - echo "REGISTRY_RELAY_STATE_EPOCH must contain only lowercase letters, digits, and underscores" >&2 - exit 1 - ;; -esac - -provision_relay() { - key=$1 - runtime_password=$2 - maintenance_password=$3 - reader_password=$4 - database="solmara_relay_${key}_consultation_${relay_state_epoch}" - owner="${database}_owner" - runtime="${database}_runtime" - maintenance="${database}_keyring_maintenance" - reader="${database}_keyring_reader" - - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --set=database="$database" \ - --set=owner="$owner" \ - --set=runtime="$runtime" \ - --set=maintenance="$maintenance" \ - --set=reader="$reader" \ - --set=runtime_password="$runtime_password" \ - --set=maintenance_password="$maintenance_password" \ - --set=reader_password="$reader_password" <<'SQL' -SELECT format( - 'CREATE ROLE %I NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS', - :'owner' -) -WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'owner') -\gexec -SELECT format( - 'CREATE ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', - role_name, - role_password -) -FROM (VALUES - (:'runtime', :'runtime_password'), - (:'maintenance', :'maintenance_password'), - (:'reader', :'reader_password') -) AS requested(role_name, role_password) -WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = requested.role_name) -\gexec -SELECT format( - 'ALTER ROLE %I NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS', - :'owner' -) -\gexec -SELECT format( - 'ALTER ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', - role_name, - role_password -) -FROM (VALUES - (:'runtime', :'runtime_password'), - (:'maintenance', :'maintenance_password'), - (:'reader', :'reader_password') -) AS requested(role_name, role_password) -\gexec -SELECT format('REVOKE %I FROM %I', granted.rolname, member.rolname) -FROM pg_catalog.pg_auth_members AS membership -JOIN pg_catalog.pg_roles AS granted ON granted.oid = membership.roleid -JOIN pg_catalog.pg_roles AS member ON member.oid = membership.member -WHERE member.rolname IN (:'owner', :'runtime', :'maintenance', :'reader') -\gexec -SELECT format('CREATE DATABASE %I OWNER %I', :'database', :'owner') -WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_database WHERE datname = :'database') -\gexec -SELECT format('ALTER DATABASE %I OWNER TO %I', :'database', :'owner') -\gexec -SELECT format('REVOKE ALL ON DATABASE %I FROM PUBLIC', :'database') -\gexec -SELECT format( - 'REVOKE ALL ON DATABASE %I FROM %I, %I, %I, %I', - :'database', :'owner', :'runtime', :'maintenance', :'reader' -) -\gexec -SELECT format('GRANT CONNECT, CREATE ON DATABASE %I TO %I', :'database', :'owner') -\gexec -SELECT format( - 'GRANT CONNECT ON DATABASE %I TO %I, %I, %I', - :'database', :'runtime', :'maintenance', :'reader' -) -\gexec -SQL - - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$database" <<'SQL' -REVOKE ALL ON SCHEMA public FROM PUBLIC; -SQL -} - -provision_notary() { - key=$1 - migrator_password=$2 - runtime_password=$3 - database="solmara_notary_${key}" - owner="${database}_owner" - migrator="${database}_migrator" - runtime="${database}_runtime" - - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --set=database="$database" \ - --set=owner="$owner" \ - --set=migrator="$migrator" \ - --set=runtime="$runtime" \ - --set=migrator_password="$migrator_password" \ - --set=runtime_password="$runtime_password" <<'SQL' -SELECT format( - 'CREATE ROLE %I NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS', - :'owner' -) -WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'owner') -\gexec -SELECT format( - 'CREATE ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', - :'migrator', - :'migrator_password' -) -WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'migrator') -\gexec -SELECT format( - 'CREATE ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', - :'runtime', - :'runtime_password' -) -WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'runtime') -\gexec -SELECT format( - 'ALTER ROLE %I NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS', - :'owner' -) -\gexec -SELECT format( - 'ALTER ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', - :'migrator', - :'migrator_password' -) -\gexec -SELECT format( - 'ALTER ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', - :'runtime', - :'runtime_password' -) -\gexec -SELECT format('GRANT %I TO %I', :'owner', :'migrator') -WHERE NOT EXISTS ( - SELECT 1 - FROM pg_catalog.pg_auth_members AS membership - JOIN pg_catalog.pg_roles AS granted_role ON granted_role.oid = membership.roleid - JOIN pg_catalog.pg_roles AS member_role ON member_role.oid = membership.member - WHERE granted_role.rolname = :'owner' AND member_role.rolname = :'migrator' -) -\gexec -SELECT format('REVOKE %I FROM %I', :'owner', :'runtime') -WHERE EXISTS ( - SELECT 1 - FROM pg_catalog.pg_auth_members AS membership - JOIN pg_catalog.pg_roles AS granted_role ON granted_role.oid = membership.roleid - JOIN pg_catalog.pg_roles AS member_role ON member_role.oid = membership.member - WHERE granted_role.rolname = :'owner' AND member_role.rolname = :'runtime' -) -\gexec -SELECT format('CREATE DATABASE %I OWNER %I', :'database', :'owner') -WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_database WHERE datname = :'database') -\gexec -SELECT format('ALTER DATABASE %I OWNER TO %I', :'database', :'owner') -\gexec -SELECT format('REVOKE ALL ON DATABASE %I FROM PUBLIC', :'database') -\gexec -SELECT format('REVOKE ALL ON DATABASE %I FROM %I, %I', :'database', :'migrator', :'runtime') -\gexec -SELECT format('GRANT CONNECT ON DATABASE %I TO %I, %I', :'database', :'migrator', :'runtime') -\gexec -SQL - - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$database" <<'SQL' -REVOKE ALL ON SCHEMA public FROM PUBLIC; -SQL -} - -provision_source_reader() { - key=$1 - password=$2 - role="solmara_source_${key}_reader" - - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --set=database="$POSTGRES_DB" \ - --set=role="$role" \ - --set=password="$password" <<'SQL' -SELECT format( - 'CREATE ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', - :'role', - :'password' -) -WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'role') -\gexec -SELECT format( - 'ALTER ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', - :'role', - :'password' -) -\gexec -SELECT format('REVOKE %I FROM %I', granted.rolname, member.rolname) -FROM pg_catalog.pg_auth_members AS membership -JOIN pg_catalog.pg_roles AS granted ON granted.oid = membership.roleid -JOIN pg_catalog.pg_roles AS member ON member.oid = membership.member -WHERE member.rolname = :'role' -\gexec -SELECT format('REVOKE ALL ON DATABASE %I FROM %I', :'database', :'role') -\gexec -SELECT format('GRANT CONNECT ON DATABASE %I TO %I', :'database', :'role') -\gexec -SQL - - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --set=role="$role" <<'SQL' -REVOKE CREATE ON SCHEMA public FROM PUBLIC; -SELECT format('REVOKE ALL ON ALL TABLES IN SCHEMA public FROM %I', :'role') -\gexec -SELECT format('GRANT USAGE ON SCHEMA public TO %I', :'role') -\gexec -SQL - - case "$key" in - nia) - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --set=role="$role" <<'SQL' -SELECT format('GRANT SELECT ON TABLE public.population_person TO %I', :'role') -\gexec -SQL - ;; - sipf) - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --set=role="$role" <<'SQL' -SELECT format( - 'GRANT SELECT ON TABLE public.sipf_pension_payment, public.sipf_survivor_benefit TO %I', - :'role' -) -\gexec -SQL - ;; - *) - echo "Unsupported source reader" >&2 - exit 1 - ;; - esac -} - -for key in ${SOLMARA_SOURCE_READER_DATABASES:-}; do - case "$key" in - nia) - provision_source_reader "$key" \ - "${NIA_SOURCE_POSTGRES_READER_PASSWORD:?missing NIA source reader password}" - ;; - sipf) - provision_source_reader "$key" \ - "${SIPF_SOURCE_POSTGRES_READER_PASSWORD:?missing SIPF source reader password}" - ;; - *) - echo "Unsupported SOLMARA_SOURCE_READER_DATABASES entry" >&2 - exit 1 - ;; - esac -done - -for key in ${SOLMARA_RELAY_DATABASES:-}; do - case "$key" in - cra) - provision_relay "$key" \ - "${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD:?missing CRA Relay runtime password}" \ - "${CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD:?missing CRA Relay keyring maintenance password}" \ - "${CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD:?missing CRA Relay keyring reader password}" - ;; - nia) - provision_relay "$key" \ - "${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD:?missing NIA Relay runtime password}" \ - "${NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD:?missing NIA Relay keyring maintenance password}" \ - "${NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD:?missing NIA Relay keyring reader password}" - ;; - sro) - provision_relay "$key" \ - "${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD:?missing SRO Relay runtime password}" \ - "${SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD:?missing SRO Relay keyring maintenance password}" \ - "${SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD:?missing SRO Relay keyring reader password}" - ;; - programme) - provision_relay "$key" \ - "${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD:?missing programme Relay runtime password}" \ - "${PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD:?missing programme Relay keyring maintenance password}" \ - "${PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD:?missing programme Relay keyring reader password}" - ;; - sipf) - provision_relay "$key" \ - "${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD:?missing SIPF Relay runtime password}" \ - "${SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD:?missing SIPF Relay keyring maintenance password}" \ - "${SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD:?missing SIPF Relay keyring reader password}" - ;; - nagdi) - provision_relay "$key" \ - "${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD:?missing NAgDI Relay runtime password}" \ - "${NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD:?missing NAgDI Relay keyring maintenance password}" \ - "${NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD:?missing NAgDI Relay keyring reader password}" - ;; - *) - echo "Unsupported SOLMARA_RELAY_DATABASES entry" >&2 - exit 1 - ;; - esac -done - -for key in ${SOLMARA_NOTARY_DATABASES:-}; do - case "$key" in - cra) - provision_notary "$key" \ - "${CRA_NOTARY_POSTGRES_MIGRATOR_PASSWORD:?missing CRA Notary migrator password}" \ - "${CRA_NOTARY_POSTGRES_RUNTIME_PASSWORD:?missing CRA Notary runtime password}" - ;; - nia) - provision_notary "$key" \ - "${NIA_NOTARY_POSTGRES_MIGRATOR_PASSWORD:?missing NIA Notary migrator password}" \ - "${NIA_NOTARY_POSTGRES_RUNTIME_PASSWORD:?missing NIA Notary runtime password}" - ;; - sro) - provision_notary "$key" \ - "${SRO_NOTARY_POSTGRES_MIGRATOR_PASSWORD:?missing SRO Notary migrator password}" \ - "${SRO_NOTARY_POSTGRES_RUNTIME_PASSWORD:?missing SRO Notary runtime password}" - ;; - programme) - provision_notary "$key" \ - "${PROGRAMME_NOTARY_POSTGRES_MIGRATOR_PASSWORD:?missing programme Notary migrator password}" \ - "${PROGRAMME_NOTARY_POSTGRES_RUNTIME_PASSWORD:?missing programme Notary runtime password}" - ;; - sipf) - provision_notary "$key" \ - "${SIPF_NOTARY_POSTGRES_MIGRATOR_PASSWORD:?missing SIPF Notary migrator password}" \ - "${SIPF_NOTARY_POSTGRES_RUNTIME_PASSWORD:?missing SIPF Notary runtime password}" - ;; - nagdi) - provision_notary "$key" \ - "${NAGDI_NOTARY_POSTGRES_MIGRATOR_PASSWORD:?missing NAgDI Notary migrator password}" \ - "${NAGDI_NOTARY_POSTGRES_RUNTIME_PASSWORD:?missing NAgDI Notary runtime password}" - ;; - *) - echo "Unsupported SOLMARA_NOTARY_DATABASES entry" >&2 - exit 1 - ;; - esac -done diff --git a/scripts/init-registry-postgresql.sh b/scripts/init-registry-postgresql.sh deleted file mode 100755 index c8a0704..0000000 --- a/scripts/init-registry-postgresql.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env sh -set -eu - -# Provision least-privilege readers for the two PostgreSQL-backed Records API -# sources. Evidence never receives these credentials. Each Relay holds its own -# reader and releases only the reviewed field projection. -for fixture_script in \ - /docker-entrypoint-initdb.d/001-schema.sql \ - /docker-entrypoint-initdb.d/002-load.sql \ - /docker-entrypoint-initdb.d/003-schema.sql \ - /docker-entrypoint-initdb.d/004-load.sql; do - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --file "$fixture_script" -done - -provision_source_reader() { - key=$1 - password=$2 - role="solmara_source_${key}_reader" - - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --set=database="$POSTGRES_DB" \ - --set=role="$role" \ - --set=password="$password" <<'SQL' -SELECT format( - 'CREATE ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', - :'role', - :'password' -) -WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = :'role') -\gexec -SELECT format( - 'ALTER ROLE %I LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', - :'role', - :'password' -) -\gexec -SELECT format('REVOKE %I FROM %I', granted.rolname, member.rolname) -FROM pg_catalog.pg_auth_members AS membership -JOIN pg_catalog.pg_roles AS granted ON granted.oid = membership.roleid -JOIN pg_catalog.pg_roles AS member ON member.oid = membership.member -WHERE member.rolname = :'role' -\gexec -SELECT format('REVOKE ALL ON DATABASE %I FROM %I', :'database', :'role') -\gexec -SELECT format('GRANT CONNECT ON DATABASE %I TO %I', :'database', :'role') -\gexec -SQL - - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --set=role="$role" <<'SQL' -REVOKE CREATE ON SCHEMA public FROM PUBLIC; -SELECT format('REVOKE ALL ON ALL TABLES IN SCHEMA public FROM %I', :'role') -\gexec -SELECT format('GRANT USAGE ON SCHEMA public TO %I', :'role') -\gexec -SQL - - case "$key" in - nia) - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --set=role="$role" <<'SQL' -SELECT format('GRANT SELECT ON TABLE public.population_person TO %I', :'role') -\gexec -SQL - ;; - sipf) - psql --quiet --set=ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ - --set=role="$role" <<'SQL' -SELECT format( - 'GRANT SELECT ON TABLE public.sipf_pension_payment, public.sipf_survivor_benefit TO %I', - :'role' -) -\gexec -SQL - ;; - *) - echo "Unsupported source reader" >&2 - exit 1 - ;; - esac -} - -for key in ${SOLMARA_SOURCE_READER_DATABASES:-}; do - case "$key" in - nia) - provision_source_reader "$key" \ - "${NIA_SOURCE_POSTGRES_READER_PASSWORD:?missing NIA source reader password}" - ;; - sipf) - provision_source_reader "$key" \ - "${SIPF_SOURCE_POSTGRES_READER_PASSWORD:?missing SIPF source reader password}" - ;; - *) - echo "Unsupported SOLMARA_SOURCE_READER_DATABASES entry" >&2 - exit 1 - ;; - esac -done diff --git a/scripts/lifecycle_proof.py b/scripts/lifecycle_proof.py new file mode 100755 index 0000000..0b8d2db --- /dev/null +++ b/scripts/lifecycle_proof.py @@ -0,0 +1,378 @@ +#!/usr/bin/env python3 +"""Prove the two Solmara publisher cadences without touching active data.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import sqlite3 +import stat +import sys +import tempfile +from datetime import datetime, timedelta, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "generator")) + +from solmara_lab.publisher import ( # noqa: E402 + DEFAULT_EXTRACTS, + EVIDENCE_DIRECTORY, + PUBLISHERS, + RELAY_DIRECTORY, + RELAY_FILENAMES, + mutate_mosd_state, + publish_all, + publish_extract, +) + +MAX_EXTRACT_AGE = timedelta(seconds=86_400) +MOSD_TEST_UIN = "2300010248" +SRO_TEST_UIN = "2300010248" + + +class LifecycleProofError(RuntimeError): + """Raised when a lifecycle invariant is not demonstrated.""" + + +def _digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _database_uri(path: Path) -> str: + return f"file:{path.resolve().as_posix()}?mode=ro" + + +class GovernedMosdObservation: + """A long-lived, read-only source binding like Relay's mounted source.""" + + def __init__(self, database: Path) -> None: + self.database = database.resolve() + self.connection = sqlite3.connect(_database_uri(self.database), uri=True) + + def close(self) -> None: + self.connection.close() + + def observe_duplicate_flag(self, uin: str) -> tuple[int, str, str]: + row = self.connection.execute( + """ + SELECT duplicate_flag, record_revision, recorded_at + FROM relay_beneficiary_enrolment + WHERE uin = ? + """, + (uin,), + ).fetchone() + if row is None: + raise LifecycleProofError("governed MoSD lookup did not resolve exactly once") + return int(row[0]), str(row[1]), str(row[2]) + + +def _parse_timestamp(value: str) -> datetime: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + if parsed.tzinfo is None: + raise LifecycleProofError("extract timestamp is not timezone-aware") + return parsed.astimezone(timezone.utc) + + +def _validate_sro_extract( + path: Path, + *, + expected_extract_id: str, + observed_at: datetime, +) -> None: + if path.name != f"{expected_extract_id}.sqlite": + raise LifecycleProofError("extract filename does not match its binding") + if stat.S_IMODE(path.stat().st_mode) & 0o222: + raise LifecycleProofError("extract has a writable mode") + + with sqlite3.connect(_database_uri(path), uri=True) as connection: + if connection.execute("PRAGMA quick_check").fetchone() != ("ok",): + raise LifecycleProofError("extract integrity check failed") + metadata = connection.execute( + "SELECT published_at, publisher, extract_id FROM evidence_extract" + ).fetchall() + if len(metadata) != 1: + raise LifecycleProofError("extract metadata cardinality is invalid") + published_at, publisher, extract_id = metadata[0] + if publisher != PUBLISHERS["sro"] or extract_id != expected_extract_id: + raise LifecycleProofError("extract metadata does not match its binding") + published = _parse_timestamp(str(published_at)) + age = observed_at.astimezone(timezone.utc) - published + if age < timedelta(0) or age > MAX_EXTRACT_AGE: + raise LifecycleProofError("extract is outside its accepted age") + + expected_columns = [ + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "poverty_band", + ] + columns = [ + row[1] + for row in connection.execute("PRAGMA table_info(poverty_evidence)") + ] + if columns != expected_columns: + raise LifecycleProofError("extract schema does not match the SRO contract") + + +class SroExtractBinding: + """An Evidence-cell binding fixed to one immutable extract filename.""" + + def __init__( + self, + path: Path, + *, + expected_extract_id: str, + observed_at: datetime, + ) -> None: + self.path = path.resolve() + self.expected_extract_id = expected_extract_id + _validate_sro_extract( + self.path, + expected_extract_id=expected_extract_id, + observed_at=observed_at, + ) + self.connection = sqlite3.connect(_database_uri(self.path), uri=True) + + def close(self) -> None: + self.connection.close() + + def observe_poverty_band(self, uin: str) -> str: + row = self.connection.execute( + "SELECT poverty_band FROM poverty_evidence WHERE uin = ?", (uin,) + ).fetchone() + if row is None: + raise LifecycleProofError("bound SRO extract did not resolve exactly once") + return str(row[0]) + + +def _revision( + *, + record_id: str, + lifecycle_state: str, + recorded_at: str, + uin: str, + poverty_band: str, +) -> str: + value = { + "record_id": record_id, + "lifecycle_state": lifecycle_state, + "recorded_at": recorded_at, + "uin": uin, + "poverty_band": poverty_band, + } + encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode() + return f"rev-{hashlib.sha256(encoded).hexdigest()[:16]}" + + +def _publish_changed_sro_extract( + root: Path, + *, + extract_id: str, + published_at: str, + poverty_band: str, + metadata_extract_id: str | None = None, +) -> Path: + """Stage changes before atomically publishing a never-before-used filename.""" + + staging_root = root / ".lifecycle-staging" / extract_id + staged = publish_extract(staging_root, "sro", published_at, extract_id) + staged.chmod(0o600) + with sqlite3.connect(staged) as connection: + current = connection.execute( + """ + SELECT record_id, lifecycle_state, recorded_at + FROM poverty_evidence + WHERE uin = ? + """, + (SRO_TEST_UIN,), + ).fetchone() + if current is None: + raise LifecycleProofError("staged SRO extract is missing its control row") + connection.execute( + """ + UPDATE poverty_evidence + SET poverty_band = ?, record_revision = ? + WHERE uin = ? + """, + ( + poverty_band, + _revision( + record_id=str(current[0]), + lifecycle_state=str(current[1]), + recorded_at=str(current[2]), + uin=SRO_TEST_UIN, + poverty_band=poverty_band, + ), + SRO_TEST_UIN, + ), + ) + if metadata_extract_id is not None: + connection.execute( + "UPDATE evidence_extract SET extract_id = ?", (metadata_extract_id,) + ) + connection.commit() + connection.execute("VACUUM") + staged.chmod(0o444) + + target = root / EVIDENCE_DIRECTORY / f"{extract_id}.sqlite" + target.parent.mkdir(parents=True, exist_ok=True) + try: + os.link(staged, target) + except FileExistsError: + raise LifecycleProofError("immutable extract publication refused overwrite") from None + return target + + +def run_proof() -> dict[str, object]: + reference_time = datetime(2026, 7, 5, 8, 30, tzinfo=timezone.utc) + with tempfile.TemporaryDirectory(prefix="solmara-lifecycle-") as temporary: + root = Path(temporary) + publish_all(root) + + mosd = root / RELAY_DIRECTORY / RELAY_FILENAMES["mosd"] + mosd_inode = (mosd.stat().st_dev, mosd.stat().st_ino) + mosd_binding = GovernedMosdObservation(mosd) + try: + connection_identity = id(mosd_binding.connection) + before = mosd_binding.observe_duplicate_flag(MOSD_TEST_UIN) + mutate_mosd_state( + mosd, + MOSD_TEST_UIN, + True, + "2026-07-05T08:15:00Z", + ) + after = mosd_binding.observe_duplicate_flag(MOSD_TEST_UIN) + if (before[0], after[0]) != (0, 1): + raise LifecycleProofError("live MoSD state did not change on next observation") + if before[1] == after[1] or before[2] == after[2]: + raise LifecycleProofError("live MoSD revision metadata did not advance") + if (mosd.stat().st_dev, mosd.stat().st_ino) != mosd_inode: + raise LifecycleProofError("live MoSD mutation replaced the mounted inode") + if id(mosd_binding.connection) != connection_identity: + raise LifecycleProofError("live MoSD source binding was restarted") + finally: + mosd_binding.close() + + original_id = DEFAULT_EXTRACTS["sro"] + original = root / EVIDENCE_DIRECTORY / f"{original_id}.sqlite" + original_digest = _digest(original) + original_inode = (original.stat().st_dev, original.stat().st_ino) + old_binding = SroExtractBinding( + original, + expected_extract_id=original_id, + observed_at=reference_time, + ) + replacement_id = "sro-poverty-20260705T080000Z" + replacement = _publish_changed_sro_extract( + root, + extract_id=replacement_id, + published_at="2026-07-05T08:00:00Z", + poverty_band="not_eligible", + ) + try: + old_result_before = old_binding.observe_poverty_band(SRO_TEST_UIN) + old_result_after_publication = old_binding.observe_poverty_band(SRO_TEST_UIN) + if old_result_before != old_result_after_publication: + raise LifecycleProofError("active SRO binding changed before a restart") + if _digest(original) != original_digest: + raise LifecycleProofError("active immutable SRO extract was overwritten") + if (original.stat().st_dev, original.stat().st_ino) != original_inode: + raise LifecycleProofError("active immutable SRO extract was replaced") + try: + publish_extract(root, "sro", "2026-07-05T08:00:00Z", original_id) + except FileExistsError: + pass + else: + raise LifecycleProofError("publisher allowed an active extract overwrite") + finally: + old_binding.close() + + rebound = SroExtractBinding( + replacement, + expected_extract_id=replacement_id, + observed_at=reference_time, + ) + try: + rebound_result = rebound.observe_poverty_band(SRO_TEST_UIN) + finally: + rebound.close() + if rebound_result == old_result_after_publication: + raise LifecycleProofError("SRO-only rebind did not expose the new assertion input") + + stale_id = "sro-poverty-20260703T080000Z" + stale = _publish_changed_sro_extract( + root, + extract_id=stale_id, + published_at="2026-07-03T08:00:00Z", + poverty_band="not_eligible", + ) + try: + SroExtractBinding( + stale, + expected_extract_id=stale_id, + observed_at=reference_time, + ) + except LifecycleProofError: + pass + else: + raise LifecycleProofError("stale SRO extract did not fail closed") + + invalid_id = "sro-poverty-20260705T081000Z" + invalid = _publish_changed_sro_extract( + root, + extract_id=invalid_id, + published_at="2026-07-05T08:10:00Z", + poverty_band="not_eligible", + metadata_extract_id="metadata-does-not-match-binding", + ) + try: + SroExtractBinding( + invalid, + expected_extract_id=invalid_id, + observed_at=reference_time, + ) + except LifecycleProofError: + pass + else: + raise LifecycleProofError("invalid SRO extract did not fail closed") + + return { + "status": "pass", + "fixtureState": "isolated-temporary-directory", + "checks": { + "mosdInPlaceMutationVisibleWithoutRestart": True, + "sroNewFilenameRequired": True, + "sroOldBindingStableUntilRebind": True, + "activeExtractOverwriteDenied": True, + "staleReplacementDenied": True, + "invalidReplacementDenied": True, + }, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--json", + action="store_true", + help="print the sanitized result as compact JSON", + ) + args = parser.parse_args() + result = run_proof() + if args.json: + print(json.dumps(result, sort_keys=True, separators=(",", ":"))) + else: + print("Solmara lifecycle proof: PASS") + for check in result["checks"]: + print(f"- {check}: PASS") + print("- fixtureState: isolated temporary directory") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/live-lifecycle-proof.py b/scripts/live-lifecycle-proof.py new file mode 100755 index 0000000..c567c89 --- /dev/null +++ b/scripts/live-lifecycle-proof.py @@ -0,0 +1,839 @@ +#!/usr/bin/env python3 +"""Prove Solmara's live Relay and immutable-extract cadences over HTTP. + +The proof deliberately emits only named checks. Selectors, tokens, signed JWS +bytes, source values, extract names, and private operational data stay inside +the process and are never copied into its result or errors. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shlex +import sqlite3 +import stat +import subprocess +import sys +import tempfile +import time +from collections.abc import Sequence +from dataclasses import dataclass +from datetime import UTC, datetime, timedelta +from pathlib import Path, PurePosixPath +from typing import Any, Protocol + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) +if str(ROOT / "generator") not in sys.path: + sys.path.insert(0, str(ROOT / "generator")) + +from scenarios import common as evidence_common # noqa: E402 +from scenarios.service_config import requirement_config, service_url # noqa: E402 +from solmara_lab import publisher # noqa: E402 + +MOSD_SERVICE = "mosd-programme-evidence" +MOSD_RELAY_SERVICE = "mosd-relay" +SRO_SERVICE = "sro-evidence" +NON_SRO_SERVICES = ( + "cra-relay", + "nia-relay", + MOSD_RELAY_SERVICE, + "sipf-relay", + "nagdi-relay", + "mint", + "cra-evidence", + "nia-evidence", + MOSD_SERVICE, + "sipf-evidence", + "nagdi-evidence", +) +TOPOLOGY_SERVICES = (*NON_SRO_SERVICES, SRO_SERVICE) +MOSD_REQUIREMENT = "programme-child-benefit" +SRO_REQUIREMENT = "sro-child-benefit" +MOSD_CONTROL_SUBJECT = "2300010248" +SRO_CONTROL_SUBJECT = "2300010248" +MOSD_CONCEPT = "not-already-enrolled" +SRO_CONCEPT = "household-below-poverty-threshold" +MOSD_PUBLISHER_SERVICE = "mosd-source-publisher" +MOSD_SOURCE_VOLUME = "mosd-relay-source" +RELAY_SOURCE_DESTINATION = "/var/lib/relay/source" +SRO_RUNTIME = Path("runtime/evidence-cells/cells/sro/runtime.yaml") +SRO_PROFILE = "sro-poverty-extract" +SRO_CONTAINER_DIRECTORY = PurePosixPath("/var/lib/registry-evidence/sro/extracts") +READINESS_ATTEMPTS = 45 +READINESS_INTERVAL_SECONDS = 1.0 +REFUSAL_OBSERVATION_SECONDS = 8.0 + +_PUBLIC_ERROR = "lifecycle proof did not complete" +_FORBIDDEN_OUTPUT_PATTERNS = ( + re.compile(r"\b(?:uin|selector|token|authorization|jws|signature|payload)\b", re.I), + re.compile(r"\b(?:poverty_band|duplicate_flag|record_revision|source row)\b", re.I), + re.compile(r"\b(?:\.sqlite|runtime\.yaml|/var/lib/)\b", re.I), + re.compile(r"\b[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b"), +) + + +class LifecycleProofError(RuntimeError): + """A value-free lifecycle proof failure.""" + + +class Operations(Protocol): + def load_environment(self) -> None: ... + + def validate_preconditions(self) -> None: ... + + def observe_evidence(self, requirement: str) -> bool: ... + + def generation(self, service: str) -> str: ... + + def capture_mosd_fixture(self) -> "MosdFixture": ... + + def mutate_mosd(self, duplicate: bool) -> None: ... + + def restore_mosd_fixture(self, fixture: "MosdFixture") -> None: ... + + def capture_sro_binding(self) -> bytes: ... + + def publish_changed_sro(self) -> "ExtractPublication": ... + + def bind_sro(self, publication: "ExtractPublication") -> None: ... + + def restart_sro(self) -> None: ... + + def wait_sro_ready(self) -> None: ... + + def prove_replacement_refusals(self) -> None: ... + + def restore_sro_binding(self, original: bytes) -> None: ... + + def discard_sro_publication(self, publication: "ExtractPublication") -> None: ... + + +@dataclass(frozen=True) +class ExtractPublication: + path: Path + container_path: str + extract_id: str + + +@dataclass(frozen=True) +class MosdFixture: + publisher_backup_started: bool + + +def _now() -> datetime: + return datetime.now(UTC) + + +def _timestamp(value: datetime) -> str: + return ( + value.astimezone(UTC).isoformat(timespec="microseconds").replace("+00:00", "Z") + ) + + +def _run_command( + command: Sequence[str], *, cwd: Path = ROOT, check: bool = True +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + command, + cwd=cwd, + check=check, + capture_output=True, + text=True, + ) + + +def _load_dotenv(path: Path) -> None: + if not path.exists(): + return + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, raw_value = line.split("=", 1) + key = key.strip() + if not key or key in os.environ: + continue + parts = shlex.split(raw_value, posix=True) + os.environ[key] = parts[0] if parts else "" + + +def _compose_command(*arguments: str) -> list[str]: + return [ + "docker", + "compose", + "--env-file", + "versions.env", + "--env-file", + ".env", + "-f", + "compose.yaml", + *arguments, + ] + + +def _safe_failure(error: BaseException) -> LifecycleProofError: + return LifecycleProofError(_PUBLIC_ERROR) + + +def _claim_value(result: evidence_common.StepHttpResult, concept: str) -> bool: + body = result.body if isinstance(result.body, dict) else {} + values = { + member.get("claim_id"): member.get("value") + for member in body.get("results", []) + if isinstance(member, dict) + } + value = values.get(concept) + if not isinstance(value, bool): + raise LifecycleProofError(_PUBLIC_ERROR) + return value + + +def _verify_signed_observation(*, service_id: str, subject: str, purpose: str) -> bool: + token = evidence_common.evidence_access_token() + if not token: + raise LifecycleProofError(_PUBLIC_ERROR) + config = requirement_config(service_id) + request = evidence_common.evidence_body( + subject, + str(config["requirement"]), + purpose, + ) + response = evidence_common.http_json( + "POST", + service_url(service_id), + evidence_common.evidence_headers(token), + request, + timeout=10.0, + ) + verified = evidence_common.normalized_evidence_result( + response, + request=request, + service_id=service_id, + ) + if verified.status != 200: + raise LifecycleProofError(_PUBLIC_ERROR) + concept = MOSD_CONCEPT if service_id == MOSD_REQUIREMENT else SRO_CONCEPT + return _claim_value(verified, concept) + + +def _load_sro_binding(path: Path) -> tuple[dict[str, Any], str, str]: + try: + document = yaml.safe_load(path.read_text(encoding="utf-8")) + extracts = document["sourceExtracts"] + if set(extracts) != {SRO_PROFILE}: + raise ValueError + binding = extracts[SRO_PROFILE] + if set(binding) != {"path"} or not isinstance(binding["path"], str): + raise ValueError + container_path = PurePosixPath(binding["path"]) + if ( + container_path.parent != SRO_CONTAINER_DIRECTORY + or not container_path.name.startswith("sro-poverty-") + or container_path.suffix != ".sqlite" + ): + raise ValueError + return document, binding["path"], container_path.stem + except (KeyError, OSError, TypeError, UnicodeError, ValueError, yaml.YAMLError): + raise LifecycleProofError(_PUBLIC_ERROR) from None + + +def _replace_runtime_binding(path: Path, container_path: str) -> None: + _, old_container_path, _ = _load_sro_binding(path) + new_path = PurePosixPath(container_path) + if ( + new_path.parent != SRO_CONTAINER_DIRECTORY + or not new_path.name.startswith("sro-poverty-") + or new_path.suffix != ".sqlite" + ): + raise LifecycleProofError(_PUBLIC_ERROR) + original = path.read_text(encoding="utf-8") + pattern = re.compile(rf"(? None: + original_mode = stat.S_IMODE(path.stat().st_mode) + directory_mode = stat.S_IMODE(path.parent.stat().st_mode) + path.parent.chmod(directory_mode | stat.S_IWUSR) + temporary = path.with_name(f".{path.name}.lifecycle-{os.getpid()}.tmp") + try: + if temporary.exists(): + temporary.unlink() + temporary.write_bytes(content) + temporary.chmod(original_mode) + os.replace(temporary, path) + finally: + temporary.unlink(missing_ok=True) + path.parent.chmod(directory_mode) + + +def _publish_changed_sro(root: Path) -> ExtractPublication: + published_at = _timestamp(_now()) + extract_id = publisher.timestamped_extract_id("sro", published_at) + target = publisher.extract_path(root, extract_id) + if target.exists() or target.is_symlink(): + raise LifecycleProofError(_PUBLIC_ERROR) + staging_parent = root / "runtime" + staging_parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory( + prefix=".lifecycle-sro-", dir=staging_parent + ) as temporary: + staged = publisher.publish_extract( + Path(temporary), "sro", published_at, extract_id + ) + original_mode = stat.S_IMODE(staged.stat().st_mode) + try: + staged.chmod(original_mode | stat.S_IWUSR) + with sqlite3.connect(staged) as connection: + row = connection.execute( + """ + SELECT record_id, lifecycle_state, recorded_at + FROM poverty_evidence + WHERE uin = ? + """, + (SRO_CONTROL_SUBJECT,), + ).fetchone() + if row is None: + raise LifecycleProofError(_PUBLIC_ERROR) + changed = "standard" + revision_input = { + "record_id": str(row[0]), + "lifecycle_state": str(row[1]), + "recorded_at": str(row[2]), + "uin": SRO_CONTROL_SUBJECT, + "poverty_band": changed, + } + revision = ( + "rev-" + + hashlib.sha256( + json.dumps( + revision_input, sort_keys=True, separators=(",", ":") + ).encode() + ).hexdigest()[:16] + ) + connection.execute( + """ + UPDATE poverty_evidence + SET poverty_band = ?, record_revision = ? + WHERE uin = ? + """, + (changed, revision, SRO_CONTROL_SUBJECT), + ) + connection.commit() + connection.execute("VACUUM") + finally: + staged.chmod(0o444) + target.parent.mkdir(parents=True, exist_ok=True) + try: + os.link(staged, target) + except FileExistsError: + raise LifecycleProofError(_PUBLIC_ERROR) from None + try: + publisher.validate_extract( + target, + "sro", + observed_at=published_at, + expected_extract_id=extract_id, + expected_published_at=published_at, + ) + except BaseException: + target.unlink(missing_ok=True) + raise + return ExtractPublication( + target, + str(SRO_CONTAINER_DIRECTORY / target.name), + extract_id, + ) + + +def _publish_invalid_sro(root: Path, *, stale: bool) -> Path: + published_at = _timestamp(_now() - (timedelta(days=2) if stale else timedelta())) + suffix = "stale" if stale else "invalid" + base_id = publisher.timestamped_extract_id("sro", published_at) + extract_id = f"{base_id}-{suffix}-{os.getpid()}" + path = publisher.publish_extract(root, "sro", published_at, extract_id) + if not stale: + original_mode = stat.S_IMODE(path.stat().st_mode) + path.chmod(original_mode | stat.S_IWUSR) + try: + with sqlite3.connect(path) as connection: + connection.execute( + "UPDATE evidence_extract SET published_at = ?", ("invalid",) + ) + connection.commit() + except BaseException: + path.chmod(0o444) + path.unlink(missing_ok=True) + raise + finally: + if path.exists(): + path.chmod(0o444) + return path + + +class LocalOperations: + """Real local runtime operations, with all sensitive values kept internal.""" + + def __init__(self, root: Path = ROOT) -> None: + self.root = root.resolve() + self.sro_runtime = self.root / SRO_RUNTIME + + def load_environment(self) -> None: + _load_dotenv(self.root / ".env") + os.environ.pop("SOLMARA_EVIDENCE_ACCESS_TOKEN", None) + os.environ["SOLMARA_MINT_URL"] = "https://localhost:4341" + os.environ["SOLMARA_MINT_ASSERTION_AUDIENCE"] = ( + "https://mint.solmara.registrystack.org/token" + ) + os.environ["SOLMARA_EVIDENCE_CLIENT_ID"] = "solmara-demo" + os.environ["SOLMARA_EVIDENCE_CLIENT_KEY"] = str( + self.root + / "config/evidence/local/cells/mint/clients/solmara-demo-client-key" + ) + os.environ["SOLMARA_EVIDENCE_CA_BUNDLE"] = str( + self.root / "config/evidence/local/tls/ca.crt" + ) + os.environ["SOLMARA_SRO_EVIDENCE_URL"] = "https://localhost:4341/evidence/sro" + os.environ["SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL"] = ( + "https://localhost:4341/evidence/mosd-programme" + ) + os.environ["SOLMARA_EVIDENCE_AUDIENCE"] = evidence_common.EVIDENCE_AUDIENCE + evidence_common._TOKEN_CACHE = ("", 0.0) + evidence_common._JWKS_CACHE.clear() + + def validate_preconditions(self) -> None: + required_files = ( + self.sro_runtime, + self.root / "scripts/local-relay-source-publisher.py", + Path(os.environ["SOLMARA_EVIDENCE_CLIENT_KEY"]), + Path(os.environ["SOLMARA_EVIDENCE_CA_BUNDLE"]), + ) + if not all(path.is_file() for path in required_files): + raise LifecycleProofError(_PUBLIC_ERROR) + _, _, bound_id = _load_sro_binding(self.sro_runtime) + bound = publisher.extract_path(self.root, bound_id) + publisher.validate_extract( + bound, + "sro", + observed_at=_timestamp(_now()), + expected_extract_id=bound_id, + ) + for service in TOPOLOGY_SERVICES: + self.generation(service) + self._require_read_only_named_volume( + MOSD_RELAY_SERVICE, + MOSD_SOURCE_VOLUME, + RELAY_SOURCE_DESTINATION, + ) + self._require_bind_mount(SRO_SERVICE, self.sro_runtime.parent) + self._require_bind_mount(SRO_SERVICE, self.root / publisher.EVIDENCE_DIRECTORY) + + def _require_bind_mount(self, service: str, expected: Path) -> None: + completed = _run_command(_compose_command("ps", "-q", service)) + container_id = completed.stdout.strip() + if not container_id: + raise LifecycleProofError(_PUBLIC_ERROR) + inspection = _run_command( + ["docker", "inspect", "--format", "{{json .Mounts}}", container_id] + ) + try: + mounts = json.loads(inspection.stdout) + if not isinstance(mounts, list): + raise ValueError + matched = any( + isinstance(mount, dict) + and mount.get("Type") == "bind" + and isinstance(mount.get("Source"), str) + and Path(mount["Source"]).exists() + and os.path.samefile(mount["Source"], expected) + for mount in mounts + ) + except (OSError, TypeError, ValueError, json.JSONDecodeError): + raise LifecycleProofError(_PUBLIC_ERROR) from None + if not matched: + raise LifecycleProofError(_PUBLIC_ERROR) + + def _require_read_only_named_volume( + self, service: str, volume: str, destination: str + ) -> None: + completed = _run_command(_compose_command("ps", "-q", service)) + container_id = completed.stdout.strip() + if not container_id: + raise LifecycleProofError(_PUBLIC_ERROR) + inspection = _run_command( + ["docker", "inspect", "--format", "{{json .Mounts}}", container_id] + ) + project = os.environ.get("COMPOSE_PROJECT_NAME", "") + expected_name = f"{project}_{volume}" if project else "" + try: + mounts = json.loads(inspection.stdout) + if not isinstance(mounts, list): + raise ValueError + matched = any( + isinstance(mount, dict) + and mount.get("Type") == "volume" + and mount.get("Destination") == destination + and mount.get("RW") is False + and isinstance(mount.get("Name"), str) + and ( + mount["Name"] == expected_name + if expected_name + else mount["Name"].endswith(f"_{volume}") + ) + for mount in mounts + ) + except (TypeError, ValueError, json.JSONDecodeError): + raise LifecycleProofError(_PUBLIC_ERROR) from None + if not matched: + raise LifecycleProofError(_PUBLIC_ERROR) + + def observe_evidence(self, requirement: str) -> bool: + purpose = ( + evidence_common.PURPOSES["child_benefit"] + if requirement in {MOSD_REQUIREMENT, SRO_REQUIREMENT} + else "" + ) + subject = ( + MOSD_CONTROL_SUBJECT + if requirement == MOSD_REQUIREMENT + else SRO_CONTROL_SUBJECT + ) + return _verify_signed_observation( + service_id=requirement, + subject=subject, + purpose=purpose, + ) + + def generation(self, service: str) -> str: + completed = _run_command(_compose_command("ps", "--format", "json", service)) + try: + parsed = json.loads(completed.stdout) + documents = parsed if isinstance(parsed, list) else [parsed] + except json.JSONDecodeError: + documents = [ + json.loads(line) + for line in completed.stdout.splitlines() + if line.strip() + ] + if len(documents) != 1: + raise LifecycleProofError(_PUBLIC_ERROR) + document = documents[0] + container_id = document.get("ID") + state = document.get("State") + if not isinstance(container_id, str) or not container_id or state != "running": + raise LifecycleProofError(_PUBLIC_ERROR) + inspection = _run_command( + [ + "docker", + "inspect", + "--format", + "{{.Id}} {{.State.StartedAt}} {{.RestartCount}}", + container_id, + ] + ).stdout.strip() + if not inspection: + raise LifecycleProofError(_PUBLIC_ERROR) + return inspection + + def capture_mosd_fixture(self) -> MosdFixture: + try: + self._run_mosd_publisher("begin-proof") + except BaseException: + try: + self._run_mosd_publisher("restore-proof") + except BaseException: + pass + raise LifecycleProofError(_PUBLIC_ERROR) from None + return MosdFixture(publisher_backup_started=True) + + def mutate_mosd(self, duplicate: bool) -> None: + if duplicate is not True: + raise LifecycleProofError(_PUBLIC_ERROR) + self._run_mosd_publisher("set-proof-state") + + def restore_mosd_fixture(self, fixture: MosdFixture) -> None: + if fixture.publisher_backup_started is not True: + raise LifecycleProofError(_PUBLIC_ERROR) + self._run_mosd_publisher("restore-proof") + + def _run_mosd_publisher(self, verb: str) -> None: + if verb not in {"begin-proof", "set-proof-state", "restore-proof"}: + raise LifecycleProofError(_PUBLIC_ERROR) + _run_command( + _compose_command( + "run", + "--rm", + "-T", + "--no-deps", + MOSD_PUBLISHER_SERVICE, + verb, + ) + ) + + def capture_sro_binding(self) -> bytes: + _load_sro_binding(self.sro_runtime) + return self.sro_runtime.read_bytes() + + def publish_changed_sro(self) -> ExtractPublication: + return _publish_changed_sro(self.root) + + def bind_sro(self, publication: ExtractPublication) -> None: + _replace_runtime_binding(self.sro_runtime, publication.container_path) + + def restart_sro(self) -> None: + _run_command(_compose_command("restart", "--no-deps", SRO_SERVICE)) + + def wait_sro_ready(self) -> None: + last_error: BaseException | None = None + for _ in range(READINESS_ATTEMPTS): + try: + result = evidence_common.http_json( + "GET", + service_url(SRO_REQUIREMENT, "/ready"), + {}, + timeout=2.0, + ) + if result.status in {200, 204}: + return + except BaseException as error: + last_error = error + time.sleep(READINESS_INTERVAL_SECONDS) + raise _safe_failure(last_error or LifecycleProofError(_PUBLIC_ERROR)) + + def _wait_sro_refused(self) -> None: + deadline = time.monotonic() + REFUSAL_OBSERVATION_SECONDS + while time.monotonic() < deadline: + # Readiness describes process health, so it may remain healthy when + # a bound extract is refused. Once ready, require the signed public + # operation itself to fail; otherwise the bounded absence of + # readiness is the fail-closed path. + try: + result = evidence_common.http_json( + "GET", + service_url(SRO_REQUIREMENT, "/ready"), + {}, + timeout=1.0, + ) + if result.status in {200, 204}: + break + except BaseException: + pass + time.sleep(0.5) + try: + self.observe_evidence(SRO_REQUIREMENT) + except LifecycleProofError: + return + raise LifecycleProofError(_PUBLIC_ERROR) + + def prove_replacement_refusals(self) -> None: + active_document, _, active_id = _load_sro_binding(self.sro_runtime) + active_runtime = self.sro_runtime.read_bytes() + active = publisher.extract_path(self.root, active_id) + before = active.read_bytes() + try: + publisher.publish_extract( + self.root, + "sro", + _timestamp(_now()), + active_id, + ) + except FileExistsError: + pass + else: + raise LifecycleProofError(_PUBLIC_ERROR) + if active.read_bytes() != before: + raise LifecycleProofError(_PUBLIC_ERROR) + if not isinstance(active_document, dict): + raise LifecycleProofError(_PUBLIC_ERROR) + + invalid_paths: list[Path] = [] + try: + for stale in (True, False): + candidate = _publish_invalid_sro(self.root, stale=stale) + invalid_paths.append(candidate) + _replace_runtime_binding( + self.sro_runtime, + str(SRO_CONTAINER_DIRECTORY / candidate.name), + ) + self.restart_sro() + self._wait_sro_refused() + _replace_file(self.sro_runtime, active_runtime) + self.restart_sro() + self.wait_sro_ready() + finally: + _replace_file(self.sro_runtime, active_runtime) + try: + self.restart_sro() + self.wait_sro_ready() + except BaseException: + pass + for candidate in invalid_paths: + candidate.unlink(missing_ok=True) + + def restore_sro_binding(self, original: bytes) -> None: + _replace_file(self.sro_runtime, original) + + def discard_sro_publication(self, publication: ExtractPublication) -> None: + _, _, active_id = _load_sro_binding(self.sro_runtime) + if active_id == publication.extract_id: + raise LifecycleProofError(_PUBLIC_ERROR) + publication.path.unlink(missing_ok=True) + + +def run_proof(operations: Operations | None = None) -> dict[str, Any]: + """Run the live proof and return a deliberately value-free result.""" + + operations = operations or LocalOperations() + checks: dict[str, bool] = { + "signed-http-observations": False, + "relay-live-change-without-restart": False, + "extract-stays-bound-until-restart": False, + "sro-only-restart-activates-publication": False, + "replacement-failures-close": False, + "deterministic-state-restored": False, + } + original_binding: bytes | None = None + initial_generations: dict[str, str] = {} + published: ExtractPublication | None = None + mosd_fixture: MosdFixture | None = None + sro_may_need_restore = False + primary_error: BaseException | None = None + cleanup_error: BaseException | None = None + + try: + operations.load_environment() + operations.validate_preconditions() + initial_generations = { + service: operations.generation(service) for service in TOPOLOGY_SERVICES + } + mosd_fixture = operations.capture_mosd_fixture() + original_binding = operations.capture_sro_binding() + + mosd_before = operations.observe_evidence(MOSD_REQUIREMENT) + sro_before = operations.observe_evidence(SRO_REQUIREMENT) + if mosd_before is not True or sro_before is not True: + raise LifecycleProofError(_PUBLIC_ERROR) + checks["signed-http-observations"] = True + + operations.mutate_mosd(True) + mosd_after = operations.observe_evidence(MOSD_REQUIREMENT) + if mosd_after is not False: + raise LifecycleProofError(_PUBLIC_ERROR) + for service in (MOSD_RELAY_SERVICE, MOSD_SERVICE): + if operations.generation(service) != initial_generations[service]: + raise LifecycleProofError(_PUBLIC_ERROR) + checks["relay-live-change-without-restart"] = True + + published = operations.publish_changed_sro() + if operations.observe_evidence(SRO_REQUIREMENT) is not sro_before: + raise LifecycleProofError(_PUBLIC_ERROR) + + sro_may_need_restore = True + operations.bind_sro(published) + if operations.observe_evidence(SRO_REQUIREMENT) is not sro_before: + raise LifecycleProofError(_PUBLIC_ERROR) + checks["extract-stays-bound-until-restart"] = True + + operations.restart_sro() + operations.wait_sro_ready() + if operations.observe_evidence(SRO_REQUIREMENT) is not False: + raise LifecycleProofError(_PUBLIC_ERROR) + for service in NON_SRO_SERVICES: + if operations.generation(service) != initial_generations[service]: + raise LifecycleProofError(_PUBLIC_ERROR) + if operations.generation(SRO_SERVICE) == initial_generations[SRO_SERVICE]: + raise LifecycleProofError(_PUBLIC_ERROR) + checks["sro-only-restart-activates-publication"] = True + + operations.prove_replacement_refusals() + for service in NON_SRO_SERVICES: + if operations.generation(service) != initial_generations[service]: + raise LifecycleProofError(_PUBLIC_ERROR) + checks["replacement-failures-close"] = True + except BaseException as error: + primary_error = error + finally: + cleanup_errors: list[BaseException] = [] + if mosd_fixture is not None: + try: + operations.restore_mosd_fixture(mosd_fixture) + except BaseException as error: + cleanup_errors.append(error) + sro_restored = original_binding is None + if original_binding is not None: + try: + operations.restore_sro_binding(original_binding) + if sro_may_need_restore: + operations.restart_sro() + operations.wait_sro_ready() + sro_restored = True + except BaseException as error: + cleanup_errors.append(error) + if published is not None and sro_restored: + try: + operations.discard_sro_publication(published) + except BaseException as error: + cleanup_errors.append(error) + if primary_error is None and not cleanup_errors: + try: + if operations.observe_evidence(MOSD_REQUIREMENT) is not True: + raise LifecycleProofError(_PUBLIC_ERROR) + if operations.observe_evidence(SRO_REQUIREMENT) is not True: + raise LifecycleProofError(_PUBLIC_ERROR) + for service in NON_SRO_SERVICES: + if operations.generation(service) != initial_generations[service]: + raise LifecycleProofError(_PUBLIC_ERROR) + checks["deterministic-state-restored"] = True + except BaseException as error: + cleanup_errors.append(error) + if cleanup_errors: + cleanup_error = cleanup_errors[0] + + if primary_error is not None or cleanup_error is not None: + raise _safe_failure( + cleanup_error or primary_error or LifecycleProofError(_PUBLIC_ERROR) + ) + if not all(checks.values()): + raise LifecycleProofError(_PUBLIC_ERROR) + return {"status": "pass", "proof": "live-http", "checks": checks} + + +def _assert_sanitized(value: str) -> None: + for pattern in _FORBIDDEN_OUTPUT_PATTERNS: + if pattern.search(value): + raise LifecycleProofError("proof output violated its redaction contract") + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Prove Relay-live and immutable-extract Evidence lifecycles" + ) + parser.add_argument("--json", action="store_true") + args = parser.parse_args(argv) + try: + result = run_proof() + rendered = json.dumps(result, sort_keys=True) + _assert_sanitized(rendered) + if args.json: + print(rendered) + else: + print("live-lifecycle-proof: pass") + return 0 + except BaseException: + print("live-lifecycle-proof: failed", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/local-relay-source-publisher.py b/scripts/local-relay-source-publisher.py new file mode 100644 index 0000000..ddc3974 --- /dev/null +++ b/scripts/local-relay-source-publisher.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +"""Publish authority-isolated Relay SQLite sources into local named volumes. + +The command deliberately reports only a generic operation result. Source rows, +selectors, backup contents, and database diagnostics never cross its output +boundary. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import sqlite3 +import stat +import sys +import tempfile +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +AUTHORITIES = ("cra", "nia", "mosd", "sipf", "nagdi") +EXPECTED_OBJECTS = { + "cra": { + ("table", "civil_person_source"), + ("view", "relay_civil_person"), + }, + "nia": { + ("table", "population_person_source"), + ("view", "relay_population_person"), + }, + "mosd": { + ("table", "beneficiary_enrolment_source"), + ("view", "relay_beneficiary_enrolment"), + }, + "sipf": { + ("table", "pension_case_source"), + ("table", "survivor_case_source"), + ("view", "relay_pension_payment"), + ("view", "relay_survivor_case"), + }, + "nagdi": { + ("table", "farmer_voucher_source"), + ("table", "livestock_movement_source"), + ("view", "relay_farmer_voucher"), + ("view", "relay_livestock_movement"), + }, +} +MOSD_CONTROL_SELECTOR = "2300010248" +MOSD_TABLE = "beneficiary_enrolment_source" +MOSD_COLUMNS = ( + "record_id", + "record_revision", + "lifecycle_state", + "recorded_at", + "uin", + "duplicate_flag", +) +BACKUP_NAME = ".lifecycle-proof-backup.json" +BACKUP_VERSION = 1 +SUCCESS_MESSAGE = "publisher operation completed" +FAILURE_MESSAGE = "publisher operation failed" + + +class PublisherError(RuntimeError): + """A deliberately value-free publisher failure.""" + + +class QuietArgumentParser(argparse.ArgumentParser): + def error(self, message: str) -> None: + del message + raise PublisherError(FAILURE_MESSAGE) + + +def _fail() -> PublisherError: + return PublisherError(FAILURE_MESSAGE) + + +def _expected_filename(authority: str) -> str: + return f"{authority}.sqlite" + + +def _validate_paths(authority: str, database: Path, seed: Path) -> None: + if authority not in EXPECTED_OBJECTS: + raise _fail() + expected = _expected_filename(authority) + if database.name != expected or seed.name != expected: + raise _fail() + if database.resolve(strict=False) == seed.resolve(strict=False): + raise _fail() + + +def _connect_read_only(path: Path) -> sqlite3.Connection: + try: + return sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True) + except (OSError, sqlite3.Error): + raise _fail() from None + + +def _structural_schema(path: Path, authority: str) -> tuple[Any, ...]: + try: + if not path.is_file() or path.is_symlink(): + raise _fail() + with _connect_read_only(path) as connection: + integrity = connection.execute("PRAGMA quick_check").fetchall() + if integrity != [("ok",)]: + raise _fail() + objects = connection.execute( + """ + SELECT type, name, tbl_name, sql + FROM sqlite_schema + WHERE name NOT LIKE 'sqlite_%' + ORDER BY type, name + """ + ).fetchall() + object_names = {(row[0], row[1]) for row in objects} + if object_names != EXPECTED_OBJECTS[authority]: + raise _fail() + user_version = connection.execute("PRAGMA user_version").fetchone() + application_id = connection.execute("PRAGMA application_id").fetchone() + encoding = connection.execute("PRAGMA encoding").fetchone() + return (tuple(objects), user_version, application_id, encoding) + except PublisherError: + raise + except (OSError, sqlite3.Error): + raise _fail() from None + + +def _validate_schema_match(authority: str, database: Path, seed: Path) -> None: + seed_schema = _structural_schema(seed, authority) + database_schema = _structural_schema(database, authority) + if database_schema != seed_schema: + raise _fail() + + +def _validate_database(authority: str, database: Path, seed: Path) -> None: + _validate_paths(authority, database, seed) + _validate_schema_match(authority, database, seed) + + +def _fsync_directory(directory: Path) -> None: + descriptor = os.open(directory, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + + +def ensure_seeded(authority: str, database: Path, seed: Path) -> None: + database = database.absolute() + seed = seed.absolute() + _validate_paths(authority, database, seed) + _structural_schema(seed, authority) + try: + database.parent.mkdir(parents=True, exist_ok=True) + if os.path.lexists(database): + _validate_database(authority, database, seed) + return + + descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{database.name}.", suffix=".tmp", dir=database.parent + ) + temporary = Path(temporary_name) + with os.fdopen(descriptor, "wb") as output, seed.open("rb") as source: + shutil.copyfileobj(source, output) + output.flush() + os.fsync(output.fileno()) + # The publisher remains the sole writer. Relay mounts the authority + # volume read-only and may run under a different unprivileged UID. + temporary.chmod( + stat.S_IRUSR + | stat.S_IWUSR + | stat.S_IRGRP + | stat.S_IROTH + ) + _validate_schema_match(authority, temporary, seed) + try: + os.link(temporary, database) + except FileExistsError: + _validate_database(authority, database, seed) + _fsync_directory(database.parent) + _validate_database(authority, database, seed) + except PublisherError: + raise + except (OSError, sqlite3.Error): + raise _fail() from None + finally: + if "temporary" in locals(): + temporary.unlink(missing_ok=True) + + +def _backup_path(database: Path) -> Path: + return database.parent / BACKUP_NAME + + +def _json_bytes(value: dict[str, Any]) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + + +def _backup_envelope(database: Path, row: tuple[Any, ...]) -> dict[str, Any]: + identity = database.stat() + payload: dict[str, Any] = { + "version": BACKUP_VERSION, + "authority": "mosd", + "database_device": identity.st_dev, + "database_inode": identity.st_ino, + "row": list(row), + } + return { + "payload": payload, + "sha256": hashlib.sha256(_json_bytes(payload)).hexdigest(), + } + + +def _read_backup(database: Path) -> dict[str, Any]: + path = _backup_path(database) + try: + if not path.is_file() or path.is_symlink(): + raise _fail() + if stat.S_IMODE(path.stat().st_mode) != 0o600: + raise _fail() + envelope = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(envelope, dict) or set(envelope) != {"payload", "sha256"}: + raise _fail() + payload = envelope["payload"] + if not isinstance(payload, dict) or set(payload) != { + "version", + "authority", + "database_device", + "database_inode", + "row", + }: + raise _fail() + if hashlib.sha256(_json_bytes(payload)).hexdigest() != envelope["sha256"]: + raise _fail() + identity = database.stat() + if ( + payload["version"] != BACKUP_VERSION + or payload["authority"] != "mosd" + or payload["database_device"] != identity.st_dev + or payload["database_inode"] != identity.st_ino + or not isinstance(payload["row"], list) + or len(payload["row"]) != len(MOSD_COLUMNS) + or payload["row"][4] != MOSD_CONTROL_SELECTOR + ): + raise _fail() + return payload + except PublisherError: + raise + except (OSError, ValueError, TypeError, json.JSONDecodeError): + raise _fail() from None + + +def _write_backup(database: Path, envelope: dict[str, Any]) -> None: + path = _backup_path(database) + descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{BACKUP_NAME}.", suffix=".tmp", dir=database.parent + ) + temporary = Path(temporary_name) + try: + os.fchmod(descriptor, 0o600) + with os.fdopen(descriptor, "wb") as output: + output.write(_json_bytes(envelope)) + output.flush() + os.fsync(output.fileno()) + try: + os.link(temporary, path) + except FileExistsError: + _read_backup(database) + _fsync_directory(database.parent) + except PublisherError: + raise + except OSError: + raise _fail() from None + finally: + temporary.unlink(missing_ok=True) + + +def _validate_mosd(authority: str, database: Path, seed: Path) -> None: + if authority != "mosd": + raise _fail() + _validate_database(authority, database, seed) + + +def _open_mutable(database: Path) -> sqlite3.Connection: + try: + connection = sqlite3.connect(database) + connection.execute("PRAGMA journal_mode = DELETE") + connection.execute("PRAGMA synchronous = FULL") + connection.execute("PRAGMA foreign_keys = ON") + connection.execute("PRAGMA secure_delete = ON") + return connection + except sqlite3.Error: + raise _fail() from None + + +def _control_row(connection: sqlite3.Connection) -> tuple[Any, ...]: + try: + row = connection.execute( + f"SELECT {', '.join(MOSD_COLUMNS)} FROM {MOSD_TABLE} WHERE uin = ?", + (MOSD_CONTROL_SELECTOR,), + ).fetchone() + if row is None: + raise _fail() + return tuple(row) + except PublisherError: + raise + except sqlite3.Error: + raise _fail() from None + + +def begin_proof(authority: str, database: Path, seed: Path) -> None: + database = database.absolute() + seed = seed.absolute() + _validate_mosd(authority, database, seed) + backup = _backup_path(database) + if os.path.lexists(backup): + _read_backup(database) + return + try: + with _open_mutable(database) as connection: + row = _control_row(connection) + _write_backup(database, _backup_envelope(database, row)) + except PublisherError: + raise + except (OSError, sqlite3.Error): + raise _fail() from None + + +def _revision(row: tuple[Any, ...], recorded_at: str) -> str: + value = { + "record_id": str(row[0]), + "lifecycle_state": str(row[2]), + "recorded_at": recorded_at, + "uin": MOSD_CONTROL_SELECTOR, + "duplicate_flag": 1, + } + encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode() + return f"rev-{hashlib.sha256(encoded).hexdigest()[:16]}" + + +def _assert_same_inode(database: Path, before: os.stat_result) -> None: + after = database.stat() + if (before.st_dev, before.st_ino) != (after.st_dev, after.st_ino): + raise _fail() + + +def set_proof_state(authority: str, database: Path, seed: Path) -> None: + database = database.absolute() + seed = seed.absolute() + _validate_mosd(authority, database, seed) + payload = _read_backup(database) + before = database.stat() + try: + with _open_mutable(database) as connection: + connection.execute("BEGIN IMMEDIATE") + row = _control_row(connection) + if row[0] != payload["row"][0] or row[4] != payload["row"][4]: + raise _fail() + if row[5] != 1: + recorded_at = ( + datetime.now(UTC) + .isoformat(timespec="microseconds") + .replace("+00:00", "Z") + ) + cursor = connection.execute( + f""" + UPDATE {MOSD_TABLE} + SET duplicate_flag = ?, record_revision = ?, recorded_at = ? + WHERE uin = ? + """, + (1, _revision(row, recorded_at), recorded_at, MOSD_CONTROL_SELECTOR), + ) + if cursor.rowcount != 1: + raise _fail() + connection.commit() + _assert_same_inode(database, before) + except PublisherError: + raise + except (OSError, sqlite3.Error): + raise _fail() from None + + +def restore_proof(authority: str, database: Path, seed: Path) -> None: + database = database.absolute() + seed = seed.absolute() + _validate_mosd(authority, database, seed) + backup = _backup_path(database) + if not os.path.lexists(backup): + return + payload = _read_backup(database) + before = database.stat() + row = payload["row"] + try: + with _open_mutable(database) as connection: + connection.execute("BEGIN IMMEDIATE") + current = _control_row(connection) + if current[0] != row[0] or current[4] != row[4]: + raise _fail() + assignments = ", ".join(f"{column} = ?" for column in MOSD_COLUMNS) + cursor = connection.execute( + f"UPDATE {MOSD_TABLE} SET {assignments} WHERE uin = ?", + (*row, MOSD_CONTROL_SELECTOR), + ) + if cursor.rowcount != 1: + raise _fail() + connection.commit() + _assert_same_inode(database, before) + backup.unlink() + _fsync_directory(database.parent) + except PublisherError: + raise + except (OSError, sqlite3.Error): + raise _fail() from None + + +def _parser() -> argparse.ArgumentParser: + parser = QuietArgumentParser(description="Manage a local Relay source") + parser.add_argument("--authority", choices=AUTHORITIES, required=True) + parser.add_argument("--database", type=Path, required=True) + parser.add_argument("--seed", type=Path, required=True) + commands = parser.add_subparsers(dest="command", required=True) + for command in ("ensure-seeded", "begin-proof", "set-proof-state", "restore-proof"): + commands.add_parser(command) + return parser + + +def run(argv: list[str] | None = None) -> None: + args = _parser().parse_args(argv) + operations = { + "ensure-seeded": ensure_seeded, + "begin-proof": begin_proof, + "set-proof-state": set_proof_state, + "restore-proof": restore_proof, + } + operations[args.command](args.authority, args.database, args.seed) + + +def main() -> None: + try: + run() + except (PublisherError, OSError, sqlite3.Error): + print(FAILURE_MESSAGE, file=sys.stderr) + raise SystemExit(1) from None + print(SUCCESS_MESSAGE) + + +if __name__ == "__main__": + main() diff --git a/scripts/local-transit-proxy.py b/scripts/local-transit-proxy.py new file mode 100755 index 0000000..80e3a6d --- /dev/null +++ b/scripts/local-transit-proxy.py @@ -0,0 +1,421 @@ +#!/usr/bin/env python3 +"""Serve one local ES256 key through a narrow Transit-compatible Unix socket. + +The proxy is deliberately one process, one key, and one socket. Registry +processes receive only the socket. The operator-owned private JWK stays outside +their containers and is never rendered in responses or diagnostics. +""" + +from __future__ import annotations + +import argparse +import base64 +import binascii +import json +import os +import re +import signal +import socket +import socketserver +import stat +from pathlib import Path +from typing import Any + +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, utils + +MAX_KEY_BYTES = 16 * 1024 +MAX_REQUEST_LINE_BYTES = 2 * 1024 +MAX_HEADER_BYTES = 8 * 1024 +MAX_HEADER_LINE_BYTES = 2 * 1024 +MAX_REQUEST_BODY_BYTES = 2 * 1024 +MAX_RESPONSE_BODY_BYTES = 64 * 1024 +SOCKET_TIMEOUT_SECONDS = 3 +ERROR_DOCUMENT = b'{"errors":["request refused"]}' +KEY_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}\Z") +HEADER_NAME = re.compile(rb"[!#$%&'*+.^_`|~0-9A-Za-z-]+\Z") +JWK_MEMBERS = {"alg", "crv", "d", "kid", "kty", "x", "y"} + + +class ProxyError(Exception): + """Value-free configuration or request refusal.""" + + +def _strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ProxyError("invalid document") + result[key] = value + return result + + +def _decode_base64url(value: Any) -> bytes: + if not isinstance(value, str) or not value or "=" in value: + raise ProxyError("invalid key") + try: + decoded = base64.b64decode( + value + "=" * (-len(value) % 4), altchars=b"-_", validate=True + ) + except (binascii.Error, ValueError) as error: + raise ProxyError("invalid key") from error + if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != value: + raise ProxyError("invalid key") + return decoded + + +def _read_private_jwk(path: Path) -> tuple[ec.EllipticCurvePrivateKey, str]: + if not path.is_absolute(): + raise ProxyError("invalid key") + flags = os.O_RDONLY + if hasattr(os, "O_CLOEXEC"): + flags |= os.O_CLOEXEC + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + try: + descriptor = os.open(path, flags) + except OSError as error: + raise ProxyError("invalid key") from error + try: + metadata = os.fstat(descriptor) + if ( + not stat.S_ISREG(metadata.st_mode) + or metadata.st_uid != os.geteuid() + or metadata.st_mode & 0o077 + or metadata.st_size <= 0 + or metadata.st_size > MAX_KEY_BYTES + ): + raise ProxyError("invalid key") + chunks = bytearray() + while len(chunks) <= MAX_KEY_BYTES: + chunk = os.read(descriptor, min(4096, MAX_KEY_BYTES + 1 - len(chunks))) + if not chunk: + break + chunks.extend(chunk) + if len(chunks) > MAX_KEY_BYTES: + raise ProxyError("invalid key") + finally: + os.close(descriptor) + + try: + document = json.loads(chunks, object_pairs_hook=_strict_object) + except (json.JSONDecodeError, UnicodeDecodeError, ProxyError) as error: + raise ProxyError("invalid key") from error + if not isinstance(document, dict) or set(document) != JWK_MEMBERS: + raise ProxyError("invalid key") + if ( + document["kty"] != "EC" + or document["crv"] != "P-256" + or document["alg"] != "ES256" + or not isinstance(document["kid"], str) + or not document["kid"].strip() + or len(document["kid"]) > 256 + or any( + ord(character) < 0x20 or ord(character) == 0x7F + for character in document["kid"] + ) + ): + raise ProxyError("invalid key") + + x_bytes = _decode_base64url(document["x"]) + y_bytes = _decode_base64url(document["y"]) + scalar_bytes = _decode_base64url(document["d"]) + if len(x_bytes) != 32 or len(y_bytes) != 32 or len(scalar_bytes) != 32: + raise ProxyError("invalid key") + try: + private_key = ec.derive_private_key( + int.from_bytes(scalar_bytes, "big"), ec.SECP256R1() + ) + except ValueError as error: + raise ProxyError("invalid key") from error + public = private_key.public_key().public_numbers() + if ( + public.x.to_bytes(32, "big") != x_bytes + or public.y.to_bytes(32, "big") != y_bytes + ): + raise ProxyError("invalid key") + public_pem = private_key.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ).decode("ascii") + return private_key, public_pem + + +def _validate_socket_path(path: Path) -> None: + if not path.is_absolute() or path.exists() or path.is_symlink(): + raise ProxyError("invalid socket") + try: + parent = path.parent.stat() + except OSError as error: + raise ProxyError("invalid socket") from error + if ( + not stat.S_ISDIR(parent.st_mode) + or parent.st_uid != os.geteuid() + or parent.st_mode & 0o022 + ): + raise ProxyError("invalid socket") + + +class TransitApplication: + def __init__(self, private_jwk: Path, key_name: str) -> None: + if not KEY_NAME.fullmatch(key_name): + raise ProxyError("invalid key") + self._private_key, public_pem = _read_private_jwk(private_jwk) + self._metadata = json.dumps( + { + "data": { + "allow_plaintext_backup": False, + "deletion_allowed": False, + "derived": False, + "exportable": False, + "imported": True, + "keys": {"1": {"public_key": public_pem}}, + "latest_version": 1, + "min_decryption_version": 1, + "min_encryption_version": 1, + "name": key_name, + "supports_decryption": False, + "supports_derivation": False, + "supports_encryption": False, + "supports_signing": True, + "type": "ecdsa-p256", + } + }, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + self.metadata_path = f"/v1/transit/keys/{key_name}" + self.sign_path = f"/v1/transit/sign/{key_name}/sha2-256" + + def dispatch( + self, method: str, path: str, headers: dict[str, str], body: bytes + ) -> tuple[int, bytes]: + if headers.get("x-vault-request") != "true": + return 403, ERROR_DOCUMENT + if method == "GET" and path == self.metadata_path: + if body: + return 400, ERROR_DOCUMENT + return 200, self._metadata + if method == "POST" and path == self.sign_path: + if headers.get("content-type") != "application/json": + return 400, ERROR_DOCUMENT + try: + document = json.loads(body, object_pairs_hook=_strict_object) + except (json.JSONDecodeError, UnicodeDecodeError, ProxyError): + return 400, ERROR_DOCUMENT + if ( + not isinstance(document, dict) + or set(document) != { + "input", + "key_version", + "marshaling_algorithm", + "prehashed", + } + or type(document["key_version"]) is not int + or document["key_version"] != 1 + or document["marshaling_algorithm"] != "jws" + or document["prehashed"] is not True + or not isinstance(document["input"], str) + ): + return 400, ERROR_DOCUMENT + try: + digest = base64.b64decode(document["input"], validate=True) + except (binascii.Error, ValueError): + return 400, ERROR_DOCUMENT + if len(digest) != 32 or base64.b64encode(digest).decode("ascii") != document["input"]: + return 400, ERROR_DOCUMENT + der_signature = self._private_key.sign( + digest, ec.ECDSA(utils.Prehashed(hashes.SHA256())) + ) + r_value, s_value = utils.decode_dss_signature(der_signature) + raw_signature = r_value.to_bytes(32, "big") + s_value.to_bytes(32, "big") + encoded = base64.urlsafe_b64encode(raw_signature).rstrip(b"=").decode("ascii") + response = json.dumps( + {"data": {"signature": f"vault:v1:{encoded}"}}, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + return 200, response + return 404, ERROR_DOCUMENT + + +class TransitRequestHandler(socketserver.StreamRequestHandler): + server: "TransitServer" + + def handle(self) -> None: + self.connection.settimeout(SOCKET_TIMEOUT_SECONDS) + try: + method, path, headers, body = self._read_request() + status, response = self.server.application.dispatch(method, path, headers, body) + except ProxyError: + status, response = 400, ERROR_DOCUMENT + except (OSError, TimeoutError): + return + except Exception: + status, response = 500, ERROR_DOCUMENT + try: + self._write_response(status, response) + except OSError: + pass + + def _read_request(self) -> tuple[str, str, dict[str, str], bytes]: + request_line = self.rfile.readline(MAX_REQUEST_LINE_BYTES + 1) + if ( + not request_line.endswith(b"\r\n") + or len(request_line) > MAX_REQUEST_LINE_BYTES + ): + raise ProxyError("invalid request") + try: + method, path, version = request_line[:-2].decode("ascii").split(" ") + except (UnicodeDecodeError, ValueError) as error: + raise ProxyError("invalid request") from error + if ( + method not in {"GET", "POST"} + or version != "HTTP/1.1" + or not path.startswith("/") + ): + raise ProxyError("invalid request") + + headers: dict[str, str] = {} + total_header_bytes = 0 + while True: + line = self.rfile.readline(MAX_HEADER_LINE_BYTES + 1) + total_header_bytes += len(line) + if ( + not line + or len(line) > MAX_HEADER_LINE_BYTES + or total_header_bytes > MAX_HEADER_BYTES + or not line.endswith(b"\r\n") + ): + raise ProxyError("invalid request") + if line == b"\r\n": + break + if line[:1] in {b" ", b"\t"} or b":" not in line: + raise ProxyError("invalid request") + name, value = line[:-2].split(b":", 1) + if not HEADER_NAME.fullmatch(name): + raise ProxyError("invalid request") + normalized = name.decode("ascii").lower() + if normalized in headers: + raise ProxyError("invalid request") + try: + decoded = value.strip(b" \t").decode("ascii") + except UnicodeDecodeError as error: + raise ProxyError("invalid request") from error + if any( + ord(character) < 0x20 or ord(character) == 0x7F + for character in decoded + ): + raise ProxyError("invalid request") + headers[normalized] = decoded + + if "transfer-encoding" in headers: + raise ProxyError("invalid request") + length_text = headers.get("content-length") + if length_text is None: + content_length = 0 + elif not length_text.isascii() or not length_text.isdecimal(): + raise ProxyError("invalid request") + else: + content_length = int(length_text) + if content_length > MAX_REQUEST_BODY_BYTES: + raise ProxyError("invalid request") + body = self.rfile.read(content_length) + if len(body) != content_length: + raise ProxyError("invalid request") + return method, path, headers, body + + def _write_response(self, status: int, body: bytes) -> None: + if len(body) > MAX_RESPONSE_BODY_BYTES: + status, body = 500, ERROR_DOCUMENT + reason = { + 200: "OK", + 400: "Bad Request", + 403: "Forbidden", + 404: "Not Found", + 500: "Internal Server Error", + }[status] + head = ( + f"HTTP/1.1 {status} {reason}\r\n" + "Content-Type: application/json\r\n" + "Cache-Control: no-store\r\n" + f"Content-Length: {len(body)}\r\n" + "Connection: close\r\n\r\n" + ).encode("ascii") + self.wfile.write(head + body) + + +class TransitServer(socketserver.UnixStreamServer): + allow_reuse_address = False + + def __init__(self, socket_path: Path, application: TransitApplication) -> None: + _validate_socket_path(socket_path) + self.application = application + self._socket_path = socket_path + self._socket_identity: tuple[int, int] | None = None + previous_umask = os.umask(0o077) + try: + super().__init__(str(socket_path), TransitRequestHandler) + socket_path.chmod(0o600) + metadata = socket_path.stat() + if not stat.S_ISSOCK(metadata.st_mode) or stat.S_IMODE(metadata.st_mode) != 0o600: + raise ProxyError("invalid socket") + self._socket_identity = (metadata.st_dev, metadata.st_ino) + except Exception: + try: + socket_path.unlink() + except OSError: + pass + raise + finally: + os.umask(previous_umask) + + def handle_error(self, request: socket.socket, client_address: object) -> None: + # Deliberately suppress value-bearing exception diagnostics. + del request, client_address + + def server_close(self) -> None: + super().server_close() + try: + metadata = self._socket_path.lstat() + except FileNotFoundError: + return + identity = (metadata.st_dev, metadata.st_ino) + if stat.S_ISSOCK(metadata.st_mode) and identity == self._socket_identity: + self._socket_path.unlink() + + +def build_server(private_jwk: Path, socket_path: Path, key_name: str) -> TransitServer: + return TransitServer(socket_path, TransitApplication(private_jwk, key_name)) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Expose one operator-owned ES256 JWK on one local Transit Unix socket." + ) + parser.add_argument("--private-jwk", required=True, type=Path) + parser.add_argument("--socket", required=True, type=Path) + parser.add_argument("--key-name", required=True) + arguments = parser.parse_args() + try: + server = build_server(arguments.private_jwk, arguments.socket, arguments.key_name) + except (OSError, ProxyError, ValueError): + print("local Transit proxy could not start", file=os.sys.stderr) + return 1 + + def stop(_signum: int, _frame: object) -> None: + raise KeyboardInterrupt + + signal.signal(signal.SIGTERM, stop) + signal.signal(signal.SIGINT, stop) + try: + server.serve_forever(poll_interval=0.1) + except KeyboardInterrupt: + pass + finally: + server.server_close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/local-transit-signers.py b/scripts/local-transit-signers.py new file mode 100644 index 0000000..c45e765 --- /dev/null +++ b/scripts/local-transit-signers.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Start and stop the seven PID-tracked local Transit signer proxies.""" + +from __future__ import annotations + +import argparse +import hashlib +import os +import signal +import socket +import stat +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PROVIDERS = ("mint", "cra", "nia", "sro", "mosd-programme", "sipf", "nagdi") +STATE = ROOT / "runtime/local-transit-signers" +PROXY = ROOT / "scripts/local-transit-proxy.py" +ALIAS_ROOT = Path("/tmp") / f"solmara-transit-{hashlib.sha256(str(ROOT).encode()).hexdigest()[:12]}" + + +def key_name(provider: str) -> str: + return "solmara-mint" if provider == "mint" else f"solmara-evidence-{provider}" + + +def paths(provider: str) -> tuple[Path, Path, Path]: + base = ROOT / "config/evidence/local/cells" / provider + return base / "secrets/signing.jwk", base / "transit/transit-proxy.sock", STATE / f"{provider}.pid" + + +def bind_path(provider: str, socket_path: Path) -> Path: + ALIAS_ROOT.mkdir(mode=0o700, exist_ok=True) + ALIAS_ROOT.chmod(0o700) + alias = ALIAS_ROOT / provider + expected = socket_path.parent.resolve() + if alias.is_symlink(): + if alias.resolve() != expected: + raise OSError("unexpected Transit alias") + elif alias.exists(): + raise OSError("unexpected Transit alias") + else: + alias.symlink_to(expected, target_is_directory=True) + return alias / socket_path.name + + +def owned_process(pid_file: Path) -> int | None: + try: + value = pid_file.read_text(encoding="ascii").strip() + pid = int(value) + os.kill(pid, 0) + except (FileNotFoundError, ProcessLookupError, PermissionError, ValueError): + return None + command = subprocess.run( + ["ps", "-p", str(pid), "-o", "command="], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ).stdout + if str(PROXY) not in command: + return None + return pid + + +def wait_for_socket(path: Path, timeout: float = 5.0) -> bool: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + if stat.S_ISSOCK(path.stat().st_mode): + with socket.socket(socket.AF_UNIX) as client: + client.settimeout(0.25) + client.connect(str(path)) + return True + except (FileNotFoundError, OSError): + pass + time.sleep(0.05) + return False + + +def start() -> int: + STATE.mkdir(parents=True, exist_ok=True, mode=0o700) + STATE.chmod(0o700) + started: list[str] = [] + for provider in PROVIDERS: + private_jwk, socket_path, pid_file = paths(provider) + existing = owned_process(pid_file) + socket_bind_path = bind_path(provider, socket_path) + if existing is not None and wait_for_socket(socket_bind_path, timeout=0.1): + continue + pid_file.unlink(missing_ok=True) + if socket_path.exists() or socket_path.is_symlink(): + print(f"{provider}: refusing an unowned Transit socket", file=sys.stderr) + stop(started) + return 1 + socket_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + socket_path.parent.chmod(0o700) + if not private_jwk.is_file(): + print(f"{provider}: signing key is missing; run just generate", file=sys.stderr) + stop(started) + return 1 + process = subprocess.Popen( + [sys.executable, str(PROXY), "--private-jwk", str(private_jwk), "--socket", str(socket_bind_path), "--key-name", key_name(provider)], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + ) + pid_file.write_text(f"{process.pid}\n", encoding="ascii") + pid_file.chmod(0o600) + started.append(provider) + if not wait_for_socket(socket_bind_path): + print(f"{provider}: Transit proxy did not become ready", file=sys.stderr) + stop(started) + return 1 + return 0 + + +def stop(providers: list[str] | tuple[str, ...] = PROVIDERS) -> int: + for provider in reversed(providers): + _, socket_path, pid_file = paths(provider) + pid = owned_process(pid_file) + if pid is None: + pid_file.unlink(missing_ok=True) + continue + os.kill(pid, signal.SIGTERM) + deadline = time.monotonic() + 5 + while time.monotonic() < deadline: + try: + os.kill(pid, 0) + except ProcessLookupError: + break + time.sleep(0.05) + pid_file.unlink(missing_ok=True) + if socket_path.exists() and not stat.S_ISSOCK(socket_path.lstat().st_mode): + print(f"{provider}: signer stopped but socket path was replaced", file=sys.stderr) + return 1 + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("action", choices=("up", "down")) + args = parser.parse_args() + return start() if args.action == "up" else stop() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/notary_state_restart.py b/scripts/notary_state_restart.py deleted file mode 100644 index 186c477..0000000 --- a/scripts/notary_state_restart.py +++ /dev/null @@ -1,560 +0,0 @@ -#!/usr/bin/env python3 -"""Prove that local Notary PostgreSQL correctness state survives replacement. - -This is deliberately a Solmara-local PostgreSQL 16 gate. It exercises the -operator-facing ``just down`` / ``just up`` path, verifies that PostgreSQL uses -the checkout's named data volume, and compares every Notary correctness-state -table before any post-restart doctor or scenario request can create new state. -""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -import time -from dataclasses import dataclass -from pathlib import Path, PurePosixPath -from typing import Mapping, Sequence - - -ROOT = Path(__file__).resolve().parents[1] -EXPECTED_POSTGRES_MAJOR = 16 -EXPECTED_PGDATA = PurePosixPath("/var/lib/postgresql/data") -DEFAULT_WAIT_SECONDS = 300.0 - -AUTHORITIES = ( - ("cra", "cra-notary"), - ("nia", "nia-notary"), - ("sro", "sro-notary"), - ("programme", "programme-notary"), - ("sipf", "sipf-notary"), - ("nagdi", "nagdi-notary"), -) - -CORRECTNESS_TABLES = ( - "replay_identifier", - "consumable_nonce", - "evaluation", - "batch_idempotency", - "credential_status", - "machine_quota", - "subject_access_quota", - "preauthorization_login_state", - "preauthorization_tx_code", -) - - -class GateError(RuntimeError): - """The restart-persistence proof could not be completed safely.""" - - -class CommandError(GateError): - """One bounded external command failed.""" - - -class CommandRunner: - """Injectable command and clock boundary used by the production gate.""" - - def run( - self, - command: Sequence[str], - *, - cwd: Path, - env: Mapping[str, str], - ) -> str: - completed = subprocess.run( - list(command), - cwd=cwd, - env=dict(env), - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - check=False, - ) - if completed.returncode != 0: - detail = (completed.stderr or completed.stdout).strip() - if len(detail) > 4_000: - detail = detail[-4_000:] - rendered = " ".join(command) - suffix = f": {detail}" if detail else "" - raise CommandError( - f"command failed with exit {completed.returncode}: {rendered}{suffix}" - ) - return completed.stdout - - def monotonic(self) -> float: - return time.monotonic() - - def sleep(self, seconds: float) -> None: - time.sleep(seconds) - - -@dataclass(frozen=True) -class StateSnapshot: - container_id: str - system_identifier: str - server_version_num: int - data_directory: str - volume_name: str - counts: tuple[tuple[str, str, int], ...] - - def totals(self) -> dict[str, int]: - totals = {authority: 0 for authority, _service in AUTHORITIES} - for authority, _table, count in self.counts: - totals[authority] += count - return totals - - -def _is_postgres_storage_path(path: PurePosixPath) -> bool: - """Return true when a mount can cover, replace, or subdivide PGDATA.""" - - return ( - path == EXPECTED_PGDATA - or path in EXPECTED_PGDATA.parents - or EXPECTED_PGDATA in path.parents - ) - - -def validate_compose_pgdata_layout(config: object) -> str: - """Return the resolved named volume after validating the PG16 layout.""" - - if not isinstance(config, dict): - raise GateError("Compose configuration is not an object") - services = config.get("services") - volumes = config.get("volumes") - if not isinstance(services, dict) or not isinstance(volumes, dict): - raise GateError("Compose configuration is missing services or volumes") - postgres = services.get("postgres") - declared = volumes.get("postgres-data") - if not isinstance(postgres, dict) or not isinstance(declared, dict): - raise GateError("Compose must declare postgres and postgres-data") - resolved_name = declared.get("name") - if not isinstance(resolved_name, str) or not resolved_name: - raise GateError("Compose did not resolve the postgres-data volume name") - - configured = postgres.get("volumes") - if not isinstance(configured, list): - raise GateError("PostgreSQL has no configured volumes") - postgres_storage = [] - for mount in configured: - if not isinstance(mount, dict): - raise GateError("PostgreSQL volume configuration is not normalized") - target = mount.get("target") - if isinstance(target, str) and _is_postgres_storage_path(PurePosixPath(target)): - postgres_storage.append(mount) - expected = [ - mount - for mount in postgres_storage - if mount.get("type") == "volume" - and mount.get("source") == "postgres-data" - and mount.get("target") == str(EXPECTED_PGDATA) - and not mount.get("read_only", False) - ] - if len(postgres_storage) != 1 or len(expected) != 1: - raise GateError( - "PostgreSQL 16 must mount only named postgres-data directly and " - "read-write at /var/lib/postgresql/data" - ) - return resolved_name - - -def validate_runtime_pgdata_mounts( - mounts: object, - *, - expected_volume: str, - volume_labels: object, - compose_project_name: str, -) -> None: - """Reject anonymous, parent, nested, bind, and read-only PGDATA mounts.""" - - if not isinstance(mounts, list): - raise GateError("Docker did not return PostgreSQL mounts") - storage_mounts = [] - for mount in mounts: - if not isinstance(mount, dict): - raise GateError("Docker returned a malformed PostgreSQL mount") - destination = mount.get("Destination") - if isinstance(destination, str) and _is_postgres_storage_path( - PurePosixPath(destination) - ): - storage_mounts.append(mount) - if len(storage_mounts) != 1: - raise GateError( - "PostgreSQL has an anonymous, parent, or nested volume overlapping PGDATA" - ) - mount = storage_mounts[0] - if ( - mount.get("Type") != "volume" - or mount.get("Name") != expected_volume - or mount.get("Destination") != str(EXPECTED_PGDATA) - or mount.get("RW") is not True - ): - raise GateError( - "PostgreSQL PGDATA is not the expected writable named postgres-data volume" - ) - if not isinstance(volume_labels, dict): - raise GateError("PostgreSQL named volume has no Compose ownership labels") - if ( - volume_labels.get("com.docker.compose.project") != compose_project_name - or volume_labels.get("com.docker.compose.volume") != "postgres-data" - ): - raise GateError( - "PostgreSQL PGDATA volume is anonymous or belongs to another Compose project" - ) - - -def parse_control_snapshot(output: str) -> tuple[str, int, str]: - lines = [line.strip() for line in output.splitlines() if line.strip()] - if len(lines) != 3: - raise GateError("PostgreSQL control query did not return exactly three values") - system_identifier, version_text, data_directory = lines - if not system_identifier.isdecimal() or int(system_identifier) <= 0: - raise GateError("PostgreSQL returned an invalid system identifier") - if not version_text.isdecimal(): - raise GateError("PostgreSQL returned an invalid server_version_num") - server_version_num = int(version_text) - if server_version_num // 10_000 != EXPECTED_POSTGRES_MAJOR: - raise GateError( - "the local restart gate is deliberately pinned to PostgreSQL 16; " - "follow the documented major-upgrade procedure" - ) - if data_directory != str(EXPECTED_PGDATA): - raise GateError( - f"PostgreSQL data_directory is {data_directory!r}, expected " - f"{str(EXPECTED_PGDATA)!r}" - ) - return system_identifier, server_version_num, data_directory - - -def parse_authority_counts(authority: str, output: str) -> dict[str, int]: - expected = set(CORRECTNESS_TABLES) - parsed: dict[str, int] = {} - for line in output.splitlines(): - if not line.strip(): - continue - parts = line.strip().split("|") - if len(parts) != 2: - raise GateError(f"{authority} returned a malformed correctness-state row") - table, count_text = parts - if table not in expected or table in parsed or not count_text.isdecimal(): - raise GateError(f"{authority} returned invalid or duplicate table counts") - parsed[table] = int(count_text) - if set(parsed) != expected: - missing = sorted(expected - set(parsed)) - raise GateError( - f"{authority} correctness-state snapshot is incomplete: {', '.join(missing)}" - ) - if sum(parsed.values()) == 0: - raise GateError( - f"{authority} has no correctness rows; run live smoke before the restart gate" - ) - return parsed - - -def compare_snapshots(before: StateSnapshot, after: StateSnapshot) -> None: - if before.container_id == after.container_id: - raise GateError("just down/up reused the PostgreSQL container") - if before.system_identifier != after.system_identifier: - raise GateError( - "PostgreSQL system_identifier changed across just down/up; " - "the named cluster was not preserved" - ) - if before.server_version_num != after.server_version_num: - raise GateError("PostgreSQL server_version_num changed across restart") - if before.data_directory != after.data_directory: - raise GateError("PostgreSQL data_directory changed across restart") - if before.volume_name != after.volume_name: - raise GateError("PostgreSQL named volume changed across restart") - if before.counts != after.counts: - before_map = {(a, t): c for a, t, c in before.counts} - after_map = {(a, t): c for a, t, c in after.counts} - changes = [ - f"{authority}.{table}: {before_map.get((authority, table))} -> " - f"{after_map.get((authority, table))}" - for authority, table in sorted(set(before_map) | set(after_map)) - if before_map.get((authority, table)) != after_map.get((authority, table)) - ] - raise GateError( - "Notary correctness-state counts changed across restart: " + "; ".join(changes) - ) - - -class RestartPersistenceGate: - def __init__( - self, - *, - root: Path = ROOT, - runner: CommandRunner | None = None, - environ: Mapping[str, str] | None = None, - wait_seconds: float = DEFAULT_WAIT_SECONDS, - ) -> None: - self.root = root.resolve() - self.runner = runner or CommandRunner() - self.env = dict(os.environ if environ is None else environ) - self.wait_seconds = wait_seconds - self.compose_project_name = "" - self.compose: list[str] = [] - self.expected_volume = "" - - def _run(self, command: Sequence[str]) -> str: - return self.runner.run(command, cwd=self.root, env=self.env) - - def resolve_compose(self) -> None: - for required in (".env", "versions.env", "compose.yaml"): - if not (self.root / required).is_file(): - raise GateError(f"{required} is missing") - project = self.env.get("COMPOSE_PROJECT_NAME", "").strip() - if not project: - project = self._run( - [sys.executable, str(self.root / "scripts" / "compose_project_name.py")] - ).strip() - if not project: - raise GateError("could not resolve COMPOSE_PROJECT_NAME") - self.compose_project_name = project - self.env["COMPOSE_PROJECT_NAME"] = project - self.compose = [ - "docker", - "compose", - "--env-file", - str(self.root / "versions.env"), - "--env-file", - str(self.root / ".env"), - "-f", - str(self.root / "compose.yaml"), - ] - rendered = self._run([*self.compose, "config", "--format", "json"]) - try: - config = json.loads(rendered) - except json.JSONDecodeError as error: - raise GateError("Docker Compose returned invalid configuration JSON") from error - self.expected_volume = validate_compose_pgdata_layout(config) - - def _compose(self, *arguments: str) -> str: - if not self.compose: - raise GateError("Compose has not been resolved") - return self._run([*self.compose, *arguments]) - - def service_container_id(self, service: str, *, include_stopped: bool = False) -> str: - arguments = ["ps", "-q"] - if include_stopped: - arguments.append("--all") - arguments.append(service) - output = self._compose(*arguments).strip() - identifiers = [line for line in output.splitlines() if line] - if len(identifiers) > 1: - raise GateError(f"Compose returned multiple containers for {service}") - return identifiers[0] if identifiers else "" - - def _container_health(self, container_id: str) -> str: - return self._run( - [ - "docker", - "inspect", - "--format", - "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}", - container_id, - ] - ).strip() - - def _installer_status(self, container_id: str) -> tuple[str, int]: - output = self._run( - [ - "docker", - "inspect", - "--format", - "{{.State.Status}}|{{.State.ExitCode}}", - container_id, - ] - ).strip() - parts = output.split("|") - if len(parts) != 2 or not parts[1].lstrip("-").isdecimal(): - raise GateError("Docker returned a malformed installer status") - return parts[0], int(parts[1]) - - def wait_for_stack(self) -> None: - deadline = self.runner.monotonic() + self.wait_seconds - health_services = ("postgres", *(service for _key, service in AUTHORITIES)) - installers = tuple(f"{service}-state-install" for _key, service in AUTHORITIES) - last_pending: list[str] = [] - while self.runner.monotonic() < deadline: - pending: list[str] = [] - failed: list[str] = [] - for service in health_services: - container = self.service_container_id(service) - if not container: - pending.append(f"{service}=missing") - continue - status = self._container_health(container) - if status != "healthy": - pending.append(f"{service}={status or 'unknown'}") - for service in installers: - container = self.service_container_id(service, include_stopped=True) - if not container: - pending.append(f"{service}=missing") - continue - status, exit_code = self._installer_status(container) - if status == "exited" and exit_code == 0: - continue - if status == "exited": - failed.append(f"{service}=exit-{exit_code}") - else: - pending.append(f"{service}={status}") - if failed: - raise GateError("Notary state installer failed: " + ", ".join(failed)) - if not pending: - return - last_pending = pending - self.runner.sleep(2.0) - detail = ", ".join(last_pending) if last_pending else "unknown status" - raise GateError(f"stack did not become healthy within the deadline: {detail}") - - def doctor_all(self) -> None: - for _authority, service in AUTHORITIES: - self._compose( - "run", - "--rm", - "--no-deps", - service, - "--config", - "/etc/registry-notary/notary.yaml", - "state", - "doctor", - ) - print(f"notary-state: {service} doctor passed") - - def validate_runtime_mount(self, container_id: str) -> None: - mounts_output = self._run( - ["docker", "inspect", "--format", "{{json .Mounts}}", container_id] - ) - labels_output = self._run( - [ - "docker", - "volume", - "inspect", - "--format", - "{{json .Labels}}", - self.expected_volume, - ] - ) - try: - mounts = json.loads(mounts_output) - labels = json.loads(labels_output) - except json.JSONDecodeError as error: - raise GateError("Docker returned invalid mount metadata") from error - validate_runtime_pgdata_mounts( - mounts, - expected_volume=self.expected_volume, - volume_labels=labels, - compose_project_name=self.compose_project_name, - ) - - def _postgres_query(self, database: str, sql: str) -> str: - return self._compose( - "exec", - "-T", - "postgres", - "sh", - "-eu", - "-c", - 'exec psql -X -q -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$1" -Atc "$2"', - "notary-state", - database, - sql, - ) - - @staticmethod - def _counts_sql() -> str: - branches = [ - f"SELECT '{table}'::text AS table_name, count(*)::bigint AS row_count " - f"FROM registry_notary_private.{table}" - for table in CORRECTNESS_TABLES - ] - return ( - "WITH correctness_counts AS (" - + " UNION ALL ".join(branches) - + ") SELECT table_name || '|' || row_count::text " - "FROM correctness_counts ORDER BY table_name" - ) - - def capture_snapshot(self) -> StateSnapshot: - container_id = self.service_container_id("postgres") - if not container_id: - raise GateError("PostgreSQL container is not running") - self.validate_runtime_mount(container_id) - control = self._postgres_query( - "solmara_lab", - "SELECT system_identifier FROM pg_control_system(); " - "SHOW server_version_num; SHOW data_directory", - ) - system_identifier, server_version_num, data_directory = parse_control_snapshot( - control - ) - rows: list[tuple[str, str, int]] = [] - query = self._counts_sql() - for authority, _service in AUTHORITIES: - parsed = parse_authority_counts( - authority, - self._postgres_query(f"solmara_notary_{authority}", query), - ) - rows.extend( - (authority, table, parsed[table]) for table in CORRECTNESS_TABLES - ) - return StateSnapshot( - container_id=container_id, - system_identifier=system_identifier, - server_version_num=server_version_num, - data_directory=data_directory, - volume_name=self.expected_volume, - counts=tuple(sorted(rows)), - ) - - def run_just(self, recipe: str) -> None: - output = self._run(["just", recipe]) - if output: - print(output, end="" if output.endswith("\n") else "\n") - - def run_gate(self) -> StateSnapshot: - self.resolve_compose() - self.wait_for_stack() - self.doctor_all() - before = self.capture_snapshot() - - self.run_just("down") - if self.service_container_id("postgres", include_stopped=True): - raise GateError("just down did not remove the PostgreSQL container") - self.run_just("up") - - self.wait_for_stack() - # No doctor or scenario request may move above this snapshot. Installer - # schema metadata and readiness probes are deliberately excluded from - # the nine correctness-state tables. - after = self.capture_snapshot() - compare_snapshots(before, after) - self.doctor_all() - - totals = after.totals() - rendered_totals = ", ".join( - f"{authority}={totals[authority]}" for authority, _service in AUTHORITIES - ) - print( - "notary-state: just down/up preserved PostgreSQL system_identifier " - f"{after.system_identifier} on {after.volume_name}" - ) - print(f"notary-state: preserved correctness rows: {rendered_totals}") - return after - - -def main() -> int: - try: - RestartPersistenceGate().run_gate() - except GateError as error: - print(f"notary-state: {error}", file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/postgres-entrypoint.sh b/scripts/postgres-entrypoint.sh deleted file mode 100755 index d29ccba..0000000 --- a/scripts/postgres-entrypoint.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env sh -set -eu - -ssl_src=/run/solmara-postgres-ssl -ssl_dst=/var/lib/postgresql/server-ssl - -mkdir -p "$ssl_dst" -cp "$ssl_src/server.crt" "$ssl_dst/server.crt" -cp "$ssl_src/server.key" "$ssl_dst/server.key" -chown postgres:postgres "$ssl_dst/server.crt" "$ssl_dst/server.key" -chmod 0644 "$ssl_dst/server.crt" -chmod 0600 "$ssl_dst/server.key" - -ready_file=/tmp/solmara-postgres-provisioned -rm -f "$ready_file" - -/usr/local/bin/docker-entrypoint.sh "$@" & -postgres_pid=$! - -stop_postgres() { - kill -TERM "$postgres_pid" 2>/dev/null || true -} -trap stop_postgres INT TERM - -until pg_isready --quiet --host 127.0.0.1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB"; do - if ! kill -0 "$postgres_pid" 2>/dev/null; then - wait "$postgres_pid" - exit $? - fi - sleep 1 -done - -if ! /usr/local/bin/solmara-provision-postgresql.sh; then - stop_postgres - wait "$postgres_pid" || true - exit 1 -fi - -touch "$ready_file" -wait "$postgres_pid" diff --git a/scripts/prepare-authority-runtime.sh b/scripts/prepare-authority-runtime.sh new file mode 100755 index 0000000..3c69761 --- /dev/null +++ b/scripts/prepare-authority-runtime.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +relayctl_image=${REGISTRY_RELAYCTL_IMAGE:-solmara-lab-relayctl:source} + +cd "$root/generator" +uv run python -c 'from pathlib import Path; from solmara_lab.publisher import publish_relay_sources; publish_relay_sources(Path(".."))' + +cd "$root" +uv run scripts/publish-runtime-extracts.py + +for authority in cra nia mosd sipf nagdi; do + destination="$root/relays/$authority/package" + temporary_root=$(mktemp -d "$root/relays/$authority/.package.XXXXXX") + temporary="$temporary_root/package" + cleanup() { + rm -rf "$temporary_root" + } + trap cleanup EXIT HUP INT TERM + docker run --rm \ + --user "$(id -u):$(id -g)" \ + --volume "$root:/workspace" \ + --volume "$root/output/sqlite/relay/$authority.sqlite:/var/lib/relay/source/$authority.sqlite:ro" \ + --workdir /workspace \ + "$relayctl_image" \ + --json package "relays/$authority" --output "${temporary#"$root/"}" >/dev/null + rm -rf "$destination" + mv "$temporary" "$destination" + rmdir "$temporary_root" + trap - EXIT HUP INT TERM +done diff --git a/scripts/project-runtime-secrets.py b/scripts/project-runtime-secrets.py new file mode 100644 index 0000000..2202faf --- /dev/null +++ b/scripts/project-runtime-secrets.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Project runtime secrets while excluding every provider signing key.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PRIVATE = ROOT / "config/evidence/local/cells" +OUTPUT = ROOT / "runtime/evidence-cells/secrets" +CELL_CLIENTS = { + "cra": ("cra-pension-evidence", "cra-citizen-evidence"), + "nia": (), "sro": (), + "mosd-programme": ("mosd-child-benefit-evidence",), + "sipf": ("sipf-pension-evidence", "sipf-survivor-evidence"), + "nagdi": ("nagdi-voucher-evidence", "nagdi-livestock-evidence"), +} + + +def project(private: Path = PRIVATE, output: Path = OUTPUT) -> None: + output.mkdir(parents=True, exist_ok=True) + for cell, clients in CELL_CLIENTS.items(): + destination = output / cell + destination.mkdir(mode=0o700) + client_files = tuple(f"{client}-{suffix}" for client in clients for suffix in ("client-id", "client-key")) + for name in ("audit-hmac-key", "subject-binding-hmac-key", *client_files): + shutil.copyfile(private / cell / "secrets" / name, destination / name) + (destination / name).chmod(0o600) + mint = output / "mint" + mint.mkdir(mode=0o700) + shutil.copyfile(private / "mint/secrets/audit-hmac-key", mint / "audit-hmac-key") + (mint / "audit-hmac-key").chmod(0o600) + + +if __name__ == "__main__": + project() diff --git a/scripts/provision-hosted-runtime.py b/scripts/provision-hosted-runtime.py new file mode 100644 index 0000000..45f09a8 --- /dev/null +++ b/scripts/provision-hosted-runtime.py @@ -0,0 +1,800 @@ +#!/usr/bin/env python3 +"""Materialize one authority-owned hosted runtime from immutable public assets.""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import importlib +import importlib.util +import json +import os +import shutil +import sqlite3 +import stat +import sys +import tempfile +from datetime import UTC, datetime +from pathlib import Path +import yaml +from cryptography.hazmat.primitives.asymmetric import ec, rsa + +GENERIC_ERROR = "hosted target provisioning failed" +SUCCESS = "hosted target ready" +MAX_SECRET_BYTES = 16 * 1024 +RELAYS = ("cra", "nia", "mosd", "sipf", "nagdi") +CELLS = ("cra", "nia", "sro", "mosd-programme", "sipf", "nagdi") +DIRECT = { + "cra": ("cra-birth-extract", "cra-birth"), + "nia": ("nia-population-extract", "nia-population"), + "sro": ("sro-poverty-extract", "sro-poverty"), +} +EXPECTED_BIND_HOST = { + "mint": "172.29.1.20", + "cra": "172.29.1.21", + "nia": "172.29.1.22", + "sro": "172.29.1.23", + "mosd-programme": "172.29.1.24", + "sipf": "172.29.1.25", + "nagdi": "172.29.1.26", +} +CELL_CLIENTS = { + "cra": ("cra-pension-evidence", "cra-citizen-evidence"), + "nia": (), + "sro": (), + "mosd-programme": ("mosd-child-benefit-evidence",), + "sipf": ("sipf-pension-evidence", "sipf-survivor-evidence"), + "nagdi": ("nagdi-voucher-evidence", "nagdi-livestock-evidence"), +} +MINT_CLIENTS = { + "cra-pension-evidence": ( + "solmara:relay:cra:death-by-uin", + "https://id.registrystack.org/solmara/purpose/pension-payment-review", + ), + "cra-citizen-evidence": ( + "solmara:relay:cra:citizen-link-by-uin", + "https://id.registrystack.org/solmara/purpose/citizen-self-service", + ), + "mosd-child-benefit-evidence": ( + "solmara:relay:mosd:by-uin", + "https://id.registrystack.org/solmara/purpose/child-benefit-review", + ), + "sipf-pension-evidence": ( + "solmara:relay:sipf:by-pensioner-uin", + "https://id.registrystack.org/solmara/purpose/pension-payment-review", + ), + "sipf-survivor-evidence": ( + "solmara:relay:sipf:by-spouse-uin", + "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination", + ), + "nagdi-voucher-evidence": ( + "solmara:relay:nagdi:voucher-by-farmer-id", + "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review", + ), + "nagdi-livestock-evidence": ( + "solmara:relay:nagdi:movement-by-farmer-id", + "https://id.registrystack.org/solmara/purpose/livestock-movement-control", + ), + "nia-esignet": ( + "solmara:relay:nia:esignet-userinfo", + "https://id.registrystack.org/solmara/purpose/esignet-identity-verification", + ), +} + + +class ProvisionError(RuntimeError): + """A value-free provisioning refusal.""" + + +class QuietParser(argparse.ArgumentParser): + def error(self, message: str) -> None: + del message + raise ProvisionError("invalid arguments") + + +def _digest(path: Path) -> str: + value = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + value.update(block) + return value.hexdigest() + + +def verify_assets(root: Path) -> None: + try: + module_path = Path(__file__).with_name("hosted-runtime-assets.py") + spec = importlib.util.spec_from_file_location( + "hosted_runtime_assets", module_path + ) + if spec is None or spec.loader is None: + raise ProvisionError("invalid assets") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + module.verify_manifest(root) + except Exception: # noqa: BLE001 - public boundary is deliberately value-free. + raise ProvisionError("invalid assets") from None + + +def _read_secret(root: Path, name: str) -> bytes: + path = root / name + try: + if path.parent.resolve() != root.resolve() or path.is_symlink(): + raise ProvisionError("invalid secret") + metadata = path.stat() + if ( + not stat.S_ISREG(metadata.st_mode) + or metadata.st_mode & 0o022 + or metadata.st_size < 1 + or metadata.st_size > MAX_SECRET_BYTES + ): + raise ProvisionError("invalid secret") + value = path.read_bytes() + if len(value) != metadata.st_size: + raise ProvisionError("invalid secret") + return value.rstrip(b"\n") + except ProvisionError: + raise + except OSError: + raise ProvisionError("invalid secret") from None + + +def _hmac_secret(root: Path, name: str) -> bytes: + value = _read_secret(root, name) + if ( + len(value) < 32 + or len(value) > 128 + or any(byte < 0x21 or byte > 0x7E for byte in value) + ): + raise ProvisionError("invalid secret") + return value + + +def _b64decode(value: str) -> bytes: + return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) + + +def _public_jwk(data: bytes, *, allow_rsa: bool = True) -> dict[str, str]: + try: + value = json.loads(data.decode("utf-8")) + if not isinstance(value, dict) or "d" in value: + raise ProvisionError("invalid public key") + if value.get("kty") == "EC": + if ( + set(value) != {"kty", "crv", "alg", "x", "y", "kid"} + or value.get("crv") != "P-256" + or value.get("alg") != "ES256" + ): + raise ProvisionError("invalid public key") + x, y = ( + int.from_bytes(_b64decode(value["x"]), "big"), + int.from_bytes(_b64decode(value["y"]), "big"), + ) + ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key() + thumb = {key: value[key] for key in ("crv", "kty", "x", "y")} + elif allow_rsa and value.get("kty") == "RSA": + if ( + set(value) != {"kty", "alg", "n", "e", "kid"} + or value.get("alg") != "RS256" + ): + raise ProvisionError("invalid public key") + rsa.RSAPublicNumbers( + int.from_bytes(_b64decode(value["e"]), "big"), + int.from_bytes(_b64decode(value["n"]), "big"), + ).public_key() + thumb = {key: value[key] for key in ("e", "kty", "n")} + else: + raise ProvisionError("invalid public key") + kid = ( + base64.urlsafe_b64encode( + hashlib.sha256( + json.dumps(thumb, separators=(",", ":"), sort_keys=True).encode() + ).digest() + ) + .rstrip(b"=") + .decode() + ) + if value["kid"] != kid: + raise ProvisionError("invalid public key") + return value + except ProvisionError: + raise + except (UnicodeError, json.JSONDecodeError, KeyError, TypeError, ValueError): + raise ProvisionError("invalid public key") from None + + +def _private_client_jwk(data: bytes) -> None: + try: + value = json.loads(data.decode("utf-8")) + if ( + not isinstance(value, dict) + or not isinstance(value.get("d"), str) + or not isinstance(value.get("kid"), str) + ): + raise ProvisionError("invalid client key") + if value.get("kty") == "EC": + required = {"kty", "crv", "alg", "x", "y", "d", "kid"} + if ( + set(value) != required + or value.get("crv") != "P-256" + or value.get("alg") != "ES256" + ): + raise ProvisionError("invalid client key") + scalar = int.from_bytes(_b64decode(value["d"]), "big") + public = ( + ec.derive_private_key(scalar, ec.SECP256R1()) + .public_key() + .public_numbers() + ) + if ( + int.from_bytes(_b64decode(value["x"]), "big") != public.x + or int.from_bytes(_b64decode(value["y"]), "big") != public.y + ): + raise ProvisionError("invalid client key") + public_value = { + key: value[key] for key in ("kty", "crv", "alg", "x", "y", "kid") + } + elif value.get("kty") == "RSA": + required = {"kty", "alg", "n", "e", "d", "p", "q", "dp", "dq", "qi", "kid"} + if set(value) != required or value.get("alg") != "RS256": + raise ProvisionError("invalid client key") + numbers = { + name: int.from_bytes(_b64decode(value[name]), "big") + for name in ("n", "e", "d", "p", "q", "dp", "dq", "qi") + } + rsa.RSAPrivateNumbers( + p=numbers["p"], + q=numbers["q"], + d=numbers["d"], + dmp1=numbers["dp"], + dmq1=numbers["dq"], + iqmp=numbers["qi"], + public_numbers=rsa.RSAPublicNumbers(numbers["e"], numbers["n"]), + ).private_key() + public_value = {key: value[key] for key in ("kty", "alg", "n", "e", "kid")} + else: + raise ProvisionError("invalid client key") + _public_jwk(json.dumps(public_value).encode()) + except ProvisionError: + raise + except (UnicodeError, json.JSONDecodeError, KeyError, TypeError, ValueError): + raise ProvisionError("invalid client key") from None + + +def _tree_digest(root: Path) -> dict[str, tuple[str, int]]: + if not root.exists(): + return {} + result: dict[str, tuple[str, int]] = {} + for path in sorted(root.rglob("*")): + if path.is_symlink(): + raise ProvisionError("invalid existing output") + if path.is_file(): + result[path.relative_to(root).as_posix()] = ( + _digest(path), + stat.S_IMODE(path.stat().st_mode), + ) + return result + + +def _install_tree( + staged: Path, + destination: Path, + *, + root_mode: int, + owner: tuple[int, int] | None = None, +) -> None: + expected = _tree_digest(staged) + current = _tree_digest(destination) + if current: + if any( + relative not in expected or expected[relative] != value + for relative, value in current.items() + ): + raise ProvisionError("existing output mismatch") + destination.mkdir(parents=True, exist_ok=True) + for source in sorted(staged.rglob("*")): + relative = source.relative_to(staged) + target = destination / relative + if source.is_dir(): + target.mkdir(parents=True, exist_ok=True) + target.chmod(0o755) + else: + target.parent.mkdir(parents=True, exist_ok=True) + target.parent.chmod(0o755) + if target.exists(): + continue + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + descriptor = os.open(target, flags, stat.S_IMODE(source.stat().st_mode)) + with os.fdopen(descriptor, "wb") as output: + output.write(source.read_bytes()) + target.chmod(stat.S_IMODE(source.stat().st_mode)) + for source in sorted( + (path for path in staged.rglob("*") if path.is_dir()), + key=lambda path: len(path.parts), + reverse=True, + ): + (destination / source.relative_to(staged)).chmod( + stat.S_IMODE(source.stat().st_mode) + ) + destination.chmod(root_mode) + if owner is not None: + uid, gid = owner + for target in sorted(destination.rglob("*"), reverse=True): + os.chown(target, uid, gid, follow_symlinks=False) + os.chown(destination, uid, gid, follow_symlinks=False) + if _tree_digest(destination) != expected: + raise ProvisionError("output verification failed") + + +def _copy_tree(source: Path, destination: Path) -> None: + shutil.copytree(source, destination) + + +def _freeze_tree(destination: Path) -> None: + for path in sorted(destination.rglob("*"), reverse=True): + path.chmod(0o555 if path.is_dir() else 0o444) + destination.chmod(0o555) + + +def _write(path: Path, value: bytes, mode: int) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(value) + path.chmod(mode) + + +def _patch_runtime(path: Path, bind_host: str, extract_name: str | None = None) -> None: + config = yaml.safe_load(path.read_text(encoding="utf-8")) + config["listener"]["bindHost"] = bind_host + if extract_name is not None: + profile = next(iter(config["sourceExtracts"])) + old_path = Path(config["sourceExtracts"][profile]["path"]) + config["sourceExtracts"][profile]["path"] = str( + old_path.with_name(extract_name) + ) + path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") + + +def _stage_relay(assets: Path, authority: str, runtime: Path, source: Path) -> None: + relay = assets / "relays" / authority + _write(runtime / "runtime.yaml", (relay / "runtime.yaml").read_bytes(), 0o444) + _copy_tree(relay / "package", runtime / "package") + _write( + source / f"{authority}.sqlite", + (relay / "source" / f"{authority}.sqlite").read_bytes(), + 0o644, + ) + _freeze_tree(runtime) + + +def _load_publisher(assets: Path): + sys.path.insert(0, str(assets / "generator")) + return importlib.import_module("solmara_lab.publisher") + + +def _stage_extract( + assets: Path, cell: str, destination: Path, published_at: str, observed_at: str +) -> str: + publisher = _load_publisher(assets) + extract_id = publisher.timestamped_extract_id(cell, published_at) + with tempfile.TemporaryDirectory(prefix="solmara-extract-") as temporary: + generated = publisher.publish_extract( + Path(temporary), cell, published_at, extract_id + ) + publisher.validate_extract( + generated, + cell, + observed_at=observed_at, + expected_extract_id=extract_id, + expected_published_at=published_at, + ) + target = destination / generated.name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(generated, target) + target.chmod(0o444) + return f"{extract_id}.sqlite" + + +def _publication_time( + assets: Path, + cell: str, + runtime_output: Path, + extract_output: Path, + observed_at: str, + *, + require_fresh: bool = True, +) -> str: + runtime_file = runtime_output / "runtime.yaml" + if not runtime_file.exists(): + existing = sorted(extract_output.glob("*.sqlite")) + if not existing: + return observed_at + if len(existing) != 1: + raise ProvisionError("invalid existing extract") + try: + publisher = _load_publisher(assets) + metadata = publisher.validate_extract( + existing[0], cell, observed_at=observed_at + ) + if existing[0].name != f"{metadata.extract_id}.sqlite": + raise ProvisionError("invalid existing extract") + return metadata.published_at + except ProvisionError: + raise + except (OSError, ValueError, TypeError, AttributeError): + raise ProvisionError("invalid existing extract") from None + try: + config = yaml.safe_load(runtime_file.read_text(encoding="utf-8")) + profile = next(iter(config["sourceExtracts"])) + extract_name = Path(config["sourceExtracts"][profile]["path"]).name + existing = extract_output / extract_name + with sqlite3.connect(f"file:{existing}?mode=ro", uri=True) as connection: + rows = connection.execute( + "SELECT published_at, extract_id FROM evidence_extract" + ).fetchall() + if len(rows) != 1: + raise ProvisionError("invalid existing extract") + published_at, extract_id = rows[0] + publisher = _load_publisher(assets) + publisher.validate_extract( + existing, + cell, + observed_at=observed_at if require_fresh else published_at, + expected_extract_id=extract_id, + expected_published_at=published_at, + ) + if extract_name != f"{extract_id}.sqlite": + raise ProvisionError("invalid existing extract") + return published_at + except ProvisionError: + raise + except (OSError, sqlite3.Error, TypeError, KeyError, StopIteration, yaml.YAMLError): + raise ProvisionError("invalid existing extract") from None + + +def _append_file(staged: Path, destination: Path) -> None: + destination.mkdir(parents=True, exist_ok=True) + destination.chmod(0o755) + try: + target = destination / staged.name + if target.exists(): + if target.is_symlink() or _digest(target) != _digest(staged): + raise ProvisionError("existing output mismatch") + return + descriptor = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o444) + with os.fdopen(descriptor, "wb") as output: + output.write(staged.read_bytes()) + output.flush() + os.fsync(output.fileno()) + target.chmod(0o444) + finally: + destination.chmod(0o555) + + +def _replace_extract_binding( + runtime_output: Path, + cell: str, + previous_name: str, + replacement_name: str, +) -> None: + runtime_file = runtime_output / "runtime.yaml" + try: + if runtime_file.is_symlink() or not runtime_file.is_file(): + raise ProvisionError("invalid existing runtime") + config = yaml.safe_load(runtime_file.read_text(encoding="utf-8")) + if config["listener"]["bindHost"] != EXPECTED_BIND_HOST[cell]: + raise ProvisionError("invalid existing runtime") + if set(config["sourceExtracts"]) != {DIRECT[cell][0]}: + raise ProvisionError("invalid existing runtime") + binding = config["sourceExtracts"][DIRECT[cell][0]] + current = Path(binding["path"]) + if current.name != previous_name or current.parent != Path( + f"/var/lib/registry-evidence/{cell}/extracts" + ): + raise ProvisionError("invalid existing runtime") + binding["path"] = str(current.with_name(replacement_name)) + rendered = yaml.safe_dump(config, sort_keys=False).encode() + runtime_output.chmod(0o755) + descriptor, temporary_name = tempfile.mkstemp( + prefix=".runtime-", suffix=".yaml", dir=runtime_output + ) + temporary = Path(temporary_name) + try: + with os.fdopen(descriptor, "wb") as output: + output.write(rendered) + output.flush() + os.fsync(output.fileno()) + temporary.chmod(0o444) + os.replace(temporary, runtime_file) + finally: + temporary.unlink(missing_ok=True) + except ProvisionError: + raise + except (OSError, TypeError, KeyError, yaml.YAMLError): + raise ProvisionError("invalid existing runtime") from None + finally: + try: + runtime_output.chmod(0o555) + except OSError: + pass + + +def publish_extract(args: argparse.Namespace) -> None: + assets = args.assets.resolve() + verify_assets(assets) + if not args.target.endswith("-evidence"): + raise ProvisionError("invalid target") + cell = args.target.removesuffix("-evidence") + if cell not in DIRECT: + raise ProvisionError("invalid target") + runtime_output = args.runtime_output.resolve() + extract_output = args.extract_output.resolve() + now = datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z") + previous_publication = _publication_time( + assets, + cell, + runtime_output, + extract_output, + now, + require_fresh=False, + ) + previous_name = f"{_load_publisher(assets).timestamped_extract_id(cell, previous_publication)}.sqlite" + with tempfile.TemporaryDirectory( + prefix="solmara-extract-publication-" + ) as temporary: + staging = Path(temporary) + replacement_name = _stage_extract(assets, cell, staging, now, now) + if replacement_name == previous_name: + raise ProvisionError("extract publication is not newer") + _append_file(staging / replacement_name, extract_output) + _replace_extract_binding(runtime_output, cell, previous_name, replacement_name) + + +def _stage_evidence( + assets: Path, + cell: str, + secrets: Path, + runtime: Path, + secret_output: Path, + extract_output: Path | None, + bind_host: str, + published_at: str, + observed_at: str, +) -> None: + source = assets / "evidence" / "cells" / cell + _copy_tree(source, runtime) + public = _public_jwk(_read_secret(secrets, "signing-public.jwk"), allow_rsa=False) + bundle_config = runtime / "bundle" / "evidence.yaml" + config = yaml.safe_load(bundle_config.read_text(encoding="utf-8")) + config["signing"]["activePublicJwkFile"] = f"public-keys/{public['kid']}.jwk.json" + bundle_config.chmod(0o644) + bundle_config.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") + bundle_config.chmod(0o444) + _write( + runtime / "bundle" / "public-keys" / f"{public['kid']}.jwk.json", + json.dumps(public, indent=2, sort_keys=True).encode(), + 0o444, + ) + extract_name = ( + _stage_extract(assets, cell, extract_output, published_at, observed_at) + if extract_output is not None + else None + ) + runtime_file = runtime / "runtime.yaml" + runtime_file.chmod(0o644) + _patch_runtime(runtime_file, bind_host, extract_name) + runtime_file.chmod(0o444) + _write( + secret_output / "audit-hmac-key", _hmac_secret(secrets, "audit-hmac-key"), 0o400 + ) + _write( + secret_output / "subject-binding-hmac-key", + _hmac_secret(secrets, "subject-binding-hmac-key"), + 0o400, + ) + for client in CELL_CLIENTS[cell]: + value = _read_secret(secrets, f"{client}-client-key") + _private_client_jwk(value) + _write(secret_output / f"{client}-client-key", value, 0o400) + _write(secret_output / f"{client}-client-id", client.encode(), 0o400) + _freeze_tree(runtime) + for directory in [secret_output, *secret_output.rglob("*")]: + if directory.is_dir(): + directory.chmod(0o700) + + +def _stage_mint( + assets: Path, secrets: Path, runtime: Path, secret_output: Path, bind_host: str +) -> None: + public = _public_jwk(_read_secret(secrets, "signing-public.jwk"), allow_rsa=False) + config = yaml.safe_load((assets / "mint" / "mint.yaml").read_text(encoding="utf-8")) + config["listener"]["address"] = bind_host + config["signing"]["activePublicJwkFile"] = f"public-keys/{public['kid']}.jwk.json" + _write( + runtime / "mint.yaml", yaml.safe_dump(config, sort_keys=False).encode(), 0o444 + ) + _write( + runtime / "public-keys" / f"{public['kid']}.jwk.json", + json.dumps(public, indent=2, sort_keys=True).encode(), + 0o444, + ) + for client, (scope, purpose) in MINT_CLIENTS.items(): + key = _public_jwk(_read_secret(secrets, f"{client}-public.jwk")) + registration = { + "clientId": client, + "principal": f"https://id.registrystack.org/solmara/client/{client}", + "authorization": {"scopes": [scope], "claims": {"purpose": purpose}}, + "keys": [key], + } + _write( + runtime / "clients" / f"{client}.yaml", + yaml.safe_dump(registration, sort_keys=False).encode(), + 0o444, + ) + demo = _public_jwk( + _read_secret(secrets, "solmara-demo-client-public.jwk"), allow_rsa=False + ) + registration = { + "clientId": "solmara-demo", + "principal": "https://id.registrystack.org/solmara/client/solmara-demo", + "evidenceAudience": "https://id.registrystack.org/solmara/audience/demo-client", + "requesterTags": ["solmara-demo"], + "keys": [demo], + } + _write( + runtime / "clients" / "solmara-demo.yaml", + yaml.safe_dump(registration, sort_keys=False).encode(), + 0o444, + ) + _write( + secret_output / "audit-hmac-key", _hmac_secret(secrets, "audit-hmac-key"), 0o400 + ) + _freeze_tree(runtime) + for directory in [secret_output, *secret_output.rglob("*")]: + if directory.is_dir(): + directory.chmod(0o700) + + +def provision(args: argparse.Namespace) -> None: + assets, target = args.assets.resolve(), args.target + verify_assets(assets) + now = datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z") + with tempfile.TemporaryDirectory(prefix="solmara-provision-") as temporary: + root = Path(temporary) + runtime = root / "runtime" + source = root / "source" + secret_output = root / "secrets" + extracts = root / "extracts" + if target.endswith("-relay"): + authority = target.removesuffix("-relay") + if ( + authority not in RELAYS + or args.source_output is None + or args.secret_output + or args.extract_output + or args.bind_host + ): + raise ProvisionError("invalid target") + _stage_relay(assets, authority, runtime, source) + _install_tree(source, args.source_output.resolve(), root_mode=0o755) + elif target == "mint": + if ( + args.secret_output is None + or not args.bind_host + or args.source_output + or args.extract_output + ): + raise ProvisionError("invalid target") + _stage_mint( + assets, args.secrets.resolve(), runtime, secret_output, args.bind_host + ) + if args.bind_host != EXPECTED_BIND_HOST["mint"]: + raise ProvisionError("invalid bind host") + _install_tree( + secret_output, + args.secret_output.resolve(), + root_mode=0o700, + owner=(65532, 65532), + ) + elif target.endswith("-evidence"): + cell = target.removesuffix("-evidence") + if ( + cell not in CELLS + or args.secret_output is None + or not args.bind_host + or args.source_output + ): + raise ProvisionError("invalid target") + if (cell in DIRECT) != (args.extract_output is not None): + raise ProvisionError("invalid target") + if args.bind_host != EXPECTED_BIND_HOST[cell]: + raise ProvisionError("invalid bind host") + published_at = ( + _publication_time( + assets, + cell, + args.runtime_output.resolve(), + args.extract_output.resolve(), + now, + ) + if cell in DIRECT + else now + ) + _stage_evidence( + assets, + cell, + args.secrets.resolve(), + runtime, + secret_output, + extracts if cell in DIRECT else None, + args.bind_host, + published_at, + now, + ) + _install_tree( + secret_output, + args.secret_output.resolve(), + root_mode=0o700, + owner=(65532, 65532), + ) + if cell in DIRECT: + _install_tree(extracts, args.extract_output.resolve(), root_mode=0o555) + else: + raise ProvisionError("invalid target") + _install_tree(runtime, args.runtime_output.resolve(), root_mode=0o555) + + +def init_audit(destinations: list[Path], uid: int, gid: int) -> None: + if not destinations or uid != 65532 or gid != 65532: + raise ProvisionError("invalid audit target") + for destination in destinations: + metadata = destination.lstat() + if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode): + raise ProvisionError("invalid audit target") + for destination in destinations: + os.chown(destination, uid, gid) + os.chmod(destination, 0o700) + + +def parser() -> argparse.ArgumentParser: + result = QuietParser(add_help=False) + sub = result.add_subparsers(dest="command", required=True) + ready = sub.add_parser("provision", add_help=False) + ready.add_argument("--target", required=True) + ready.add_argument("--assets", required=True, type=Path) + ready.add_argument("--secrets", required=True, type=Path) + ready.add_argument("--runtime-output", required=True, type=Path) + ready.add_argument("--source-output", type=Path) + ready.add_argument("--secret-output", type=Path) + ready.add_argument("--extract-output", type=Path) + ready.add_argument("--bind-host") + publication = sub.add_parser("publish-extract", add_help=False) + publication.add_argument("--target", required=True) + publication.add_argument("--assets", required=True, type=Path) + publication.add_argument("--runtime-output", required=True, type=Path) + publication.add_argument("--extract-output", required=True, type=Path) + audit = sub.add_parser("init-audit", add_help=False) + audit.add_argument("--destination", action="append", required=True, type=Path) + audit.add_argument("--uid", required=True, type=int) + audit.add_argument("--gid", required=True, type=int) + return result + + +def main(argv: list[str] | None = None) -> int: + os.umask(0o077) + try: + args = parser().parse_args(argv) + if args.command == "provision": + provision(args) + elif args.command == "publish-extract": + publish_extract(args) + else: + init_audit(args.destination, args.uid, args.gid) + except (ProvisionError, OSError, ValueError, TypeError, KeyError, yaml.YAMLError): + print(GENERIC_ERROR, file=sys.stderr) + return 1 + print(SUCCESS) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/publish-metadata.py b/scripts/publish-metadata.py index af8a3af..1d7217b 100755 --- a/scripts/publish-metadata.py +++ b/scripts/publish-metadata.py @@ -27,74 +27,188 @@ VOUCHER_REVIEW_PURPOSE = ( "https://id.registrystack.org/solmara/purpose/voucher-eligibility-review" ) -CHILD_BENEFIT_COLLECTION_MEDIA_TYPE = "application/json" -CHILD_BENEFIT_FEDERATOR_URL = ( - "https://child-benefit-federator.solmara.registrystack.org" -) - -DATASET_OFFERING_DEFAULTS = { +DATASET_DEFAULTS = { "cra-civil": { - "evidence_type": "birth-registration-evidence", - "entity": "civil_person", - "service": "child-benefit-review", - "endpoint": "https://cra-notary.solmara.registrystack.org/v1/evaluations", - "discovery": "https://cra-notary.solmara.registrystack.org/.well-known/evidence-service", - "purposes": [CHILD_BENEFIT_PURPOSE], "concepts": [ "https://publicschema.org/crvs/Birth", "https://publicschema.org/crvs/Death", ], }, "nia-population": { - "evidence_type": "population-status-evidence", - "entity": "person", - "service": "child-benefit-review", - "endpoint": "https://nia-notary.solmara.registrystack.org/v1/evaluations", - "discovery": "https://nia-notary.solmara.registrystack.org/.well-known/evidence-service", - "purposes": [CHILD_BENEFIT_PURPOSE], "concepts": ["https://publicschema.org/Person"], }, "sro-social": { - "evidence_type": "household-poverty-evidence", - "entity": "household", - "service": "child-benefit-review", - "endpoint": "https://sro-notary.solmara.registrystack.org/v1/evaluations", - "discovery": "https://sro-notary.solmara.registrystack.org/.well-known/evidence-service", - "purposes": [CHILD_BENEFIT_PURPOSE], "concepts": [ "https://publicschema.org/Household", "https://publicschema.org/SocioEconomicProfile", ], }, "mosd-programme": { - "evidence_type": "beneficiary-enrollment-evidence", - "entity": "enrollment", - "service": "child-benefit-review", - "endpoint": "https://programme-notary.solmara.registrystack.org/v1/evaluations", - "discovery": "https://programme-notary.solmara.registrystack.org/.well-known/evidence-service", - "purposes": [CHILD_BENEFIT_PURPOSE], "concepts": ["https://publicschema.org/sp/Enrollment"], }, "sipf-pensions": { - "evidence_type": "pension-case-evidence", - "entity": "pension_case", - "service": "pension-survivor-review", - "endpoint": "https://sipf-notary.solmara.registrystack.org/v1/evaluations", - "discovery": "https://sipf-notary.solmara.registrystack.org/.well-known/evidence-service", - "purposes": [PENSION_PAYMENT_PURPOSE, SURVIVOR_BENEFIT_PURPOSE], "concepts": ["https://id.registrystack.org/solmara/semantics/pension-case"], }, "nagdi-agriculture": { - "evidence_type": "farmer-voucher-evidence", - "entity": "farmer_voucher", - "service": "nagdi-voucher-review", - "endpoint": "https://nagdi-notary.solmara.registrystack.org/v1/evaluations", - "discovery": "https://nagdi-notary.solmara.registrystack.org/.well-known/evidence-service", - "purposes": [VOUCHER_REVIEW_PURPOSE], "concepts": ["https://publicschema.org/Farm"], }, } +AUTHORITY_EVIDENCE_URLS = { + "cra": "https://cra-evidence.solmara.registrystack.org", + "nia": "https://nia-evidence.solmara.registrystack.org", + "sro": "https://sro-evidence.solmara.registrystack.org", + "mosd-programme-mis": "https://mosd-programme-evidence.solmara.registrystack.org", + "sipf": "https://sipf-evidence.solmara.registrystack.org", + "nagdi": "https://nagdi-evidence.solmara.registrystack.org", +} + +EVIDENCE_OFFERING_SPECS = [ + { + "id": "cra-child-benefit-v1-offering", + "dataset": "cra-civil", + "entity": "civil_person", + "authority": "cra", + "evidence_type": "cra-child-benefit-v1", + "service": "child-benefit-review", + "purposes": [CHILD_BENEFIT_PURPOSE], + "lookup_keys": ["uin"], + "concepts": [ + "https://id.registrystack.org/solmara/concept/birth-is-registered", + "https://id.registrystack.org/solmara/concept/child-age-under-5", + ], + "source": "immutable extract", + }, + { + "id": "cra-pension-death-v1-offering", + "dataset": "cra-civil", + "entity": "civil_person", + "authority": "cra", + "evidence_type": "cra-death-status-v1", + "service": "pension-survivor-review", + "purposes": [PENSION_PAYMENT_PURPOSE], + "lookup_keys": ["uin"], + "concepts": ["https://id.registrystack.org/solmara/concept/person-is-deceased"], + "source": "Relay lookup", + }, + { + "id": "cra-citizen-record-v1-offering", + "dataset": "cra-civil", + "entity": "civil_person", + "authority": "cra", + "evidence_type": "civil-record-linked-v1", + "service": "citizen-self-service", + "purposes": ["https://id.registrystack.org/solmara/purpose/citizen-self-service"], + "lookup_keys": ["uin"], + "concepts": ["https://id.registrystack.org/solmara/concept/civil-record-linked"], + "source": "Relay lookup", + }, + { + "id": "nia-child-benefit-v1-offering", + "dataset": "nia-population", + "entity": "population_person", + "authority": "nia", + "evidence_type": "population-active-v1", + "service": "child-benefit-review", + "purposes": [CHILD_BENEFIT_PURPOSE], + "lookup_keys": ["uin"], + "concepts": ["https://id.registrystack.org/solmara/concept/population-record-active"], + "source": "immutable extract", + }, + { + "id": "nia-citizen-status-v1-offering", + "dataset": "nia-population", + "entity": "population_person", + "authority": "nia", + "evidence_type": "citizen-population-active-v1", + "service": "citizen-self-service", + "purposes": ["https://id.registrystack.org/solmara/purpose/citizen-self-service"], + "lookup_keys": ["uin"], + "concepts": ["https://id.registrystack.org/solmara/concept/citizen-population-record-active"], + "source": "immutable extract", + }, + { + "id": "sro-child-benefit-v1-offering", + "dataset": "sro-social", + "entity": "poverty_record", + "authority": "sro", + "evidence_type": "poverty-priority-v1", + "service": "child-benefit-review", + "purposes": [CHILD_BENEFIT_PURPOSE], + "lookup_keys": ["uin"], + "concepts": ["https://id.registrystack.org/solmara/concept/household-below-poverty-threshold"], + "source": "immutable extract", + }, + { + "id": "mosd-child-benefit-v1-offering", + "dataset": "mosd-programme", + "entity": "beneficiary_enrolment", + "authority": "mosd-programme-mis", + "evidence_type": "not-enrolled-v1", + "service": "child-benefit-review", + "purposes": [CHILD_BENEFIT_PURPOSE], + "lookup_keys": ["uin"], + "concepts": ["https://id.registrystack.org/solmara/concept/not-already-enrolled"], + "source": "Relay lookup", + }, + { + "id": "sipf-pension-payment-v1-offering", + "dataset": "sipf-pensions", + "entity": "pension_payment", + "authority": "sipf", + "evidence_type": "pension-payment-active-v1", + "service": "pension-survivor-review", + "purposes": [PENSION_PAYMENT_PURPOSE], + "lookup_keys": ["pensioner_uin"], + "concepts": ["https://id.registrystack.org/solmara/concept/pension-payment-active"], + "source": "Relay lookup", + }, + { + "id": "sipf-survivor-benefit-v1-offering", + "dataset": "sipf-pensions", + "entity": "survivor_case", + "authority": "sipf", + "evidence_type": "survivor-benefit-v1", + "service": "pension-survivor-review", + "purposes": [SURVIVOR_BENEFIT_PURPOSE], + "lookup_keys": ["spouse_uin"], + "concepts": ["https://id.registrystack.org/solmara/concept/survivor-is-eligible"], + "source": "Relay lookup", + }, + { + "id": "nagdi-voucher-v1-offering", + "dataset": "nagdi-agriculture", + "entity": "farmer_voucher", + "authority": "nagdi", + "evidence_type": "climate-smart-voucher-v1", + "service": "agriculture-review", + "purposes": [VOUCHER_REVIEW_PURPOSE], + "lookup_keys": ["farmer_id"], + "concepts": [ + "https://id.registrystack.org/solmara/concept/farmer-registered", + "https://id.registrystack.org/solmara/concept/data-use-authorized-for-purpose", + "https://id.registrystack.org/solmara/concept/eligible-for-climate-smart-input-voucher", + ], + "source": "Relay lookup", + }, + { + "id": "nagdi-livestock-v1-offering", + "dataset": "nagdi-agriculture", + "entity": "livestock_movement", + "authority": "nagdi", + "evidence_type": "livestock-movement-v1", + "service": "agriculture-review", + "purposes": ["https://id.registrystack.org/solmara/purpose/livestock-movement-control"], + "lookup_keys": ["farmer_id"], + "concepts": [ + "https://id.registrystack.org/solmara/concept/registered-herd", + "https://id.registrystack.org/solmara/concept/origin-district-not-quarantined-for-species", + "https://id.registrystack.org/solmara/concept/eligible-for-livestock-movement-permit", + ], + "source": "Relay lookup", + }, +] + GRAY_REGISTRIES = [ { "id": "land-cadastre", @@ -240,46 +354,11 @@ def build_bundle( normalize_dataset(dataset, fragment_index) for dataset in manifest.get("datasets", []) ] - offerings = [] - for dataset in datasets: - source_offerings = dataset.pop("source_evidence_offerings", []) - if source_offerings: - offerings.extend( - normalize_source_offering(dataset, offering, authorities) - for offering in source_offerings - ) - else: - offerings.append(synthetic_offering(dataset, authorities)) - - # NAgDI has two live evidence paths in wave 1; keep both visible to the visitor center. - if not any( - offering["id"] == "nagdi-agriculture-livestock-movement-offering" - for offering in offerings - ): - nagdi = next( - dataset for dataset in datasets if dataset["id"] == "nagdi-agriculture" - ) - livestock = synthetic_offering( - nagdi, - authorities, - evidence_type="livestock-movement-evidence", - entity="livestock_movement", - ) - livestock["id"] = "nagdi-agriculture-livestock-movement-offering" - livestock["title"] = "NAgDI livestock movement evidence offering" - livestock["description"] = ( - "Livestock movement-control predicates for permit checks." - ) - livestock["public_services"] = ["livestock-movement-control"] - livestock["purposes"] = [ - "https://id.registrystack.org/solmara/purpose/livestock-movement-control" - ] - livestock["semantics"]["concepts"] = ["https://publicschema.org/livestock-type"] - offerings.append(livestock) - - collection_offering_id = "solmara.child-benefit.authority-predicate-collection" - if not any(offering["id"] == collection_offering_id for offering in offerings): - offerings.append(child_benefit_collection_offering(authorities)) + datasets_by_id = {dataset["id"]: dataset for dataset in datasets} + offerings = [ + authority_evidence_offering(spec, datasets_by_id, authorities) + for spec in EVIDENCE_OFFERING_SPECS + ] policies = [policy_for_offering(offering) for offering in offerings] catalog = { @@ -310,7 +389,7 @@ def normalize_dataset( dataset: dict[str, Any], fragment_index: dict[str, dict[str, Any]] ) -> dict[str, Any]: fragment = fragment_index.get(dataset["id"], {}) - defaults = DATASET_OFFERING_DEFAULTS.get(dataset["id"], {}) + defaults = DATASET_DEFAULTS.get(dataset["id"], {}) entities = [ normalize_entity(item, fragment.get("purposes", []), defaults) for item in dataset.get("entities", []) @@ -326,7 +405,6 @@ def normalize_dataset( "application_profiles": fragment.get("application_profiles", []), "purposes": fragment.get("purposes", []), "entities": entities, - "source_evidence_offerings": dataset.get("evidence_offerings", []), } @@ -357,123 +435,41 @@ def normalize_entity( } -def normalize_source_offering( - dataset: dict[str, Any], offering: dict[str, Any], authorities: dict[str, Any] -) -> dict[str, Any]: - default = synthetic_offering( - dataset, - authorities, - evidence_type=offering.get("evidence_type"), - entity=offering.get("entity"), - ) - policy = offering.get("policy") if isinstance(offering.get("policy"), dict) else {} - default.update( - { - "id": offering["id"], - "iri": offering.get("iri", default["iri"]), - "title": text(offering.get("title"), default["title"]), - "description": text(offering.get("description"), default["description"]), - "lookup_keys": offering.get("lookup_keys", []), - "public_services": offering.get( - "procedure_contexts", default["public_services"] - ), - "access": offering.get("access", default["access"]), - "purposes": policy.get("purpose", dataset["purposes"]), - } - ) - default["policy"] = f"{default['id']}-policy" - return default - - -def synthetic_offering( - dataset: dict[str, Any], +def authority_evidence_offering( + spec: dict[str, Any], + datasets: dict[str, dict[str, Any]], authorities: dict[str, Any], - *, - evidence_type: str | None = None, - entity: str | None = None, ) -> dict[str, Any]: - defaults = DATASET_OFFERING_DEFAULTS.get(dataset["id"], {}) - evidence_type = evidence_type or defaults.get( - "evidence_type", f"{dataset['id']}-evidence" - ) - entity = entity or defaults.get("entity") or dataset["entities"][0]["name"] - authority = dataset.get("authority", {}) - authority_id = authority.get("id", dataset["id"]) - endpoint = defaults.get( - "endpoint", "https://metadata.solmara.registrystack.org/v1/evaluations" - ) - discovery = defaults.get( - "discovery", - "https://metadata.solmara.registrystack.org/.well-known/evidence-service", - ) - offering_id = f"{dataset['id']}-{evidence_type.replace('-evidence', '')}-offering" - purposes = defaults.get("purposes", dataset.get("purposes", [])) - return { - "id": offering_id, - "iri": f"https://id.registrystack.org/solmara/evidence-offerings/{slug(offering_id)}", - "title": f"{dataset['title']} evidence offering", - "description": f"Purpose-limited evidence predicates from {dataset['title']}.", - "dataset": dataset["id"], - "entity": entity, - "evidence_type": evidence_type, - "issuing_authority": authorities.get(authority_id, authority), - "lookup_keys": ["uin"] - if entity not in {"farmer_voucher", "livestock_movement"} - else ["farmer_id"], - "public_services": [defaults.get("service", "citizen-self-service")], - "access": { - "kind": "evidence-verification-api", - "conforms_to": "https://spec.openapis.org/oas/v3.1.0", - "endpoint_url": endpoint, - "discovery_url": discovery, - }, - "purposes": purposes, - "semantics": { - "concepts": defaults.get( - "concepts", - ["https://id.registrystack.org/solmara/semantics/registry-record"], - ), - "application_profiles": ["cpsv-ap"], - }, - "policy": f"{offering_id}-policy", - } - - -def child_benefit_collection_offering(authorities: dict[str, Any]) -> dict[str, Any]: - offering_id = "solmara.child-benefit.authority-predicate-collection" + dataset = datasets[spec["dataset"]] + authority = authorities[spec["authority"]] + base_url = AUTHORITY_EVIDENCE_URLS[spec["authority"]] return { - "id": offering_id, - "iri": "https://id.registrystack.org/solmara/evidence-offerings/child-benefit-authority-predicate-collection", - "title": "Child Benefit Authority Predicate Collection", + "id": spec["id"], + "iri": f"https://id.registrystack.org/solmara/evidence-offerings/{spec['id']}", + "title": f"{authority['name']} {humanize(spec['evidence_type'])}", "description": ( - "A transient collection of source-owned child benefit predicates. " - "It contains no copied source rows and no composed eligibility decision." + f"An independently signed {spec['source']} assertion issued by " + f"{authority['name']}." ), - "dataset": "mosd-programme", - "entity": "enrollment", - "evidence_type": "child-benefit-authority-predicate-collection-evidence", - "issuing_authority": authorities["mosd-programme-mis"], - "lookup_keys": ["uin"], - "public_services": ["child-benefit-review"], + "dataset": dataset["id"], + "entity": spec["entity"], + "evidence_type": spec["evidence_type"], + "issuing_authority": authority, + "lookup_keys": spec["lookup_keys"], + "public_services": [spec["service"]], "access": { - "kind": "authority-predicate-collection-api", - "conforms_to": "https://id.registrystack.org/solmara/contracts/authority-predicate-collection/v1", - "endpoint_url": f"{CHILD_BENEFIT_FEDERATOR_URL}/v1/evaluations", - "discovery_url": f"{CHILD_BENEFIT_FEDERATOR_URL}/v1/claims", - "media_type": CHILD_BENEFIT_COLLECTION_MEDIA_TYPE, - "ruleset": "source-owned-child-benefit-predicates-v1", + "kind": "evidence-verification-api", + "conforms_to": "https://id.registrystack.org/spec/registry-evidence/v1", + "endpoint_url": f"{base_url}/v1/evidence", + "discovery_url": f"{base_url}/v1/evidence-definitions", + "source_type": spec["source"], }, - "purposes": [CHILD_BENEFIT_PURPOSE], + "purposes": spec["purposes"], "semantics": { - "concepts": [ - "https://publicschema.org/Person", - "https://publicschema.org/crvs/Birth", - "https://publicschema.org/Household", - "https://publicschema.org/sp/Enrollment", - ], + "concepts": spec["concepts"], "application_profiles": ["cpsv-ap"], }, - "policy": "solmara-child-benefit-authority-predicate-collection-policy", + "policy": f"{spec['id']}-policy", } diff --git a/scripts/publish-runtime-extracts.py b/scripts/publish-runtime-extracts.py new file mode 100755 index 0000000..61cd9b4 --- /dev/null +++ b/scripts/publish-runtime-extracts.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Publish and bind fresh immutable extracts in the generated local runtime.""" + +from __future__ import annotations + +import argparse +import importlib +import json +import os +import re +import stat +import sys +import tempfile +from datetime import UTC, datetime +from pathlib import Path, PurePosixPath + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "generator")) +publisher = importlib.import_module("solmara_lab.publisher") + +AUTHORITIES = { + "cra": ("cra-birth-extract", "cra-birth"), + "nia": ("nia-population-extract", "nia-population"), + "sro": ("sro-poverty-extract", "sro-poverty"), +} +RUNTIME_DIRECTORY = Path("runtime/evidence-cells/cells") + + +class RuntimeExtractError(RuntimeError): + """Raised when runtime publication or binding cannot be completed safely.""" + + +def current_publication_time() -> str: + return publisher.canonical_published_at(datetime.now(UTC).isoformat()) + + +def _runtime_path(root: Path, authority: str) -> Path: + return root / RUNTIME_DIRECTORY / authority / "runtime.yaml" + + +def _load_binding(runtime_path: Path, authority: str) -> tuple[str, str]: + profile, prefix = AUTHORITIES[authority] + try: + document = yaml.safe_load(runtime_path.read_text(encoding="utf-8")) + source_extracts = document["sourceExtracts"] + if set(source_extracts) != {profile}: + raise RuntimeExtractError("generated runtime has an unexpected extract profile") + binding = source_extracts[profile] + if set(binding) != {"path"} or not isinstance(binding["path"], str): + raise RuntimeExtractError("generated runtime has an invalid extract binding") + except RuntimeExtractError: + raise + except (KeyError, OSError, TypeError, UnicodeError, yaml.YAMLError): + raise RuntimeExtractError("generated runtime cannot be validated") from None + + container_path = PurePosixPath(binding["path"]) + expected_parent = PurePosixPath( + f"/var/lib/registry-evidence/{authority}/extracts" + ) + if ( + container_path.parent != expected_parent + or not container_path.name.startswith(prefix + "-") + or not container_path.name.endswith(".sqlite") + ): + raise RuntimeExtractError("generated runtime has an invalid extract binding") + return binding["path"], container_path.stem + + +def _render_binding( + runtime_path: Path, old_container_path: str, new_container_path: str +) -> bytes: + original = runtime_path.read_text(encoding="utf-8") + pattern = re.compile(rf"(? None: + directory = path.parent + original_directory_mode = stat.S_IMODE(directory.stat().st_mode) + original_file_mode = stat.S_IMODE(path.stat().st_mode) + directory.chmod(original_directory_mode | stat.S_IWUSR) + temporary: Path | None = None + try: + descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{path.name}.", suffix=".tmp", dir=directory + ) + temporary = Path(temporary_name) + with os.fdopen(descriptor, "wb") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + temporary.chmod(original_file_mode) + os.replace(temporary, path) + temporary = None + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + directory.chmod(original_directory_mode) + + +def prepare_runtime_extracts( + root: Path = ROOT, published_at: str | None = None +) -> dict[str, dict[str, str]]: + """Select fresh extracts, publish missing replacements, and bind generated cells.""" + + root = root.resolve() + publication_time = publisher.canonical_published_at( + published_at if published_at is not None else current_publication_time() + ) + planned: dict[str, dict[str, object]] = {} + + # Validate every existing binding and every possible target before writing + # any file. A malformed or writable extract therefore leaves all bindings + # on their previously reviewed values. + for authority in AUTHORITIES: + runtime_path = _runtime_path(root, authority) + old_container_path, bound_extract_id = _load_binding(runtime_path, authority) + bound_path = publisher.extract_path(root, bound_extract_id) + selected_id = bound_extract_id + selected_path = bound_path + status = "reused" + if bound_path.exists() or bound_path.is_symlink(): + try: + publisher.validate_extract( + bound_path, + authority, + observed_at=publication_time, + expected_extract_id=bound_extract_id, + ) + except publisher.StaleExtractError: + status = "published" + except publisher.ExtractValidationError as error: + raise RuntimeExtractError( + f"{authority} bound extract failed validation: {error}" + ) from None + else: + status = "published" + + if status == "published": + selected_id = publisher.timestamped_extract_id( + authority, publication_time + ) + selected_path = publisher.extract_path(root, selected_id) + if selected_path.exists() or selected_path.is_symlink(): + try: + publisher.validate_extract( + selected_path, + authority, + observed_at=publication_time, + expected_extract_id=selected_id, + expected_published_at=publication_time, + ) + except publisher.ExtractValidationError as error: + raise RuntimeExtractError( + f"{authority} publication target failed validation: {error}" + ) from None + status = "recovered" + + new_container_path = str( + PurePosixPath(old_container_path).with_name(selected_path.name) + ) + planned[authority] = { + "runtime_path": runtime_path, + "old_container_path": old_container_path, + "new_container_path": new_container_path, + "selected_id": selected_id, + "selected_path": selected_path, + "status": status, + } + + rendered = { + authority: _render_binding( + plan["runtime_path"], + str(plan["old_container_path"]), + str(plan["new_container_path"]), + ) + for authority, plan in planned.items() + if plan["old_container_path"] != plan["new_container_path"] + } + + for authority, plan in planned.items(): + if plan["status"] != "published": + continue + selected_path = publisher.publish_extract( + root, authority, publication_time, str(plan["selected_id"]) + ) + publisher.validate_extract( + selected_path, + authority, + observed_at=publication_time, + expected_extract_id=str(plan["selected_id"]), + expected_published_at=publication_time, + ) + + for authority, content in rendered.items(): + _replace_read_only_file(Path(planned[authority]["runtime_path"]), content) + + return { + authority: { + "extractId": str(plan["selected_id"]), + "path": str(Path(plan["selected_path"]).relative_to(root)), + "status": str(plan["status"]), + } + for authority, plan in planned.items() + } + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Publish and bind fresh immutable Evidence extracts" + ) + parser.add_argument("--root", type=Path, default=ROOT) + parser.add_argument( + "--published-at", + help="explicit RFC 3339 publication time; defaults to the current UTC time", + ) + args = parser.parse_args() + result = prepare_runtime_extracts(args.root, args.published_at) + print(json.dumps(result, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/scripts/registry-projects.sh b/scripts/registry-projects.sh deleted file mode 100755 index a545da0..0000000 --- a/scripts/registry-projects.sh +++ /dev/null @@ -1,179 +0,0 @@ -#!/bin/sh -set -eu - -ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) -VERSION_FILE="$ROOT/versions.env" -REGISTRYCTL=${REGISTRYCTL_BIN:-} - -required_version=$(sed -n 's/^REGISTRYCTL_VERSION=//p' "$VERSION_FILE") -if [ -z "$required_version" ]; then - echo "versions.env must set REGISTRYCTL_VERSION" >&2 - exit 1 -fi - -if [ -z "$REGISTRYCTL" ]; then - REGISTRYCTL=$("$ROOT/scripts/registryctl-pinned.sh" path) -fi -actual_version=$("$REGISTRYCTL" --version 2>/dev/null || true) -if [ "$actual_version" != "registryctl $required_version" ]; then - echo "registryctl $required_version is required; got ${actual_version:-no executable}" >&2 - echo "set REGISTRYCTL_BIN to the matching release binary when it is not on PATH" >&2 - exit 1 -fi -for command in check test build; do - if ! "$REGISTRYCTL" "$command" --help >/dev/null 2>&1; then - echo "registryctl $required_version with project-authoring check/test/build/capabilities is required" >&2 - echo "set REGISTRYCTL_BIN to a compatible Registry Stack build" >&2 - exit 1 - fi -done -if ! "$REGISTRYCTL" tooling editor --help >/dev/null 2>&1; then - echo "registryctl $required_version with project-authoring editor setup is required" >&2 - echo "set REGISTRYCTL_BIN to a compatible Registry Stack build" >&2 - exit 1 -fi - -projects=" -cra-civil -nia-population -sro-social -mosd-programme -sipf-pensions -nagdi-agriculture -" - -build_project_output() { - project=$1 - environment=$2 - project_directory="$ROOT/projects/$project" - echo "registryctl build: $project ($environment)" >&2 - build_report=$( - "$REGISTRYCTL" build \ - --project-dir "$project_directory" \ - --environment "$environment" \ - --format json - ) - printf '%s\n' "$build_report" | - python3 "$ROOT/scripts/registryctl-build-output.py" \ - --project-dir "$project_directory" \ - --environment "$environment" -} - -build_projects() { - environment=$1 - for project in $projects; do - build_project_output "$project" "$environment" - done -} - -check_projects() { - detail=$1 - for project in $projects; do - for environment in local hosted; do - echo "registryctl check: $project ($environment)" - if [ "$detail" = "explain" ]; then - "$REGISTRYCTL" check \ - --project-dir "$ROOT/projects/$project" \ - --environment "$environment" \ - --explain - else - "$REGISTRYCTL" check \ - --project-dir "$ROOT/projects/$project" \ - --environment "$environment" - fi - done - done -} - -inspect_capabilities() { - for project in $projects; do - for environment in local hosted; do - echo "registryctl capabilities: $project ($environment)" - "$REGISTRYCTL" check \ - --project-dir "$ROOT/projects/$project" \ - --environment "$environment" \ - --explain - done - done -} - -sync_editor_support() { - for project in $projects; do - echo "registryctl authoring editor: $project" - "$REGISTRYCTL" tooling editor \ - --project-dir "$ROOT/projects/$project" - done -} - -stage_runtime() { - destination=$1 - for environment in local hosted; do - for project in $projects; do - build_root=$(build_project_output "$project" "$environment") - source="$build_root/private" - target="$destination/$environment/$project" - mkdir -p "$target/relay" "$target/relay-consultation" - cp -R "$source/relay-public/config/." "$target/relay/" - cp -R "$source/relay-consultation/config/." "$target/relay-consultation/" - done - done - chmod -R u=rwX,go=rX "$destination" -} - -action=${1:-} -case "$action" in - test) - for project in $projects; do - for environment in local hosted; do - echo "registryctl test: $project ($environment)" - "$REGISTRYCTL" test \ - --project-dir "$ROOT/projects/$project" \ - --environment "$environment" \ - --format json | - python3 "$ROOT/scripts/registryctl-test-output.py" - done - done - ;; - check) - check_projects concise - ;; - review) - check_projects explain - ;; - capabilities) - inspect_capabilities - ;; - editor) - sync_editor_support - ;; - build) - environment=${2:-} - case "$environment" in - local | hosted) ;; - *) - echo "usage: $0 build " >&2 - exit 2 - ;; - esac - build_projects "$environment" - ;; - sync-runtime) - temporary=$(mktemp -d "${TMPDIR:-/tmp}/solmara-registry-runtime.XXXXXX") - trap 'rm -rf "$temporary"' EXIT HUP INT TERM - stage_runtime "$temporary/registry-projects" - target="$ROOT/runtime/registry-projects" - rm -rf "$target" - mkdir -p "$(dirname "$target")" - mv "$temporary/registry-projects" "$target" - ;; - check-runtime) - temporary=$(mktemp -d "${TMPDIR:-/tmp}/solmara-registry-runtime.XXXXXX") - trap 'rm -rf "$temporary"' EXIT HUP INT TERM - stage_runtime "$temporary/registry-projects" - diff -ruN "$ROOT/runtime/registry-projects" "$temporary/registry-projects" - ;; - *) - echo "usage: $0 |sync-runtime|check-runtime>" >&2 - exit 2 - ;; -esac diff --git a/scripts/registryctl-build-output.py b/scripts/registryctl-build-output.py deleted file mode 100755 index 02da531..0000000 --- a/scripts/registryctl-build-output.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -"""Validate a registryctl build report and print its generated output root.""" - -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path -from typing import Any - - -REPORT_SCHEMA = "registryctl.reviewed_project_build_report.v1" -BUILD_SCHEMA = "registryctl.project_command.v1" -MAX_REPORT_BYTES = 8 * 1024 * 1024 - - -class BuildReportError(ValueError): - """A safe validation error for a registryctl build report.""" - - -def reject_duplicate_fields(pairs: list[tuple[str, Any]]) -> dict[str, Any]: - result: dict[str, Any] = {} - for key, value in pairs: - if key in result: - raise BuildReportError("the JSON report contains a duplicate field") - result[key] = value - return result - - -def parse_build_output( - raw: bytes, - *, - project_directory: Path, - environment: str, -) -> Path: - if not raw: - raise BuildReportError("registryctl returned an empty report") - if len(raw) > MAX_REPORT_BYTES: - raise BuildReportError("the JSON report exceeds the size limit") - try: - report = json.loads(raw, object_pairs_hook=reject_duplicate_fields) - except (json.JSONDecodeError, UnicodeDecodeError, RecursionError) as error: - raise BuildReportError("registryctl did not return strict UTF-8 JSON") from error - if not isinstance(report, dict): - raise BuildReportError("the JSON report is not an object") - if report.get("schema_version") != REPORT_SCHEMA: - raise BuildReportError("the JSON report has an unsupported schema") - build = report.get("build") - if not isinstance(build, dict) or build.get("schema_version") != BUILD_SCHEMA: - raise BuildReportError("the JSON report has no supported build result") - if build.get("status") != "built": - raise BuildReportError("registryctl did not report a completed build") - if build.get("environment") != environment: - raise BuildReportError("the JSON report has the wrong environment binding") - if not isinstance(build.get("project"), str) or not build["project"]: - raise BuildReportError("the JSON report has no project identity") - - output_value = build.get("output") - if not isinstance(output_value, str) or not output_value: - raise BuildReportError("the JSON report has no output root") - output = Path(output_value) - - try: - project_root = project_directory.resolve(strict=True) - candidate = output if output.is_absolute() else project_root / output - output_root = candidate.resolve(strict=True) - output_root.relative_to(project_root) - except (FileNotFoundError, RuntimeError, ValueError) as error: - raise BuildReportError( - "the JSON report output root is not a real project-owned directory" - ) from error - if not output_root.is_dir(): - raise BuildReportError("the JSON report output root is not a directory") - - relay_public = output_root / "private" / "relay-public" / "config" / "relay.yaml" - relay_consultation = ( - output_root / "private" / "relay-consultation" / "config" / "relay.yaml" - ) - if not relay_public.is_file() or not relay_consultation.is_file(): - raise BuildReportError( - "the generated Relay configuration closure is incomplete" - ) - for required in (relay_public, relay_consultation): - try: - required.resolve(strict=True).relative_to(output_root) - except (FileNotFoundError, RuntimeError, ValueError) as error: - raise BuildReportError( - "a generated configuration path escapes the build output root" - ) from error - - return output_root - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--project-dir", required=True, type=Path) - parser.add_argument("--environment", required=True) - args = parser.parse_args(argv) - raw = sys.stdin.buffer.read(MAX_REPORT_BYTES + 1) - try: - output = parse_build_output( - raw, - project_directory=args.project_dir, - environment=args.environment, - ) - except BuildReportError as error: - print(f"registryctl build report invalid: {error}", file=sys.stderr) - return 1 - print(output) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/registryctl-pinned.sh b/scripts/registryctl-pinned.sh deleted file mode 100755 index 39c661a..0000000 --- a/scripts/registryctl-pinned.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/sh -set -eu - -ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) -VERSION_FILE="$ROOT/versions.env" - -version=$(sed -n 's/^REGISTRYCTL_VERSION=//p' "$VERSION_FILE") -if [ -z "$version" ]; then - echo "versions.env must set REGISTRYCTL_VERSION" >&2 - exit 1 -fi - -verify_version() { - actual=$("$1" --version 2>/dev/null || true) - [ "$actual" = "registryctl $version" ] -} - -if [ -n "${REGISTRYCTL_BIN:-}" ]; then - if ! verify_version "$REGISTRYCTL_BIN"; then - echo "REGISTRYCTL_BIN must point to registryctl $version" >&2 - exit 1 - fi - registryctl=$REGISTRYCTL_BIN -elif command -v registryctl >/dev/null 2>&1 && verify_version "$(command -v registryctl)"; then - registryctl=$(command -v registryctl) -else - source_commit=$(sed -n 's/^REGISTRY_STACK_SOURCE_COMMIT=//p' "$VERSION_FILE") - source_dir=${REGISTRY_STACK_SOURCE_DIR:-"$ROOT/../registry-stack"} - if [ ! -d "$source_dir/.git" ]; then - echo "REGISTRY_STACK_SOURCE_DIR must name a Registry Stack checkout" >&2 - exit 1 - fi - actual_commit=$(git -C "$source_dir" rev-parse HEAD) - if [ "$actual_commit" != "$source_commit" ]; then - echo "Registry Stack source mismatch: expected $source_commit, found $actual_commit" >&2 - exit 1 - fi - registryctl="$source_dir/target/release/registryctl" - if ! verify_version "$registryctl"; then - cargo build --locked --release --manifest-path "$source_dir/Cargo.toml" -p registryctl - fi - if ! verify_version "$registryctl"; then - echo "the source-built registryctl did not report registryctl $version" >&2 - exit 1 - fi -fi - -case "${1:-}" in - path) - [ "$#" -eq 1 ] || { echo "usage: $0 path" >&2; exit 2; } - printf '%s\n' "$registryctl" - ;; - run) - shift - [ "$#" -gt 0 ] || { echo "usage: $0 run " >&2; exit 2; } - exec "$registryctl" "$@" - ;; - *) - echo "usage: $0 >" >&2 - exit 2 - ;; -esac diff --git a/scripts/registryctl-test-output.py b/scripts/registryctl-test-output.py deleted file mode 100755 index 3f6db2f..0000000 --- a/scripts/registryctl-test-output.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -"""Validate a current-main registryctl fixture and coverage report.""" - -from __future__ import annotations - -import json -import sys -from typing import Any - - -REPORT_SCHEMA = "registryctl.project_command.v1" -MAX_REPORT_BYTES = 16 * 1024 * 1024 - - -class TestReportError(ValueError): - """A safe validation error for a registryctl test report.""" - - -def reject_duplicate_fields(pairs: list[tuple[str, Any]]) -> dict[str, Any]: - result: dict[str, Any] = {} - for key, value in pairs: - if key in result: - raise TestReportError("the JSON report contains a duplicate field") - result[key] = value - return result - - -def validate_test_report(raw: bytes) -> tuple[str, int, int]: - if not raw: - raise TestReportError("registryctl returned an empty report") - if len(raw) > MAX_REPORT_BYTES: - raise TestReportError("the JSON report exceeds the size limit") - try: - report = json.loads(raw, object_pairs_hook=reject_duplicate_fields) - except (json.JSONDecodeError, UnicodeDecodeError, RecursionError) as error: - raise TestReportError("registryctl did not return strict UTF-8 JSON") from error - if not isinstance(report, dict): - raise TestReportError("the JSON report is not an object") - if report.get("schema_version") != REPORT_SCHEMA: - raise TestReportError("the JSON report has an unsupported schema") - if report.get("status") != "passed": - raise TestReportError("registryctl did not report a passing fixture suite") - project = report.get("project") - if not isinstance(project, str) or not project: - raise TestReportError("the JSON report has no project identity") - - fixtures = report.get("fixtures") - if ( - not isinstance(fixtures, list) - or not fixtures - or any( - not isinstance(fixture, dict) or fixture.get("passed") is not True - for fixture in fixtures - ) - ): - raise TestReportError("the JSON report contains a non-passing fixture") - - coverage = report.get("fixture_coverage") - targets = coverage.get("targets") if isinstance(coverage, dict) else None - if not isinstance(targets, list) or not targets: - raise TestReportError("the JSON report has no fixture coverage targets") - for target in targets: - if not isinstance(target, dict): - raise TestReportError("a fixture coverage target is not an object") - identity = target.get("identity") - compiled = target.get("compiled_contract") - inventory = target.get("fixture_inventory") - if ( - target.get("fixture_set_state") != "fixture_bearing" - or not isinstance(identity, dict) - or not isinstance(identity.get("integration"), str) - or not isinstance(identity.get("capability"), str) - or not isinstance(compiled, dict) - or compiled.get("kind") != "compiled_contract" - or not isinstance(compiled.get("digest"), str) - or not compiled["digest"].startswith("sha256:") - or not isinstance(inventory, list) - or not inventory - or any( - not isinstance(item, dict) - or item.get("pass_state") != "passed" - or not isinstance(item.get("fixture_digest"), str) - or not item["fixture_digest"].startswith("sha256:") - for item in inventory - ) - ): - raise TestReportError("every coverage target must bind passing fixtures to a compiled contract") - return project, len(fixtures), len(targets) - - -def main() -> int: - raw = sys.stdin.buffer.read(MAX_REPORT_BYTES + 1) - try: - project, fixture_count, target_count = validate_test_report(raw) - except TestReportError as error: - print(f"registryctl test report invalid: {error}", file=sys.stderr) - return 1 - print( - f"PASS: {project}: {fixture_count}/{fixture_count} fixtures passed; " - f"compiled fixture coverage includes {target_count}/{target_count} targets" - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/relay_workload_identity_agent.py b/scripts/relay_workload_identity_agent.py deleted file mode 100755 index a87c5d6..0000000 --- a/scripts/relay_workload_identity_agent.py +++ /dev/null @@ -1,754 +0,0 @@ -#!/usr/bin/env python3 -"""Issue short-lived Relay workload tokens for co-located consumers. - -The agent binds only to IPv4 loopback. It publishes the public half of every -configured Ed25519 JWK from one endpoint and atomically maintains one access -token file per identity. Tokens may identify either the local loopback issuer -or a separately published HTTPS issuer whose public-only JWKS contains the same -keys. Private JWK values are read indirectly: each bounded -WORKLOAD_IDENTITIES_JSON entry names the environment variable containing its -JWK JSON. -""" - -from __future__ import annotations - -import base64 -import binascii -import hashlib -import ipaddress -import json -import os -import re -import secrets -import signal -import stat -import sys -import tempfile -import threading -import time -from collections.abc import Callable, Mapping -from dataclasses import dataclass -from http import HTTPStatus -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path -from typing import Any, Never, cast -from urllib.parse import urlsplit - -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey -from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat - - -LOOPBACK_HOST = "127.0.0.1" -DEFAULT_PORT = 8090 -DEFAULT_TOKEN_TTL_SECONDS = 300 -DEFAULT_ROTATE_BEFORE_SECONDS = 60 -DEFAULT_ROTATION_INTERVAL_SECONDS = 5 -DEFAULT_TOKEN_UID = 65534 -DEFAULT_TOKEN_GID = 65534 - -MAX_IDENTITIES = 8 -MAX_IDENTITIES_JSON_BYTES = 64 * 1024 -MAX_JWK_BYTES = 16 * 1024 -MAX_TEXT_LENGTH = 256 -MAX_PATH_LENGTH = 4096 -MAX_SCOPE_COUNT = 32 -MAX_SCOPE_LENGTH = 256 -MAX_TOKEN_TTL_SECONDS = 900 -MAX_TOKEN_FILE_BYTES = 16 * 1024 -MAX_ROTATION_INTERVAL_SECONDS = 60 -MAX_ID = 2_147_483_647 - -ENV_NAME_PATTERN = re.compile(r"[A-Z][A-Z0-9_]{0,127}\Z") -TOKEN_VALUE_PATTERN = re.compile(r"[\x21-\x7e]+\Z") -BASE64URL_PATTERN = re.compile(r"[A-Za-z0-9_-]+\Z") -DNS_NAME_PATTERN = re.compile( - r"(?=.{1,253}\Z)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+" - r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\Z" -) - - -class ConfigurationError(ValueError): - """Raised when workload identity configuration is unsafe or incomplete.""" - - -def _reject_constant(_value: str) -> Never: - raise ConfigurationError("JSON contains a non-finite number") - - -def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: - result: dict[str, Any] = {} - for key, value in pairs: - if key in result: - raise ConfigurationError("JSON contains a duplicate field") - result[key] = value - return result - - -def _required_text( - environ: Mapping[str, str], - name: str, - *, - maximum: int = MAX_TEXT_LENGTH, -) -> str: - value = environ.get(name) - if value is None or not value or len(value) > maximum: - raise ConfigurationError(f"{name} is missing or outside its size bound") - return value - - -def _bounded_integer( - environ: Mapping[str, str], - name: str, - default: int, - *, - minimum: int, - maximum: int, -) -> int: - raw = environ.get(name, str(default)) - if not raw or len(raw) > 10 or not raw.isascii() or not raw.isdecimal(): - raise ConfigurationError(f"{name} must be a bounded decimal integer") - value = int(raw) - if value < minimum or value > maximum: - raise ConfigurationError(f"{name} is outside its allowed range") - return value - - -def _document_integer( - document: Mapping[str, Any], - field: str, - default: int, - *, - minimum: int, - maximum: int, -) -> int: - value = document.get(field, default) - if isinstance(value, bool) or not isinstance(value, int): - raise ConfigurationError(f"workload identity {field} must be an integer") - if value < minimum or value > maximum: - raise ConfigurationError( - f"workload identity {field} is outside its allowed range" - ) - return value - - -def _document_token(document: Mapping[str, Any], field: str) -> str: - value = document.get(field) - if ( - not isinstance(value, str) - or not value - or len(value) > MAX_TEXT_LENGTH - or TOKEN_VALUE_PATTERN.fullmatch(value) is None - ): - raise ConfigurationError( - f"workload identity {field} must contain bounded visible ASCII without spaces" - ) - return value - - -def _decode_base64url(value: object, field: str) -> bytes: - if not isinstance(value, str) or BASE64URL_PATTERN.fullmatch(value) is None: - raise ConfigurationError(f"private JWK {field} is not base64url") - try: - raw = base64.b64decode( - value + "=" * (-len(value) % 4), altchars=b"-_", validate=True - ) - except (binascii.Error, ValueError) as error: - raise ConfigurationError(f"private JWK {field} is not base64url") from error - if _base64url(raw) != value: - raise ConfigurationError(f"private JWK {field} is not canonical base64url") - return raw - - -def _base64url(value: bytes) -> str: - return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") - - -@dataclass(frozen=True) -class KeyMaterial: - private_key: Ed25519PrivateKey - public_jwk: dict[str, str] - - @classmethod - def from_json(cls, encoded: str) -> KeyMaterial: - if not encoded or len(encoded.encode("utf-8")) > MAX_JWK_BYTES: - raise ConfigurationError("private JWK is missing or outside its size bound") - try: - document = json.loads( - encoded, - object_pairs_hook=_unique_object, - parse_constant=_reject_constant, - ) - except (json.JSONDecodeError, UnicodeError) as error: - raise ConfigurationError("private JWK is not valid JSON") from error - if not isinstance(document, dict): - raise ConfigurationError("private JWK must be a JSON object") - - allowed_fields = {"alg", "crv", "d", "kid", "kty", "use", "x"} - if set(document) - allowed_fields: - raise ConfigurationError("private JWK contains unsupported fields") - if document.get("kty") != "OKP": - raise ConfigurationError("private JWK kty must be OKP") - if document.get("crv") != "Ed25519": - raise ConfigurationError("private JWK crv must be Ed25519") - if document.get("alg") != "EdDSA": - raise ConfigurationError("private JWK alg must be EdDSA") - if document.get("use") not in (None, "sig"): - raise ConfigurationError("private JWK use must be sig when present") - - kid = document.get("kid") - if ( - not isinstance(kid, str) - or not kid - or len(kid) > MAX_TEXT_LENGTH - or TOKEN_VALUE_PATTERN.fullmatch(kid) is None - ): - raise ConfigurationError("private JWK kid is invalid") - - private_bytes = _decode_base64url(document.get("d"), "d") - public_bytes = _decode_base64url(document.get("x"), "x") - if len(private_bytes) != 32 or len(public_bytes) != 32: - raise ConfigurationError("private JWK key material must be 32 bytes") - private_key = Ed25519PrivateKey.from_private_bytes(private_bytes) - derived_public = private_key.public_key().public_bytes( - Encoding.Raw, PublicFormat.Raw - ) - if not secrets.compare_digest(derived_public, public_bytes): - raise ConfigurationError( - "private JWK public and private values do not match" - ) - - public_jwk = { - "alg": "EdDSA", - "crv": "Ed25519", - "kid": kid, - "kty": "OKP", - "x": document["x"], - } - if document.get("use") == "sig": - public_jwk["use"] = "sig" - return cls(private_key=private_key, public_jwk=public_jwk) - - -@dataclass(frozen=True) -class IdentityConfig: - audience: str - azp: str - subject: str - scope: str - token_file: Path - token_uid: int - token_gid: int - key_material: KeyMaterial - - -@dataclass(frozen=True) -class Config: - bind_host: str - port: int - issuer: str - identities: tuple[IdentityConfig, ...] - token_ttl_seconds: int - rotate_before_seconds: int - rotation_interval_seconds: int - - @classmethod - def from_environ(cls, environ: Mapping[str, str]) -> Config: - bind_host = environ.get("WORKLOAD_BIND_HOST", LOOPBACK_HOST) - if bind_host != LOOPBACK_HOST: - raise ConfigurationError("WORKLOAD_BIND_HOST must be IPv4 loopback") - port = _bounded_integer( - environ, "WORKLOAD_PORT", DEFAULT_PORT, minimum=1, maximum=65535 - ) - - try: - issuer = _required_text(environ, "WORKLOAD_ISSUER") - parsed_issuer = urlsplit(issuer) - issuer_port = parsed_issuer.port - except ValueError as error: - raise ConfigurationError("WORKLOAD_ISSUER is not a valid URL") from error - has_unsafe_url_component = ( - bool(parsed_issuer.path) - or bool(parsed_issuer.query) - or bool(parsed_issuer.fragment) - or parsed_issuer.username is not None - or parsed_issuer.password is not None - ) - local_issuer = ( - parsed_issuer.scheme == "http" - and parsed_issuer.hostname == bind_host - and issuer_port == port - and parsed_issuer.netloc == f"{bind_host}:{port}" - and not has_unsafe_url_component - ) - hosted_hostname = parsed_issuer.hostname or "" - try: - ipaddress.ip_address(hosted_hostname) - except ValueError: - hosted_hostname_is_ip = False - else: - hosted_hostname_is_ip = True - hosted_issuer = ( - parsed_issuer.scheme == "https" - and not hosted_hostname_is_ip - and DNS_NAME_PATTERN.fullmatch(hosted_hostname) is not None - and issuer_port in (None, 443) - and parsed_issuer.netloc - in (hosted_hostname, f"{hosted_hostname}:443") - and not has_unsafe_url_component - ) - if not local_issuer and not hosted_issuer: - raise ConfigurationError( - "WORKLOAD_ISSUER must be the exact loopback listener or a " - "path-free HTTPS DNS origin" - ) - - token_ttl_seconds = _bounded_integer( - environ, - "WORKLOAD_TOKEN_TTL_SECONDS", - DEFAULT_TOKEN_TTL_SECONDS, - minimum=30, - maximum=MAX_TOKEN_TTL_SECONDS, - ) - rotate_before_seconds = _bounded_integer( - environ, - "WORKLOAD_ROTATE_BEFORE_SECONDS", - DEFAULT_ROTATE_BEFORE_SECONDS, - minimum=5, - maximum=token_ttl_seconds - 5, - ) - rotation_interval_seconds = _bounded_integer( - environ, - "WORKLOAD_ROTATION_INTERVAL_SECONDS", - DEFAULT_ROTATION_INTERVAL_SECONDS, - minimum=1, - maximum=min(MAX_ROTATION_INTERVAL_SECONDS, rotate_before_seconds), - ) - default_token_uid = _bounded_integer( - environ, - "WORKLOAD_TOKEN_UID", - DEFAULT_TOKEN_UID, - minimum=0, - maximum=MAX_ID, - ) - default_token_gid = _bounded_integer( - environ, - "WORKLOAD_TOKEN_GID", - DEFAULT_TOKEN_GID, - minimum=0, - maximum=MAX_ID, - ) - - encoded_identities = _required_text( - environ, - "WORKLOAD_IDENTITIES_JSON", - maximum=MAX_IDENTITIES_JSON_BYTES, - ) - if len(encoded_identities.encode("utf-8")) > MAX_IDENTITIES_JSON_BYTES: - raise ConfigurationError( - "WORKLOAD_IDENTITIES_JSON is outside its size bound" - ) - try: - identity_documents = json.loads( - encoded_identities, - object_pairs_hook=_unique_object, - parse_constant=_reject_constant, - ) - except (json.JSONDecodeError, UnicodeError) as error: - raise ConfigurationError( - "WORKLOAD_IDENTITIES_JSON is not valid JSON" - ) from error - if ( - not isinstance(identity_documents, list) - or not identity_documents - or len(identity_documents) > MAX_IDENTITIES - ): - raise ConfigurationError( - f"WORKLOAD_IDENTITIES_JSON must contain 1..{MAX_IDENTITIES} identities" - ) - - identities: list[IdentityConfig] = [] - allowed_fields = { - "audience", - "azp", - "private_jwk_env", - "scopes", - "subject", - "token_file", - "token_gid", - "token_uid", - } - for document in identity_documents: - if not isinstance(document, dict): - raise ConfigurationError("each workload identity must be a JSON object") - if set(document) - allowed_fields: - raise ConfigurationError( - "workload identity contains unsupported fields" - ) - - audience = _document_token(document, "audience") - azp = _document_token(document, "azp") - subject = _document_token(document, "subject") - - scopes = document.get("scopes") - if ( - not isinstance(scopes, list) - or not scopes - or len(scopes) > MAX_SCOPE_COUNT - or any( - not isinstance(item, str) - or not item - or len(item) > MAX_SCOPE_LENGTH - or TOKEN_VALUE_PATTERN.fullmatch(item) is None - for item in scopes - ) - or len(set(scopes)) != len(scopes) - ): - raise ConfigurationError( - "workload identity scopes are not a bounded list" - ) - scope = " ".join(scopes) - - token_file_raw = document.get("token_file") - if ( - not isinstance(token_file_raw, str) - or len(token_file_raw) > MAX_PATH_LENGTH - ): - raise ConfigurationError("workload identity token_file is invalid") - token_file = Path(token_file_raw) - if ( - not token_file.is_absolute() - or ".." in token_file.parts - or os.path.normpath(token_file_raw) != token_file_raw - or not token_file.name - or not token_file.parent.is_dir() - ): - raise ConfigurationError( - "workload identity token_file is not a safe target" - ) - - token_uid = _document_integer( - document, - "token_uid", - default_token_uid, - minimum=0, - maximum=MAX_ID, - ) - token_gid = _document_integer( - document, - "token_gid", - default_token_gid, - minimum=0, - maximum=MAX_ID, - ) - key_environment_name = document.get("private_jwk_env") - if ( - not isinstance(key_environment_name, str) - or ENV_NAME_PATTERN.fullmatch(key_environment_name) is None - ): - raise ConfigurationError( - "workload identity private_jwk_env is not a valid variable name" - ) - key_material = KeyMaterial.from_json( - _required_text(environ, key_environment_name, maximum=MAX_JWK_BYTES) - ) - - identities.append( - IdentityConfig( - audience=audience, - azp=azp, - subject=subject, - scope=scope, - token_file=token_file, - token_uid=token_uid, - token_gid=token_gid, - key_material=key_material, - ) - ) - - uniqueness_fields: tuple[tuple[str, list[object]], ...] = ( - ("azp", [identity.azp for identity in identities]), - ("subject", [identity.subject for identity in identities]), - ( - "kid", - [identity.key_material.public_jwk["kid"] for identity in identities], - ), - ( - "public key", - [identity.key_material.public_jwk["x"] for identity in identities], - ), - ("token_file", [identity.token_file for identity in identities]), - ) - for field, values in uniqueness_fields: - if len(set(values)) != len(values): - raise ConfigurationError( - f"workload identity {field} values must be unique" - ) - - return cls( - bind_host=bind_host, - port=port, - issuer=issuer, - identities=tuple(identities), - token_ttl_seconds=token_ttl_seconds, - rotate_before_seconds=rotate_before_seconds, - rotation_interval_seconds=rotation_interval_seconds, - ) - - -def _encode_json(value: object) -> bytes: - return json.dumps(value, separators=(",", ":"), sort_keys=True).encode("utf-8") - - -def _mint_token( - config: Config, identity: IdentityConfig, issued_at: int -) -> tuple[str, int]: - expires_at = issued_at + config.token_ttl_seconds - header = { - "alg": "EdDSA", - "kid": identity.key_material.public_jwk["kid"], - "typ": "at+jwt", - } - claims = { - "aud": identity.audience, - "azp": identity.azp, - "exp": expires_at, - "iat": issued_at, - "iss": config.issuer, - "jti": secrets.token_urlsafe(18), - "nbf": issued_at, - "scope": identity.scope, - "sub": identity.subject, - } - signing_input = b".".join( - ( - _base64url(_encode_json(header)).encode(), - _base64url(_encode_json(claims)).encode(), - ) - ) - signature = identity.key_material.private_key.sign(signing_input) - return f"{signing_input.decode('ascii')}.{_base64url(signature)}", expires_at - - -def _atomic_write_token(path: Path, token: str, *, uid: int, gid: int) -> None: - target_directory = str(path.parent) - descriptor, temporary_path = tempfile.mkstemp( - dir=target_directory, prefix=f".{path.name}.", suffix=".tmp" - ) - try: - os.fchmod(descriptor, 0o600) - os.fchown(descriptor, uid, gid) - with os.fdopen(descriptor, "wb", closefd=True) as output: - descriptor = -1 - output.write(token.encode("ascii")) - output.flush() - os.fsync(output.fileno()) - os.replace(temporary_path, path) - temporary_path = "" - directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) - directory_descriptor = os.open(target_directory, directory_flags) - try: - os.fsync(directory_descriptor) - finally: - os.close(directory_descriptor) - finally: - if descriptor >= 0: - os.close(descriptor) - if temporary_path: - try: - os.unlink(temporary_path) - except FileNotFoundError: - pass - - -class IdentityState: - def __init__( - self, config: Config, *, clock: Callable[[], float] = time.time - ) -> None: - self.config = config - self._clock = clock - self._expires_at = [0] * len(config.identities) - self._published_hashes: list[bytes | None] = [None] * len(config.identities) - self._lock = threading.RLock() - - def jwks_document(self) -> dict[str, list[dict[str, str]]]: - return { - "keys": [ - dict(identity.key_material.public_jwk) - for identity in self.config.identities - ] - } - - def rotate(self) -> None: - with self._lock: - issued_at = int(self._clock()) - for index, identity in enumerate(self.config.identities): - token, expires_at = _mint_token(self.config, identity, issued_at) - _atomic_write_token( - identity.token_file, - token, - uid=identity.token_uid, - gid=identity.token_gid, - ) - self._expires_at[index] = expires_at - self._published_hashes[index] = hashlib.sha256( - token.encode("ascii") - ).digest() - - def rotate_if_due(self) -> bool: - with self._lock: - issued_at = int(self._clock()) - rotated = False - for index, identity in enumerate(self.config.identities): - if ( - issued_at - < self._expires_at[index] - self.config.rotate_before_seconds - ): - continue - token, expires_at = _mint_token(self.config, identity, issued_at) - _atomic_write_token( - identity.token_file, - token, - uid=identity.token_uid, - gid=identity.token_gid, - ) - self._expires_at[index] = expires_at - self._published_hashes[index] = hashlib.sha256( - token.encode("ascii") - ).digest() - rotated = True - return rotated - - def ready(self) -> bool: - with self._lock: - now = int(self._clock()) - for index, identity in enumerate(self.config.identities): - if ( - not identity.key_material.public_jwk - or self._expires_at[index] <= now - or self._published_hashes[index] is None - ): - return False - try: - file_status = os.lstat(identity.token_file) - except OSError: - return False - if not ( - stat.S_ISREG(file_status.st_mode) - and stat.S_IMODE(file_status.st_mode) == 0o600 - and file_status.st_size > 0 - and file_status.st_size <= MAX_TOKEN_FILE_BYTES - and file_status.st_uid == identity.token_uid - and file_status.st_gid == identity.token_gid - ): - return False - try: - published = identity.token_file.read_bytes() - except OSError: - return False - if not secrets.compare_digest( - hashlib.sha256(published).digest(), self._published_hashes[index] - ): - return False - return True - - -class AgentHTTPServer(ThreadingHTTPServer): - daemon_threads = True - - def __init__( - self, - server_address: tuple[str, int], - state: IdentityState, - ) -> None: - self.identity_state = state - super().__init__(server_address, AgentRequestHandler) - - -class AgentRequestHandler(BaseHTTPRequestHandler): - server_version = "solmara-workload-identity" - sys_version = "" - - def do_GET(self) -> None: # noqa: N802 - server = cast(AgentHTTPServer, self.server) - if self.path == "/.well-known/jwks.json": - self._send_json(HTTPStatus.OK, server.identity_state.jwks_document()) - return - if self.path == "/health": - ready = server.identity_state.ready() - self._send_json( - HTTPStatus.OK if ready else HTTPStatus.SERVICE_UNAVAILABLE, - {"status": "ready" if ready else "not_ready"}, - ) - return - self._send_json(HTTPStatus.NOT_FOUND, {"error": "not_found"}) - - def _send_json(self, status_code: HTTPStatus, document: object) -> None: - body = _encode_json(document) - self.send_response(status_code) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(body))) - self.send_header("Cache-Control", "no-store") - self.end_headers() - self.wfile.write(body) - - def log_message(self, _format: str, *args: object) -> None: - return - - -def _rotation_loop(state: IdentityState, stop_event: threading.Event) -> None: - while not stop_event.wait(state.config.rotation_interval_seconds): - try: - state.rotate_if_due() - except Exception: - # Health becomes not ready when any last published token expires or - # is no longer a correctly owned, nonempty regular file. - continue - - -def serve(config: Config) -> None: - state = IdentityState(config) - state.rotate() - stop_event = threading.Event() - server = AgentHTTPServer((config.bind_host, config.port), state) - rotation_thread = threading.Thread( - target=_rotation_loop, - args=(state, stop_event), - name="token-rotation", - daemon=True, - ) - rotation_thread.start() - - def request_stop(_signal_number: int, _frame: object) -> None: - stop_event.set() - threading.Thread(target=server.shutdown, daemon=True).start() - - signal.signal(signal.SIGTERM, request_stop) - signal.signal(signal.SIGINT, request_stop) - try: - server.serve_forever(poll_interval=0.25) - finally: - stop_event.set() - server.server_close() - rotation_thread.join(timeout=config.rotation_interval_seconds + 1) - - -def main() -> int: - try: - config = Config.from_environ(os.environ) - serve(config) - except ConfigurationError as error: - print( - f"workload identity agent configuration rejected: {error}", file=sys.stderr - ) - return 2 - except Exception: - print( - "workload identity agent stopped after an operational failure", - file=sys.stderr, - ) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/review.sh b/scripts/review.sh deleted file mode 100755 index d74d646..0000000 --- a/scripts/review.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -if [ "$#" -ne 0 ]; then - echo "usage: scripts/review.sh" >&2 - exit 2 -fi - -"$root/scripts/check-fiction.sh" -"$root/scripts/check-image-pins.py" -"$root/scripts/check-config-secrets.py" -"$root/scripts/registry-projects.sh" check -"$root/scripts/registry-projects.sh" check-runtime -"$root/scripts/check-evidence-runtime.py" -uv run --project "$root" "$root/scripts/publish-metadata.py" --check -uv run --project "$root" "$root/scripts/metadata-lint.py" - -if git -C "$root" grep -I -n \ - -E "(BEGIN (RSA|OPENSSH|EC|PRIVATE) KEY|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9_]{20,})" \ - -- .; then - echo "Potential secret material found in repository files." >&2 - exit 1 -fi - -cat <<'CHECKLIST' -Security checklist for reviewer: -1. Purpose-gated Relay entities require Data-Purpose where sensitive data is exposed. -2. Evidence source credentials are short-lived Relay workload tokens held in a private runtime volume. -3. Mint authenticates the application with private_key_jwt and issues only the configured Evidence audience. -4. Evidence requirements disclose reviewed concept values as flattened signed JWS, never source rows. -5. Run `just smoke` against the live stack to exercise Mint, Evidence, Relay sources, scenarios, and the portal. -6. Audit and subject-binding secrets are generated locally and remain uncommitted. -7. The exact Registry Stack main commit in versions.env owns all three locally built runtime images. -CHECKLIST diff --git a/scripts/seed-esignet.py b/scripts/seed-esignet.py index 523de92..ae185a5 100755 --- a/scripts/seed-esignet.py +++ b/scripts/seed-esignet.py @@ -236,14 +236,9 @@ def main() -> int: seed_esignet(jwk, key_hash) clear_esignet_client_cache() account_source = os.environ.get("ESIGNET_ACCOUNT_SOURCE_LABEL", "NIA population Relay profile solmara-nia-userinfo") - demo_subject = os.environ.get("ESIGNET_DEMO_SUBJECT") - demo_otp = os.environ.get("ESIGNET_DEMO_OTP", "111111") print(f"Seeded eSignet client {CLIENT_ID}.") print(f"Relay-backed account source: {account_source}.") - if demo_subject: - print(f"Demo subject available through Relay: {demo_subject}. Local static OTP: {demo_otp}.") - else: - print(f"Local static OTP: {demo_otp}.") + print("Demo credentials are configured without being written to logs.") if os.environ.get("ESIGNET_SEED_STAY_READY") == "true": Path("/tmp/ready").write_text("ready\n", encoding="utf-8") while True: diff --git a/scripts/smoke-child-benefit-application.py b/scripts/smoke-child-benefit-application.py deleted file mode 100755 index 91bcb83..0000000 --- a/scripts/smoke-child-benefit-application.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/env python3 -"""Smoke child-benefit application composition over authority Notaries. - -The legacy-named child-benefit service is an application evidence collector, -not a Notary. It uses the ordinary Registry Notary HTTP API to collect -minimized predicates from the CRA, NIA, SRO, and Programme authority Notaries. -""" - -from __future__ import annotations - -import json -import os -import shlex -import sys -from pathlib import Path -from typing import Any - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from scenarios.common import ( # noqa: E402 - CHILD_BENEFIT_AS_OF_DATE, - PURPOSES, - auth_headers, - evaluation_body, - http_json, - joined_url, -) - - -POSITIVE_SUBJECT = "2300010248" -APPLICATION_URL_ENV = "CHILD_BENEFIT_FEDERATOR_URL" -APPLICATION_URL_DEFAULT = "http://127.0.0.1:4321" -APPLICATION_TOKEN_ENV = "CHILD_BENEFIT_FEDERATOR_TOKEN" -UNSUPPORTED_PURPOSE = "https://id.registrystack.org/solmara/purpose/unsupported-application-smoke" -EXPECTED_CLAIM_OWNERS = { - "birth-is-registered": "cra-notary", - "child-age-under-5": "cra-notary", - "population-record-active": "nia-notary", - "household-below-poverty-threshold": "sro-notary", - "not-already-enrolled": "programme-notary", -} - - -def main() -> int: - load_dotenv(ROOT / ".env") - token = os.environ.get(APPLICATION_TOKEN_ENV, "") - if not token: - print( - f"smoke-child-benefit-application: missing {APPLICATION_TOKEN_ENV}; run `just generate` before live smoke", - file=sys.stderr, - ) - return 1 - - base_url = os.environ.get(APPLICATION_URL_ENV, APPLICATION_URL_DEFAULT) - claims = list(EXPECTED_CLAIM_OWNERS) - failures = application_evidence_failures(base_url, token, POSITIVE_SUBJECT, claims) - failures.extend(wrong_purpose_failures(base_url, token, POSITIVE_SUBJECT)) - failures.extend(raw_household_denial_failures(base_url, token, POSITIVE_SUBJECT)) - if failures: - for failure in failures: - print(f"smoke-child-benefit-application: {failure}", file=sys.stderr) - return 1 - - print( - "smoke-child-benefit-application: authority-owned evidence, application non-composition, " - "unsupported-purpose denial, and raw-source denial checks passed" - ) - return 0 - - -def load_dotenv(path: Path) -> None: - if not path.exists(): - return - for raw_line in path.read_text().splitlines(): - line = raw_line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, raw_value = line.split("=", 1) - key = key.strip() - if not key or key in os.environ: - continue - if raw_value == "": - os.environ[key] = "" - continue - parts = shlex.split(raw_value, posix=True) - os.environ[key] = parts[0] if parts else "" - - -def application_evidence_failures( - base_url: str, token: str, subject: str, claims: list[str] -) -> list[str]: - response = http_json( - "POST", - joined_url(base_url, "/v1/evaluations"), - auth_headers(token, PURPOSES["child_benefit"], "application/json"), - evaluation_body( - subject, - claims, - scheme="solmara_uin", - format="application/json", - variables={"as_of_date": CHILD_BENEFIT_AS_OF_DATE}, - ), - ) - return validated_evidence_failures(response.status, response.body, subject, claims) - - -def validated_evidence_failures( - status: int | None, body: Any, subject: str, claims: list[str] -) -> list[str]: - if status != 200: - return [f"positive application request returned HTTP {status}, expected 200"] - if not isinstance(body, dict): - return ["positive application response was not a JSON object"] - - failures: list[str] = [] - for obsolete in ("federation_trace", "federator", "federation"): - if obsolete in body: - failures.append(f"positive application response retained obsolete {obsolete}") - - orchestration = body.get("orchestration") - if not isinstance(orchestration, dict): - failures.append("positive application response omitted orchestration ownership") - else: - if orchestration.get("service_id") != "child-benefit-federator": - failures.append("positive application response identified the wrong application") - if orchestration.get("decision") != "not_composed": - failures.append("positive application response crossed the programme decision boundary") - - results = body.get("results") - result_by_claim = ( - { - item.get("claim_id"): item - for item in results - if isinstance(item, dict) and isinstance(item.get("claim_id"), str) - } - if isinstance(results, list) - else {} - ) - if ( - not isinstance(results, list) - or set(result_by_claim) != set(claims) - or len(results) != len(claims) - ): - failures.append("positive application response did not return exactly the requested predicates") - for claim_id in claims: - result = result_by_claim.get(claim_id) - if not isinstance(result, dict): - continue - if any(key in result for key in ("error", "source_record", "raw")): - failures.append(f"{claim_id} leaked an error or raw source representation") - if result.get("satisfied") is not True or result.get("disclosure") != "predicate": - failures.append(f"positive application response did not satisfy {claim_id} as a predicate") - if result.get("notary_service_id") != EXPECTED_CLAIM_OWNERS[claim_id]: - failures.append(f"{claim_id} was not attributed to its authority Notary") - - source_trace = body.get("source_trace") - expected_services = {EXPECTED_CLAIM_OWNERS[claim_id] for claim_id in claims} - traced_services: set[str] = set() - if not isinstance(source_trace, list): - failures.append("positive application response omitted its ordinary source trace") - else: - for item in source_trace: - if not isinstance(item, dict): - failures.append("positive application source trace contained an invalid entry") - continue - service_id = item.get("service_id") - if isinstance(service_id, str): - traced_services.add(service_id) - summary = item.get("response_summary") - if not isinstance(summary, dict) or summary.get("status") != 200: - failures.append(f"{service_id or 'unknown authority'} source trace did not record HTTP 200") - if traced_services != expected_services: - failures.append("positive application source trace did not cover the requested authority Notaries") - - if subject in json.dumps(body, sort_keys=True): - failures.append("positive application response echoed the raw subject identifier") - return failures - - -def wrong_purpose_failures(base_url: str, token: str, subject: str) -> list[str]: - response = http_json( - "POST", - joined_url(base_url, "/v1/evaluations"), - auth_headers(token, UNSUPPORTED_PURPOSE, "application/json"), - evaluation_body( - subject, - ["birth-is-registered"], - scheme="solmara_uin", - format="application/json", - ), - ) - return denial_failures( - "unsupported-purpose request", - response.status, - response.headers, - response.body, - expected_status=403, - expected_code="pdp.purpose_not_permitted", - ) - - -def denial_failures( - label: str, - status: int | None, - headers: dict[str, str], - body: Any, - *, - expected_status: int, - expected_code: str, -) -> list[str]: - failures: list[str] = [] - if status != expected_status: - failures.append(f"{label} returned HTTP {status}, expected {expected_status}") - content_type = headers.get("content-type", "").split(";", 1)[0].strip().lower() - if content_type != "application/problem+json": - failures.append( - f"{label} returned {content_type or 'no content type'}, expected application/problem+json" - ) - code = body.get("code") if isinstance(body, dict) else None - if code != expected_code: - failures.append(f"{label} returned problem code {code!r}, expected {expected_code!r}") - return failures - - -def raw_household_denial_failures( - base_url: str, token: str, subject: str -) -> list[str]: - response = http_json( - "POST", - joined_url(base_url, "/v1/evaluations"), - auth_headers(token, PURPOSES["child_benefit"], "application/json"), - evaluation_body( - subject, - ["household-poverty-score"], - scheme="solmara_uin", - disclosure="value", - format="application/json", - ), - ) - failures = denial_failures( - "raw household request", - response.status, - response.headers, - response.body, - expected_status=403, - expected_code="pdp.purpose_not_permitted", - ) - serialized = json.dumps(response.body, sort_keys=True) - if "raw_household_score" in serialized or subject in serialized: - failures.append("raw household denial reflected protected source data") - return failures - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/smoke-esignet-login.mjs b/scripts/smoke-esignet-login.mjs new file mode 100644 index 0000000..e038f46 --- /dev/null +++ b/scripts/smoke-esignet-login.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +import { chromium } from '../portal/node_modules/@playwright/test/index.mjs'; + +const portalUrl = (process.env.SOLMARA_PORTAL_PUBLIC_BASE_URL || 'http://127.0.0.1:4300').replace(/\/$/, ''); +const subject = process.env.ESIGNET_DEMO_SUBJECT || '2300018263'; +const otp = process.env.ESIGNET_DEMO_OTP || '111111'; + +const browser = await chromium.launch({ headless: true }); +try { + const page = await browser.newPage(); + const deadline = Date.now() + 60_000; + while (true) { + await page.goto(`${portalUrl}/auth/login`); + try { + await page.getByRole('button', { name: 'Verify with OTP' }).waitFor({ timeout: 5_000 }); + break; + } catch { + // eSignet may still be starting. Retry the whole authorization request. + } + if (Date.now() >= deadline) { + throw new Error('eSignet login did not become ready'); + } + await page.waitForTimeout(1_000); + } + await page.getByRole('button', { name: 'Verify with OTP' }).click(); + await page.getByRole('textbox', { name: 'UIN/VID' }).fill(subject); + const sendOtpResponse = page.waitForResponse((response) => response.url().includes('/authorization/send-otp')); + await page.getByRole('button', { name: 'Get OTP' }).click(); + const sendOtp = await (await sendOtpResponse).json(); + if (Array.isArray(sendOtp.errors) && sendOtp.errors.length) { + throw new Error('eSignet send-otp failed'); + } + const otpInputs = page.locator('input[type="tel"]'); + if (await otpInputs.count() === 1) { + await otpInputs.fill(otp); + } else { + for (const digit of otp) { + await otpInputs.first().press(digit); + await page.waitForTimeout(100); + } + } + await page.getByRole('button', { name: /verify|continue/i }).click(); + const servicesUrl = `${portalUrl}/services`; + await page.waitForURL((url) => url.href === servicesUrl || url.pathname.endsWith('/consent'), { + timeout: 60_000, + }); + if (page.url() !== servicesUrl) { + const consent = page.getByRole('button', { name: /allow|consent|continue|accept/i }); + await consent.first().waitFor({ timeout: 10_000 }); + const claimCheckboxes = page.locator('input[type="checkbox"]'); + const allClaims = page.getByRole('checkbox', { name: 'voluntary_claims' }); + if (await allClaims.count()) { + await allClaims.check({ force: true }); + } else { + for (let index = 0; index < await claimCheckboxes.count(); index += 1) { + const checkbox = claimCheckboxes.nth(index); + if (!(await checkbox.isChecked())) await checkbox.check({ force: true }); + } + } + await consent.first().click(); + } + await page.waitForURL(servicesUrl, { timeout: 60_000 }); + console.log('smoke-esignet-login: PASS'); +} finally { + await browser.close(); +} diff --git a/scripts/smoke-esignet.py b/scripts/smoke-esignet.py index 506599b..dbb7431 100755 --- a/scripts/smoke-esignet.py +++ b/scripts/smoke-esignet.py @@ -127,6 +127,7 @@ def check_esignet_discovery(targets: SmokeTargets, timeout: float) -> None: root_issuer = root_doc.get("issuer") root_oauth_issuer = root_oauth_doc.get("issuer") ui_issuer = ui_doc.get("issuer") + expected_token_endpoint = f"{targets.esignet_url}/v1/esignet/oauth/v2/token" if not isinstance(service_issuer, str) or not service_issuer: raise SmokeFailure("service discovery omitted issuer") if root_issuer != service_issuer: @@ -141,6 +142,14 @@ def check_esignet_discovery(targets: SmokeTargets, timeout: float) -> None: raise SmokeFailure( "UI discovery issuer does not match service discovery issuer" ) + for name, document in ( + ("service", service_doc), + ("root OpenID", root_doc), + ("root OAuth", root_oauth_doc), + ("UI", ui_doc), + ): + if document.get("token_endpoint") != expected_token_endpoint: + raise SmokeFailure(f"{name} discovery token endpoint is not the public endpoint") def wait_for_json( diff --git a/scripts/smoke-hosted-provisioner-image.py b/scripts/smoke-hosted-provisioner-image.py new file mode 100755 index 0000000..c3bebb6 --- /dev/null +++ b/scripts/smoke-hosted-provisioner-image.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Exercise hosted Evidence and Mint provisioning through the built image.""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +LOCAL = ROOT / "config" / "evidence" / "local" / "cells" +GENERIC_ERROR = "hosted provisioner image smoke failed" + + +def _build_cells_module(): + path = ROOT / "evidence" / "scripts" / "build-cells.py" + spec = importlib.util.spec_from_file_location("build_cells", path) + if spec is None or spec.loader is None: + raise RuntimeError("unavailable helper") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _write_json(path: Path, value: object) -> None: + path.write_text(json.dumps(value, sort_keys=True), encoding="utf-8") + path.chmod(0o400) + + +def _copy_secret(source: Path, destination: Path) -> None: + shutil.copyfile(source, destination) + destination.chmod(0o400) + + +def _run( + image: str, arguments: list[str], mounts: list[tuple[Path, str, bool]] +) -> None: + command = [ + "docker", + "run", + "--rm", + "--platform", + "linux/amd64", + "--network", + "none", + "--read-only", + "--tmpfs", + "/tmp", + ] + for source, target, readonly in mounts: + specification = f"type=bind,source={source},target={target}" + if readonly: + specification += ",readonly" + command.extend(["--mount", specification]) + command.extend([image, *arguments]) + subprocess.run( + command, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + +def smoke(image: str, state_root: Path) -> None: + helper = _build_cells_module() + cra_input = state_root / "cra-input" + cra_input.mkdir(parents=True) + _write_json( + cra_input / "signing-public.jwk", + helper.public_jwk(LOCAL / "cra" / "secrets" / "signing.jwk"), + ) + for name in ("audit-hmac-key", "subject-binding-hmac-key"): + _copy_secret(LOCAL / "cra" / "secrets" / name, cra_input / name) + for client in ("cra-pension-evidence", "cra-citizen-evidence"): + _copy_secret( + LOCAL / "cra" / "secrets" / f"{client}-client-key", + cra_input / f"{client}-client-key", + ) + cra = state_root / "cra" + for name in ("runtime", "secrets", "extracts"): + (cra / name).mkdir(parents=True) + cra_mounts = [ + (cra_input, "/run/secrets/solmara-provisioning", True), + (cra / "runtime", "/provisioned/runtime", False), + (cra / "secrets", "/provisioned/secrets", False), + (cra / "extracts", "/provisioned/extracts", False), + ] + cra_arguments = [ + "provision", + "--target", + "cra-evidence", + "--assets", + "/opt/solmara-hosted-assets", + "--secrets", + "/run/secrets/solmara-provisioning", + "--runtime-output", + "/provisioned/runtime", + "--secret-output", + "/provisioned/secrets", + "--extract-output", + "/provisioned/extracts", + "--bind-host", + "172.29.1.21", + ] + _run(image, cra_arguments, cra_mounts) + _run(image, cra_arguments, cra_mounts) + + mint_input = state_root / "mint-input" + mint_input.mkdir() + _write_json( + mint_input / "signing-public.jwk", + helper.public_jwk(LOCAL / "mint" / "secrets" / "signing.jwk"), + ) + _copy_secret( + LOCAL / "mint" / "secrets" / "audit-hmac-key", + mint_input / "audit-hmac-key", + ) + clients = { + "cra": ("cra-pension-evidence", "cra-citizen-evidence"), + "mosd-programme": ("mosd-child-benefit-evidence",), + "sipf": ("sipf-pension-evidence", "sipf-survivor-evidence"), + "nagdi": ("nagdi-voucher-evidence", "nagdi-livestock-evidence"), + } + for cell, names in clients.items(): + for client in names: + _write_json( + mint_input / f"{client}-public.jwk", + helper.public_jwk(LOCAL / cell / "secrets" / f"{client}-client-key"), + ) + _write_json( + mint_input / "nia-esignet-public.jwk", + helper.public_jwk(LOCAL / "mint" / "clients" / "nia-esignet-rsa-client-key"), + ) + _write_json( + mint_input / "solmara-demo-client-public.jwk", + helper.public_jwk(LOCAL / "mint" / "clients" / "solmara-demo-client-key"), + ) + mint = state_root / "mint" + for name in ("runtime", "secrets"): + (mint / name).mkdir(parents=True) + mint_mounts = [ + (mint_input, "/run/secrets/solmara-provisioning", True), + (mint / "runtime", "/provisioned/runtime", False), + (mint / "secrets", "/provisioned/secrets", False), + ] + mint_arguments = [ + "provision", + "--target", + "mint", + "--assets", + "/opt/solmara-hosted-assets", + "--secrets", + "/run/secrets/solmara-provisioning", + "--runtime-output", + "/provisioned/runtime", + "--secret-output", + "/provisioned/secrets", + "--bind-host", + "172.29.1.20", + ] + _run(image, mint_arguments, mint_mounts) + _run(image, mint_arguments, mint_mounts) + + cra_config = yaml.safe_load((cra / "runtime/bundle/evidence.yaml").read_text()) + if not ( + cra / "runtime/bundle" / cra_config["signing"]["activePublicJwkFile"] + ).is_file(): + raise RuntimeError("missing Evidence public key") + if len(list((cra / "extracts").glob("*.sqlite"))) != 1: + raise RuntimeError("invalid Evidence extract publication") + mint_config = yaml.safe_load((mint / "runtime/mint.yaml").read_text()) + if not (mint / "runtime" / mint_config["signing"]["activePublicJwkFile"]).is_file(): + raise RuntimeError("missing Mint public key") + if len(list((mint / "runtime/clients").glob("*.yaml"))) != 9: + raise RuntimeError("invalid Mint client publication") + + +def main() -> int: + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--image", required=True) + parser.add_argument("--state-root", type=Path) + try: + arguments = parser.parse_args() + state_root = arguments.state_root or Path( + tempfile.mkdtemp(prefix="solmara-hosted-provisioner-smoke-") + ) + state_root.mkdir(parents=True, exist_ok=True) + smoke(arguments.image, state_root.resolve()) + except Exception: # noqa: BLE001 - CI output is a public redaction boundary. + print(GENERIC_ERROR, file=sys.stderr) + return 1 + print("hosted provisioner image smoke passed") + return 0 + + +if __name__ == "__main__": + os.umask(0o077) + raise SystemExit(main()) diff --git a/scripts/smoke-hosted.py b/scripts/smoke-hosted.py deleted file mode 100755 index 13a9f67..0000000 --- a/scripts/smoke-hosted.py +++ /dev/null @@ -1,569 +0,0 @@ -#!/usr/bin/env python3 -"""Public hosted smoke checks for Solmara Lab.""" - -from __future__ import annotations - -import argparse -import json -import os -import subprocess -import sys -import time -import urllib.error -import urllib.request -from dataclasses import dataclass -from pathlib import Path -from typing import Any -from urllib.parse import urljoin - - -ROOT = Path(__file__).resolve().parents[1] -DEFAULT_DOMAIN = "solmara.registrystack.org" -EXPECTED_SCENARIOS = { - "birth-to-child-benefit", - "death-to-pension-survivor", - "farmer-climate-smart-voucher", - "citizen-self-service", -} -EXPECTED_CHILD_BENEFIT_CLAIMS = { - "birth-is-registered", - "population-record-active", - "child-age-under-5", - "household-below-poverty-threshold", - "not-already-enrolled", -} -EXPECTED_CHILD_BENEFIT_NOTARIES = { - "cra-notary", - "nia-notary", - "sro-notary", - "programme-notary", -} -EXPECTED_CHILD_BENEFIT_ATTRIBUTION = { - "birth-is-registered": "cra-notary", - "child-age-under-5": "cra-notary", - "population-record-active": "nia-notary", - "household-below-poverty-threshold": "sro-notary", - "not-already-enrolled": "programme-notary", -} - - -@dataclass(frozen=True) -class ServiceTarget: - name: str - base_url: str - health_path: str = "/healthz" - env_name: str | None = None - - -@dataclass(frozen=True) -class HostedTargets: - home_url: str - portal_url: str - metadata_url: str - esignet_url: str - esignet_ui_url: str - wallet_url: str - relays: tuple[ServiceTarget, ...] - notaries: tuple[ServiceTarget, ...] - applications: tuple[ServiceTarget, ...] - - -class SmokeFailure(Exception): - """A stable smoke failure with a short operator-facing message.""" - - -def main(argv: list[str] | None = None) -> int: - args = parse_args(normalize_argv(sys.argv[1:] if argv is None else argv)) - targets = default_targets(args.domain, args.scheme) - env = hosted_env(os.environ, targets) - env["SOLMARA_SMOKE_READY_TIMEOUT_SECONDS"] = str(args.timeout) - - checks: list[tuple[str, Any]] = [ - ( - "public routes and service health", - lambda: check_public_routes(targets, args.timeout), - ), - ( - "Visitor Center scenario runner proxy", - lambda: check_home_demo(targets.home_url, args.timeout), - ), - ( - "eSignet backend", - lambda: run_command( - esignet_smoke_command(targets), - env, - cwd=ROOT, - ), - ), - ( - "Relay source endpoints", - lambda: run_command( - [sys.executable, str(ROOT / "scripts" / "smoke-relay-sources.py")], - env, - cwd=ROOT, - ), - ), - ( - "Notary scenario evaluations", - lambda: run_command( - [sys.executable, str(ROOT / "scripts" / "smoke-live.py")], - env, - cwd=ROOT, - ), - ), - ( - "published demo token refusals", - lambda: run_command( - [sys.executable, str(ROOT / "scripts" / "smoke-published-tokens.py")], - env, - cwd=ROOT, - ), - ), - ( - "portal live BFF", - lambda: run_command( - [sys.executable, str(ROOT / "scripts" / "smoke-portal-compose.py")], - env, - cwd=ROOT, - ), - ), - ] - - if args.browser: - checks.extend( - [ - ( - "Visitor Center browser e2e", - lambda: run_command( - ["pnpm", "e2e"], - { - **env, - "SOLMARA_HOME_E2E_MODE": "live", - "PLAYWRIGHT_BASE_URL": targets.home_url, - }, - cwd=ROOT / "home", - ), - ), - ( - "portal browser e2e", - lambda: run_command( - ["pnpm", "e2e"], - { - **env, - "SOLMARA_PORTAL_E2E_MODE": "hosted", - "PLAYWRIGHT_BASE_URL": targets.portal_url, - }, - cwd=ROOT / "portal", - ), - ), - ] - ) - - for name, check in checks: - print(f"check: {name}", flush=True) - try: - check() - except SmokeFailure as error: - print(f"FAILED: {name}: {error}", file=sys.stderr) - return 1 - except subprocess.CalledProcessError as error: - print(f"FAILED: {name}: command exited {error.returncode}", file=sys.stderr) - return error.returncode or 1 - - suffix = " with browser e2e" if args.browser else "" - print(f"smoke-hosted: Solmara hosted smoke passed{suffix}") - return 0 - - -def esignet_smoke_command(targets: HostedTargets) -> list[str]: - return [ - sys.executable, - str(ROOT / "scripts" / "smoke-esignet.py"), - "--esignet-url", - targets.esignet_url, - "--esignet-ui-url", - targets.esignet_ui_url, - ] - - -def parse_args(argv: list[str] | None) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--domain", - default=os.environ.get("SOLMARA_HOSTED_DOMAIN", DEFAULT_DOMAIN), - help=f"root hosted domain, default {DEFAULT_DOMAIN}", - ) - parser.add_argument( - "--scheme", - default=os.environ.get("SOLMARA_HOSTED_SCHEME", "https"), - choices=("http", "https"), - help="public URL scheme, default https", - ) - parser.add_argument( - "--timeout", - type=float, - default=float(os.environ.get("SOLMARA_SMOKE_READY_TIMEOUT_SECONDS", "90")), - help="seconds to wait for each public endpoint", - ) - parser.add_argument( - "--browser", - action="store_true", - default=truthy(os.environ.get("SOLMARA_HOSTED_SMOKE_BROWSER")), - help="also run hosted Playwright e2e for the Visitor Center and portal", - ) - return parser.parse_args(argv) - - -def normalize_argv(argv: list[str]) -> list[str]: - return argv[1:] if argv[:1] == ["--"] else argv - - -def default_targets(domain: str, scheme: str = "https") -> HostedTargets: - clean_domain = normalize_domain(domain) - - def public_url(host: str) -> str: - return f"{scheme}://{host}" - - def subdomain(name: str) -> str: - return public_url(f"{name}.{clean_domain}") - - return HostedTargets( - home_url=public_url(clean_domain), - portal_url=subdomain("portal"), - metadata_url=subdomain("metadata"), - esignet_url=subdomain("esignet"), - esignet_ui_url=subdomain("esignet-ui"), - wallet_url=subdomain("wallet"), - relays=( - ServiceTarget( - "CRA civil relay", - subdomain("cra-relay"), - env_name="SOLMARA_CRA_RELAY_URL", - ), - ServiceTarget( - "NIA population relay", - subdomain("nia-relay"), - env_name="SOLMARA_NIA_RELAY_URL", - ), - ServiceTarget( - "SRO social relay", - subdomain("sro-relay"), - env_name="SOLMARA_SRO_RELAY_URL", - ), - ServiceTarget( - "Programme MIS relay", - subdomain("mosd-programme-relay"), - env_name="SOLMARA_PROGRAMME_RELAY_URL", - ), - ServiceTarget( - "SIPF pensions relay", - subdomain("sipf-relay"), - env_name="SOLMARA_SIPF_RELAY_URL", - ), - ServiceTarget( - "NAgDI agriculture relay", - subdomain("nagdi-relay"), - env_name="SOLMARA_NAGDI_RELAY_URL", - ), - ), - notaries=( - ServiceTarget( - "CRA Notary", - subdomain("cra-notary"), - health_path="/ready", - env_name="CRA_NOTARY_URL", - ), - ServiceTarget( - "NIA Notary", - subdomain("nia-notary"), - health_path="/ready", - env_name="NIA_NOTARY_URL", - ), - ServiceTarget( - "SRO Notary", - subdomain("sro-notary"), - health_path="/ready", - env_name="SRO_NOTARY_URL", - ), - ServiceTarget( - "Programme Notary", - subdomain("programme-notary"), - health_path="/ready", - env_name="PROGRAMME_NOTARY_URL", - ), - ServiceTarget( - "SIPF Notary", - subdomain("sipf-notary"), - health_path="/ready", - env_name="SIPF_NOTARY_URL", - ), - ServiceTarget( - "NAgDI notary", - subdomain("nagdi-notary"), - health_path="/ready", - env_name="NAGDI_NOTARY_URL", - ), - ), - applications=( - ServiceTarget( - "child benefit evidence collector", - subdomain("child-benefit-federator"), - health_path="/health", - env_name="CHILD_BENEFIT_FEDERATOR_URL", - ), - ), - ) - - -def normalize_domain(domain: str) -> str: - clean = domain.strip() - if clean.startswith("http://"): - clean = clean.removeprefix("http://") - if clean.startswith("https://"): - clean = clean.removeprefix("https://") - return clean.strip("/") - - -def hosted_env( - base_env: os._Environ[str] | dict[str, str], targets: HostedTargets -) -> dict[str, str]: - env = dict(base_env) - env.update( - { - "PORTAL_URL": targets.portal_url, - "SOLMARA_PORTAL_URL": targets.portal_url, - "STATIC_METADATA_URL": targets.metadata_url, - "SOLMARA_ESIGNET_PUBLIC_BASE_URL": targets.esignet_url, - "SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL": targets.esignet_ui_url, - "SOLMARA_WALLET_URL": targets.wallet_url, - "SOLMARA_PORTAL_EXPECT_AUTH_REQUIRED": "1", - } - ) - for target in (*targets.relays, *targets.notaries, *targets.applications): - if target.env_name: - env[target.env_name] = target.base_url - return env - - -def check_public_routes(targets: HostedTargets, timeout: float) -> None: - checks = [ - ServiceTarget("Visitor Center", targets.home_url, "/"), - ServiceTarget("portal", targets.portal_url, "/"), - ServiceTarget("Walt wallet", targets.wallet_url, "/"), - ServiceTarget("static metadata", targets.metadata_url, "/metadata/index.json"), - *targets.relays, - *targets.notaries, - *targets.applications, - ] - failures: list[str] = [] - for target in checks: - url = joined_url(target.base_url, target.health_path) - result = wait_for_http("GET", url, timeout=timeout) - if not result.ok: - failures.append(f"{target.name} at {url}: {result.detail}") - if failures: - raise SmokeFailure("; ".join(failures)) - - -def check_home_demo(home_url: str, timeout: float) -> None: - scenarios = request_json( - "GET", joined_url(home_url, "/api/scenarios"), timeout=timeout - ) - if scenarios.status != 200: - raise SmokeFailure(f"/api/scenarios returned {scenarios.detail}") - scenario_body = scenarios.body if isinstance(scenarios.body, dict) else {} - scenario_items = scenario_body.get("scenarios", []) - scenario_ids = {item.get("id") for item in scenario_items if isinstance(item, dict)} - missing = sorted(EXPECTED_SCENARIOS - scenario_ids) - if missing: - raise SmokeFailure(f"/api/scenarios missing {', '.join(missing)}") - - positive = request_json( - "POST", - joined_url( - home_url, "/api/scenarios/birth-to-child-benefit/steps/positive/run" - ), - body={}, - timeout=timeout, - ) - positive_result = result_payload(positive) - positive_status = nested(positive_result, "response_source", "status") - if positive.status != 200 or positive_status != 200: - raise SmokeFailure( - f"child positive step returned outer={positive.status}, inner={positive_status}" - ) - positive_body = nested(positive_result, "response_source", "body") - if not isinstance(positive_body, dict): - raise SmokeFailure("child positive step did not return a predicate bundle") - orchestration = positive_body.get("orchestration") - if ( - not isinstance(orchestration, dict) - or orchestration.get("service_id") != "child-benefit-federator" - ): - raise SmokeFailure( - "child positive step did not identify the evidence-collector application" - ) - if orchestration.get("decision") != "not_composed": - raise SmokeFailure("child positive step composed an eligibility decision") - results = positive_body.get("results") - claim_ids = ( - { - result.get("claim_id") - for result in results - if isinstance(result, dict) and isinstance(result.get("claim_id"), str) - } - if isinstance(results, list) - else set() - ) - if claim_ids != EXPECTED_CHILD_BENEFIT_CLAIMS: - raise SmokeFailure( - f"child positive step returned unexpected predicates, claim_ids={sorted(claim_ids)!r}" - ) - if "eligible-for-child-benefit" in claim_ids: - raise SmokeFailure("child positive step returned a composed eligibility claim") - attribution = ( - { - result.get("claim_id"): result.get("notary_service_id") - for result in results - if isinstance(result, dict) - and isinstance(result.get("claim_id"), str) - and isinstance(result.get("notary_service_id"), str) - } - if isinstance(results, list) - else {} - ) - if attribution != EXPECTED_CHILD_BENEFIT_ATTRIBUTION: - raise SmokeFailure( - f"child positive step returned incorrect authority attribution, attribution={attribution!r}" - ) - trace = positive_body.get("source_trace") - source_notaries = ( - { - item.get("service_id") - for item in trace - if isinstance(item, dict) and isinstance(item.get("service_id"), str) - } - if isinstance(trace, list) - else set() - ) - if source_notaries != EXPECTED_CHILD_BENEFIT_NOTARIES: - raise SmokeFailure( - f"child positive step returned an incomplete source trace, source_notaries={sorted(source_notaries)!r}" - ) - if "credential" in positive_result: - raise SmokeFailure("child positive step unexpectedly issued a credential") - - denial = request_json( - "POST", - joined_url( - home_url, "/api/scenarios/birth-to-child-benefit/steps/purpose-denial/run" - ), - body={}, - timeout=timeout, - ) - denial_result = result_payload(denial) - denial_status = nested(denial_result, "response_source", "status") - denial_code = nested(denial_result, "response_source", "body", "code") - if ( - denial.status != 200 - or not isinstance(denial_status, int) - or not 400 <= denial_status < 500 - ): - raise SmokeFailure( - f"child purpose-denial step returned outer={denial.status}, inner={denial_status}" - ) - if not denial_code: - raise SmokeFailure( - "child purpose-denial step did not return a stable problem code" - ) - - -def result_payload(response: "HttpResult") -> dict[str, Any]: - if response.status != 200 or not isinstance(response.body, dict): - raise SmokeFailure(f"scenario step returned {response.detail}") - result = response.body.get("result") - if not isinstance(result, dict): - raise SmokeFailure("scenario step response missing result object") - return result - - -@dataclass(frozen=True) -class HttpResult: - status: int | None - body: Any - error: str = "" - - @property - def ok(self) -> bool: - return self.status is not None and 200 <= self.status < 300 - - @property - def detail(self) -> str: - if self.status is not None: - return f"HTTP {self.status}" - return self.error or "no response" - - -def wait_for_http(method: str, url: str, timeout: float) -> HttpResult: - deadline = time.monotonic() + timeout - last = HttpResult(None, {}, "timeout") - while time.monotonic() < deadline: - last = request_json(method, url, timeout=min(8.0, max(1.0, timeout))) - if last.ok: - return last - time.sleep(1) - return last - - -def request_json( - method: str, url: str, body: Any | None = None, timeout: float = 8.0 -) -> HttpResult: - data = json.dumps(body).encode("utf-8") if body is not None else None - headers = {"Accept": "application/json", "User-Agent": "solmara-hosted-smoke/1.0"} - if data is not None: - headers["Content-Type"] = "application/json" - request = urllib.request.Request(url, data=data, headers=headers, method=method) - try: - with urllib.request.urlopen(request, timeout=timeout) as response: - return HttpResult(response.status, parse_body(response.read())) - except urllib.error.HTTPError as error: - body = parse_body(error.read()) - error.close() - return HttpResult(error.code, body) - except Exception as error: # noqa: BLE001 - return HttpResult(None, {}, error.__class__.__name__) - - -def parse_body(raw: bytes) -> Any: - if not raw: - return {} - text = raw.decode("utf-8", errors="replace") - try: - return json.loads(text) - except json.JSONDecodeError: - return text - - -def joined_url(base_url: str, path: str) -> str: - return urljoin(base_url.rstrip("/") + "/", path.lstrip("/")) - - -def nested(value: Any, *keys: str) -> Any: - current = value - for key in keys: - if not isinstance(current, dict): - return None - current = current.get(key) - return current - - -def run_command(command: list[str], env: dict[str, str], cwd: Path) -> None: - subprocess.run(command, cwd=cwd, env=env, check=True) - - -def truthy(value: str | None) -> bool: - return str(value or "").lower() in {"1", "true", "yes", "on"} - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/smoke-live.py b/scripts/smoke-live.py deleted file mode 100755 index 5926cad..0000000 --- a/scripts/smoke-live.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python3 -"""Smoke the running local Registry Evidence and Mint topology.""" - -from __future__ import annotations - -import importlib -import os -import shlex -import sys -import time -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from scenarios.common import http_json, joined_url # noqa: E402 - - -@dataclass(frozen=True) -class ScenarioCase: - name: str - module_name: str - step_id: str - expected_status: int | tuple[int, ...] | range - expected_values: dict[str, bool] - - -SCENARIO_CASES = ( - ScenarioCase( - "child eligible", - "child_benefit", - "positive", - 200, - { - "birth-is-registered": True, - "population-record-active": True, - "child-age-under-5": True, - "household-below-poverty-threshold": True, - "not-already-enrolled": True, - }, - ), - ScenarioCase("child deceased control", "child_benefit", "deceased-control", 200, {"child-age-under-5": False}), - ScenarioCase("child poverty control", "child_benefit", "poverty-control", 200, {"household-below-poverty-threshold": False}), - ScenarioCase("child unregistered control", "child_benefit", "unregistered-control", 200, {"birth-is-registered": False}), - ScenarioCase("child duplicate control", "child_benefit", "duplicate-control", 200, {"not-already-enrolled": False}), - ScenarioCase("child purpose denial", "child_benefit", "purpose-denial", range(400, 500), {}), - ScenarioCase( - "pension stop payment", - "pension_survivor", - "stop-payment", - 200, - {"person-is-deceased": True, "pension-payment-active": True}, - ), - ScenarioCase("pension stale death control", "pension_survivor", "stale-control", 200, {"person-is-deceased": False}), - ScenarioCase("pension survivor benefit", "pension_survivor", "survivor-benefit", 200, {"survivor-is-eligible": True}), - ScenarioCase("pension dissolved marriage control", "pension_survivor", "dissolved-control", 200, {"survivor-is-eligible": False}), - ScenarioCase("pension over-disclosure denial", "pension_survivor", "cause-of-death-denial", range(400, 500), {}), - ScenarioCase("farmer voucher eligible", "farmer_voucher", "positive", 200, {"eligible-for-climate-smart-input-voucher": True}), - ScenarioCase("farmer authorization control", "farmer_voucher", "authorization-control", 200, {"eligible-for-climate-smart-input-voucher": False}), - ScenarioCase("farmer redeemed control", "farmer_voucher", "redeemed-control", 200, {"eligible-for-climate-smart-input-voucher": False}), - ScenarioCase("livestock movement eligible", "farmer_voucher", "movement-permit", 200, {"eligible-for-livestock-movement-permit": True}), - ScenarioCase("livestock purpose denial", "farmer_voucher", "purpose-denial", range(400, 500), {}), - ScenarioCase( - "citizen self-service", - "citizen", - "positive", - 200, - {"citizen-population-record-active": True, "civil-record-linked": True}, - ), - ScenarioCase("citizen purpose denial", "citizen", "purpose-denial", range(400, 500), {}), -) - - -def main() -> int: - load_dotenv(ROOT / ".env") - failures = check_runtime() - for case in SCENARIO_CASES: - failures.extend(check_case(case)) - - if failures: - for failure in failures: - print(f"smoke-live: {failure}", file=sys.stderr) - return 1 - - print(f"smoke-live: Mint, Evidence, and {len(SCENARIO_CASES)} scenario checks passed") - return 0 - - -def load_dotenv(path: Path) -> None: - if not path.exists(): - return - for raw_line in path.read_text().splitlines(): - line = raw_line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, raw_value = line.split("=", 1) - key = key.strip() - if not key or key in os.environ: - continue - parts = shlex.split(raw_value, posix=True) - os.environ[key] = parts[0] if parts else "" - - -def check_runtime() -> list[str]: - evidence_url = os.environ.get("SOLMARA_EVIDENCE_URL", "https://localhost:4341") - failures: list[str] = [] - readiness = wait_for_readiness(evidence_url) - if readiness is not None: - failures.append(readiness) - - metadata = http_json( - "GET", - joined_url(evidence_url, "/.well-known/oauth-authorization-server"), - {"Accept": "application/json"}, - timeout=5.0, - ) - if metadata.status != 200: - failures.append(f"Mint metadata returned {metadata.status or metadata.error}; body={compact_body(metadata.body)}") - elif not isinstance(metadata.body, dict) or metadata.body.get("issuer") != "https://mint.evidence.solmara.invalid": - failures.append(f"Mint metadata exposed an unexpected issuer; body={compact_body(metadata.body)}") - return failures - - -def wait_for_readiness(evidence_url: str) -> str | None: - url = joined_url(evidence_url, "/ready") - deadline = time.monotonic() + float(os.environ.get("SOLMARA_SMOKE_READY_TIMEOUT_SECONDS", "90")) - last_status: int | None = None - last_error = "" - while time.monotonic() < deadline: - result = http_json("GET", url, {}, timeout=2.0) - last_status = result.status - last_error = result.error - if result.status in {200, 204}: - return None - time.sleep(1) - detail = f"status {last_status}" if last_status is not None else last_error or "no response" - return f"Evidence /ready did not become ready at {url} ({detail})" - - -def check_case(case: ScenarioCase) -> list[str]: - module = importlib.import_module(f"scenarios.{case.module_name}") - result = module.run_step({}, case.step_id) - response = result.get("response_source", {}) - status = response.get("status") - if not status_matches(status, case.expected_status): - return [ - f"{case.name}: expected HTTP {format_expected(case.expected_status)}, got {status}; " - f"body={compact_body(response.get('body'))}" - ] - if not case.expected_values: - return [] - - values = claim_values(response.get("body", {})) - return [ - f"{case.name}: expected {claim_id}={expected}, got {values.get(claim_id)}; " - f"body={compact_body(response.get('body'))}" - for claim_id, expected in case.expected_values.items() - if values.get(claim_id) is not expected - ] - - -def status_matches(status: Any, expected: int | tuple[int, ...] | range) -> bool: - if not isinstance(status, int): - return False - return status == expected if isinstance(expected, int) else status in expected - - -def format_expected(expected: int | tuple[int, ...] | range) -> str: - if isinstance(expected, range): - return f"{expected.start}-{expected.stop - 1}" - if isinstance(expected, tuple): - return ",".join(str(item) for item in expected) - return str(expected) - - -def claim_values(body: Any) -> dict[str, bool | None]: - if not isinstance(body, dict): - return {} - results = body.get("results", []) - if not isinstance(results, list): - return {} - return { - item["claim_id"]: item.get("value") if isinstance(item.get("value"), bool) else item.get("satisfied") - for item in results - if isinstance(item, dict) and isinstance(item.get("claim_id"), str) - } - - -def compact_body(body: Any) -> str: - text = str(body) - return text if len(text) <= 500 else text[:497] + "..." - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/smoke-nia-attribute-release.py b/scripts/smoke-nia-attribute-release.py deleted file mode 100755 index cbc2cd6..0000000 --- a/scripts/smoke-nia-attribute-release.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -"""Prove the NIA eSignet attribute-release profile against the live Relay.""" - -from __future__ import annotations - -import json -import os -import sys -import urllib.error -import urllib.request -from typing import Any - - -PURPOSE = "https://id.registrystack.org/solmara/purpose/esignet-identity-verification" -PROFILE_PATH = ( - "/v1/attribute-releases/solmara-nia-userinfo/versions/v1/resolve" -) -SUBJECT = "2300018263" - - -def main() -> int: - token = os.environ.get("NIA_ESIGNET_RELAY_TOKEN", "") - if not token: - print( - "smoke-nia-attribute-release: missing NIA_ESIGNET_RELAY_TOKEN", - file=sys.stderr, - ) - return 1 - - base_url = os.environ.get("SOLMARA_NIA_RELAY_URL", "http://127.0.0.1:4312") - request = urllib.request.Request( - f"{base_url.rstrip('/')}{PROFILE_PATH}", - data=json.dumps( - {"subject": {"id_type": "national_id", "value": SUBJECT}} - ).encode("utf-8"), - headers={ - "Accept": "application/json", - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - "Data-Purpose": PURPOSE, - }, - method="POST", - ) - - try: - with urllib.request.urlopen(request, timeout=8.0) as response: - status = response.status - body = parse_json(response.read()) - except urllib.error.HTTPError as error: - status = error.code - body = parse_json(error.read()) - except Exception as error: - print( - "smoke-nia-attribute-release: request failed " - f"({error.__class__.__name__})", - file=sys.stderr, - ) - return 1 - - failure = validate_response(status, body) - if failure: - print(f"smoke-nia-attribute-release: {failure}", file=sys.stderr) - return 1 - - print( - "smoke-nia-attribute-release: governed eSignet identity resolution passed" - ) - return 0 - - -def parse_json(raw: bytes) -> Any: - try: - return json.loads(raw.decode("utf-8")) - except (UnicodeDecodeError, json.JSONDecodeError): - return {} - - -def validate_response(status: int, body: Any) -> str | None: - if status != 200: - code = body.get("code") if isinstance(body, dict) else None - return f"expected HTTP 200, got {status}" + (f" {code}" if code else "") - if not isinstance(body, dict): - return "response was not a JSON object" - if body.get("profile_id") != "solmara-nia-userinfo": - return "response profile_id did not match" - if body.get("profile_version") != "v1": - return "response profile_version did not match" - claims = body.get("claims") - if not isinstance(claims, dict): - return "response omitted the minimized claims object" - if claims.get("individual_id") != SUBJECT or claims.get("name") != "Elena Dela Cruz": - return "response claims did not match the synthetic eSignet subject" - if "source" in body: - return "response disclosed source metadata" - return None - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/smoke-programme-acceptance.py b/scripts/smoke-programme-acceptance.py new file mode 100755 index 0000000..ad40106 --- /dev/null +++ b/scripts/smoke-programme-acceptance.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +"""Run sanitized programme acceptance against the live Scenario Runner HTTP API.""" + +from __future__ import annotations + +import json +import os +import time +import urllib.error +import urllib.request +from collections import Counter +from dataclasses import dataclass +from typing import Any, Callable + + +RUNNER_URL = os.environ.get("SOLMARA_SCENARIO_RUNNER_URL", "http://127.0.0.1:8080") +FEDERATOR_URL = os.environ.get( + "SOLMARA_CHILD_BENEFIT_FEDERATOR_URL", + os.environ.get("CHILD_BENEFIT_FEDERATOR_URL", "http://child-benefit-federator:8080"), +) +READY_TIMEOUT_SECONDS = float(os.environ.get("SOLMARA_SMOKE_READY_TIMEOUT_SECONDS", "90")) + +CRA = "did:web:id.registrystack.org:solmara:authority:cra" +NIA = "did:web:id.registrystack.org:solmara:authority:nia" +SRO = "did:web:id.registrystack.org:solmara:authority:sro" +MOSD = "did:web:id.registrystack.org:solmara:authority:mosd-programme-mis" +SIPF = "did:web:id.registrystack.org:solmara:authority:sipf" +NAGDI = "did:web:id.registrystack.org:solmara:authority:nagdi" + +CHILD_CLAIMS = { + "birth-is-registered": CRA, + "child-age-under-5": CRA, + "population-record-active": NIA, + "household-below-poverty-threshold": SRO, + "not-already-enrolled": MOSD, +} +PENSION_CLAIMS = { + "person-is-deceased": CRA, + "pension-payment-active": SIPF, +} +SURVIVOR_CLAIMS = {"survivor-is-eligible": SIPF} +VOUCHER_CLAIMS = { + "farmer-registered": NAGDI, + "data-use-authorized-for-purpose": NAGDI, + "eligible-for-climate-smart-input-voucher": NAGDI, +} +LIVESTOCK_CLAIMS = { + "registered-herd": NAGDI, + "origin-district-not-quarantined-for-species": NAGDI, + "eligible-for-livestock-movement-permit": NAGDI, +} + + +@dataclass(frozen=True) +class HttpResult: + status: int | None + body: Any + + +@dataclass(frozen=True) +class Check: + label: str + run: Callable[[], bool] + + +def joined_url(base: str, path: str) -> str: + return f"{base.rstrip('/')}/{path.lstrip('/')}" + + +def request_json(method: str, url: str, body: Any | None = None, timeout: float = 30.0) -> HttpResult: + data = json.dumps(body, separators=(",", ":")).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + request = urllib.request.Request(url, data=data, headers=headers, method=method) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + return HttpResult(response.status, parse_json(response.read())) + except urllib.error.HTTPError as error: + try: + return HttpResult(error.code, parse_json(error.read())) + finally: + error.close() + except Exception: + return HttpResult(None, {}) + + +def parse_json(raw: bytes) -> Any: + if not raw: + return {} + try: + return json.loads(raw.decode("utf-8"), object_pairs_hook=closed_object) + except (UnicodeDecodeError, json.JSONDecodeError, ValueError): + return {} + + +def closed_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ValueError("duplicate JSON member") + result[key] = value + return result + + +def wait_for_runner() -> bool: + deadline = time.monotonic() + READY_TIMEOUT_SECONDS + while time.monotonic() < deadline: + result = request_json("GET", joined_url(RUNNER_URL, "/health"), timeout=2.0) + if result.status == 200 and result.body == {"service": "scenario-runner", "status": "ok"}: + return True + time.sleep(1) + return False + + +def run_scenario(scenario: str, step: str) -> dict[str, Any] | None: + result = request_json( + "POST", + joined_url(RUNNER_URL, f"/v1/scenarios/{scenario}/steps/{step}/run"), + {}, + ) + if result.status != 200 or not isinstance(result.body, dict): + return None + if result.body.get("scenario_id") != scenario: + return None + payload = result.body.get("result") + return payload if isinstance(payload, dict) else None + + +def verified_claims( + payload: dict[str, Any] | None, + expected_claims: dict[str, str], + expected_services: dict[str, tuple[str, str]], +) -> bool: + if payload is None or payload.get("response_source") != {"status": 200, "code": "ok"}: + return False + results = payload.get("results") + traces = payload.get("source_trace") + presentations = payload.get("presentations") + if not isinstance(results, list) or not isinstance(traces, list) or not isinstance(presentations, list): + return False + if len(results) != len(expected_claims) or len(traces) != len(expected_services): + return False + + actual_claims: dict[str, str] = {} + expected_identity_by_issuer = { + issuer: source for issuer, source in expected_services.values() + } + for item in results: + if not isinstance(item, dict) or item.get("satisfied") is not True or item.get("value") is not True: + return False + claim = item.get("claim_id") + presentation = item.get("presentation") + if not isinstance(claim, str) or not isinstance(presentation, dict): + return False + issuer = presentation.get("issuer") + if ( + set(presentation) != {"authority", "issuer", "provider", "source"} + or not all(isinstance(presentation.get(key), str) for key in presentation) + or not isinstance(issuer, str) + or presentation.get("source") != expected_identity_by_issuer.get(issuer) + or claim in actual_claims + ): + return False + actual_claims[claim] = issuer + if actual_claims != expected_claims: + return False + + actual_presentation_issuers: list[str] = [] + for presentation in presentations: + if ( + not isinstance(presentation, dict) + or set(presentation) != {"authority", "issuer", "provider", "source"} + or not all(isinstance(presentation.get(key), str) for key in presentation) + or presentation.get("source") + != expected_identity_by_issuer.get(presentation.get("issuer")) + ): + return False + actual_presentation_issuers.append(presentation["issuer"]) + if Counter(actual_presentation_issuers) != Counter( + issuer for issuer, _ in expected_services.values() + ): + return False + + actual_services: set[str] = set() + trace_issuers: set[str] = set() + for trace in traces: + if ( + not isinstance(trace, dict) + or set(trace) + != {"authority", "service_id", "issuer", "provider", "source", "status"} + or not all( + isinstance(trace.get(key), str) + for key in ("authority", "service_id", "issuer", "provider", "source") + ) + or trace.get("status") != 200 + ): + return False + service = trace.get("service_id") + if not isinstance(service, str) or service not in expected_services or service in actual_services: + return False + expected_identity = expected_services.get(service) + if expected_identity is None or (trace.get("issuer"), trace.get("source")) != expected_identity: + return False + actual_services.add(service) + trace_issuers.add(expected_identity[0]) + return actual_services == set(expected_services) and trace_issuers == set(expected_claims.values()) + + +def child_benefit_positive() -> bool: + return verified_claims( + run_scenario("birth-to-child-benefit", "positive"), + CHILD_CLAIMS, + { + "cra-evidence": (CRA, "immutable extract"), + "nia-evidence": (NIA, "immutable extract"), + "sro-evidence": (SRO, "immutable extract"), + "mosd-programme-evidence": (MOSD, "Relay lookup"), + }, + ) + + +def pension_stop() -> bool: + payload = run_scenario("death-to-pension-survivor", "stop-payment") + return bool( + verified_claims( + payload, + PENSION_CLAIMS, + { + "cra-evidence": (CRA, "Relay lookup"), + "sipf-evidence": (SIPF, "Relay lookup"), + }, + ) + and payload is not None + and payload.get("derived_decisions") + == {"pension-payment-should-stop": True, "owner": "pension-review-application"} + and excludes_cause_of_death(payload) + ) + + +def pension_survivor() -> bool: + payload = run_scenario("death-to-pension-survivor", "survivor-benefit") + return verified_claims( + payload, + SURVIVOR_CLAIMS, + {"sipf-evidence": (SIPF, "Relay lookup")}, + ) and excludes_cause_of_death(payload) + + +def agriculture_voucher() -> bool: + return verified_claims( + run_scenario("farmer-climate-smart-voucher", "positive"), + VOUCHER_CLAIMS, + {"nagdi-evidence": (NAGDI, "Relay lookup")}, + ) + + +def agriculture_livestock() -> bool: + return verified_claims( + run_scenario("farmer-climate-smart-voucher", "movement-permit"), + LIVESTOCK_CLAIMS, + {"nagdi-evidence": (NAGDI, "Relay lookup")}, + ) + + +def excludes_cause_of_death(payload: Any) -> bool: + serialized = json.dumps(payload, sort_keys=True).lower() + return not any( + marker in serialized + for marker in ("cause-of-death", "cause_of_death", "causeofdeath", "cause of death") + ) + + +def generic_scenario_refusal(scenario: str, step: str) -> bool: + payload = run_scenario(scenario, step) + if payload is None: + return False + response = payload.get("response_source") + return bool( + isinstance(response, dict) + and set(response) == {"status", "code"} + and isinstance(response.get("status"), int) + and 400 <= response["status"] < 500 + and response.get("code") == "request_refused" + and payload.get("results") == [] + and payload.get("presentations") == [] + ) + + +def application_unauthorized() -> bool: + result = request_json("GET", joined_url(FEDERATOR_URL, "/v1/claims"), timeout=8.0) + body = result.body + return bool( + result.status == 401 + and isinstance(body, dict) + and set(body) == {"type", "title", "status", "code", "detail"} + and body.get("status") == 401 + and body.get("code") == "authentication_required" + and body.get("type") + == "https://id.registrystack.org/problems/solmara/authentication_required" + ) + + +def checks() -> tuple[Check, ...]: + return ( + Check("runner-ready", wait_for_runner), + Check("child-benefit-four-authorities-five-concepts", child_benefit_positive), + Check("pension-cra-sipf-stop-decision", pension_stop), + Check("pension-survivor-minimized-assertion", pension_survivor), + Check("agriculture-voucher", agriculture_voucher), + Check("agriculture-livestock", agriculture_livestock), + Check( + "child-benefit-wrong-purpose-generic-refusal", + lambda: generic_scenario_refusal("birth-to-child-benefit", "purpose-denial"), + ), + Check( + "pension-unauthorized-requirement-generic-refusal", + lambda: generic_scenario_refusal("death-to-pension-survivor", "cause-of-death-denial"), + ), + Check( + "agriculture-wrong-purpose-generic-refusal", + lambda: generic_scenario_refusal("farmer-climate-smart-voucher", "purpose-denial"), + ), + Check("programme-application-unauthorized-generic-refusal", application_unauthorized), + ) + + +def main() -> int: + failed = False + for check in checks(): + try: + passed = check.run() + except Exception: + passed = False + print(f"programme-acceptance: {'PASS' if passed else 'FAIL'} {check.label}") + failed = failed or not passed + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/smoke-published-tokens.py b/scripts/smoke-published-tokens.py deleted file mode 100755 index ce121fa..0000000 --- a/scripts/smoke-published-tokens.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python3 -"""Off-script skeptic smoke (spec DoD 9). - -Use the published demo tokens exactly as the engineer door hands them out and -confirm the two off-script attempts a skeptic would make get clean refusals with -stable problem codes: - -1. A wrong-purpose evaluation returns 403 with `pdp.purpose_not_permitted`. -2. A raw-row read attempt (disclosure="raw") is refused with a stable code and - never returns a 2xx that could leak a source row. - -Message text is never asserted; only stable codes and status ranges. -""" - -from __future__ import annotations - -import os -import shlex -import sys -from pathlib import Path -from typing import Any - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from scenarios.common import ( # noqa: E402 - PURPOSES, - auth_headers, - evaluation_body, - http_json, - joined_url, -) - -# The published demo tokens the engineer door exposes: name -> (url env, token env). -CHILD_URL_ENV = "CHILD_BENEFIT_FEDERATOR_URL" -CHILD_URL_DEFAULT = "http://127.0.0.1:4321" -CHILD_TOKEN_ENV = "CHILD_BENEFIT_FEDERATOR_TOKEN" - -POSITIVE_SUBJECT = "2300010248" -CLAIM_IDS = ["birth-is-registered"] - - -def problem_code(body: Any) -> str: - if not isinstance(body, dict): - return "" - code = body.get("code") - if isinstance(code, str) and code: - return code - type_uri = body.get("type") - if isinstance(type_uri, str) and "/" in type_uri: - return type_uri.rstrip("/").rsplit("/", 1)[-1] - return "" - - -def main() -> int: - load_dotenv(ROOT / ".env") - token = os.environ.get(CHILD_TOKEN_ENV) - if not token: - print(f"smoke-published-tokens: missing {CHILD_TOKEN_ENV}; run `just generate` before live smoke", file=sys.stderr) - return 1 - - base_url = os.environ.get(CHILD_URL_ENV, CHILD_URL_DEFAULT) - eval_url = joined_url(base_url, "/v1/evaluations") - failures: list[str] = [] - - # 1. Wrong purpose: ask the child-benefit application under a pension purpose. - wrong_headers = auth_headers(token, PURPOSES["pension_payment"], "application/json") - wrong_body = evaluation_body(POSITIVE_SUBJECT, CLAIM_IDS, scheme="solmara_uin", format="application/json") - wrong = http_json("POST", eval_url, wrong_headers, wrong_body, timeout=8.0) - wrong_code = problem_code(wrong.body) - if wrong.status != 403: - failures.append(f"wrong-purpose: expected HTTP 403, got {wrong.status}") - if wrong_code != "pdp.purpose_not_permitted": - failures.append(f"wrong-purpose: expected code pdp.purpose_not_permitted, got '{wrong_code}'") - - # 2. Raw-row read attempt: ask for the raw source row under a permitted purpose. - raw_headers = auth_headers(token, PURPOSES["child_benefit"], "application/json") - raw_body = evaluation_body( - POSITIVE_SUBJECT, CLAIM_IDS, scheme="solmara_uin", disclosure="raw", format="application/json" - ) - raw = http_json("POST", eval_url, raw_headers, raw_body, timeout=8.0) - raw_code = problem_code(raw.body) - if not (isinstance(raw.status, int) and 400 <= raw.status < 500): - failures.append(f"raw-row attempt: expected a 4xx refusal, got {raw.status}") - if raw_code != "pdp.purpose_not_permitted": - failures.append( - "raw-row attempt: expected code pdp.purpose_not_permitted, " - f"got '{raw_code}'" - ) - - if failures: - for failure in failures: - print(f"smoke-published-tokens: {failure}", file=sys.stderr) - return 1 - - print( - "smoke-published-tokens: published-token wrong-purpose (403 " - f"{wrong_code}) and raw-row attempt ({raw.status} {raw_code}) both refused cleanly" - ) - return 0 - - -def load_dotenv(path: Path) -> None: - if not path.exists(): - return - for raw_line in path.read_text().splitlines(): - line = raw_line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, raw_value = line.split("=", 1) - key = key.strip() - if not key or key in os.environ: - continue - if raw_value == "": - os.environ[key] = "" - continue - parts = shlex.split(raw_value, posix=True) - os.environ[key] = parts[0] if parts else "" - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/smoke-relay-sources.py b/scripts/smoke-relay-sources.py deleted file mode 100755 index b3e7fe8..0000000 --- a/scripts/smoke-relay-sources.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 -"""Probe Relay readiness and the unauthenticated Records API boundary. - -Registry Evidence reaches these APIs with short-lived workload credentials. -This host-side probe deliberately has no token: it proves that all six source -Relays are ready and refuse unauthenticated record reads with the stable -missing-credential problem. -""" - -from __future__ import annotations - -import json -import os -import sys -import time -import urllib.error -import urllib.parse -import urllib.request -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -ROOT = Path(__file__).resolve().parents[1] -OUTPUT = ROOT / "output" / "smoke" / "relay-sources.json" -HttpResult = tuple[int | None, dict[str, str], Any, str] -RelayReadiness = tuple[HttpResult, HttpResult] - - -@dataclass(frozen=True) -class RelayBoundary: - name: str - base_url_env: str - default_base_url: str - dataset: str - entity: str - - -RELAYS = ( - RelayBoundary( - "CRA Relay", - "SOLMARA_CRA_RELAY_URL", - "http://127.0.0.1:4311", - "civil_people", - "civil_people", - ), - RelayBoundary( - "NIA Relay", - "SOLMARA_NIA_RELAY_URL", - "http://127.0.0.1:4312", - "population", - "population", - ), - RelayBoundary( - "SRO Relay", - "SOLMARA_SRO_RELAY_URL", - "http://127.0.0.1:4313", - "sro_child", - "sro_child", - ), - RelayBoundary( - "Programme Relay", - "SOLMARA_PROGRAMME_RELAY_URL", - "http://127.0.0.1:4314", - "mosd_enroll", - "mosd_enroll", - ), - RelayBoundary( - "SIPF Relay", - "SOLMARA_SIPF_RELAY_URL", - "http://127.0.0.1:4315", - "pension", - "pension", - ), - RelayBoundary( - "NAgDI Relay", - "SOLMARA_NAGDI_RELAY_URL", - "http://127.0.0.1:4316", - "nagdi_farmer", - "nagdi_farmer", - ), -) - - -def main() -> int: - OUTPUT.parent.mkdir(parents=True, exist_ok=True) - failures: list[str] = [] - results: list[dict[str, Any]] = [] - readiness = wait_for_relays() - - for relay in RELAYS: - result = run_probe(relay, *readiness[relay.name]) - results.append(result) - if result["status"] != "ok": - failures.append( - f"{relay.name}: {result['status']} ({result.get('detail', 'no detail')})" - ) - - OUTPUT.write_text( - json.dumps(results, indent=2, sort_keys=True) + "\n", encoding="utf-8" - ) - if failures: - for failure in failures: - print(f"smoke-relay-sources: {failure}", file=sys.stderr) - print( - f"smoke-relay-sources: wrote {OUTPUT.relative_to(ROOT)}", file=sys.stderr - ) - return 1 - - print( - f"smoke-relay-sources: {len(RELAYS)} public Relay readiness and authentication-boundary checks passed; " - f"wrote {OUTPUT.relative_to(ROOT)}" - ) - return 0 - - -def wait_for_relays() -> dict[str, RelayReadiness]: - deadline = time.monotonic() + float( - os.environ.get("SOLMARA_SMOKE_READY_TIMEOUT_SECONDS", "90") - ) - latest: dict[str, RelayReadiness] = {} - while True: - for relay in RELAYS: - base_url = os.environ.get(relay.base_url_env, relay.default_base_url) - latest[relay.name] = ( - http_get(joined_url(base_url, "/healthz")), - http_get(joined_url(base_url, "/ready")), - ) - if all( - health[0] == 200 and ready[0] == 200 - for health, ready in latest.values() - ) or time.monotonic() >= deadline: - return latest - time.sleep(1) - - -def run_probe( - relay: RelayBoundary, - health: HttpResult, - ready: HttpResult, -) -> dict[str, Any]: - base_url = os.environ.get(relay.base_url_env, relay.default_base_url) - if health[0] != 200: - return { - "name": relay.name, - "status": "health_unavailable", - "detail": status_detail(health), - } - - if ready[0] != 200: - return { - "name": relay.name, - "status": "not_ready", - "detail": status_detail(ready), - } - - dataset = urllib.parse.quote(relay.dataset, safe=".-_") - entity = urllib.parse.quote(relay.entity, safe=".-_") - denial = http_get( - joined_url(base_url, f"/v1/datasets/{dataset}/entities/{entity}/records") - ) - denial_failure = validate_unauthenticated_denial(denial) - if denial_failure: - return { - "name": relay.name, - "status": "records_boundary_failed", - "detail": denial_failure, - } - - return { - "name": relay.name, - "status": "ok", - "liveness_status": health[0], - "readiness_status": ready[0], - "unauthenticated_records_status": denial[0], - "unauthenticated_records_code": "auth.missing_credential", - } - - -def http_get(url: str) -> HttpResult: - request = urllib.request.Request( - url, headers={"Accept": "application/json"}, method="GET" - ) - try: - with urllib.request.urlopen(request, timeout=5.0) as response: - return ( - response.status, - {key.lower(): value for key, value in response.headers.items()}, - parse_json(response.read()), - "", - ) - except urllib.error.HTTPError as error: - return ( - error.code, - {key.lower(): value for key, value in error.headers.items()}, - parse_json(error.read()), - "", - ) - except Exception as error: - return None, {}, {}, error.__class__.__name__ - - -def validate_unauthenticated_denial( - response: HttpResult, -) -> str | None: - status, headers, body, error = response - if status != 401: - return f"expected HTTP 401, got {status or error}" - content_type = headers.get("content-type", "").split(";", 1)[0].strip().lower() - if content_type != "application/problem+json": - return f"expected application/problem+json, got {content_type or 'no content type'}" - code = body.get("code") if isinstance(body, dict) else None - if code != "auth.missing_credential": - return f"expected auth.missing_credential, got {code!r}" - if isinstance(body, dict) and any( - key in body for key in ("data", "outputs", "results", "source_record") - ): - return "unauthenticated denial included source-shaped data" - return None - - -def joined_url(base_url: str, path: str) -> str: - return f"{base_url.rstrip('/')}/{path.lstrip('/')}" - - -def parse_json(raw: bytes) -> Any: - if not raw: - return {} - try: - return json.loads(raw.decode("utf-8")) - except (UnicodeDecodeError, json.JSONDecodeError): - return {"unparsed": raw.decode("utf-8", errors="replace")[:200]} - - -def status_detail(response: HttpResult) -> str: - status, _, body, error = response - if status is None: - return error or "no response" - code = body.get("code") if isinstance(body, dict) else None - return f"HTTP {status}" + (f" {code}" if code else "") - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/smoke-story-previews.py b/scripts/smoke-story-previews.py deleted file mode 100755 index 118375f..0000000 --- a/scripts/smoke-story-previews.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python3 -"""Render every guided story request without sending network calls.""" - -from __future__ import annotations - -import importlib -import json -import sys -from pathlib import Path -from typing import Any - -ROOT = Path(__file__).resolve().parents[1] -SCENARIOS = [ - "scenarios.child_benefit", - "scenarios.pension_survivor", - "scenarios.farmer_voucher", - "scenarios.citizen", -] - - -def validate_request(module_name: str, step_id: str, request: dict[str, Any]) -> None: - method = request.get("method") - url = request.get("url") - headers = request.get("headers") - if method == "MULTI": - if url != "solmara://registry-evidence": - raise ValueError( - f"{module_name}:{step_id}: expected Evidence collection URL, got {url!r}" - ) - if not isinstance(request.get("purpose"), str): - raise ValueError(f"{module_name}:{step_id}: missing Evidence purpose") - requests = request.get("requests") - if not isinstance(requests, list) or not requests: - raise ValueError( - f"{module_name}:{step_id}: Evidence collection has no requests" - ) - for index, authority_request in enumerate(requests): - if not isinstance(authority_request, dict): - raise ValueError( - f"{module_name}:{step_id}: Evidence request {index} is invalid" - ) - validate_request( - module_name, - f"{step_id}/authority-{index + 1}", - authority_request, - ) - return - if method not in {"GET", "POST"}: - raise ValueError(f"{module_name}:{step_id}: invalid method {method!r}") - if not isinstance(url, str) or not ( - url.startswith("http://127.0.0.1:") - or url.startswith("https://localhost:") - or url.startswith("https://evidence.solmara.invalid/") - ): - raise ValueError(f"{module_name}:{step_id}: expected local Evidence URL, got {url!r}") - if not isinstance(headers, dict): - raise ValueError(f"{module_name}:{step_id}: missing headers") - if "Data-Purpose" not in headers and "Authorization" not in headers: - raise ValueError(f"{module_name}:{step_id}: missing application or Evidence authorization") - for header in ("Authorization", "x-api-key"): - if header in headers and "runtime token hidden" not in headers[header] and "runtime token missing" not in headers[header]: - raise ValueError(f"{module_name}:{step_id}: {header} header was not redacted") - if method == "POST" and "body" not in request: - raise ValueError(f"{module_name}:{step_id}: POST preview is missing a body") - - -def main() -> int: - output: dict[str, Any] = {} - for module_name in SCENARIOS: - module = importlib.import_module(module_name) - story = module.story() - previews = [] - for step in story["steps"]: - step_id = step["id"] - request = module.preview_step({}, step_id) - validate_request(module_name, step_id, request) - previews.append({"step_id": step_id, "request": request}) - output[story["id"]] = previews - - out_path = ROOT / "output" / "smoke" / "story-previews.json" - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(json.dumps(output, indent=2, sort_keys=True) + "\n", encoding="utf-8") - print(f"Wrote {out_path.relative_to(ROOT)}") - return 0 - - -if __name__ == "__main__": - sys.path.insert(0, str(ROOT)) - raise SystemExit(main()) diff --git a/scripts/smoke.sh b/scripts/smoke.sh index cee6868..25e387b 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -1,42 +1,30 @@ -#!/usr/bin/env bash -set -euo pipefail +#!/usr/bin/env sh +set -eu -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -mkdir -p "$root/output/smoke" +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +set -a +. "$root/versions.env" +. "$root/.env" +set +a -ran=0 -"$root/scripts/smoke-story-previews.py" -ran=1 +uv run --project "$root" "$root/scripts/check-signer-public-keys.py" -if [ "${SOLMARA_SMOKE_LIVE:-1}" != "0" ]; then - ran=1 - "$root/scripts/smoke-relay-sources.py" - compose=( - docker compose - --env-file "$root/versions.env" - --env-file "$root/.env" - -f "$root/compose.yaml" - ) - nia_esignet_relay_token=$( - "${compose[@]}" exec -T nia-workload-agent \ - cat /run/esignet-secrets/solmara-esignet-relay-token - ) - NIA_ESIGNET_RELAY_TOKEN="$nia_esignet_relay_token" \ - "$root/scripts/smoke-nia-attribute-release.py" - unset nia_esignet_relay_token - # The Mint private_key_jwt smoke needs cryptography from the locked environment. - uv run --locked --project "$root" "$root/scripts/smoke-live.py" - "$root/scripts/smoke-portal-compose.py" -fi +python3 - <<'PY' +import json +import os +import urllib.request -for script in "$root"/scripts/stories/*.sh; do - if [ -x "$script" ]; then - ran=1 - "$script" - fi -done +for name, url in { + "home": f"http://127.0.0.1:{os.getenv('SOLMARA_HOME_PORT', '4301')}/", + "portal": f"http://127.0.0.1:{os.getenv('SOLMARA_PORTAL_PORT', '4300')}/", +}.items(): + with urllib.request.urlopen(url, timeout=10) as response: + if response.status != 200: + raise SystemExit(f"{name} returned {response.status}") -if [ "$ran" -eq 0 ]; then - echo "No story smoke scripts are installed yet." >&2 - exit 1 -fi +for authority, port in {"cra": 4311, "nia": 4312, "mosd": 4314, "sipf": 4315, "nagdi": 4316}.items(): + with urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=10) as response: + body = json.load(response) + if body.get("status") != "ok": + raise SystemExit(f"{authority} Relay is not healthy") +PY diff --git a/scripts/start-esignet-relay.sh b/scripts/start-esignet-relay.sh index 49b3598..d8d2926 100755 --- a/scripts/start-esignet-relay.sh +++ b/scripts/start-esignet-relay.sh @@ -8,6 +8,19 @@ keystore_password="${REGISTRY_ESIGNET_KYC_SIGNING_KEYSTORE_PASSWORD:?missing REG key_alias="${REGISTRY_ESIGNET_KYC_SIGNING_KEY_ALIAS:?missing REGISTRY_ESIGNET_KYC_SIGNING_KEY_ALIAS}" key_password="${REGISTRY_ESIGNET_KYC_SIGNING_KEY_PASSWORD:?missing REGISTRY_ESIGNET_KYC_SIGNING_KEY_PASSWORD}" +if [[ -n "${REGISTRY_TLS_CA_CERT:-}" ]]; then + truststore=/tmp/registry-esignet-truststore.p12 + rm -f "$truststore" + keytool -importcert -noprompt \ + -alias registry-runtime-ca \ + -file "$REGISTRY_TLS_CA_CERT" \ + -keystore "$truststore" \ + -storetype PKCS12 \ + -storepass changeit \ + >/dev/null + export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS:-} -Djavax.net.ssl.trustStore=$truststore -Djavax.net.ssl.trustStorePassword=changeit" +fi + mkdir -p "$(dirname "$keystore_path")" if [[ -f "$keystore_path" ]] && ! keytool -list \ diff --git a/scripts/test_contract_generation_proof.py b/scripts/test_contract_generation_proof.py deleted file mode 100644 index 7395093..0000000 --- a/scripts/test_contract_generation_proof.py +++ /dev/null @@ -1,316 +0,0 @@ -from __future__ import annotations - -import contextlib -import importlib.util -import io -import os -import shutil -import subprocess -import sys -import tempfile -import unittest -from unittest import mock -from pathlib import Path - -import yaml - - -ROOT = Path(__file__).resolve().parents[1] - - -def load_proof(): - spec = importlib.util.spec_from_file_location( - "contract_generation_proof", ROOT / "scripts" / "contract-generation-proof.py" - ) - if spec is None or spec.loader is None: - raise RuntimeError("could not load contract-generation-proof.py") - module = importlib.util.module_from_spec(spec) - sys.modules["contract_generation_proof"] = module - spec.loader.exec_module(module) - return module - - -class ContractGenerationProofTests(unittest.TestCase): - def setUp(self) -> None: - self.proof = load_proof() - - def test_successor_is_a_revision_only_change(self) -> None: - with tempfile.TemporaryDirectory() as directory: - project = Path(directory) / "sro-social" - source = ROOT / "projects" / "sro-social" - shutil.copytree(source, project) - integration = ( - project - / "integrations" - / "child-benefit-household-by-uin" - / "integration.yaml" - ) - before = yaml.safe_load(integration.read_text(encoding="utf-8")) - self.proof.make_successor(project) - after = yaml.safe_load(integration.read_text(encoding="utf-8")) - self.assertEqual(before | {"revision": 2}, after) - - def test_proof_copy_is_readable_by_the_unprivileged_runtime(self) -> None: - with tempfile.TemporaryDirectory() as directory: - runtime = Path(directory) / "runtime" - nested = runtime / "artifacts" - nested.mkdir(parents=True, mode=0o700) - config = runtime / "relay.yaml" - config.write_text("server: {}\n", encoding="utf-8") - config.chmod(0o600) - artifact = nested / "contract.json" - artifact.write_text("{}\n", encoding="utf-8") - artifact.chmod(0o600) - - self.proof.make_runtime_readable(runtime) - - self.assertEqual(runtime.stat().st_mode & 0o777, 0o755) - self.assertEqual(nested.stat().st_mode & 0o777, 0o755) - self.assertEqual(config.stat().st_mode & 0o777, 0o644) - self.assertEqual(artifact.stat().st_mode & 0o777, 0o644) - - def test_proof_uses_the_digest_pinned_canonical_relay(self) -> None: - relay = "ghcr.io/registrystack/registry-relay@sha256:" + "a" * 64 - self.assertEqual( - self.proof.relay_image({"REGISTRY_RELAY_IMAGE": relay}), - relay, - ) - - def test_proof_rejects_a_mutable_canonical_relay_reference(self) -> None: - with self.assertRaisesRegex( - self.proof.ProofFailure, - "must pin the canonical Relay release by digest", - ): - self.proof.relay_image( - { - "REGISTRY_RELAY_IMAGE": "ghcr.io/registrystack/registry-relay:v0.15.2" - } - ) - - def test_mixed_override_replaces_only_notary_generation(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - generation = root / "green" - (generation / "relay").mkdir(parents=True) - (generation / "notary").mkdir() - override = root / "mixed.yaml" - self.proof.write_override(override, generation, relay=False, notary=True) - services = yaml.safe_load(override.read_text(encoding="utf-8"))["services"] - self.assertEqual(set(services), {"sro-notary", "sro-notary-state-install"}) - self.assertNotIn("sro-social-relay-consultation", services) - self.assertIn( - f"{generation / 'notary' / 'notary.yaml'}:/etc/registry-notary/notary.yaml:ro", - services["sro-notary"]["volumes"], - ) - - def test_mixed_generation_asserts_the_stable_notary_problem_code(self) -> None: - self.assertEqual( - self.proof.MIXED_GENERATION_PROBLEM_CODE, - "notary.relay.profile_mismatch", - ) - - def test_complete_override_replaces_private_consultation_generation(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - generation = root / "green" - (generation / "relay").mkdir(parents=True) - (generation / "notary").mkdir() - override = root / "green.yaml" - self.proof.write_override( - override, - generation, - relay=True, - notary=True, - ) - services = yaml.safe_load(override.read_text(encoding="utf-8"))[ - "services" - ] - self.assertIn("sro-relay-state-bootstrap", services) - self.assertIn("sro-social-relay-consultation", services) - self.assertNotIn("sro-social-relay", services) - - def test_success_response_must_be_minimized_and_subject_free(self) -> None: - response = { - "results": [ - { - "claim_id": self.proof.CLAIM_ID, - "disclosure": "predicate", - "satisfied": True, - } - ] - } - self.assertTrue( - self.proof.successful_evaluation(response, self.proof.BLUE_SUBJECT) - ) - response["debug"] = self.proof.BLUE_SUBJECT - self.assertFalse( - self.proof.successful_evaluation(response, self.proof.BLUE_SUBJECT) - ) - - def test_sensitive_scan_reports_category_without_value(self) -> None: - secret = b"synthetic-secret-value" - with tempfile.TemporaryDirectory() as directory: - artifact = Path(directory) / "captured.log" - artifact.write_bytes(b"prefix " + secret + b" suffix") - with self.assertRaises(self.proof.ProofFailure) as rejected: - self.proof.scan_paths([artifact], {"test credential": secret}) - message = str(rejected.exception) - self.assertIn("test credential", message) - self.assertNotIn(secret.decode("utf-8"), message) - - def test_failed_command_output_is_actionable_bounded_and_redacted(self) -> None: - secret = "runtime-secret-that-must-not-appear" - output_lines = "\n".join(f"diagnostic line {index}" for index in range(40)) - command = ( - 'printf "%s\\n" "explanation: registry image was unavailable" ' - '"$INHERITED_TOKEN" "2300027390"; ' - f'printf "%s\\n" "{output_lines}"; exit 7' - ) - with mock.patch.dict(os.environ, {"INHERITED_TOKEN": secret}, clear=False): - with self.assertRaises(self.proof.ProofFailure) as rejected: - self.proof.run(["/bin/sh", "-c", command]) - message = str(rejected.exception) - self.assertIn("explanation: registry image was unavailable", message) - self.assertIn("command output (redacted and bounded)", message) - self.assertNotIn(secret, message) - self.assertNotIn(self.proof.BLUE_SUBJECT, message) - diagnostic = message.split("command output (redacted and bounded):\n", 1)[1] - self.assertLessEqual( - len(diagnostic.splitlines()), self.proof.MAX_DIAGNOSTIC_LINES - ) - self.assertLessEqual( - len(diagnostic.encode("utf-8")), self.proof.MAX_DIAGNOSTIC_BYTES - ) - - def test_failed_generation_reports_redacted_bootstrap_logs(self) -> None: - secret = "bootstrap-secret-that-must-not-appear" - compose = ["docker", "compose", "--project-name", "proof"] - with mock.patch.object( - self.proof, - "run", - side_effect=[ - subprocess.CompletedProcess(compose, 1, "service failed"), - subprocess.CompletedProcess( - compose, - 0, - f"sro-relay-state-bootstrap | migration rejected {secret}", - ), - ], - ) as mocked_run: - with self.assertRaises(self.proof.ProofFailure) as rejected: - self.proof.start_generation( - compose, - {"REGISTRY_RELAY_STATE_MIGRATION_PASSWORD": secret}, - ) - message = str(rejected.exception) - self.assertIn("failed service logs", message) - self.assertIn("migration rejected", message) - self.assertNotIn(secret, message) - self.assertEqual(mocked_run.call_count, 2) - self.assertEqual( - mocked_run.call_args_list[1].args[0][-4:], - [ - "postgres", - "registry-postgresql-bootstrap", - "sro-relay-state-bootstrap", - "sro-social-relay-consultation", - ], - ) - - def test_cleanup_failures_are_fatal_without_a_primary_failure(self) -> None: - failures = ( - lambda: subprocess.CompletedProcess(["docker"], 1, "cleanup rejected"), - lambda: (_ for _ in ()).throw( - subprocess.TimeoutExpired(["docker"], timeout=1) - ), - lambda: (_ for _ in ()).throw(OSError("unsafe operating-system detail")), - ) - for cleanup in failures: - with self.subTest(cleanup=cleanup): - with self.assertRaises(self.proof.ProofFailure): - self.proof.preserve_cleanup_failure( - cleanup, - environment={}, - primary_failure_active=False, - ) - - def test_cleanup_failure_does_not_replace_primary_and_is_safely_reported(self) -> None: - secret = "cleanup-secret-that-must-not-appear" - stderr = io.StringIO() - with contextlib.redirect_stderr(stderr): - with self.assertRaisesRegex(self.proof.ProofFailure, "primary proof failure"): - try: - raise self.proof.ProofFailure("primary proof failure") - finally: - self.proof.preserve_cleanup_failure( - lambda: subprocess.CompletedProcess( - ["docker"], - 1, - f"cleanup explanation; credential={secret}; subject={self.proof.GREEN_SUBJECT}", - ), - environment={"CLEANUP_TOKEN": secret}, - primary_failure_active=sys.exc_info()[0] is not None, - ) - diagnostic = stderr.getvalue() - self.assertIn("secondary cleanup failure", diagnostic) - self.assertIn("cleanup explanation", diagnostic) - self.assertNotIn(secret, diagnostic) - self.assertNotIn(self.proof.GREEN_SUBJECT, diagnostic) - - def test_mixed_timeout_preserves_primary_when_emergency_cleanup_fails(self) -> None: - secret = "emergency-cleanup-secret" - cleanup_failures = ( - lambda: subprocess.CompletedProcess( - ["docker"], - 1, - f"remove failed; credential={secret}; subject={self.proof.BLUE_SUBJECT}", - ), - lambda: (_ for _ in ()).throw( - subprocess.TimeoutExpired(["docker", "rm"], timeout=1) - ), - ) - for emergency_cleanup in cleanup_failures: - with self.subTest(emergency_cleanup=emergency_cleanup): - stderr = io.StringIO() - with contextlib.redirect_stderr(stderr): - with self.assertRaisesRegex( - self.proof.ProofFailure, - "mixed-generation Notary unexpectedly kept serving", - ): - self.proof.raise_mixed_notary_timeout( - "bounded-mixed-notary", - subprocess.TimeoutExpired( - ["docker", "compose"], timeout=45 - ), - environment={"EMERGENCY_TOKEN": secret}, - emergency_cleanup=emergency_cleanup, - ) - diagnostic = stderr.getvalue() - self.assertIn("secondary cleanup failure", diagnostic) - self.assertNotIn(secret, diagnostic) - self.assertNotIn(self.proof.BLUE_SUBJECT, diagnostic) - - def test_workflows_use_the_pinned_compiler_and_live_proof(self) -> None: - ci = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") - candidate = (ROOT / ".github" / "workflows" / "release-candidate.yml").read_text( - encoding="utf-8" - ) - self.assertIn("just registry-projects-runtime-check", ci) - self.assertNotIn("just contract-generation-proof", ci) - self.assertIn("just registry-projects-runtime-check", candidate) - self.assertIn("just contract-generation-proof", candidate) - - def test_clean_checkout_journey_is_one_documented_target(self) -> None: - justfile = (ROOT / "justfile").read_text(encoding="utf-8") - readme = (ROOT / "README.md").read_text(encoding="utf-8") - self.assertRegex( - justfile, - r"up-generated:\n just generate\n just registry-projects-runtime-check\n just up", - ) - quick_start = readme.split("## Quick Start", 1)[1].split("```bash", 1)[1].split("```", 1)[0] - self.assertIn("just up-generated", quick_start) - self.assertNotIn("just contract-generation-proof", quick_start) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_gen_secrets.py b/scripts/test_gen_secrets.py index 34fc868..ae37f05 100644 --- a/scripts/test_gen_secrets.py +++ b/scripts/test_gen_secrets.py @@ -1,73 +1,102 @@ from __future__ import annotations -import hashlib import importlib.util -import sys +import json import tempfile import unittest from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] - - -def load_generator(): - compose_spec = importlib.util.spec_from_file_location( - "compose_project_name", ROOT / "scripts/compose_project_name.py" - ) - if compose_spec is None or compose_spec.loader is None: - raise RuntimeError("could not load compose_project_name.py") - compose_module = importlib.util.module_from_spec(compose_spec) - sys.modules["compose_project_name"] = compose_module - compose_spec.loader.exec_module(compose_module) - - spec = importlib.util.spec_from_file_location( - "solmara_gen_secrets", ROOT / "scripts/gen-secrets.py" - ) - if spec is None or spec.loader is None: - raise RuntimeError("could not load gen-secrets.py") - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -class EvidenceMaterialTest(unittest.TestCase): - def setUp(self) -> None: - self.generator = load_generator() - - def test_complete_material_is_preserved_across_regeneration(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - self.generator.EVIDENCE_LOCAL_DIR = root - self.generator.ensure_evidence_material() - first = { - path.relative_to(root): hashlib.sha256(path.read_bytes()).digest() - for path in root.rglob("*") - if path.is_file() - } - - self.generator.ensure_evidence_material() - second = { - path.relative_to(root): hashlib.sha256(path.read_bytes()).digest() - for path in root.rglob("*") - if path.is_file() - } - - self.assertEqual(first, second) - self.assertEqual(len(first), 11) - - def test_partial_material_fails_without_rotating_existing_key(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - existing = root / "evidence/audit-hmac-key" - existing.parent.mkdir(parents=True) - existing.write_text("existing-audit-key\n") - self.generator.EVIDENCE_LOCAL_DIR = root - - with self.assertRaisesRegex(SystemExit, "incomplete local Evidence material"): - self.generator.ensure_evidence_material() - - self.assertEqual(existing.read_text(), "existing-audit-key\n") +import sys +from unittest import mock + +SCRIPT = Path(__file__).with_name("gen-secrets.py") +sys.path.insert(0, str(SCRIPT.parent)) +SPEC = importlib.util.spec_from_file_location("gen_secrets", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class SecretGenerationTests(unittest.TestCase): + def test_p256_key_is_private_and_thumbprint_bound(self) -> None: + jwk = json.loads(MODULE.p256_jwk()) + self.assertEqual((jwk["kty"], jwk["crv"], jwk["alg"]), ("EC", "P-256", "ES256")) + self.assertIn("d", jwk) + self.assertEqual(len(jwk["kid"]), 43) + + def test_rsa_key_is_private_and_thumbprint_bound(self) -> None: + jwk = json.loads(MODULE.rsa_jwk()) + self.assertEqual((jwk["kty"], jwk["alg"]), ("RSA", "RS256")) + self.assertEqual(set(jwk), {"kty", "alg", "n", "e", "d", "p", "q", "dp", "dq", "qi", "kid"}) + self.assertEqual(len(jwk["kid"]), 43) + + def test_operator_material_is_create_only_and_per_cell(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + previous = MODULE.LOCAL + MODULE.LOCAL = Path(temporary) + try: + first = MODULE.ensure_operator_material() + signing = MODULE.LOCAL / "cells/cra/secrets/signing.jwk" + before = signing.read_bytes() + second = MODULE.ensure_operator_material() + self.assertEqual(signing.read_bytes(), before) + self.assertEqual(first, second) + self.assertTrue((MODULE.LOCAL / "cells/nagdi/secrets/subject-binding-hmac-key").exists()) + client_id = MODULE.LOCAL / "cells/cra/secrets/cra-pension-evidence-client-id" + self.assertEqual(client_id.read_bytes(), b"cra-pension-evidence") + self.assertEqual(client_id.stat().st_mode & 0o777, 0o600) + esignet_key = MODULE.LOCAL / "cells/mint/clients/nia-esignet-rsa-client-key" + self.assertEqual(json.loads(esignet_key.read_text())["kty"], "RSA") + self.assertEqual(esignet_key.stat().st_mode & 0o777, 0o600) + finally: + MODULE.LOCAL = previous + + def test_client_identifier_migrates_only_the_previous_trailing_newline(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + client_id = Path(temporary) / "client-id" + client_id.write_bytes(b"registered-client\n") + MODULE.ensure_client_identifier(client_id, "registered-client") + self.assertEqual(client_id.read_bytes(), b"registered-client") + + client_id.write_bytes(b"different-client") + with self.assertRaises(ValueError): + MODULE.ensure_client_identifier(client_id, "registered-client") + self.assertEqual(client_id.read_bytes(), b"different-client") + + def test_generated_environment_preserves_runtime_secrets(self) -> None: + existing = { + key: f"stable-{index}" + for index, key in enumerate(MODULE.RANDOM_ENV_KEYS) + } + existing["PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64"] = "stable-portal-key" + operator = { + "NIA_ESIGNET_CLIENT_PRIVATE_JWK": "operator-jwk", + "SOLMARA_EVIDENCE_CLIENT_KEY": "/operator/client-key", + } + with ( + mock.patch.object(MODULE, "raw_key", side_effect=AssertionError("rotated")), + mock.patch.object( + MODULE, + "rsa_private_key_b64", + side_effect=AssertionError("rotated"), + ), + ): + values = MODULE.compose_environment_values(existing, operator) + for key, value in existing.items(): + self.assertEqual(values[key], value) + self.assertEqual(values["NIA_ESIGNET_CLIENT_PRIVATE_JWK"], "operator-jwk") + + def test_generated_environment_rejects_duplicate_or_empty_values(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / ".env" + path.write_text("KEY=value\nKEY=other\n", encoding="utf-8") + with self.assertRaisesRegex(ValueError, "duplicate"): + MODULE.load_environment(path) + path.write_text("KEY=\n", encoding="utf-8") + with self.assertRaisesRegex(ValueError, "invalid"): + MODULE.load_environment(path) + path.write_text("KEY='unterminated\n", encoding="utf-8") + with self.assertRaisesRegex(ValueError, "malformed"): + MODULE.load_environment(path) if __name__ == "__main__": diff --git a/scripts/test_hosted_authority_rollout.py b/scripts/test_hosted_authority_rollout.py new file mode 100644 index 0000000..7ec038a --- /dev/null +++ b/scripts/test_hosted_authority_rollout.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import importlib.util +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("hosted-authority-rollout.py") +SPEC = importlib.util.spec_from_file_location("authority_rollout", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class HostedAuthorityRolloutTests(unittest.TestCase): + def test_every_phase_is_recoverable(self) -> None: + for phase in ("side-by-side", "switch", "disable"): + plan = MODULE.operation(phase) + self.assertEqual(plan["volumePolicy"], "retain") + self.assertEqual(plan["destructiveCommand"], "none") + + def test_unknown_phase_is_refused(self) -> None: + with self.assertRaises(ValueError): + MODULE.operation("delete") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_hosted_home_topology.py b/scripts/test_hosted_home_topology.py new file mode 100644 index 0000000..4d38497 --- /dev/null +++ b/scripts/test_hosted_home_topology.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import json +import unittest +from pathlib import Path +from urllib.parse import urlparse + +import yaml + + +ROOT = Path(__file__).parents[1] +COMPOSE_PATH = ROOT / "compose.coolify.yaml" + + +class HostedHomeTopologyTests(unittest.TestCase): + def test_home_build_context_exposes_only_public_generated_inputs(self) -> None: + patterns = (ROOT / ".dockerignore").read_text(encoding="utf-8").splitlines() + self.assertIn("!generator/output/**", patterns) + self.assertIn("output/*", patterns) + self.assertIn("!output/smoke", patterns) + self.assertIn("output/smoke/*", patterns) + self.assertIn("!output/smoke/.gitkeep", patterns) + self.assertNotIn("!output/**", patterns) + + def test_home_uses_the_declared_public_topology_without_secrets(self) -> None: + compose = yaml.safe_load(COMPOSE_PATH.read_text(encoding="utf-8")) + home = compose["services"]["home"] + environment = home["environment"] + + expected_origins = { + "PORTAL_URL": "https://portal.solmara.registrystack.org", + "STATIC_METADATA_URL": "https://metadata.solmara.registrystack.org", + "CRA_CIVIL_RELAY_URL": "https://cra-relay.solmara.registrystack.org", + "NIA_POPULATION_RELAY_URL": "https://nia-relay.solmara.registrystack.org", + "MOSD_PROGRAMME_RELAY_URL": "https://mosd-programme-relay.solmara.registrystack.org", + "SIPF_PENSIONS_RELAY_URL": "https://sipf-relay.solmara.registrystack.org", + "NAGDI_AGRICULTURE_RELAY_URL": "https://nagdi-relay.solmara.registrystack.org", + "SOLMARA_CRA_EVIDENCE_URL": "https://cra-evidence.solmara.registrystack.org", + "SOLMARA_NIA_EVIDENCE_URL": "https://nia-evidence.solmara.registrystack.org", + "SOLMARA_SRO_EVIDENCE_URL": "https://sro-evidence.solmara.registrystack.org", + "SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL": "https://mosd-programme-evidence.solmara.registrystack.org", + "SOLMARA_SIPF_EVIDENCE_URL": "https://sipf-evidence.solmara.registrystack.org", + "SOLMARA_NAGDI_EVIDENCE_URL": "https://nagdi-evidence.solmara.registrystack.org", + "MINT_URL": "https://mint.solmara.registrystack.org", + "CHILD_BENEFIT_FEDERATOR_URL": "https://child-benefit.solmara.registrystack.org", + "SCENARIO_RUNNER_URL": "https://scenarios.solmara.registrystack.org", + "PORTAL_PROBE_URL": "https://portal.solmara.registrystack.org", + } + self.assertEqual( + {key: environment.get(key) for key in expected_origins}, + expected_origins, + ) + + declared_hosts = set() + for path in ROOT.glob("compose.coolify*.yaml"): + hosted_compose = yaml.safe_load(path.read_text(encoding="utf-8")) + for service in hosted_compose.get("services", {}).values(): + host = service.get("labels", {}).get("solmara.lab.host") + if host: + declared_hosts.add(host) + self.assertTrue( + {urlparse(origin).hostname for origin in expected_origins.values()} + <= declared_hosts + ) + + public_url_map = json.loads(environment["SOLMARA_PUBLIC_URL_MAP"]) + self.assertEqual( + public_url_map, + { + "child-benefit-federator:8080": expected_origins["CHILD_BENEFIT_FEDERATOR_URL"], + "deterministic-publisher:8080": expected_origins["STATIC_METADATA_URL"], + "cra-evidence:8080": expected_origins["SOLMARA_CRA_EVIDENCE_URL"], + "nia-evidence:8080": expected_origins["SOLMARA_NIA_EVIDENCE_URL"], + "sro-evidence:8080": expected_origins["SOLMARA_SRO_EVIDENCE_URL"], + "mosd-programme-evidence:8080": expected_origins["SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL"], + "sipf-evidence:8080": expected_origins["SOLMARA_SIPF_EVIDENCE_URL"], + "nagdi-evidence:8080": expected_origins["SOLMARA_NAGDI_EVIDENCE_URL"], + "mint:8081": expected_origins["MINT_URL"], + "cra-relay:8080": expected_origins["CRA_CIVIL_RELAY_URL"], + "nia-relay:8080": expected_origins["NIA_POPULATION_RELAY_URL"], + "mosd-relay:8080": expected_origins["MOSD_PROGRAMME_RELAY_URL"], + "sipf-relay:8080": expected_origins["SIPF_PENSIONS_RELAY_URL"], + "nagdi-relay:8080": expected_origins["NAGDI_AGRICULTURE_RELAY_URL"], + "static-metadata:8080": expected_origins["STATIC_METADATA_URL"], + "scenario-runner:8080": expected_origins["SCENARIO_RUNNER_URL"], + "portal:4000": expected_origins["PORTAL_PROBE_URL"], + }, + ) + + self.assertNotIn("secrets", home) + self.assertNotIn("volumes", home) + self.assertFalse( + {"SOLMARA_EVIDENCE_CLIENT_KEY", "CHILD_BENEFIT_FEDERATOR_TOKEN"} + & environment.keys() + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py new file mode 100644 index 0000000..b9faab9 --- /dev/null +++ b/scripts/test_hosted_image_manifest.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +import contextlib +import importlib.util +import io +import os +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "hosted-image-manifest.py" +DIGESTS = { + "SOLMARA_EVIDENCE_IMAGE": "1" * 64, + "SOLMARA_MINT_IMAGE": "2" * 64, + "SOLMARA_AUTHORITY_PROVISIONER_IMAGE": "3" * 64, + "SOLMARA_TRANSIT_SIGNER_IMAGE": "4" * 64, + "SOLMARA_STATIC_METADATA_IMAGE": "5" * 64, + "SOLMARA_SCENARIO_RUNNER_IMAGE": "6" * 64, + "SOLMARA_HOME_IMAGE": "7" * 64, + "SOLMARA_PORTAL_IMAGE": "8" * 64, + "SOLMARA_ESIGNET_RELAY_IMAGE": "9" * 64, + "SOLMARA_ESIGNET_POSTGRES_IMAGE": "a" * 64, + "SOLMARA_ESIGNET_UI_IMAGE": "b" * 64, + "SOLMARA_ESIGNET_SEED_IMAGE": "c" * 64, +} + + +def load_module(): + spec = importlib.util.spec_from_file_location("hosted_image_manifest", SCRIPT) + if spec is None or spec.loader is None: + raise RuntimeError(f"could not load {SCRIPT}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class HostedImageManifestTests(unittest.TestCase): + def setUp(self) -> None: + self.module = load_module() + self.directory = tempfile.TemporaryDirectory() + self.root = Path(self.directory.name) + self.manifest = self.root / "release" / "solmara-hosted-images.env" + self.environment = { + key: self.module.expected_reference(key, digest) + for key, digest in DIGESTS.items() + } + core = [ + key + for key in self.module.EXPECTED_KEYS + if not key.startswith("SOLMARA_ESIGNET_") + ] + esignet = [ + key + for key in self.module.EXPECTED_KEYS + if key.startswith("SOLMARA_ESIGNET_") + ] + (self.root / "compose.hosted.yaml").write_text( + "services:\n" + + "".join( + f" {index}:\n image: ${{" + key + ":?required}\n" + for index, key in enumerate(core) + ), + encoding="utf-8", + ) + (self.root / "compose.coolify.esignet.yaml").write_text( + "services:\n" + + "".join( + f" {index}:\n image: ${{" + key + ":?required}\n" + for index, key in enumerate(esignet) + ), + encoding="utf-8", + ) + + def tearDown(self) -> None: + self.directory.cleanup() + + def invoke(self, *arguments: str, environment: dict[str, str] | None = None) -> tuple[int, str]: + stderr = io.StringIO() + with ( + mock.patch.dict(os.environ, environment or {}, clear=True), + contextlib.redirect_stderr(stderr), + ): + result = self.module.main(["--compose-root", str(self.root), *arguments]) + return result, stderr.getvalue() + + def test_write_is_canonical_deterministic_and_sanitized(self) -> None: + supplied = {**self.environment, "COOLIFY_API_TOKEN": "must-not-leave-process"} + result, stderr = self.invoke( + "write", "--output", str(self.manifest), environment=supplied + ) + self.assertEqual(result, 0, stderr) + + expected = "".join( + f"{key}={self.environment[key]}\n" for key in self.module.EXPECTED_KEYS + ) + self.assertEqual(self.manifest.read_text(encoding="utf-8"), expected) + self.assertNotIn("must-not-leave-process", expected) + self.assertEqual(self.manifest.stat().st_mode & 0o777, 0o644) + + result, stderr = self.invoke( + "write", "--output", str(self.manifest), environment=supplied + ) + self.assertEqual(result, 0, stderr) + self.assertEqual(self.manifest.read_text(encoding="utf-8"), expected) + + def test_every_image_must_use_its_exact_ghcr_repository_and_digest(self) -> None: + invalid_values = ( + "ghcr.io/registrystack/solmara-lab-evidence:candidate", + f"docker.io/registrystack/solmara-lab-evidence@sha256:{'b' * 64}", + f"ghcr.io/registrystack/solmara-lab-mint@sha256:{'b' * 64}", + f"ghcr.io/registrystack/solmara-lab-evidence@sha256:{'B' * 64}", + f"ghcr.io/registrystack/solmara-lab-evidence@sha256:{'b' * 63}", + ) + for invalid in invalid_values: + with self.subTest(invalid=invalid): + environment = {**self.environment, "SOLMARA_EVIDENCE_IMAGE": invalid} + result, stderr = self.invoke( + "write", "--output", str(self.manifest), environment=environment + ) + self.assertEqual(result, 1) + self.assertIn("SOLMARA_EVIDENCE_IMAGE must", stderr) + + def test_missing_image_fails_without_writing_a_partial_manifest(self) -> None: + environment = dict(self.environment) + del environment["SOLMARA_PORTAL_IMAGE"] + result, stderr = self.invoke( + "write", "--output", str(self.manifest), environment=environment + ) + self.assertEqual(result, 1) + self.assertIn("environment is missing SOLMARA_PORTAL_IMAGE", stderr) + self.assertFalse(self.manifest.exists()) + + def test_validator_refuses_extra_keys_and_noncanonical_order(self) -> None: + canonical = self.module.render_manifest(self.environment) + cases = ( + canonical + f"SOLMARA_DATABASE_PASSWORD={'b' * 64}\n", + "\n".join(reversed(canonical.rstrip("\n").splitlines())) + "\n", + ) + for index, content in enumerate(cases): + with self.subTest(index=index): + self.manifest.parent.mkdir(parents=True, exist_ok=True) + self.manifest.write_text(content, encoding="utf-8") + result, stderr = self.invoke( + "validate", "--manifest", str(self.manifest) + ) + self.assertEqual(result, 1) + self.assertTrue( + "unexpected key" in stderr or "canonical order" in stderr, + stderr, + ) + + def test_validator_refuses_crlf_and_missing_final_newline(self) -> None: + canonical = self.module.render_manifest(self.environment) + cases = ( + canonical.replace("\n", "\r\n").encode("utf-8"), + canonical.rstrip("\n").encode("utf-8"), + ) + for index, content in enumerate(cases): + with self.subTest(index=index): + self.manifest.parent.mkdir(parents=True, exist_ok=True) + self.manifest.write_bytes(content) + result, stderr = self.invoke( + "validate", "--manifest", str(self.manifest) + ) + self.assertEqual(result, 1) + self.assertIn("manifest must", stderr) + + def test_compose_inventory_is_closed_and_requires_variables(self) -> None: + hosted = self.root / "compose.hosted.yaml" + hosted.write_text( + hosted.read_text(encoding="utf-8") + + " unexpected:\n image: ${SOLMARA_OTHER_IMAGE:?required}\n", + encoding="utf-8", + ) + result, stderr = self.invoke("inventory") + self.assertEqual(result, 1) + self.assertIn("unexpected SOLMARA_OTHER_IMAGE", stderr) + + hosted.write_text( + hosted.read_text(encoding="utf-8").replace( + "${SOLMARA_OTHER_IMAGE:?required}", "${SOLMARA_OTHER_IMAGE:-latest}" + ), + encoding="utf-8", + ) + result, stderr = self.invoke("inventory") + self.assertEqual(result, 1) + self.assertIn("must be required variables", stderr) + + def test_release_workflow_generates_validates_and_uploads_the_manifest(self) -> None: + workflow_path = ROOT / ".github" / "workflows" / "release-candidate.yml" + workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8")) + steps = workflow["jobs"]["verify-and-publish"]["steps"] + names = [step.get("name") for step in steps] + provisioner_index = names.index("Build and push authority provisioner") + signer_index = names.index("Build and push Transit signer") + generate_index = names.index("Generate Coolify image manifest") + upload_index = names.index("Upload Coolify image manifest") + self.assertLess(provisioner_index, generate_index) + self.assertLess(signer_index, generate_index) + self.assertGreater(generate_index, names.index("Build and push eSignet seed image")) + self.assertGreater(upload_index, generate_index) + + provisioner = steps[provisioner_index] + self.assertEqual(provisioner["id"], "authority_provisioner") + self.assertEqual( + provisioner["with"]["file"], + "docker/hosted-authority-provisioner/Dockerfile", + ) + self.assertIn( + "/solmara-lab-authority-provisioner:", + provisioner["with"]["tags"], + ) + self.assertIn( + "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=${{ env.REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL }}", + provisioner["with"]["build-args"], + ) + self.assertIn( + "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256=${{ env.REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256 }}", + provisioner["with"]["build-args"], + ) + signer = steps[signer_index] + self.assertEqual(signer["id"], "transit_signer") + self.assertEqual( + signer["with"]["file"], "docker/hosted-transit-signer/Dockerfile" + ) + self.assertIn("/solmara-lab-transit-signer:", signer["with"]["tags"]) + for step in (provisioner, signer): + self.assertIn("org.opencontainers.image.revision=${{ github.sha }}", step["with"]["labels"]) + self.assertIn( + "org.opencontainers.image.source=https://github.com/registrystack/solmara-lab", + step["with"]["labels"], + ) + + label_verification = next( + step + for step in steps + if step.get("name") == "Verify hosted authority image source labels" + )["run"] + self.assertIn("steps.authority_provisioner.outputs.digest", label_verification) + self.assertIn("steps.transit_signer.outputs.digest", label_verification) + self.assertIn("org.opencontainers.image.revision", label_verification) + self.assertIn("org.registrystack.release.revision", label_verification) + + provisioner_smoke = next( + step + for step in steps + if step.get("name") == "Smoke hosted authority provisioner image" + )["run"] + self.assertIn("steps.authority_provisioner.outputs.digest", provisioner_smoke) + self.assertIn("--network none --read-only", provisioner_smoke) + self.assertIn('test "$status" -eq 1', provisioner_smoke) + self.assertIn("hosted target provisioning failed", provisioner_smoke) + + signer_smoke = next( + step + for step in steps + if step.get("name") == "Smoke hosted Transit signer image" + )["run"] + self.assertIn("steps.transit_signer.outputs.digest", signer_smoke) + self.assertIn("--network none --read-only --entrypoint python", signer_smoke) + self.assertIn("import cryptography", signer_smoke) + self.assertIn('test "$status" -eq 1', signer_smoke) + self.assertIn("hosted Transit signer could not start", signer_smoke) + + packages = next( + step for step in steps if step.get("name") == "Require pre-provisioned public Solmara packages" + )["run"] + self.assertIn("solmara-lab-authority-provisioner", packages) + self.assertIn("solmara-lab-transit-signer", packages) + + generate = steps[generate_index] + self.assertIn("hosted-image-manifest.py write", generate["run"]) + self.assertIn("hosted-image-manifest.py validate", generate["run"]) + self.assertEqual(set(generate["env"]), set(self.module.EXPECTED_KEYS)) + self.assertEqual( + generate["env"]["SOLMARA_AUTHORITY_PROVISIONER_IMAGE"], + "${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-authority-provisioner@${{ steps.authority_provisioner.outputs.digest }}", + ) + self.assertEqual( + generate["env"]["SOLMARA_TRANSIT_SIGNER_IMAGE"], + "${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-transit-signer@${{ steps.transit_signer.outputs.digest }}", + ) + + upload = steps[upload_index] + self.assertEqual( + upload["uses"], + "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02", + ) + self.assertEqual(upload["with"]["if-no-files-found"], "error") + self.assertIn("solmara-hosted-images.env", upload["with"]["path"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py new file mode 100644 index 0000000..cb994a6 --- /dev/null +++ b/scripts/test_hosted_provisioning_topology.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +PROVISION = ROOT / "compose.coolify.provision.yaml" +RUNTIME_COMPOSES = ( + ROOT / "compose.coolify.yaml", + ROOT / "compose.coolify.interior.yaml", + ROOT / "compose.coolify.social-development.yaml", + ROOT / "compose.coolify.labour-pensions.yaml", + ROOT / "compose.coolify.agriculture.yaml", +) +PROVIDERS = ("mint", "cra", "nia", "sro", "mosd", "sipf", "nagdi") +RELAYS = ("cra", "nia", "mosd", "sipf", "nagdi") + + +class HostedProvisioningTopologyTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.provision = yaml.safe_load(PROVISION.read_text(encoding="utf-8")) + cls.runtime = { + path.name: yaml.safe_load(path.read_text(encoding="utf-8")) + for path in RUNTIME_COMPOSES + } + + def test_provisioning_application_owns_exactly_34_active_volumes(self) -> None: + volumes = self.provision["volumes"] + self.assertEqual(len(volumes), 34) + self.assertTrue(all(not value.get("external", False) for value in volumes.values())) + self.assertTrue(all(value["name"].startswith("solmara-authority-cells-") for value in volumes.values())) + + def test_runtime_applications_attach_active_volumes_read_only_by_fixed_name(self) -> None: + provisioned = { + value["name"] for value in self.provision["volumes"].values() + } + attached: set[str] = set() + for compose in self.runtime.values(): + for key, value in compose.get("volumes", {}).items(): + if not value or not value.get("external"): + continue + name = value.get("name", "") + if name in provisioned: + attached.add(name) + for service in compose["services"].values(): + for mount in service.get("volumes", []): + if isinstance(mount, str) and mount.startswith(f"{key}:"): + self.assertTrue(mount.endswith(":ro"), mount) + self.assertEqual(attached, provisioned) + + def test_private_signing_keys_are_mounted_only_into_matching_signers(self) -> None: + services = self.provision["services"] + for provider in PROVIDERS: + secret = "mint-signing-jwk" if provider == "mint" else f"{provider}-evidence-signing-jwk" + consumers = { + service_name + for service_name, service in services.items() + if any(item["source"] == secret for item in service.get("secrets", [])) + } + self.assertEqual(consumers, {f"{provider}-signer"}) + for name, service in services.items(): + if name.endswith("provisioner"): + targets = {item["target"] for item in service.get("secrets", [])} + self.assertNotIn("solmara-provisioning/signing.jwk", targets) + + def test_each_signer_requires_its_matching_public_projection(self) -> None: + services = self.provision["services"] + for provider in PROVIDERS: + prefix = "mint" if provider == "mint" else f"{provider}-evidence" + signer = services[f"{provider}-signer"] + secrets = {item["target"]: item["source"] for item in signer["secrets"]} + self.assertEqual(secrets["signing.jwk"], f"{prefix}-signing-jwk") + self.assertEqual( + secrets["signing-public.jwk"], f"{prefix}-signing-public-jwk" + ) + self.assertEqual( + signer["command"][0:4], + [ + "--private-jwk", + "/run/secrets/signing.jwk", + "--public-jwk", + "/run/secrets/signing-public.jwk", + ], + ) + + def test_mint_client_public_keys_use_provisioner_contract_names(self) -> None: + mint = self.provision["services"]["mint-provisioner"] + targets = {secret["target"] for secret in mint["secrets"]} + clients = ( + "cra-pension-evidence", + "cra-citizen-evidence", + "mosd-child-benefit-evidence", + "sipf-pension-evidence", + "sipf-survivor-evidence", + "nagdi-voucher-evidence", + "nagdi-livestock-evidence", + "nia-esignet", + ) + for client in clients: + self.assertIn(f"solmara-provisioning/{client}-public.jwk", targets) + self.assertIn( + "solmara-provisioning/solmara-demo-client-public.jwk", targets + ) + + def test_each_evidence_provisioner_receives_only_its_public_signing_key(self) -> None: + services = self.provision["services"] + for provider in PROVIDERS[1:]: + provisioner = services[f"{provider}-evidence-provisioner"] + sources = {item["source"] for item in provisioner["secrets"]} + self.assertIn(f"{provider}-evidence-signing-public-jwk", sources) + self.assertFalse(any(source.endswith("-signing-jwk") for source in sources)) + foreign_prefixes = { + f"{other}-evidence-" for other in PROVIDERS[1:] if other != provider + } + self.assertFalse( + any(source.startswith(tuple(foreign_prefixes)) for source in sources), + (provider, sources), + ) + + def test_relay_provisioners_receive_no_secret(self) -> None: + services = self.provision["services"] + for authority in RELAYS: + self.assertNotIn("secrets", services[f"{authority}-relay-provisioner"]) + + def test_each_signer_and_transit_initializer_mount_only_its_matching_volume(self) -> None: + services = self.provision["services"] + for provider in PROVIDERS: + volume = "mint-transit" if provider == "mint" else f"{provider}-evidence-transit" + self.assertEqual(services[f"{provider}-transit-init"]["volumes"], [f"{volume}:/transit"]) + signer = services[f"{provider}-signer"] + self.assertEqual(signer["volumes"], [f"{volume}:/transit"]) + self.assertEqual(signer["depends_on"], {f"{provider}-transit-init": {"condition": "service_completed_successfully"}}) + + def test_relay_runtime_secrets_are_authority_scoped(self) -> None: + services = {} + for compose in self.runtime.values(): + services.update(compose["services"]) + for authority in RELAYS: + environment = services[f"{authority}-relay"]["environment"] + expected = { + "SOLMARA_RELAY_AUDIT_KEY": f"${{{authority.upper()}_RELAY_AUDIT_KEY:?required}}" + } + if authority in {"sipf", "nagdi"}: + expected["SOLMARA_RELAY_CURSOR_KEY"] = f"${{{authority.upper()}_RELAY_CURSOR_KEY:?required}}" + self.assertEqual(environment, expected) + + def test_runtime_consumers_depend_on_app_local_audit_init(self) -> None: + for compose in self.runtime.values(): + services = compose["services"] + audit_init = services["audit-permissions"] + self.assertEqual(audit_init["network_mode"], "none") + self.assertEqual(audit_init["cap_add"], ["CHOWN", "FOWNER"]) + for name, service in services.items(): + if name in {"audit-permissions", "static-metadata", "scenario-runner", "child-benefit-federator", "home", "portal"}: + continue + self.assertEqual( + service["depends_on"]["audit-permissions"], + {"condition": "service_completed_successfully"}, + name, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_hosted_relay_bundles.py b/scripts/test_hosted_relay_bundles.py deleted file mode 100644 index df3a34b..0000000 --- a/scripts/test_hosted_relay_bundles.py +++ /dev/null @@ -1,174 +0,0 @@ -from __future__ import annotations - -import importlib.util -import sys -import tempfile -import unittest -from pathlib import Path - -import yaml - - -ROOT = Path(__file__).resolve().parents[1] - - -def load_checker(): - spec = importlib.util.spec_from_file_location( - "check_hosted_relay_bundles", - ROOT / "scripts" / "check-hosted-relay-bundles.py", - ) - if spec is None or spec.loader is None: - raise RuntimeError("could not load check-hosted-relay-bundles.py") - module = importlib.util.module_from_spec(spec) - sys.modules["check_hosted_relay_bundles"] = module - spec.loader.exec_module(module) - return module - - -def load_generator(): - spec = importlib.util.spec_from_file_location( - "generate_hosted_relay_bundles", - ROOT / "scripts" / "generate-hosted-relay-bundles.py", - ) - if spec is None or spec.loader is None: - raise RuntimeError("could not load generate-hosted-relay-bundles.py") - module = importlib.util.module_from_spec(spec) - sys.modules["generate_hosted_relay_bundles"] = module - spec.loader.exec_module(module) - return module - - -class HostedRelayBundleTests(unittest.TestCase): - def test_expected_bundle_sequence_comes_from_versions_file(self) -> None: - checker = load_checker() - with tempfile.TemporaryDirectory() as temporary_directory: - checker.ROOT = Path(temporary_directory) - (checker.ROOT / "versions.env").write_text( - "SOLMARA_RELAY_BUNDLE_SEQUENCE=7\n", - encoding="utf-8", - ) - - self.assertEqual(checker.expected_bundle_sequence(), 7) - - def test_expected_bundle_sequence_must_be_positive(self) -> None: - checker = load_checker() - with tempfile.TemporaryDirectory() as temporary_directory: - checker.ROOT = Path(temporary_directory) - (checker.ROOT / "versions.env").write_text( - "SOLMARA_RELAY_BUNDLE_SEQUENCE=0\n", - encoding="utf-8", - ) - - with self.assertRaisesRegex(SystemExit, "must be a positive integer"): - checker.expected_bundle_sequence() - - def test_generator_uses_the_same_configured_bundle_sequence(self) -> None: - generator = load_generator() - with tempfile.TemporaryDirectory() as temporary_directory: - generator.ROOT = Path(temporary_directory) - (generator.ROOT / "versions.env").write_text( - "SOLMARA_RELAY_BUNDLE_SEQUENCE=7\n", - encoding="utf-8", - ) - - self.assertEqual(generator.configured_bundle_sequence(), 7) - - def test_generator_accepts_a_1password_private_jwk_reference(self) -> None: - generator = load_generator() - reference = "op://vault-id/item-id/private_jwk" - self.assertEqual( - generator.validate_private_jwk_reference(reference), - reference, - ) - - def test_generator_rejects_an_empty_1password_private_jwk_reference(self) -> None: - generator = load_generator() - with self.assertRaisesRegex( - SystemExit, - "invalid 1Password private JWK reference", - ): - generator.validate_private_jwk_reference("op://") - - def test_generator_rejects_private_material_in_the_public_jwk(self) -> None: - generator = load_generator() - with tempfile.TemporaryDirectory() as temporary_directory: - public_jwk = Path(temporary_directory) / "public.jwk" - public_jwk.write_text('{"kty":"OKP","d":"private"}\n', encoding="utf-8") - with self.assertRaisesRegex(SystemExit, "expected a public-only JWK"): - generator.validate_public_jwk(public_jwk) - - def test_public_and_consultation_bundle_variants_are_explicit(self) -> None: - generator = load_generator() - checker = load_checker() - expected = ( - ("public", "relay.yaml", "", False), - ("consultation", "relay-consultation.yaml", "consultation", True), - ) - self.assertEqual(generator.BUNDLE_VARIANTS, expected) - self.assertEqual(checker.BUNDLE_VARIANTS, expected) - - def test_governed_config_uses_variant_specific_trust_paths(self) -> None: - generator = load_generator() - with tempfile.TemporaryDirectory() as temporary_directory: - root = Path(temporary_directory) - source = root / "relay-consultation.yaml" - destination = root / "relay.yaml" - source.write_text( - "instance:\n id: example-relay-consultation\nconsultation: {}\n", - encoding="utf-8", - ) - container_dir = ( - generator.CONTAINER_ROOT / "example" / "consultation" - ) - generator.write_governed_config( - source, - destination, - container_dir, - ) - governed = yaml.safe_load(destination.read_text(encoding="utf-8")) - self.assertEqual( - governed["config_trust"], - { - "trust_anchor_path": str(container_dir / "trust-anchor.json"), - "bundle_path": str(container_dir / "bundle"), - "antirollback_state_path": generator.ANTIROLLBACK_PATH, - }, - ) - - def test_artifact_closure_accepts_identical_complete_trees(self) -> None: - checker = load_checker() - with tempfile.TemporaryDirectory() as temporary_directory: - root = Path(temporary_directory) - bundled = root / "bundled" - source = root / "source" - for directory in (bundled, source): - (directory / "contracts").mkdir(parents=True) - (directory / "contracts" / "contract.json").write_text( - '{"version":1}\n', encoding="utf-8" - ) - - checker.verify_artifact_closure("example", bundled, source) - - def test_artifact_closure_rejects_changed_or_missing_files(self) -> None: - checker = load_checker() - with tempfile.TemporaryDirectory() as temporary_directory: - root = Path(temporary_directory) - bundled = root / "bundled" - source = root / "source" - bundled.mkdir() - source.mkdir() - (bundled / "contract.json").write_text("signed", encoding="utf-8") - (source / "contract.json").write_text("changed", encoding="utf-8") - - with self.assertRaisesRegex( - SystemExit, "signed artifact contract.json differs" - ): - checker.verify_artifact_closure("example", bundled, source) - - (source / "extra.json").write_text("extra", encoding="utf-8") - with self.assertRaisesRegex(SystemExit, "artifact paths differ"): - checker.verify_artifact_closure("example", bundled, source) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_hosted_runtime_assets.py b/scripts/test_hosted_runtime_assets.py new file mode 100644 index 0000000..54847b0 --- /dev/null +++ b/scripts/test_hosted_runtime_assets.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import importlib.util +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "hosted-runtime-assets.py" +SPEC = importlib.util.spec_from_file_location("hosted_runtime_assets", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) +PROVISIONER_SCRIPT = ROOT / "scripts" / "provision-hosted-runtime.py" +PROVISIONER_SPEC = importlib.util.spec_from_file_location( + "provision_hosted_runtime", PROVISIONER_SCRIPT +) +assert PROVISIONER_SPEC and PROVISIONER_SPEC.loader +PROVISIONER = importlib.util.module_from_spec(PROVISIONER_SPEC) +PROVISIONER_SPEC.loader.exec_module(PROVISIONER) + + +class HostedRuntimeAssetTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) / "source" + self.root.mkdir() + self._copy_inventory( + ROOT / "generator" / "solmara_lab", + self.root / "generator" / "solmara_lab", + MODULE.GENERATOR_FILES, + ) + for authority in MODULE.AUTHORITIES: + self._copy_inventory( + ROOT / "relays" / authority, + self.root / "relays" / authority, + MODULE.RELAY_FILES[authority], + ) + for cell in MODULE.EVIDENCE_CELLS: + self._copy_inventory( + ROOT / "evidence" / "cells" / cell, + self.root / "evidence" / "cells" / cell, + MODULE.EVIDENCE_FILES[cell], + ) + mint = self.root / "evidence" / "mint.yaml" + mint.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / "evidence" / "mint.yaml", mint) + self.relayctl = Path(self.temporary.name) / "relayctl" + self.relayctl.write_text( + """#!/usr/bin/env python3 +import hashlib +import json +import os +import pathlib +import sys + +if os.environ.get("PYTHONDONTWRITEBYTECODE") != "1": + raise SystemExit(3) +project = pathlib.Path(sys.argv[sys.argv.index("package") + 1]) +output = pathlib.Path(sys.argv[sys.argv.index("--output") + 1]) +runtime = (project / "runtime.yaml").read_text() +if "path: source.sqlite" not in runtime or not (project / "source.sqlite").is_file(): + raise SystemExit(4) +digest = hashlib.sha256() +for path in sorted(project.rglob("*")): + if path.is_file(): + digest.update(path.relative_to(project).as_posix().encode()) + digest.update(path.read_bytes()) +output.mkdir(parents=True) +(output / "relay-package.json").write_text( + json.dumps({"projectDigest": digest.hexdigest()}, sort_keys=True) + "\\n" +) +""", + encoding="utf-8", + ) + self.relayctl.chmod(0o755) + + def tearDown(self) -> None: + self.temporary.cleanup() + + @staticmethod + def _copy_inventory(source: Path, destination: Path, files: frozenset[str]) -> None: + for relative in files: + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source / relative, target) + + @staticmethod + def _digests(root: Path) -> dict[str, str]: + result = {} + for path in sorted(root.rglob("*")): + if path.is_file(): + result[path.relative_to(root).as_posix()] = MODULE._digest(path) + return result + + def test_build_contains_only_closed_runtime_assets(self) -> None: + output = Path(self.temporary.name) / "assets" + MODULE.build(self.root, output, self.relayctl) + MODULE.verify_manifest(output) + + manifest = json.loads((output / "manifest.json").read_text(encoding="utf-8")) + files = set(manifest["files"]) + for authority in MODULE.AUTHORITIES: + prefix = f"relays/{authority}" + self.assertIn(f"{prefix}/runtime.yaml", files) + self.assertIn(f"{prefix}/package/relay-package.json", files) + self.assertIn(f"{prefix}/source/{authority}.sqlite", files) + for cell in MODULE.EVIDENCE_CELLS: + expected = { + f"evidence/cells/{cell}/{relative}" + for relative in MODULE.EVIDENCE_FILES[cell] + } + self.assertTrue(expected.issubset(files)) + self.assertIn("mint/mint.yaml", files) + self.assertEqual( + { + path.removeprefix("generator/solmara_lab/") + for path in files + if path.startswith("generator/solmara_lab/") + }, + set(MODULE.GENERATOR_FILES), + ) + self.assertFalse(any(path.endswith(".pyc") for path in files)) + self.assertFalse(any("/secrets/" in f"/{path}/" for path in files)) + + def test_unexpected_secret_symlink_and_bytecode_are_refused(self) -> None: + injections = ( + ("evidence/cells/cra/bundle/signing.jwk", b"PRIVATE-JWK-CANARY"), + ("evidence/cells/nia/bundle/audit/events.jsonl", b"AUDIT-CANARY"), + ("generator/solmara_lab/__pycache__/publisher.pyc", b"PYC-CANARY"), + ) + for index, (relative, content) in enumerate(injections): + with self.subTest(relative=relative): + path = self.root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + with self.assertRaisesRegex(MODULE.AssetBuildError, "inventory"): + MODULE._validate_sources(self.root) + path.unlink() + for parent in path.parents: + if parent == self.root or any(parent.iterdir()): + break + parent.rmdir() + + link = self.root / "evidence" / "cells" / "sro" / "bundle" / "linked.yaml" + link.symlink_to(self.root / "evidence" / "mint.yaml") + with self.assertRaisesRegex(MODULE.AssetBuildError, "symbolic link"): + MODULE._validate_sources(self.root) + + def test_manifest_refuses_tampering_extra_files_and_noncanonical_bytes( + self, + ) -> None: + output = Path(self.temporary.name) / "assets" + MODULE.build(self.root, output, self.relayctl) + PROVISIONER.verify_assets(output) + target = output / "mint" / "mint.yaml" + original = target.read_bytes() + target.write_bytes(original + b"# tampered\n") + with self.assertRaisesRegex(MODULE.AssetBuildError, "verification"): + MODULE.verify_manifest(output) + with self.assertRaisesRegex(PROVISIONER.ProvisionError, "invalid assets"): + PROVISIONER.verify_assets(output) + target.write_bytes(original) + + extra = output / "unexpected.txt" + extra.write_text("unexpected", encoding="utf-8") + with self.assertRaisesRegex(MODULE.AssetBuildError, "verification"): + MODULE.verify_manifest(output) + with self.assertRaisesRegex(PROVISIONER.ProvisionError, "invalid assets"): + PROVISIONER.verify_assets(output) + extra.unlink() + + manifest = output / "manifest.json" + canonical = manifest.read_text(encoding="utf-8") + manifest.write_text(json.dumps(json.loads(canonical)) + "\n", encoding="utf-8") + with self.assertRaisesRegex(MODULE.AssetBuildError, "verification"): + MODULE.verify_manifest(output) + + def test_two_builds_are_byte_for_byte_deterministic(self) -> None: + first = Path(self.temporary.name) / "first" + second = Path(self.temporary.name) / "second" + MODULE.build(self.root, first, self.relayctl) + MODULE.build(self.root, second, self.relayctl) + self.assertEqual(self._digests(first), self._digests(second)) + + def test_relayctl_failure_is_redacted_and_leaves_no_output(self) -> None: + canary = "RELAYCTL-PRIVATE-CANARY" + failing = Path(self.temporary.name) / "failing-relayctl" + failing.write_text( + f"#!/bin/sh\nprintf '%s\\n' '{canary}'\nprintf '%s\\n' '{canary}' >&2\nexit 19\n", + encoding="utf-8", + ) + failing.chmod(0o755) + output = Path(self.temporary.name) / "failed-assets" + completed = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "build", + "--root", + str(self.root), + "--output", + str(output), + "--relayctl", + str(failing), + ], + check=False, + capture_output=True, + text=True, + env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}, + ) + self.assertEqual(completed.returncode, 1) + self.assertEqual(completed.stdout, "") + self.assertEqual(completed.stderr.strip(), "hosted runtime asset build failed") + self.assertNotIn(canary, completed.stdout + completed.stderr) + self.assertFalse(output.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_hosted_transit_signer.py b/scripts/test_hosted_transit_signer.py new file mode 100644 index 0000000..5bab83c --- /dev/null +++ b/scripts/test_hosted_transit_signer.py @@ -0,0 +1,364 @@ +from __future__ import annotations + +import importlib.util +import base64 +import hashlib +import json +import os +import socket +import stat +import tempfile +import unittest +from contextlib import redirect_stderr +from io import StringIO +from pathlib import Path +from unittest import mock + +from cryptography.hazmat.primitives.asymmetric import ec + +SCRIPT = Path(__file__).with_name("hosted-transit-signer.py") +SPEC = importlib.util.spec_from_file_location("hosted_transit_signer", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class HostedTransitSignerTests(unittest.TestCase): + def setUp(self) -> None: + safe_parent = Path(__file__).resolve().parent + self.temporary = tempfile.TemporaryDirectory(dir=safe_parent) + self.root = Path(self.temporary.name) + self.socket_temporary = tempfile.TemporaryDirectory(dir="/tmp", prefix="hst-") + self.socket_root = Path(self.socket_temporary.name) + self.secret_directory = self.root / "secrets" + self.secret_directory.mkdir(mode=0o700) + self.secret = self.secret_directory / "signing.jwk" + self.canary = b'{"d":"PRIVATE-SIGNER-CANARY"}' + self.secret.write_bytes(self.canary) + self.secret.chmod(0o400) + self.public = self.secret_directory / "signing-public.jwk" + self.public.write_bytes(b"{}") + self.public.chmod(0o400) + self.staging = self.root / "staging" + self.staging.mkdir(mode=0o700) + + def tearDown(self) -> None: + self.socket_temporary.cleanup() + self.temporary.cleanup() + + def test_compose_secret_is_copied_to_owner_only_staging(self) -> None: + staged = MODULE._stage_secret(self.secret, self.staging) + + self.assertEqual(staged.read_bytes(), self.canary) + self.assertEqual(stat.S_IMODE(staged.stat().st_mode), 0o600) + self.assertEqual(stat.S_IMODE(staged.parent.stat().st_mode), 0o700) + self.assertEqual(staged.stat().st_uid, os.geteuid()) + + def test_only_root_owned_sticky_writable_parent_is_confined(self) -> None: + def directory(mode: int, uid: int = 0) -> os.stat_result: + return os.stat_result( + [stat.S_IFDIR | mode, 1, 0, 1, uid, 0, 0, 0, 0, 0] + ) + + self.assertTrue(MODULE._directory_is_confined(directory(0o755))) + self.assertTrue(MODULE._directory_is_confined(directory(0o1777))) + self.assertFalse(MODULE._directory_is_confined(directory(0o0777))) + self.assertFalse( + MODULE._directory_is_confined(directory(0o1777, os.geteuid() + 1)) + ) + + def test_private_key_must_match_exact_public_projection(self) -> None: + private = ec.generate_private_key(ec.SECP256R1()).private_numbers() + numbers = private.public_numbers + + def encode(value: int) -> str: + return ( + base64.urlsafe_b64encode(value.to_bytes(32, "big")) + .rstrip(b"=") + .decode() + ) + + public = { + "kty": "EC", + "crv": "P-256", + "alg": "ES256", + "x": encode(numbers.x), + "y": encode(numbers.y), + } + thumbprint = {key: public[key] for key in ("crv", "kty", "x", "y")} + public["kid"] = ( + base64.urlsafe_b64encode( + hashlib.sha256( + json.dumps( + thumbprint, separators=(",", ":"), sort_keys=True + ).encode() + ).digest() + ) + .rstrip(b"=") + .decode() + ) + private_jwk = { + **public, + "d": encode(private.private_value), + } + self.secret.chmod(0o600) + self.secret.write_text(json.dumps(private_jwk), encoding="utf-8") + self.secret.chmod(0o400) + self.public.chmod(0o600) + self.public.write_text(json.dumps(public), encoding="utf-8") + self.public.chmod(0o400) + MODULE._verify_public_match(self.secret, self.public) + + public["x"] = encode(numbers.x - 1) + self.public.chmod(0o600) + self.public.write_text(json.dumps(public), encoding="utf-8") + self.public.chmod(0o400) + with self.assertRaisesRegex(MODULE.SignerError, "invalid key pair"): + MODULE._verify_public_match(self.secret, self.public) + + def test_symlinked_secret_or_parent_is_refused(self) -> None: + link = self.secret_directory / "linked.jwk" + link.symlink_to(self.secret) + with self.assertRaisesRegex(MODULE.SignerError, "invalid secret"): + MODULE._read_secret(link) + + linked_parent = self.root / "linked-parent" + linked_parent.symlink_to(self.secret_directory, target_is_directory=True) + with self.assertRaisesRegex(MODULE.SignerError, "invalid secret"): + MODULE._read_secret(linked_parent / self.secret.name) + + def test_writable_hardlinked_empty_and_oversized_secrets_are_refused(self) -> None: + self.secret.chmod(0o620) + with self.assertRaisesRegex(MODULE.SignerError, "invalid secret"): + MODULE._read_secret(self.secret) + + self.secret.chmod(0o400) + hardlink = self.secret_directory / "hardlink.jwk" + os.link(self.secret, hardlink) + with self.assertRaisesRegex(MODULE.SignerError, "invalid secret"): + MODULE._read_secret(self.secret) + hardlink.unlink() + + self.secret.chmod(0o600) + self.secret.write_bytes(b"") + self.secret.chmod(0o400) + with self.assertRaisesRegex(MODULE.SignerError, "invalid secret"): + MODULE._read_secret(self.secret) + self.secret.chmod(0o600) + self.secret.write_bytes(b"x" * (MODULE.MAX_SECRET_BYTES + 1)) + self.secret.chmod(0o400) + with self.assertRaisesRegex(MODULE.SignerError, "invalid secret"): + MODULE._read_secret(self.secret) + + def test_socket_requires_the_exact_absent_path_and_private_owned_directory( + self, + ) -> None: + socket_directory = self.socket_root / "transit" + socket_directory.mkdir(mode=0o700) + exact_socket = socket_directory / "transit-proxy.sock" + with mock.patch.object(MODULE, "SOCKET_PATH", exact_socket): + MODULE._validate_socket(exact_socket) + + exact_socket.write_text("replacement", encoding="ascii") + with self.assertRaisesRegex(MODULE.SignerError, "invalid socket"): + MODULE._validate_socket(exact_socket) + exact_socket.unlink() + + socket_directory.chmod(0o770) + with self.assertRaisesRegex(MODULE.SignerError, "invalid socket"): + MODULE._validate_socket(exact_socket) + + with self.assertRaisesRegex(MODULE.SignerError, "invalid socket"): + MODULE._validate_socket(socket_directory / "other.sock") + + def test_stale_owned_socket_is_removed_but_live_socket_is_refused(self) -> None: + socket_directory = self.socket_root / "transit" + socket_directory.mkdir(mode=0o700) + exact_socket = socket_directory / "transit-proxy.sock" + with mock.patch.object(MODULE, "SOCKET_PATH", exact_socket): + stale = socket.socket(socket.AF_UNIX) + stale.bind(str(exact_socket)) + stale.close() + exact_socket.chmod(0o600) + MODULE._validate_socket(exact_socket) + self.assertFalse(exact_socket.exists()) + + live = socket.socket(socket.AF_UNIX) + live.bind(str(exact_socket)) + live.listen(1) + exact_socket.chmod(0o600) + try: + with self.assertRaisesRegex(MODULE.SignerError, "invalid socket"): + MODULE._validate_socket(exact_socket) + self.assertTrue(exact_socket.exists()) + finally: + live.close() + exact_socket.unlink(missing_ok=True) + + def test_stale_socket_refuses_symlink_regular_file_and_open_mode(self) -> None: + socket_directory = self.socket_root / "transit" + socket_directory.mkdir(mode=0o700) + exact_socket = socket_directory / "transit-proxy.sock" + with mock.patch.object(MODULE, "SOCKET_PATH", exact_socket): + regular = self.root / "regular" + regular.write_text("replacement", encoding="ascii") + exact_socket.symlink_to(regular) + with self.assertRaisesRegex(MODULE.SignerError, "invalid socket"): + MODULE._validate_socket(exact_socket) + exact_socket.unlink() + + exact_socket.write_text("replacement", encoding="ascii") + with self.assertRaisesRegex(MODULE.SignerError, "invalid socket"): + MODULE._validate_socket(exact_socket) + exact_socket.unlink() + + stale = socket.socket(socket.AF_UNIX) + stale.bind(str(exact_socket)) + stale.close() + exact_socket.chmod(0o660) + with self.assertRaisesRegex(MODULE.SignerError, "invalid socket"): + MODULE._validate_socket(exact_socket) + exact_socket.unlink() + + def test_stale_socket_owned_by_another_uid_is_refused(self) -> None: + class ForeignSocket: + def lstat(self) -> os.stat_result: + values = [ + stat.S_IFSOCK | 0o600, + 1, + MODULE.os.geteuid() + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + return os.stat_result(values) + + with self.assertRaisesRegex(MODULE.SignerError, "invalid socket"): + MODULE._socket_identity(ForeignSocket()) + + def test_stale_socket_inode_replacement_is_refused_without_unlinking_replacement( + self, + ) -> None: + socket_directory = self.socket_root / "transit" + socket_directory.mkdir(mode=0o700) + exact_socket = socket_directory / "transit-proxy.sock" + first = socket.socket(socket.AF_UNIX) + first.bind(str(exact_socket)) + first.close() + exact_socket.chmod(0o600) + + replacement = socket.socket(socket.AF_UNIX) + + original = MODULE._socket_identity + + def replace_before_recheck(path: Path) -> tuple[int, int, int, int]: + identity = original(path) + if replace_before_recheck.calls == 0: + replace_before_recheck.calls += 1 + return identity + path.unlink() + replacement.bind(str(path)) + path.chmod(0o600) + return original(path) + + replace_before_recheck.calls = 0 + with ( + mock.patch.object(MODULE, "SOCKET_PATH", exact_socket), + mock.patch.object( + MODULE, "_socket_identity", side_effect=replace_before_recheck + ), + self.assertRaisesRegex(MODULE.SignerError, "invalid socket"), + ): + MODULE._validate_socket(exact_socket) + self.assertTrue(exact_socket.exists()) + replacement.close() + exact_socket.unlink(missing_ok=True) + + def test_exec_is_one_proxy_one_staged_key_and_one_exact_socket(self) -> None: + socket_directory = self.socket_root / "transit" + socket_directory.mkdir(mode=0o700) + socket_path = socket_directory / "transit-proxy.sock" + proxy = self.root / "local-transit-proxy.py" + proxy.write_text("# fixed proxy\n", encoding="ascii") + proxy.chmod(0o500) + key_name = "solmara-evidence-cra" + stage_secret = MODULE._stage_secret + + with ( + mock.patch.object(MODULE, "SECRET_PATH", self.secret), + mock.patch.object(MODULE, "PUBLIC_PATH", self.public), + mock.patch.object(MODULE, "SOCKET_PATH", socket_path), + mock.patch.object(MODULE, "STAGING_ROOT", self.staging), + mock.patch.object( + MODULE, + "_stage_secret", + side_effect=lambda source: stage_secret(source, self.staging), + ), + mock.patch.object(MODULE, "_verify_public_match"), + mock.patch.object( + MODULE.os, "execve", side_effect=RuntimeError("exec captured") + ) as execute, + self.assertRaisesRegex(RuntimeError, "exec captured"), + ): + MODULE.exec_signer(self.secret, self.public, socket_path, key_name, proxy) + + executable, arguments, environment = execute.call_args.args + self.assertEqual(executable, os.sys.executable) + self.assertEqual(arguments[0:2], [os.sys.executable, str(proxy)]) + self.assertEqual( + arguments[-4:], ["--socket", str(socket_path), "--key-name", key_name] + ) + staged = Path(arguments[3]) + self.assertNotEqual(staged, self.secret) + self.assertEqual(staged.read_bytes(), self.canary) + self.assertNotIn(self.canary.decode("ascii"), repr(execute.call_args)) + self.assertEqual( + set(environment), {"LANG", "PYTHONDONTWRITEBYTECODE", "PYTHONUNBUFFERED"} + ) + + def test_unlisted_key_and_noncanonical_secret_fail_before_staging(self) -> None: + with mock.patch.object(MODULE, "_stage_secret") as stage: + with self.assertRaisesRegex( + MODULE.SignerError, "invalid signer configuration" + ): + MODULE.exec_signer( + Path("/another/secret"), + MODULE.PUBLIC_PATH, + MODULE.SOCKET_PATH, + "private-canary", + Path("/proxy"), + ) + stage.assert_not_called() + + def test_cli_failure_is_generic_and_redacts_rejected_values(self) -> None: + error = StringIO() + canaries = ["PRIVATE-PATH-CANARY", "PRIVATE-KEY-CANARY"] + with ( + mock.patch.object( + os.sys, + "argv", + [ + SCRIPT.name, + "--private-jwk", + canaries[0], + "--socket", + "/wrong", + "--key-name", + canaries[1], + ], + ), + redirect_stderr(error), + ): + self.assertEqual(MODULE.main(), 1) + + self.assertEqual(error.getvalue().strip(), MODULE.GENERIC_ERROR) + for canary in canaries: + self.assertNotIn(canary, error.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_image_pins.py b/scripts/test_image_pins.py index 0b4b94e..7466025 100644 --- a/scripts/test_image_pins.py +++ b/scripts/test_image_pins.py @@ -43,6 +43,13 @@ def setUp(self) -> None: f"EVIDENCE_GATEWAY_IMAGE={GATEWAY}\n", encoding="utf-8", ) + with (self.root / "versions.env").open("a", encoding="utf-8") as versions: + for key in ( + "PYTHON_STATIC_IMAGE", "NODE_BUILD_IMAGE", "UV_BUILD_IMAGE", + "ESIGNET_REDIS_IMAGE", "ESIGNET_BASE_IMAGE", + "ESIGNET_UI_IMAGE", "ESIGNET_POSTGRES_IMAGE", + ): + versions.write(f"{key}=example.invalid/image@sha256:{'6' * 64}\n") def tearDown(self) -> None: self.directory.cleanup() @@ -80,6 +87,18 @@ def test_gateway_must_be_digest_pinned(self) -> None: self.assertEqual(result, 1) self.assertIn("EVIDENCE_GATEWAY_IMAGE must use image@sha256", stderr) + def test_nonexistent_upstream_evidence_image_is_rejected(self) -> None: + self.run_check() + (self.root / "compose.hosted.yaml").write_text( + "services:\n evidence:\n image: ghcr.io/registrystack/evidence@sha256:" + "a" * 64 + "\n", + encoding="utf-8", + ) + stderr = io.StringIO() + with contextlib.redirect_stderr(stderr): + result = self.module.main() + self.assertEqual(result, 1) + self.assertIn("is not a published Registry Stack image", stderr.getvalue()) + if __name__ == "__main__": unittest.main() diff --git a/scripts/test_lifecycle_proof.py b/scripts/test_lifecycle_proof.py new file mode 100644 index 0000000..96f60e0 --- /dev/null +++ b/scripts/test_lifecycle_proof.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +import hashlib +import importlib.util +import json +import stat +import subprocess +import sys +import tempfile +import unittest +from datetime import datetime, timezone +from pathlib import Path + +SCRIPT = Path(__file__).with_name("lifecycle_proof.py") +SPEC = importlib.util.spec_from_file_location("lifecycle_proof", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +lifecycle = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(lifecycle) + + +class LifecycleProofTests(unittest.TestCase): + def test_complete_proof_is_isolated_and_passes_every_check(self) -> None: + result = lifecycle.run_proof() + + self.assertEqual(result["status"], "pass") + self.assertEqual(result["fixtureState"], "isolated-temporary-directory") + self.assertTrue(all(result["checks"].values())) + + def test_long_lived_read_only_mosd_binding_observes_in_place_mutation(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + lifecycle.publish_all(root) + database = ( + root + / lifecycle.RELAY_DIRECTORY + / lifecycle.RELAY_FILENAMES["mosd"] + ) + inode = database.stat().st_ino + binding = lifecycle.GovernedMosdObservation(database) + try: + connection_id = id(binding.connection) + before = binding.observe_duplicate_flag(lifecycle.MOSD_TEST_UIN) + lifecycle.mutate_mosd_state( + database, + lifecycle.MOSD_TEST_UIN, + True, + "2026-07-05T08:15:00Z", + ) + after = binding.observe_duplicate_flag(lifecycle.MOSD_TEST_UIN) + finally: + binding.close() + + self.assertEqual((before[0], after[0]), (0, 1)) + self.assertNotEqual(before[1:], after[1:]) + self.assertEqual(database.stat().st_ino, inode) + self.assertEqual(id(binding.connection), connection_id) + + def test_sro_binding_requires_explicit_rebind_to_changed_extract(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + lifecycle.publish_all(root) + observed_at = datetime(2026, 7, 5, 8, 30, tzinfo=timezone.utc) + original_id = lifecycle.DEFAULT_EXTRACTS["sro"] + original = ( + root / lifecycle.EVIDENCE_DIRECTORY / f"{original_id}.sqlite" + ) + original_digest = hashlib.sha256(original.read_bytes()).hexdigest() + binding = lifecycle.SroExtractBinding( + original, + expected_extract_id=original_id, + observed_at=observed_at, + ) + replacement_id = "sro-poverty-20260705T080000Z" + replacement = lifecycle._publish_changed_sro_extract( + root, + extract_id=replacement_id, + published_at="2026-07-05T08:00:00Z", + poverty_band="not_eligible", + ) + try: + still_old = binding.observe_poverty_band(lifecycle.SRO_TEST_UIN) + finally: + binding.close() + + rebound = lifecycle.SroExtractBinding( + replacement, + expected_extract_id=replacement_id, + observed_at=observed_at, + ) + try: + changed = rebound.observe_poverty_band(lifecycle.SRO_TEST_UIN) + finally: + rebound.close() + + self.assertNotEqual(still_old, changed) + self.assertEqual(hashlib.sha256(original.read_bytes()).hexdigest(), original_digest) + self.assertNotEqual(original, replacement) + + def test_invalid_stale_and_writable_extracts_fail_closed(self) -> None: + observed_at = datetime(2026, 7, 5, 8, 30, tzinfo=timezone.utc) + cases = ( + ("stale", "2026-07-03T08:00:00Z", None, False), + ( + "metadata", + "2026-07-05T08:00:00Z", + "metadata-does-not-match-binding", + False, + ), + ("writable", "2026-07-05T08:00:00Z", None, True), + ) + for suffix, published_at, metadata_id, make_writable in cases: + with self.subTest(case=suffix), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + lifecycle.publish_all(root) + extract_id = f"sro-poverty-20260705T08-{suffix}" + extract = lifecycle._publish_changed_sro_extract( + root, + extract_id=extract_id, + published_at=published_at, + poverty_band="not_eligible", + metadata_extract_id=metadata_id, + ) + if make_writable: + extract.chmod(0o644) + with self.assertRaises(lifecycle.LifecycleProofError): + lifecycle.SroExtractBinding( + extract, + expected_extract_id=extract_id, + observed_at=observed_at, + ) + + def test_immutable_publication_refuses_overwrite_without_changing_bytes(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + lifecycle.publish_all(root) + extract_id = lifecycle.DEFAULT_EXTRACTS["sro"] + extract = root / lifecycle.EVIDENCE_DIRECTORY / f"{extract_id}.sqlite" + before = hashlib.sha256(extract.read_bytes()).digest() + with self.assertRaises(FileExistsError): + lifecycle.publish_extract( + root, + "sro", + "2026-07-05T08:00:00Z", + extract_id, + ) + self.assertEqual(hashlib.sha256(extract.read_bytes()).digest(), before) + self.assertEqual(stat.S_IMODE(extract.stat().st_mode), 0o444) + + def test_json_cli_emits_only_sanitized_proof_state(self) -> None: + completed = subprocess.run( + [sys.executable, str(SCRIPT), "--json"], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + result = json.loads(completed.stdout) + self.assertEqual(result["status"], "pass") + serialized = completed.stdout.lower() + for forbidden in ( + lifecycle.MOSD_TEST_UIN, + lifecycle.SRO_TEST_UIN, + "poverty_band", + "record_revision", + "sqlite", + ): + self.assertNotIn(forbidden.lower(), serialized) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_live_lifecycle_proof.py b/scripts/test_live_lifecycle_proof.py new file mode 100644 index 0000000..8f47df7 --- /dev/null +++ b/scripts/test_live_lifecycle_proof.py @@ -0,0 +1,509 @@ +from __future__ import annotations + +import importlib.util +import io +import json +import os +import sqlite3 +import stat +import sys +import tempfile +import unittest +from contextlib import redirect_stderr, redirect_stdout +from pathlib import Path + +SCRIPT = Path(__file__).with_name("live-lifecycle-proof.py") +SPEC = importlib.util.spec_from_file_location("live_lifecycle_proof", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +lifecycle = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = lifecycle +SPEC.loader.exec_module(lifecycle) + + +class FakeOperations: + def __init__(self, fail_at: str = "", *, hot_reload_sro: bool = False) -> None: + self.fail_at = fail_at + self.hot_reload_sro = hot_reload_sro + self.events: list[str] = [] + self.mosd_duplicate = False + self.sro_changed = False + self.sro_bound = False + self.original_binding = b"original-binding" + self.original_mosd_fixture = lifecycle.MosdFixture(True) + self.mosd_generation = "mosd-generation" + self.sro_generation = "sro-generation-1" + self.publication = lifecycle.ExtractPublication( + Path("private-publication"), + "/private/extract", + "private-extract-id", + ) + + def _event(self, name: str) -> None: + self.events.append(name) + if self.fail_at == name: + raise RuntimeError( + "selector=2300010248 token=private source row private.sqlite" + ) + + def load_environment(self) -> None: + self._event("load") + + def validate_preconditions(self) -> None: + self._event("preconditions") + + def observe_evidence(self, requirement: str) -> bool: + name = ( + "observe-mosd" + if requirement == lifecycle.MOSD_REQUIREMENT + else "observe-sro" + ) + self._event(name) + return ( + not self.mosd_duplicate if name == "observe-mosd" else not self.sro_changed + ) + + def generation(self, service: str) -> str: + name = ( + "generation-sro" if service == lifecycle.SRO_SERVICE else "generation-mosd" + ) + self._event(name) + return ( + self.sro_generation + if service == lifecycle.SRO_SERVICE + else self.mosd_generation + ) + + def capture_mosd_fixture(self): + self._event("capture-mosd") + return self.original_mosd_fixture + + def mutate_mosd(self, duplicate: bool) -> None: + self._event(f"mutate-mosd-{str(duplicate).lower()}") + self.mosd_duplicate = duplicate + + def restore_mosd_fixture(self, fixture) -> None: + self._event("restore-mosd") + if fixture != self.original_mosd_fixture: + raise AssertionError + self.mosd_duplicate = False + + def capture_sro_binding(self) -> bytes: + self._event("capture-binding") + return self.original_binding + + def publish_changed_sro(self): + self._event("publish-sro") + return self.publication + + def bind_sro(self, publication) -> None: + self._event("bind-sro") + if publication is not self.publication: + raise AssertionError + self.sro_bound = True + if self.hot_reload_sro: + self.sro_changed = True + + def restart_sro(self) -> None: + self._event("restart-sro") + self.sro_changed = self.sro_bound + self.sro_generation = ( + "sro-generation-2" if self.sro_bound else "sro-generation-restored" + ) + + def wait_sro_ready(self) -> None: + self._event("wait-sro") + + def prove_replacement_refusals(self) -> None: + self._event("replacement-refusals") + + def restore_sro_binding(self, original: bytes) -> None: + self._event("restore-binding") + if original != self.original_binding: + raise AssertionError + self.sro_bound = False + + def discard_sro_publication(self, publication) -> None: + self._event("discard-sro") + if publication is not self.publication or self.sro_bound: + raise AssertionError + + +class LiveLifecycleProofTests(unittest.TestCase): + def test_complete_proof_uses_signed_http_observations_and_restores_state( + self, + ) -> None: + operations = FakeOperations() + + result = lifecycle.run_proof(operations) + + self.assertEqual(result["status"], "pass") + self.assertEqual(result["proof"], "live-http") + self.assertTrue(all(result["checks"].values())) + self.assertFalse(operations.mosd_duplicate) + self.assertFalse(operations.sro_bound) + self.assertFalse(operations.sro_changed) + self.assertIn("discard-sro", operations.events) + self.assertIn("replacement-refusals", operations.events) + self.assertGreaterEqual(operations.events.count("observe-mosd"), 3) + self.assertGreaterEqual(operations.events.count("observe-sro"), 4) + + def test_failure_after_live_mutation_restores_mosd_and_sro(self) -> None: + operations = FakeOperations(fail_at="replacement-refusals") + + with self.assertRaisesRegex( + lifecycle.LifecycleProofError, + "lifecycle proof did not complete", + ): + lifecycle.run_proof(operations) + + self.assertFalse(operations.mosd_duplicate) + self.assertFalse(operations.sro_bound) + self.assertFalse(operations.sro_changed) + self.assertIn("restore-binding", operations.events) + self.assertGreaterEqual(operations.events.count("restart-sro"), 2) + + def test_failure_before_binding_capture_restores_captured_mosd(self) -> None: + operations = FakeOperations(fail_at="capture-binding") + + with self.assertRaises(lifecycle.LifecycleProofError): + lifecycle.run_proof(operations) + + self.assertFalse(operations.mosd_duplicate) + self.assertIn("restore-mosd", operations.events) + self.assertNotIn("restore-binding", operations.events) + + def test_cleanup_failure_wins_without_exposing_private_details(self) -> None: + operations = FakeOperations(fail_at="restore-mosd") + + with self.assertRaisesRegex( + lifecycle.LifecycleProofError, + "lifecycle proof did not complete", + ) as caught: + lifecycle.run_proof(operations) + + rendered = str(caught.exception).lower() + for forbidden in ("2300010248", "selector", "token", "sqlite", "source row"): + self.assertNotIn(forbidden, rendered) + self.assertIn("restore-binding", operations.events) + self.assertIn("discard-sro", operations.events) + self.assertFalse(operations.sro_bound) + + def test_failure_while_binding_still_discards_unbound_publication(self) -> None: + operations = FakeOperations(fail_at="bind-sro") + + with self.assertRaises(lifecycle.LifecycleProofError): + lifecycle.run_proof(operations) + + self.assertIn("restore-binding", operations.events) + self.assertIn("discard-sro", operations.events) + self.assertFalse(operations.mosd_duplicate) + + def test_hot_reloaded_sro_binding_is_rejected_before_restart(self) -> None: + operations = FakeOperations(hot_reload_sro=True) + + with self.assertRaises(lifecycle.LifecycleProofError): + lifecycle.run_proof(operations) + + binding_index = operations.events.index("bind-sro") + restart_index = operations.events.index("restart-sro") + self.assertIn("observe-sro", operations.events[binding_index:restart_index]) + self.assertFalse(operations.sro_bound) + self.assertFalse(operations.sro_changed) + + def test_changed_extract_is_staged_then_published_read_only(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + publication = lifecycle._publish_changed_sro(root) + try: + self.assertTrue(publication.path.is_file()) + self.assertEqual( + stat.S_IMODE(publication.path.stat().st_mode) & 0o222, + 0, + ) + lifecycle.publisher.validate_extract( + publication.path, + "sro", + observed_at=lifecycle._timestamp(lifecycle._now()), + expected_extract_id=publication.extract_id, + ) + with sqlite3.connect(publication.path) as connection: + row = connection.execute( + "SELECT poverty_band FROM poverty_evidence WHERE uin = ?", + (lifecycle.SRO_CONTROL_SUBJECT,), + ).fetchone() + self.assertEqual(row, ("standard",)) + self.assertEqual(list((root / "runtime").glob(".lifecycle-sro-*")), []) + finally: + publication.path.unlink(missing_ok=True) + + def test_failed_publication_validation_leaves_no_replacement_file(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + original = lifecycle.publisher.validate_extract + lifecycle.publisher.validate_extract = lambda *args, **kwargs: ( + _ for _ in () + ).throw(RuntimeError("private validation detail")) + try: + with self.assertRaises(RuntimeError): + lifecycle._publish_changed_sro(root) + finally: + lifecycle.publisher.validate_extract = original + + extracts = root / lifecycle.publisher.EVIDENCE_DIRECTORY + self.assertEqual(list(extracts.glob("*.sqlite")), []) + + def test_mosd_lifecycle_uses_only_fixed_publisher_verbs(self) -> None: + commands: list[list[str]] = [] + original = lifecycle._run_command + + def record(command, **kwargs): + commands.append(command) + return lifecycle.subprocess.CompletedProcess(command, 0, "", "") + + lifecycle._run_command = record + try: + operations = lifecycle.LocalOperations(Path("/unused")) + fixture = operations.capture_mosd_fixture() + operations.mutate_mosd(True) + operations.restore_mosd_fixture(fixture) + finally: + lifecycle._run_command = original + + self.assertEqual( + [command[-1] for command in commands], + ["begin-proof", "set-proof-state", "restore-proof"], + ) + expected_prefix = [ + "run", + "--rm", + "-T", + "--no-deps", + lifecycle.MOSD_PUBLISHER_SERVICE, + ] + for command in commands: + self.assertEqual(command[-6:-1], expected_prefix) + rendered = " ".join(command).lower() + self.assertNotIn(lifecycle.MOSD_CONTROL_SUBJECT, rendered) + self.assertNotIn("duplicate", rendered) + + def test_failed_mosd_begin_attempts_idempotent_publisher_restore(self) -> None: + commands: list[list[str]] = [] + original = lifecycle._run_command + + def fail_begin(command, **kwargs): + commands.append(command) + if command[-1] == "begin-proof": + raise RuntimeError("private publisher failure") + return lifecycle.subprocess.CompletedProcess(command, 0, "", "") + + lifecycle._run_command = fail_begin + try: + with self.assertRaisesRegex( + lifecycle.LifecycleProofError, + "lifecycle proof did not complete", + ): + lifecycle.LocalOperations(Path("/unused")).capture_mosd_fixture() + finally: + lifecycle._run_command = original + + self.assertEqual( + [command[-1] for command in commands], + ["begin-proof", "restore-proof"], + ) + + def test_mosd_relay_requires_its_project_named_volume_read_only(self) -> None: + operations = lifecycle.LocalOperations(Path("/unused")) + original_command = lifecycle._run_command + original_project = os.environ.get("COMPOSE_PROJECT_NAME") + os.environ["COMPOSE_PROJECT_NAME"] = "solmara-test" + commands: list[list[str]] = [] + responses = iter( + ( + lifecycle.subprocess.CompletedProcess([], 0, "relay-container\n", ""), + lifecycle.subprocess.CompletedProcess( + [], + 0, + json.dumps( + [ + { + "Type": "volume", + "Name": "solmara-test_mosd-relay-source", + "Destination": "/var/lib/relay/source", + "RW": False, + } + ] + ), + "", + ), + ) + ) + + def respond(command, **kwargs): + commands.append(command) + return next(responses) + + lifecycle._run_command = respond + try: + operations._require_read_only_named_volume( + lifecycle.MOSD_RELAY_SERVICE, + lifecycle.MOSD_SOURCE_VOLUME, + lifecycle.RELAY_SOURCE_DESTINATION, + ) + finally: + lifecycle._run_command = original_command + if original_project is None: + os.environ.pop("COMPOSE_PROJECT_NAME", None) + else: + os.environ["COMPOSE_PROJECT_NAME"] = original_project + + self.assertIn("ps", commands[0]) + self.assertEqual(commands[1][:2], ["docker", "inspect"]) + + def test_writable_or_bind_mosd_source_mount_is_refused(self) -> None: + operations = lifecycle.LocalOperations(Path("/unused")) + original_command = lifecycle._run_command + original_project = os.environ.get("COMPOSE_PROJECT_NAME") + os.environ["COMPOSE_PROJECT_NAME"] = "solmara-test" + cases = ( + {"Type": "volume", "RW": True}, + {"Type": "bind", "RW": False}, + ) + try: + for changed in cases: + with self.subTest(changed=changed): + mount = { + "Type": "volume", + "Name": "solmara-test_mosd-relay-source", + "Destination": "/var/lib/relay/source", + "RW": False, + **changed, + } + responses = iter( + ( + lifecycle.subprocess.CompletedProcess( + [], 0, "relay-container\n", "" + ), + lifecycle.subprocess.CompletedProcess( + [], 0, json.dumps([mount]), "" + ), + ) + ) + lifecycle._run_command = lambda command, **kwargs: next(responses) + with self.assertRaises(lifecycle.LifecycleProofError): + operations._require_read_only_named_volume( + lifecycle.MOSD_RELAY_SERVICE, + lifecycle.MOSD_SOURCE_VOLUME, + lifecycle.RELAY_SOURCE_DESTINATION, + ) + finally: + lifecycle._run_command = original_command + if original_project is None: + os.environ.pop("COMPOSE_PROJECT_NAME", None) + else: + os.environ["COMPOSE_PROJECT_NAME"] = original_project + + def test_runtime_patch_changes_only_the_bound_extract_path(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + runtime = Path(temporary) / "runtime.yaml" + old_path = ( + "/var/lib/registry-evidence/sro/extracts/" + "sro-poverty-20260812T010203Z.sqlite" + ) + new_path = ( + "/var/lib/registry-evidence/sro/extracts/" + "sro-poverty-20260812T020304Z.sqlite" + ) + original = ( + "version: 1\n" + "# operator-generated runtime\n" + "sourceExtracts:\n" + f" sro-poverty-extract: {{ path: {old_path} }}\n" + ).encode() + runtime.write_bytes(original) + + lifecycle._replace_runtime_binding(runtime, new_path) + + self.assertEqual( + runtime.read_bytes(), + original.replace(old_path.encode(), new_path.encode()), + ) + + def test_ready_sro_with_invalid_source_requires_evidence_failure(self) -> None: + operations = lifecycle.LocalOperations(Path("/unused")) + original_http_json = lifecycle.evidence_common.http_json + original_monotonic = lifecycle.time.monotonic + original_sleep = lifecycle.time.sleep + original_observe = operations.observe_evidence + lifecycle.evidence_common.http_json = lambda *args, **kwargs: ( + lifecycle.evidence_common.StepHttpResult(200, {}, {}) + ) + clock = iter((0.0, 0.0, lifecycle.REFUSAL_OBSERVATION_SECONDS + 1.0)) + lifecycle.time.monotonic = lambda: next(clock) + lifecycle.time.sleep = lambda _seconds: None + + def refused(_requirement: str) -> bool: + raise lifecycle.LifecycleProofError("private runtime detail") + + operations.observe_evidence = refused + try: + operations._wait_sro_refused() + finally: + lifecycle.evidence_common.http_json = original_http_json + lifecycle.time.monotonic = original_monotonic + lifecycle.time.sleep = original_sleep + operations.observe_evidence = original_observe + + def test_ready_sro_with_invalid_source_rejects_successful_evidence(self) -> None: + operations = lifecycle.LocalOperations(Path("/unused")) + original_monotonic = lifecycle.time.monotonic + original_observe = operations.observe_evidence + clock = iter((0.0, lifecycle.REFUSAL_OBSERVATION_SECONDS + 1.0)) + lifecycle.time.monotonic = lambda: next(clock) + operations.observe_evidence = lambda _requirement: True + try: + with self.assertRaisesRegex( + lifecycle.LifecycleProofError, + "lifecycle proof did not complete", + ): + operations._wait_sro_refused() + finally: + lifecycle.time.monotonic = original_monotonic + operations.observe_evidence = original_observe + + def test_cli_failure_emits_only_the_public_failure_class(self) -> None: + original = lifecycle.run_proof + lifecycle.run_proof = lambda: (_ for _ in ()).throw( + RuntimeError("selector=2300010248 token=private signed.jws") + ) + stdout = io.StringIO() + stderr = io.StringIO() + try: + with redirect_stdout(stdout), redirect_stderr(stderr): + status = lifecycle.main(["--json"]) + finally: + lifecycle.run_proof = original + + self.assertEqual(status, 1) + self.assertEqual(stdout.getvalue(), "") + self.assertEqual(stderr.getvalue(), "live-lifecycle-proof: failed\n") + + def test_success_document_contains_only_sanitized_named_checks(self) -> None: + result = lifecycle.run_proof(FakeOperations()) + rendered = json.dumps(result, sort_keys=True) + + lifecycle._assert_sanitized(rendered) + for forbidden in ( + lifecycle.MOSD_CONTROL_SUBJECT, + lifecycle.SRO_CONTROL_SUBJECT, + "selector", + "token", + "jws", + "signature", + "sqlite", + "source row", + ): + self.assertNotIn(forbidden.lower(), rendered.lower()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_local_relay_source_publisher.py b/scripts/test_local_relay_source_publisher.py new file mode 100644 index 0000000..9f7052e --- /dev/null +++ b/scripts/test_local_relay_source_publisher.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +import importlib.util +import json +import sqlite3 +import stat +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("local-relay-source-publisher.py") +SPEC = importlib.util.spec_from_file_location("local_relay_source_publisher", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + +CONTROL_ROW = ( + "MOSD-ENROL-CONTROL", + "rev-original", + "active", + "2026-07-04T09:00:00Z", + MODULE.MOSD_CONTROL_SELECTOR, + 0, +) + + +def create_mosd_database(path: Path, *, include_control: bool = True) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with sqlite3.connect(path) as connection: + connection.executescript( + """ + PRAGMA journal_mode = DELETE; + PRAGMA user_version = 1; + CREATE TABLE beneficiary_enrolment_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + uin TEXT NOT NULL UNIQUE, + duplicate_flag INTEGER NOT NULL CHECK (duplicate_flag IN (0, 1)) + ) STRICT; + CREATE VIEW relay_beneficiary_enrolment AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + uin, duplicate_flag + FROM beneficiary_enrolment_source; + """ + ) + if include_control: + connection.execute( + "INSERT INTO beneficiary_enrolment_source VALUES (?, ?, ?, ?, ?, ?)", + CONTROL_ROW, + ) + + +def create_cra_database(path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with sqlite3.connect(path) as connection: + connection.executescript( + """ + PRAGMA journal_mode = DELETE; + PRAGMA user_version = 1; + CREATE TABLE civil_person_source ( + record_id TEXT PRIMARY KEY, + record_revision TEXT NOT NULL, + lifecycle_state TEXT NOT NULL, + recorded_at TEXT NOT NULL, + uin TEXT NOT NULL UNIQUE, + deceased INTEGER NOT NULL CHECK (deceased IN (0, 1)) + ) STRICT; + CREATE VIEW relay_civil_person AS + SELECT record_id, record_revision, lifecycle_state, recorded_at, + uin, deceased + FROM civil_person_source; + """ + ) + + +def read_control(path: Path) -> tuple[object, ...]: + with sqlite3.connect(path) as connection: + row = connection.execute( + """ + SELECT record_id, record_revision, lifecycle_state, recorded_at, + uin, duplicate_flag + FROM beneficiary_enrolment_source + WHERE uin = ? + """, + (MODULE.MOSD_CONTROL_SELECTOR,), + ).fetchone() + assert row is not None + return row + + +class LocalRelaySourcePublisherTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.seed = self.root / "seed" / "mosd.sqlite" + self.database = self.root / "volume" / "mosd.sqlite" + create_mosd_database(self.seed) + + def tearDown(self) -> None: + self.temporary.cleanup() + + def test_seed_is_published_once_and_existing_content_is_preserved(self) -> None: + MODULE.ensure_seeded("mosd", self.database, self.seed) + original_inode = self.database.stat().st_ino + self.assertEqual(stat.S_IMODE(self.database.stat().st_mode), 0o644) + with sqlite3.connect(self.database) as connection: + connection.execute( + """ + UPDATE beneficiary_enrolment_source + SET duplicate_flag = 1, record_revision = 'rev-runtime' + WHERE uin = ? + """, + (MODULE.MOSD_CONTROL_SELECTOR,), + ) + + MODULE.ensure_seeded("mosd", self.database, self.seed) + + self.assertEqual(self.database.stat().st_ino, original_inode) + self.assertEqual(read_control(self.database)[1::4], ("rev-runtime", 1)) + + def test_authority_filename_and_schema_isolation_are_enforced(self) -> None: + cra_seed = self.root / "cra-seed" / "cra.sqlite" + cra_database = self.root / "cra-volume" / "cra.sqlite" + create_cra_database(cra_seed) + MODULE.ensure_seeded("cra", cra_database, cra_seed) + + with self.assertRaisesRegex(MODULE.PublisherError, MODULE.FAILURE_MESSAGE): + MODULE.ensure_seeded("mosd", cra_database, cra_seed) + + wrong_schema = self.root / "other-volume" / "mosd.sqlite" + wrong_schema.parent.mkdir(parents=True) + wrong_schema.write_bytes(cra_seed.read_bytes()) + with self.assertRaisesRegex(MODULE.PublisherError, MODULE.FAILURE_MESSAGE): + MODULE.ensure_seeded("mosd", wrong_schema, self.seed) + + def test_existing_database_must_match_the_seed_structure_exactly(self) -> None: + MODULE.ensure_seeded("mosd", self.database, self.seed) + inode = self.database.stat().st_ino + with sqlite3.connect(self.database) as connection: + connection.execute("DROP VIEW relay_beneficiary_enrolment") + connection.execute( + """ + CREATE VIEW relay_beneficiary_enrolment AS + SELECT record_id, uin FROM beneficiary_enrolment_source + """ + ) + + with self.assertRaisesRegex(MODULE.PublisherError, MODULE.FAILURE_MESSAGE): + MODULE.ensure_seeded("mosd", self.database, self.seed) + self.assertEqual(self.database.stat().st_ino, inode) + + def test_proof_mutates_and_restores_the_exact_row_on_the_same_inode(self) -> None: + MODULE.ensure_seeded("mosd", self.database, self.seed) + before = read_control(self.database) + inode = self.database.stat().st_ino + + MODULE.begin_proof("mosd", self.database, self.seed) + backup = self.database.parent / MODULE.BACKUP_NAME + self.assertEqual(stat.S_IMODE(backup.stat().st_mode), 0o600) + + MODULE.set_proof_state("mosd", self.database, self.seed) + changed = read_control(self.database) + self.assertEqual(changed[5], 1) + self.assertNotEqual(changed[1], before[1]) + self.assertNotEqual(changed[3], before[3]) + self.assertEqual(self.database.stat().st_ino, inode) + + MODULE.restore_proof("mosd", self.database, self.seed) + self.assertEqual(read_control(self.database), before) + self.assertEqual(self.database.stat().st_ino, inode) + self.assertFalse(backup.exists()) + + MODULE.restore_proof("mosd", self.database, self.seed) + self.assertEqual(read_control(self.database), before) + + def test_existing_backup_supports_crash_recovery_without_recapture(self) -> None: + MODULE.ensure_seeded("mosd", self.database, self.seed) + before = read_control(self.database) + MODULE.begin_proof("mosd", self.database, self.seed) + backup = self.database.parent / MODULE.BACKUP_NAME + backup_bytes = backup.read_bytes() + MODULE.set_proof_state("mosd", self.database, self.seed) + + MODULE.begin_proof("mosd", self.database, self.seed) + + self.assertEqual(backup.read_bytes(), backup_bytes) + MODULE.restore_proof("mosd", self.database, self.seed) + self.assertEqual(read_control(self.database), before) + + def test_proof_refuses_other_authorities_missing_backup_and_invalid_backup(self) -> None: + MODULE.ensure_seeded("mosd", self.database, self.seed) + for operation, authority in ( + (MODULE.begin_proof, "cra"), + (MODULE.set_proof_state, "mosd"), + ): + with self.subTest(operation=operation.__name__): + with self.assertRaisesRegex( + MODULE.PublisherError, MODULE.FAILURE_MESSAGE + ): + operation(authority, self.database, self.seed) + + MODULE.begin_proof("mosd", self.database, self.seed) + backup = self.database.parent / MODULE.BACKUP_NAME + backup.chmod(0o644) + with self.assertRaisesRegex(MODULE.PublisherError, MODULE.FAILURE_MESSAGE): + MODULE.set_proof_state("mosd", self.database, self.seed) + + backup.chmod(0o600) + envelope = json.loads(backup.read_text(encoding="utf-8")) + envelope["sha256"] = "0" * 64 + backup.write_text(json.dumps(envelope), encoding="utf-8") + backup.chmod(0o600) + with self.assertRaisesRegex(MODULE.PublisherError, MODULE.FAILURE_MESSAGE): + MODULE.restore_proof("mosd", self.database, self.seed) + + def test_missing_control_row_and_cli_failures_are_redacted(self) -> None: + empty_seed = self.root / "redacted-seed" / "mosd.sqlite" + database = self.root / "sensitive-selector-source-value" / "mosd.sqlite" + create_mosd_database(empty_seed, include_control=False) + MODULE.ensure_seeded("mosd", database, empty_seed) + + result = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "--authority", + "mosd", + "--database", + str(database), + "--seed", + str(empty_seed), + "begin-proof", + ], + check=False, + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 1) + self.assertEqual(result.stdout, "") + self.assertEqual(result.stderr.strip(), MODULE.FAILURE_MESSAGE) + combined = result.stdout + result.stderr + for forbidden in ( + MODULE.MOSD_CONTROL_SELECTOR, + "sensitive-selector-source-value", + str(database), + str(empty_seed), + ): + self.assertNotIn(forbidden, combined) + + def test_cli_accepts_global_options_before_the_subcommand(self) -> None: + result = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "--authority", + "mosd", + "--database", + str(self.database), + "--seed", + str(self.seed), + "ensure-seeded", + ], + check=False, + capture_output=True, + text=True, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), MODULE.SUCCESS_MESSAGE) + self.assertEqual(result.stderr, "") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_local_transit_providers.py b/scripts/test_local_transit_providers.py new file mode 100644 index 0000000..5ed1180 --- /dev/null +++ b/scripts/test_local_transit_providers.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import importlib.util +import socket +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("check-local-transit-providers.py") +SPEC = importlib.util.spec_from_file_location("transit_check", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class TransitProviderTests(unittest.TestCase): + def test_missing_providers_fail_explicitly(self) -> None: + with tempfile.TemporaryDirectory(dir="/tmp", prefix="s") as temporary: + self.assertEqual(len(MODULE.failures(Path(temporary))), 7) + + def test_every_unix_socket_passes(self) -> None: + with tempfile.TemporaryDirectory(dir="/tmp", prefix="s") as temporary: + root = Path(temporary) + sockets = [] + try: + for provider in MODULE.PROVIDERS: + path = root / "config/evidence/local/cells" / provider / "transit/transit-proxy.sock" + path.parent.mkdir(parents=True) + instance = socket.socket(socket.AF_UNIX) + instance.bind(str(path)) + sockets.append(instance) + self.assertEqual(MODULE.failures(root), []) + finally: + for instance in sockets: + instance.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_local_transit_proxy.py b/scripts/test_local_transit_proxy.py new file mode 100644 index 0000000..e075720 --- /dev/null +++ b/scripts/test_local_transit_proxy.py @@ -0,0 +1,325 @@ +from __future__ import annotations + +import base64 +import hashlib +import importlib.util +import json +import socket +import stat +import subprocess +import sys +import tempfile +import threading +import time +import unittest +from pathlib import Path + +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, utils + +SCRIPT = Path(__file__).with_name("local-transit-proxy.py") +SPEC = importlib.util.spec_from_file_location("local_transit_proxy", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +def b64url(raw: bytes) -> str: + return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") + + +def private_jwk(private_key: ec.EllipticCurvePrivateKey) -> dict[str, str]: + private = private_key.private_numbers() + public = private.public_numbers + return { + "alg": "ES256", + "crv": "P-256", + "d": b64url(private.private_value.to_bytes(32, "big")), + "kid": "solmara-test-key", + "kty": "EC", + "x": b64url(public.x.to_bytes(32, "big")), + "y": b64url(public.y.to_bytes(32, "big")), + } + + +class LocalTransitProxyTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory(dir="/tmp", prefix="stp-") + self.root = Path(self.temporary.name) + self.private_key = ec.generate_private_key(ec.SECP256R1()) + self.jwk = private_jwk(self.private_key) + self.key_path = self.root / "signing.jwk" + self.key_path.write_text( + json.dumps(self.jwk, separators=(",", ":"), sort_keys=True), + encoding="utf-8", + ) + self.key_path.chmod(0o600) + self.socket_path = self.root / "transit.sock" + self.server = None + self.thread = None + + def tearDown(self) -> None: + if self.server is not None: + if self.thread is not None and self.thread.is_alive(): + self.server.shutdown() + self.thread.join(timeout=2) + self.server.server_close() + self.temporary.cleanup() + + def start(self) -> None: + self.server = MODULE.build_server( + self.key_path, self.socket_path, "solmara-test-key" + ) + self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) + self.thread.start() + + def request( + self, + method: str, + path: str, + *, + body: bytes = b"", + vault_header: str | None = "true", + content_type: str | None = None, + extra_headers: list[tuple[str, str]] | None = None, + declared_length: int | None = None, + ) -> tuple[int, bytes, bytes]: + headers = [("Host", "localhost"), ("Connection", "close")] + if vault_header is not None: + headers.append(("X-Vault-Request", vault_header)) + if content_type is not None: + headers.append(("Content-Type", content_type)) + if body or declared_length is not None: + headers.append( + ( + "Content-Length", + str(len(body) if declared_length is None else declared_length), + ) + ) + headers.extend(extra_headers or []) + request = ( + f"{method} {path} HTTP/1.1\r\n" + + "".join(f"{name}: {value}\r\n" for name, value in headers) + + "\r\n" + ).encode("ascii") + body + with socket.socket(socket.AF_UNIX) as client: + client.settimeout(2) + client.connect(str(self.socket_path)) + client.sendall(request) + try: + client.shutdown(socket.SHUT_WR) + except OSError: + # A small refusal can be written and closed before shutdown. + pass + chunks = bytearray() + while True: + chunk = client.recv(4096) + if not chunk: + break + chunks.extend(chunk) + head, response_body = bytes(chunks).split(b"\r\n\r\n", 1) + status = int(head.split(b" ", 2)[1]) + return status, head, response_body + + def sign_body(self, payload: bytes, **updates: object) -> bytes: + document: dict[str, object] = { + "input": base64.b64encode(hashlib.sha256(payload).digest()).decode("ascii"), + "key_version": 1, + "marshaling_algorithm": "jws", + "prehashed": True, + } + document.update(updates) + return json.dumps(document, separators=(",", ":")).encode("utf-8") + + def test_metadata_and_prehashed_signature_match_the_transit_contract(self) -> None: + self.start() + status, _, body = self.request("GET", "/v1/transit/keys/solmara-test-key") + self.assertEqual(status, 200) + data = json.loads(body)["data"] + self.assertEqual(data["type"], "ecdsa-p256") + self.assertIs(data["supports_signing"], True) + for field in ("derived", "exportable", "allow_plaintext_backup"): + self.assertIs(data[field], False) + self.assertEqual(data["latest_version"], 1) + self.assertEqual(data["min_encryption_version"], 1) + public_from_metadata = serialization.load_pem_public_key( + data["keys"]["1"]["public_key"].encode("ascii") + ) + self.assertEqual( + public_from_metadata.public_numbers(), + self.private_key.public_key().public_numbers(), + ) + + payload = b"Solmara authority-owned Evidence" + sign_body = self.sign_body(payload) + status, _, body = self.request( + "POST", + "/v1/transit/sign/solmara-test-key/sha2-256", + body=sign_body, + content_type="application/json", + ) + self.assertEqual(status, 200) + signature_text = json.loads(body)["data"]["signature"] + self.assertTrue(signature_text.startswith("vault:v1:")) + raw = base64.urlsafe_b64decode(signature_text.removeprefix("vault:v1:") + "==") + self.assertEqual(len(raw), 64) + der = utils.encode_dss_signature( + int.from_bytes(raw[:32], "big"), int.from_bytes(raw[32:], "big") + ) + self.private_key.public_key().verify( + der, + hashlib.sha256(payload).digest(), + ec.ECDSA(utils.Prehashed(hashes.SHA256())), + ) + + def test_wrong_path_header_and_key_are_generically_refused(self) -> None: + self.start() + cases = [ + self.request("GET", "/v1/transit/keys/another-key"), + self.request("GET", "/v1/other/keys/solmara-test-key"), + self.request( + "GET", "/v1/transit/keys/solmara-test-key", vault_header=None + ), + self.request( + "GET", "/v1/transit/keys/solmara-test-key", vault_header="false" + ), + ] + self.assertEqual([status for status, _, _ in cases], [404, 404, 403, 403]) + self.assertTrue(all(body == MODULE.ERROR_DOCUMENT for _, _, body in cases)) + + def test_wrong_version_and_every_nonexact_body_are_refused(self) -> None: + self.start() + path = "/v1/transit/sign/solmara-test-key/sha2-256" + bodies = [ + self.sign_body(b"payload", key_version=2), + self.sign_body(b"payload", prehashed=False), + self.sign_body(b"payload", marshaling_algorithm="asn1"), + self.sign_body(b"payload", input=base64.b64encode(b"short").decode()), + self.sign_body(b"payload", unexpected=True), + b'{"input":"one","input":"two","key_version":1,"marshaling_algorithm":"jws","prehashed":true}', + b"not-json", + ] + for body in bodies: + with self.subTest(body=body[:40]): + status, _, response = self.request( + "POST", path, body=body, content_type="application/json" + ) + self.assertEqual(status, 400) + self.assertEqual(response, MODULE.ERROR_DOCUMENT) + status, _, response = self.request("POST", path, body=self.sign_body(b"payload")) + self.assertEqual((status, response), (400, MODULE.ERROR_DOCUMENT)) + + def test_request_and_header_bounds_fail_closed(self) -> None: + self.start() + path = "/v1/transit/sign/solmara-test-key/sha2-256" + status, _, response = self.request( + "POST", + path, + content_type="application/json", + declared_length=MODULE.MAX_REQUEST_BODY_BYTES + 1, + ) + self.assertEqual((status, response), (400, MODULE.ERROR_DOCUMENT)) + status, _, response = self.request( + "GET", + "/v1/transit/keys/solmara-test-key", + extra_headers=[("X-Fill", "a" * MODULE.MAX_HEADER_LINE_BYTES)], + ) + self.assertEqual((status, response), (400, MODULE.ERROR_DOCUMENT)) + + def test_key_and_socket_permissions_are_enforced(self) -> None: + self.key_path.chmod(0o644) + with self.assertRaisesRegex(MODULE.ProxyError, "invalid key"): + MODULE.build_server(self.key_path, self.socket_path, "solmara-test-key") + self.key_path.chmod(0o600) + self.start() + metadata = self.socket_path.stat() + self.assertTrue(stat.S_ISSOCK(metadata.st_mode)) + self.assertEqual(stat.S_IMODE(metadata.st_mode), 0o600) + + def test_symlinked_key_and_writable_socket_directory_are_refused(self) -> None: + link = self.root / "link.jwk" + link.symlink_to(self.key_path) + with self.assertRaisesRegex(MODULE.ProxyError, "invalid key"): + MODULE.build_server(link, self.socket_path, "solmara-test-key") + self.root.chmod(0o777) + try: + with self.assertRaisesRegex(MODULE.ProxyError, "invalid socket"): + MODULE.build_server(self.key_path, self.socket_path, "solmara-test-key") + finally: + self.root.chmod(0o700) + + def test_responses_and_errors_never_expose_private_material(self) -> None: + self.start() + private_canary = self.jwk["d"].encode("ascii") + responses = [ + self.request("GET", "/v1/transit/keys/solmara-test-key")[2], + self.request("GET", "/v1/transit/keys/private-canary")[2], + self.request( + "POST", + "/v1/transit/sign/solmara-test-key/sha2-256", + body=b'{"input":"private-canary"}', + content_type="application/json", + )[2], + ] + self.assertTrue(all(private_canary not in response for response in responses)) + metadata = json.loads(responses[0]) + self.assertNotIn("d", metadata["data"]["keys"]["1"]) + application = self.server.application + + class FailingKey: + def sign(self, _digest: bytes, _algorithm: object) -> bytes: + raise ValueError("private-canary") + + application._private_key = FailingKey() + failure = self.request( + "POST", + "/v1/transit/sign/solmara-test-key/sha2-256", + body=self.sign_body(b"payload"), + content_type="application/json", + ) + self.assertEqual((failure[0], failure[2]), (500, MODULE.ERROR_DOCUMENT)) + self.assertNotIn(b"private-canary", failure[2]) + malformed = dict(self.jwk) + malformed["d"] = "private-canary" + self.key_path.write_text(json.dumps(malformed), encoding="utf-8") + self.key_path.chmod(0o600) + with self.assertRaises(MODULE.ProxyError) as context: + MODULE.TransitApplication(self.key_path, "solmara-test-key") + self.assertNotIn("private-canary", str(context.exception)) + + def test_cli_sigterm_stops_cleanly_and_removes_its_socket(self) -> None: + process = subprocess.Popen( + [ + sys.executable, + str(SCRIPT), + "--private-jwk", + str(self.key_path), + "--socket", + str(self.socket_path), + "--key-name", + "solmara-test-key", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + try: + for _ in range(100): + if self.socket_path.exists(): + break + self.assertIsNone(process.poll()) + time.sleep(0.01) + else: + self.fail("proxy socket did not become ready") + process.terminate() + stdout, stderr = process.communicate(timeout=2) + finally: + if process.poll() is None: + process.kill() + process.wait(timeout=2) + self.assertEqual(process.returncode, 0) + self.assertEqual((stdout, stderr), (b"", b"")) + self.assertFalse(self.socket_path.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_local_transit_signers.py b/scripts/test_local_transit_signers.py new file mode 100644 index 0000000..e2603f2 --- /dev/null +++ b/scripts/test_local_transit_signers.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import importlib.util +import os +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("local-transit-signers.py") +SPEC = importlib.util.spec_from_file_location("local_transit_signers", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class LocalTransitSignerOrchestrationTests(unittest.TestCase): + def test_exact_provider_and_key_inventory(self) -> None: + self.assertEqual( + MODULE.PROVIDERS, + ("mint", "cra", "nia", "sro", "mosd-programme", "sipf", "nagdi"), + ) + self.assertEqual(MODULE.key_name("mint"), "solmara-mint") + for provider in MODULE.PROVIDERS[1:]: + self.assertEqual(MODULE.key_name(provider), f"solmara-evidence-{provider}") + private_jwk, socket_path, _ = MODULE.paths(provider) + self.assertEqual(private_jwk.name, "signing.jwk") + self.assertEqual(socket_path.name, "transit-proxy.sock") + + def test_stop_never_claims_an_unrelated_pid(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + pid_file = Path(temporary) / "unrelated.pid" + pid_file.write_text(f"{os.getpid()}\n", encoding="ascii") + self.assertIsNone(MODULE.owned_process(pid_file)) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_metadata_authority_contracts.py b/scripts/test_metadata_authority_contracts.py index 43d69d3..1322808 100644 --- a/scripts/test_metadata_authority_contracts.py +++ b/scripts/test_metadata_authority_contracts.py @@ -10,24 +10,41 @@ class AuthorityMetadataContractTests(unittest.TestCase): - def test_notary_data_services_publish_real_runtime_routes(self) -> None: + def test_six_authority_evidence_services_publish_real_runtime_routes(self) -> None: catalog = json.loads((METADATA / "catalog.json").read_text(encoding="utf-8")) authority_services = [ service for service in catalog["data_services"] - if service["id"].endswith("-notary-api") + if service["id"].endswith("-evidence-api") ] self.assertEqual(len(authority_services), 6) + self.assertEqual( + {service["id"] for service in authority_services}, + { + "cra-evidence-api", + "nia-evidence-api", + "sro-evidence-api", + "mosd-programme-evidence-api", + "sipf-evidence-api", + "nagdi-evidence-api", + }, + ) for service in authority_services: with self.subTest(service=service["id"]): - self.assertTrue(service["iri"].endswith("/v1/evaluations")) + self.assertTrue(service["iri"].endswith("/v1/evidence")) self.assertEqual(service["endpoint_url"], service["iri"]) self.assertTrue( - service["endpoint_description"].endswith("/openapi.json") + service["endpoint_description"].endswith( + "/v1/evidence-definitions" + ) + ) + self.assertEqual( + service["conforms_to"], + "https://id.registrystack.org/spec/registry-evidence/v1", ) - def test_authority_offerings_publish_evaluation_and_discovery_routes(self) -> None: + def test_all_eleven_operational_requirements_have_authority_offerings(self) -> None: document = json.loads( (METADATA / "evidence-offerings.json").read_text(encoding="utf-8") ) @@ -37,41 +54,49 @@ def test_authority_offerings_publish_evaluation_and_discovery_routes(self) -> No if offering["access"]["kind"] == "evidence-verification-api" ] - self.assertEqual(len(authority_offerings), 8) + self.assertEqual(len(authority_offerings), 11) + self.assertEqual( + {offering["access"]["source_type"] for offering in authority_offerings}, + {"immutable extract", "Relay lookup"}, + ) for offering in authority_offerings: with self.subTest(offering=offering["id"]): access = offering["access"] - self.assertTrue(access["endpoint_url"].endswith("/v1/evaluations")) + self.assertTrue(access["endpoint_url"].endswith("/v1/evidence")) self.assertTrue( - access["discovery_url"].endswith("/.well-known/evidence-service") + access["discovery_url"].endswith("/v1/evidence-definitions") ) - def test_offering_purposes_match_notary_services(self) -> None: + def test_offering_purposes_match_authority_requirements(self) -> None: document = json.loads( (METADATA / "evidence-offerings.json").read_text(encoding="utf-8") ) offerings = {offering["id"]: offering for offering in document["offerings"]} self.assertEqual( - offerings["sipf-pensions-pension-case-offering"]["purposes"], + offerings["sipf-pension-payment-v1-offering"]["purposes"], + ["https://id.registrystack.org/solmara/purpose/pension-payment-review"], + ) + self.assertEqual( + offerings["sipf-survivor-benefit-v1-offering"]["purposes"], [ - "https://id.registrystack.org/solmara/purpose/pension-payment-review", - "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination", + "https://id.registrystack.org/solmara/purpose/survivor-benefit-determination" ], ) self.assertEqual( - offerings["nagdi-agriculture-farmer-voucher-offering"]["purposes"], + offerings["nagdi-voucher-v1-offering"]["purposes"], ["https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"], ) self.assertEqual( - offerings["nagdi-agriculture-livestock-movement-offering"]["purposes"], + offerings["nagdi-livestock-v1-offering"]["purposes"], ["https://id.registrystack.org/solmara/purpose/livestock-movement-control"], ) def test_cra_offerings_publish_only_the_supported_uin_lookup(self) -> None: for offering_id in ( - "cra-birth-registration-offering", - "cra-death-registration-offering", + "cra-child-benefit-v1-offering", + "cra-pension-death-v1-offering", + "cra-citizen-record-v1-offering", ): with self.subTest(offering=offering_id): offering = json.loads( @@ -90,8 +115,8 @@ def test_survivor_example_targets_the_surviving_spouse(self) -> None: / "03 - Read survivor eligibility.bru" ).read_text(encoding="utf-8") - self.assertIn('"value": "2300118698"', request) - self.assertNotIn('"value": "2300109568"', request) + self.assertIn('"values": {"uin": "2300118698"}', request) + self.assertNotIn('"values": {"uin": "2300109568"}', request) if __name__ == "__main__": diff --git a/scripts/test_notary_state_restart.py b/scripts/test_notary_state_restart.py deleted file mode 100644 index c32e30e..0000000 --- a/scripts/test_notary_state_restart.py +++ /dev/null @@ -1,486 +0,0 @@ -"""Deterministic tests for the Notary PostgreSQL restart-persistence gate.""" - -from __future__ import annotations - -import importlib.util -import io -import json -import sys -import tempfile -import unittest -from contextlib import redirect_stdout -from pathlib import Path - - -SCRIPT = Path(__file__).with_name("notary_state_restart.py") -SPEC = importlib.util.spec_from_file_location("notary_state_restart", SCRIPT) -if SPEC is None or SPEC.loader is None: - raise RuntimeError(f"could not load {SCRIPT}") -restart = importlib.util.module_from_spec(SPEC) -sys.modules[SPEC.name] = restart -SPEC.loader.exec_module(restart) - - -def valid_compose_config() -> dict[str, object]: - return { - "services": { - "postgres": { - "volumes": [ - { - "type": "volume", - "source": "postgres-data", - "target": "/var/lib/postgresql/data", - "read_only": False, - } - ] - } - }, - "volumes": {"postgres-data": {"name": "demo_postgres-data"}}, - } - - -def valid_counts(*, increment: int = 0) -> tuple[tuple[str, str, int], ...]: - rows = [] - for authority_index, (authority, _service) in enumerate(restart.AUTHORITIES): - for table_index, table in enumerate(restart.CORRECTNESS_TABLES): - rows.append( - (authority, table, authority_index + table_index + 1 + increment) - ) - return tuple(sorted(rows)) - - -def snapshot( - *, - container_id: str = "postgres-before", - system_identifier: str = "7541234567890123456", - version: int = 160010, - data_directory: str = "/var/lib/postgresql/data", - volume_name: str = "demo_postgres-data", - counts: tuple[tuple[str, str, int], ...] | None = None, -) -> restart.StateSnapshot: - return restart.StateSnapshot( - container_id=container_id, - system_identifier=system_identifier, - server_version_num=version, - data_directory=data_directory, - volume_name=volume_name, - counts=valid_counts() if counts is None else counts, - ) - - -class RecordingRunner: - def __init__(self, *, outputs: list[str] | None = None) -> None: - self.calls: list[tuple[list[str], Path, dict[str, str]]] = [] - self.outputs = list(outputs or []) - self.now = 0.0 - - def run( - self, - command: list[str] | tuple[str, ...], - *, - cwd: Path, - env: dict[str, str], - ) -> str: - self.calls.append((list(command), cwd, dict(env))) - return self.outputs.pop(0) if self.outputs else "" - - def monotonic(self) -> float: - return self.now - - def sleep(self, seconds: float) -> None: - self.now += seconds - - -class ComposeLayoutTests(unittest.TestCase): - def test_accepts_exact_postgresql_16_named_volume(self) -> None: - self.assertEqual( - restart.validate_compose_pgdata_layout(valid_compose_config()), - "demo_postgres-data", - ) - - def test_rejects_nonexact_storage_mounts(self) -> None: - invalid_mounts = ( - { - "type": "volume", - "source": "other", - "target": "/var/lib/postgresql/data", - }, - { - "type": "bind", - "source": "./postgres-data", - "target": "/var/lib/postgresql/data", - }, - { - "type": "volume", - "source": "postgres-data", - "target": "/var/lib/postgresql/data", - "read_only": True, - }, - { - "type": "volume", - "source": "postgres-data", - "target": "/var/lib/postgresql", - }, - { - "type": "volume", - "source": "postgres-data", - "target": "/var/lib/postgresql/data/nested", - }, - ) - for mount in invalid_mounts: - with self.subTest(mount=mount): - config = valid_compose_config() - config["services"]["postgres"]["volumes"] = [mount] - with self.assertRaises(restart.GateError): - restart.validate_compose_pgdata_layout(config) - - def test_rejects_second_mount_above_pgdata(self) -> None: - config = valid_compose_config() - config["services"]["postgres"]["volumes"].append( - { - "type": "bind", - "source": "/tmp", - "target": "/var/lib", - } - ) - with self.assertRaises(restart.GateError): - restart.validate_compose_pgdata_layout(config) - - -class RuntimeMountTests(unittest.TestCase): - def setUp(self) -> None: - self.mount = { - "Type": "volume", - "Name": "demo_postgres-data", - "Destination": "/var/lib/postgresql/data", - "RW": True, - } - self.labels = { - "com.docker.compose.project": "demo", - "com.docker.compose.volume": "postgres-data", - } - - def validate(self, mounts: object, labels: object | None = None) -> None: - restart.validate_runtime_pgdata_mounts( - mounts, - expected_volume="demo_postgres-data", - volume_labels=self.labels if labels is None else labels, - compose_project_name="demo", - ) - - def test_accepts_exact_runtime_mount_and_labels(self) -> None: - self.validate([self.mount]) - - def test_rejects_anonymous_bind_readonly_parent_and_nested_mounts(self) -> None: - changes = ( - {"Name": ""}, - {"Type": "bind"}, - {"RW": False}, - {"Destination": "/var/lib/postgresql"}, - {"Destination": "/var/lib/postgresql/data/nested"}, - ) - for change in changes: - with self.subTest(change=change): - mount = {**self.mount, **change} - with self.assertRaises(restart.GateError): - self.validate([mount]) - - def test_rejects_any_overlapping_second_mount(self) -> None: - for destination in ( - "/", - "/var/lib", - "/var/lib/postgresql", - "/var/lib/postgresql/data/nested", - ): - with self.subTest(destination=destination): - overlapping = { - "Type": "bind", - "Source": "/tmp", - "Destination": destination, - "RW": True, - } - with self.assertRaises(restart.GateError): - self.validate([self.mount, overlapping]) - - def test_rejects_wrong_compose_ownership_labels(self) -> None: - for key in self.labels: - with self.subTest(key=key): - labels = {**self.labels, key: "other"} - with self.assertRaises(restart.GateError): - self.validate([self.mount], labels) - - -class SnapshotParsingTests(unittest.TestCase): - def test_parses_postgresql_16_control_snapshot(self) -> None: - self.assertEqual( - restart.parse_control_snapshot( - "7541234567890123456\n160010\n/var/lib/postgresql/data\n" - ), - ("7541234567890123456", 160010, "/var/lib/postgresql/data"), - ) - - def test_rejects_wrong_major_or_data_directory(self) -> None: - for output in ( - "7541234567890123456\n170001\n/var/lib/postgresql/data\n", - "7541234567890123456\n160010\n/var/lib/postgresql/16/docker\n", - "not-a-number\n160010\n/var/lib/postgresql/data\n", - ): - with self.subTest(output=output): - with self.assertRaises(restart.GateError): - restart.parse_control_snapshot(output) - - def test_parses_exact_nine_authority_counts(self) -> None: - output = "\n".join( - f"{table}|{index + 1}" - for index, table in enumerate(restart.CORRECTNESS_TABLES) - ) - parsed = restart.parse_authority_counts("cra", output) - self.assertEqual(set(parsed), set(restart.CORRECTNESS_TABLES)) - self.assertEqual(sum(parsed.values()), 45) - - def test_rejects_zero_missing_duplicate_or_unknown_counts(self) -> None: - valid_lines = [ - f"{table}|1" for table in restart.CORRECTNESS_TABLES - ] - invalid = ( - [f"{table}|0" for table in restart.CORRECTNESS_TABLES], - valid_lines[:-1], - [*valid_lines, valid_lines[0]], - [*valid_lines[:-1], "schema_metadata|1"], - ) - for lines in invalid: - with self.subTest(lines=lines): - with self.assertRaises(restart.GateError): - restart.parse_authority_counts("cra", "\n".join(lines)) - - def test_count_query_has_only_the_nine_correctness_tables(self) -> None: - query = restart.RestartPersistenceGate._counts_sql() - for table in restart.CORRECTNESS_TABLES: - self.assertIn(f"registry_notary_private.{table}", query) - self.assertNotIn("schema_metadata", query) - self.assertEqual(query.count("count(*)"), 9) - - -class SnapshotComparisonTests(unittest.TestCase): - def test_accepts_replacement_container_with_identical_state(self) -> None: - restart.compare_snapshots( - snapshot(container_id="postgres-before"), - snapshot(container_id="postgres-after"), - ) - - def test_rejects_reused_container_or_changed_identity_metadata_and_counts(self) -> None: - after_changes = ( - {}, - {"container_id": "postgres-after", "system_identifier": "123"}, - {"container_id": "postgres-after", "version": 160011}, - { - "container_id": "postgres-after", - "data_directory": "/var/lib/postgresql/other", - }, - {"container_id": "postgres-after", "volume_name": "other"}, - { - "container_id": "postgres-after", - "counts": valid_counts(increment=1), - }, - ) - before = snapshot() - for change in after_changes: - with self.subTest(change=change): - with self.assertRaises(restart.GateError): - restart.compare_snapshots(before, snapshot(**change)) - - -class ResolveComposeTests(unittest.TestCase): - def test_preserves_explicit_project_and_inherited_image_overrides(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - for name in (".env", "versions.env", "compose.yaml"): - (root / name).touch() - runner = RecordingRunner(outputs=[json.dumps(valid_compose_config())]) - environment = { - "COMPOSE_PROJECT_NAME": "explicit-project", - "POSTGRES_IMAGE": "postgres:16.99", - "REGISTRY_NOTARY_IMAGE": "notary:test", - } - gate = restart.RestartPersistenceGate( - root=root, - runner=runner, - environ=environment, - ) - - gate.resolve_compose() - - self.assertEqual(gate.compose_project_name, "explicit-project") - self.assertEqual(gate.expected_volume, "demo_postgres-data") - self.assertEqual(len(runner.calls), 1) - command, cwd, used_environment = runner.calls[0] - self.assertEqual(command[-3:], ["config", "--format", "json"]) - self.assertEqual(cwd, root.resolve()) - self.assertEqual(used_environment, environment) - - -class WaitForStackTests(unittest.TestCase): - class HealthyRunner(RecordingRunner): - def __init__(self, *, installer_exit_code: int = 0) -> None: - super().__init__() - self.installer_exit_code = installer_exit_code - - def run( - self, - command: list[str] | tuple[str, ...], - *, - cwd: Path, - env: dict[str, str], - ) -> str: - command = list(command) - self.calls.append((command, cwd, dict(env))) - if "ps" in command and "-q" in command: - return "container-" + command[-1] - if command[:3] == ["docker", "inspect", "--format"]: - template = command[3] - if "ExitCode" in template: - return f"exited|{self.installer_exit_code}" - return "healthy" - raise AssertionError(f"unexpected command: {command}") - - def gate(self, runner: RecordingRunner) -> restart.RestartPersistenceGate: - gate = restart.RestartPersistenceGate( - root=Path.cwd(), - runner=runner, - environ={"COMPOSE_PROJECT_NAME": "demo"}, - wait_seconds=5, - ) - gate.compose = ["docker", "compose"] - return gate - - def test_waits_for_all_healthchecks_and_installers(self) -> None: - runner = self.HealthyRunner() - self.gate(runner).wait_for_stack() - compose_ps = [call[0] for call in runner.calls if "ps" in call[0]] - self.assertEqual(len(compose_ps), 13) - installer_services = { - command[-1] for command in compose_ps if "--all" in command - } - self.assertEqual( - installer_services, - { - f"{service}-state-install" - for _authority, service in restart.AUTHORITIES - }, - ) - self.assertEqual(runner.now, 0.0) - - def test_fails_immediately_when_an_installer_exits_nonzero(self) -> None: - runner = self.HealthyRunner(installer_exit_code=7) - with self.assertRaisesRegex(restart.GateError, "installer failed"): - self.gate(runner).wait_for_stack() - - def test_times_out_with_the_pending_service(self) -> None: - runner = self.HealthyRunner() - - def unhealthy_run( - command: list[str] | tuple[str, ...], - *, - cwd: Path, - env: dict[str, str], - ) -> str: - output = WaitForStackTests.HealthyRunner.run( - runner, command, cwd=cwd, env=env - ) - if command[:3] == ["docker", "inspect", "--format"]: - if "ExitCode" not in command[3]: - return "starting" - return output - - runner.run = unhealthy_run - with self.assertRaisesRegex(restart.GateError, "postgres=starting"): - self.gate(runner).wait_for_stack() - self.assertGreaterEqual(runner.now, 5.0) - - -class OrchestrationTests(unittest.TestCase): - class HarnessGate(restart.RestartPersistenceGate): - def __init__(self, runner: RecordingRunner, events: list[str]) -> None: - super().__init__( - root=Path.cwd(), - runner=runner, - environ={ - "COMPOSE_PROJECT_NAME": "explicit-project", - "POSTGRES_IMAGE": "postgres:16.99", - "REGISTRY_NOTARY_IMAGE": "notary:test", - }, - ) - self.events = events - self.snapshots = [ - snapshot(container_id="postgres-before"), - snapshot(container_id="postgres-after"), - ] - - def resolve_compose(self) -> None: - self.events.append("resolve") - self.compose_project_name = self.env["COMPOSE_PROJECT_NAME"] - self.compose = ["docker", "compose"] - self.expected_volume = "demo_postgres-data" - - def wait_for_stack(self) -> None: - self.events.append("wait") - - def doctor_all(self) -> None: - self.events.append("doctor") - - def capture_snapshot(self) -> restart.StateSnapshot: - name = "capture-before" if len(self.snapshots) == 2 else "capture-after" - self.events.append(name) - return self.snapshots.pop(0) - - def service_container_id( - self, service: str, *, include_stopped: bool = False - ) -> str: - self.events.append(f"assert-removed:{service}:{include_stopped}") - return "" - - def test_exact_down_up_order_and_inherited_environment(self) -> None: - events: list[str] = [] - runner = RecordingRunner() - gate = self.HarnessGate(runner, events) - - with redirect_stdout(io.StringIO()): - gate.run_gate() - - self.assertEqual( - events, - [ - "resolve", - "wait", - "doctor", - "capture-before", - "assert-removed:postgres:True", - "wait", - "capture-after", - "doctor", - ], - ) - self.assertEqual([call[0] for call in runner.calls], [["just", "down"], ["just", "up"]]) - for _command, cwd, environment in runner.calls: - self.assertEqual(cwd, Path.cwd().resolve()) - self.assertEqual(environment["COMPOSE_PROJECT_NAME"], "explicit-project") - self.assertEqual(environment["POSTGRES_IMAGE"], "postgres:16.99") - self.assertEqual(environment["REGISTRY_NOTARY_IMAGE"], "notary:test") - - def test_command_failure_stops_before_up(self) -> None: - class FailingRunner(RecordingRunner): - def run(self, command, *, cwd, env): - super().run(command, cwd=cwd, env=env) - raise restart.CommandError("down failed") - - events: list[str] = [] - runner = FailingRunner() - gate = self.HarnessGate(runner, events) - with self.assertRaisesRegex(restart.CommandError, "down failed"): - gate.run_gate() - self.assertEqual([call[0] for call in runner.calls], [["just", "down"]]) - self.assertEqual(events[-1], "capture-before") - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_project_runtime_secrets.py b/scripts/test_project_runtime_secrets.py new file mode 100644 index 0000000..845fbe7 --- /dev/null +++ b/scripts/test_project_runtime_secrets.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import importlib.util +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("project-runtime-secrets.py") +SPEC = importlib.util.spec_from_file_location("secret_projection", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class RuntimeSecretProjectionTests(unittest.TestCase): + def test_signing_keys_never_enter_runtime_projection(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + private, output = root / "private", root / "runtime" + for cell, clients in MODULE.CELL_CLIENTS.items(): + secrets = private / cell / "secrets" + secrets.mkdir(parents=True) + client_files = tuple(f"{client}-{suffix}" for client in clients for suffix in ("client-id", "client-key")) + for name in ("signing.jwk", "audit-hmac-key", "subject-binding-hmac-key", *client_files): + (secrets / name).write_text(name) + mint = private / "mint/secrets" + mint.mkdir(parents=True) + (mint / "signing.jwk").write_text("private") + (mint / "audit-hmac-key").write_text("audit") + MODULE.project(private, output) + self.assertFalse(list(output.rglob("signing.jwk"))) + self.assertTrue((output / "cra/cra-pension-evidence-client-key").exists()) + self.assertEqual( + (output / "cra/cra-pension-evidence-client-id").read_text(), + "cra-pension-evidence-client-id", + ) + self.assertEqual((output / "mint/audit-hmac-key").read_text(), "audit") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_provision_hosted_runtime.py b/scripts/test_provision_hosted_runtime.py new file mode 100644 index 0000000..89102de --- /dev/null +++ b/scripts/test_provision_hosted_runtime.py @@ -0,0 +1,341 @@ +from __future__ import annotations + +import hashlib +import importlib.util +import json +import os +import sqlite3 +import stat +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest import mock + +import yaml + + +SCRIPT = Path(__file__).with_name("provision-hosted-runtime.py") +SPEC = importlib.util.spec_from_file_location("provision_hosted_runtime", SCRIPT) +assert SPEC and SPEC.loader +provisioner = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(provisioner) + + +def write_manifest(root: Path) -> None: + files = { + path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted(root.rglob("*")) + if path.is_file() and path.name != "manifest.json" + } + (root / "manifest.json").write_text( + json.dumps({"format": 1, "files": files}, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +class HostedProvisionerTests(unittest.TestCase): + def test_binary_write_is_exact(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + target = Path(temporary) / "source.sqlite" + content = b"SQLite format 3\x00\n\xff\x00" + provisioner._write(target, content, 0o444) + self.assertEqual(target.read_bytes(), content) + self.assertEqual(stat.S_IMODE(target.stat().st_mode), 0o444) + + def test_install_resumes_exact_partial_tree_and_refuses_mismatch(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + staged, destination = root / "staged", root / "destination" + (staged / "nested").mkdir(parents=True) + provisioner._write(staged / "one", b"one", 0o444) + provisioner._write(staged / "nested" / "two", b"two", 0o444) + destination.mkdir() + provisioner._write(destination / "one", b"one", 0o444) + + provisioner._install_tree(staged, destination, root_mode=0o555) + self.assertEqual((destination / "nested" / "two").read_bytes(), b"two") + self.assertEqual(stat.S_IMODE(destination.stat().st_mode), 0o555) + + (destination / "one").chmod(0o644) + with self.assertRaises(provisioner.ProvisionError): + provisioner._install_tree(staged, destination, root_mode=0o555) + + def test_secret_reader_rejects_writable_and_symbolic_files(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + safe = root / "safe" + safe.write_bytes(b"value") + safe.chmod(0o444) + self.assertEqual(provisioner._read_secret(root, "safe"), b"value") + + safe.chmod(0o666) + with self.assertRaises(provisioner.ProvisionError): + provisioner._read_secret(root, "safe") + safe.chmod(0o444) + os.symlink(safe, root / "link") + with self.assertRaises(provisioner.ProvisionError): + provisioner._read_secret(root, "link") + + def test_hmac_secret_rejects_weak_or_non_text_material(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + secret = root / "hmac" + secret.write_bytes(b"short") + secret.chmod(0o400) + with self.assertRaises(provisioner.ProvisionError): + provisioner._hmac_secret(root, "hmac") + secret.chmod(0o600) + secret.write_bytes(b"a" * 31 + b"\x00") + secret.chmod(0o400) + with self.assertRaises(provisioner.ProvisionError): + provisioner._hmac_secret(root, "hmac") + secret.chmod(0o600) + secret.write_bytes(b"a" * 32) + secret.chmod(0o400) + self.assertEqual(provisioner._hmac_secret(root, "hmac"), b"a" * 32) + + def test_relay_provision_preserves_database_and_is_idempotent(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + assets = root / "assets" + relay = assets / "relays" / "cra" + (relay / "package").mkdir(parents=True) + (relay / "package" / "sealed.json").write_bytes(b"sealed") + (relay / "source").mkdir() + database = b"SQLite format 3\x00\n\xff\x00" + (relay / "source" / "cra.sqlite").write_bytes(database) + (relay / "runtime.yaml").write_text("version: 1\n", encoding="utf-8") + write_manifest(assets) + secrets = root / "unused-secrets" + secrets.mkdir() + runtime, source = root / "runtime", root / "source" + arguments = provisioner.parser().parse_args( + [ + "provision", + "--target", + "cra-relay", + "--assets", + str(assets), + "--secrets", + str(secrets), + "--runtime-output", + str(runtime), + "--source-output", + str(source), + ] + ) + + provisioner.provision(arguments) + provisioner.provision(arguments) + self.assertEqual((source / "cra.sqlite").read_bytes(), database) + self.assertEqual( + (runtime / "package" / "sealed.json").read_bytes(), b"sealed" + ) + + def test_relay_cli_loads_manifest_verifier_directly(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + assets = root / "assets" + relay = assets / "relays" / "cra" + (relay / "package").mkdir(parents=True) + (relay / "package" / "sealed.json").write_bytes(b"sealed") + (relay / "source").mkdir() + (relay / "source" / "cra.sqlite").write_bytes(b"SQLite format 3\x00") + (relay / "runtime.yaml").write_text("version: 1\n", encoding="utf-8") + write_manifest(assets) + runtime, source = root / "runtime", root / "source" + completed = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "provision", + "--target", + "cra-relay", + "--assets", + str(assets), + "--secrets", + str(root / "unused"), + "--runtime-output", + str(runtime), + "--source-output", + str(source), + ], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertEqual(completed.stdout.strip(), provisioner.SUCCESS) + self.assertEqual(completed.stderr, "") + + def test_existing_direct_extract_publication_is_reused(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + runtime, extracts = root / "runtime", root / "extracts" + runtime.mkdir() + extracts.mkdir() + published_at = "2026-08-12T09:00:00Z" + extract_id = "sro-poverty-20260812T090000Z" + extract = extracts / f"{extract_id}.sqlite" + with sqlite3.connect(extract) as connection: + connection.execute( + "CREATE TABLE evidence_extract (published_at TEXT, publisher TEXT, extract_id TEXT)" + ) + connection.execute( + "INSERT INTO evidence_extract VALUES (?, ?, ?)", + (published_at, "did:web:example", extract_id), + ) + (runtime / "runtime.yaml").write_text( + yaml.safe_dump( + { + "sourceExtracts": { + "sro-poverty-extract": {"path": f"/extracts/{extract.name}"} + } + } + ), + encoding="utf-8", + ) + publisher = mock.Mock() + with mock.patch.object( + provisioner, "_load_publisher", return_value=publisher + ): + observed = provisioner._publication_time( + root / "assets", + "sro", + runtime, + extracts, + "2026-08-12T09:01:00Z", + ) + self.assertEqual(observed, published_at) + publisher.validate_extract.assert_called_once() + + def test_orphan_direct_extract_recovers_after_pre_runtime_crash(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + runtime, extracts = root / "runtime", root / "extracts" + runtime.mkdir() + extracts.mkdir() + published_at = "2026-08-12T09:00:00Z" + extract_id = "sro-poverty-20260812T090000Z" + extract = extracts / f"{extract_id}.sqlite" + extract.write_bytes(b"immutable") + extract.chmod(0o444) + metadata = SimpleNamespace(published_at=published_at, extract_id=extract_id) + publisher = mock.Mock() + publisher.validate_extract.return_value = metadata + with mock.patch.object( + provisioner, "_load_publisher", return_value=publisher + ): + observed = provisioner._publication_time( + root / "assets", + "sro", + runtime, + extracts, + "2026-08-12T09:01:00Z", + ) + self.assertEqual(observed, published_at) + publisher.validate_extract.assert_called_once_with( + extract, "sro", observed_at="2026-08-12T09:01:00Z" + ) + + def test_extract_publication_appends_and_atomically_rebinds(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + runtime, extracts = root / "runtime", root / "extracts" + runtime.mkdir() + extracts.mkdir() + previous_name = "sro-poverty-20260812T090000Z.sqlite" + replacement_name = "sro-poverty-20260812T100000Z.sqlite" + (extracts / previous_name).write_bytes(b"previous") + (extracts / previous_name).chmod(0o444) + extracts.chmod(0o555) + (runtime / "runtime.yaml").write_text( + yaml.safe_dump( + { + "listener": {"bindHost": "172.29.1.23"}, + "sourceExtracts": { + "sro-poverty-extract": { + "path": f"/var/lib/registry-evidence/sro/extracts/{previous_name}" + } + }, + }, + sort_keys=False, + ), + encoding="utf-8", + ) + (runtime / "runtime.yaml").chmod(0o444) + runtime.chmod(0o555) + publisher = mock.Mock() + publisher.timestamped_extract_id.return_value = previous_name.removesuffix( + ".sqlite" + ) + + def stage(_assets, _cell, destination, _published_at, _observed_at): + (destination / replacement_name).write_bytes(b"replacement") + (destination / replacement_name).chmod(0o444) + return replacement_name + + arguments = SimpleNamespace( + target="sro-evidence", + assets=root / "assets", + runtime_output=runtime, + extract_output=extracts, + ) + with ( + mock.patch.object(provisioner, "verify_assets"), + mock.patch.object( + provisioner, + "_publication_time", + return_value="2026-08-12T09:00:00Z", + ), + mock.patch.object( + provisioner, "_load_publisher", return_value=publisher + ), + mock.patch.object(provisioner, "_stage_extract", side_effect=stage), + ): + provisioner.publish_extract(arguments) + + self.assertEqual((extracts / previous_name).read_bytes(), b"previous") + self.assertEqual((extracts / replacement_name).read_bytes(), b"replacement") + config = yaml.safe_load((runtime / "runtime.yaml").read_text()) + self.assertEqual( + Path(config["sourceExtracts"]["sro-poverty-extract"]["path"]).name, + replacement_name, + ) + self.assertEqual(stat.S_IMODE(runtime.stat().st_mode), 0o555) + self.assertEqual(stat.S_IMODE(extracts.stat().st_mode), 0o555) + + def test_evidence_bind_address_is_closed_before_staging(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + arguments = provisioner.parser().parse_args( + [ + "provision", + "--target", + "sipf-evidence", + "--assets", + str(root / "assets"), + "--secrets", + str(root / "secrets"), + "--runtime-output", + str(root / "runtime"), + "--secret-output", + str(root / "output-secrets"), + "--bind-host", + "172.29.1.99", + ] + ) + with ( + mock.patch.object(provisioner, "verify_assets"), + mock.patch.object(provisioner, "_stage_evidence") as stage, + self.assertRaises(provisioner.ProvisionError), + ): + provisioner.provision(arguments) + stage.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_publish_runtime_extracts.py b/scripts/test_publish_runtime_extracts.py new file mode 100644 index 0000000..932eec5 --- /dev/null +++ b/scripts/test_publish_runtime_extracts.py @@ -0,0 +1,280 @@ +from __future__ import annotations + +import hashlib +import importlib.util +import json +import shutil +import sqlite3 +import stat +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = Path(__file__).with_name("publish-runtime-extracts.py") +SPEC = importlib.util.spec_from_file_location("runtime_extract_publication", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) +PUBLISHER = importlib.import_module("solmara_lab.publisher") + + +def digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +class RuntimeExtractPublicationTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + for authority in MODULE.AUTHORITIES: + authored = self.root / "evidence/cells" / authority / "runtime.yaml" + authored.parent.mkdir(parents=True) + shutil.copy2(ROOT / "evidence/cells" / authority / "runtime.yaml", authored) + generated = ( + self.root + / MODULE.RUNTIME_DIRECTORY + / authority + / "runtime.yaml" + ) + generated.parent.mkdir(parents=True) + shutil.copy2(authored, generated) + generated.chmod(0o444) + generated.parent.chmod(0o555) + + def tearDown(self) -> None: + for path in sorted(self.root.rglob("*"), reverse=True): + if path.is_dir(): + path.chmod(0o755) + elif path.exists(): + path.chmod(0o644) + self.temporary.cleanup() + + def _runtime_binding(self, authority: str) -> str: + runtime = yaml.safe_load( + ( + self.root + / MODULE.RUNTIME_DIRECTORY + / authority + / "runtime.yaml" + ).read_text(encoding="utf-8") + ) + profile = MODULE.AUTHORITIES[authority][0] + return runtime["sourceExtracts"][profile]["path"] + + def _extracts(self) -> list[Path]: + return sorted((self.root / PUBLISHER.EVIDENCE_DIRECTORY).glob("*.sqlite")) + + def _authored_digests(self) -> dict[Path, str]: + return { + path: digest(path) + for path in sorted((self.root / "evidence/cells").rglob("*")) + if path.is_file() + } + + def test_fresh_publication_binds_only_generated_runtime_configs(self) -> None: + authored_before = self._authored_digests() + published_at = "2026-08-12T09:30:00.123456Z" + + result = MODULE.prepare_runtime_extracts(self.root, published_at) + + self.assertEqual( + {authority: item["status"] for authority, item in result.items()}, + {authority: "published" for authority in MODULE.AUTHORITIES}, + ) + self.assertEqual(len(self._extracts()), 3) + for authority, item in result.items(): + expected_id = PUBLISHER.timestamped_extract_id(authority, published_at) + self.assertEqual(item["extractId"], expected_id) + self.assertEqual( + self._runtime_binding(authority), + f"/var/lib/registry-evidence/{authority}/extracts/{expected_id}.sqlite", + ) + extract = self.root / item["path"] + self.assertEqual(stat.S_IMODE(extract.stat().st_mode), 0o444) + with sqlite3.connect(extract) as connection: + self.assertEqual( + connection.execute( + "SELECT published_at, publisher, extract_id FROM evidence_extract" + ).fetchall(), + [(published_at, PUBLISHER.PUBLISHERS[authority], expected_id)], + ) + self.assertEqual(self._authored_digests(), authored_before) + + def test_default_publication_uses_one_explicit_current_utc_time(self) -> None: + current = "2026-08-12T09:30:00.654321Z" + with mock.patch.object( + MODULE, "current_publication_time", return_value=current + ) as clock: + result = MODULE.prepare_runtime_extracts(self.root) + + clock.assert_called_once_with() + for authority, item in result.items(): + extract = self.root / item["path"] + with sqlite3.connect(extract) as connection: + self.assertEqual( + connection.execute( + "SELECT published_at FROM evidence_extract" + ).fetchone(), + (current,), + ) + self.assertEqual( + item["extractId"], + PUBLISHER.timestamped_extract_id(authority, current), + ) + + def test_fresh_exact_bound_extracts_are_reused(self) -> None: + first = MODULE.prepare_runtime_extracts( + self.root, "2026-08-12T09:00:00Z" + ) + before = {path: digest(path) for path in self._extracts()} + runtimes_before = { + authority: self._runtime_binding(authority) + for authority in MODULE.AUTHORITIES + } + + second = MODULE.prepare_runtime_extracts( + self.root, "2026-08-12T10:00:00Z" + ) + + self.assertEqual( + {authority: item["status"] for authority, item in second.items()}, + {authority: "reused" for authority in MODULE.AUTHORITIES}, + ) + self.assertEqual( + {authority: item["extractId"] for authority, item in second.items()}, + {authority: item["extractId"] for authority, item in first.items()}, + ) + self.assertEqual({path: digest(path) for path in self._extracts()}, before) + self.assertEqual( + { + authority: self._runtime_binding(authority) + for authority in MODULE.AUTHORITIES + }, + runtimes_before, + ) + + def test_stale_extracts_are_retained_and_replaced_under_new_filenames(self) -> None: + first = MODULE.prepare_runtime_extracts( + self.root, "2026-08-10T09:00:00Z" + ) + old_paths = { + authority: self.root / item["path"] + for authority, item in first.items() + } + old_digests = {authority: digest(path) for authority, path in old_paths.items()} + + second = MODULE.prepare_runtime_extracts( + self.root, "2026-08-12T09:00:00Z" + ) + + self.assertEqual(len(self._extracts()), 6) + for authority, item in second.items(): + self.assertEqual(item["status"], "published") + self.assertNotEqual(item["extractId"], first[authority]["extractId"]) + self.assertTrue(old_paths[authority].exists()) + self.assertEqual(digest(old_paths[authority]), old_digests[authority]) + self.assertTrue(self._runtime_binding(authority).endswith( + f"/{item['extractId']}.sqlite" + )) + + def test_metadata_mismatch_fails_before_any_new_publication_or_binding(self) -> None: + MODULE.prepare_runtime_extracts(self.root, "2026-08-12T09:00:00Z") + cra = Path(self._runtime_binding("cra")).name + cra_extract = self.root / PUBLISHER.EVIDENCE_DIRECTORY / cra + cra_extract.chmod(0o644) + with sqlite3.connect(cra_extract) as connection: + connection.execute( + "UPDATE evidence_extract SET publisher = ?", + (PUBLISHER.PUBLISHERS["nia"],), + ) + connection.commit() + cra_extract.chmod(0o444) + runtime_before = { + authority: self._runtime_binding(authority) + for authority in MODULE.AUTHORITIES + } + extracts_before = list(self._extracts()) + + with self.assertRaisesRegex(MODULE.RuntimeExtractError, "failed validation"): + MODULE.prepare_runtime_extracts(self.root, "2026-08-12T10:00:00Z") + + self.assertEqual(self._extracts(), extracts_before) + self.assertEqual( + { + authority: self._runtime_binding(authority) + for authority in MODULE.AUTHORITIES + }, + runtime_before, + ) + + def test_writable_bound_extract_fails_closed(self) -> None: + result = MODULE.prepare_runtime_extracts( + self.root, "2026-08-12T09:00:00Z" + ) + nia_extract = self.root / result["nia"]["path"] + nia_extract.chmod(0o644) + runtimes_before = { + authority: self._runtime_binding(authority) + for authority in MODULE.AUTHORITIES + } + + with self.assertRaisesRegex(MODULE.RuntimeExtractError, "writable mode"): + MODULE.prepare_runtime_extracts(self.root, "2026-08-12T10:00:00Z") + + self.assertEqual( + { + authority: self._runtime_binding(authority) + for authority in MODULE.AUTHORITIES + }, + runtimes_before, + ) + + def test_unpatchable_generated_binding_fails_before_publication(self) -> None: + runtime = ( + self.root / MODULE.RUNTIME_DIRECTORY / "sro" / "runtime.yaml" + ) + runtime.parent.chmod(0o755) + runtime.chmod(0o644) + original = runtime.read_text(encoding="utf-8") + bound_path = self._runtime_binding("sro") + runtime.write_text( + original.replace(bound_path, f'"{bound_path}"'), encoding="utf-8" + ) + runtime.chmod(0o444) + runtime.parent.chmod(0o555) + + with self.assertRaisesRegex(MODULE.RuntimeExtractError, "uniquely patchable"): + MODULE.prepare_runtime_extracts(self.root, "2026-08-12T09:00:00Z") + + self.assertEqual(self._extracts(), []) + + def test_cli_accepts_an_explicit_deterministic_publication_time(self) -> None: + completed = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "--root", + str(self.root), + "--published-at", + "2026-08-12T09:00:00Z", + ], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + result = json.loads(completed.stdout) + self.assertEqual(set(result), set(MODULE.AUTHORITIES)) + self.assertEqual( + result["sro"]["extractId"], "sro-poverty-20260812T090000Z" + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_quality_scripts.py b/scripts/test_quality_scripts.py deleted file mode 100644 index b252c0b..0000000 --- a/scripts/test_quality_scripts.py +++ /dev/null @@ -1,2039 +0,0 @@ -from __future__ import annotations - -import contextlib -import csv -import importlib.util -import io -import json -import os -import re -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path -from types import SimpleNamespace - -import yaml - - -ROOT = Path(__file__).resolve().parents[1] - - -class ComposeLoader(yaml.SafeLoader): - """Safe loader that treats Compose merge tags as their underlying value.""" - - -def _construct_compose_tag( - loader: ComposeLoader, node: yaml.Node -) -> object: - if isinstance(node, yaml.SequenceNode): - return loader.construct_sequence(node) - if isinstance(node, yaml.MappingNode): - return loader.construct_mapping(node) - return loader.construct_scalar(node) - - -for compose_tag in ("!override", "!reset"): - ComposeLoader.add_constructor(compose_tag, _construct_compose_tag) - - -def load_smoke_live(): - spec = importlib.util.spec_from_file_location( - "smoke_live", ROOT / "scripts" / "smoke-live.py" - ) - if spec is None or spec.loader is None: - raise RuntimeError("could not load smoke-live.py") - module = importlib.util.module_from_spec(spec) - sys.modules["smoke_live"] = module - spec.loader.exec_module(module) - return module - - -def load_compose_project_name(): - spec = importlib.util.spec_from_file_location( - "compose_project_name", ROOT / "scripts" / "compose_project_name.py" - ) - if spec is None or spec.loader is None: - raise RuntimeError("could not load compose_project_name.py") - module = importlib.util.module_from_spec(spec) - sys.modules["compose_project_name"] = module - spec.loader.exec_module(module) - return module - - -def load_config_secret_check(): - spec = importlib.util.spec_from_file_location( - "check_config_secrets", ROOT / "scripts" / "check-config-secrets.py" - ) - if spec is None or spec.loader is None: - raise RuntimeError("could not load check-config-secrets.py") - module = importlib.util.module_from_spec(spec) - sys.modules["check_config_secrets"] = module - spec.loader.exec_module(module) - return module - - -def load_secret_generator(): - load_compose_project_name() - spec = importlib.util.spec_from_file_location( - "solmara_gen_secrets", ROOT / "scripts" / "gen-secrets.py" - ) - if spec is None or spec.loader is None: - raise RuntimeError("could not load gen-secrets.py") - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -class QualityScriptTests(unittest.TestCase): - def test_performance_guide_references_every_k6_entrypoint(self) -> None: - guide = (ROOT / "perf" / "README.md").read_text(encoding="utf-8") - referenced = set(re.findall(r"perf/k6/[A-Za-z0-9_.-]+\.js", guide)) - entrypoints = { - path.relative_to(ROOT).as_posix() - for path in (ROOT / "perf" / "k6").glob("*.js") - } - self.assertEqual(referenced, entrypoints) - - def test_authority_notary_cel_ceiling_is_local_only_and_generated(self) -> None: - projects = ( - "cra-civil", - "nia-population", - "sro-social", - "mosd-programme", - "sipf-pensions", - "nagdi-agriculture", - ) - for project in projects: - with self.subTest(environment="local", project=project): - local_authored = yaml.safe_load( - ( - ROOT - / "projects" - / project - / "environments" - / "local.yaml" - ).read_text(encoding="utf-8") - ) - local_generated = yaml.safe_load( - ( - ROOT - / "runtime" - / "registry-projects" - / "local" - / project - / "notary" - / "notary.yaml" - ).read_text(encoding="utf-8") - ) - self.assertEqual( - local_authored["notary_cel"], - {"worker_memory_bytes": 1_073_741_824}, - ) - self.assertEqual( - local_generated["cel"], - local_authored["notary_cel"], - ) - - with self.subTest(environment="hosted", project=project): - hosted_authored = yaml.safe_load( - ( - ROOT - / "projects" - / project - / "environments" - / "hosted.yaml" - ).read_text(encoding="utf-8") - ) - hosted_generated = yaml.safe_load( - ( - ROOT - / "runtime" - / "registry-projects" - / "hosted" - / project - / "notary" - / "notary.yaml" - ).read_text(encoding="utf-8") - ) - self.assertNotIn("notary_cel", hosted_authored) - self.assertNotIn("cel", hosted_generated) - - def test_registry_project_reference_schemas_use_exact_uin_bounds(self) -> None: - schema_paths = sorted((ROOT / "projects").glob("*/entities/*.yaml")) - schema_paths.extend( - sorted( - (ROOT / "projects").glob( - "*/integrations/*/integration.yaml" - ) - ) - ) - - def assert_exact_uin_bounds(value: object, path: Path) -> None: - if isinstance(value, dict): - if value.get("pattern") == "^[0-9]{10}$": - self.assertEqual( - value.get("minLength"), - 10, - f"{path} must state the exact UIN lower bound", - ) - self.assertEqual( - value.get("maxLength"), - 10, - f"{path} must state the exact UIN upper bound", - ) - for nested in value.values(): - assert_exact_uin_bounds(nested, path) - elif isinstance(value, list): - for nested in value: - assert_exact_uin_bounds(nested, path) - - for path in schema_paths: - with self.subTest(schema=path.relative_to(ROOT)): - assert_exact_uin_bounds( - yaml.safe_load(path.read_text(encoding="utf-8")), - path, - ) - - def test_generated_secret_contract_uses_authority_owners(self) -> None: - module = load_secret_generator() - self.assertEqual( - set(module.RAW_HASH_PAIRS), - { - ( - "CRA_CHILD_BENEFIT_CLIENT_TOKEN", - "CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH", - ), - ("CRA_PENSION_CLIENT_TOKEN", "CRA_PENSION_CLIENT_TOKEN_HASH"), - ("CRA_CITIZEN_CLIENT_TOKEN", "CRA_CITIZEN_CLIENT_TOKEN_HASH"), - ( - "NIA_CHILD_BENEFIT_CLIENT_TOKEN", - "NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH", - ), - ("NIA_CITIZEN_CLIENT_TOKEN", "NIA_CITIZEN_CLIENT_TOKEN_HASH"), - ( - "SRO_CHILD_BENEFIT_CLIENT_TOKEN", - "SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH", - ), - ( - "PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN", - "PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH", - ), - ("SIPF_PENSION_CLIENT_TOKEN", "SIPF_PENSION_CLIENT_TOKEN_HASH"), - ("NAGDI_NOTARY_TOKEN", "NAGDI_CLIENT_TOKEN_HASH"), - }, - ) - self.assertEqual( - set(module.JWK_KIDS), - { - "CRA_RELAY_WORKLOAD_JWK", - "NIA_RELAY_WORKLOAD_JWK", - "NIA_ESIGNET_RELAY_WORKLOAD_JWK", - "SRO_RELAY_WORKLOAD_JWK", - "PROGRAMME_RELAY_WORKLOAD_JWK", - "SIPF_RELAY_WORKLOAD_JWK", - "NAGDI_RELAY_WORKLOAD_JWK", - "NIA_NOTARY_ISSUER_JWK", - "SIPF_NOTARY_ISSUER_JWK", - "NAGDI_NOTARY_ISSUER_JWK", - }, - ) - - with tempfile.TemporaryDirectory() as directory: - temporary_root = Path(directory) - module.ROOT = temporary_root - module.POSTGRES_SSL_DIR = temporary_root / "config" / "postgres" / "ssl" - module.compose_project_name = lambda _root: "solmara-lab-test" - federation_output = temporary_root / "federation.env" - with contextlib.redirect_stderr(io.StringIO()): - with self.assertRaises(SystemExit) as rejected: - module.main(["--federation-output", str(federation_output)]) - self.assertEqual(rejected.exception.code, 2) - self.assertFalse(federation_output.exists()) - - self.assertEqual(module.main([]), 0) - output = temporary_root / ".env" - self.assertEqual(output.stat().st_mode & 0o777, 0o600) - - generated_keys = { - line.split("=", 1)[0] - for line in output.read_text(encoding="utf-8").splitlines() - if line and not line.startswith("#") - } - declared_keys = { - line.split("=", 1)[0] - for line in (ROOT / ".env.example") - .read_text(encoding="utf-8") - .splitlines() - if line and not line.startswith("#") and "=" in line - } - self.assertEqual(generated_keys - declared_keys, set()) - - retired_names = { - "CIVIL_CHILD_BENEFIT_NOTARY_POSTGRES_RUNTIME_PASSWORD", - "NIA_CHILD_BENEFIT_NOTARY_POSTGRES_RUNTIME_PASSWORD", - "SRO_CHILD_BENEFIT_NOTARY_POSTGRES_RUNTIME_PASSWORD", - "PROGRAMME_CHILD_BENEFIT_NOTARY_POSTGRES_RUNTIME_PASSWORD", - "PENSION_NOTARY_POSTGRES_RUNTIME_PASSWORD", - "CITIZEN_NOTARY_POSTGRES_RUNTIME_PASSWORD", - "CITIZEN_ISSUER_NOTARY_POSTGRES_RUNTIME_PASSWORD", - "PENSION_NOTARY_TOKEN", - "PORTAL_CITIZEN_NOTARY_TOKEN", - "PORTAL_RELAY_TOKEN", - "SOLMARA_ESIGNET_IDENTITY_RELEASE_RAW", - "SOLMARA_ESIGNET_IDENTITY_RELEASE_HASH", - "CHILD_BENEFIT_PUBLIC_DOMAIN", - "CHILD_BENEFIT_FEDERATOR_REQUEST_JWK", - "CIVIL_CHILD_BENEFIT_PAIRWISE_SECRET", - "PENSION_NOTARY_ISSUER_JWK", - "CITIZEN_NOTARY_ISSUER_JWK", - "CITIZEN_ISSUER_ESIGNET_RP_JWK", - "CIVIL_CHILD_BENEFIT_NOTARY_URL", - "PENSION_NOTARY_URL", - "PORTAL_CIVIL_RELAY_URL", - } - self.assertEqual(retired_names & generated_keys, set()) - self.assertEqual(retired_names & declared_keys, set()) - self.assertEqual( - { - name - for name in generated_keys | declared_keys - if name.endswith(("_SOURCE_RAW", "_SOURCE_HASH")) - }, - set(), - ) - - def test_workload_issuer_contract_is_bounded_and_esignet_isolated(self) -> None: - compose = yaml.safe_load((ROOT / "compose.yaml").read_text(encoding="utf-8")) - services = compose["services"] - expected = { - "cra-workload-agent": ( - "cra-notary", - { - "registry:consult:cra-child-benefit", - "registry:consult:cra-citizen-record", - "registry:consult:cra-pension-death", - }, - ), - "nia-workload-agent": ( - "nia-notary", - { - "registry:consult:nia-child-benefit", - "registry:consult:nia-citizen-status", - }, - ), - "sro-workload-agent": ( - "sro-notary", - {"registry:consult:child-benefit"}, - ), - "programme-workload-agent": ( - "programme-notary", - {"registry:consult:child-benefit"}, - ), - "sipf-workload-agent": ( - "sipf-notary", - { - "registry:consult:sipf-pension-payment-review", - "registry:consult:sipf-survivor-benefit", - }, - ), - "nagdi-workload-agent": ( - "nagdi-notary", - {"registry:consult:livestock", "registry:consult:voucher"}, - ), - } - relay_services = { - "cra-workload-agent": "cra-civil-relay", - "nia-workload-agent": "nia-population-relay", - "sro-workload-agent": "sro-social-relay", - "programme-workload-agent": "programme-mis-relay", - "sipf-workload-agent": "sipf-pensions-relay", - "nagdi-workload-agent": "nagdi-agriculture-relay", - } - for service_name, (client_id, scopes) in expected.items(): - with self.subTest(service=service_name): - environment = services[service_name]["environment"] - identities = json.loads(environment["WORKLOAD_IDENTITIES_JSON"]) - notary = next( - identity for identity in identities if identity["azp"] == client_id - ) - self.assertEqual(notary["subject"], client_id) - self.assertEqual(notary["audience"], "registry-relay") - self.assertEqual(set(notary["scopes"]), scopes) - self.assertEqual( - notary["token_file"], f"/run/secrets/{client_id}-relay-token" - ) - self.assertEqual( - len({identity["azp"] for identity in identities}), len(identities) - ) - self.assertEqual( - len({identity["subject"] for identity in identities}), - len(identities), - ) - self.assertEqual( - len({identity["token_file"] for identity in identities}), - len(identities), - ) - for retired in ( - "WORKLOAD_AUDIENCE", - "WORKLOAD_AZP", - "WORKLOAD_SUB", - "WORKLOAD_SCOPE", - "WORKLOAD_TOKEN_FILE", - "WORKLOAD_PRIVATE_JWK_ENV", - ): - self.assertNotIn(retired, environment) - relay_service = relay_services[service_name] - self.assertEqual( - services[service_name]["network_mode"], - f"service:{relay_service}", - ) - self.assertNotIn("WORKLOAD_BIND_HOST", environment) - self.assertEqual( - services[service_name]["depends_on"][relay_service]["condition"], - "service_started", - ) - - consultation_service = service_name.replace( - "-workload-agent", "-consultation-workload-agent" - ) - consultation_relay = f"{relay_service}-consultation" - consultation_agent = services[consultation_service] - consultation_environment = consultation_agent["environment"] - consultation_identities = json.loads( - consultation_environment["WORKLOAD_IDENTITIES_JSON"] - ) - self.assertEqual(consultation_identities, [notary]) - self.assertEqual( - consultation_agent["network_mode"], - f"service:{consultation_relay}", - ) - self.assertNotIn("WORKLOAD_BIND_HOST", consultation_environment) - self.assertEqual( - consultation_agent["depends_on"][consultation_relay]["condition"], - "service_started", - ) - - nia_identities = json.loads( - services["nia-workload-agent"]["environment"]["WORKLOAD_IDENTITIES_JSON"] - ) - self.assertEqual(len(nia_identities), 2) - esignet_identity = next( - identity - for identity in nia_identities - if identity["azp"] == "solmara-esignet" - ) - self.assertEqual(esignet_identity["subject"], "solmara-esignet") - self.assertEqual(esignet_identity["scopes"], ["population:identity_release"]) - self.assertEqual(esignet_identity["token_uid"], 1001) - self.assertEqual(esignet_identity["token_gid"], 1001) - self.assertEqual( - esignet_identity["token_file"], - "/run/esignet-secrets/solmara-esignet-relay-token", - ) - self.assertEqual( - esignet_identity["private_jwk_env"], - "NIA_ESIGNET_RELAY_WORKLOAD_JWK", - ) - self.assertIn( - "nia-esignet-workload-token:/run/esignet-secrets", - services["nia-workload-agent"]["volumes"], - ) - self.assertNotIn( - "nia-esignet-workload-token:/run/esignet-secrets", - services["nia-notary"]["volumes"], - ) - - hosted_overlay = yaml.load( - (ROOT / "compose.hosted.yaml").read_text(encoding="utf-8"), - Loader=ComposeLoader, - ) - hosted_services = hosted_overlay["services"] - hosted_volumes = hosted_overlay["volumes"] - external_volume_names = { - "cra": "${CRA_WORKLOAD_TOKEN_VOLUME:-solmara-cra-workload-token}", - "nia": "${NIA_WORKLOAD_TOKEN_VOLUME:-solmara-nia-workload-token}", - "sro": "${SRO_WORKLOAD_TOKEN_VOLUME:-solmara-sro-workload-token}", - "programme": ( - "${PROGRAMME_WORKLOAD_TOKEN_VOLUME:" - "-solmara-programme-workload-token}" - ), - "sipf": "${SIPF_WORKLOAD_TOKEN_VOLUME:-solmara-sipf-workload-token}", - "nagdi": "${NAGDI_WORKLOAD_TOKEN_VOLUME:-solmara-nagdi-workload-token}", - } - for service_name in expected: - authority = service_name.removesuffix("-workload-agent") - consultation_service = ( - f"{authority}-consultation-workload-agent" - ) - with self.subTest(hosted_authority=authority): - self.assertEqual( - hosted_services[service_name]["profiles"], - ["local-workload-issuer"], - ) - self.assertEqual( - hosted_services[consultation_service]["profiles"], - ["local-workload-issuer"], - ) - volume = hosted_volumes[ - f"{authority}-consultation-workload-token" - ] - self.assertTrue(volume["external"]) - self.assertEqual( - volume["name"], external_volume_names[authority] - ) - - local_esignet = yaml.safe_load( - (ROOT / "compose.esignet.yaml").read_text(encoding="utf-8") - )["services"]["esignet"] - hosted_compose = yaml.safe_load( - (ROOT / "compose.coolify.esignet.yaml").read_text(encoding="utf-8") - ) - hosted_esignet = hosted_compose["services"]["esignet"] - hosted_agent = hosted_compose["services"]["nia-esignet-workload-agent"] - for esignet in (local_esignet, hosted_esignet): - self.assertEqual( - esignet["environment"]["REGISTRY_RELAY_AUTH_BEARER_TOKEN_FILE"], - "/run/secrets/solmara-esignet-relay-token", - ) - self.assertNotIn("REGISTRY_RELAY_AUTH_BEARER_TOKEN", esignet["environment"]) - self.assertNotIn( - "REGISTRY_RELAY_AUTH_CREDENTIAL_KIND", esignet["environment"] - ) - self.assertIn( - "nia-esignet-workload-token:/run/secrets:ro", esignet["volumes"] - ) - self.assertEqual( - local_esignet["depends_on"]["nia-workload-agent"]["condition"], - "service_healthy", - ) - self.assertEqual( - hosted_esignet["depends_on"]["nia-esignet-workload-agent"]["condition"], - "service_healthy", - ) - hosted_identity = json.loads( - hosted_agent["environment"]["WORKLOAD_IDENTITIES_JSON"] - ) - self.assertEqual( - hosted_agent["environment"]["WORKLOAD_ISSUER"], - "https://workload-issuer.solmara.registrystack.org", - ) - self.assertEqual( - hosted_agent["environment"]["NIA_ESIGNET_RELAY_WORKLOAD_JWK"], - "${NIA_ESIGNET_RELAY_WORKLOAD_JWK}", - ) - self.assertEqual( - hosted_identity, - [ - { - "audience": "registry-relay", - "azp": "solmara-esignet", - "subject": "solmara-esignet", - "scopes": ["population:identity_release"], - "token_file": "/run/secrets/solmara-esignet-relay-token", - "private_jwk_env": "NIA_ESIGNET_RELAY_WORKLOAD_JWK", - "token_uid": 1001, - "token_gid": 1001, - } - ], - ) - self.assertNotIn("ports", local_esignet) - local_esignet_edge = yaml.safe_load( - (ROOT / "compose.esignet.yaml").read_text(encoding="utf-8") - )["services"]["esignet-edge"] - self.assertEqual( - local_esignet_edge["ports"], ["${SOLMARA_ESIGNET_PORT:-4308}:3000"] - ) - self.assertEqual( - local_esignet_edge["depends_on"]["esignet"]["condition"], - "service_healthy", - ) - self.assertIn( - "config/esignet/nginx.conf", - local_esignet_edge["build"]["args"]["ESIGNET_NGINX_CONF"], - ) - self.assertIsNone(hosted_compose["volumes"]["nia-esignet-workload-token"]) - - hosted_interior = yaml.safe_load( - (ROOT / "compose.coolify.interior.yaml").read_text(encoding="utf-8") - ) - for service_name in ("nia-notary", "nia-notary-state-install"): - self.assertTrue( - all( - "nia-esignet-workload-token" not in volume - for volume in hosted_interior["services"][service_name]["volumes"] - ) - ) - - jwks = json.loads( - ( - ROOT / "metadata" / "public" / ".well-known" / "jwks.json" - ).read_text(encoding="utf-8") - ) - expected_kids = { - value - for value in load_secret_generator().JWK_KIDS.values() - if "relay-workload-key" in value - } - self.assertEqual({key["kid"] for key in jwks["keys"]}, expected_kids) - self.assertEqual(len(jwks["keys"]), len(expected_kids)) - for key in jwks["keys"]: - self.assertEqual(set(key), {"alg", "crv", "kid", "kty", "x"}) - self.assertEqual(key["alg"], "EdDSA") - self.assertEqual(key["crv"], "Ed25519") - self.assertEqual(key["kty"], "OKP") - - hosted_core = yaml.safe_load( - (ROOT / "compose.coolify.yaml").read_text(encoding="utf-8") - ) - issuer = hosted_core["services"]["workload-issuer"] - self.assertEqual( - issuer["labels"]["solmara.lab.host"], - "workload-issuer.solmara.registrystack.org", - ) - self.assertTrue( - any( - "/.well-known/jwks.json" in part - for part in issuer["healthcheck"]["test"] - ) - ) - - retired_static_names = { - "SOLMARA_ESIGNET_IDENTITY_RELEASE_RAW", - "SOLMARA_ESIGNET_IDENTITY_RELEASE_HASH", - } - self.assertTrue( - retired_static_names.isdisjoint( - name for pair in load_secret_generator().RAW_HASH_PAIRS for name in pair - ) - ) - - def test_esignet_identity_input_resolves_the_nia_uin(self) -> None: - project = yaml.safe_load( - (ROOT / "projects" / "nia-population" / "registry-stack.yaml").read_text( - encoding="utf-8" - ) - ) - authored_profile = project["services"]["nia-population-records"]["api"][ - "attribute_release_profiles" - ]["solmara-nia-userinfo"] - self.assertNotIn("input", authored_profile["subject"]) - self.assertEqual(authored_profile["subject"]["source_field"], "uin") - self.assertEqual(authored_profile["subject"]["id_type"], "national_id") - self.assertNotIn("response", authored_profile) - self.assertEqual( - authored_profile["claims"]["individual_id"]["source_field"], "uin" - ) - - for environment in ("local", "hosted"): - relay = yaml.safe_load( - ( - ROOT - / "runtime" - / "registry-projects" - / environment - / "nia-population" - / "relay" - / "relay.yaml" - ).read_text(encoding="utf-8") - ) - population = next( - entity - for dataset in relay["datasets"] - for entity in dataset["entities"] - if entity["name"] == "population" - ) - generated_profile = next( - profile - for profile in population["attribute_release_profiles"] - if profile["id"] == "solmara-nia-userinfo" - ) - self.assertEqual(generated_profile["subject"]["source_field"], "uin") - - population_fixture = ( - ROOT - / "ministries" - / "interior-population" - / "fixtures" - / "population_person.csv" - ) - with population_fixture.open(encoding="utf-8", newline="") as fixture: - elena = next( - row for row in csv.DictReader(fixture) if row["uin"] == "2300018263" - ) - self.assertEqual( - (elena["given_name"], elena["family_name"]), ("Elena", "Dela Cruz") - ) - - def test_fiction_lint_passes_current_tree(self) -> None: - result = subprocess.run( - [str(ROOT / "scripts" / "check-fiction.sh")], - cwd=ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - self.assertEqual(result.returncode, 0, result.stderr) - - def test_secret_lint_passes_current_tree(self) -> None: - result = subprocess.run( - [str(ROOT / "scripts" / "check-config-secrets.py")], - cwd=ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - self.assertEqual(result.returncode, 0, result.stderr) - - def test_secret_lint_scans_runtime_and_limits_workload_volume_exemption( - self, - ) -> None: - module = load_config_secret_check() - scanned = {path.relative_to(ROOT).as_posix() for path in module.iter_files()} - self.assertIn( - "runtime/registry-projects/local/cra-civil/notary/notary.yaml", - scanned, - ) - self.assertIn("projects/cra-civil/environments/local.yaml", scanned) - self.assertIn("compose.hosted.yaml", scanned) - self.assertFalse(any(path.startswith("notaries/") for path in scanned)) - self.assertFalse(any(path.startswith("hosted/notaries/") for path in scanned)) - self.assertTrue( - module.line_is_allowed(" - cra-workload-token:/run/secrets:ro") - ) - self.assertTrue( - module.line_is_allowed( - " - nia-esignet-workload-token:/run/esignet-secrets" - ) - ) - self.assertTrue( - module.line_is_allowed( - " api_key_fingerprint: { secret: CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH }" - ) - ) - self.assertFalse( - module.line_is_allowed("token: leaked-cra-notary-workload-token:value") - ) - self.assertFalse(module.line_is_allowed("signing_key: a-raw-private-key")) - - def test_registry_projects_are_explicit_and_use_the_pinned_registryctl( - self, - ) -> None: - required_version = next( - line.split("=", 1)[1] - for line in (ROOT / "versions.env").read_text(encoding="utf-8").splitlines() - if line.startswith("REGISTRYCTL_VERSION=") - ) - projects = [ - "cra-civil", - "nia-population", - "sro-social", - "mosd-programme", - "sipf-pensions", - "nagdi-agriculture", - ] - with tempfile.TemporaryDirectory() as directory: - temporary = Path(directory) - registryctl = temporary / "registryctl" - log = temporary / "commands.log" - registryctl.write_text( - f"""#!/bin/sh -if [ "${1:-}" = "--version" ]; then - echo "registryctl {required_version}" - exit 0 -fi -if [ "${2:-}" = "--help" ]; then - case "${1:-}" in - check | test | build | capabilities) exit 0 ;; - esac -fi -if [ "${1:-}" = "authoring" ] && [ "${2:-}" = "editor" ] && [ "${3:-}" = "--help" ]; then - exit 0 -fi -printf '%s\\n' "$*" >> "$REGISTRYCTL_LOG" -""", - encoding="utf-8", - ) - registryctl.chmod(0o755) - result = subprocess.run( - [str(ROOT / "scripts" / "registry-projects.sh"), "check"], - cwd=ROOT, - env={ - **os.environ, - "REGISTRYCTL_BIN": str(registryctl), - "REGISTRYCTL_LOG": str(log), - }, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - self.assertEqual(result.returncode, 0, result.stderr) - commands = log.read_text(encoding="utf-8").splitlines() - expected = [ - f"check --project-dir {ROOT / 'projects' / project} --environment {environment}" - for project in projects - for environment in ("local", "hosted") - ] - self.assertEqual(commands, expected) - - log.write_text("", encoding="utf-8") - review = subprocess.run( - [str(ROOT / "scripts" / "registry-projects.sh"), "review"], - cwd=ROOT, - env={ - **os.environ, - "REGISTRYCTL_BIN": str(registryctl), - "REGISTRYCTL_LOG": str(log), - }, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - self.assertEqual(review.returncode, 0, review.stderr) - review_commands = log.read_text(encoding="utf-8").splitlines() - self.assertEqual( - review_commands, - [f"{command} --explain" for command in expected], - ) - - log.write_text("", encoding="utf-8") - capabilities = subprocess.run( - [str(ROOT / "scripts" / "registry-projects.sh"), "capabilities"], - cwd=ROOT, - env={ - **os.environ, - "REGISTRYCTL_BIN": str(registryctl), - "REGISTRYCTL_LOG": str(log), - }, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - self.assertEqual(capabilities.returncode, 0, capabilities.stderr) - capability_commands = log.read_text(encoding="utf-8").splitlines() - self.assertEqual( - capability_commands, - [ - f"capabilities --project-dir {ROOT / 'projects' / project} " - f"--environment {environment}" - for project in projects - for environment in ("local", "hosted") - ], - ) - - log.write_text("", encoding="utf-8") - editor = subprocess.run( - [str(ROOT / "scripts" / "registry-projects.sh"), "editor"], - cwd=ROOT, - env={ - **os.environ, - "REGISTRYCTL_BIN": str(registryctl), - "REGISTRYCTL_LOG": str(log), - }, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - self.assertEqual(editor.returncode, 0, editor.stderr) - editor_commands = log.read_text(encoding="utf-8").splitlines() - self.assertEqual( - editor_commands, - [ - f"authoring editor --project-dir {ROOT / 'projects' / project}" - for project in projects - ], - ) - - registryctl.write_text( - "#!/bin/sh\necho 'registryctl 0.8.3'\n", - encoding="utf-8", - ) - rejected = subprocess.run( - [str(ROOT / "scripts" / "registry-projects.sh"), "check"], - cwd=ROOT, - env={**os.environ, "REGISTRYCTL_BIN": str(registryctl)}, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - self.assertEqual(rejected.returncode, 1) - self.assertIn( - f"registryctl {required_version} is required", rejected.stderr - ) - - registryctl.write_text( - f'#!/bin/sh\nif [ "${{1:-}}" = "--version" ]; then echo \'registryctl {required_version}\'; exit 0; fi\nexit 1\n', - encoding="utf-8", - ) - incompatible = subprocess.run( - [str(ROOT / "scripts" / "registry-projects.sh"), "check"], - cwd=ROOT, - env={**os.environ, "REGISTRYCTL_BIN": str(registryctl)}, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - self.assertEqual(incompatible.returncode, 1) - self.assertIn( - "with project-authoring check/test/build/capabilities is required", - incompatible.stderr, - ) - - registry_projects = ( - ROOT / "scripts" / "registry-projects.sh" - ).read_text(encoding="utf-8") - self.assertIn("--format json", registry_projects) - self.assertIn("registryctl-build-output.py", registry_projects) - self.assertIn("registryctl-test-output.py", registry_projects) - self.assertNotIn( - ".registry-stack/build/$environment/private", - registry_projects, - ) - - def test_registry_project_secret_references_have_local_producers(self) -> None: - module = load_secret_generator() - produced = ( - {hashed for _, hashed in module.RAW_HASH_PAIRS} - | set(module.JWK_KIDS) - | module.DIRECT_PROJECT_SECRET_NAMES - ) - declared = { - line.split("=", 1)[0] - for line in (ROOT / ".env.example").read_text(encoding="utf-8").splitlines() - if line and not line.startswith("#") and "=" in line - } - consumed = set() - - def collect(value) -> None: - if isinstance(value, dict): - if set(value) == {"secret"} and isinstance(value["secret"], str): - consumed.add(value["secret"]) - for child in value.values(): - collect(child) - elif isinstance(value, list): - for child in value: - collect(child) - - for environment in sorted((ROOT / "projects").glob("*/environments/*.yaml")): - collect(yaml.safe_load(environment.read_text(encoding="utf-8"))) - - self.assertEqual(consumed - produced, set()) - self.assertEqual(consumed - declared, set()) - - def test_relay_consultation_state_uses_the_v015_epoch(self) -> None: - versions = dict( - line.split("=", 1) - for line in (ROOT / "versions.env").read_text(encoding="utf-8").splitlines() - if line and not line.startswith("#") and "=" in line - ) - self.assertEqual(versions["REGISTRY_RELAY_STATE_EPOCH"], "v015") - - compose_names = ( - "compose.yaml", - "compose.coolify.interior.yaml", - "compose.coolify.social-development.yaml", - "compose.coolify.labour-pensions.yaml", - "compose.coolify.agriculture.yaml", - ) - epoch_reference = "${REGISTRY_RELAY_STATE_EPOCH:-v015}" - for compose_name in compose_names: - with self.subTest(compose=compose_name): - compose_path = ROOT / compose_name - raw = compose_path.read_text(encoding="utf-8") - compose = yaml.safe_load(raw) - services = compose["services"] - self.assertEqual( - services["postgres"]["environment"][ - "REGISTRY_RELAY_STATE_EPOCH" - ], - epoch_reference, - ) - self.assertEqual( - services["postgres"]["healthcheck"]["test"], - [ - "CMD-SHELL", - "pg_isready -h 127.0.0.1 -U " - "$${POSTGRES_USER} -d $${POSTGRES_DB}", - ], - ) - for authority in ("cra", "nia", "sro", "programme", "sipf", "nagdi"): - legacy = f"solmara_relay_{authority}_consultation" - self.assertNotIn(f"{legacy}_runtime", raw) - self.assertNotIn(f"{legacy}_owner", raw) - self.assertNotIn(f"/{legacy}?sslmode=require", raw) - - state_urls = [ - environment["REGISTRY_RELAY_CONSULTATION_DATABASE_URL"] - for service in services.values() - if ( - environment := service.get("environment") - ) - and "REGISTRY_RELAY_CONSULTATION_DATABASE_URL" in environment - ] - self.assertTrue(state_urls) - self.assertTrue( - all(f"_consultation_{epoch_reference}" in url for url in state_urls) - ) - - bootstrap_commands = [ - service["command"] - for name, service in services.items() - if name.endswith("-relay-state-bootstrap") - ] - self.assertTrue(bootstrap_commands) - for command in bootstrap_commands: - owner = command[command.index("--owner-role") + 1] - self.assertIn(f"_consultation_{epoch_reference}_owner", owner) - - provisioner = (ROOT / "scripts" / "init-notary-postgresql.sh").read_text( - encoding="utf-8" - ) - self.assertIn( - 'database="solmara_relay_${key}_consultation_${relay_state_epoch}"', - provisioner, - ) - self.assertIn( - "REGISTRY_RELAY_STATE_EPOCH must contain only lowercase letters", - provisioner, - ) - - def test_generated_public_and_consultation_relays_are_separate(self) -> None: - projects = ( - "cra-civil", - "nia-population", - "sro-social", - "mosd-programme", - "sipf-pensions", - "nagdi-agriculture", - ) - for project in projects: - for environment in ("local", "hosted"): - with self.subTest(project=project, environment=environment): - authored = yaml.safe_load( - ( - ROOT - / "projects" - / project - / "environments" - / f"{environment}.yaml" - ).read_text(encoding="utf-8") - ) - relay_root = ( - ROOT - / "runtime" - / "registry-projects" - / environment - / project - / "relay" - ) - public = yaml.safe_load( - (relay_root / "relay.yaml").read_text(encoding="utf-8") - ) - consultation = yaml.safe_load( - (relay_root / "relay-consultation.yaml").read_text( - encoding="utf-8" - ) - ) - service_id = authored["deployment"]["relay"]["service"] - workload_id = authored["notary_relay"]["workload_client_id"] - self.assertNotIn("consultation", public) - self.assertIn("consultation", consultation) - self.assertEqual(public["instance"]["id"], service_id) - self.assertEqual( - consultation["instance"]["id"], f"{service_id}-consultation" - ) - self.assertEqual(public["datasets"], consultation["datasets"]) - self.assertEqual( - public["auth"]["oidc"]["allowed_clients"], - sorted(set(authored["relay"].get("allowed_clients", []))), - ) - self.assertEqual( - consultation["auth"]["oidc"]["allowed_clients"], - [workload_id], - ) - self.assertEqual( - consultation["consultation"]["authorized_workload"][ - "client_value" - ], - workload_id, - ) - self.assertEqual( - consultation["server"]["bind"], "127.0.0.1:8080" - ) - - def test_relay_uses_canonical_release_with_explicit_source_dev_path(self) -> None: - versions = dict( - line.split("=", 1) - for line in (ROOT / "versions.env").read_text(encoding="utf-8").splitlines() - if line and not line.startswith("#") and "=" in line - ) - self.assertRegex( - versions["REGISTRY_STACK_SOURCE_REF"], - r"^v[0-9]+\.[0-9]+\.[0-9]+$", - ) - self.assertRegex(versions["REGISTRY_STACK_SOURCE_COMMIT"], r"^[0-9a-f]{40}$") - self.assertNotIn("REGISTRY_RELAY_FEATURES", versions) - self.assertNotIn("SOLMARA_RELAY_RUNTIME_IMAGE", versions) - self.assertRegex( - versions["VOLUME_INIT_IMAGE"], - r"^busybox@sha256:[0-9a-f]{64}$", - ) - self.assertEqual( - versions["SOLMARA_RELAY_DEV_IMAGE"], - "solmara-lab-registry-relay:source", - ) - self.assertEqual(int(versions["SOLMARA_RELAY_BUNDLE_SEQUENCE"]), 2) - - dockerfile = ( - ROOT / "docker" / "relay-runtime" / "Dockerfile" - ).read_text(encoding="utf-8") - self.assertIn("FROM ${REGISTRY_RELAY_IMAGE}", dockerfile) - self.assertIn( - 'LABEL org.opencontainers.image.base.name="${REGISTRY_RELAY_IMAGE}"', - dockerfile, - ) - self.assertIn( - 'LABEL org.opencontainers.image.source="https://github.com/registrystack/solmara-lab"', - dockerfile, - ) - self.assertIn("--package registry-relay", dockerfile) - self.assertNotIn("--features", dockerfile) - self.assertNotIn("--all-features", dockerfile) - - justfile = (ROOT / "justfile").read_text(encoding="utf-8") - self.assertEqual(justfile.count("scripts/build-relay-runtime.sh"), 2) - self.assertNotIn("SOLMARA_RELAY_RUNTIME_IMAGE", justfile) - self.assertEqual( - justfile.count( - 'REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_DEV_IMAGE"' - ), - 2, - ) - normal_up = justfile.split("\nup:\n", 1)[1].split("\n\n", 1)[0] - normal_esignet = justfile.split("\nup-esignet:\n", 1)[1].split( - "\n\n", - 1, - )[0] - self.assertNotIn("build-relay-runtime.sh", normal_up) - self.assertNotIn("build-relay-runtime.sh", normal_esignet) - self.assertNotIn("REGISTRY_RELAY_IMAGE=", normal_up) - self.assertNotIn("REGISTRY_RELAY_IMAGE=", normal_esignet) - - review_script = (ROOT / "scripts" / "review.sh").read_text( - encoding="utf-8" - ) - self.assertIn('git -C "$root" grep -I -n', review_script) - self.assertNotIn("grep -RIn", review_script) - - release_workflow = ( - ROOT / ".github" / "workflows" / "release-candidate.yml" - ).read_text(encoding="utf-8") - self.assertNotIn( - "- name: Build and push feature-enabled Relay runtime", - release_workflow, - ) - self.assertNotIn("SOLMARA_RELAY_RUNTIME_IMAGE", release_workflow) - self.assertIn( - "REGISTRY_RELAY_IMAGE=${{ env.REGISTRY_RELAY_IMAGE }}", - release_workflow, - ) - self.assertIn( - "VOLUME_INIT_IMAGE=${{ env.VOLUME_INIT_IMAGE }}", - release_workflow, - ) - self.assertIn("SOLMARA_RELAY_BUNDLE_SEQUENCE", release_workflow) - self.assertLess( - release_workflow.index("- name: Build and push hosted Relay image"), - release_workflow.index("- name: Verify hosted Relay signed bundles"), - ) - self.assertIn( - "config verify-bundle", - release_workflow, - ) - self.assertIn('"$project/consultation"', release_workflow) - - def test_coolify_authority_state_is_postgresql_isolated(self) -> None: - authority_groups = { - "compose.coolify.interior.yaml": ( - "nia", - ( - ("cra", "cra-civil", "cra-civil-relay", "cra-notary"), - ("nia", "nia-population", "nia-population-relay", "nia-notary"), - ), - ), - "compose.coolify.social-development.yaml": ( - "", - ( - ("sro", "sro-social", "sro-social-relay", "sro-notary"), - ( - "programme", - "mosd-programme", - "programme-mis-relay", - "programme-notary", - ), - ), - ), - "compose.coolify.labour-pensions.yaml": ( - "sipf", - (("sipf", "sipf-pensions", "sipf-pensions-relay", "sipf-notary"),), - ), - "compose.coolify.agriculture.yaml": ( - "", - ( - ( - "nagdi", - "nagdi-agriculture", - "nagdi-agriculture-relay", - "nagdi-notary", - ), - ), - ), - } - - for compose_name, (source_readers, authorities) in authority_groups.items(): - with self.subTest(compose=compose_name): - compose = yaml.safe_load( - (ROOT / compose_name).read_text(encoding="utf-8") - ) - declared_volumes = set((compose.get("volumes") or {}).keys()) - services = compose["services"] - self.assertNotIn("redis", services) - self.assertIn("postgres-data", declared_volumes) - self.assertIn( - "postgres-data:/var/lib/postgresql/data", - services["postgres"]["volumes"], - ) - postgres_env = services["postgres"]["environment"] - authority_keys = [authority[0] for authority in authorities] - self.assertEqual( - postgres_env["SOLMARA_RELAY_DATABASES"].split(), authority_keys - ) - self.assertEqual( - postgres_env["SOLMARA_NOTARY_DATABASES"].split(), authority_keys - ) - self.assertEqual( - postgres_env["SOLMARA_SOURCE_READER_DATABASES"], source_readers - ) - self.assertEqual( - services["registry-postgresql-bootstrap"]["restart"], "no" - ) - for service_name, service in services.items(): - for mount in service.get("volumes") or []: - with self.subTest(service=service_name, mount=mount): - self.assertIn( - mount.split(":", 1)[0], - declared_volumes, - "Coolify authority services must use named volumes; " - "repository bind mounts are not deployable closures", - ) - - for key, project, relay_name, notary_name in authorities: - with self.subTest(authority=key): - relay = services[relay_name] - consultation_relay_name = f"{relay_name}-consultation" - consultation_relay = services[consultation_relay_name] - notary = services[notary_name] - installer = services[f"{notary_name}-state-install"] - workload_agent_name = f"{key}-workload-agent" - workload_agent = services[workload_agent_name] - bootstrap = services[f"{key}-relay-state-bootstrap"] - config_state_init = services[ - f"{key}-relay-config-state-init" - ] - consultation_config_state_init = services[ - f"{key}-relay-consultation-config-state-init" - ] - relay_mounts = set(relay.get("volumes") or []) - consultation_relay_mounts = set( - consultation_relay.get("volumes") or [] - ) - bootstrap_mounts = set(bootstrap.get("volumes") or []) - config_state_mounts = set( - config_state_init.get("volumes") or [] - ) - consultation_config_state_mounts = set( - consultation_config_state_init.get("volumes") or [] - ) - notary_mounts = set(notary.get("volumes") or []) - public_relay_config = ( - f"/etc/solmara/hosted-relay-bundles/{project}/bootstrap.yaml" - ) - consultation_relay_config = ( - f"/etc/solmara/hosted-relay-bundles/{project}/" - "consultation/bootstrap.yaml" - ) - notary_config = ( - f"/etc/solmara/registry-projects/hosted/{project}/notary/notary.yaml" - ) - self.assertEqual( - relay["command"], ["--config", public_relay_config] - ) - self.assertEqual( - consultation_relay["command"], - [ - "--config", - consultation_relay_config, - "--bind", - "127.0.0.1:8080", - ], - ) - self.assertEqual( - bootstrap["command"][ - bootstrap["command"].index("--config") + 1 - ], - consultation_relay_config, - ) - self.assertIn(f"{key}-relay-cache", declared_volumes) - self.assertIn( - f"{key}-relay-consultation-cache", declared_volumes - ) - self.assertIn( - f"{key}-relay-cache:/var/lib/registry-relay/cache", - relay_mounts, - ) - self.assertIn( - f"{key}-relay-consultation-cache:" - "/var/lib/registry-relay/cache", - consultation_relay_mounts, - ) - self.assertIn( - f"{key}-relay-consultation-cache:" - "/var/lib/registry-relay/cache", - bootstrap_mounts, - ) - self.assertNotIn( - "REGISTRY_RELAY_CONSULTATION_DATABASE_URL", - relay["environment"], - ) - self.assertIn( - "REGISTRY_RELAY_CONSULTATION_DATABASE_URL", - consultation_relay["environment"], - ) - self.assertEqual(relay["expose"], ["8080"]) - self.assertEqual(consultation_relay["expose"], ["8081"]) - self.assertNotIn("labels", consultation_relay) - self.assertEqual(config_state_init["user"], "0:0") - self.assertEqual( - config_state_init["entrypoint"], - ["/bin/busybox", "sh", "-eu", "-c"], - ) - self.assertEqual( - consultation_config_state_init["entrypoint"], - ["/bin/busybox", "sh", "-eu", "-c"], - ) - self.assertIn( - f"{key}-relay-cache:/var/lib/registry-relay/cache", - config_state_mounts, - ) - self.assertEqual( - relay["depends_on"][f"{key}-relay-config-state-init"][ - "condition" - ], - "service_completed_successfully", - ) - self.assertIn( - f"{key}-relay-consultation-cache:" - "/var/lib/registry-relay/cache", - consultation_config_state_mounts, - ) - self.assertEqual( - bootstrap["depends_on"][ - f"{key}-relay-consultation-config-state-init" - ]["condition"], - "service_completed_successfully", - ) - self.assertEqual( - consultation_relay["depends_on"][ - f"{key}-relay-state-bootstrap" - ]["condition"], - "service_completed_successfully", - ) - self.assertEqual( - notary["command"], ["--config", notary_config] - ) - self.assertEqual( - installer["command"][ - installer["command"].index("--config") + 1 - ], - notary_config, - ) - self.assertEqual( - notary["network_mode"], - f"service:{consultation_relay_name}", - ) - self.assertEqual(notary["user"], "65534:65534") - self.assertNotIn( - "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", - notary["environment"], - ) - self.assertIn( - "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", - installer["environment"], - ) - self.assertEqual(installer["restart"], "no") - self.assertEqual( - notary["depends_on"][f"{notary_name}-state-install"][ - "condition" - ], - "service_completed_successfully", - ) - self.assertEqual( - notary["depends_on"][workload_agent_name]["condition"], - "service_healthy", - ) - self.assertEqual( - installer["depends_on"][workload_agent_name]["condition"], - "service_healthy", - ) - workload_environment = workload_agent["environment"] - workload_identity = json.loads( - workload_environment["WORKLOAD_IDENTITIES_JSON"] - ) - self.assertEqual( - workload_environment["WORKLOAD_ISSUER"], - "https://workload-issuer.solmara.registrystack.org", - ) - private_jwk_env = f"{key.upper()}_RELAY_WORKLOAD_JWK" - self.assertEqual( - workload_environment[private_jwk_env], - f"${{{private_jwk_env}}}", - ) - self.assertEqual(len(workload_identity), 1) - self.assertEqual( - workload_identity[0]["private_jwk_env"], - private_jwk_env, - ) - self.assertEqual( - workload_identity[0]["token_file"], - f"/run/secrets/{key}-notary-relay-token", - ) - self.assertIn( - f"{key}-workload-token:/run/secrets", - workload_agent["volumes"], - ) - self.assertIsNone( - compose["volumes"][f"{key}-workload-token"] - ) - self.assertEqual( - notary["labels"]["solmara.lab.host"], - f"{notary_name}.solmara.registrystack.org", - ) - self.assertFalse( - any( - "/var/lib/registry-notary" in mount - for mount in notary_mounts - ) - ) - - def test_hosted_authority_images_contain_runtime_closures_and_tls(self) -> None: - relay_dockerfile = (ROOT / "docker" / "relay" / "Dockerfile").read_text( - encoding="utf-8" - ) - notary_dockerfile = ( - ROOT / "docker" / "notary" / "Dockerfile" - ).read_text(encoding="utf-8") - projects = ( - "cra-civil", - "nia-population", - "sro-social", - "mosd-programme", - "sipf-pensions", - "nagdi-agriculture", - ) - for project in projects: - with self.subTest(project=project): - self.assertIn( - f"COPY runtime/registry-projects/hosted/{project}/notary/notary.yaml " - f"/etc/solmara/registry-projects/hosted/{project}/notary/notary.yaml", - notary_dockerfile, - ) - self.assertIn( - "COPY config/hosted-relay-bundles " - "/etc/solmara/hosted-relay-bundles", - relay_dockerfile, - ) - self.assertIn( - "COPY --from=volume-init /bin/busybox /bin/busybox", - relay_dockerfile, - ) - self.assertNotIn( - "COPY runtime/registry-projects/hosted/", - relay_dockerfile, - ) - tls_copy = ( - "COPY config/postgres/ssl/server.crt /etc/solmara/postgres/root.crt" - ) - self.assertIn(tls_copy, relay_dockerfile) - self.assertIn(tls_copy, notary_dockerfile) - - def test_hosted_postgresql_image_contains_both_live_source_fixtures(self) -> None: - dockerfile = (ROOT / "docker" / "postgres" / "Dockerfile").read_text( - encoding="utf-8" - ) - self.assertIn( - "COPY ministries/interior-population/fixtures /docker-entrypoint-initdb.d", - dockerfile, - ) - self.assertIn( - "COPY ministries/labour-pensions/fixtures /docker-entrypoint-initdb.d", - dockerfile, - ) - - def test_notary_postgresql_state_is_isolated_and_redis_free(self) -> None: - notaries = { - "cra-notary": ("cra-civil", "cra", "cra-civil-relay"), - "nia-notary": ("nia-population", "nia", "nia-population-relay"), - "sro-notary": ("sro-social", "sro", "sro-social-relay"), - "programme-notary": ( - "mosd-programme", - "programme", - "programme-mis-relay", - ), - "sipf-notary": ("sipf-pensions", "sipf", "sipf-pensions-relay"), - "nagdi-notary": ( - "nagdi-agriculture", - "nagdi", - "nagdi-agriculture-relay", - ), - } - local = yaml.safe_load((ROOT / "compose.yaml").read_text(encoding="utf-8")) - services = local["services"] - declared_volumes = set((local.get("volumes") or {}).keys()) - self.assertNotIn("redis", services) - self.assertIn("postgres-data", declared_volumes) - self.assertIn( - "postgres-data:/var/lib/postgresql/data", - services["postgres"]["volumes"], - ) - self.assertNotIn( - "postgres-data:/var/lib/postgresql", - services["postgres"]["volumes"], - ) - state_proof = (ROOT / "scripts" / "notary_state_restart.py").read_text( - encoding="utf-8" - ) - for required in ( - "SELECT system_identifier FROM pg_control_system()", - "validate_runtime_pgdata_mounts", - 'self.run_just("down")', - 'self.run_just("up")', - "compare_snapshots(before, after)", - 'volume_labels.get("com.docker.compose.volume") != "postgres-data"', - ): - self.assertIn(required, state_proof) - self.assertNotIn('run_just("reset")', state_proof) - self.assertNotIn('run_just("restart")', state_proof) - self.assertEqual( - {name for name in services if name.endswith("-notary")}, - set(notaries), - ) - self.assertEqual( - services["postgres"]["environment"]["SOLMARA_NOTARY_DATABASES"].split(), - ["cra", "nia", "sro", "programme", "sipf", "nagdi"], - ) - self.assertEqual( - services["postgres"]["environment"]["SOLMARA_RELAY_DATABASES"].split(), - ["cra", "nia", "sro", "programme", "sipf", "nagdi"], - ) - source_urls = { - services["nia-population-relay"]["environment"]["SOLMARA_NIA_DATABASE_URL"], - services["sipf-pensions-relay"]["environment"]["SOLMARA_SIPF_DATABASE_URL"], - } - self.assertEqual( - source_urls, - {"${SOLMARA_NIA_DATABASE_URL}", "${SOLMARA_SIPF_DATABASE_URL}"}, - ) - env_example = (ROOT / ".env.example").read_text(encoding="utf-8") - self.assertIn( - "SOLMARA_NIA_DATABASE_URL=postgres://solmara_source_nia_reader:", - env_example, - ) - self.assertIn( - "SOLMARA_SIPF_DATABASE_URL=postgres://solmara_source_sipf_reader:", - env_example, - ) - self.assertNotIn( - "SOLMARA_NIA_DATABASE_URL=postgres://solmara_registry:", env_example - ) - self.assertNotIn( - "SOLMARA_SIPF_DATABASE_URL=postgres://solmara_registry:", env_example - ) - for token_name in ( - "CRA_CHILD_BENEFIT_CLIENT_TOKEN", - "NIA_CHILD_BENEFIT_CLIENT_TOKEN", - "SRO_CHILD_BENEFIT_CLIENT_TOKEN", - "PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN", - ): - self.assertIn(f"{token_name}=", env_example) - self.assertNotIn("CIVIL_CHILD_BENEFIT_NOTARY_TOKEN=", env_example) - self.assertNotIn("NIA_CHILD_BENEFIT_NOTARY_TOKEN=", env_example) - self.assertNotIn("SRO_CHILD_BENEFIT_NOTARY_TOKEN=", env_example) - self.assertNotIn("PROGRAMME_CHILD_BENEFIT_NOTARY_TOKEN=", env_example) - bootstrap = services["registry-postgresql-bootstrap"] - self.assertEqual(bootstrap["restart"], "no") - self.assertEqual( - bootstrap["depends_on"]["postgres"]["condition"], - "service_healthy", - ) - - runtime_urls = set() - migrator_urls = set() - relay_urls = set() - for service_name, (project, database_key, relay_name) in notaries.items(): - with self.subTest(notary=service_name): - config = yaml.safe_load( - ( - ROOT - / "runtime" - / "registry-projects" - / "local" - / project - / "notary" - / "notary.yaml" - ).read_text(encoding="utf-8") - ) - state = config["state"] - self.assertEqual(state["storage"], "postgresql") - self.assertEqual( - state["postgresql"]["url_env"], "REGISTRY_NOTARY_POSTGRES_URL" - ) - self.assertEqual( - state["postgresql"]["root_certificate_path"], - "/etc/solmara/postgres/root.crt", - ) - self.assertNotIn("replay", config) - - expected_database = f"solmara_notary_{database_key}" - expected_runtime = f"{expected_database}_runtime" - expected_migrator = f"{expected_database}_migrator" - expected_owner = f"{expected_database}_owner" - installer_name = f"{service_name}-state-install" - public_workload_agent = f"{database_key}-workload-agent" - workload_agent = f"{database_key}-consultation-workload-agent" - - runtime = services[service_name] - installer = services[installer_name] - agent = services[workload_agent] - consultation_relay_name = f"{relay_name}-consultation" - public_relay = services[relay_name] - consultation_relay = services[consultation_relay_name] - runtime_url = runtime["environment"]["REGISTRY_NOTARY_POSTGRES_URL"] - migrator_url = installer["environment"][ - "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL" - ] - relay_url = consultation_relay["environment"][ - "REGISTRY_RELAY_CONSULTATION_DATABASE_URL" - ] - relay_cache = f"{database_key}-relay-consultation-cache" - workload_token = f"{database_key}-consultation-workload-token" - self.assertIn(relay_cache, declared_volumes) - self.assertIn(workload_token, declared_volumes) - self.assertIn( - f"{relay_cache}:/var/lib/registry-relay/cache", - consultation_relay["volumes"], - ) - self.assertNotIn( - "REGISTRY_RELAY_CONSULTATION_DATABASE_URL", - public_relay["environment"], - ) - self.assertEqual( - public_relay["command"], - ["--config", "/etc/registry-relay/relay.yaml"], - ) - self.assertEqual( - consultation_relay["command"], - [ - "--config", - "/etc/registry-relay/relay-consultation.yaml", - "--bind", - "127.0.0.1:8080", - ], - ) - self.assertEqual( - consultation_relay["environment"]["REGISTRY_RELAY_BIND"], - "127.0.0.1:8080", - ) - self.assertIn(f"{expected_runtime}:", runtime_url) - self.assertIn(f"/{expected_database}?sslmode=require", runtime_url) - self.assertNotIn( - "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", runtime["environment"] - ) - self.assertIn(f"{expected_migrator}:", migrator_url) - self.assertIn(f"/{expected_database}?sslmode=require", migrator_url) - self.assertIn(expected_owner, installer["command"]) - self.assertIn(expected_runtime, installer["command"]) - self.assertEqual(installer["restart"], "no") - self.assertEqual( - runtime["network_mode"], f"service:{consultation_relay_name}" - ) - self.assertEqual(runtime["user"], "65534:65534") - self.assertEqual( - runtime["healthcheck"]["test"], - [ - "CMD", - "/usr/local/bin/registry-notary", - "healthcheck", - "--url", - "http://127.0.0.1:8081/ready", - ], - ) - self.assertEqual( - services[public_workload_agent]["network_mode"], - f"service:{relay_name}", - ) - self.assertEqual( - agent["network_mode"], f"service:{consultation_relay_name}" - ) - self.assertNotIn("WORKLOAD_BIND_HOST", agent["environment"]) - self.assertEqual( - agent["environment"]["WORKLOAD_ISSUER"], - "http://127.0.0.1:8090", - ) - self.assertIn( - f"{workload_token}:/run/secrets:ro", runtime["volumes"] - ) - self.assertIn( - f"{workload_token}:/run/secrets:ro", installer["volumes"] - ) - authored_environment = yaml.safe_load( - ( - ROOT - / "projects" - / project - / "environments" - / "local.yaml" - ).read_text(encoding="utf-8") - ) - self.assertEqual( - authored_environment["relay"]["jwks_url"], - "http://127.0.0.1:8090/.well-known/jwks.json", - ) - self.assertEqual( - installer["depends_on"]["registry-postgresql-bootstrap"][ - "condition" - ], - "service_completed_successfully", - ) - self.assertEqual( - installer["depends_on"][workload_agent]["condition"], - "service_healthy", - ) - self.assertEqual( - runtime["depends_on"][installer_name]["condition"], - "service_completed_successfully", - ) - self.assertEqual( - runtime["depends_on"][workload_agent]["condition"], - "service_healthy", - ) - - runtime_urls.add(runtime_url) - migrator_urls.add(migrator_url) - relay_urls.add(relay_url) - - self.assertEqual(len(runtime_urls), len(notaries)) - self.assertEqual(len(migrator_urls), len(notaries)) - self.assertEqual(len(relay_urls), len(notaries)) - - esignet = yaml.safe_load( - (ROOT / "compose.coolify.esignet.yaml").read_text(encoding="utf-8") - ) - self.assertIn("esignet-redis", esignet["services"]) - - def test_hosted_child_benefit_topology_is_source_owned(self) -> None: - core = yaml.safe_load( - (ROOT / "compose.coolify.yaml").read_text(encoding="utf-8") - ) - core_services = core["services"] - self.assertIn("child-benefit-federator", core_services) - self.assertNotIn("child-benefit-notary", core_services) - federator = core_services["child-benefit-federator"] - federator_env = federator["environment"] - self.assertEqual( - federator["labels"]["solmara.lab.host"], - "child-benefit-federator.solmara.registrystack.org", - ) - - expected = ( - ( - "compose.coolify.interior.yaml", - "cra-notary", - "cra-civil-relay", - "cra-civil", - "CRA_NOTARY_URL", - "CRA_CHILD_BENEFIT_CLIENT_TOKEN_HASH", - {"birth-is-registered", "child-age-under-5"}, - ), - ( - "compose.coolify.interior.yaml", - "nia-notary", - "nia-population-relay", - "nia-population", - "NIA_NOTARY_URL", - "NIA_CHILD_BENEFIT_CLIENT_TOKEN_HASH", - {"population-record-active"}, - ), - ( - "compose.coolify.social-development.yaml", - "sro-notary", - "sro-social-relay", - "sro-social", - "SRO_NOTARY_URL", - "SRO_CHILD_BENEFIT_CLIENT_TOKEN_HASH", - {"household-below-poverty-threshold"}, - ), - ( - "compose.coolify.social-development.yaml", - "programme-notary", - "programme-mis-relay", - "mosd-programme", - "PROGRAMME_NOTARY_URL", - "PROGRAMME_CHILD_BENEFIT_CLIENT_TOKEN_HASH", - {"not-already-enrolled"}, - ), - ) - for ( - compose_name, - service_id, - relay_id, - project, - url_env, - token_hash_env, - expected_claims, - ) in expected: - with self.subTest(service=service_id): - compose = yaml.safe_load( - (ROOT / compose_name).read_text(encoding="utf-8") - ) - services = compose["services"] - self.assertNotIn("child-benefit-notary", services) - service = services[service_id] - self.assertEqual( - service["network_mode"], - f"service:{relay_id}-consultation", - ) - self.assertEqual( - service["labels"]["solmara.lab.host"], - f"{service_id}.solmara.registrystack.org", - ) - self.assertIn(token_hash_env, service["environment"]) - - public_url = f"https://{service_id}.solmara.registrystack.org" - self.assertEqual(federator_env[url_env], public_url) - config = yaml.safe_load( - ( - ROOT - / "runtime" - / "registry-projects" - / "hosted" - / project - / "notary" - / "notary.yaml" - ).read_text(encoding="utf-8") - ) - self.assertEqual(config["instance"]["id"], service_id) - self.assertEqual(config["evidence"]["service_id"], service_id) - self.assertEqual( - config["evidence"]["relay"]["base_url"], - "http://127.0.0.1:8080", - ) - self.assertTrue( - config["evidence"]["relay"]["allow_insecure_localhost"] - ) - self.assertEqual( - config["evidence"]["relay"]["workload_client_id"], service_id - ) - self.assertEqual( - config["evidence"]["relay"]["token_file"], - f"/run/secrets/{service_id}-relay-token", - ) - self.assertEqual( - { - claim["id"] - for claim in config["evidence"]["claims"] - if claim["purpose"] - == "https://id.registrystack.org/solmara/purpose/child-benefit-review" - }, - expected_claims, - ) - - self.assertFalse((ROOT / "hosted" / "notaries" / "child-benefit.yaml").exists()) - - def test_story_preview_smoke_passes_current_tree(self) -> None: - result = subprocess.run( - [str(ROOT / "scripts" / "smoke-story-previews.py")], - cwd=ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - self.assertEqual(result.returncode, 0, result.stderr) - - def test_live_smoke_extracts_claim_values(self) -> None: - smoke_live = load_smoke_live() - values = smoke_live.claim_values( - { - "results": [ - {"claim_id": "population-record-active", "value": True}, - {"claim_id": "not-already-enrolled", "satisfied": False}, - ] - } - ) - - self.assertEqual( - values, - {"population-record-active": True, "not-already-enrolled": False}, - ) - - def test_live_smoke_extracts_catalog_claim_ids(self) -> None: - smoke_live = load_smoke_live() - - self.assertEqual( - smoke_live.catalog_claim_ids( - {"data": [{"id": "person-is-deceased"}, {"id": "survivor-is-eligible"}]} - ), - {"person-is-deceased", "survivor-is-eligible"}, - ) - - def test_live_smoke_waits_for_notary_readiness(self) -> None: - smoke_live = load_smoke_live() - urls: list[str] = [] - original_http_json = smoke_live.http_json - - def ready(method, url, headers, *, timeout): - urls.append(url) - return SimpleNamespace(status=200, error="") - - smoke_live.http_json = ready - try: - self.assertIsNone( - smoke_live.wait_for_readiness("http://notary.test", "Test Notary") - ) - finally: - smoke_live.http_json = original_http_json - - self.assertEqual(urls, ["http://notary.test/ready"]) - - def test_child_benefit_offerings_advertise_only_the_endpoint_purpose(self) -> None: - expected_purposes = [ - "https://id.registrystack.org/solmara/purpose/child-benefit-review" - ] - offering_ids = ( - "cra-birth-registration-offering", - "nia-population-population-status-offering", - "sro-social-household-poverty-offering", - "mosd-programme-beneficiary-enrollment-offering", - ) - - for offering_id in offering_ids: - with self.subTest(offering=offering_id): - path = ( - ROOT - / "metadata" - / "public" - / "metadata" - / "evidence-offerings" - / f"{offering_id}.json" - ) - offering = json.loads(path.read_text(encoding="utf-8")) - self.assertEqual(offering["purposes"], expected_purposes) - - def test_child_benefit_authority_predicate_collection_is_publicly_discoverable( - self, - ) -> None: - catalog_path = ROOT / "metadata" / "public" / "metadata" / "catalog.json" - catalog = json.loads(catalog_path.read_text(encoding="utf-8")) - services = {service["id"]: service for service in catalog["data_services"]} - collector = services["child-benefit-evidence-collector-api"] - - self.assertEqual( - collector["endpoint_url"], - "https://child-benefit-federator.solmara.registrystack.org/v1/evaluations", - ) - child_service = next( - service - for service in catalog["public_services"] - if service["id"] == "child-benefit-review" - ) - self.assertIn( - "child-benefit-evidence-collector-api", child_service["data_services"] - ) - - offering_path = ( - ROOT - / "metadata" - / "public" - / "metadata" - / "evidence-offerings" - / "solmara.child-benefit.authority-predicate-collection.json" - ) - offering = json.loads(offering_path.read_text(encoding="utf-8")) - self.assertEqual( - offering["access"]["endpoint_url"], - "https://child-benefit-federator.solmara.registrystack.org/v1/evaluations", - ) - self.assertEqual( - offering["access"]["media_type"], - "application/json", - ) - self.assertEqual( - offering["purposes"], - ["https://id.registrystack.org/solmara/purpose/child-benefit-review"], - ) - self.assertEqual(offering["public_services"], ["child-benefit-review"]) - - def test_compose_project_name_is_stable_and_checkout_scoped(self) -> None: - compose_names = load_compose_project_name() - - first = compose_names.compose_project_name(Path("/tmp/solmara-lab")) - second = compose_names.compose_project_name(Path("/tmp/other/solmara-lab")) - - self.assertRegex(first, r"^solmara-lab-[0-9a-f]{10}$") - self.assertNotEqual(first, second) - - def test_notary_bru_requests_match_configured_auth_and_disclosure(self) -> None: - requests = [ - ROOT - / "requests" - / "registry-lab" - / "20 - Child Benefit" - / "01 - Collect source predicates.bru", - ROOT - / "requests" - / "registry-lab" - / "30 - Pension Survivor" - / "01 - Evaluate pension stop.bru", - ROOT - / "requests" - / "registry-lab" - / "30 - Pension Survivor" - / "02 - Read active pension payment.bru", - ROOT - / "requests" - / "registry-lab" - / "30 - Pension Survivor" - / "03 - Read survivor eligibility.bru", - ROOT - / "requests" - / "registry-lab" - / "40 - NAgDI Voucher" - / "01 - Voucher eligibility.bru", - ROOT - / "requests" - / "registry-lab" - / "40 - NAgDI Voucher" - / "02 - Livestock movement control.bru", - ] - - for request_path in requests: - with self.subTest(request=request_path.name): - request = request_path.read_text() - self.assertIn("x-api-key: {{", request) - self.assertNotIn("Authorization: Bearer", request) - self.assertIn('"disclosure": "predicate"', request) - self.assertNotIn('"disclosure": "decision"', request) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_registry_stack_release_pin.py b/scripts/test_registry_stack_release_pin.py new file mode 100644 index 0000000..3bb7fc2 --- /dev/null +++ b/scripts/test_registry_stack_release_pin.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import importlib.util +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("check-registry-stack-release-pin.py") +SPEC = importlib.util.spec_from_file_location("release_pin", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +def authenticator_values() -> dict[str, str]: + base = "https://github.com/jeremi/esignet-relay-authenticator/releases/download/v0.2.0/" + return { + "ESIGNET_AUTHENTICATOR_VERSION": "0.2.0", + "ESIGNET_AUTHENTICATOR_RELEASE_URL": "https://github.com/jeremi/esignet-relay-authenticator/releases/tag/v0.2.0", + "ESIGNET_AUTHENTICATOR_JAR_URL": base + "esignet-relay-authenticator-0.2.0.jar", + "ESIGNET_AUTHENTICATOR_JAR_SHA256": "e" * 64, + "ESIGNET_AUTHENTICATOR_CHECKSUM_URL": base + "esignet-relay-authenticator-0.2.0.jar.sha256", + } + + +class ReleasePinTests(unittest.TestCase): + def test_v0200_is_rejected_in_favour_of_fix_forward_release(self) -> None: + values = {"REGISTRY_STACK_REQUIRED_VERSION": "0.20.0", **authenticator_values()} + self.assertIn("must be 0.20.1", MODULE.validate(values, require_public=False)[0]) + + def test_missing_public_digest_is_an_explicit_blocker(self) -> None: + values = { + "REGISTRY_STACK_REQUIRED_VERSION": "0.20.1", + "REGISTRY_STACK_SOURCE_REF": "v0.20.1", + **authenticator_values(), + } + self.assertEqual(MODULE.validate(values, require_public=False), []) + self.assertIn("promotion is blocked", MODULE.validate(values, require_public=True)[0]) + + def test_digest_must_be_exact(self) -> None: + values = { + "REGISTRY_STACK_REQUIRED_VERSION": "0.20.1", + "REGISTRY_STACK_SOURCE_REF": "v0.20.1", + "REGISTRY_STACK_SOURCE_COMMIT": "f" * 40, + "REGISTRY_STACK_RELEASE_RELAY_DIGEST": "a" * 64, + "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL": "https://github.com/registrystack/registry-stack/releases/download/v0.20.1/evidence-v0.20.1-linux-amd64", + "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256": "b" * 64, + "REGISTRY_STACK_RELEASE_MINT_ASSET_URL": "https://github.com/registrystack/registry-stack/releases/download/v0.20.1/mint-v0.20.1-linux-amd64", + "REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256": "c" * 64, + "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL": "https://github.com/registrystack/registry-stack/releases/download/v0.20.1/relayctl-v0.20.1-linux-amd64", + "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256": "d" * 64, + **authenticator_values(), + } + self.assertEqual(MODULE.validate(values, require_public=True), []) + values["REGISTRY_STACK_RELEASE_RELAY_DIGEST"] = "sha256:" + "a" * 64 + self.assertTrue(MODULE.validate(values, require_public=True)) + + def test_public_source_ref_must_bind_the_release_tag(self) -> None: + values = { + "REGISTRY_STACK_REQUIRED_VERSION": "0.20.1", + "REGISTRY_STACK_SOURCE_REF": "main", + **authenticator_values(), + } + self.assertIn( + "REGISTRY_STACK_SOURCE_REF must be v0.20.1", + MODULE.validate(values, require_public=False), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_registryctl_build_output.py b/scripts/test_registryctl_build_output.py deleted file mode 100644 index b2415d5..0000000 --- a/scripts/test_registryctl_build_output.py +++ /dev/null @@ -1,133 +0,0 @@ -from __future__ import annotations - -import importlib.util -import json -import tempfile -import unittest -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] -SPEC = importlib.util.spec_from_file_location( - "registryctl_build_output", - ROOT / "scripts" / "registryctl-build-output.py", -) -assert SPEC and SPEC.loader -MODULE = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(MODULE) - - -class RegistryctlBuildOutputTests(unittest.TestCase): - def setUp(self) -> None: - self.temporary = tempfile.TemporaryDirectory() - self.addCleanup(self.temporary.cleanup) - self.project = Path(self.temporary.name) / "project" - self.project.mkdir() - (self.project / "registry-stack.yaml").write_text( - "version: 1\nregistry:\n id: example\n", - encoding="utf-8", - ) - self.output = self.project / ".registry-stack" / "build" / "local" - for lane in ("relay-public", "relay-consultation"): - relay = self.output / "private" / lane / "config" - relay.mkdir(parents=True) - (relay / "relay.yaml").write_text("instance: {}\n", encoding="utf-8") - - def report(self, **overrides: object) -> bytes: - build: dict[str, object] = { - "schema_version": MODULE.BUILD_SCHEMA, - "status": "built", - "project": "example", - "environment": "local", - "fixtures": [], - "semantic_changes": [], - "baseline": "initial_without_baseline", - "output": ".registry-stack/build/local", - } - build.update(overrides) - report: dict[str, object] = { - "schema_version": MODULE.REPORT_SCHEMA, - "affected_lanes": ["relay-public", "relay-consultation"], - "build": build, - } - return json.dumps(report).encode("utf-8") - - def test_accepts_the_versioned_project_owned_build_root(self) -> None: - self.assertEqual( - MODULE.parse_build_output( - self.report(), - project_directory=self.project, - environment="local", - ), - self.output.resolve(), - ) - - def test_accepts_the_legacy_absolute_project_owned_build_root(self) -> None: - self.assertEqual( - MODULE.parse_build_output( - self.report(output=str(self.output)), - project_directory=self.project, - environment="local", - ), - self.output.resolve(), - ) - - def test_rejects_the_wrong_environment_without_echoing_report_values(self) -> None: - with self.assertRaisesRegex( - MODULE.BuildReportError, - "wrong environment binding", - ) as rejected: - MODULE.parse_build_output( - self.report(environment="hosted", secret="must-not-echo"), - project_directory=self.project, - environment="local", - ) - self.assertNotIn("must-not-echo", str(rejected.exception)) - - def test_rejects_an_output_root_outside_the_project(self) -> None: - outside = Path(self.temporary.name) / "outside" - outside.mkdir() - with self.assertRaisesRegex( - MODULE.BuildReportError, - "not a real project-owned directory", - ): - MODULE.parse_build_output( - self.report(output=str(outside)), - project_directory=self.project, - environment="local", - ) - - def test_rejects_a_relative_output_root_that_escapes_the_project(self) -> None: - outside = Path(self.temporary.name) / "outside" - outside.mkdir() - with self.assertRaisesRegex( - MODULE.BuildReportError, - "not a real project-owned directory", - ): - MODULE.parse_build_output( - self.report(output="../outside"), - project_directory=self.project, - environment="local", - ) - - def test_rejects_an_incomplete_product_closure(self) -> None: - ( - self.output - / "private" - / "relay-consultation" - / "config" - / "relay.yaml" - ).unlink() - with self.assertRaisesRegex( - MODULE.BuildReportError, - "configuration closure is incomplete", - ): - MODULE.parse_build_output( - self.report(), - project_directory=self.project, - environment="local", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_registryctl_test_output.py b/scripts/test_registryctl_test_output.py deleted file mode 100644 index 13171e3..0000000 --- a/scripts/test_registryctl_test_output.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import annotations - -import importlib.util -import json -import unittest -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] -SPEC = importlib.util.spec_from_file_location( - "registryctl_test_output", - ROOT / "scripts" / "registryctl-test-output.py", -) -assert SPEC and SPEC.loader -MODULE = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(MODULE) - - -class RegistryctlTestOutputTests(unittest.TestCase): - def report(self, **overrides: object) -> bytes: - report: dict[str, object] = { - "schema_version": MODULE.REPORT_SCHEMA, - "status": "passed", - "project": "example", - "fixtures": [{"fixture": "match", "passed": True}], - "fixture_coverage": { - "targets": [ - { - "identity": {"integration": "example", "capability": "snapshot"}, - "fixture_set_state": "fixture_bearing", - "compiled_contract": { - "kind": "compiled_contract", - "digest": "sha256:compiled", - }, - "fixture_inventory": [ - { - "fixture_id": "match", - "fixture_digest": "sha256:fixture", - "pass_state": "passed", - } - ], - } - ] - }, - } - report.update(overrides) - return json.dumps(report).encode("utf-8") - - def test_accepts_passing_compiled_fixture_coverage(self) -> None: - self.assertEqual( - MODULE.validate_test_report(self.report()), - ("example", 1, 1), - ) - - def test_rejects_a_target_without_a_compiled_contract(self) -> None: - coverage = {"targets": [{"identity": {"integration": "example", "capability": "snapshot"}, "fixture_set_state": "fixture_bearing", "fixture_inventory": []}]} - with self.assertRaisesRegex( - MODULE.TestReportError, - "must bind passing fixtures", - ): - MODULE.validate_test_report(self.report(fixture_coverage=coverage)) - - def test_rejects_a_non_passing_coverage_fixture(self) -> None: - report = json.loads(self.report()) - report["fixture_coverage"]["targets"][0]["fixture_inventory"][0]["pass_state"] = "failed" - with self.assertRaisesRegex( - MODULE.TestReportError, - "must bind passing fixtures", - ): - MODULE.validate_test_report(json.dumps(report).encode()) - - def test_rejects_a_non_passing_fixture_without_echoing_values(self) -> None: - with self.assertRaisesRegex( - MODULE.TestReportError, - "non-passing fixture", - ) as rejected: - MODULE.validate_test_report( - self.report( - fixtures=[ - { - "fixture": "secret-fixture-name", - "passed": False, - } - ] - ) - ) - self.assertNotIn("secret-fixture-name", str(rejected.exception)) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_relay_workload_identity_agent.py b/scripts/test_relay_workload_identity_agent.py deleted file mode 100644 index 51ec770..0000000 --- a/scripts/test_relay_workload_identity_agent.py +++ /dev/null @@ -1,591 +0,0 @@ -from __future__ import annotations - -import base64 -import contextlib -import importlib.util -import io -import json -import os -import stat -import sys -import tempfile -import threading -import unittest -import urllib.error -import urllib.request -from pathlib import Path -from unittest import mock - -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey -from cryptography.hazmat.primitives.serialization import ( - Encoding, - NoEncryption, - PrivateFormat, - PublicFormat, -) - - -ROOT = Path(__file__).resolve().parents[1] -SCRIPT = ROOT / "scripts" / "relay_workload_identity_agent.py" -SPEC = importlib.util.spec_from_file_location("relay_workload_identity_agent", SCRIPT) -assert SPEC and SPEC.loader -agent = importlib.util.module_from_spec(SPEC) -sys.modules["relay_workload_identity_agent"] = agent -SPEC.loader.exec_module(agent) - - -def b64url(value: bytes) -> str: - return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") - - -def decode_segment(value: str) -> dict[str, object]: - raw = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) - document = json.loads(raw) - assert isinstance(document, dict) - return document - - -def private_jwk( - *, kid: str = "test-workload-key" -) -> tuple[dict[str, str], Ed25519PrivateKey]: - private_key = Ed25519PrivateKey.generate() - private_bytes = private_key.private_bytes( - Encoding.Raw, PrivateFormat.Raw, NoEncryption() - ) - public_bytes = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) - return ( - { - "alg": "EdDSA", - "crv": "Ed25519", - "d": b64url(private_bytes), - "kid": kid, - "kty": "OKP", - "x": b64url(public_bytes), - }, - private_key, - ) - - -def identity_document( - directory: Path, - *, - azp: str = "cra-notary", - subject: str | None = None, - kid_env: str = "TEST_WORKLOAD_PRIVATE_JWK", - token_name: str = "relay-token", - scopes: list[str] | None = None, - token_uid: int | None = None, - token_gid: int | None = None, -) -> dict[str, object]: - document: dict[str, object] = { - "audience": "registry-relay", - "azp": azp, - "subject": subject or azp, - "scopes": scopes - or [ - "registry:consult:cra-child-benefit", - "registry:consult:cra-citizen-record", - ], - "token_file": str(directory / token_name), - "private_jwk_env": kid_env, - } - if token_uid is not None: - document["token_uid"] = token_uid - if token_gid is not None: - document["token_gid"] = token_gid - return document - - -def valid_environment(directory: Path) -> tuple[dict[str, str], Ed25519PrivateKey]: - jwk, private_key = private_jwk() - environment = { - "WORKLOAD_ISSUER": "http://127.0.0.1:8090", - "WORKLOAD_IDENTITIES_JSON": json.dumps([identity_document(directory)]), - "TEST_WORKLOAD_PRIVATE_JWK": json.dumps(jwk), - "WORKLOAD_TOKEN_UID": str(os.getuid()), - "WORKLOAD_TOKEN_GID": str(os.getgid()), - } - return environment, private_key - - -def add_esignet_identity( - environment: dict[str, str], directory: Path -) -> Ed25519PrivateKey: - jwk, private_key = private_jwk(kid="test-esignet-key") - environment["TEST_ESIGNET_PRIVATE_JWK"] = json.dumps(jwk) - identities = json.loads(environment["WORKLOAD_IDENTITIES_JSON"]) - identities.append( - identity_document( - directory, - azp="solmara-esignet", - kid_env="TEST_ESIGNET_PRIVATE_JWK", - token_name="esignet-relay-token", - scopes=["population:identity_release"], - token_uid=os.getuid(), - token_gid=os.getgid(), - ) - ) - environment["WORKLOAD_IDENTITIES_JSON"] = json.dumps(identities) - return private_key - - -class FakeClock: - def __init__(self, value: int) -> None: - self.value = value - - def __call__(self) -> float: - return float(self.value) - - -class WorkloadIdentityAgentTests(unittest.TestCase): - def test_https_dns_issuer_is_independent_from_loopback_listener(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - environment, _ = valid_environment(Path(temporary_directory)) - environment["WORKLOAD_ISSUER"] = ( - "https://workload-issuer.solmara.registrystack.org" - ) - - config = agent.Config.from_environ(environment) - - self.assertEqual(config.bind_host, "127.0.0.1") - self.assertEqual(config.port, 8090) - self.assertEqual( - config.issuer, - "https://workload-issuer.solmara.registrystack.org", - ) - - def test_each_identity_gets_exact_claims_and_its_own_signature(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - directory = Path(temporary_directory) - environment, notary_key = valid_environment(directory) - esignet_key = add_esignet_identity(environment, directory) - config = agent.Config.from_environ(environment) - - tokens = [ - agent._mint_token(config, identity, 1_700_000_000)[0] - for identity in config.identities - ] - claims = [] - for token, expected_key in zip( - tokens, (notary_key, esignet_key), strict=True - ): - encoded_header, encoded_claims, encoded_signature = token.split(".") - signature = base64.urlsafe_b64decode( - encoded_signature + "=" * (-len(encoded_signature) % 4) - ) - expected_key.public_key().verify( - signature, f"{encoded_header}.{encoded_claims}".encode("ascii") - ) - claims.append(decode_segment(encoded_claims)) - - self.assertEqual(claims[0]["azp"], "cra-notary") - self.assertEqual(claims[0]["sub"], "cra-notary") - self.assertEqual( - claims[0]["scope"], - "registry:consult:cra-child-benefit registry:consult:cra-citizen-record", - ) - self.assertEqual(claims[1]["azp"], "solmara-esignet") - self.assertEqual(claims[1]["sub"], "solmara-esignet") - self.assertEqual(claims[1]["scope"], "population:identity_release") - self.assertNotEqual(claims[0]["jti"], claims[1]["jti"]) - for token_claims in claims: - self.assertEqual(token_claims["iss"], environment["WORKLOAD_ISSUER"]) - self.assertEqual(token_claims["aud"], "registry-relay") - self.assertEqual(token_claims["iat"], 1_700_000_000) - self.assertEqual(token_claims["nbf"], 1_700_000_000) - self.assertEqual(token_claims["exp"], 1_700_000_300) - - def test_public_jwks_contains_every_distinct_public_key_only(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - directory = Path(temporary_directory) - environment, _ = valid_environment(directory) - add_esignet_identity(environment, directory) - config = agent.Config.from_environ(environment) - - document = agent.IdentityState(config).jwks_document() - - self.assertEqual( - {key["kid"] for key in document["keys"]}, - {"test-workload-key", "test-esignet-key"}, - ) - for key in document["keys"]: - self.assertEqual(set(key), {"alg", "crv", "kid", "kty", "x"}) - - def test_atomic_rotation_publishes_mode_and_ownership_together(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - directory = Path(temporary_directory) - environment, _ = valid_environment(directory) - config = agent.Config.from_environ(environment) - identity = config.identities[0] - identity.token_file.write_text("previous-token\n", encoding="ascii") - os.chmod(identity.token_file, 0o644) - state = agent.IdentityState(config, clock=FakeClock(1_700_000_000)) - original_replace = os.replace - replace_observed = False - - def inspect_then_replace(source: str, destination: Path) -> None: - nonlocal replace_observed - replace_observed = True - source_status = os.stat(source) - self.assertEqual(stat.S_IMODE(source_status.st_mode), 0o600) - self.assertEqual(source_status.st_uid, os.getuid()) - self.assertEqual(source_status.st_gid, os.getgid()) - self.assertEqual( - identity.token_file.read_text(encoding="ascii"), "previous-token\n" - ) - original_replace(source, destination) - - with mock.patch.object( - agent.os, "replace", side_effect=inspect_then_replace - ): - state.rotate() - - self.assertTrue(replace_observed) - file_status = identity.token_file.stat() - self.assertEqual(stat.S_IMODE(file_status.st_mode), 0o600) - self.assertEqual(file_status.st_uid, os.getuid()) - self.assertEqual(file_status.st_gid, os.getgid()) - self.assertEqual( - len(identity.token_file.read_text(encoding="ascii").split(".")), 3 - ) - self.assertFalse(identity.token_file.read_bytes().endswith(b"\n")) - self.assertTrue(state.ready()) - - def test_readiness_requires_every_current_regular_output(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - directory = Path(temporary_directory) - environment, _ = valid_environment(directory) - add_esignet_identity(environment, directory) - clock = FakeClock(1_700_000_000) - config = agent.Config.from_environ(environment) - state = agent.IdentityState(config, clock=clock) - state.rotate() - self.assertTrue(state.ready()) - - config.identities[1].token_file.write_text( - "not-the-current-token\n", encoding="ascii" - ) - self.assertFalse(state.ready()) - state.rotate() - self.assertTrue(state.ready()) - - config.identities[1].token_file.write_bytes(b"") - self.assertFalse(state.ready()) - state.rotate() - self.assertTrue(state.ready()) - - config.identities[1].token_file.unlink() - config.identities[1].token_file.symlink_to(config.identities[0].token_file) - self.assertFalse(state.ready()) - - config.identities[1].token_file.unlink() - state.rotate() - clock.value += config.token_ttl_seconds - self.assertFalse(state.ready()) - - def test_rotation_occurs_only_inside_the_configured_expiry_window(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - environment, _ = valid_environment(Path(temporary_directory)) - environment["WORKLOAD_TOKEN_TTL_SECONDS"] = "120" - environment["WORKLOAD_ROTATE_BEFORE_SECONDS"] = "30" - environment["WORKLOAD_ROTATION_INTERVAL_SECONDS"] = "5" - config = agent.Config.from_environ(environment) - identity = config.identities[0] - clock = FakeClock(1_700_000_000) - state = agent.IdentityState(config, clock=clock) - state.rotate() - first_claims = decode_segment( - identity.token_file.read_text(encoding="ascii").strip().split(".")[1] - ) - - clock.value += 89 - self.assertFalse(state.rotate_if_due()) - clock.value += 1 - self.assertTrue(state.rotate_if_due()) - rotated_claims = decode_segment( - identity.token_file.read_text(encoding="ascii").strip().split(".")[1] - ) - - self.assertNotEqual(first_claims["jti"], rotated_claims["jti"]) - self.assertEqual(rotated_claims["iat"], 1_700_000_090) - self.assertEqual(rotated_claims["exp"], 1_700_000_210) - - def test_health_and_jwks_reflect_all_published_state(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - directory = Path(temporary_directory) - environment, _ = valid_environment(directory) - add_esignet_identity(environment, directory) - config = agent.Config.from_environ(environment) - state = agent.IdentityState(config, clock=FakeClock(1_700_000_000)) - server = agent.AgentHTTPServer(("127.0.0.1", 0), state) - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() - base_url = f"http://127.0.0.1:{server.server_address[1]}" - try: - status, health = self._get_json(f"{base_url}/health") - self.assertEqual(status, 503) - self.assertEqual(health, {"status": "not_ready"}) - - status, jwks = self._get_json(f"{base_url}/.well-known/jwks.json") - self.assertEqual(status, 200) - self.assertEqual(len(jwks["keys"]), 2) - self.assertTrue(all("d" not in key for key in jwks["keys"])) - - state.rotate() - status, health = self._get_json(f"{base_url}/health") - self.assertEqual(status, 200) - self.assertEqual(health, {"status": "ready"}) - - config.identities[1].token_file.write_bytes(b"") - status, health = self._get_json(f"{base_url}/health") - self.assertEqual(status, 503) - self.assertEqual(health, {"status": "not_ready"}) - - status, missing = self._get_json(f"{base_url}/other") - self.assertEqual(status, 404) - self.assertEqual(missing, {"error": "not_found"}) - finally: - server.shutdown() - server.server_close() - thread.join(timeout=5) - - def test_configuration_defaults_and_per_identity_ownership_override(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - directory = Path(temporary_directory) - environment, _ = valid_environment(directory) - del environment["WORKLOAD_TOKEN_UID"] - del environment["WORKLOAD_TOKEN_GID"] - add_esignet_identity(environment, directory) - identities = json.loads(environment["WORKLOAD_IDENTITIES_JSON"]) - identities[1]["token_uid"] = 1001 - identities[1]["token_gid"] = 1001 - environment["WORKLOAD_IDENTITIES_JSON"] = json.dumps(identities) - - config = agent.Config.from_environ(environment) - - self.assertEqual(config.identities[0].token_uid, 65534) - self.assertEqual(config.identities[0].token_gid, 65534) - self.assertEqual(config.identities[1].token_uid, 1001) - self.assertEqual(config.identities[1].token_gid, 1001) - - @unittest.skipUnless(os.geteuid() == 0, "requires root to verify distinct owners") - def test_rotation_applies_distinct_configured_owners(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - directory = Path(temporary_directory) - environment, _ = valid_environment(directory) - add_esignet_identity(environment, directory) - identities = json.loads(environment["WORKLOAD_IDENTITIES_JSON"]) - identities[0]["token_uid"] = 65534 - identities[0]["token_gid"] = 65534 - identities[1]["token_uid"] = 1001 - identities[1]["token_gid"] = 1001 - environment["WORKLOAD_IDENTITIES_JSON"] = json.dumps(identities) - config = agent.Config.from_environ(environment) - - state = agent.IdentityState(config, clock=FakeClock(1_700_000_000)) - state.rotate() - - self.assertEqual( - ( - config.identities[0].token_file.stat().st_uid, - config.identities[0].token_file.stat().st_gid, - ), - (65534, 65534), - ) - self.assertEqual( - ( - config.identities[1].token_file.stat().st_uid, - config.identities[1].token_file.stat().st_gid, - ), - (1001, 1001), - ) - self.assertTrue(state.ready()) - - def test_configuration_rejects_unsafe_or_ambiguous_values(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - directory = Path(temporary_directory) - baseline, _ = valid_environment(directory) - jwk = json.loads(baseline["TEST_WORKLOAD_PRIVATE_JWK"]) - mismatched_jwk, _ = private_jwk() - - def identities(change: dict[str, object]) -> str: - document = identity_document(directory) - document.update(change) - return json.dumps([document]) - - cases: dict[str, dict[str, str | None]] = { - "non_loopback_bind": {"WORKLOAD_BIND_HOST": "0.0.0.0"}, - "non_loopback_issuer": {"WORKLOAD_ISSUER": "http://localhost:8090"}, - "https_ip_issuer": {"WORKLOAD_ISSUER": "https://127.0.0.1"}, - "https_localhost_issuer": {"WORKLOAD_ISSUER": "https://localhost"}, - "https_userinfo_issuer": { - "WORKLOAD_ISSUER": "https://user@example.test" - }, - "https_nonstandard_port": { - "WORKLOAD_ISSUER": "https://issuer.example.test:8443" - }, - "malformed_issuer": {"WORKLOAD_ISSUER": "http://["}, - "issuer_path": {"WORKLOAD_ISSUER": "http://127.0.0.1:8090/issuer"}, - "https_issuer_path": { - "WORKLOAD_ISSUER": "https://issuer.example.test/issuer" - }, - "issuer_port_mismatch": {"WORKLOAD_PORT": "8091"}, - "blank_audience": { - "WORKLOAD_IDENTITIES_JSON": identities({"audience": ""}) - }, - "spaced_azp": { - "WORKLOAD_IDENTITIES_JSON": identities({"azp": "cra notary"}) - }, - "duplicate_scope": { - "WORKLOAD_IDENTITIES_JSON": identities( - {"scopes": ["registry:read", "registry:read"]} - ) - }, - "empty_scope_entry": { - "WORKLOAD_IDENTITIES_JSON": identities( - {"scopes": ["registry:read", ""]} - ) - }, - "relative_token_file": { - "WORKLOAD_IDENTITIES_JSON": identities( - {"token_file": "relay-token"} - ) - }, - "missing_token_parent": { - "WORKLOAD_IDENTITIES_JSON": identities( - {"token_file": str(directory / "missing" / "relay-token")} - ) - }, - "ttl_too_long": {"WORKLOAD_TOKEN_TTL_SECONDS": "901"}, - "rotation_too_late": { - "WORKLOAD_TOKEN_TTL_SECONDS": "30", - "WORKLOAD_ROTATE_BEFORE_SECONDS": "26", - }, - "rotation_poll_too_slow": { - "WORKLOAD_ROTATE_BEFORE_SECONDS": "10", - "WORKLOAD_ROTATION_INTERVAL_SECONDS": "11", - }, - "negative_uid": { - "WORKLOAD_IDENTITIES_JSON": identities({"token_uid": -1}) - }, - "boolean_gid": { - "WORKLOAD_IDENTITIES_JSON": identities({"token_gid": True}) - }, - "bad_key_environment_name": { - "WORKLOAD_IDENTITIES_JSON": identities( - {"private_jwk_env": "bad-name"} - ) - }, - "missing_key_environment": {"TEST_WORKLOAD_PRIVATE_JWK": None}, - "wrong_curve": { - "TEST_WORKLOAD_PRIVATE_JWK": json.dumps({**jwk, "crv": "X25519"}) - }, - "unsupported_key_field": { - "TEST_WORKLOAD_PRIVATE_JWK": json.dumps( - {**jwk, "key_ops": ["sign"]} - ) - }, - "mismatched_public_key": { - "TEST_WORKLOAD_PRIVATE_JWK": json.dumps( - {**jwk, "x": mismatched_jwk["x"]} - ) - }, - "duplicate_key_field": { - "TEST_WORKLOAD_PRIVATE_JWK": ( - '{"kty":"OKP","kty":"OKP","crv":"Ed25519",' - '"alg":"EdDSA","kid":"key","x":"a","d":"a"}' - ) - }, - "empty_identity_list": {"WORKLOAD_IDENTITIES_JSON": "[]"}, - "too_many_identities": { - "WORKLOAD_IDENTITIES_JSON": json.dumps( - [identity_document(directory)] * (agent.MAX_IDENTITIES + 1) - ) - }, - "unsupported_identity_field": { - "WORKLOAD_IDENTITIES_JSON": identities({"name": "notary"}) - }, - "duplicate_identity_field": { - "WORKLOAD_IDENTITIES_JSON": ( - '[{"audience":"registry-relay","audience":"other"}]' - ) - }, - } - for name, changes in cases.items(): - with self.subTest(name=name): - environment = dict(baseline) - for key, value in changes.items(): - if value is None: - environment.pop(key, None) - else: - environment[key] = value - with self.assertRaises(agent.ConfigurationError): - agent.Config.from_environ(environment) - - def test_configuration_rejects_duplicate_identity_bindings(self) -> None: - with tempfile.TemporaryDirectory() as temporary_directory: - directory = Path(temporary_directory) - baseline, _ = valid_environment(directory) - second_jwk, _ = private_jwk(kid="second-key") - baseline["SECOND_PRIVATE_JWK"] = json.dumps(second_jwk) - cloned_key = json.loads(baseline["TEST_WORKLOAD_PRIVATE_JWK"]) - cloned_key["kid"] = "cloned-key-id" - baseline["CLONED_PRIVATE_JWK"] = json.dumps(cloned_key) - base = identity_document(directory) - independent = identity_document( - directory, - azp="second-client", - kid_env="SECOND_PRIVATE_JWK", - token_name="second-token", - ) - duplicate_cases = { - "azp": {**independent, "azp": base["azp"]}, - "subject": {**independent, "subject": base["subject"]}, - "kid": {**independent, "private_jwk_env": "TEST_WORKLOAD_PRIVATE_JWK"}, - "public key": { - **independent, - "private_jwk_env": "CLONED_PRIVATE_JWK", - }, - "token_file": {**independent, "token_file": base["token_file"]}, - } - for field, duplicate in duplicate_cases.items(): - with self.subTest(field=field): - environment = dict(baseline) - environment["WORKLOAD_IDENTITIES_JSON"] = json.dumps( - [base, duplicate] - ) - with self.assertRaisesRegex( - agent.ConfigurationError, rf"{field} values must be unique" - ): - agent.Config.from_environ(environment) - - def test_main_does_not_echo_rejected_key_material(self) -> None: - marker = "private-key-material-must-not-appear" - with tempfile.TemporaryDirectory() as temporary_directory: - environment, _ = valid_environment(Path(temporary_directory)) - environment["TEST_WORKLOAD_PRIVATE_JWK"] = marker - stderr = io.StringIO() - with mock.patch.dict(os.environ, environment, clear=True): - with contextlib.redirect_stderr(stderr): - exit_code = agent.main() - - self.assertEqual(exit_code, 2) - self.assertNotIn(marker, stderr.getvalue()) - self.assertEqual( - stderr.getvalue(), - "workload identity agent configuration rejected: " - "private JWK is not valid JSON\n", - ) - - def _get_json(self, url: str) -> tuple[int, dict[str, object]]: - try: - response = urllib.request.urlopen(url, timeout=2) - except urllib.error.HTTPError as error: - response = error - with response: - document = json.loads(response.read()) - return response.status, document - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_release_pins.py b/scripts/test_release_pins.py deleted file mode 100644 index 25042d8..0000000 --- a/scripts/test_release_pins.py +++ /dev/null @@ -1,321 +0,0 @@ -from __future__ import annotations - -import contextlib -import importlib.util -import io -import os -import subprocess -import sys -import tempfile -import unittest -from pathlib import Path -from unittest import mock - - -ROOT = Path(__file__).resolve().parents[1] -RELAY = "ghcr.io/registrystack/registry-relay@sha256:" + "1" * 64 -NOTARY = "ghcr.io/registrystack/registry-notary@sha256:" + "2" * 64 - - -def load_check_release_pins(): - spec = importlib.util.spec_from_file_location( - "check_release_pins", ROOT / "scripts" / "check-release-pins.py" - ) - if spec is None or spec.loader is None: - raise RuntimeError("could not load check-release-pins.py") - module = importlib.util.module_from_spec(spec) - sys.modules["check_release_pins"] = module - spec.loader.exec_module(module) - return module - - -class ReleasePinTests(unittest.TestCase): - def setUp(self) -> None: - self.environment = mock.patch.dict(os.environ, {}) - self.environment.start() - self.addCleanup(self.environment.stop) - os.environ.pop("REGISTRY_RELAY_IMAGE", None) - os.environ.pop("REGISTRY_NOTARY_IMAGE", None) - - self.module = load_check_release_pins() - self.resolve_tag_implementation = self.module.resolve_tag_commit - self.resolve_tag = mock.patch.object( - self.module, - "resolve_tag_commit", - return_value="a" * 40, - ) - self.resolve_tag.start() - self.addCleanup(self.resolve_tag.stop) - self.directory = tempfile.TemporaryDirectory() - self.root = Path(self.directory.name) - self.module.ROOT = self.root - (self.root / "versions.env").write_text( - f"REGISTRY_RELAY_IMAGE={RELAY}\n" - f"REGISTRY_NOTARY_IMAGE={NOTARY}\n" - "REGISTRYCTL_VERSION=1.0.0\n" - "REGISTRY_STACK_SOURCE_REF=v1.0.0\n" - f"REGISTRY_STACK_SOURCE_COMMIT={'a' * 40}\n", - encoding="utf-8", - ) - - def tearDown(self) -> None: - self.directory.cleanup() - - def test_matching_environment_overrides_validate_committed_pins(self) -> None: - with ( - mock.patch.dict( - os.environ, - {"REGISTRY_RELAY_IMAGE": RELAY, "REGISTRY_NOTARY_IMAGE": NOTARY}, - clear=True, - ), - mock.patch.object( - self.module, - "inspect_tag_digest", - side_effect=["sha256:" + "1" * 64, "sha256:" + "2" * 64], - ) as inspect, - ): - self.assertEqual(self.module.main(["check-release-pins.py", "v1.0.0"]), 0) - - self.assertEqual(inspect.call_count, 2) - - def test_temporary_versions_are_isolated_from_ambient_image_overrides(self) -> None: - self.assertNotIn("REGISTRY_RELAY_IMAGE", os.environ) - self.assertNotIn("REGISTRY_NOTARY_IMAGE", os.environ) - - def test_candidate_test_passes_with_workflow_image_environment(self) -> None: - committed = self.module.read_versions(ROOT / "versions.env") - environment = os.environ.copy() - environment.update( - { - "REGISTRY_RELAY_IMAGE": committed["REGISTRY_RELAY_IMAGE"], - "REGISTRY_NOTARY_IMAGE": committed["REGISTRY_NOTARY_IMAGE"], - } - ) - - result = subprocess.run( - [ - sys.executable, - "-m", - "unittest", - "test_release_pins.ReleasePinTests." - "test_candidate_prerelease_tag_is_accepted", - ], - cwd=ROOT / "scripts", - env=environment, - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - self.assertEqual(result.returncode, 0, result.stderr) - - def test_candidate_prerelease_tag_is_accepted(self) -> None: - versions = (self.root / "versions.env").read_text(encoding="utf-8") - (self.root / "versions.env").write_text( - versions.replace( - "REGISTRYCTL_VERSION=1.0.0", - "REGISTRYCTL_VERSION=1.0.0-rc.1", - ), - encoding="utf-8", - ) - versions = (self.root / "versions.env").read_text(encoding="utf-8") - (self.root / "versions.env").write_text( - versions.replace( - "REGISTRY_STACK_SOURCE_REF=v1.0.0", - "REGISTRY_STACK_SOURCE_REF=v1.0.0-rc.1", - ), - encoding="utf-8", - ) - with mock.patch.object( - self.module, - "inspect_tag_digest", - side_effect=["sha256:" + "1" * 64, "sha256:" + "2" * 64], - ): - self.assertEqual( - self.module.main(["check-release-pins.py", "v1.0.0-rc.1"]), - 0, - ) - - def test_registryctl_version_must_match_release_tag(self) -> None: - versions = (self.root / "versions.env").read_text(encoding="utf-8") - (self.root / "versions.env").write_text( - versions.replace( - "REGISTRYCTL_VERSION=1.0.0", - "REGISTRYCTL_VERSION=0.13.0", - ), - encoding="utf-8", - ) - stderr = io.StringIO() - with ( - mock.patch.object(self.module, "inspect_tag_digest") as inspect, - contextlib.redirect_stderr(stderr), - ): - self.assertEqual(self.module.main(["check-release-pins.py", "v1.0.0"]), 1) - - inspect.assert_not_called() - self.assertIn( - "REGISTRYCTL_VERSION from versions.env is 0.13.0, expected 1.0.0", - stderr.getvalue(), - ) - - def test_source_commit_must_match_resolved_release_tag(self) -> None: - self.module.resolve_tag_commit.return_value = "b" * 40 - stderr = io.StringIO() - with ( - mock.patch.object(self.module, "inspect_tag_digest") as inspect, - contextlib.redirect_stderr(stderr), - ): - self.assertEqual(self.module.main(["check-release-pins.py", "v1.0.0"]), 1) - - inspect.assert_not_called() - self.assertIn( - f"REGISTRY_STACK_SOURCE_COMMIT from versions.env is {'a' * 40}", - stderr.getvalue(), - ) - self.assertIn( - f"v1.0.0 resolves to {'b' * 40}", - stderr.getvalue(), - ) - - def test_resolve_tag_commit_prefers_the_peeled_annotated_tag(self) -> None: - direct = "1" * 40 - peeled = "2" * 40 - completed = subprocess.CompletedProcess( - args=[], - returncode=0, - stdout=( - f"{direct}\trefs/tags/v1.0.0\n" - f"{peeled}\trefs/tags/v1.0.0^{{}}\n" - ), - stderr="", - ) - with mock.patch.object(self.module.subprocess, "run", return_value=completed): - self.assertEqual( - self.resolve_tag_implementation("v1.0.0"), - peeled, - ) - - def test_resolve_tag_commit_accepts_a_lightweight_tag(self) -> None: - direct = "3" * 40 - completed = subprocess.CompletedProcess( - args=[], - returncode=0, - stdout=f"{direct}\trefs/tags/v1.0.0\n", - stderr="", - ) - with mock.patch.object(self.module.subprocess, "run", return_value=completed): - self.assertEqual( - self.resolve_tag_implementation("v1.0.0"), - direct, - ) - - def test_source_ref_must_match_the_release(self) -> None: - versions = (self.root / "versions.env").read_text(encoding="utf-8") - (self.root / "versions.env").write_text( - versions.replace( - "REGISTRY_STACK_SOURCE_REF=v1.0.0", - "REGISTRY_STACK_SOURCE_REF=v0.13.0", - ), - encoding="utf-8", - ) - stderr = io.StringIO() - with ( - mock.patch.object(self.module, "inspect_tag_digest") as inspect, - contextlib.redirect_stderr(stderr), - ): - self.assertEqual(self.module.main(["check-release-pins.py", "v1.0.0"]), 1) - - inspect.assert_not_called() - self.assertIn( - "REGISTRY_STACK_SOURCE_REF from versions.env is v0.13.0, expected v1.0.0", - stderr.getvalue(), - ) - - def test_mismatched_environment_override_fails_before_registry_lookup(self) -> None: - stderr = io.StringIO() - with ( - mock.patch.dict( - os.environ, - {"REGISTRY_RELAY_IMAGE": RELAY[:-1] + "3"}, - clear=True, - ), - mock.patch.object(self.module, "inspect_tag_digest") as inspect, - contextlib.redirect_stderr(stderr), - ): - self.assertEqual(self.module.main(["check-release-pins.py", "v1.0.0"]), 1) - - self.assertEqual(inspect.call_count, 0) - self.assertIn( - "REGISTRY_RELAY_IMAGE environment override must match versions.env", - stderr.getvalue(), - ) - - def test_malicious_tags_are_rejected_before_registry_lookup(self) -> None: - malicious_tags = ( - "v1.0.0; echo INJECTED", - "v1.0.0 rc.1", - "v1.0.0'quoted", - 'v1.0.0"quoted', - "v1.0.0$(echo INJECTED)", - "v1.0.0\necho INJECTED", - ) - for tag in malicious_tags: - with self.subTest(tag=tag): - stderr = io.StringIO() - with ( - mock.patch.object(self.module, "inspect_tag_digest") as inspect, - contextlib.redirect_stderr(stderr), - ): - result = self.module.main(["check-release-pins.py", tag]) - - self.assertEqual(result, 2) - inspect.assert_not_called() - self.assertIn("tag must match", stderr.getvalue()) - - def test_release_recipe_dry_runs_do_not_interpolate_tag(self) -> None: - malicious_tags = ( - "v1.0.0; echo INJECTED", - "v1.0.0 rc.1", - "v1.0.0'quoted", - 'v1.0.0"quoted', - "v1.0.0$(echo INJECTED)", - "v1.0.0\necho INJECTED", - ) - recipes = { - "release-pins": 'scripts/check-release-pins.py "$1"', - "review-release": ( - 'scripts/check-release-pins.py "$1"\nscripts/review.sh' - ), - } - for recipe, expected in recipes.items(): - for tag in malicious_tags: - with self.subTest(recipe=recipe, tag=tag): - result = subprocess.run( - ["just", "--dry-run", recipe, tag], - cwd=ROOT, - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stderr.strip(), expected) - - def test_review_release_recipe_requires_tag(self) -> None: - result = subprocess.run( - ["just", "--dry-run", "review-release"], - cwd=ROOT, - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("takes 1", result.stderr) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py new file mode 100644 index 0000000..76fcc48 --- /dev/null +++ b/scripts/test_runtime_topology.py @@ -0,0 +1,404 @@ +from __future__ import annotations + +import importlib.util +import tempfile +import unittest +from pathlib import Path + +import yaml + +SCRIPT = Path(__file__).with_name("check-runtime-topology.py") +SPEC = importlib.util.spec_from_file_location("runtime_topology", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class RuntimeTopologyTests(unittest.TestCase): + def test_retired_surfaces_are_rejected(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "compose.yaml" + path.write_text("services:\n old:\n image: postgres:16\n", encoding="utf-8") + self.assertEqual(MODULE.failures([path]), ["compose.yaml:3: retired database"]) + + def test_current_active_topology_is_closed(self) -> None: + self.assertEqual(MODULE.failures(), []) + + def test_local_relays_use_the_v2_runtime_filesystem_contract(self) -> None: + compose_path = SCRIPT.parents[1] / "compose.yaml" + compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) + + for authority in ("cra", "nia", "mosd", "sipf", "nagdi"): + service = compose["services"][f"{authority}-relay"] + self.assertEqual( + service["command"], + ["serve", "--runtime", f"/etc/relay/{authority}/runtime.yaml"], + ) + volumes = set(service["volumes"]) + self.assertIn( + f"./relays/{authority}/runtime.yaml:/etc/relay/{authority}/runtime.yaml:ro", + volumes, + ) + self.assertIn( + f"./relays/{authority}/package:/etc/relay/{authority}/package:ro", + volumes, + ) + self.assertIn( + f"{authority}-relay-source:/var/lib/relay/source:ro", + volumes, + ) + self.assertIn( + f"{authority}-relay-audit:/var/lib/relay/audit", + volumes, + ) + self.assertEqual( + service["depends_on"]["relay-audit-init"]["condition"], + "service_completed_successfully", + ) + self.assertEqual( + service["depends_on"]["relay-issuer-readiness"]["condition"], + "service_completed_successfully", + ) + self.assertEqual( + service["depends_on"][f"{authority}-source-publisher"]["condition"], + "service_completed_successfully", + ) + + publisher = compose["services"][f"{authority}-source-publisher"] + self.assertEqual(publisher["network_mode"], "none") + self.assertEqual(publisher["user"], "0:0") + self.assertTrue(publisher["read_only"]) + self.assertEqual(publisher["cap_drop"], ["ALL"]) + self.assertIn("no-new-privileges:true", publisher["security_opt"]) + self.assertEqual(publisher["restart"], "no") + self.assertEqual(publisher["command"], ["ensure-seeded"]) + publisher_volumes = set(publisher["volumes"]) + self.assertIn( + f"./output/sqlite/relay/{authority}.sqlite:/seed/{authority}.sqlite:ro", + publisher_volumes, + ) + self.assertIn( + f"{authority}-relay-source:/var/lib/relay/source", + publisher_volumes, + ) + for other in {"cra", "nia", "mosd", "sipf", "nagdi"} - {authority}: + self.assertFalse( + any(f"{other}-relay-source:" in volume for volume in publisher_volumes), + (authority, other), + ) + + initializer = compose["services"]["relay-audit-init"] + self.assertEqual(initializer["network_mode"], "none") + self.assertEqual(initializer["user"], "0:0") + self.assertEqual(initializer["cap_drop"], ["ALL"]) + self.assertEqual(set(initializer["cap_add"]), {"CHOWN", "FOWNER"}) + self.assertIn("no-new-privileges:true", initializer["security_opt"]) + audit_init = initializer["command"][2] + self.assertIn("os.scandir(path)", audit_init) + self.assertIn("stat.S_ISREG", audit_init) + self.assertIn("metadata.st_nlink != 1", audit_init) + self.assertIn("audit\\.jsonl\\.\\d{8}", audit_init) + self.assertIn("follow_symlinks=False", audit_init) + for authority in ("cra", "nia", "mosd", "sipf", "nagdi"): + self.assertIn( + f"{authority}-relay-audit:/audit/{authority}", + initializer["volumes"], + ) + + issuer_readiness = compose["services"]["relay-issuer-readiness"] + self.assertEqual(issuer_readiness["user"], "65532:65532") + self.assertTrue(issuer_readiness["read_only"]) + self.assertEqual(issuer_readiness["cap_drop"], ["ALL"]) + self.assertIn("no-new-privileges:true", issuer_readiness["security_opt"]) + self.assertEqual(issuer_readiness["restart"], "no") + self.assertEqual(issuer_readiness["networks"], ["issuer-validation"]) + self.assertEqual( + issuer_readiness["depends_on"], + { + "mint": {"condition": "service_started"}, + "evidence-gateway": {"condition": "service_started"}, + }, + ) + readiness_code = issuer_readiness["command"][2] + self.assertIn("/.well-known/openid-configuration", readiness_code) + self.assertIn("document.get('issuer') == issuer", readiness_code) + self.assertIn("ssl.create_default_context", readiness_code) + self.assertNotIn("token", readiness_code.lower()) + + def test_authority_cells_have_fixed_private_addresses(self) -> None: + compose_path = SCRIPT.parents[1] / "compose.yaml" + compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) + expected = { + "mint": "172.29.1.20", + "cra-evidence": "172.29.1.21", + "nia-evidence": "172.29.1.22", + "sro-evidence": "172.29.1.23", + "mosd-programme-evidence": "172.29.1.24", + "sipf-evidence": "172.29.1.25", + "nagdi-evidence": "172.29.1.26", + } + for service_name, address in expected.items(): + self.assertEqual( + compose["services"][service_name]["networks"]["runtime"]["ipv4_address"], + address, + ) + + caddy = (SCRIPT.parents[1] / "config/evidence/Caddyfile").read_text(encoding="utf-8") + evidence_routes = { + "cra": "172.29.1.21", + "nia": "172.29.1.22", + "sro": "172.29.1.23", + "mosd-programme": "172.29.1.24", + "sipf": "172.29.1.25", + "nagdi": "172.29.1.26", + } + for authority, address in evidence_routes.items(): + self.assertIn(f"handle_path /evidence/{authority}/*", caddy) + self.assertIn(f"reverse_proxy {address}:8080", caddy) + + def test_local_oidc_issuer_uses_an_isolated_testnet_address(self) -> None: + compose_path = SCRIPT.parents[1] / "compose.yaml" + compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) + + issuer_network = compose["networks"]["issuer-validation"] + self.assertTrue(issuer_network["internal"]) + self.assertEqual( + issuer_network["ipam"]["config"], + [{"subnet": "192.0.2.0/27", "ip_range": "192.0.2.0/28"}], + ) + + gateway_networks = compose["services"]["evidence-gateway"]["networks"] + self.assertNotIn( + "mint.solmara.registrystack.org", + gateway_networks["runtime"]["aliases"], + ) + self.assertEqual( + gateway_networks["issuer-validation"], + { + "ipv4_address": "192.0.2.30", + "aliases": ["mint.solmara.registrystack.org"], + }, + ) + + strict_oidc_consumers = { + "cra-relay", + "nia-relay", + "mosd-relay", + "sipf-relay", + "nagdi-relay", + "cra-evidence", + "nia-evidence", + "sro-evidence", + "mosd-programme-evidence", + "sipf-evidence", + "nagdi-evidence", + } + for service_name in strict_oidc_consumers: + self.assertIn( + "issuer-validation", + compose["services"][service_name]["networks"], + service_name, + ) + + def test_local_transit_signers_are_one_key_one_socket_sidecars(self) -> None: + compose_path = SCRIPT.parents[1] / "compose.yaml" + compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) + providers = { + "mint": ("mint", "mint", "solmara-mint"), + "cra": ("cra", "cra-evidence", "solmara-evidence-cra"), + "nia": ("nia", "nia-evidence", "solmara-evidence-nia"), + "sro": ("sro", "sro-evidence", "solmara-evidence-sro"), + "mosd-programme": ( + "mosd-programme", + "mosd-programme-evidence", + "solmara-evidence-mosd-programme", + ), + "sipf": ("sipf", "sipf-evidence", "solmara-evidence-sipf"), + "nagdi": ("nagdi", "nagdi-evidence", "solmara-evidence-nagdi"), + } + for signer_name, (provider, consumer_name, key_name) in providers.items(): + signer = compose["services"][f"{signer_name}-signer"] + self.assertEqual(signer["network_mode"], "none") + self.assertEqual(signer["user"], "0:0") + self.assertEqual(signer["cap_drop"], ["ALL"]) + self.assertIn("no-new-privileges:true", signer["security_opt"]) + self.assertEqual(signer["command"][-1], key_name) + signer_volumes = set(signer["volumes"]) + self.assertIn( + f"./config/evidence/local/cells/{provider}/secrets/signing.jwk:/run/secrets/signing.jwk:ro", + signer_volumes, + ) + self.assertIn(f"{signer_name}-transit:/transit", signer_volumes) + + consumer = compose["services"][consumer_name] + socket_destination = ( + "/run/registry-mint" + if provider == "mint" + else f"/run/registry-evidence/{provider}" + ) + self.assertIn( + f"{signer_name}-transit:{socket_destination}:ro", + set(consumer["volumes"]), + ) + self.assertEqual( + consumer["depends_on"][f"{signer_name}-signer"]["condition"], + "service_healthy", + ) + + def test_hosted_relays_keep_runtime_and_source_read_only(self) -> None: + compose_paths = ( + SCRIPT.parents[1] / "compose.coolify.interior.yaml", + SCRIPT.parents[1] / "compose.coolify.social-development.yaml", + SCRIPT.parents[1] / "compose.coolify.labour-pensions.yaml", + SCRIPT.parents[1] / "compose.coolify.agriculture.yaml", + ) + services: dict[str, object] = {} + for compose_path in compose_paths: + compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) + services.update(compose["services"]) + + for authority in ("cra", "nia", "mosd", "sipf", "nagdi"): + service = services[f"{authority}-relay"] + self.assertEqual( + service["command"], + ["serve", "--runtime", f"/etc/relay/{authority}/runtime.yaml"], + ) + volumes = set(service["volumes"]) + self.assertIn( + f"{authority}-relay-runtime:/etc/relay/{authority}:ro", + volumes, + ) + self.assertIn( + f"{authority}-relay-source:/var/lib/relay/source:ro", + volumes, + ) + self.assertIn( + f"{authority}-relay-audit:/var/lib/relay/audit", + volumes, + ) + + def test_hosted_programme_services_receive_only_their_application_secrets(self) -> None: + compose_path = SCRIPT.parents[1] / "compose.coolify.yaml" + compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) + services = compose["services"] + secret_name = "solmara-demo-client-key" + secret_target = "solmara-evidence-client.jwk" + federator_token = "${CHILD_BENEFIT_FEDERATOR_TOKEN:?required}" + + self.assertEqual( + compose["secrets"][secret_name], + {"environment": "SOLMARA_DEMO_CLIENT_PRIVATE_JWK"}, + ) + for service_name in ("scenario-runner", "child-benefit-federator", "portal"): + service = services[service_name] + self.assertEqual( + service["secrets"], + [{"source": secret_name, "target": secret_target}], + service_name, + ) + self.assertEqual( + service["environment"]["SOLMARA_EVIDENCE_CLIENT_ID"], + "solmara-demo", + service_name, + ) + self.assertEqual( + service["environment"]["SOLMARA_EVIDENCE_CLIENT_KEY"], + f"/run/secrets/{secret_target}", + service_name, + ) + self.assertEqual( + service["environment"]["CHILD_BENEFIT_FEDERATOR_TOKEN"], + federator_token, + service_name, + ) + + self.assertEqual( + services["scenario-runner"]["environment"]["CHILD_BENEFIT_FEDERATOR_URL"], + "https://child-benefit.solmara.registrystack.org", + ) + self.assertEqual( + services["portal"]["environment"]["CHILD_BENEFIT_FEDERATOR_URL"], + "https://child-benefit.solmara.registrystack.org", + ) + self.assertEqual( + services["child-benefit-federator"]["environment"]["CHILD_BENEFIT_FEDERATOR_HOST"], + "0.0.0.0", + ) + self.assertEqual( + services["child-benefit-federator"]["environment"]["CHILD_BENEFIT_FEDERATOR_PORT"], + "8080", + ) + + for service_name in ("home", "static-metadata"): + service = services[service_name] + self.assertNotIn("secrets", service, service_name) + environment = service.get("environment", {}) + self.assertNotIn("SOLMARA_EVIDENCE_CLIENT_KEY", environment, service_name) + self.assertNotIn("CHILD_BENEFIT_FEDERATOR_TOKEN", environment, service_name) + + def test_hosted_esignet_overlay_wires_the_main_portal_with_a_separate_key(self) -> None: + overlay_path = SCRIPT.parents[1] / "compose.coolify.esignet.yaml" + overlay = yaml.safe_load(overlay_path.read_text(encoding="utf-8")) + services = overlay["services"] + portal = services["portal"]["environment"] + expected = { + "PORTAL_AUTH_PROVIDER": "esignet", + "PORTAL_SECURE_COOKIES": "true", + "PORTAL_ESIGNET_CLIENT_ID": "${PORTAL_ESIGNET_CLIENT_ID:-solmara-portal}", + "PORTAL_ESIGNET_CLIENT_KEY_ID": "${PORTAL_ESIGNET_CLIENT_KEY_ID:-solmara-portal-key-1}", + "PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64": "${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?required}", + "PORTAL_ESIGNET_ISSUER": "https://esignet.solmara.registrystack.org", + "PORTAL_ESIGNET_AUTHORIZATION_ENDPOINT": "https://esignet-ui.solmara.registrystack.org/authorize", + "PORTAL_ESIGNET_TOKEN_ENDPOINT": "https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token", + "PORTAL_ESIGNET_CLIENT_ASSERTION_AUDIENCE": "https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token", + "PORTAL_ESIGNET_USERINFO_ENDPOINT": "https://esignet.solmara.registrystack.org/v1/esignet/oidc/userinfo", + "PORTAL_ESIGNET_REDIRECT_URI": "https://portal.solmara.registrystack.org/auth/callback", + "PORTAL_ESIGNET_SCOPE": "openid profile", + "PORTAL_ESIGNET_SUBJECT_CLAIM": "sub", + } + self.assertEqual(portal, expected) + self.assertNotIn("name", overlay) + + private_key_value = "${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?required}" + self.assertEqual( + { + service_name + for service_name, service in services.items() + if service.get("environment", {}).get("PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64") + == private_key_value + }, + {"portal"}, + ) + self.assertEqual( + services["esignet-seed"]["environment"]["ESIGNET_CLIENT_PRIVATE_KEY_B64"], + private_key_value, + ) + self.assertNotIn("SOLMARA_EVIDENCE_CLIENT_KEY", portal) + self.assertNotIn("NIA_ESIGNET_CLIENT_PRIVATE_JWK", portal) + + def test_bruno_workspace_covers_only_the_eight_governed_v2_lookups(self) -> None: + relay_requests = SCRIPT.parents[1] / "requests/registry-lab/50 - Relay V2" + rendered = "\n".join( + path.read_text(encoding="utf-8") + for path in sorted(relay_requests.glob("*.bru")) + ) + expected = { + "/v2/resources/civil-person/lookups/death-by-uin", + "/v2/resources/civil-person/lookups/citizen-link-by-uin", + "/v2/resources/population-person/lookups/esignet-userinfo", + "/v2/resources/beneficiary-enrolment/lookups/by-uin", + "/v2/resources/pension-payment/lookups/by-pensioner-uin", + "/v2/resources/survivor-case/lookups/by-spouse-uin", + "/v2/resources/farmer/lookups/voucher-by-farmer-id", + "/v2/resources/livestock-herd/lookups/movement-by-farmer-id", + } + self.assertEqual(len(list(relay_requests.glob("*.bru"))), len(expected)) + for path in expected: + self.assertIn(path, rendered) + self.assertNotIn("/records", rendered) + self.assertNotIn("/search", rendered) + self.assertNotIn("Data-Purpose", rendered) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_signer_public_keys.py b/scripts/test_signer_public_keys.py new file mode 100644 index 0000000..ec2645b --- /dev/null +++ b/scripts/test_signer_public_keys.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("check-signer-public-keys.py") +SPEC = importlib.util.spec_from_file_location("signer_public_keys", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class SignerPublicKeyTests(unittest.TestCase): + def test_current_generated_signers_match(self) -> None: + self.assertEqual(MODULE.failures(), []) + + def test_mismatch_fails_closed_without_private_value_output(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for provider in MODULE.PROVIDERS: + private = {"alg": "ES256", "crv": "P-256", "kid": provider, "kty": "EC", "x": "x", "y": "y", "d": "private"} + private_path = root / "config/evidence/local/cells" / provider / "secrets/signing.jwk" + private_path.parent.mkdir(parents=True) + private_path.write_text(json.dumps(private), encoding="utf-8") + public_root = root / "runtime/evidence-cells/mint/public-keys" if provider == "mint" else root / "runtime/evidence-cells/cells" / provider / "bundle/public-keys" + public_root.mkdir(parents=True) + public = {name: private[name] for name in MODULE.PUBLIC_MEMBERS} + if provider == "cra": + public["x"] = "different" + (public_root / f"{provider}.jwk.json").write_text(json.dumps(public), encoding="utf-8") + found = MODULE.failures(root) + self.assertEqual(found, ["cra: generated public key does not match operator signer"]) + self.assertNotIn("private", " ".join(found)) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_smoke_child_benefit_application.py b/scripts/test_smoke_child_benefit_application.py deleted file mode 100644 index 7001f48..0000000 --- a/scripts/test_smoke_child_benefit_application.py +++ /dev/null @@ -1,160 +0,0 @@ -from __future__ import annotations - -import importlib.util -import unittest -from pathlib import Path -from types import SimpleNamespace -from typing import Any - - -SCRIPT = Path(__file__).with_name("smoke-child-benefit-application.py") -SPEC = importlib.util.spec_from_file_location("smoke_child_benefit_application", SCRIPT) -assert SPEC is not None and SPEC.loader is not None -smoke = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(smoke) - - -class ChildBenefitApplicationSmokeTests(unittest.TestCase): - def test_accepts_source_owned_application_evidence(self) -> None: - claims = list(smoke.EXPECTED_CLAIM_OWNERS) - - failures = smoke.validated_evidence_failures( - 200, - valid_evidence(claims), - smoke.POSITIVE_SUBJECT, - claims, - ) - - self.assertEqual([], failures) - - def test_rejects_obsolete_federation_and_raw_source_fields(self) -> None: - claims = ["birth-is-registered"] - body = valid_evidence(claims) - body["federation_trace"] = [] - body["results"][0]["source_record"] = {"birth_brn": "protected"} - - failures = smoke.validated_evidence_failures( - 200, - body, - smoke.POSITIVE_SUBJECT, - claims, - ) - - self.assertTrue(any("obsolete federation_trace" in failure for failure in failures)) - self.assertTrue(any("raw source" in failure for failure in failures)) - - def test_rejects_wrong_owner_duplicate_result_and_subject_echo(self) -> None: - claims = ["birth-is-registered"] - body = valid_evidence(claims) - body["results"][0]["notary_service_id"] = "child-benefit-federator" - body["results"].append(dict(body["results"][0])) - body["debug_subject"] = smoke.POSITIVE_SUBJECT - - failures = smoke.validated_evidence_failures( - 200, - body, - smoke.POSITIVE_SUBJECT, - claims, - ) - - self.assertTrue(any("exactly the requested predicates" in failure for failure in failures)) - self.assertTrue(any("authority Notary" in failure for failure in failures)) - self.assertTrue(any("raw subject identifier" in failure for failure in failures)) - - def test_application_request_uses_ordinary_json(self) -> None: - claims = ["birth-is-registered"] - captured: dict[str, Any] = {} - original_http_json = smoke.http_json - - def fake_http_json( - method: str, - url: str, - headers: dict[str, str], - body: Any, - ) -> Any: - captured.update(method=method, url=url, headers=headers, body=body) - return SimpleNamespace(status=200, body=valid_evidence(claims)) - - smoke.http_json = fake_http_json - try: - failures = smoke.application_evidence_failures( - "http://application.test", - "test-token", - smoke.POSITIVE_SUBJECT, - claims, - ) - finally: - smoke.http_json = original_http_json - - self.assertEqual([], failures) - self.assertEqual("application/json", captured["headers"]["Accept"]) - self.assertEqual("application/json", captured["body"]["format"]) - self.assertEqual(claims, captured["body"]["claims"]) - self.assertEqual( - {"as_of_date": smoke.CHILD_BENEFIT_AS_OF_DATE}, - captured["body"]["variables"], - ) - - def test_raw_household_denial_requires_stable_purpose_problem(self) -> None: - captured: dict[str, Any] = {} - original_http_json = smoke.http_json - - def fake_http_json( - method: str, - url: str, - headers: dict[str, str], - body: Any, - ) -> Any: - captured.update(method=method, url=url, headers=headers, body=body) - return SimpleNamespace( - status=403, - headers={"content-type": "application/problem+json"}, - body={"code": "pdp.purpose_not_permitted"}, - ) - - smoke.http_json = fake_http_json - try: - failures = smoke.raw_household_denial_failures( - "http://application.test", - "test-token", - smoke.POSITIVE_SUBJECT, - ) - finally: - smoke.http_json = original_http_json - - self.assertEqual([], failures) - self.assertEqual(["household-poverty-score"], captured["body"]["claims"]) - self.assertEqual("value", captured["body"]["disclosure"]) - self.assertEqual("application/json", captured["headers"]["Accept"]) - - -def valid_evidence(claims: list[str]) -> dict[str, Any]: - services = {smoke.EXPECTED_CLAIM_OWNERS[claim_id] for claim_id in claims} - return { - "schema_version": "solmara-child-benefit-evidence/v1", - "orchestration": { - "service_id": "child-benefit-federator", - "decision": "not_composed", - }, - "target": {"type": "Person", "identifier_schemes": ["solmara_uin"]}, - "results": [ - { - "claim_id": claim_id, - "satisfied": True, - "disclosure": "predicate", - "notary_service_id": smoke.EXPECTED_CLAIM_OWNERS[claim_id], - } - for claim_id in claims - ], - "source_trace": [ - { - "service_id": service_id, - "response_summary": {"status": 200}, - } - for service_id in sorted(services) - ], - } - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_smoke_esignet.py b/scripts/test_smoke_esignet.py index b64dc3d..b34bb5b 100644 --- a/scripts/test_smoke_esignet.py +++ b/scripts/test_smoke_esignet.py @@ -12,6 +12,8 @@ from typing import Any from unittest import mock +import yaml + ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "scripts" / "smoke-esignet.py" @@ -81,6 +83,78 @@ def url(self) -> str: class SmokeEsignetTests(unittest.TestCase): + def test_local_and_hosted_profiles_use_the_v020_authenticator_contract(self) -> None: + local = yaml.safe_load((ROOT / "compose.esignet.yaml").read_text()) + hosted = yaml.safe_load((ROOT / "compose.coolify.esignet.yaml").read_text()) + local_env = local["services"]["esignet"]["environment"] + hosted_env = hosted["services"]["esignet"]["environment"] + self.assertEqual( + local["services"]["portal"]["environment"]["PORTAL_ESIGNET_SUBJECT_CLAIM"], + "sub", + ) + + for environment in (local_env, hosted_env): + self.assertIn("REGISTRY_MINT_PRIVATE_JWK", environment) + self.assertNotIn("REGISTRY_MINT_CLIENT_PRIVATE_JWK", environment) + self.assertEqual( + environment["REGISTRY_MINT_TOKEN_ENDPOINT"], + "https://mint.solmara.registrystack.org/token", + ) + self.assertEqual( + environment["MOSIP_ESIGNET_DATABASE_URL"], + "jdbc:postgresql://esignet-database:5432/mosip_esignet?currentSchema=esignet", + ) + self.assertEqual( + environment["MOSIP_ESIGNET_INTEGRATION_AUTHENTICATOR"], + "RelayAuthenticationService", + ) + self.assertIn("esignet-relay-authenticator.jar", environment["plugin_name_env"]) + self.assertEqual(environment["REGISTRY_ESIGNET_ACCOUNT_CHECK_CLAIMS"], "individualId") + scope_claims = environment["MOSIP_ESIGNET_OPENID_SCOPE_CLAIMS"] + self.assertNotIn("'name'", scope_claims) + for claim in ( + "'given_name'", + "'family_name'", + "'gender'", + "'birthdate'", + "'individual_id'", + ): + self.assertIn(claim, scope_claims) + self.assertEqual( + environment["REGISTRY_RELAY_DEFAULT_CLAIMS"], + "individualId,givenName,familyName,birthdate,gender", + ) + claim_map = json.loads(environment["SPRING_APPLICATION_JSON"]) + self.assertEqual( + claim_map["registry"]["esignet"]["claim-map"], + { + "sub": "$$psut", + "individual_id": "individualId", + "given_name": "givenName", + "family_name": "familyName", + "birthdate": "birthdate", + "gender": "gender", + }, + ) + + mint = yaml.safe_load((ROOT / "evidence" / "mint.yaml").read_text()) + self.assertEqual(mint["clientAssertion"]["algorithms"], ["ES256", "RS256"]) + + def test_browser_smoke_has_only_fixed_sanitized_output(self) -> None: + source = (ROOT / "scripts" / "smoke-esignet-login.mjs").read_text() + self.assertEqual(source.count("console.log("), 1) + self.assertIn("console.log('smoke-esignet-login: PASS')", source) + self.assertIn("getByRole('checkbox', { name: 'voluntary_claims' })", source) + self.assertIn("allClaims.check({ force: true })", source) + self.assertIn("url.pathname.endsWith('/consent')", source) + self.assertIn("timeout: 60_000", source) + self.assertNotIn("error.errorCode", source) + self.assertNotIn("page.title()", source) + + seed_source = (ROOT / "scripts" / "seed-esignet.py").read_text() + self.assertNotIn("Local static OTP", seed_source) + self.assertNotIn("Demo subject available", seed_source) + def test_load_env_file_handles_quoted_values(self) -> None: with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / ".env" @@ -93,16 +167,18 @@ def test_load_env_file_handles_quoted_values(self) -> None: ) def test_discovery_requires_root_and_mosip_paths_to_share_issuer(self) -> None: - issuer_doc = {"issuer": "https://esignet.solmara.registrystack.org"} - routes = { - ("GET", "/v1/esignet/oidc/.well-known/openid-configuration"): ( - 200, - issuer_doc, - ), - ("GET", "/.well-known/openid-configuration"): (200, issuer_doc), - ("GET", "/.well-known/oauth-authorization-server"): (200, issuer_doc), - } - with StubServer(routes) as server: + with StubServer({}) as server: + issuer_doc = { + "issuer": server.url, + "token_endpoint": f"{server.url}/v1/esignet/oauth/v2/token", + } + server.routes.update( + { + ("GET", "/v1/esignet/oidc/.well-known/openid-configuration"): (200, issuer_doc), + ("GET", "/.well-known/openid-configuration"): (200, issuer_doc), + ("GET", "/.well-known/oauth-authorization-server"): (200, issuer_doc), + } + ) targets = smoke_esignet.SmokeTargets(server.url, server.url) smoke_esignet.check_esignet_discovery(targets, timeout=2) diff --git a/scripts/test_smoke_hosted.py b/scripts/test_smoke_hosted.py deleted file mode 100644 index d9866a7..0000000 --- a/scripts/test_smoke_hosted.py +++ /dev/null @@ -1,287 +0,0 @@ -from __future__ import annotations - -import importlib.util -import json -import sys -import threading -import unittest -from http import HTTPStatus -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -SCRIPT = ROOT / "scripts" / "smoke-hosted.py" -SPEC = importlib.util.spec_from_file_location("smoke_hosted", SCRIPT) -smoke_hosted = importlib.util.module_from_spec(SPEC) -assert SPEC and SPEC.loader -sys.modules["smoke_hosted"] = smoke_hosted -SPEC.loader.exec_module(smoke_hosted) - - -class StubServer: - def __init__(self, routes: dict[tuple[str, str], Any]) -> None: - self.routes = routes - self.server: ThreadingHTTPServer | None = None - self.thread: threading.Thread | None = None - - def __enter__(self) -> "StubServer": - outer = self - - class Handler(BaseHTTPRequestHandler): - def do_GET(self) -> None: - self._handle("GET") - - def do_POST(self) -> None: - self._handle("POST") - - def _handle(self, method: str) -> None: - route = outer.routes.get((method, self.path)) - if route is None: - self.send_error(HTTPStatus.NOT_FOUND) - return - status, payload, *header_items = route - data = json.dumps(payload).encode("utf-8") - self.send_response(status) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(data))) - for key, value in dict(header_items[0] if header_items else {}).items(): - self.send_header(key, value) - self.end_headers() - self.wfile.write(data) - - def log_message(self, fmt: str, *args: object) -> None: - return - - self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) - self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) - self.thread.start() - return self - - def __exit__(self, *args: object) -> None: - assert self.server is not None - self.server.shutdown() - self.server.server_close() - assert self.thread is not None - self.thread.join(timeout=5) - - @property - def url(self) -> str: - assert self.server is not None - host, port = self.server.server_address - return f"http://{host}:{port}" - - -def hosted_routes() -> dict[tuple[str, str], Any]: - return { - ("GET", "/api/scenarios"): ( - 200, - { - "default_scenario_id": "birth-to-child-benefit", - "scenarios": [ - {"id": "birth-to-child-benefit"}, - {"id": "death-to-pension-survivor"}, - {"id": "farmer-climate-smart-voucher"}, - {"id": "citizen-self-service"}, - ], - }, - ), - ("POST", "/api/scenarios/birth-to-child-benefit/steps/positive/run"): ( - 200, - { - "result": { - "response_source": { - "status": 200, - "body": { - "orchestration": { - "service_id": "child-benefit-federator", - "decision": "not_composed", - }, - "results": [ - { - "claim_id": "birth-is-registered", - "satisfied": True, - "notary_service_id": "cra-notary", - }, - { - "claim_id": "population-record-active", - "satisfied": True, - "notary_service_id": "nia-notary", - }, - { - "claim_id": "child-age-under-5", - "satisfied": True, - "notary_service_id": "cra-notary", - }, - { - "claim_id": "household-below-poverty-threshold", - "satisfied": True, - "notary_service_id": "sro-notary", - }, - { - "claim_id": "not-already-enrolled", - "satisfied": True, - "notary_service_id": "programme-notary", - }, - ], - "source_trace": [ - {"service_id": "cra-notary"}, - {"service_id": "nia-notary"}, - {"service_id": "sro-notary"}, - {"service_id": "programme-notary"}, - ], - }, - }, - } - }, - ), - ("POST", "/api/scenarios/birth-to-child-benefit/steps/purpose-denial/run"): ( - 200, - { - "result": { - "response_source": { - "status": 403, - "body": {"code": "pdp.purpose_not_permitted"}, - } - } - }, - ), - } - - -class HostedSmokeTests(unittest.TestCase): - def test_default_targets_use_public_solmara_domains(self) -> None: - targets = smoke_hosted.default_targets("https://solmara.registrystack.org/") - self.assertEqual(targets.home_url, "https://solmara.registrystack.org") - self.assertEqual(targets.portal_url, "https://portal.solmara.registrystack.org") - self.assertEqual( - targets.esignet_url, "https://esignet.solmara.registrystack.org" - ) - self.assertEqual( - targets.esignet_ui_url, "https://esignet-ui.solmara.registrystack.org" - ) - self.assertEqual(targets.wallet_url, "https://wallet.solmara.registrystack.org") - self.assertIn( - "https://cra-relay.solmara.registrystack.org", - {relay.base_url for relay in targets.relays}, - ) - authority_urls = { - "https://cra-notary.solmara.registrystack.org", - "https://nia-notary.solmara.registrystack.org", - "https://sro-notary.solmara.registrystack.org", - "https://programme-notary.solmara.registrystack.org", - "https://sipf-notary.solmara.registrystack.org", - "https://nagdi-notary.solmara.registrystack.org", - } - self.assertEqual( - authority_urls, - {notary.base_url for notary in targets.notaries}, - ) - self.assertEqual( - {"https://child-benefit-federator.solmara.registrystack.org"}, - {application.base_url for application in targets.applications}, - ) - - def test_hosted_env_overrides_public_service_urls_without_tokens(self) -> None: - targets = smoke_hosted.default_targets("solmara.registrystack.org") - env = smoke_hosted.hosted_env( - {"CHILD_BENEFIT_FEDERATOR_TOKEN": "keep-local"}, targets - ) - self.assertEqual( - env["CHILD_BENEFIT_FEDERATOR_URL"], - "https://child-benefit-federator.solmara.registrystack.org", - ) - self.assertEqual( - env["CRA_NOTARY_URL"], "https://cra-notary.solmara.registrystack.org" - ) - self.assertEqual( - env["NIA_NOTARY_URL"], "https://nia-notary.solmara.registrystack.org" - ) - self.assertEqual( - env["SRO_NOTARY_URL"], "https://sro-notary.solmara.registrystack.org" - ) - self.assertEqual( - env["PROGRAMME_NOTARY_URL"], - "https://programme-notary.solmara.registrystack.org", - ) - self.assertEqual( - env["SIPF_NOTARY_URL"], "https://sipf-notary.solmara.registrystack.org" - ) - self.assertEqual( - env["NAGDI_NOTARY_URL"], - "https://nagdi-notary.solmara.registrystack.org", - ) - self.assertEqual( - env["SOLMARA_CRA_RELAY_URL"], "https://cra-relay.solmara.registrystack.org" - ) - self.assertEqual( - env["SOLMARA_PORTAL_URL"], "https://portal.solmara.registrystack.org" - ) - self.assertEqual( - env["SOLMARA_ESIGNET_PUBLIC_BASE_URL"], - "https://esignet.solmara.registrystack.org", - ) - self.assertEqual( - env["SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL"], - "https://esignet-ui.solmara.registrystack.org", - ) - self.assertEqual( - env["SOLMARA_WALLET_URL"], "https://wallet.solmara.registrystack.org" - ) - self.assertEqual(env["SOLMARA_PORTAL_EXPECT_AUTH_REQUIRED"], "1") - self.assertEqual(env["CHILD_BENEFIT_FEDERATOR_TOKEN"], "keep-local") - - def test_normalize_argv_accepts_just_separator(self) -> None: - self.assertEqual( - smoke_hosted.normalize_argv(["--", "--browser"]), ["--browser"] - ) - self.assertEqual(smoke_hosted.normalize_argv(["--browser"]), ["--browser"]) - - def test_esignet_smoke_uses_only_supported_discovery_arguments(self) -> None: - targets = smoke_hosted.default_targets("solmara.registrystack.org") - - command = smoke_hosted.esignet_smoke_command(targets) - - self.assertNotIn("--relay-url", command) - self.assertEqual( - command[-4:], - [ - "--esignet-url", - "https://esignet.solmara.registrystack.org", - "--esignet-ui-url", - "https://esignet-ui.solmara.registrystack.org", - ], - ) - - def test_home_demo_accepts_expected_scenario_flow(self) -> None: - with StubServer(hosted_routes()) as server: - smoke_hosted.check_home_demo(server.url, timeout=2) - - def test_home_demo_rejects_composed_child_benefit_decision(self) -> None: - routes = hosted_routes() - positive = routes[ - ("POST", "/api/scenarios/birth-to-child-benefit/steps/positive/run") - ][1] - positive["result"]["response_source"]["body"]["orchestration"]["decision"] = ( - "eligible" - ) - with StubServer(routes) as server: - with self.assertRaises(smoke_hosted.SmokeFailure): - smoke_hosted.check_home_demo(server.url, timeout=2) - - def test_home_demo_requires_stable_denial_code(self) -> None: - routes = hosted_routes() - routes[ - ("POST", "/api/scenarios/birth-to-child-benefit/steps/purpose-denial/run") - ] = ( - 200, - {"result": {"response_source": {"status": 403, "body": {}}}}, - ) - with StubServer(routes) as server: - with self.assertRaises(smoke_hosted.SmokeFailure): - smoke_hosted.check_home_demo(server.url, timeout=2) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_smoke_hosted_provisioner_image.py b/scripts/test_smoke_hosted_provisioner_image.py new file mode 100644 index 0000000..bebb3c3 --- /dev/null +++ b/scripts/test_smoke_hosted_provisioner_image.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import importlib.util +import os +import tempfile +import unittest +from contextlib import redirect_stderr, redirect_stdout +from io import StringIO +from pathlib import Path +from unittest import mock + +SCRIPT = Path(__file__).with_name("smoke-hosted-provisioner-image.py") +SPEC = importlib.util.spec_from_file_location("smoke_hosted_provisioner_image", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class HostedProvisionerImageSmokeTests(unittest.TestCase): + def test_success_is_sanitized(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + output, error = StringIO(), StringIO() + with ( + mock.patch.object( + os.sys, + "argv", + [ + SCRIPT.name, + "--image", + "image@sha256:" + "a" * 64, + "--state-root", + temporary, + ], + ), + mock.patch.object(MODULE, "smoke") as smoke, + redirect_stdout(output), + redirect_stderr(error), + ): + self.assertEqual(MODULE.main(), 0) + smoke.assert_called_once() + self.assertEqual( + output.getvalue().strip(), "hosted provisioner image smoke passed" + ) + self.assertEqual(error.getvalue(), "") + + def test_failure_redacts_dependency_details(self) -> None: + canary = "PRIVATE-HOSTED-SMOKE-CANARY" + output, error = StringIO(), StringIO() + with ( + mock.patch.object(os.sys, "argv", [SCRIPT.name, "--image", "invalid"]), + mock.patch.object(MODULE, "smoke", side_effect=RuntimeError(canary)), + redirect_stdout(output), + redirect_stderr(error), + ): + self.assertEqual(MODULE.main(), 1) + self.assertEqual(output.getvalue(), "") + self.assertEqual(error.getvalue().strip(), MODULE.GENERIC_ERROR) + self.assertNotIn(canary, error.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_smoke_nia_attribute_release.py b/scripts/test_smoke_nia_attribute_release.py deleted file mode 100644 index 74804a8..0000000 --- a/scripts/test_smoke_nia_attribute_release.py +++ /dev/null @@ -1,109 +0,0 @@ -from __future__ import annotations - -import importlib.util -import io -import json -import os -import unittest -from contextlib import redirect_stderr -from pathlib import Path -from unittest import mock - - -ROOT = Path(__file__).resolve().parents[1] -SPEC = importlib.util.spec_from_file_location( - "smoke_nia_attribute_release", - ROOT / "scripts" / "smoke-nia-attribute-release.py", -) -assert SPEC and SPEC.loader -MODULE = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(MODULE) - - -class FakeResponse: - def __init__(self, body: dict[str, object]) -> None: - self.status = 200 - self.body = body - - def __enter__(self) -> FakeResponse: - return self - - def __exit__(self, *_args: object) -> None: - return None - - def read(self) -> bytes: - return json.dumps(self.body).encode("utf-8") - - -class NiaAttributeReleaseSmokeTests(unittest.TestCase): - def test_live_request_uses_the_minimized_profile_contract(self) -> None: - response = FakeResponse( - { - "profile_id": "solmara-nia-userinfo", - "profile_version": "v1", - "claims": { - "individual_id": MODULE.SUBJECT, - "name": "Elena Dela Cruz", - }, - } - ) - with ( - mock.patch.dict( - os.environ, - {"NIA_ESIGNET_RELAY_TOKEN": "runtime-token"}, - clear=True, - ), - mock.patch.object( - MODULE.urllib.request, - "urlopen", - return_value=response, - ) as urlopen, - ): - self.assertEqual(MODULE.main(), 0) - - request = urlopen.call_args.args[0] - self.assertEqual(request.method, "POST") - self.assertEqual(request.get_header("Authorization"), "Bearer runtime-token") - self.assertEqual(request.get_header("Data-purpose"), MODULE.PURPOSE) - self.assertEqual( - json.loads(request.data), - { - "subject": { - "id_type": "national_id", - "value": MODULE.SUBJECT, - } - }, - ) - - def test_source_metadata_is_rejected(self) -> None: - stderr = io.StringIO() - with ( - mock.patch.dict( - os.environ, - {"NIA_ESIGNET_RELAY_TOKEN": "runtime-token"}, - clear=True, - ), - mock.patch.object( - MODULE.urllib.request, - "urlopen", - return_value=FakeResponse( - { - "profile_id": "solmara-nia-userinfo", - "profile_version": "v1", - "claims": { - "individual_id": MODULE.SUBJECT, - "name": "Elena Dela Cruz", - }, - "source": {"dataset": "population"}, - } - ), - ), - redirect_stderr(stderr), - ): - self.assertEqual(MODULE.main(), 1) - - self.assertIn("response disclosed source metadata", stderr.getvalue()) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/test_smoke_programme_acceptance.py b/scripts/test_smoke_programme_acceptance.py new file mode 100644 index 0000000..a4950ee --- /dev/null +++ b/scripts/test_smoke_programme_acceptance.py @@ -0,0 +1,306 @@ +from __future__ import annotations + +import importlib.util +import io +import json +import re +import sys +import threading +import unittest +from contextlib import redirect_stderr, redirect_stdout +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "smoke-programme-acceptance.py" +SPEC = importlib.util.spec_from_file_location("smoke_programme_acceptance", SCRIPT) +smoke = importlib.util.module_from_spec(SPEC) +assert SPEC and SPEC.loader +sys.modules["smoke_programme_acceptance"] = smoke +SPEC.loader.exec_module(smoke) + + +class StubServer: + def __init__(self, routes: dict[tuple[str, str], tuple[int, Any]]) -> None: + self.routes = routes + self.server: ThreadingHTTPServer | None = None + self.thread: threading.Thread | None = None + + def __enter__(self) -> "StubServer": + outer = self + + class Handler(BaseHTTPRequestHandler): + def do_GET(self) -> None: + self.handle_request("GET") + + def do_POST(self) -> None: + self.handle_request("POST") + + def handle_request(self, method: str) -> None: + status, payload = outer.routes.get( + (method, self.path), + (404, {"canary": "private-not-found-detail"}), + ) + encoded = json.dumps(payload).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(encoded))) + self.end_headers() + self.wfile.write(encoded) + + def log_message(self, fmt: str, *args: object) -> None: + return + + self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) + self.thread.start() + return self + + def __exit__(self, *args: object) -> None: + assert self.server is not None and self.thread is not None + self.server.shutdown() + self.server.server_close() + self.thread.join(timeout=5) + + @property + def url(self) -> str: + assert self.server is not None + host, port = self.server.server_address + return f"http://{host}:{port}" + + +def scenario_path(scenario: str, step: str) -> str: + return f"/v1/scenarios/{scenario}/steps/{step}/run" + + +def success_payload( + scenario: str, + claims: dict[str, str], + services: dict[str, tuple[str, str]], + *, + derived_decisions: dict[str, Any] | None = None, +) -> dict[str, Any]: + presentation_by_issuer = { + issuer: { + "authority": f"Public authority {index}", + "issuer": issuer, + "provider": f"provider-{index}", + "source": source, + } + for index, (issuer, source) in enumerate(services.values(), start=1) + } + result: dict[str, Any] = { + "response_source": {"status": 200, "code": "ok"}, + "results": [ + { + "claim_id": claim, + "concept_id": f"https://id.registrystack.org/solmara/concept/{claim}", + "satisfied": True, + "value": True, + "presentation": presentation_by_issuer[issuer], + } + for claim, issuer in claims.items() + ], + "presentations": [ + presentation_by_issuer[issuer] for issuer, _ in services.values() + ], + "source_trace": [ + { + "authority": f"Public authority {index}", + "service_id": service, + "issuer": issuer, + "provider": f"provider-{index}", + "source": source, + "status": 200, + } + for index, (service, (issuer, source)) in enumerate(services.items(), start=1) + ], + } + if derived_decisions is not None: + result["derived_decisions"] = derived_decisions + return { + "schema_version": "solmara-scenario-runner/v1", + "scenario_id": scenario, + "result": result, + } + + +def refusal_payload(scenario: str) -> dict[str, Any]: + return { + "schema_version": "solmara-scenario-runner/v1", + "scenario_id": scenario, + "result": { + "response_source": {"status": 403, "code": "request_refused"}, + "results": [], + "presentations": [], + "source_trace": [], + }, + } + + +def passing_routes() -> dict[tuple[str, str], tuple[int, Any]]: + child_services = { + "cra-evidence": (smoke.CRA, "immutable extract"), + "nia-evidence": (smoke.NIA, "immutable extract"), + "sro-evidence": (smoke.SRO, "immutable extract"), + "mosd-programme-evidence": (smoke.MOSD, "Relay lookup"), + } + pension_services = { + "cra-evidence": (smoke.CRA, "Relay lookup"), + "sipf-evidence": (smoke.SIPF, "Relay lookup"), + } + sipf_services = {"sipf-evidence": (smoke.SIPF, "Relay lookup")} + nagdi_services = {"nagdi-evidence": (smoke.NAGDI, "Relay lookup")} + return { + ("GET", "/health"): (200, {"service": "scenario-runner", "status": "ok"}), + ("POST", scenario_path("birth-to-child-benefit", "positive")): ( + 200, + success_payload("birth-to-child-benefit", smoke.CHILD_CLAIMS, child_services), + ), + ("POST", scenario_path("death-to-pension-survivor", "stop-payment")): ( + 200, + success_payload( + "death-to-pension-survivor", + smoke.PENSION_CLAIMS, + pension_services, + derived_decisions={ + "pension-payment-should-stop": True, + "owner": "pension-review-application", + }, + ), + ), + ("POST", scenario_path("death-to-pension-survivor", "survivor-benefit")): ( + 200, + success_payload( + "death-to-pension-survivor", + smoke.SURVIVOR_CLAIMS, + sipf_services, + ), + ), + ("POST", scenario_path("farmer-climate-smart-voucher", "positive")): ( + 200, + success_payload( + "farmer-climate-smart-voucher", + smoke.VOUCHER_CLAIMS, + nagdi_services, + ), + ), + ("POST", scenario_path("farmer-climate-smart-voucher", "movement-permit")): ( + 200, + success_payload( + "farmer-climate-smart-voucher", + smoke.LIVESTOCK_CLAIMS, + nagdi_services, + ), + ), + ("POST", scenario_path("birth-to-child-benefit", "purpose-denial")): ( + 200, + refusal_payload("birth-to-child-benefit"), + ), + ("POST", scenario_path("death-to-pension-survivor", "cause-of-death-denial")): ( + 200, + refusal_payload("death-to-pension-survivor"), + ), + ("POST", scenario_path("farmer-climate-smart-voucher", "purpose-denial")): ( + 200, + refusal_payload("farmer-climate-smart-voucher"), + ), + ("GET", "/v1/claims"): ( + 401, + { + "type": "https://id.registrystack.org/problems/solmara/authentication_required", + "title": "Authentication Required", + "status": 401, + "code": "authentication_required", + "detail": "A valid local application token is required.", + }, + ), + } + + +class ProgrammeAcceptanceSmokeTests(unittest.TestCase): + def run_main(self, routes: dict[tuple[str, str], tuple[int, Any]]) -> tuple[int, str, str]: + with StubServer(routes) as server: + previous_runner = smoke.RUNNER_URL + previous_federator = smoke.FEDERATOR_URL + previous_timeout = smoke.READY_TIMEOUT_SECONDS + smoke.RUNNER_URL = server.url + smoke.FEDERATOR_URL = server.url + smoke.READY_TIMEOUT_SECONDS = 0.2 + stdout = io.StringIO() + stderr = io.StringIO() + try: + with redirect_stdout(stdout), redirect_stderr(stderr): + result = smoke.main() + finally: + smoke.RUNNER_URL = previous_runner + smoke.FEDERATOR_URL = previous_federator + smoke.READY_TIMEOUT_SECONDS = previous_timeout + return result, stdout.getvalue(), stderr.getvalue() + + def test_all_programme_and_generic_refusal_checks_pass(self) -> None: + status, stdout, stderr = self.run_main(passing_routes()) + self.assertEqual(status, 0) + self.assertEqual(stderr, "") + lines = stdout.splitlines() + self.assertEqual(len(lines), 10) + self.assertTrue( + all(re.fullmatch(r"programme-acceptance: PASS [a-z0-9-]+", line) for line in lines) + ) + + def test_failure_output_never_echoes_response_values(self) -> None: + routes = passing_routes() + routes[("POST", scenario_path("birth-to-child-benefit", "positive"))] = ( + 500, + { + "selector": "PRIVATE-SELECTOR-CANARY", + "token": "PRIVATE-TOKEN-CANARY", + "source_row": "PRIVATE-ROW-CANARY", + "detail": "PRIVATE-DEPENDENCY-CANARY", + }, + ) + status, stdout, stderr = self.run_main(routes) + self.assertEqual(status, 1) + self.assertEqual(stderr, "") + self.assertIn( + "programme-acceptance: FAIL child-benefit-four-authorities-five-concepts", + stdout, + ) + self.assertNotIn("CANARY", stdout) + self.assertTrue( + all( + re.fullmatch(r"programme-acceptance: (?:PASS|FAIL) [a-z0-9-]+", line) + for line in stdout.splitlines() + ) + ) + + def test_child_acceptance_requires_four_distinct_expected_issuers(self) -> None: + routes = passing_routes() + payload = routes[("POST", scenario_path("birth-to-child-benefit", "positive"))][1] + payload["result"]["source_trace"][0]["issuer"] = smoke.NIA + status, stdout, _ = self.run_main(routes) + self.assertEqual(status, 1) + self.assertIn("FAIL child-benefit-four-authorities-five-concepts", stdout) + + def test_pension_acceptance_refuses_cause_of_death_disclosure(self) -> None: + routes = passing_routes() + payload = routes[("POST", scenario_path("death-to-pension-survivor", "survivor-benefit"))][1] + payload["result"]["cause_of_death"] = "PRIVATE-CAUSE-CANARY" + status, stdout, _ = self.run_main(routes) + self.assertEqual(status, 1) + self.assertIn("FAIL pension-survivor-minimized-assertion", stdout) + self.assertNotIn("PRIVATE-CAUSE-CANARY", stdout) + + def test_generic_refusal_must_not_release_results(self) -> None: + routes = passing_routes() + payload = routes[("POST", scenario_path("farmer-climate-smart-voucher", "purpose-denial"))][1] + payload["result"]["results"] = [{"claim_id": "private", "value": True}] + status, stdout, _ = self.run_main(routes) + self.assertEqual(status, 1) + self.assertIn("FAIL agriculture-wrong-purpose-generic-refusal", stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_smoke_relay_sources.py b/scripts/test_smoke_relay_sources.py deleted file mode 100644 index 4087928..0000000 --- a/scripts/test_smoke_relay_sources.py +++ /dev/null @@ -1,66 +0,0 @@ -from __future__ import annotations - -import importlib.util -import sys -import unittest -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] -SCRIPT = ROOT / "scripts" / "smoke-relay-sources.py" -SPEC = importlib.util.spec_from_file_location("smoke_relay_sources", SCRIPT) -smoke_relay_sources = importlib.util.module_from_spec(SPEC) -assert SPEC and SPEC.loader -sys.modules["smoke_relay_sources"] = smoke_relay_sources -SPEC.loader.exec_module(smoke_relay_sources) - - -class RelaySourceSmokeTests(unittest.TestCase): - def test_accepts_the_missing_credential_problem_for_an_anonymous_request( - self, - ) -> None: - response = ( - 401, - {"content-type": "application/problem+json"}, - {"code": "auth.missing_credential"}, - "", - ) - - self.assertIsNone( - smoke_relay_sources.validate_unauthenticated_denial(response) - ) - - def test_rejects_the_invalid_credential_problem_when_no_token_was_sent( - self, - ) -> None: - response = ( - 401, - {"content-type": "application/problem+json"}, - {"code": "auth.invalid_credentials"}, - "", - ) - - self.assertEqual( - smoke_relay_sources.validate_unauthenticated_denial(response), - "expected auth.missing_credential, got 'auth.invalid_credentials'", - ) - - def test_rejects_source_shaped_data_in_the_denial(self) -> None: - response = ( - 401, - {"content-type": "application/problem+json; charset=utf-8"}, - { - "code": "auth.missing_credential", - "source_record": {"uin": "must-not-be-returned"}, - }, - "", - ) - - self.assertEqual( - smoke_relay_sources.validate_unauthenticated_denial(response), - "unauthenticated denial included source-shaped data", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/versions.env b/versions.env index 992405d..2ac134e 100644 --- a/versions.env +++ b/versions.env @@ -1,13 +1,40 @@ -REGISTRY_RELAY_IMAGE=solmara-lab-registry-relay:source -REGISTRYCTL_VERSION=0.17.0 -REGISTRY_STACK_SOURCE_REF=main -REGISTRY_STACK_SOURCE_COMMIT=9a4365eb3693f485e538ca07e7f8655538ab2df6 -SOLMARA_RELAY_DEV_IMAGE=solmara-lab-registry-relay:source -SOLMARA_EVIDENCE_IMAGE=solmara-lab-registry-evidence:source -SOLMARA_MINT_IMAGE=solmara-lab-registry-mint:source -SOLMARA_RELAY_BUNDLE_SEQUENCE=2 -POSTGRES_IMAGE=postgres:16-alpine -STATIC_METADATA_IMAGE=python:3.12-alpine +# Registry Stack v0.20.0 is immutable and lacks the required Evidence prerequisite. +# The Solmara authority-cell reset therefore fixes forward to the published v0.20.1. +REGISTRY_STACK_REQUIRED_VERSION=0.20.1 +REGISTRY_STACK_RELEASE_RELAY_DIGEST=0727843669569ad8816d9863d695f9450c0507894d859bfacb1b5b98354c76c2 +REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL=https://github.com/registrystack/registry-stack/releases/download/v0.20.1/evidence-v0.20.1-linux-amd64 +REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256=114e249d2f76cd6534a619c339ace46a1f56824f807b65434b593a8d43799a8f +REGISTRY_STACK_RELEASE_MINT_ASSET_URL=https://github.com/registrystack/registry-stack/releases/download/v0.20.1/mint-v0.20.1-linux-amd64 +REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256=91908695241b8960532f1785f72fc3304b5b99776429f65c88c411da631581cc +REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=https://github.com/registrystack/registry-stack/releases/download/v0.20.1/relayctl-v0.20.1-linux-amd64 +REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256=8d02695c64a5f5720af889c74c5ea5c90555e987e61899e6541850e15d7f3773 + +# Exact release source identity and local thin-image tags. Every Registry Stack +# executable is fetched from the published release or the digest-pinned OCI. +REGISTRY_STACK_SOURCE_REF=v0.20.1 +REGISTRY_STACK_SOURCE_COMMIT=b9f6d12d7d4b62199558351087487550eedb2bdc +REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/relay@sha256:0727843669569ad8816d9863d695f9450c0507894d859bfacb1b5b98354c76c2 +REGISTRY_RELAYCTL_IMAGE=solmara-lab-relayctl:v0.20.1 +SOLMARA_EVIDENCE_IMAGE=solmara-lab-evidence:v0.20.1 +SOLMARA_MINT_IMAGE=solmara-lab-mint:v0.20.1 + +# Third-party runtime images are immutable. Refresh intentionally, never by tag. +PYTHON_STATIC_IMAGE=python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df +NODE_BUILD_IMAGE=node@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 +UV_BUILD_IMAGE=ghcr.io/astral-sh/uv@sha256:440fd6477af86a2f1b38080c539f1672cd22acb1b1a47e321dba5158ab08864d VOLUME_INIT_IMAGE=busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 EVIDENCE_GATEWAY_IMAGE=docker.io/library/caddy@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d +ESIGNET_REDIS_IMAGE=redis@sha256:b99ffd0554dc8d300230b9d1b9f2a129a6abf595bf8589883beb980ed1feae3d +ESIGNET_BASE_IMAGE=mosipid/esignet-with-plugins@sha256:47fffdb5a45198b29885a533841129877a7385a12bcb6020c0f6d4335477be39 +ESIGNET_UI_IMAGE=mosipid/oidc-ui@sha256:8a2a6839b4e22be6c967dabc6308190c165c54604a778c2d9b1aae8091db93e7 +ESIGNET_POSTGRES_IMAGE=postgres@sha256:9479eac93922431e8a512b016d7362ae264f4ba139f840891b51f02931e450db +ESIGNET_AUTHENTICATOR_VERSION=0.2.0 +ESIGNET_AUTHENTICATOR_RELEASE_URL=https://github.com/jeremi/esignet-relay-authenticator/releases/tag/v0.2.0 +ESIGNET_AUTHENTICATOR_JAR_URL=https://github.com/jeremi/esignet-relay-authenticator/releases/download/v0.2.0/esignet-relay-authenticator-0.2.0.jar +ESIGNET_AUTHENTICATOR_JAR_SHA256=2c36901acb990d3002b5ff7f691a3878f30ca1e21a350c12e54f71b8940da5a8 +ESIGNET_AUTHENTICATOR_CHECKSUM_URL=https://github.com/jeremi/esignet-relay-authenticator/releases/download/v0.2.0/esignet-relay-authenticator-0.2.0.jar.sha256 + +SOLMARA_SCENARIO_RUNNER_IMAGE=solmara-lab-scenario-runner:local +SOLMARA_CHILD_BENEFIT_FEDERATOR_IMAGE=solmara-lab-scenario-runner:local SOLMARA_PORTAL_IMAGE=solmara-lab-portal:local +SOLMARA_HOME_IMAGE=solmara-lab-home:local From 808d6ad5faafa8434667153d7be37ecb4c614f6c Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 12 Aug 2026 23:32:45 +0700 Subject: [PATCH 03/53] Fix pnpm setup in CI Signed-off-by: Jeremi Joslin --- .github/workflows/ci.yml | 2 -- .github/workflows/release-candidate.yml | 2 -- 2 files changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f15f2a..63c1fbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,8 +21,6 @@ jobs: - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4.4.0 with: node-version: "22" - cache: pnpm - cache-dependency-path: portal/pnpm-lock.yaml - name: Install just run: | sudo apt-get update diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index a825cf9..af8e4a3 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -69,8 +69,6 @@ jobs: - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4.4.0 with: node-version: "22" - cache: pnpm - cache-dependency-path: portal/pnpm-lock.yaml - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v3.11.1 - name: Install just run: | From f632a6775593ae72e164e24f4d5288b86a5f7182 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 12 Aug 2026 23:36:15 +0700 Subject: [PATCH 04/53] Stabilize signer replacement race test Signed-off-by: Jeremi Joslin --- scripts/test_hosted_transit_signer.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scripts/test_hosted_transit_signer.py b/scripts/test_hosted_transit_signer.py index 5bab83c..82e0785 100644 --- a/scripts/test_hosted_transit_signer.py +++ b/scripts/test_hosted_transit_signer.py @@ -56,9 +56,7 @@ def test_compose_secret_is_copied_to_owner_only_staging(self) -> None: def test_only_root_owned_sticky_writable_parent_is_confined(self) -> None: def directory(mode: int, uid: int = 0) -> os.stat_result: - return os.stat_result( - [stat.S_IFDIR | mode, 1, 0, 1, uid, 0, 0, 0, 0, 0] - ) + return os.stat_result([stat.S_IFDIR | mode, 1, 0, 1, uid, 0, 0, 0, 0, 0]) self.assertTrue(MODULE._directory_is_confined(directory(0o755))) self.assertTrue(MODULE._directory_is_confined(directory(0o1777))) @@ -252,6 +250,9 @@ def test_stale_socket_inode_replacement_is_refused_without_unlinking_replacement exact_socket.chmod(0o600) replacement = socket.socket(socket.AF_UNIX) + replacement_path = socket_directory / "replacement.sock" + replacement.bind(str(replacement_path)) + replacement_path.chmod(0o600) original = MODULE._socket_identity @@ -260,9 +261,7 @@ def replace_before_recheck(path: Path) -> tuple[int, int, int, int]: if replace_before_recheck.calls == 0: replace_before_recheck.calls += 1 return identity - path.unlink() - replacement.bind(str(path)) - path.chmod(0o600) + os.replace(replacement_path, path) return original(path) replace_before_recheck.calls = 0 From 1b5f934e1659e242be0ef1478b219e0c6c1704d7 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 12 Aug 2026 23:40:06 +0700 Subject: [PATCH 05/53] Report failing Relay verification stage Signed-off-by: Jeremi Joslin --- scripts/check-relay-projects.sh | 56 +++++++++++++++------------------ 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/scripts/check-relay-projects.sh b/scripts/check-relay-projects.sh index 06b8635..f13a91f 100755 --- a/scripts/check-relay-projects.sh +++ b/scripts/check-relay-projects.sh @@ -16,43 +16,37 @@ cleanup() { trap cleanup EXIT HUP INT TERM cd "$root" -for authority in cra nia mosd sipf nagdi; do - project="relays/$authority" - database="$root/output/sqlite/relay/$authority.sqlite" - generated="$temporary_root/$authority-generated" - package="$temporary_root/$authority-package" - docker run --rm \ - --platform linux/amd64 \ - --user "$(id -u):$(id -g)" \ - --volume "$root:/workspace" \ - --volume "$database:/var/lib/relay/source/$authority.sqlite:ro" \ - --workdir /workspace \ - "$REGISTRY_RELAYCTL_IMAGE" \ - --json check "$project" --production >/dev/null - docker run --rm \ - --platform linux/amd64 \ - --user "$(id -u):$(id -g)" \ - --volume "$root:/workspace" \ - --volume "$database:/var/lib/relay/source/$authority.sqlite:ro" \ - --workdir /workspace \ - "$REGISTRY_RELAYCTL_IMAGE" \ - --json generate "$project" --output "${generated#"$root/"}" >/dev/null - docker run --rm \ - --platform linux/amd64 \ - --user "$(id -u):$(id -g)" \ - --volume "$root:/workspace" \ - --volume "$database:/var/lib/relay/source/$authority.sqlite:ro" \ - --workdir /workspace \ - "$REGISTRY_RELAYCTL_IMAGE" \ - --json test "$project" >/dev/null - docker run --rm \ +run_relayctl() { + authority=$1 + database=$2 + stage=$3 + shift 3 + if ! docker run --rm \ --platform linux/amd64 \ --user "$(id -u):$(id -g)" \ --volume "$root:/workspace" \ --volume "$database:/var/lib/relay/source/$authority.sqlite:ro" \ --workdir /workspace \ "$REGISTRY_RELAYCTL_IMAGE" \ - --json package "$project" --output "${package#"$root/"}" >/dev/null + --json "$@" >/dev/null; then + printf 'relay-check: %s %s failed\n' "$authority" "$stage" >&2 + return 1 + fi +} + +for authority in cra nia mosd sipf nagdi; do + project="relays/$authority" + database="$root/output/sqlite/relay/$authority.sqlite" + generated="$temporary_root/$authority-generated" + package="$temporary_root/$authority-package" + run_relayctl "$authority" "$database" check \ + check "$project" --production + run_relayctl "$authority" "$database" generate \ + generate "$project" --output "${generated#"$root/"}" + run_relayctl "$authority" "$database" test \ + test "$project" + run_relayctl "$authority" "$database" package \ + package "$project" --output "${package#"$root/"}" done printf '%s\n' 'relay-check: five production Relay projects passed check, generate, test, and package' From 4ba7b5852694f66959f076891157c912192ef668 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 12 Aug 2026 23:41:17 +0700 Subject: [PATCH 06/53] Harden hosted authority provisioning Signed-off-by: Jeremi Joslin --- .../hosted-authority-provisioner/Dockerfile | 4 + scripts/hosted-runtime-assets.py | 2 +- scripts/provision-hosted-runtime.py | 169 ++++++++- scripts/test_hosted_runtime_assets.py | 52 ++- scripts/test_provision_hosted_runtime.py | 352 +++++++++++++++++- 5 files changed, 538 insertions(+), 41 deletions(-) diff --git a/docker/hosted-authority-provisioner/Dockerfile b/docker/hosted-authority-provisioner/Dockerfile index 7d5f77f..0d62d10 100644 --- a/docker/hosted-authority-provisioner/Dockerfile +++ b/docker/hosted-authority-provisioner/Dockerfile @@ -43,7 +43,11 @@ WORKDIR /app COPY pyproject.toml uv.lock ./ RUN uv export --frozen --no-dev --no-hashes -o /requirements.txt && \ pip install --no-cache-dir -r /requirements.txt && rm /requirements.txt +RUN install -d -o 65532 -g 65532 -m 0755 \ + /provisioned/runtime /provisioned/source /provisioned/extracts && \ + install -d -o 65532 -g 65532 -m 0700 /provisioned/secrets COPY --from=assets /opt/solmara-hosted-assets /opt/solmara-hosted-assets COPY scripts/hosted-runtime-assets.py /app/scripts/hosted-runtime-assets.py COPY scripts/provision-hosted-runtime.py /app/scripts/provision-hosted-runtime.py +USER 65532:65532 ENTRYPOINT ["python", "/app/scripts/provision-hosted-runtime.py"] diff --git a/scripts/hosted-runtime-assets.py b/scripts/hosted-runtime-assets.py index 7893f9f..fdead54 100755 --- a/scripts/hosted-runtime-assets.py +++ b/scripts/hosted-runtime-assets.py @@ -410,7 +410,7 @@ def build(root: Path, output: Path, relayctl: Path) -> None: staging = Path(temporary) / "assets" staging.mkdir() - published = publisher.publish_relay_sources(staging) + published = publisher.publish_relay_sources(Path(temporary) / "published") for authority in AUTHORITIES: project = Path(temporary) / "projects" / authority _copy_exact_tree( diff --git a/scripts/provision-hosted-runtime.py b/scripts/provision-hosted-runtime.py index 45f09a8..56128a2 100644 --- a/scripts/provision-hosted-runtime.py +++ b/scripts/provision-hosted-runtime.py @@ -10,11 +10,13 @@ import importlib.util import json import os +import re import shutil import sqlite3 import stat import sys import tempfile +from collections.abc import Callable from datetime import UTC, datetime from pathlib import Path import yaml @@ -81,6 +83,10 @@ "https://id.registrystack.org/solmara/purpose/esignet-identity-verification", ), } +ROLLBACK_RUNTIME = re.compile( + r"^runtime\.rollback-(?:cra-birth|nia-population|sro-poverty)-" + r"[0-9]{8}T[0-9]{6}(?:[0-9]{6})?Z\.yaml$" +) class ProvisionError(RuntimeError): @@ -150,6 +156,27 @@ def _hmac_secret(root: Path, name: str) -> bytes: return value +def _validate_secret_inventory(root: Path, expected: set[str]) -> None: + try: + if root.is_symlink() or not root.is_dir(): + raise ProvisionError("invalid secret inventory") + observed: set[str] = set() + with os.scandir(root) as entries: + for entry in entries: + metadata = entry.stat(follow_symlinks=False) + if not stat.S_ISREG(metadata.st_mode) or entry.name not in expected: + raise ProvisionError("invalid secret inventory") + observed.add(entry.name) + if observed != expected: + raise ProvisionError("invalid secret inventory") + for name in sorted(expected): + _read_secret(root, name) + except ProvisionError: + raise + except OSError: + raise ProvisionError("invalid secret inventory") from None + + def _b64decode(value: str) -> bytes: return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) @@ -268,29 +295,50 @@ def _tree_digest(root: Path) -> dict[str, tuple[str, int]]: for path in sorted(root.rglob("*")): if path.is_symlink(): raise ProvisionError("invalid existing output") - if path.is_file(): + if path.is_dir(): + result[path.relative_to(root).as_posix()] = ( + "directory", + stat.S_IMODE(path.stat().st_mode), + ) + elif path.is_file(): result[path.relative_to(root).as_posix()] = ( _digest(path), stat.S_IMODE(path.stat().st_mode), ) + else: + raise ProvisionError("invalid existing output") return result +def _check_install_tree( + staged: Path, + destination: Path, + *, + preserve: Callable[[str, tuple[str, int]], bool] | None = None, +) -> None: + if destination.is_symlink(): + raise ProvisionError("invalid existing output") + expected = _tree_digest(staged) + current = _tree_digest(destination) + active = { + relative: value + for relative, value in current.items() + if preserve is None or not preserve(relative, value) + } + if active and active != expected: + raise ProvisionError("existing output mismatch") + + def _install_tree( staged: Path, destination: Path, *, root_mode: int, owner: tuple[int, int] | None = None, + preserve: Callable[[str, tuple[str, int]], bool] | None = None, ) -> None: + _check_install_tree(staged, destination, preserve=preserve) expected = _tree_digest(staged) - current = _tree_digest(destination) - if current: - if any( - relative not in expected or expected[relative] != value - for relative, value in current.items() - ): - raise ProvisionError("existing output mismatch") destination.mkdir(parents=True, exist_ok=True) for source in sorted(staged.rglob("*")): relative = source.relative_to(staged) @@ -322,10 +370,24 @@ def _install_tree( for target in sorted(destination.rglob("*"), reverse=True): os.chown(target, uid, gid, follow_symlinks=False) os.chown(destination, uid, gid, follow_symlinks=False) - if _tree_digest(destination) != expected: + observed = { + relative: value + for relative, value in _tree_digest(destination).items() + if preserve is None or not preserve(relative, value) + } + if observed != expected: raise ProvisionError("output verification failed") +def _preserve_extract_rollback(relative: str, value: tuple[str, int]) -> bool: + return ( + "/" not in relative + and ROLLBACK_RUNTIME.fullmatch(relative) is not None + and value[0] != "directory" + and value[1] == 0o444 + ) + + def _copy_tree(source: Path, destination: Path) -> None: shutil.copytree(source, destination) @@ -361,8 +423,9 @@ def _stage_relay(assets: Path, authority: str, runtime: Path, source: Path) -> N _write( source / f"{authority}.sqlite", (relay / "source" / f"{authority}.sqlite").read_bytes(), - 0o644, + 0o444, ) + _freeze_tree(source) _freeze_tree(runtime) @@ -420,12 +483,19 @@ def _publication_time( return metadata.published_at except ProvisionError: raise - except (OSError, ValueError, TypeError, AttributeError): + except Exception: # noqa: BLE001 - dependency errors become one refusal. raise ProvisionError("invalid existing extract") from None try: + if runtime_file.is_symlink() or not runtime_file.is_file(): + raise ProvisionError("invalid existing extract") config = yaml.safe_load(runtime_file.read_text(encoding="utf-8")) - profile = next(iter(config["sourceExtracts"])) - extract_name = Path(config["sourceExtracts"][profile]["path"]).name + profile = DIRECT[cell][0] + if set(config["sourceExtracts"]) != {profile}: + raise ProvisionError("invalid existing extract") + bound_path = Path(config["sourceExtracts"][profile]["path"]) + if bound_path.parent != Path(f"/var/lib/registry-evidence/{cell}/extracts"): + raise ProvisionError("invalid existing extract") + extract_name = bound_path.name existing = extract_output / extract_name with sqlite3.connect(f"file:{existing}?mode=ro", uri=True) as connection: rows = connection.execute( @@ -447,7 +517,7 @@ def _publication_time( return published_at except ProvisionError: raise - except (OSError, sqlite3.Error, TypeError, KeyError, StopIteration, yaml.YAMLError): + except Exception: # noqa: BLE001 - dependency errors become one refusal. raise ProvisionError("invalid existing extract") from None @@ -480,7 +550,8 @@ def _replace_extract_binding( try: if runtime_file.is_symlink() or not runtime_file.is_file(): raise ProvisionError("invalid existing runtime") - config = yaml.safe_load(runtime_file.read_text(encoding="utf-8")) + original = runtime_file.read_bytes() + config = yaml.safe_load(original.decode("utf-8")) if config["listener"]["bindHost"] != EXPECTED_BIND_HOST[cell]: raise ProvisionError("invalid existing runtime") if set(config["sourceExtracts"]) != {DIRECT[cell][0]}: @@ -494,6 +565,24 @@ def _replace_extract_binding( binding["path"] = str(current.with_name(replacement_name)) rendered = yaml.safe_dump(config, sort_keys=False).encode() runtime_output.chmod(0o755) + rollback = runtime_output / f"runtime.rollback-{Path(previous_name).stem}.yaml" + if rollback.exists(): + if ( + rollback.is_symlink() + or not rollback.is_file() + or rollback.read_bytes() != original + or stat.S_IMODE(rollback.stat().st_mode) != 0o444 + ): + raise ProvisionError("invalid existing runtime") + else: + rollback_descriptor = os.open( + rollback, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o444 + ) + with os.fdopen(rollback_descriptor, "wb") as output: + output.write(original) + output.flush() + os.fsync(output.fileno()) + rollback.chmod(0o444) descriptor, temporary_name = tempfile.mkstemp( prefix=".runtime-", suffix=".yaml", dir=runtime_output ) @@ -505,6 +594,11 @@ def _replace_extract_binding( os.fsync(output.fileno()) temporary.chmod(0o444) os.replace(temporary, runtime_file) + directory_descriptor = os.open(runtime_output, os.O_RDONLY) + try: + os.fsync(directory_descriptor) + finally: + os.close(directory_descriptor) finally: temporary.unlink(missing_ok=True) except ProvisionError: @@ -675,7 +769,9 @@ def provision(args: argparse.Namespace) -> None: ): raise ProvisionError("invalid target") _stage_relay(assets, authority, runtime, source) - _install_tree(source, args.source_output.resolve(), root_mode=0o755) + _check_install_tree(source, args.source_output.resolve()) + _check_install_tree(runtime, args.runtime_output.resolve()) + _install_tree(source, args.source_output.resolve(), root_mode=0o555) elif target == "mint": if ( args.secret_output is None @@ -684,11 +780,20 @@ def provision(args: argparse.Namespace) -> None: or args.extract_output ): raise ProvisionError("invalid target") + if args.bind_host != EXPECTED_BIND_HOST["mint"]: + raise ProvisionError("invalid bind host") + mint_secrets = { + "signing-public.jwk", + "audit-hmac-key", + "solmara-demo-client-public.jwk", + *(f"{client}-public.jwk" for client in MINT_CLIENTS), + } + _validate_secret_inventory(args.secrets.resolve(), mint_secrets) _stage_mint( assets, args.secrets.resolve(), runtime, secret_output, args.bind_host ) - if args.bind_host != EXPECTED_BIND_HOST["mint"]: - raise ProvisionError("invalid bind host") + _check_install_tree(secret_output, args.secret_output.resolve()) + _check_install_tree(runtime, args.runtime_output.resolve()) _install_tree( secret_output, args.secret_output.resolve(), @@ -708,6 +813,13 @@ def provision(args: argparse.Namespace) -> None: raise ProvisionError("invalid target") if args.bind_host != EXPECTED_BIND_HOST[cell]: raise ProvisionError("invalid bind host") + cell_secrets = { + "signing-public.jwk", + "audit-hmac-key", + "subject-binding-hmac-key", + *(f"{client}-client-key" for client in CELL_CLIENTS[cell]), + } + _validate_secret_inventory(args.secrets.resolve(), cell_secrets) published_at = ( _publication_time( assets, @@ -730,6 +842,13 @@ def provision(args: argparse.Namespace) -> None: published_at, now, ) + runtime_preserve = _preserve_extract_rollback if cell in DIRECT else None + _check_install_tree(secret_output, args.secret_output.resolve()) + if cell in DIRECT: + _check_install_tree(extracts, args.extract_output.resolve()) + _check_install_tree( + runtime, args.runtime_output.resolve(), preserve=runtime_preserve + ) _install_tree( secret_output, args.secret_output.resolve(), @@ -740,7 +859,17 @@ def provision(args: argparse.Namespace) -> None: _install_tree(extracts, args.extract_output.resolve(), root_mode=0o555) else: raise ProvisionError("invalid target") - _install_tree(runtime, args.runtime_output.resolve(), root_mode=0o555) + _install_tree( + runtime, + args.runtime_output.resolve(), + root_mode=0o555, + preserve=( + _preserve_extract_rollback + if target.endswith("-evidence") + and target.removesuffix("-evidence") in DIRECT + else None + ), + ) def init_audit(destinations: list[Path], uid: int, gid: int) -> None: @@ -789,7 +918,7 @@ def main(argv: list[str] | None = None) -> int: publish_extract(args) else: init_audit(args.destination, args.uid, args.gid) - except (ProvisionError, OSError, ValueError, TypeError, KeyError, yaml.YAMLError): + except Exception: # noqa: BLE001 - the command boundary is deliberately value-free. print(GENERIC_ERROR, file=sys.stderr) return 1 print(SUCCESS) diff --git a/scripts/test_hosted_runtime_assets.py b/scripts/test_hosted_runtime_assets.py index 54847b0..644050f 100644 --- a/scripts/test_hosted_runtime_assets.py +++ b/scripts/test_hosted_runtime_assets.py @@ -98,6 +98,19 @@ def _digests(root: Path) -> dict[str, str]: result[path.relative_to(root).as_posix()] = MODULE._digest(path) return result + def _assert_no_path_leakage(self, root: Path) -> None: + forbidden = ( + str(self.root).encode(), + str(root).encode(), + str(Path(self.temporary.name)).encode(), + b"solmara-hosted-assets-", + ) + for path in root.rglob("*"): + if path.is_file(): + content = path.read_bytes() + for value in forbidden: + self.assertNotIn(value, content, path) + def test_build_contains_only_closed_runtime_assets(self) -> None: output = Path(self.temporary.name) / "assets" MODULE.build(self.root, output, self.relayctl) @@ -105,36 +118,39 @@ def test_build_contains_only_closed_runtime_assets(self) -> None: manifest = json.loads((output / "manifest.json").read_text(encoding="utf-8")) files = set(manifest["files"]) + expected_files = set() for authority in MODULE.AUTHORITIES: prefix = f"relays/{authority}" - self.assertIn(f"{prefix}/runtime.yaml", files) - self.assertIn(f"{prefix}/package/relay-package.json", files) - self.assertIn(f"{prefix}/source/{authority}.sqlite", files) + expected_files.update( + { + f"{prefix}/runtime.yaml", + f"{prefix}/package/relay-package.json", + f"{prefix}/source/{authority}.sqlite", + } + ) for cell in MODULE.EVIDENCE_CELLS: - expected = { + expected_files.update( f"evidence/cells/{cell}/{relative}" for relative in MODULE.EVIDENCE_FILES[cell] - } - self.assertTrue(expected.issubset(files)) - self.assertIn("mint/mint.yaml", files) - self.assertEqual( - { - path.removeprefix("generator/solmara_lab/") - for path in files - if path.startswith("generator/solmara_lab/") - }, - set(MODULE.GENERATOR_FILES), + ) + expected_files.add("mint/mint.yaml") + expected_files.update( + f"generator/solmara_lab/{relative}" for relative in MODULE.GENERATOR_FILES ) + self.assertEqual(files, expected_files) self.assertFalse(any(path.endswith(".pyc") for path in files)) self.assertFalse(any("/secrets/" in f"/{path}/" for path in files)) + self._assert_no_path_leakage(output) def test_unexpected_secret_symlink_and_bytecode_are_refused(self) -> None: injections = ( + ("relays/cra/unexpected.yaml", b"UNEXPECTED-CANARY"), ("evidence/cells/cra/bundle/signing.jwk", b"PRIVATE-JWK-CANARY"), ("evidence/cells/nia/bundle/audit/events.jsonl", b"AUDIT-CANARY"), + ("evidence/cells/sro/bundle/extracts/source.sqlite", b"EXTRACT-CANARY"), ("generator/solmara_lab/__pycache__/publisher.pyc", b"PYC-CANARY"), ) - for index, (relative, content) in enumerate(injections): + for relative, content in injections: with self.subTest(relative=relative): path = self.root / relative path.parent.mkdir(parents=True, exist_ok=True) @@ -180,6 +196,8 @@ def test_manifest_refuses_tampering_extra_files_and_noncanonical_bytes( manifest.write_text(json.dumps(json.loads(canonical)) + "\n", encoding="utf-8") with self.assertRaisesRegex(MODULE.AssetBuildError, "verification"): MODULE.verify_manifest(output) + with self.assertRaisesRegex(PROVISIONER.ProvisionError, "invalid assets"): + PROVISIONER.verify_assets(output) def test_two_builds_are_byte_for_byte_deterministic(self) -> None: first = Path(self.temporary.name) / "first" @@ -217,7 +235,9 @@ def test_relayctl_failure_is_redacted_and_leaves_no_output(self) -> None: self.assertEqual(completed.returncode, 1) self.assertEqual(completed.stdout, "") self.assertEqual(completed.stderr.strip(), "hosted runtime asset build failed") - self.assertNotIn(canary, completed.stdout + completed.stderr) + combined = completed.stdout + completed.stderr + for forbidden in (canary, str(self.root), str(output), str(failing)): + self.assertNotIn(forbidden, combined) self.assertFalse(output.exists()) diff --git a/scripts/test_provision_hosted_runtime.py b/scripts/test_provision_hosted_runtime.py index 89102de..d332651 100644 --- a/scripts/test_provision_hosted_runtime.py +++ b/scripts/test_provision_hosted_runtime.py @@ -1,5 +1,7 @@ from __future__ import annotations +import base64 +import contextlib import hashlib import importlib.util import json @@ -10,11 +12,13 @@ import sys import tempfile import unittest +from io import StringIO from pathlib import Path from types import SimpleNamespace from unittest import mock import yaml +from cryptography.hazmat.primitives.asymmetric import ec SCRIPT = Path(__file__).with_name("provision-hosted-runtime.py") @@ -36,7 +40,80 @@ def write_manifest(root: Path) -> None: ) +def b64url(value: bytes) -> str: + return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") + + +def key_pair() -> tuple[dict[str, str], dict[str, str]]: + private_key = ec.generate_private_key(ec.SECP256R1()) + private_numbers = private_key.private_numbers() + public_numbers = private_numbers.public_numbers + public = { + "alg": "ES256", + "crv": "P-256", + "kty": "EC", + "x": b64url(public_numbers.x.to_bytes(32, "big")), + "y": b64url(public_numbers.y.to_bytes(32, "big")), + } + thumbprint = {key: public[key] for key in ("crv", "kty", "x", "y")} + public["kid"] = b64url( + hashlib.sha256( + json.dumps(thumbprint, separators=(",", ":"), sort_keys=True).encode() + ).digest() + ) + private = {**public, "d": b64url(private_numbers.private_value.to_bytes(32, "big"))} + return public, private + + +def write_secret(root: Path, name: str, value: bytes | dict[str, str]) -> None: + root.mkdir(parents=True, exist_ok=True) + data = json.dumps(value).encode() if isinstance(value, dict) else value + path = root / name + if path.exists(): + path.chmod(0o600) + path.write_bytes(data) + path.chmod(0o400) + + class HostedProvisionerTests(unittest.TestCase): + def test_no_argument_cli_failure_is_one_generic_line(self) -> None: + completed = subprocess.run( + [sys.executable, str(SCRIPT)], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(completed.returncode, 1) + self.assertEqual(completed.stdout, "") + self.assertEqual(completed.stderr, f"{provisioner.GENERIC_ERROR}\n") + + def test_unexpected_failures_are_redacted_at_the_command_boundary(self) -> None: + stderr = StringIO() + with ( + mock.patch.object( + provisioner, "provision", side_effect=RuntimeError("canary") + ), + contextlib.redirect_stderr(stderr), + ): + result = provisioner.main( + [ + "provision", + "--target", + "cra-relay", + "--assets", + "/canary/assets", + "--secrets", + "/canary/secrets", + "--runtime-output", + "/canary/runtime", + "--source-output", + "/canary/source", + ] + ) + self.assertEqual(result, 1) + self.assertEqual(stderr.getvalue(), f"{provisioner.GENERIC_ERROR}\n") + self.assertNotIn("canary", stderr.getvalue()) + def test_binary_write_is_exact(self) -> None: with tempfile.TemporaryDirectory() as temporary: target = Path(temporary) / "source.sqlite" @@ -45,7 +122,7 @@ def test_binary_write_is_exact(self) -> None: self.assertEqual(target.read_bytes(), content) self.assertEqual(stat.S_IMODE(target.stat().st_mode), 0o444) - def test_install_resumes_exact_partial_tree_and_refuses_mismatch(self) -> None: + def test_install_is_idempotent_only_for_an_exact_existing_tree(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) staged, destination = root / "staged", root / "destination" @@ -55,9 +132,13 @@ def test_install_resumes_exact_partial_tree_and_refuses_mismatch(self) -> None: destination.mkdir() provisioner._write(destination / "one", b"one", 0o444) + with self.assertRaises(provisioner.ProvisionError): + provisioner._install_tree(staged, destination, root_mode=0o555) + + provisioner._write(destination / "nested" / "two", b"two", 0o444) + (destination / "nested").chmod(0o755) + provisioner._install_tree(staged, destination, root_mode=0o555) provisioner._install_tree(staged, destination, root_mode=0o555) - self.assertEqual((destination / "nested" / "two").read_bytes(), b"two") - self.assertEqual(stat.S_IMODE(destination.stat().st_mode), 0o555) (destination / "one").chmod(0o644) with self.assertRaises(provisioner.ProvisionError): @@ -97,6 +178,127 @@ def test_hmac_secret_rejects_weak_or_non_text_material(self) -> None: secret.chmod(0o400) self.assertEqual(provisioner._hmac_secret(root, "hmac"), b"a" * 32) + def test_secret_inventory_is_exact_and_authority_scoped(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + write_secret(root, "signing-public.jwk", b"public") + write_secret(root, "audit-hmac-key", b"a" * 32) + expected = {"signing-public.jwk", "audit-hmac-key"} + provisioner._validate_secret_inventory(root, expected) + + write_secret(root, "another-authority-client-key", b"private") + with self.assertRaises(provisioner.ProvisionError): + provisioner._validate_secret_inventory(root, expected) + + def test_evidence_output_contains_only_its_public_signer_and_own_secrets( + self, + ) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + assets = root / "assets" + cell = assets / "evidence" / "cells" / "sipf" + (cell / "bundle").mkdir(parents=True) + (cell / "bundle" / "evidence.yaml").write_text( + yaml.safe_dump({"signing": {"activePublicJwkFile": "old"}}), + encoding="utf-8", + ) + (cell / "runtime.yaml").write_text( + yaml.safe_dump({"listener": {"bindHost": "old"}}), + encoding="utf-8", + ) + secrets = root / "inputs" + signing_public, signing_private = key_pair() + _, client_private = key_pair() + write_secret(secrets, "signing-public.jwk", signing_public) + write_secret(secrets, "audit-hmac-key", b"a" * 32) + write_secret(secrets, "subject-binding-hmac-key", b"b" * 32) + for client in provisioner.CELL_CLIENTS["sipf"]: + write_secret(secrets, f"{client}-client-key", client_private) + + runtime, output_secrets = root / "runtime", root / "output-secrets" + provisioner._stage_evidence( + assets, + "sipf", + secrets, + runtime, + output_secrets, + None, + provisioner.EXPECTED_BIND_HOST["sipf"], + "2026-08-12T00:00:00Z", + "2026-08-12T00:00:00Z", + ) + + self.assertEqual( + {path.name for path in output_secrets.iterdir()}, + { + "audit-hmac-key", + "subject-binding-hmac-key", + "sipf-pension-evidence-client-id", + "sipf-pension-evidence-client-key", + "sipf-survivor-evidence-client-id", + "sipf-survivor-evidence-client-key", + }, + ) + self.assertNotIn( + "signing", " ".join(path.name for path in output_secrets.iterdir()) + ) + public_file = ( + runtime / "bundle" / "public-keys" / f"{signing_public['kid']}.jwk.json" + ) + self.assertEqual(json.loads(public_file.read_text()), signing_public) + self.assertNotIn(signing_private["d"], public_file.read_text()) + + def test_mint_writes_only_audit_secret_and_public_client_registrations( + self, + ) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + assets = root / "assets" + (assets / "mint").mkdir(parents=True) + (assets / "mint" / "mint.yaml").write_text( + yaml.safe_dump( + { + "listener": {"address": "old"}, + "signing": {"activePublicJwkFile": "old"}, + } + ), + encoding="utf-8", + ) + secrets = root / "inputs" + public, private = key_pair() + write_secret(secrets, "signing-public.jwk", public) + write_secret(secrets, "audit-hmac-key", b"a" * 32) + for client in provisioner.MINT_CLIENTS: + write_secret(secrets, f"{client}-public.jwk", public) + write_secret(secrets, "solmara-demo-client-public.jwk", public) + + runtime, output_secrets = root / "runtime", root / "output-secrets" + provisioner._stage_mint( + assets, + secrets, + runtime, + output_secrets, + provisioner.EXPECTED_BIND_HOST["mint"], + ) + self.assertEqual( + {path.name for path in output_secrets.iterdir()}, {"audit-hmac-key"} + ) + emitted = "\n".join( + path.read_text() for path in runtime.rglob("*") if path.is_file() + ) + self.assertNotIn(private["d"], emitted) + self.assertEqual(len(list((runtime / "clients").glob("*.yaml"))), 9) + + write_secret(secrets, "signing-public.jwk", private) + with self.assertRaises(provisioner.ProvisionError): + provisioner._stage_mint( + assets, + secrets, + root / "bad-runtime", + root / "bad-secrets", + provisioner.EXPECTED_BIND_HOST["mint"], + ) + def test_relay_provision_preserves_database_and_is_idempotent(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) @@ -134,6 +336,48 @@ def test_relay_provision_preserves_database_and_is_idempotent(self) -> None: self.assertEqual( (runtime / "package" / "sealed.json").read_bytes(), b"sealed" ) + self.assertEqual( + stat.S_IMODE((source / "cra.sqlite").stat().st_mode), 0o444 + ) + + def test_all_outputs_are_preflighted_before_any_volume_changes(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + runtime_output = root / "runtime-output" + runtime_output.mkdir() + provisioner._write(runtime_output / "runtime.yaml", b"active", 0o444) + secret_output = root / "secret-output" + arguments = provisioner.parser().parse_args( + [ + "provision", + "--target", + "mint", + "--assets", + str(root / "assets"), + "--secrets", + str(root / "inputs"), + "--runtime-output", + str(runtime_output), + "--secret-output", + str(secret_output), + "--bind-host", + provisioner.EXPECTED_BIND_HOST["mint"], + ] + ) + + def stage(_assets, _inputs, runtime, secrets, _bind): + provisioner._write(runtime / "runtime.yaml", b"replacement", 0o444) + provisioner._write(secrets / "audit-hmac-key", b"a" * 32, 0o400) + + with ( + mock.patch.object(provisioner, "verify_assets"), + mock.patch.object(provisioner, "_validate_secret_inventory"), + mock.patch.object(provisioner, "_stage_mint", side_effect=stage), + self.assertRaises(provisioner.ProvisionError), + ): + provisioner.provision(arguments) + self.assertFalse(secret_output.exists()) + self.assertEqual((runtime_output / "runtime.yaml").read_bytes(), b"active") def test_relay_cli_loads_manifest_verifier_directly(self) -> None: with tempfile.TemporaryDirectory() as temporary: @@ -171,6 +415,37 @@ def test_relay_cli_loads_manifest_verifier_directly(self) -> None: self.assertEqual(completed.stdout.strip(), provisioner.SUCCESS) self.assertEqual(completed.stderr, "") + def test_manifest_failure_precedes_every_output_mutation(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + assets = root / "assets" + assets.mkdir() + (assets / "manifest.json").write_text("{}\n", encoding="utf-8") + runtime, source = root / "runtime", root / "source" + runtime.mkdir() + source.mkdir() + provisioner._write(runtime / "keep", b"runtime", 0o444) + provisioner._write(source / "keep", b"source", 0o444) + arguments = provisioner.parser().parse_args( + [ + "provision", + "--target", + "cra-relay", + "--assets", + str(assets), + "--secrets", + str(root / "unused"), + "--runtime-output", + str(runtime), + "--source-output", + str(source), + ] + ) + with self.assertRaises(provisioner.ProvisionError): + provisioner.provision(arguments) + self.assertEqual((runtime / "keep").read_bytes(), b"runtime") + self.assertEqual((source / "keep").read_bytes(), b"source") + def test_existing_direct_extract_publication_is_reused(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) @@ -192,7 +467,9 @@ def test_existing_direct_extract_publication_is_reused(self) -> None: yaml.safe_dump( { "sourceExtracts": { - "sro-poverty-extract": {"path": f"/extracts/{extract.name}"} + "sro-poverty-extract": { + "path": f"/var/lib/registry-evidence/sro/extracts/{extract.name}" + } } } ), @@ -241,6 +518,31 @@ def test_orphan_direct_extract_recovers_after_pre_runtime_crash(self) -> None: extract, "sro", observed_at="2026-08-12T09:01:00Z" ) + def test_stale_or_mismatched_extract_is_a_value_free_refusal(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + runtime, extracts = root / "runtime", root / "extracts" + runtime.mkdir() + extracts.mkdir() + extract = extracts / "cra-birth-20260812T090000Z.sqlite" + extract.write_bytes(b"stale-canary") + extract.chmod(0o444) + publisher = mock.Mock() + publisher.validate_extract.side_effect = RuntimeError("stale-canary") + with ( + mock.patch.object( + provisioner, "_load_publisher", return_value=publisher + ), + self.assertRaisesRegex(provisioner.ProvisionError, "invalid existing"), + ): + provisioner._publication_time( + root / "assets", + "cra", + runtime, + extracts, + "2026-08-13T09:01:00Z", + ) + def test_extract_publication_appends_and_atomically_rebinds(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) @@ -307,6 +609,31 @@ def stage(_assets, _cell, destination, _published_at, _observed_at): ) self.assertEqual(stat.S_IMODE(runtime.stat().st_mode), 0o555) self.assertEqual(stat.S_IMODE(extracts.stat().st_mode), 0o555) + rollback = ( + runtime + / f"runtime.rollback-{previous_name.removesuffix('.sqlite')}.yaml" + ) + self.assertTrue(rollback.is_file()) + rollback_config = yaml.safe_load(rollback.read_text()) + self.assertEqual( + Path( + rollback_config["sourceExtracts"]["sro-poverty-extract"]["path"] + ).name, + previous_name, + ) + + def test_extract_append_never_overwrites_a_mismatched_filename(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + staged = root / "staged.sqlite" + destination = root / "extracts" + destination.mkdir() + target = destination / staged.name + staged.write_bytes(b"replacement") + target.write_bytes(b"active") + with self.assertRaises(provisioner.ProvisionError): + provisioner._append_file(staged, destination) + self.assertEqual(target.read_bytes(), b"active") def test_evidence_bind_address_is_closed_before_staging(self) -> None: with tempfile.TemporaryDirectory() as temporary: @@ -336,6 +663,23 @@ def test_evidence_bind_address_is_closed_before_staging(self) -> None: provisioner.provision(arguments) stage.assert_not_called() + def test_init_audit_changes_only_each_root_directory(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / "audit" + root.mkdir() + child = root / "existing.jsonl" + child.write_bytes(b"existing-audit-canary") + child.chmod(0o640) + with ( + mock.patch.object(provisioner.os, "chown") as chown, + mock.patch.object(provisioner.os, "chmod") as chmod, + ): + provisioner.init_audit([root], 65532, 65532) + chown.assert_called_once_with(root, 65532, 65532) + chmod.assert_called_once_with(root, 0o700) + self.assertEqual(child.read_bytes(), b"existing-audit-canary") + self.assertEqual(stat.S_IMODE(child.stat().st_mode), 0o640) + if __name__ == "__main__": unittest.main() From f59ef5bfb5873a1186f8de008ce14cf2baf3fc67 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 12 Aug 2026 23:43:08 +0700 Subject: [PATCH 07/53] Preserve Relay check diagnostics Signed-off-by: Jeremi Joslin --- scripts/check-relay-projects.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/check-relay-projects.sh b/scripts/check-relay-projects.sh index f13a91f..6a6ec88 100755 --- a/scripts/check-relay-projects.sh +++ b/scripts/check-relay-projects.sh @@ -21,6 +21,7 @@ run_relayctl() { database=$2 stage=$3 shift 3 + report="$temporary_root/$authority-$stage.json" if ! docker run --rm \ --platform linux/amd64 \ --user "$(id -u):$(id -g)" \ @@ -28,8 +29,11 @@ run_relayctl() { --volume "$database:/var/lib/relay/source/$authority.sqlite:ro" \ --workdir /workspace \ "$REGISTRY_RELAYCTL_IMAGE" \ - --json "$@" >/dev/null; then + --json "$@" >"$report"; then printf 'relay-check: %s %s failed\n' "$authority" "$stage" >&2 + # relayctl diagnostics contain governed contract paths and error codes, not + # selectors or source values. Preserve them so CI failures are actionable. + cat "$report" >&2 return 1 fi } From 37ef685271e3dd651f6fed6e742c83e22c81aafa Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 12 Aug 2026 23:47:51 +0700 Subject: [PATCH 08/53] Pin Relay SQLite publication runtime Signed-off-by: Jeremi Joslin --- scripts/check-relay-projects.sh | 3 +- scripts/prepare-authority-runtime.sh | 3 +- scripts/publish-relay-sources.sh | 19 +++++++++++ scripts/test_runtime_topology.py | 51 ++++++++++++++++++++++------ 4 files changed, 62 insertions(+), 14 deletions(-) create mode 100755 scripts/publish-relay-sources.sh diff --git a/scripts/check-relay-projects.sh b/scripts/check-relay-projects.sh index 6a6ec88..6964f49 100755 --- a/scripts/check-relay-projects.sh +++ b/scripts/check-relay-projects.sh @@ -6,8 +6,7 @@ set -a . "$root/versions.env" set +a -cd "$root/generator" -uv run python -c 'from pathlib import Path; from solmara_lab.publisher import publish_relay_sources; publish_relay_sources(Path(".."))' +"$root/scripts/publish-relay-sources.sh" temporary_root=$(mktemp -d "$root/output/relay-check.XXXXXX") cleanup() { diff --git a/scripts/prepare-authority-runtime.sh b/scripts/prepare-authority-runtime.sh index 3c69761..6cd41cf 100755 --- a/scripts/prepare-authority-runtime.sh +++ b/scripts/prepare-authority-runtime.sh @@ -4,8 +4,7 @@ set -eu root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) relayctl_image=${REGISTRY_RELAYCTL_IMAGE:-solmara-lab-relayctl:source} -cd "$root/generator" -uv run python -c 'from pathlib import Path; from solmara_lab.publisher import publish_relay_sources; publish_relay_sources(Path(".."))' +"$root/scripts/publish-relay-sources.sh" cd "$root" uv run scripts/publish-runtime-extracts.py diff --git a/scripts/publish-relay-sources.sh b/scripts/publish-relay-sources.sh new file mode 100755 index 0000000..5da6e51 --- /dev/null +++ b/scripts/publish-relay-sources.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +publisher_image='python:3.12-slim-trixie@sha256:229a2c5bfa27522db7815ea81f9bed70af17ccb9de9fc7ad142b1877b5830d36' + +mkdir -p "$root/output/sqlite/relay" +docker run --rm \ + --platform linux/amd64 \ + --user "$(id -u):$(id -g)" \ + --network none \ + --read-only \ + --tmpfs /tmp \ + --volume "$root:/workspace:ro" \ + --volume "$root/output/sqlite/relay:/workspace/output/sqlite/relay" \ + --workdir /workspace/generator \ + "$publisher_image" \ + python -c 'from pathlib import Path; from solmara_lab.publisher import publish_relay_sources; publish_relay_sources(Path(".."))' + diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index 76fcc48..0134c47 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -15,11 +15,25 @@ class RuntimeTopologyTests(unittest.TestCase): + def test_relay_publication_uses_pinned_linux_sqlite_runtime(self) -> None: + script = SCRIPT.with_name("publish-relay-sources.sh").read_text( + encoding="utf-8" + ) + self.assertIn("python:3.12-slim-trixie@sha256:", script) + self.assertIn("--platform linux/amd64", script) + self.assertIn("--network none", script) + self.assertIn("--read-only", script) + self.assertNotIn("uv run", script) + def test_retired_surfaces_are_rejected(self) -> None: with tempfile.TemporaryDirectory() as temporary: path = Path(temporary) / "compose.yaml" - path.write_text("services:\n old:\n image: postgres:16\n", encoding="utf-8") - self.assertEqual(MODULE.failures([path]), ["compose.yaml:3: retired database"]) + path.write_text( + "services:\n old:\n image: postgres:16\n", encoding="utf-8" + ) + self.assertEqual( + MODULE.failures([path]), ["compose.yaml:3: retired database"] + ) def test_current_active_topology_is_closed(self) -> None: self.assertEqual(MODULE.failures(), []) @@ -83,7 +97,10 @@ def test_local_relays_use_the_v2_runtime_filesystem_contract(self) -> None: ) for other in {"cra", "nia", "mosd", "sipf", "nagdi"} - {authority}: self.assertFalse( - any(f"{other}-relay-source:" in volume for volume in publisher_volumes), + any( + f"{other}-relay-source:" in volume + for volume in publisher_volumes + ), (authority, other), ) @@ -139,11 +156,15 @@ def test_authority_cells_have_fixed_private_addresses(self) -> None: } for service_name, address in expected.items(): self.assertEqual( - compose["services"][service_name]["networks"]["runtime"]["ipv4_address"], + compose["services"][service_name]["networks"]["runtime"][ + "ipv4_address" + ], address, ) - caddy = (SCRIPT.parents[1] / "config/evidence/Caddyfile").read_text(encoding="utf-8") + caddy = (SCRIPT.parents[1] / "config/evidence/Caddyfile").read_text( + encoding="utf-8" + ) evidence_routes = { "cra": "172.29.1.21", "nia": "172.29.1.22", @@ -277,7 +298,9 @@ def test_hosted_relays_keep_runtime_and_source_read_only(self) -> None: volumes, ) - def test_hosted_programme_services_receive_only_their_application_secrets(self) -> None: + def test_hosted_programme_services_receive_only_their_application_secrets( + self, + ) -> None: compose_path = SCRIPT.parents[1] / "compose.coolify.yaml" compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) services = compose["services"] @@ -321,11 +344,15 @@ def test_hosted_programme_services_receive_only_their_application_secrets(self) "https://child-benefit.solmara.registrystack.org", ) self.assertEqual( - services["child-benefit-federator"]["environment"]["CHILD_BENEFIT_FEDERATOR_HOST"], + services["child-benefit-federator"]["environment"][ + "CHILD_BENEFIT_FEDERATOR_HOST" + ], "0.0.0.0", ) self.assertEqual( - services["child-benefit-federator"]["environment"]["CHILD_BENEFIT_FEDERATOR_PORT"], + services["child-benefit-federator"]["environment"][ + "CHILD_BENEFIT_FEDERATOR_PORT" + ], "8080", ) @@ -336,7 +363,9 @@ def test_hosted_programme_services_receive_only_their_application_secrets(self) self.assertNotIn("SOLMARA_EVIDENCE_CLIENT_KEY", environment, service_name) self.assertNotIn("CHILD_BENEFIT_FEDERATOR_TOKEN", environment, service_name) - def test_hosted_esignet_overlay_wires_the_main_portal_with_a_separate_key(self) -> None: + def test_hosted_esignet_overlay_wires_the_main_portal_with_a_separate_key( + self, + ) -> None: overlay_path = SCRIPT.parents[1] / "compose.coolify.esignet.yaml" overlay = yaml.safe_load(overlay_path.read_text(encoding="utf-8")) services = overlay["services"] @@ -364,7 +393,9 @@ def test_hosted_esignet_overlay_wires_the_main_portal_with_a_separate_key(self) { service_name for service_name, service in services.items() - if service.get("environment", {}).get("PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64") + if service.get("environment", {}).get( + "PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64" + ) == private_key_value }, {"portal"}, From f1fef9bf02e18ae4b93a1186cba275859c70993e Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 12 Aug 2026 23:49:10 +0700 Subject: [PATCH 09/53] Smoke signer key binding in release workflow Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 45 ++++++++++++++++++++++++- scripts/test_hosted_image_manifest.py | 24 ++++++++++--- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index af8e4a3..b4b06af 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -280,10 +280,53 @@ jobs: set -euo pipefail image="$SOLMARA_IMAGE_REGISTRY/solmara-lab-transit-signer@${{ steps.transit_signer.outputs.digest }}" docker run --rm --network none --read-only --entrypoint python "$image" -c 'import cryptography' + state="$RUNNER_TEMP/solmara-transit-signer-state" + install -d -m 0700 "$state/secrets" "$state/transit" + sudo chown 65532:65532 "$state/secrets" "$state/transit" + sudo install -o 65532 -g 65532 -m 0400 \ + config/evidence/local/cells/cra/secrets/signing.jwk \ + "$state/secrets/signing.jwk" + sudo install -o 65532 -g 65532 -m 0400 \ + runtime/evidence-cells/cells/cra/bundle/public-keys/*.jwk.json \ + "$state/secrets/signing-public.jwk" + name="solmara-transit-signer-${GITHUB_RUN_ID}" + trap 'docker rm --force "$name" >/dev/null 2>&1 || true' EXIT + docker run --detach --name "$name" --network none --read-only --tmpfs /tmp \ + --mount "type=bind,source=$state/secrets/signing.jwk,target=/run/secrets/signing.jwk,readonly" \ + --mount "type=bind,source=$state/secrets/signing-public.jwk,target=/run/secrets/signing-public.jwk,readonly" \ + --mount "type=bind,source=$state/transit,target=/transit" \ + "$image" \ + --private-jwk /run/secrets/signing.jwk \ + --public-jwk /run/secrets/signing-public.jwk \ + --socket /transit/transit-proxy.sock \ + --key-name solmara-evidence-cra + for _ in $(seq 1 50); do + test -S "$state/transit/transit-proxy.sock" && break + sleep 0.1 + done + test -S "$state/transit/transit-proxy.sock" + curl --fail --silent --show-error \ + --unix-socket "$state/transit/transit-proxy.sock" \ + -H 'X-Vault-Request: true' \ + http://localhost/v1/transit/keys/solmara-evidence-cra >/dev/null + docker rm --force "$name" >/dev/null + trap - EXIT + + sudo install -o 65532 -g 65532 -m 0400 \ + runtime/evidence-cells/cells/nia/bundle/public-keys/*.jwk.json \ + "$state/secrets/signing-public.jwk" stdout="$RUNNER_TEMP/solmara-transit-signer.stdout" stderr="$RUNNER_TEMP/solmara-transit-signer.stderr" set +e - docker run --rm --network none --read-only "$image" >"$stdout" 2>"$stderr" + docker run --rm --network none --read-only --tmpfs /tmp \ + --mount "type=bind,source=$state/secrets/signing.jwk,target=/run/secrets/signing.jwk,readonly" \ + --mount "type=bind,source=$state/secrets/signing-public.jwk,target=/run/secrets/signing-public.jwk,readonly" \ + --mount "type=bind,source=$state/transit,target=/transit" \ + "$image" \ + --private-jwk /run/secrets/signing.jwk \ + --public-jwk /run/secrets/signing-public.jwk \ + --socket /transit/transit-proxy.sock \ + --key-name solmara-evidence-cra >"$stdout" 2>"$stderr" status=$? set -e test "$status" -eq 1 diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index b9faab9..3fcc827 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -78,7 +78,9 @@ def setUp(self) -> None: def tearDown(self) -> None: self.directory.cleanup() - def invoke(self, *arguments: str, environment: dict[str, str] | None = None) -> tuple[int, str]: + def invoke( + self, *arguments: str, environment: dict[str, str] | None = None + ) -> tuple[int, str]: stderr = io.StringIO() with ( mock.patch.dict(os.environ, environment or {}, clear=True), @@ -190,7 +192,9 @@ def test_compose_inventory_is_closed_and_requires_variables(self) -> None: self.assertEqual(result, 1) self.assertIn("must be required variables", stderr) - def test_release_workflow_generates_validates_and_uploads_the_manifest(self) -> None: + def test_release_workflow_generates_validates_and_uploads_the_manifest( + self, + ) -> None: workflow_path = ROOT / ".github" / "workflows" / "release-candidate.yml" workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8")) steps = workflow["jobs"]["verify-and-publish"]["steps"] @@ -201,7 +205,9 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest(self) -> upload_index = names.index("Upload Coolify image manifest") self.assertLess(provisioner_index, generate_index) self.assertLess(signer_index, generate_index) - self.assertGreater(generate_index, names.index("Build and push eSignet seed image")) + self.assertGreater( + generate_index, names.index("Build and push eSignet seed image") + ) self.assertGreater(upload_index, generate_index) provisioner = steps[provisioner_index] @@ -229,7 +235,10 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest(self) -> ) self.assertIn("/solmara-lab-transit-signer:", signer["with"]["tags"]) for step in (provisioner, signer): - self.assertIn("org.opencontainers.image.revision=${{ github.sha }}", step["with"]["labels"]) + self.assertIn( + "org.opencontainers.image.revision=${{ github.sha }}", + step["with"]["labels"], + ) self.assertIn( "org.opencontainers.image.source=https://github.com/registrystack/solmara-lab", step["with"]["labels"], @@ -263,11 +272,16 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest(self) -> self.assertIn("steps.transit_signer.outputs.digest", signer_smoke) self.assertIn("--network none --read-only --entrypoint python", signer_smoke) self.assertIn("import cryptography", signer_smoke) + self.assertIn("--public-jwk /run/secrets/signing-public.jwk", signer_smoke) + self.assertIn("v1/transit/keys/solmara-evidence-cra", signer_smoke) + self.assertIn("cells/nia/bundle/public-keys", signer_smoke) self.assertIn('test "$status" -eq 1', signer_smoke) self.assertIn("hosted Transit signer could not start", signer_smoke) packages = next( - step for step in steps if step.get("name") == "Require pre-provisioned public Solmara packages" + step + for step in steps + if step.get("name") == "Require pre-provisioned public Solmara packages" )["run"] self.assertIn("solmara-lab-authority-provisioner", packages) self.assertIn("solmara-lab-transit-signer", packages) From 390fbfbe3b78f3ab2bc438c7380ce9e787c82d59 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 12 Aug 2026 23:50:47 +0700 Subject: [PATCH 10/53] Retry verified Registry Stack downloads Signed-off-by: Jeremi Joslin --- docker/registry-stack-release-binary/Dockerfile | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docker/registry-stack-release-binary/Dockerfile b/docker/registry-stack-release-binary/Dockerfile index bc570a6..977a1f0 100644 --- a/docker/registry-stack-release-binary/Dockerfile +++ b/docker/registry-stack-release-binary/Dockerfile @@ -5,7 +5,9 @@ ARG REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL ARG REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256 RUN test -n "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL" && \ test -n "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256" && \ - wget -q -O /evidence "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL" && \ + { attempt=0; until wget -q -T 30 -O /evidence "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL"; do \ + attempt=$((attempt + 1)); [ "$attempt" -lt 5 ] || exit 1; sleep "$attempt"; \ + done; } && \ printf '%s %s\n' "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256" /evidence | sha256sum -c - && \ chmod 0755 /evidence @@ -14,7 +16,9 @@ ARG REGISTRY_STACK_RELEASE_MINT_ASSET_URL ARG REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256 RUN test -n "$REGISTRY_STACK_RELEASE_MINT_ASSET_URL" && \ test -n "$REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256" && \ - wget -q -O /mint "$REGISTRY_STACK_RELEASE_MINT_ASSET_URL" && \ + { attempt=0; until wget -q -T 30 -O /mint "$REGISTRY_STACK_RELEASE_MINT_ASSET_URL"; do \ + attempt=$((attempt + 1)); [ "$attempt" -lt 5 ] || exit 1; sleep "$attempt"; \ + done; } && \ printf '%s %s\n' "$REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256" /mint | sha256sum -c - && \ chmod 0755 /mint @@ -23,7 +27,9 @@ ARG REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL ARG REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256 RUN test -n "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" && \ test -n "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" && \ - wget -q -O /relayctl "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" && \ + { attempt=0; until wget -q -T 30 -O /relayctl "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL"; do \ + attempt=$((attempt + 1)); [ "$attempt" -lt 5 ] || exit 1; sleep "$attempt"; \ + done; } && \ printf '%s %s\n' "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" /relayctl | sha256sum -c - && \ chmod 0755 /relayctl From 4af1aea9dc8edeb48d750b8dee1039ab9df25012 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 00:01:36 +0700 Subject: [PATCH 11/53] Use pinned Relayctl for runtime packaging Signed-off-by: Jeremi Joslin --- scripts/prepare-authority-runtime.sh | 7 ++- scripts/test_runtime_topology.py | 68 ++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/scripts/prepare-authority-runtime.sh b/scripts/prepare-authority-runtime.sh index 6cd41cf..feddf15 100755 --- a/scripts/prepare-authority-runtime.sh +++ b/scripts/prepare-authority-runtime.sh @@ -2,7 +2,11 @@ set -eu root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -relayctl_image=${REGISTRY_RELAYCTL_IMAGE:-solmara-lab-relayctl:source} +set -a +# shellcheck disable=SC1091 +. "$root/versions.env" +set +a +relayctl_image=${REGISTRY_RELAYCTL_IMAGE:?missing REGISTRY_RELAYCTL_IMAGE} "$root/scripts/publish-relay-sources.sh" @@ -18,6 +22,7 @@ for authority in cra nia mosd sipf nagdi; do } trap cleanup EXIT HUP INT TERM docker run --rm \ + --platform linux/amd64 \ --user "$(id -u):$(id -g)" \ --volume "$root:/workspace" \ --volume "$root/output/sqlite/relay/$authority.sqlite:/var/lib/relay/source/$authority.sqlite:ro" \ diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index 0134c47..577f706 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -1,6 +1,9 @@ from __future__ import annotations import importlib.util +import os +import shutil +import subprocess import tempfile import unittest from pathlib import Path @@ -15,6 +18,71 @@ class RuntimeTopologyTests(unittest.TestCase): + def test_authority_runtime_uses_the_versioned_relayctl_image(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + scripts = root / "scripts" + scripts.mkdir() + runtime_script = scripts / "prepare-authority-runtime.sh" + shutil.copy2(SCRIPT.with_name("prepare-authority-runtime.sh"), runtime_script) + (root / "versions.env").write_text( + "REGISTRY_RELAYCTL_IMAGE=example.invalid/relayctl:v0.20.1\n", + encoding="utf-8", + ) + publisher = scripts / "publish-relay-sources.sh" + publisher.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + publisher.chmod(0o755) + + fake_bin = root / "fake-bin" + fake_bin.mkdir() + fake_uv = fake_bin / "uv" + fake_uv.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + fake_uv.chmod(0o755) + fake_docker = fake_bin / "docker" + fake_docker.write_text( + "#!/bin/sh\n" + "printf '%s\\n' \"$*\" >> \"$DOCKER_LOG\"\n" + "previous=\n" + "for argument in \"$@\"; do\n" + " if [ \"$previous\" = --output ]; then\n" + " mkdir -p \"$argument\"\n" + " : > \"$argument/relay-package.json\"\n" + " fi\n" + " previous=$argument\n" + "done\n", + encoding="utf-8", + ) + fake_docker.chmod(0o755) + + for authority in ("cra", "nia", "mosd", "sipf", "nagdi"): + (root / "relays" / authority).mkdir(parents=True) + + docker_log = root / "docker.log" + environment = os.environ.copy() + environment.update( + { + "DOCKER_LOG": str(docker_log), + "PATH": f"{fake_bin}:{environment['PATH']}", + "REGISTRY_RELAYCTL_IMAGE": "wrong.invalid/relayctl:ambient", + } + ) + subprocess.run( + [str(runtime_script)], + cwd=root, + env=environment, + check=True, + capture_output=True, + text=True, + ) + invocations = docker_log.read_text(encoding="utf-8").splitlines() + + self.assertEqual(len(invocations), 5) + self.assertTrue( + all("example.invalid/relayctl:v0.20.1" in line for line in invocations) + ) + self.assertTrue(all("--platform linux/amd64" in line for line in invocations)) + self.assertTrue(all("wrong.invalid" not in line for line in invocations)) + def test_relay_publication_uses_pinned_linux_sqlite_runtime(self) -> None: script = SCRIPT.with_name("publish-relay-sources.sh").read_text( encoding="utf-8" From 6ea2846dc9320780417c037982e0549b8579d8a0 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:00:06 +0700 Subject: [PATCH 12/53] chore: consume Registry Stack v0.21.0 images Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 100 ++++------- README.md | 14 +- compose.coolify.agriculture.yaml | 4 +- compose.coolify.interior.yaml | 8 +- compose.coolify.labour-pensions.yaml | 4 +- compose.coolify.social-development.yaml | 6 +- compose.coolify.yaml | 2 +- compose.hosted.yaml | 6 +- compose.yaml | 8 +- .../registry-stack-release-binary/Dockerfile | 45 +---- docker/registry-stack-runtime/Dockerfile | 27 --- docs/changelog.md | 21 ++- docs/hosted-deployment.md | 45 +++-- home/src/lib/components/EngineerDoor.svelte | 2 +- home/src/lib/components/TrustStrip.svelte | 2 +- home/src/lib/server/data.test.ts | 7 +- justfile | 8 +- scripts/build-registry-stack-runtime.sh | 77 ++++++--- scripts/check-coolify-compose.sh | 2 +- scripts/check-hosted-compose.sh | 2 +- scripts/check-image-pins.py | 25 +-- scripts/check-registry-stack-release-pin.py | 6 +- scripts/hosted-image-manifest.py | 14 +- scripts/test_build_registry_stack_runtime.py | 161 ++++++++++++++++++ scripts/test_hosted_image_manifest.py | 84 +++++++-- scripts/test_image_pins.py | 52 ++++-- scripts/test_registry_stack_release_pin.py | 24 ++- scripts/test_runtime_topology.py | 4 +- versions.env | 28 ++- 29 files changed, 490 insertions(+), 298 deletions(-) delete mode 100644 docker/registry-stack-runtime/Dockerfile create mode 100644 scripts/test_build_registry_stack_runtime.py diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index b4b06af..fa66960 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -30,15 +30,15 @@ jobs: set +a printf 'source_ref=%s\n' "$REGISTRY_STACK_SOURCE_REF" >> "$GITHUB_OUTPUT" printf 'source_commit=%s\n' "$REGISTRY_STACK_SOURCE_COMMIT" >> "$GITHUB_OUTPUT" - for key in ESIGNET_BASE_IMAGE ESIGNET_POSTGRES_IMAGE ESIGNET_UI_IMAGE \ + for key in REGISTRY_STACK_REQUIRED_VERSION \ + REGISTRY_RELAY_IMAGE SOLMARA_EVIDENCE_IMAGE SOLMARA_MINT_IMAGE \ + ESIGNET_BASE_IMAGE ESIGNET_POSTGRES_IMAGE ESIGNET_UI_IMAGE \ ESIGNET_AUTHENTICATOR_JAR_URL ESIGNET_AUTHENTICATOR_JAR_SHA256 \ - REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256 \ - REGISTRY_STACK_RELEASE_MINT_ASSET_URL REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256 \ REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256; do value="$(printenv "$key")" printf '%s=%s\n' "$key" "$value" >> "$GITHUB_ENV" done - - name: Require published v0.20.1 Registry Stack and authenticator pins + - name: Require published Registry Stack and authenticator pins run: scripts/check-registry-stack-release-pin.py --require-public - name: Checkout the exact Registry Stack source uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -47,24 +47,33 @@ jobs: ref: ${{ steps.registry-stack.outputs.source_commit }} path: vendor/registry-stack persist-credentials: false - - name: Verify Registry Stack source and published runtime artifacts + - name: Verify official Registry Stack runtime images and Relayctl run: | set -euo pipefail set -a source versions.env set +a test "$(git -C vendor/registry-stack rev-parse HEAD)" = "$REGISTRY_STACK_SOURCE_COMMIT" - relay_image="ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST}" - docker pull "$relay_image" - revision="$(docker image inspect "$relay_image" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}')" - test "$revision" = "$REGISTRY_STACK_SOURCE_COMMIT" - for component in evidence mint relayctl; do - url_variable="REGISTRY_STACK_RELEASE_${component^^}_ASSET_URL" - sha_variable="REGISTRY_STACK_RELEASE_${component^^}_ASSET_SHA256" - curl --fail --location --silent --show-error "${!url_variable}" --output "$component-v0.20.1" - printf '%s %s\n' "${!sha_variable}" "$component-v0.20.1" | sha256sum --check --status - rm -f "$component-v0.20.1" + for component in relay evidence mint; do + case "$component" in + relay) image="$REGISTRY_RELAY_IMAGE" ;; + evidence) image="$SOLMARA_EVIDENCE_IMAGE" ;; + mint) image="$SOLMARA_MINT_IMAGE" ;; + esac + expected_prefix="ghcr.io/registrystack/${component}@sha256:" + if ! printf '%s\n' "$image" | grep -Eq "^ghcr\\.io/registrystack/${component}@sha256:[0-9a-f]{64}$"; then + echo "$component must use ${expected_prefix}<64 lowercase hex>" >&2 + exit 1 + fi + docker pull "$image" + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}')" = "$REGISTRY_STACK_SOURCE_COMMIT" + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.version" }}')" = "$REGISTRY_STACK_REQUIRED_VERSION" + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.source" }}')" = https://github.com/registrystack/registry-stack done + relayctl="relayctl-v${REGISTRY_STACK_REQUIRED_VERSION}-linux-amd64" + curl --fail --location --silent --show-error "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" --output "$relayctl" + printf '%s %s\n' "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" "$relayctl" | sha256sum --check --status + rm -f "$relayctl" - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4.4.0 with: @@ -120,8 +129,6 @@ jobs: run: | set -euo pipefail for package in \ - solmara-lab-evidence \ - solmara-lab-mint \ solmara-lab-authority-provisioner \ solmara-lab-transit-signer; do visibility="$(gh api "/orgs/registrystack/packages/container/${package}" --jq .visibility)" @@ -130,54 +137,6 @@ jobs: exit 1 fi done - - name: Build and push verified Evidence release binary - id: evidence - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 - with: - context: . - file: docker/registry-stack-release-binary/Dockerfile - target: evidence - platforms: linux/amd64 - push: true - tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-evidence:${{ env.SOLMARA_IMAGE_TAG }} - labels: | - org.opencontainers.image.revision=${{ steps.registry-stack.outputs.source_commit }} - org.opencontainers.image.version=0.20.1 - org.opencontainers.image.source=https://github.com/registrystack/registry-stack - build-args: | - REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL=${{ env.REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL }} - REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256=${{ env.REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256 }} - - name: Build and push verified Mint release binary - id: mint - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 - with: - context: . - file: docker/registry-stack-release-binary/Dockerfile - target: mint - platforms: linux/amd64 - push: true - tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-mint:${{ env.SOLMARA_IMAGE_TAG }} - labels: | - org.opencontainers.image.revision=${{ steps.registry-stack.outputs.source_commit }} - org.opencontainers.image.version=0.20.1 - org.opencontainers.image.source=https://github.com/registrystack/registry-stack - build-args: | - REGISTRY_STACK_RELEASE_MINT_ASSET_URL=${{ env.REGISTRY_STACK_RELEASE_MINT_ASSET_URL }} - REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256=${{ env.REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256 }} - - name: Verify Solmara Evidence and Mint image source labels - run: | - set -euo pipefail - for component in evidence mint; do - case "$component" in - evidence) digest='${{ steps.evidence.outputs.digest }}' ;; - mint) digest='${{ steps.mint.outputs.digest }}' ;; - esac - image="$SOLMARA_IMAGE_REGISTRY/solmara-lab-${component}@${digest}" - docker pull "$image" - test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}')" = '${{ steps.registry-stack.outputs.source_commit }}' - test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.version" }}')" = 0.20.1 - test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.source" }}')" = https://github.com/registrystack/registry-stack - done - name: Build and push authority provisioner id: authority_provisioner uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 @@ -191,7 +150,7 @@ jobs: org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.version=${{ env.SOLMARA_IMAGE_TAG }} org.opencontainers.image.source=https://github.com/registrystack/solmara-lab - org.registrystack.release.version=0.20.1 + org.registrystack.release.version=${{ env.REGISTRY_STACK_REQUIRED_VERSION }} org.registrystack.release.revision=${{ steps.registry-stack.outputs.source_commit }} build-args: | REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=${{ env.REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL }} @@ -209,7 +168,7 @@ jobs: org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.version=${{ env.SOLMARA_IMAGE_TAG }} org.opencontainers.image.source=https://github.com/registrystack/solmara-lab - org.registrystack.release.version=0.20.1 + org.registrystack.release.version=${{ env.REGISTRY_STACK_REQUIRED_VERSION }} org.registrystack.release.revision=${{ steps.registry-stack.outputs.source_commit }} - name: Verify hosted authority image source labels run: | @@ -223,7 +182,7 @@ jobs: docker pull "$image" test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}')" = '${{ github.sha }}' test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.source" }}')" = https://github.com/registrystack/solmara-lab - test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.registrystack.release.version" }}')" = 0.20.1 + test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.registrystack.release.version" }}')" = "$REGISTRY_STACK_REQUIRED_VERSION" test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.registrystack.release.revision" }}')" = '${{ steps.registry-stack.outputs.source_commit }}' done - name: Smoke hosted authority provisioner image @@ -415,8 +374,9 @@ jobs: tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-esignet-seed:${{ env.SOLMARA_IMAGE_TAG }} - name: Generate Coolify image manifest env: - SOLMARA_EVIDENCE_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-evidence@${{ steps.evidence.outputs.digest }} - SOLMARA_MINT_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-mint@${{ steps.mint.outputs.digest }} + REGISTRY_RELAY_IMAGE: ${{ env.REGISTRY_RELAY_IMAGE }} + SOLMARA_EVIDENCE_IMAGE: ${{ env.SOLMARA_EVIDENCE_IMAGE }} + SOLMARA_MINT_IMAGE: ${{ env.SOLMARA_MINT_IMAGE }} SOLMARA_AUTHORITY_PROVISIONER_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-authority-provisioner@${{ steps.authority_provisioner.outputs.digest }} SOLMARA_TRANSIT_SIGNER_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-transit-signer@${{ steps.transit_signer.outputs.digest }} SOLMARA_STATIC_METADATA_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-static-metadata@${{ steps.static_metadata.outputs.digest }} diff --git a/README.md b/README.md index 6dabe37..c1c885d 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,18 @@ the optional eSignet UserInfo profile; NIA Evidence reads its own extract. Registry Stack v0.19.0 cannot run this topology. Its Mint cannot issue the Relay-compatible scope and purpose claims required by Relay V2, and its Evidence runtime cannot classify Relay's concealed `consultation.unresolved` -response as a data-free unresolved consultation. Registry Stack v0.20.1 is the -first immutable release containing both capabilities. Solmara pins its release -tag and source commit, the Relay OCI digest, and the Evidence, Mint, and -`relayctl` binary checksums in `versions.env`. +response as a data-free unresolved consultation. Registry Stack v0.21.0 is the +first coherent release that contains both capabilities and publishes official +Relay, Evidence, and Mint runtime images. Solmara pins its release tag and +source commit, those official OCI references by digest, and the `relayctl` +binary checksum in `versions.env`. The release handoff and every hosted +deployment consume those same full references without reconstructing them from +a second deployment input. The eSignet profile uses the separately released `esignet-relay-authenticator` v0.2.0 JAR and its matching SHA-256 checksum. No -source-build, floating-tag, or v0.19 compatibility fallback is accepted. +source-build, locally wrapped runtime, floating-tag, or v0.19 compatibility +fallback is accepted. ## Quick start diff --git a/compose.coolify.agriculture.yaml b/compose.coolify.agriculture.yaml index 677cf7d..f87a84a 100644 --- a/compose.coolify.agriculture.yaml +++ b/compose.coolify.agriculture.yaml @@ -27,7 +27,7 @@ services: nagdi-relay: <<: *runtime - image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + image: ${REGISTRY_RELAY_IMAGE:?set the digest-pinned official Registry Stack Relay image} command: [serve, --runtime, /etc/relay/nagdi/runtime.yaml] environment: {SOLMARA_RELAY_AUDIT_KEY: "${NAGDI_RELAY_AUDIT_KEY:?required}", SOLMARA_RELAY_CURSOR_KEY: "${NAGDI_RELAY_CURSOR_KEY:?required}"} volumes: [nagdi-relay-runtime:/etc/relay/nagdi:ro, nagdi-relay-source:/var/lib/relay/source:ro, nagdi-relay-audit:/var/lib/relay/audit] @@ -35,7 +35,7 @@ services: nagdi-evidence: <<: *runtime - image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/nagdi/runtime.yaml, serve] volumes: [nagdi-evidence-runtime:/etc/registry-evidence/nagdi:ro, nagdi-evidence-secrets:/run/secrets/registry-evidence/nagdi:ro, nagdi-evidence-transit:/run/registry-evidence/nagdi:ro, nagdi-evidence-audit:/var/lib/registry-evidence/nagdi/audit] networks: {runtime: {ipv4_address: 172.29.1.26}} diff --git a/compose.coolify.interior.yaml b/compose.coolify.interior.yaml index 2086ee1..24ae44a 100644 --- a/compose.coolify.interior.yaml +++ b/compose.coolify.interior.yaml @@ -27,7 +27,7 @@ services: cra-relay: <<: *runtime - image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + image: ${REGISTRY_RELAY_IMAGE:?set the digest-pinned official Registry Stack Relay image} command: [serve, --runtime, /etc/relay/cra/runtime.yaml] environment: {SOLMARA_RELAY_AUDIT_KEY: "${CRA_RELAY_AUDIT_KEY:?required}"} volumes: [cra-relay-runtime:/etc/relay/cra:ro, cra-relay-source:/var/lib/relay/source:ro, cra-relay-audit:/var/lib/relay/audit] @@ -35,7 +35,7 @@ services: nia-relay: <<: *runtime - image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + image: ${REGISTRY_RELAY_IMAGE:?set the digest-pinned official Registry Stack Relay image} command: [serve, --runtime, /etc/relay/nia/runtime.yaml] environment: {SOLMARA_RELAY_AUDIT_KEY: "${NIA_RELAY_AUDIT_KEY:?required}"} volumes: [nia-relay-runtime:/etc/relay/nia:ro, nia-relay-source:/var/lib/relay/source:ro, nia-relay-audit:/var/lib/relay/audit] @@ -43,7 +43,7 @@ services: cra-evidence: <<: *runtime - image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/cra/runtime.yaml, serve] volumes: [cra-evidence-runtime:/etc/registry-evidence/cra:ro, cra-evidence-secrets:/run/secrets/registry-evidence/cra:ro, cra-evidence-transit:/run/registry-evidence/cra:ro, cra-evidence-extracts:/var/lib/registry-evidence/cra/extracts:ro, cra-evidence-audit:/var/lib/registry-evidence/cra/audit] networks: {runtime: {ipv4_address: 172.29.1.21}} @@ -54,7 +54,7 @@ services: nia-evidence: <<: *runtime - image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/nia/runtime.yaml, serve] volumes: [nia-evidence-runtime:/etc/registry-evidence/nia:ro, nia-evidence-secrets:/run/secrets/registry-evidence/nia:ro, nia-evidence-transit:/run/registry-evidence/nia:ro, nia-evidence-extracts:/var/lib/registry-evidence/nia/extracts:ro, nia-evidence-audit:/var/lib/registry-evidence/nia/audit] networks: {runtime: {ipv4_address: 172.29.1.22}} diff --git a/compose.coolify.labour-pensions.yaml b/compose.coolify.labour-pensions.yaml index 133fbc7..a8c9d10 100644 --- a/compose.coolify.labour-pensions.yaml +++ b/compose.coolify.labour-pensions.yaml @@ -27,7 +27,7 @@ services: sipf-relay: <<: *runtime - image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + image: ${REGISTRY_RELAY_IMAGE:?set the digest-pinned official Registry Stack Relay image} command: [serve, --runtime, /etc/relay/sipf/runtime.yaml] environment: {SOLMARA_RELAY_AUDIT_KEY: "${SIPF_RELAY_AUDIT_KEY:?required}", SOLMARA_RELAY_CURSOR_KEY: "${SIPF_RELAY_CURSOR_KEY:?required}"} volumes: [sipf-relay-runtime:/etc/relay/sipf:ro, sipf-relay-source:/var/lib/relay/source:ro, sipf-relay-audit:/var/lib/relay/audit] @@ -35,7 +35,7 @@ services: sipf-evidence: <<: *runtime - image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/sipf/runtime.yaml, serve] volumes: [sipf-evidence-runtime:/etc/registry-evidence/sipf:ro, sipf-evidence-secrets:/run/secrets/registry-evidence/sipf:ro, sipf-evidence-transit:/run/registry-evidence/sipf:ro, sipf-evidence-audit:/var/lib/registry-evidence/sipf/audit] networks: {runtime: {ipv4_address: 172.29.1.25}} diff --git a/compose.coolify.social-development.yaml b/compose.coolify.social-development.yaml index 53a72a7..583c53d 100644 --- a/compose.coolify.social-development.yaml +++ b/compose.coolify.social-development.yaml @@ -27,7 +27,7 @@ services: mosd-relay: <<: *runtime - image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + image: ${REGISTRY_RELAY_IMAGE:?set the digest-pinned official Registry Stack Relay image} command: [serve, --runtime, /etc/relay/mosd/runtime.yaml] environment: {SOLMARA_RELAY_AUDIT_KEY: "${MOSD_RELAY_AUDIT_KEY:?required}"} volumes: [mosd-relay-runtime:/etc/relay/mosd:ro, mosd-relay-source:/var/lib/relay/source:ro, mosd-relay-audit:/var/lib/relay/audit] @@ -35,7 +35,7 @@ services: sro-evidence: <<: *runtime - image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/sro/runtime.yaml, serve] volumes: [sro-evidence-runtime:/etc/registry-evidence/sro:ro, sro-evidence-secrets:/run/secrets/registry-evidence/sro:ro, sro-evidence-transit:/run/registry-evidence/sro:ro, sro-evidence-extracts:/var/lib/registry-evidence/sro/extracts:ro, sro-evidence-audit:/var/lib/registry-evidence/sro/audit] networks: {runtime: {ipv4_address: 172.29.1.23}} @@ -43,7 +43,7 @@ services: mosd-programme-evidence: <<: *runtime - image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/mosd-programme/runtime.yaml, serve] volumes: [mosd-evidence-runtime:/etc/registry-evidence/mosd-programme:ro, mosd-evidence-secrets:/run/secrets/registry-evidence/mosd-programme:ro, mosd-evidence-transit:/run/registry-evidence/mosd-programme:ro, mosd-evidence-audit:/var/lib/registry-evidence/mosd-programme/audit] networks: {runtime: {ipv4_address: 172.29.1.24}} diff --git a/compose.coolify.yaml b/compose.coolify.yaml index acd1d07..ab80a60 100644 --- a/compose.coolify.yaml +++ b/compose.coolify.yaml @@ -27,7 +27,7 @@ services: security_opt: [no-new-privileges:true] volumes: [mint-audit:/audit/mint] mint: - image: ${SOLMARA_MINT_IMAGE:?set the digest-pinned Solmara Mint image built from the verified release binary} + image: ${SOLMARA_MINT_IMAGE:?set the digest-pinned official Registry Stack Mint image} command: ["serve", "--config", "/etc/registry-mint/mint.yaml"] restart: unless-stopped read_only: true diff --git a/compose.hosted.yaml b/compose.hosted.yaml index 6173072..8cec047 100644 --- a/compose.hosted.yaml +++ b/compose.hosted.yaml @@ -6,14 +6,14 @@ x-solmara-rollout: superseded-volume-policy: retain x-hosted-relay: &hosted-relay - image: ghcr.io/registrystack/relay@sha256:${REGISTRY_STACK_RELEASE_RELAY_DIGEST:?Registry Stack v0.20.1 Relay digest is not published; hosted promotion is blocked} + image: ${REGISTRY_RELAY_IMAGE:?set the digest-pinned official Registry Stack Relay image} build: !reset null labels: solmara.rollout.slot: authority-cells solmara.rollout.phase: ${SOLMARA_AUTHORITY_CELLS_PHASE:-side-by-side} x-hosted-evidence: &hosted-evidence - image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned Solmara Evidence image built from the verified release binary} + image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} build: !reset null labels: solmara.rollout.slot: authority-cells @@ -34,7 +34,7 @@ services: nagdi-evidence: {<<: *hosted-evidence} mint: - image: ${SOLMARA_MINT_IMAGE:?set the digest-pinned Solmara Mint image built from the verified release binary} + image: ${SOLMARA_MINT_IMAGE:?set the digest-pinned official Registry Stack Mint image} build: !reset null labels: solmara.rollout.slot: authority-cells diff --git a/compose.yaml b/compose.yaml index 8763343..80ec280 100644 --- a/compose.yaml +++ b/compose.yaml @@ -15,7 +15,7 @@ x-relay: &relay retries: 12 x-evidence: &evidence - image: ${SOLMARA_EVIDENCE_IMAGE:?versions.env must name the checksum-verified Evidence image} + image: ${SOLMARA_EVIDENCE_IMAGE:?versions.env must name the digest-pinned official Registry Stack Evidence image} platform: linux/amd64 restart: unless-stopped user: "0:0" @@ -272,7 +272,7 @@ services: ports: ["${SOLMARA_NAGDI_RELAY_PORT:-4316}:8080"] mint: - image: ${SOLMARA_MINT_IMAGE:?versions.env must name the checksum-verified Mint image} + image: ${SOLMARA_MINT_IMAGE:?versions.env must name the digest-pinned official Registry Stack Mint image} platform: linux/amd64 command: ["serve", "--config", "/etc/registry-mint/mint.yaml"] restart: unless-stopped @@ -351,7 +351,7 @@ services: # metadata destinations. TEST-NET-1 gives the hermetic local gateway a # non-routable issuer address without weakening that runtime policy. issuer-validation: - ipv4_address: 192.0.2.30 + ipv4_address: 192.0.2.62 aliases: [mint.solmara.registrystack.org] child-benefit-federator: @@ -457,4 +457,4 @@ networks: ipam: {config: [{subnet: 172.29.1.0/24}]} issuer-validation: internal: true - ipam: {config: [{subnet: 192.0.2.0/27, ip_range: 192.0.2.0/28}]} + ipam: {config: [{subnet: 192.0.2.0/26, ip_range: 192.0.2.0/27}]} diff --git a/docker/registry-stack-release-binary/Dockerfile b/docker/registry-stack-release-binary/Dockerfile index 977a1f0..6ddf53f 100644 --- a/docker/registry-stack-release-binary/Dockerfile +++ b/docker/registry-stack-release-binary/Dockerfile @@ -1,48 +1,5 @@ # syntax=docker/dockerfile:1.7 -FROM alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS evidence-download -ARG REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL -ARG REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256 -RUN test -n "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL" && \ - test -n "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256" && \ - { attempt=0; until wget -q -T 30 -O /evidence "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL"; do \ - attempt=$((attempt + 1)); [ "$attempt" -lt 5 ] || exit 1; sleep "$attempt"; \ - done; } && \ - printf '%s %s\n' "$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256" /evidence | sha256sum -c - && \ - chmod 0755 /evidence - -FROM alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS mint-download -ARG REGISTRY_STACK_RELEASE_MINT_ASSET_URL -ARG REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256 -RUN test -n "$REGISTRY_STACK_RELEASE_MINT_ASSET_URL" && \ - test -n "$REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256" && \ - { attempt=0; until wget -q -T 30 -O /mint "$REGISTRY_STACK_RELEASE_MINT_ASSET_URL"; do \ - attempt=$((attempt + 1)); [ "$attempt" -lt 5 ] || exit 1; sleep "$attempt"; \ - done; } && \ - printf '%s %s\n' "$REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256" /mint | sha256sum -c - && \ - chmod 0755 /mint - -FROM alpine@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS relayctl-download -ARG REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL -ARG REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256 -RUN test -n "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" && \ - test -n "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" && \ - { attempt=0; until wget -q -T 30 -O /relayctl "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL"; do \ - attempt=$((attempt + 1)); [ "$attempt" -lt 5 ] || exit 1; sleep "$attempt"; \ - done; } && \ - printf '%s %s\n' "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" /relayctl | sha256sum -c - && \ - chmod 0755 /relayctl - -FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS evidence -COPY --from=evidence-download --chown=65532:65532 /evidence /usr/local/bin/evidence -EXPOSE 8080 -ENTRYPOINT ["/usr/local/bin/evidence"] - -FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS mint -COPY --from=mint-download --chown=65532:65532 /mint /usr/local/bin/mint -EXPOSE 8081 -ENTRYPOINT ["/usr/local/bin/mint"] - FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS relayctl -COPY --from=relayctl-download --chown=65532:65532 /relayctl /usr/local/bin/relayctl +COPY --chown=65532:65532 --chmod=0755 relayctl /usr/local/bin/relayctl ENTRYPOINT ["/usr/local/bin/relayctl"] diff --git a/docker/registry-stack-runtime/Dockerfile b/docker/registry-stack-runtime/Dockerfile deleted file mode 100644 index ea142bb..0000000 --- a/docker/registry-stack-runtime/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# syntax=docker/dockerfile:1.7 - -FROM rust:1.95-trixie@sha256:f49565f188ee00bc2a18dd418183f2c5f23ef7d6e691890517ed341a598f67c3 AS relay-builder -WORKDIR /workspace -COPY . . -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/workspace/target \ - CARGO_INCREMENTAL=0 CARGO_PROFILE_RELEASE_DEBUG=0 \ - cargo build --release --locked -p registry-relay-v2 -p registry-relayctl \ - && install -D -m 0755 target/release/relay /runtime/usr/local/bin/relay \ - && install -D -m 0755 target/release/relayctl /tooling/usr/local/bin/relayctl \ - && install -D -m 0644 LICENSE /runtime/licenses/relay/LICENSE \ - && mkdir -p /runtime/etc/relay /runtime/var/lib/relay/audit /runtime/var/lib/relay/source \ - && chown -R 65532:65532 /runtime/etc/relay /runtime/var/lib/relay - -FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS relay -COPY --from=relay-builder /runtime/ / -WORKDIR /var/lib/relay -EXPOSE 8080 -ENV RELAY_HEALTHCHECK_URL=http://127.0.0.1:8080/health -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD ["/usr/local/bin/relay", "healthcheck"] -ENTRYPOINT ["/usr/local/bin/relay"] -CMD ["serve", "--runtime", "/etc/relay/runtime.yaml"] - -FROM debian:trixie-slim@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd AS relayctl -COPY --from=relay-builder /tooling/ / -ENTRYPOINT ["/usr/local/bin/relayctl"] diff --git a/docs/changelog.md b/docs/changelog.md index eb1996a..4626697 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,19 @@ A small dated log of what changed in the visitor center and the lab topology. Newest entry first. +## 2026-08-13 Official Registry Stack runtime images + +Solmara no longer assembles or publishes local Evidence and Mint runtime +wrappers. Relay, Evidence, and Mint are now closed to their official Registry +Stack v0.21.0 GHCR repositories and immutable digests from `versions.env`; +only the Relayctl authoring helper is assembled locally from a +checksum-verified release asset. The release-candidate handoff carries the +upstream Evidence and Mint +references unchanged and publishes only Solmara-owned deployment images. Relay +now follows the same closed handoff: hosted Compose consumes the canonical +`REGISTRY_RELAY_IMAGE` reference rather than rebuilding it from a separate +digest input. + ## 2026-08-12 Authority-owned Relay and Evidence reset Solmara now authors six independent Evidence cells and five Relay V2 projects. @@ -17,9 +30,11 @@ header, and authority decision-service topology. Mutable Relay publications and versioned immutable extracts now have deliberately different lifecycle proof. Registry Stack v0.19.0 was found incapable of issuing Relay-compatible Mint claims and of representing Relay's concealed unresolved outcome in Evidence -fixtures. The reset therefore fixes forward to Registry Stack v0.20.1, pinned -by exact release source, binary checksums, and Relay OCI digest. The separately -released `esignet-relay-authenticator` v0.2.0 JAR is pinned by SHA-256. +fixtures. v0.20.1 added those capabilities but did not publish official +Evidence and Mint images. The completed reset therefore pins Registry Stack +v0.21.0 by exact release source, Relayctl checksum, and official Relay, +Evidence, and Mint OCI digests. The separately released +`esignet-relay-authenticator` v0.2.0 JAR is pinned by SHA-256. ## 2026-08-06 Registry Stack main Evidence migration diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index 1463520..cae80fc 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -1,32 +1,41 @@ # Hosted deployment -Deploy the authority-cell reset only from the exact Registry Stack v0.20.1 -release and digest-pinned Solmara images. `versions.env` records the public -Registry Stack digests and release-asset checksums. A missing, floating, or -mismatched pin stops deployment. - -Registry Stack v0.20.0 remains immutable but does not contain the Evidence -capability required by this lab. The reset therefore fixes forward to the -published v0.20.1 artifacts. Do not move the v0.20.0 release or substitute a -floating source reference. +Deploy the authority-cell reset only from the exact Registry Stack release +recorded in `versions.env`. Relay, Evidence, and Mint must use their official +Registry Stack OCI images pinned by digest. Solmara-owned images are also +digest-pinned, while the local Relayctl helper is assembled from its +checksum-verified release asset. A missing, floating, or mismatched pin stops +deployment. + +The sanitized hosted image manifest carries `REGISTRY_RELAY_IMAGE`, +`SOLMARA_EVIDENCE_IMAGE`, and `SOLMARA_MINT_IMAGE` unchanged from +`versions.env`. Compose consumes those exact full references. The separate +Relay digest field remains release-verification evidence only and is not a +deployment input. + +Registry Stack v0.20.0 remains immutable and does not contain the Evidence +capability required by this lab. v0.20.1 contains the required runtime +capabilities but does not publish official Evidence and Mint OCI images. +Registry Stack v0.21.0 is the first coherent release with all three official +runtime images. Do not move a release, substitute a floating source reference, +or recreate those runtime images in Solmara. ## Release package precondition Before the first release-candidate build, an organization owner must provision -these four public, anonymously pullable GitHub Container Registry packages and +these two public, anonymously pullable GitHub Container Registry packages and grant the repository's GitHub Actions workflow write access: -- `ghcr.io/registrystack/solmara-lab-evidence` -- `ghcr.io/registrystack/solmara-lab-mint` - `ghcr.io/registrystack/solmara-lab-authority-provisioner` - `ghcr.io/registrystack/solmara-lab-transit-signer` -Evidence and Mint are thin Solmara-owned images assembled from the exact -checksum-verified Registry Stack v0.20.1 binaries. The authority provisioner -contains the reviewed contracts and deterministic publications. The Transit -signer contains only the signer runtime. Release handoff records the immutable -digest of each image. Do not reuse an unrelated package or deploy a mutable -tag. +The Registry Stack release owns the public `relay`, `evidence`, and `mint` +packages. Solmara only reads those upstream digest references from +`versions.env`; its workflow neither rebuilds nor republishes them. The +authority provisioner contains the reviewed contracts and deterministic +publications. The Transit signer contains only the signer runtime. Release +handoff records the immutable digest of every upstream and Solmara-owned image. +Do not reuse an unrelated package or deploy a mutable tag. ## Authority topology diff --git a/home/src/lib/components/EngineerDoor.svelte b/home/src/lib/components/EngineerDoor.svelte index 80e0d7d..9b369b0 100644 --- a/home/src/lib/components/EngineerDoor.svelte +++ b/home/src/lib/components/EngineerDoor.svelte @@ -32,7 +32,7 @@

    Pinned release artifacts

    -

    Registry Stack v0.20.1 {versions.REGISTRY_STACK_SOURCE_COMMIT || 'missing'}

    +

    Registry Stack {versions.REGISTRY_STACK_SOURCE_REF || 'missing'} {versions.REGISTRY_STACK_SOURCE_COMMIT || 'missing'}

    Evidence {versions.SOLMARA_EVIDENCE_IMAGE ?? 'missing'}

    Mint {versions.SOLMARA_MINT_IMAGE ?? 'missing'}

    diff --git a/home/src/lib/components/TrustStrip.svelte b/home/src/lib/components/TrustStrip.svelte index 7233ebf..1a56a41 100644 --- a/home/src/lib/components/TrustStrip.svelte +++ b/home/src/lib/components/TrustStrip.svelte @@ -45,7 +45,7 @@

    Pinned release

    -

    Registry Stack v0.20.1 {versions.REGISTRY_STACK_SOURCE_COMMIT?.slice(0, 12) || 'missing'}

    +

    Registry Stack {versions.REGISTRY_STACK_SOURCE_REF || 'missing'} {versions.REGISTRY_STACK_SOURCE_COMMIT?.slice(0, 12) || 'missing'}

    Evidence {shortDigest(versions.SOLMARA_EVIDENCE_IMAGE)}

    diff --git a/home/src/lib/server/data.test.ts b/home/src/lib/server/data.test.ts index c12bf46..8946cd4 100644 --- a/home/src/lib/server/data.test.ts +++ b/home/src/lib/server/data.test.ts @@ -18,11 +18,12 @@ describe('home data adapters', () => { it('publishes the exact Registry Stack release identity', async () => { const versions = await readVersions(); - expect(versions.REGISTRY_STACK_SOURCE_REF).toBe('v0.20.1'); + expect(versions.REGISTRY_STACK_REQUIRED_VERSION).toBe('0.21.0'); + expect(versions.REGISTRY_STACK_SOURCE_REF).toBe('v0.21.0'); expect(versions.REGISTRY_STACK_SOURCE_COMMIT).toMatch(/^[0-9a-f]{40}$/); expect(versions.REGISTRY_RELAY_IMAGE).toMatch(/@sha256:[0-9a-f]{64}$/); - expect(versions.SOLMARA_EVIDENCE_IMAGE).toContain(':v0.20.1'); - expect(versions.SOLMARA_MINT_IMAGE).toContain(':v0.20.1'); + expect(versions.SOLMARA_EVIDENCE_IMAGE).toMatch(/^ghcr\.io\/registrystack\/evidence@sha256:[0-9a-f]{64}$/); + expect(versions.SOLMARA_MINT_IMAGE).toMatch(/^ghcr\.io\/registrystack\/mint@sha256:[0-9a-f]{64}$/); }); it('keeps compose-internal health probes out of visitor links', async () => { diff --git a/justfile b/justfile index 679674a..4b381f7 100644 --- a/justfile +++ b/justfile @@ -41,6 +41,7 @@ lint: uvx ruff check --select E4,E7,E9,F . scripts/check-fiction.sh scripts/check-config-secrets.py + scripts/check-image-pins.py scripts/hosted-image-manifest.py inventory scripts/check-runtime-topology.py scripts/check-registry-stack-release-pin.py @@ -53,7 +54,7 @@ lint: test: cd generator && uv run python -m unittest discover -s tests uv run python -m unittest discover -s scenario-runner -p 'test_*.py' - uv run python -m unittest relays/test_relay_projects.py evidence/tests/test_cells.py scripts/test_metadata_authority_contracts.py scripts/test_image_pins.py scripts/test_hosted_image_manifest.py scripts/test_hosted_provisioning_topology.py scripts/test_hosted_home_topology.py scripts/test_hosted_runtime_assets.py scripts/test_provision_hosted_runtime.py scripts/test_hosted_transit_signer.py scripts/test_smoke_hosted_provisioner_image.py scripts/test_runtime_topology.py scripts/test_registry_stack_release_pin.py scripts/test_hosted_authority_rollout.py scripts/test_local_transit_proxy.py scripts/test_local_transit_signers.py scripts/test_local_transit_providers.py scripts/test_signer_public_keys.py scripts/test_project_runtime_secrets.py scripts/test_gen_secrets.py scripts/test_publish_runtime_extracts.py scripts/test_lifecycle_proof.py scripts/test_live_lifecycle_proof.py scripts/test_local_relay_source_publisher.py scripts/test_smoke_programme_acceptance.py scripts/test_smoke_esignet.py + uv run python -m unittest relays/test_relay_projects.py evidence/tests/test_cells.py scripts/test_metadata_authority_contracts.py scripts/test_image_pins.py scripts/test_build_registry_stack_runtime.py scripts/test_hosted_image_manifest.py scripts/test_hosted_provisioning_topology.py scripts/test_hosted_home_topology.py scripts/test_hosted_runtime_assets.py scripts/test_provision_hosted_runtime.py scripts/test_hosted_transit_signer.py scripts/test_smoke_hosted_provisioner_image.py scripts/test_runtime_topology.py scripts/test_registry_stack_release_pin.py scripts/test_hosted_authority_rollout.py scripts/test_local_transit_proxy.py scripts/test_local_transit_signers.py scripts/test_local_transit_providers.py scripts/test_signer_public_keys.py scripts/test_project_runtime_secrets.py scripts/test_gen_secrets.py scripts/test_publish_runtime_extracts.py scripts/test_lifecycle_proof.py scripts/test_live_lifecycle_proof.py scripts/test_local_relay_source_publisher.py scripts/test_smoke_programme_acceptance.py scripts/test_smoke_esignet.py cd portal && pnpm test cd home && pnpm test @@ -65,8 +66,9 @@ compose: @scripts/check-hosted-compose.sh @scripts/check-coolify-compose.sh -# Verify that every Registry Stack v0.20.1 release reference is public and immutable. -hosted-pin-check: +# Verify that every Registry Stack v0.21.0 release reference is public and immutable. +hosted-pin-check: build-runtime-images + scripts/check-image-pins.py scripts/check-registry-stack-release-pin.py --require-public # Run all four relayctl production gates against the five authored projects. diff --git a/scripts/build-registry-stack-runtime.sh b/scripts/build-registry-stack-runtime.sh index 52d6a5a..3abb42a 100755 --- a/scripts/build-registry-stack-runtime.sh +++ b/scripts/build-registry-stack-runtime.sh @@ -23,11 +23,6 @@ if [ "$relay_image" != "$expected_relay" ]; then echo "REGISTRY_RELAY_IMAGE must bind the published Relay digest" >&2 exit 1 fi -observed_relay=$(docker buildx imagetools inspect "ghcr.io/registrystack/relay:v$version" --format '{{.Manifest.Digest}}') -if [ "$observed_relay" != "sha256:$relay_digest" ]; then - echo "published Relay tag does not match the required digest" >&2 - exit 1 -fi registry_stack_platform=${REGISTRY_STACK_PLATFORM:-linux/amd64} if [ "$registry_stack_platform" != "linux/amd64" ]; then @@ -36,34 +31,78 @@ if [ "$registry_stack_platform" != "linux/amd64" ]; then fi platform_args="--platform $registry_stack_platform" -build_release_binary() { - target=$1 +verify_official_runtime() { + component=$1 image=$2 + expected_prefix="ghcr.io/registrystack/$component@sha256:" + if ! printf '%s\n' "$image" | grep -Eq "^ghcr\\.io/registrystack/${component}@sha256:[0-9a-f]{64}$"; then + echo "${component} runtime must use ${expected_prefix}<64 lowercase hex>" >&2 + exit 1 + fi + + pinned_digest=${image##*@} + published_digest=$( + docker buildx imagetools inspect \ + "ghcr.io/registrystack/${component}:v$version" \ + --format '{{.Manifest.Digest}}' + ) + if [ "$published_digest" != "$pinned_digest" ]; then + echo "published ${component} tag does not match the pinned digest" >&2 + exit 1 + fi + + docker pull "$image" >/dev/null + revision=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$image") + image_version=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$image") + source=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.source" }}' "$image") + if [ "$revision" != "$source_commit" ] || [ "$image_version" != "$version" ] || [ "$source" != "https://github.com/registrystack/registry-stack" ]; then + echo "${component} runtime labels do not match the pinned Registry Stack release" >&2 + exit 1 + fi +} + +verify_official_runtime relay "$relay_image" +verify_official_runtime evidence "$evidence_image" +verify_official_runtime mint "$mint_image" + +build_relayctl() { + image=$1 current_revision=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$image" 2>/dev/null || true) current_version=$(docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$image" 2>/dev/null || true) current_architecture=$(docker image inspect --format '{{.Architecture}}' "$image" 2>/dev/null || true) if [ "$current_revision" = "$source_commit" ] && [ "$current_version" = "$version" ] && [ "$current_architecture" = "amd64" ]; then - echo "$target image already matches Registry Stack v$version" + echo "relayctl image already matches Registry Stack v$version" return fi + relayctl_context=$(mktemp -d) + trap 'rm -rf -- "$relayctl_context"' EXIT HUP INT TERM + curl --fail --location --silent --show-error \ + --retry 5 --retry-all-errors --connect-timeout 30 \ + --output "$relayctl_context/relayctl" \ + "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" + if [ -L "$relayctl_context/relayctl" ] || [ ! -f "$relayctl_context/relayctl" ]; then + echo "downloaded Relayctl asset must be a regular file" >&2 + exit 1 + fi + relayctl_sha256=$(sha256sum "$relayctl_context/relayctl" | awk '{print $1}') + if [ "$relayctl_sha256" != "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" ]; then + echo "downloaded Relayctl asset checksum does not match the release pin" >&2 + exit 1 + fi + chmod 0755 "$relayctl_context/relayctl" + # shellcheck disable=SC2086 docker buildx build --load $platform_args \ - --build-arg "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL=$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL" \ - --build-arg "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256=$REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256" \ - --build-arg "REGISTRY_STACK_RELEASE_MINT_ASSET_URL=$REGISTRY_STACK_RELEASE_MINT_ASSET_URL" \ - --build-arg "REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256=$REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256" \ - --build-arg "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" \ - --build-arg "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256=$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" \ --label "org.opencontainers.image.source=https://github.com/registrystack/registry-stack" \ --label "org.opencontainers.image.revision=$source_commit" \ --label "org.opencontainers.image.version=$version" \ --tag "$image" \ --file "$root/docker/registry-stack-release-binary/Dockerfile" \ - --target "$target" \ - "$root" + --target relayctl \ + "$relayctl_context" + rm -rf -- "$relayctl_context" + trap - EXIT HUP INT TERM } -build_release_binary evidence "$evidence_image" -build_release_binary mint "$mint_image" -build_release_binary relayctl "$relayctl_image" +build_relayctl "$relayctl_image" diff --git a/scripts/check-coolify-compose.sh b/scripts/check-coolify-compose.sh index ea5f484..dd7db36 100755 --- a/scripts/check-coolify-compose.sh +++ b/scripts/check-coolify-compose.sh @@ -6,7 +6,7 @@ test_digest=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa test_image="ghcr.io/registrystack/solmara-test@sha256:$test_digest" check_compose() { - REGISTRY_STACK_RELEASE_RELAY_DIGEST="$test_digest" \ + REGISTRY_RELAY_IMAGE="ghcr.io/registrystack/relay@sha256:$test_digest" \ SOLMARA_EVIDENCE_IMAGE="$test_image" \ SOLMARA_MINT_IMAGE="$test_image" \ SOLMARA_STATIC_METADATA_IMAGE="$test_image" \ diff --git a/scripts/check-hosted-compose.sh b/scripts/check-hosted-compose.sh index 370cedb..d7cc2aa 100755 --- a/scripts/check-hosted-compose.sh +++ b/scripts/check-hosted-compose.sh @@ -5,7 +5,7 @@ root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) test_digest=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa test_image="ghcr.io/registrystack/solmara-compose-check@sha256:$test_digest" -REGISTRY_STACK_RELEASE_RELAY_DIGEST="$test_digest" \ +REGISTRY_RELAY_IMAGE="ghcr.io/registrystack/relay@sha256:$test_digest" \ SOLMARA_EVIDENCE_IMAGE="$test_image" \ SOLMARA_MINT_IMAGE="$test_image" \ SOLMARA_STATIC_METADATA_IMAGE="$test_image" \ diff --git a/scripts/check-image-pins.py b/scripts/check-image-pins.py index 5fe284a..e0fb5b6 100755 --- a/scripts/check-image-pins.py +++ b/scripts/check-image-pins.py @@ -10,11 +10,12 @@ ROOT = Path(__file__).resolve().parents[1] LINE_RE = re.compile(r"^([A-Z0-9_]+)=([^#\s]*)$") PIN_RE = re.compile(r"^[^#\s]+@sha256:[0-9a-f]{64}$") -SOURCE_IMAGE_KEYS = { - "REGISTRY_RELAY_IMAGE", - "SOLMARA_EVIDENCE_IMAGE", - "SOLMARA_MINT_IMAGE", +OFFICIAL_RUNTIME_REPOSITORIES = { + "REGISTRY_RELAY_IMAGE": "relay", + "SOLMARA_EVIDENCE_IMAGE": "evidence", + "SOLMARA_MINT_IMAGE": "mint", } +SOURCE_IMAGE_KEYS = set(OFFICIAL_RUNTIME_REPOSITORIES) PINNED_IMAGE_KEYS = { "VOLUME_INIT_IMAGE", "EVIDENCE_GATEWAY_IMAGE", "PYTHON_STATIC_IMAGE", "NODE_BUILD_IMAGE", "UV_BUILD_IMAGE", @@ -41,6 +42,14 @@ def main() -> int: continue key, value = match.groups() values[key] = value + if key in OFFICIAL_RUNTIME_REPOSITORIES: + repository = OFFICIAL_RUNTIME_REPOSITORIES[key] + expected = f"ghcr.io/registrystack/{repository}@sha256:" + if not value.startswith(expected) or not PIN_RE.match(value): + failures.append( + f"versions.env:{line_no}: {key} must use " + f"{expected}<64 lowercase hex>" + ) if key in PINNED_IMAGE_KEYS and not PIN_RE.match(value): failures.append(f"versions.env:{line_no}: {key} must use image@sha256:<64 hex>") if "@latest" in value or ":latest" in value: @@ -65,14 +74,6 @@ def main() -> int: if count == 0: failures.append(f"compose files: expected a required {key} reference") - for deployment in (ROOT / "compose.hosted.yaml", *sorted(ROOT.glob("compose.coolify*.yaml"))): - if not deployment.exists(): - continue - text = deployment.read_text(encoding="utf-8") - for nonexistent in ("ghcr.io/registrystack/evidence", "ghcr.io/registrystack/mint"): - if nonexistent in text: - failures.append(f"{deployment.name}: {nonexistent} is not a published Registry Stack image") - if failures: for failure in failures: print(failure, file=sys.stderr) diff --git a/scripts/check-registry-stack-release-pin.py b/scripts/check-registry-stack-release-pin.py index 49f1f77..a4e783e 100755 --- a/scripts/check-registry-stack-release-pin.py +++ b/scripts/check-registry-stack-release-pin.py @@ -10,7 +10,7 @@ ROOT = Path(__file__).resolve().parents[1] DIGEST = re.compile(r"^[0-9a-f]{64}$") HTTPS = re.compile(r"^https://[^\s]+$") -REQUIRED_VERSION = "0.20.1" +REQUIRED_VERSION = "0.21.0" def read_versions(path: Path) -> dict[str, str]: @@ -39,8 +39,6 @@ def validate(values: dict[str, str], *, require_public: bool) -> list[str]: failures.append(f"REGISTRY_STACK_SOURCE_COMMIT is not final; v{REQUIRED_VERSION} promotion is blocked") for key in ( "REGISTRY_STACK_RELEASE_RELAY_DIGEST", - "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256", - "REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256", "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256", ): value = values.get(key, "") @@ -49,8 +47,6 @@ def validate(values: dict[str, str], *, require_public: bool) -> list[str]: if require_public and not value: failures.append(f"{key} is not published; v{REQUIRED_VERSION} promotion is blocked") asset_names = { - "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL": f"evidence-v{REQUIRED_VERSION}-linux-amd64", - "REGISTRY_STACK_RELEASE_MINT_ASSET_URL": f"mint-v{REQUIRED_VERSION}-linux-amd64", "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL": f"relayctl-v{REQUIRED_VERSION}-linux-amd64", } release_base = f"https://github.com/registrystack/registry-stack/releases/download/v{REQUIRED_VERSION}/" diff --git a/scripts/hosted-image-manifest.py b/scripts/hosted-image-manifest.py index 6bd1a91..32bfbc5 100755 --- a/scripts/hosted-image-manifest.py +++ b/scripts/hosted-image-manifest.py @@ -13,8 +13,9 @@ ROOT = Path(__file__).resolve().parents[1] IMAGE_REPOSITORIES = ( - ("SOLMARA_EVIDENCE_IMAGE", "solmara-lab-evidence"), - ("SOLMARA_MINT_IMAGE", "solmara-lab-mint"), + ("REGISTRY_RELAY_IMAGE", "relay"), + ("SOLMARA_EVIDENCE_IMAGE", "evidence"), + ("SOLMARA_MINT_IMAGE", "mint"), ( "SOLMARA_AUTHORITY_PROVISIONER_IMAGE", "solmara-lab-authority-provisioner", @@ -33,7 +34,7 @@ EXPECTED_REPOSITORIES = dict(IMAGE_REPOSITORIES) MANIFEST_LINE_RE = re.compile(r"^([A-Z][A-Z0-9_]*)=([^\s#]+)$") HOSTED_IMAGE_LINE_RE = re.compile( - r"^\s*image:\s*\$\{(SOLMARA_[A-Z0-9_]+_IMAGE):\?[^}]+\}\s*(?:#.*)?$" + r"^\s*image:\s*\$\{(REGISTRY_RELAY_IMAGE|SOLMARA_[A-Z0-9_]+_IMAGE):\?[^}]+\}\s*(?:#.*)?$" ) DIGEST_RE = re.compile(r"^[0-9a-f]{64}$") @@ -119,7 +120,10 @@ def validate_hosted_compose_inventory(root: Path) -> None: malformed: list[str] = [] for path in paths: for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): - if "image:" not in line or "${SOLMARA_" not in line: + if "image:" not in line or not any( + marker in line + for marker in ("${REGISTRY_RELAY_IMAGE", "${SOLMARA_") + ): continue match = HOSTED_IMAGE_LINE_RE.fullmatch(line) if match is None: @@ -129,7 +133,7 @@ def validate_hosted_compose_inventory(root: Path) -> None: if malformed: raise ManifestError( - "hosted SOLMARA image references must be required variables: " + "hosted image references must be required variables: " + ", ".join(malformed) ) diff --git a/scripts/test_build_registry_stack_runtime.py b/scripts/test_build_registry_stack_runtime.py new file mode 100644 index 0000000..56183f8 --- /dev/null +++ b/scripts/test_build_registry_stack_runtime.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +import hashlib +import os +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +BUILDER = ROOT / "scripts" / "build-registry-stack-runtime.sh" + + +class RegistryStackRuntimeBuilderTests(unittest.TestCase): + def setUp(self) -> None: + self.directory = tempfile.TemporaryDirectory() + self.root = Path(self.directory.name) + scripts = self.root / "scripts" + binaries = self.root / "bin" + scripts.mkdir() + binaries.mkdir() + shutil.copy2(BUILDER, scripts / BUILDER.name) + + release_check = scripts / "check-registry-stack-release-pin.py" + release_check.write_text("#!/usr/bin/env sh\nexit 0\n", encoding="utf-8") + release_check.chmod(0o755) + + self.digests = { + "RELAY": "1" * 64, + "EVIDENCE": "2" * 64, + "MINT": "3" * 64, + } + self.relayctl_asset = self.root / "relayctl-release-asset" + self.relayctl_asset.write_bytes(b"published relayctl fixture") + relayctl_sha256 = hashlib.sha256(self.relayctl_asset.read_bytes()).hexdigest() + (self.root / "versions.env").write_text( + "REGISTRY_STACK_REQUIRED_VERSION=0.21.0\n" + "REGISTRY_STACK_SOURCE_COMMIT=" + "a" * 40 + "\n" + f"REGISTRY_STACK_RELEASE_RELAY_DIGEST={self.digests['RELAY']}\n" + f"REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/relay@sha256:{self.digests['RELAY']}\n" + f"SOLMARA_EVIDENCE_IMAGE=ghcr.io/registrystack/evidence@sha256:{self.digests['EVIDENCE']}\n" + f"SOLMARA_MINT_IMAGE=ghcr.io/registrystack/mint@sha256:{self.digests['MINT']}\n" + "REGISTRY_RELAYCTL_IMAGE=solmara-lab-relayctl:v0.21.0\n" + "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=https://example.invalid/relayctl\n" + f"REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256={relayctl_sha256}\n", + encoding="utf-8", + ) + + self.log = self.root / "docker.log" + docker = binaries / "docker" + docker.write_text( + """#!/usr/bin/env python3 +import os +import sys +from pathlib import Path + +arguments = sys.argv[1:] +with Path(os.environ["FAKE_DOCKER_LOG"]).open("a", encoding="utf-8") as log: + log.write(" ".join(arguments) + "\\n") + +if arguments[:3] == ["buildx", "imagetools", "inspect"]: + component = arguments[3].split("/")[-1].split(":", 1)[0].upper() + print(os.environ[f"TAG_DIGEST_{component}"]) +elif arguments[:2] == ["image", "inspect"]: + if os.environ.get("FORCE_RELAYCTL_BUILD") == "1" and arguments[-1].startswith("solmara-lab-relayctl:"): + raise SystemExit(0) + output_format = arguments[arguments.index("--format") + 1] + if ".Architecture" in output_format: + print("amd64") + elif "org.opencontainers.image.revision" in output_format: + print("a" * 40) + elif "org.opencontainers.image.version" in output_format: + print("0.21.0") + elif "org.opencontainers.image.source" in output_format: + print("https://github.com/registrystack/registry-stack") +elif arguments[0] == "pull": + pass +elif arguments[:2] == ["buildx", "build"]: + if os.environ.get("FORCE_RELAYCTL_BUILD") != "1": + raise SystemExit("relayctl should have been satisfied by the cached image") +else: + raise SystemExit(f"unexpected docker invocation: {arguments}") +""", + encoding="utf-8", + ) + docker.chmod(0o755) + curl = binaries / "curl" + curl.write_text( + """#!/usr/bin/env python3 +import os +import shutil +import sys + +arguments = sys.argv[1:] +output = arguments[arguments.index("--output") + 1] +shutil.copyfile(os.environ["FAKE_RELAYCTL_SOURCE"], output) +""", + encoding="utf-8", + ) + curl.chmod(0o755) + self.environment = { + **os.environ, + "PATH": f"{binaries}:{os.environ['PATH']}", + "FAKE_DOCKER_LOG": str(self.log), + "FAKE_RELAYCTL_SOURCE": str(self.relayctl_asset), + **{ + f"TAG_DIGEST_{component}": f"sha256:{digest}" + for component, digest in self.digests.items() + }, + } + + def tearDown(self) -> None: + self.directory.cleanup() + + def run_builder(self) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["sh", str(self.root / "scripts" / BUILDER.name)], + cwd=self.root, + env=self.environment, + check=False, + capture_output=True, + text=True, + ) + + def test_all_official_version_tags_match_their_pinned_digests(self) -> None: + result = self.run_builder() + + self.assertEqual(result.returncode, 0, result.stderr) + calls = self.log.read_text(encoding="utf-8") + for component in ("relay", "evidence", "mint"): + self.assertIn( + f"buildx imagetools inspect ghcr.io/registrystack/{component}:v0.21.0", + calls, + ) + + def test_mismatched_official_tag_digest_fails_closed(self) -> None: + self.environment["TAG_DIGEST_EVIDENCE"] = "sha256:" + "9" * 64 + + result = self.run_builder() + + self.assertEqual(result.returncode, 1) + self.assertIn( + "published evidence tag does not match the pinned digest", + result.stderr, + ) + calls = self.log.read_text(encoding="utf-8") + self.assertNotIn("ghcr.io/registrystack/mint:v0.21.0", calls) + + def test_relayctl_is_verified_on_the_host_before_the_minimal_image_build(self) -> None: + self.environment["FORCE_RELAYCTL_BUILD"] = "1" + + result = self.run_builder() + + self.assertEqual(result.returncode, 0, result.stderr) + calls = self.log.read_text(encoding="utf-8") + self.assertIn("buildx build --load --platform linux/amd64", calls) + self.assertIn("--target relayctl", calls) + self.assertNotIn("REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL", calls) + self.assertNotIn("REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256", calls) diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index 3fcc827..95eb53c 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -14,18 +14,19 @@ ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "scripts" / "hosted-image-manifest.py" DIGESTS = { - "SOLMARA_EVIDENCE_IMAGE": "1" * 64, - "SOLMARA_MINT_IMAGE": "2" * 64, - "SOLMARA_AUTHORITY_PROVISIONER_IMAGE": "3" * 64, - "SOLMARA_TRANSIT_SIGNER_IMAGE": "4" * 64, - "SOLMARA_STATIC_METADATA_IMAGE": "5" * 64, - "SOLMARA_SCENARIO_RUNNER_IMAGE": "6" * 64, - "SOLMARA_HOME_IMAGE": "7" * 64, - "SOLMARA_PORTAL_IMAGE": "8" * 64, - "SOLMARA_ESIGNET_RELAY_IMAGE": "9" * 64, - "SOLMARA_ESIGNET_POSTGRES_IMAGE": "a" * 64, - "SOLMARA_ESIGNET_UI_IMAGE": "b" * 64, - "SOLMARA_ESIGNET_SEED_IMAGE": "c" * 64, + "REGISTRY_RELAY_IMAGE": "1" * 64, + "SOLMARA_EVIDENCE_IMAGE": "2" * 64, + "SOLMARA_MINT_IMAGE": "3" * 64, + "SOLMARA_AUTHORITY_PROVISIONER_IMAGE": "4" * 64, + "SOLMARA_TRANSIT_SIGNER_IMAGE": "5" * 64, + "SOLMARA_STATIC_METADATA_IMAGE": "6" * 64, + "SOLMARA_SCENARIO_RUNNER_IMAGE": "7" * 64, + "SOLMARA_HOME_IMAGE": "8" * 64, + "SOLMARA_PORTAL_IMAGE": "9" * 64, + "SOLMARA_ESIGNET_RELAY_IMAGE": "a" * 64, + "SOLMARA_ESIGNET_POSTGRES_IMAGE": "b" * 64, + "SOLMARA_ESIGNET_UI_IMAGE": "c" * 64, + "SOLMARA_ESIGNET_SEED_IMAGE": "d" * 64, } @@ -111,11 +112,12 @@ def test_write_is_canonical_deterministic_and_sanitized(self) -> None: def test_every_image_must_use_its_exact_ghcr_repository_and_digest(self) -> None: invalid_values = ( - "ghcr.io/registrystack/solmara-lab-evidence:candidate", - f"docker.io/registrystack/solmara-lab-evidence@sha256:{'b' * 64}", - f"ghcr.io/registrystack/solmara-lab-mint@sha256:{'b' * 64}", - f"ghcr.io/registrystack/solmara-lab-evidence@sha256:{'B' * 64}", - f"ghcr.io/registrystack/solmara-lab-evidence@sha256:{'b' * 63}", + "ghcr.io/registrystack/evidence:candidate", + f"docker.io/registrystack/evidence@sha256:{'b' * 64}", + f"ghcr.io/registrystack/mint@sha256:{'b' * 64}", + f"ghcr.io/registrystack/solmara-lab-evidence@sha256:{'b' * 64}", + f"ghcr.io/registrystack/evidence@sha256:{'B' * 64}", + f"ghcr.io/registrystack/evidence@sha256:{'b' * 63}", ) for invalid in invalid_values: with self.subTest(invalid=invalid): @@ -126,6 +128,21 @@ def test_every_image_must_use_its_exact_ghcr_repository_and_digest(self) -> None self.assertEqual(result, 1) self.assertIn("SOLMARA_EVIDENCE_IMAGE must", stderr) + def test_relay_must_use_the_canonical_official_reference(self) -> None: + invalid = ( + "ghcr.io/registrystack/relay:v0.21.0", + f"ghcr.io/registrystack/solmara-lab-relay@sha256:{'1' * 64}", + f"ghcr.io/registrystack/relay@sha256:{'A' * 64}", + ) + for value in invalid: + with self.subTest(value=value): + environment = {**self.environment, "REGISTRY_RELAY_IMAGE": value} + result, stderr = self.invoke( + "write", "--output", str(self.manifest), environment=environment + ) + self.assertEqual(result, 1) + self.assertIn("REGISTRY_RELAY_IMAGE must", stderr) + def test_missing_image_fails_without_writing_a_partial_manifest(self) -> None: environment = dict(self.environment) del environment["SOLMARA_PORTAL_IMAGE"] @@ -199,6 +216,25 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8")) steps = workflow["jobs"]["verify-and-publish"]["steps"] names = [step.get("name") for step in steps] + self.assertNotIn("Build and push verified Evidence release binary", names) + self.assertNotIn("Build and push verified Mint release binary", names) + self.assertNotIn("Verify Solmara Evidence and Mint image source labels", names) + read_pins = next( + step + for step in steps + if step.get("name") == "Read immutable Registry Stack source identity" + )["run"] + self.assertIn("SOLMARA_EVIDENCE_IMAGE SOLMARA_MINT_IMAGE", read_pins) + runtime_verification = next( + step + for step in steps + if step.get("name") + == "Verify official Registry Stack runtime images and Relayctl" + )["run"] + self.assertIn("for component in relay evidence mint", runtime_verification) + self.assertIn("REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256", runtime_verification) + self.assertNotIn("REGISTRY_STACK_RELEASE_EVIDENCE_ASSET", runtime_verification) + self.assertNotIn("REGISTRY_STACK_RELEASE_MINT_ASSET", runtime_verification) provisioner_index = names.index("Build and push authority provisioner") signer_index = names.index("Build and push Transit signer") generate_index = names.index("Generate Coolify image manifest") @@ -285,11 +321,25 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( )["run"] self.assertIn("solmara-lab-authority-provisioner", packages) self.assertIn("solmara-lab-transit-signer", packages) + self.assertNotIn("solmara-lab-evidence", packages) + self.assertNotIn("solmara-lab-mint", packages) generate = steps[generate_index] self.assertIn("hosted-image-manifest.py write", generate["run"]) self.assertIn("hosted-image-manifest.py validate", generate["run"]) self.assertEqual(set(generate["env"]), set(self.module.EXPECTED_KEYS)) + self.assertEqual( + generate["env"]["REGISTRY_RELAY_IMAGE"], + "${{ env.REGISTRY_RELAY_IMAGE }}", + ) + self.assertEqual( + generate["env"]["SOLMARA_EVIDENCE_IMAGE"], + "${{ env.SOLMARA_EVIDENCE_IMAGE }}", + ) + self.assertEqual( + generate["env"]["SOLMARA_MINT_IMAGE"], + "${{ env.SOLMARA_MINT_IMAGE }}", + ) self.assertEqual( generate["env"]["SOLMARA_AUTHORITY_PROVISIONER_IMAGE"], "${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-authority-provisioner@${{ steps.authority_provisioner.outputs.digest }}", diff --git a/scripts/test_image_pins.py b/scripts/test_image_pins.py index 7466025..c4d6d74 100644 --- a/scripts/test_image_pins.py +++ b/scripts/test_image_pins.py @@ -10,9 +10,9 @@ ROOT = Path(__file__).resolve().parents[1] -RELAY = "solmara-lab-registry-relay:source" -EVIDENCE = "solmara-lab-registry-evidence:source" -MINT = "solmara-lab-registry-mint:source" +RELAY = "ghcr.io/registrystack/relay@sha256:" + "1" * 64 +EVIDENCE = "ghcr.io/registrystack/evidence@sha256:" + "2" * 64 +MINT = "ghcr.io/registrystack/mint@sha256:" + "3" * 64 VOLUME_INIT = "busybox@sha256:" + "4" * 64 GATEWAY = "caddy@sha256:" + "5" * 64 @@ -87,17 +87,45 @@ def test_gateway_must_be_digest_pinned(self) -> None: self.assertEqual(result, 1) self.assertIn("EVIDENCE_GATEWAY_IMAGE must use image@sha256", stderr) - def test_nonexistent_upstream_evidence_image_is_rejected(self) -> None: - self.run_check() - (self.root / "compose.hosted.yaml").write_text( - "services:\n evidence:\n image: ghcr.io/registrystack/evidence@sha256:" + "a" * 64 + "\n", - encoding="utf-8", + def test_runtime_images_must_use_their_exact_official_repository(self) -> None: + versions = (self.root / "versions.env").read_text().replace( + EVIDENCE, + "ghcr.io/registrystack/solmara-lab-evidence@sha256:" + "2" * 64, ) - stderr = io.StringIO() - with contextlib.redirect_stderr(stderr): - result = self.module.main() + (self.root / "versions.env").write_text(versions) + result, stderr = self.run_check() + self.assertEqual(result, 1) - self.assertIn("is not a published Registry Stack image", stderr.getvalue()) + self.assertIn( + "SOLMARA_EVIDENCE_IMAGE must use " + "ghcr.io/registrystack/evidence@sha256:", + stderr, + ) + + def test_runtime_builder_only_builds_checksum_verified_relayctl(self) -> None: + builder = (ROOT / "scripts" / "build-registry-stack-runtime.sh").read_text( + encoding="utf-8" + ) + dockerfile = ( + ROOT / "docker" / "registry-stack-release-binary" / "Dockerfile" + ).read_text(encoding="utf-8") + + self.assertIn('verify_official_runtime evidence "$evidence_image"', builder) + self.assertIn('verify_official_runtime mint "$mint_image"', builder) + self.assertIn('build_relayctl "$relayctl_image"', builder) + self.assertNotIn("REGISTRY_STACK_RELEASE_EVIDENCE_ASSET", builder) + self.assertNotIn("REGISTRY_STACK_RELEASE_MINT_ASSET", builder) + self.assertIn("REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256", builder) + self.assertIn("relayctl_sha256", builder) + self.assertNotIn("wget", dockerfile) + self.assertNotIn("curl", dockerfile) + self.assertNotIn(" AS evidence", dockerfile) + self.assertNotIn(" AS mint", dockerfile) + + def test_hosted_pin_gate_verifies_official_runtime_tag_digests_and_labels(self) -> None: + justfile = (ROOT / "justfile").read_text(encoding="utf-8") + + self.assertIn("hosted-pin-check: build-runtime-images", justfile) if __name__ == "__main__": diff --git a/scripts/test_registry_stack_release_pin.py b/scripts/test_registry_stack_release_pin.py index 3bb7fc2..ce4026e 100644 --- a/scripts/test_registry_stack_release_pin.py +++ b/scripts/test_registry_stack_release_pin.py @@ -23,14 +23,14 @@ def authenticator_values() -> dict[str, str]: class ReleasePinTests(unittest.TestCase): - def test_v0200_is_rejected_in_favour_of_fix_forward_release(self) -> None: - values = {"REGISTRY_STACK_REQUIRED_VERSION": "0.20.0", **authenticator_values()} - self.assertIn("must be 0.20.1", MODULE.validate(values, require_public=False)[0]) + def test_older_release_is_rejected_in_favour_of_coherent_release(self) -> None: + values = {"REGISTRY_STACK_REQUIRED_VERSION": "0.20.1", **authenticator_values()} + self.assertIn("must be 0.21.0", MODULE.validate(values, require_public=False)[0]) def test_missing_public_digest_is_an_explicit_blocker(self) -> None: values = { - "REGISTRY_STACK_REQUIRED_VERSION": "0.20.1", - "REGISTRY_STACK_SOURCE_REF": "v0.20.1", + "REGISTRY_STACK_REQUIRED_VERSION": "0.21.0", + "REGISTRY_STACK_SOURCE_REF": "v0.21.0", **authenticator_values(), } self.assertEqual(MODULE.validate(values, require_public=False), []) @@ -38,15 +38,11 @@ def test_missing_public_digest_is_an_explicit_blocker(self) -> None: def test_digest_must_be_exact(self) -> None: values = { - "REGISTRY_STACK_REQUIRED_VERSION": "0.20.1", - "REGISTRY_STACK_SOURCE_REF": "v0.20.1", + "REGISTRY_STACK_REQUIRED_VERSION": "0.21.0", + "REGISTRY_STACK_SOURCE_REF": "v0.21.0", "REGISTRY_STACK_SOURCE_COMMIT": "f" * 40, "REGISTRY_STACK_RELEASE_RELAY_DIGEST": "a" * 64, - "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL": "https://github.com/registrystack/registry-stack/releases/download/v0.20.1/evidence-v0.20.1-linux-amd64", - "REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256": "b" * 64, - "REGISTRY_STACK_RELEASE_MINT_ASSET_URL": "https://github.com/registrystack/registry-stack/releases/download/v0.20.1/mint-v0.20.1-linux-amd64", - "REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256": "c" * 64, - "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL": "https://github.com/registrystack/registry-stack/releases/download/v0.20.1/relayctl-v0.20.1-linux-amd64", + "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL": "https://github.com/registrystack/registry-stack/releases/download/v0.21.0/relayctl-v0.21.0-linux-amd64", "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256": "d" * 64, **authenticator_values(), } @@ -56,12 +52,12 @@ def test_digest_must_be_exact(self) -> None: def test_public_source_ref_must_bind_the_release_tag(self) -> None: values = { - "REGISTRY_STACK_REQUIRED_VERSION": "0.20.1", + "REGISTRY_STACK_REQUIRED_VERSION": "0.21.0", "REGISTRY_STACK_SOURCE_REF": "main", **authenticator_values(), } self.assertIn( - "REGISTRY_STACK_SOURCE_REF must be v0.20.1", + "REGISTRY_STACK_SOURCE_REF must be v0.21.0", MODULE.validate(values, require_public=False), ) diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index 577f706..46b5112 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -253,7 +253,7 @@ def test_local_oidc_issuer_uses_an_isolated_testnet_address(self) -> None: self.assertTrue(issuer_network["internal"]) self.assertEqual( issuer_network["ipam"]["config"], - [{"subnet": "192.0.2.0/27", "ip_range": "192.0.2.0/28"}], + [{"subnet": "192.0.2.0/26", "ip_range": "192.0.2.0/27"}], ) gateway_networks = compose["services"]["evidence-gateway"]["networks"] @@ -264,7 +264,7 @@ def test_local_oidc_issuer_uses_an_isolated_testnet_address(self) -> None: self.assertEqual( gateway_networks["issuer-validation"], { - "ipv4_address": "192.0.2.30", + "ipv4_address": "192.0.2.62", "aliases": ["mint.solmara.registrystack.org"], }, ) diff --git a/versions.env b/versions.env index 2ac134e..f53e493 100644 --- a/versions.env +++ b/versions.env @@ -1,22 +1,18 @@ -# Registry Stack v0.20.0 is immutable and lacks the required Evidence prerequisite. -# The Solmara authority-cell reset therefore fixes forward to the published v0.20.1. -REGISTRY_STACK_REQUIRED_VERSION=0.20.1 -REGISTRY_STACK_RELEASE_RELAY_DIGEST=0727843669569ad8816d9863d695f9450c0507894d859bfacb1b5b98354c76c2 -REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_URL=https://github.com/registrystack/registry-stack/releases/download/v0.20.1/evidence-v0.20.1-linux-amd64 -REGISTRY_STACK_RELEASE_EVIDENCE_ASSET_SHA256=114e249d2f76cd6534a619c339ace46a1f56824f807b65434b593a8d43799a8f -REGISTRY_STACK_RELEASE_MINT_ASSET_URL=https://github.com/registrystack/registry-stack/releases/download/v0.20.1/mint-v0.20.1-linux-amd64 -REGISTRY_STACK_RELEASE_MINT_ASSET_SHA256=91908695241b8960532f1785f72fc3304b5b99776429f65c88c411da631581cc -REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=https://github.com/registrystack/registry-stack/releases/download/v0.20.1/relayctl-v0.20.1-linux-amd64 -REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256=8d02695c64a5f5720af889c74c5ea5c90555e987e61899e6541850e15d7f3773 +# Registry Stack v0.21.0 is the first coherent release that publishes the +# Relay, Evidence, and Mint images required by the authority-cell reset. +REGISTRY_STACK_REQUIRED_VERSION=0.21.0 +REGISTRY_STACK_RELEASE_RELAY_DIGEST=366f926009a39c26e6bdeb8fb47f1dee6ecaef54b57451097993314ab76ad296 +REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=https://github.com/registrystack/registry-stack/releases/download/v0.21.0/relayctl-v0.21.0-linux-amd64 +REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256=7330b36f04306c71605a0b648597cd8f36b6eb2ddff8fcc4a9a3619f20cdfa00 # Exact release source identity and local thin-image tags. Every Registry Stack # executable is fetched from the published release or the digest-pinned OCI. -REGISTRY_STACK_SOURCE_REF=v0.20.1 -REGISTRY_STACK_SOURCE_COMMIT=b9f6d12d7d4b62199558351087487550eedb2bdc -REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/relay@sha256:0727843669569ad8816d9863d695f9450c0507894d859bfacb1b5b98354c76c2 -REGISTRY_RELAYCTL_IMAGE=solmara-lab-relayctl:v0.20.1 -SOLMARA_EVIDENCE_IMAGE=solmara-lab-evidence:v0.20.1 -SOLMARA_MINT_IMAGE=solmara-lab-mint:v0.20.1 +REGISTRY_STACK_SOURCE_REF=v0.21.0 +REGISTRY_STACK_SOURCE_COMMIT=06f3ec27bdc3861a81a973135a4c09f397e7f0c4 +REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/relay@sha256:366f926009a39c26e6bdeb8fb47f1dee6ecaef54b57451097993314ab76ad296 +REGISTRY_RELAYCTL_IMAGE=solmara-lab-relayctl:v0.21.0 +SOLMARA_EVIDENCE_IMAGE=ghcr.io/registrystack/evidence@sha256:3d58439222bf4fef1588eb72d2ba3542e566e2a00b92b525252b8fc8ba3793ab +SOLMARA_MINT_IMAGE=ghcr.io/registrystack/mint@sha256:b85d02ec86253707275898e64677f18d110518a9b543225f0a9c01104df1f1bb # Third-party runtime images are immutable. Refresh intentionally, never by tag. PYTHON_STATIC_IMAGE=python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df From 98dd7804abdd1aa238ba556d7d56533405137960 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:03:23 +0700 Subject: [PATCH 13/53] ci: reuse verified Relayctl asset Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 4 ++-- scripts/build-registry-stack-runtime.sh | 17 +++++++++++++---- scripts/test_build_registry_stack_runtime.py | 13 +++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index fa66960..2f3c8a5 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -70,10 +70,10 @@ jobs: test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.version" }}')" = "$REGISTRY_STACK_REQUIRED_VERSION" test "$(docker image inspect "$image" --format '{{ index .Config.Labels "org.opencontainers.image.source" }}')" = https://github.com/registrystack/registry-stack done - relayctl="relayctl-v${REGISTRY_STACK_REQUIRED_VERSION}-linux-amd64" + relayctl="$RUNNER_TEMP/relayctl-v${REGISTRY_STACK_REQUIRED_VERSION}-linux-amd64" curl --fail --location --silent --show-error "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" --output "$relayctl" printf '%s %s\n' "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256" "$relayctl" | sha256sum --check --status - rm -f "$relayctl" + printf 'REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_FILE=%s\n' "$relayctl" >> "$GITHUB_ENV" - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4.4.0 with: diff --git a/scripts/build-registry-stack-runtime.sh b/scripts/build-registry-stack-runtime.sh index 3abb42a..1ae3abc 100755 --- a/scripts/build-registry-stack-runtime.sh +++ b/scripts/build-registry-stack-runtime.sh @@ -77,10 +77,19 @@ build_relayctl() { relayctl_context=$(mktemp -d) trap 'rm -rf -- "$relayctl_context"' EXIT HUP INT TERM - curl --fail --location --silent --show-error \ - --retry 5 --retry-all-errors --connect-timeout 30 \ - --output "$relayctl_context/relayctl" \ - "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" + relayctl_asset_file=${REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_FILE:-} + if [ -n "$relayctl_asset_file" ]; then + if [ -L "$relayctl_asset_file" ] || [ ! -f "$relayctl_asset_file" ]; then + echo "provided Relayctl asset must be a regular file" >&2 + exit 1 + fi + cp -- "$relayctl_asset_file" "$relayctl_context/relayctl" + else + curl --fail --location --silent --show-error \ + --retry 5 --retry-all-errors --connect-timeout 30 \ + --output "$relayctl_context/relayctl" \ + "$REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL" + fi if [ -L "$relayctl_context/relayctl" ] || [ ! -f "$relayctl_context/relayctl" ]; then echo "downloaded Relayctl asset must be a regular file" >&2 exit 1 diff --git a/scripts/test_build_registry_stack_runtime.py b/scripts/test_build_registry_stack_runtime.py index 56183f8..564ae89 100644 --- a/scripts/test_build_registry_stack_runtime.py +++ b/scripts/test_build_registry_stack_runtime.py @@ -93,6 +93,8 @@ def setUp(self) -> None: import shutil import sys +if os.environ.get("FAIL_IF_CURL") == "1": + raise SystemExit("curl must not run when a verified asset file is supplied") arguments = sys.argv[1:] output = arguments[arguments.index("--output") + 1] shutil.copyfile(os.environ["FAKE_RELAYCTL_SOURCE"], output) @@ -159,3 +161,14 @@ def test_relayctl_is_verified_on_the_host_before_the_minimal_image_build(self) - self.assertIn("--target relayctl", calls) self.assertNotIn("REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL", calls) self.assertNotIn("REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256", calls) + + def test_preverified_relayctl_file_avoids_a_second_network_download(self) -> None: + self.environment["FORCE_RELAYCTL_BUILD"] = "1" + self.environment["FAIL_IF_CURL"] = "1" + self.environment["REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_FILE"] = str( + self.relayctl_asset + ) + + result = self.run_builder() + + self.assertEqual(result.returncode, 0, result.stderr) From cbb4a79d709c1006031d11cc4fe84f24895d7778 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:05:42 +0700 Subject: [PATCH 14/53] ci: keep Solmara lint within repository source Signed-off-by: Jeremi Joslin --- justfile | 2 +- scripts/test_image_pins.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 4b381f7..b1a2ff5 100644 --- a/justfile +++ b/justfile @@ -38,7 +38,7 @@ metadata-publish-check: uv run scripts/publish-metadata.py --check lint: - uvx ruff check --select E4,E7,E9,F . + uvx ruff check --select E4,E7,E9,F --exclude vendor . scripts/check-fiction.sh scripts/check-config-secrets.py scripts/check-image-pins.py diff --git a/scripts/test_image_pins.py b/scripts/test_image_pins.py index c4d6d74..9a0c1fb 100644 --- a/scripts/test_image_pins.py +++ b/scripts/test_image_pins.py @@ -127,6 +127,11 @@ def test_hosted_pin_gate_verifies_official_runtime_tag_digests_and_labels(self) self.assertIn("hosted-pin-check: build-runtime-images", justfile) + def test_lint_excludes_the_exact_vendor_checkout_only(self) -> None: + justfile = (ROOT / "justfile").read_text(encoding="utf-8") + + self.assertIn("ruff check --select E4,E7,E9,F --exclude vendor .", justfile) + if __name__ == "__main__": unittest.main() From 7321716d02fe1c75fe2cae5b083c2ba81730c3ad Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:08:07 +0700 Subject: [PATCH 15/53] test: isolate Relayctl asset fixture Signed-off-by: Jeremi Joslin --- scripts/test_build_registry_stack_runtime.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/test_build_registry_stack_runtime.py b/scripts/test_build_registry_stack_runtime.py index 564ae89..85e4937 100644 --- a/scripts/test_build_registry_stack_runtime.py +++ b/scripts/test_build_registry_stack_runtime.py @@ -112,6 +112,7 @@ def setUp(self) -> None: for component, digest in self.digests.items() }, } + self.environment.pop("REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_FILE", None) def tearDown(self) -> None: self.directory.cleanup() From ba30d4c45a5cf875a33b17535dc7806d0df0885e Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:14:13 +0700 Subject: [PATCH 16/53] fix: support Linux-owned local signer binds Signed-off-by: Jeremi Joslin --- compose.yaml | 14 +++++------ scripts/local-transit-proxy.py | 40 ++++++++++++++++++++++++++------ scripts/test_runtime_topology.py | 12 +++++++++- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/compose.yaml b/compose.yaml index 80ec280..5b8e432 100644 --- a/compose.yaml +++ b/compose.yaml @@ -185,37 +185,37 @@ services: mint-signer: <<: *transit-signer - command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-mint"] + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-mint", "--allow-root-bind-owner"] volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/mint/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "mint-transit:/transit"] cra-signer: <<: *transit-signer - command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-cra"] + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-cra", "--allow-root-bind-owner"] volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/cra/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "cra-transit:/transit"] nia-signer: <<: *transit-signer - command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-nia"] + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-nia", "--allow-root-bind-owner"] volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/nia/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "nia-transit:/transit"] sro-signer: <<: *transit-signer - command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-sro"] + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-sro", "--allow-root-bind-owner"] volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/sro/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "sro-transit:/transit"] mosd-programme-signer: <<: *transit-signer - command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-mosd-programme"] + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-mosd-programme", "--allow-root-bind-owner"] volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/mosd-programme/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "mosd-programme-transit:/transit"] sipf-signer: <<: *transit-signer - command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-sipf"] + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-sipf", "--allow-root-bind-owner"] volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/sipf/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "sipf-transit:/transit"] nagdi-signer: <<: *transit-signer - command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-nagdi"] + command: ["python", "/app/local-transit-proxy.py", "--private-jwk", "/run/secrets/signing.jwk", "--socket", "/transit/transit-proxy.sock", "--key-name", "solmara-evidence-nagdi", "--allow-root-bind-owner"] volumes: ["./scripts/local-transit-proxy.py:/app/local-transit-proxy.py:ro", "./config/evidence/local/cells/nagdi/secrets/signing.jwk:/run/secrets/signing.jwk:ro", "nagdi-transit:/transit"] static-metadata: diff --git a/scripts/local-transit-proxy.py b/scripts/local-transit-proxy.py index 80e3a6d..4dceb80 100755 --- a/scripts/local-transit-proxy.py +++ b/scripts/local-transit-proxy.py @@ -64,7 +64,9 @@ def _decode_base64url(value: Any) -> bytes: return decoded -def _read_private_jwk(path: Path) -> tuple[ec.EllipticCurvePrivateKey, str]: +def _read_private_jwk( + path: Path, *, allow_root_bind_owner: bool = False +) -> tuple[ec.EllipticCurvePrivateKey, str]: if not path.is_absolute(): raise ProxyError("invalid key") flags = os.O_RDONLY @@ -80,7 +82,10 @@ def _read_private_jwk(path: Path) -> tuple[ec.EllipticCurvePrivateKey, str]: metadata = os.fstat(descriptor) if ( not stat.S_ISREG(metadata.st_mode) - or metadata.st_uid != os.geteuid() + or ( + metadata.st_uid != os.geteuid() + and not (allow_root_bind_owner and os.geteuid() == 0) + ) or metadata.st_mode & 0o077 or metadata.st_size <= 0 or metadata.st_size > MAX_KEY_BYTES @@ -157,10 +162,14 @@ def _validate_socket_path(path: Path) -> None: class TransitApplication: - def __init__(self, private_jwk: Path, key_name: str) -> None: + def __init__( + self, private_jwk: Path, key_name: str, *, allow_root_bind_owner: bool = False + ) -> None: if not KEY_NAME.fullmatch(key_name): raise ProxyError("invalid key") - self._private_key, public_pem = _read_private_jwk(private_jwk) + self._private_key, public_pem = _read_private_jwk( + private_jwk, allow_root_bind_owner=allow_root_bind_owner + ) self._metadata = json.dumps( { "data": { @@ -385,8 +394,19 @@ def server_close(self) -> None: self._socket_path.unlink() -def build_server(private_jwk: Path, socket_path: Path, key_name: str) -> TransitServer: - return TransitServer(socket_path, TransitApplication(private_jwk, key_name)) +def build_server( + private_jwk: Path, + socket_path: Path, + key_name: str, + *, + allow_root_bind_owner: bool = False, +) -> TransitServer: + return TransitServer( + socket_path, + TransitApplication( + private_jwk, key_name, allow_root_bind_owner=allow_root_bind_owner + ), + ) def main() -> int: @@ -396,9 +416,15 @@ def main() -> int: parser.add_argument("--private-jwk", required=True, type=Path) parser.add_argument("--socket", required=True, type=Path) parser.add_argument("--key-name", required=True) + parser.add_argument("--allow-root-bind-owner", action="store_true") arguments = parser.parse_args() try: - server = build_server(arguments.private_jwk, arguments.socket, arguments.key_name) + server = build_server( + arguments.private_jwk, + arguments.socket, + arguments.key_name, + allow_root_bind_owner=arguments.allow_root_bind_owner, + ) except (OSError, ProxyError, ValueError): print("local Transit proxy could not start", file=os.sys.stderr) return 1 diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index 46b5112..2dc2f82 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -289,6 +289,15 @@ def test_local_oidc_issuer_uses_an_isolated_testnet_address(self) -> None: service_name, ) + def test_local_root_signers_explicitly_accept_read_only_host_bind_owners(self) -> None: + compose = yaml.safe_load((SCRIPT.parents[1] / "compose.yaml").read_text()) + + for provider in ("mint", "cra", "nia", "sro", "mosd-programme", "sipf", "nagdi"): + signer = compose["services"][f"{provider}-signer"] + self.assertEqual(signer["user"], "0:0") + self.assertIn("--allow-root-bind-owner", signer["command"]) + self.assertTrue(any(volume.endswith("signing.jwk:ro") for volume in signer["volumes"])) + def test_local_transit_signers_are_one_key_one_socket_sidecars(self) -> None: compose_path = SCRIPT.parents[1] / "compose.yaml" compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) @@ -311,7 +320,8 @@ def test_local_transit_signers_are_one_key_one_socket_sidecars(self) -> None: self.assertEqual(signer["user"], "0:0") self.assertEqual(signer["cap_drop"], ["ALL"]) self.assertIn("no-new-privileges:true", signer["security_opt"]) - self.assertEqual(signer["command"][-1], key_name) + key_name_index = signer["command"].index("--key-name") + self.assertEqual(signer["command"][key_name_index + 1], key_name) signer_volumes = set(signer["volumes"]) self.assertIn( f"./config/evidence/local/cells/{provider}/secrets/signing.jwk:/run/secrets/signing.jwk:ro", From 3b9965d6002dad048865fe978f0ccc5d8af6aeb2 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:22:25 +0700 Subject: [PATCH 17/53] fix: allow Linux CI signers to read bound keys Signed-off-by: Jeremi Joslin --- compose.yaml | 4 ++++ scripts/test_runtime_topology.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/compose.yaml b/compose.yaml index 5b8e432..0410dfc 100644 --- a/compose.yaml +++ b/compose.yaml @@ -35,6 +35,10 @@ x-transit-signer: &transit-signer user: "0:0" read_only: true cap_drop: [ALL] + # Linux CI bind mounts retain the runner's ownership on the mode-0600 key. + # Grant only the capability needed to read that one read-only bind. Hosted + # signers stage operator-owned keys and do not use this local exception. + cap_add: [DAC_OVERRIDE] security_opt: [no-new-privileges:true] network_mode: none healthcheck: diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index 2dc2f82..73cb4af 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -295,6 +295,9 @@ def test_local_root_signers_explicitly_accept_read_only_host_bind_owners(self) - for provider in ("mint", "cra", "nia", "sro", "mosd-programme", "sipf", "nagdi"): signer = compose["services"][f"{provider}-signer"] self.assertEqual(signer["user"], "0:0") + self.assertEqual(signer["cap_drop"], ["ALL"]) + self.assertEqual(signer["cap_add"], ["DAC_OVERRIDE"]) + self.assertEqual(signer["network_mode"], "none") self.assertIn("--allow-root-bind-owner", signer["command"]) self.assertTrue(any(volume.endswith("signing.jwk:ro") for volume in signer["volumes"])) From 807e7eedcae89d4945878c303cfbdb768b78f775 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:28:58 +0700 Subject: [PATCH 18/53] fix: read Linux-owned local runtime secrets Signed-off-by: Jeremi Joslin --- compose.yaml | 5 +++++ scripts/test_runtime_topology.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/compose.yaml b/compose.yaml index 0410dfc..a77f3df 100644 --- a/compose.yaml +++ b/compose.yaml @@ -21,6 +21,9 @@ x-evidence: &evidence user: "0:0" read_only: true cap_drop: [ALL] + # Generated local secrets remain mode 0400 and owned by the host user. + # Linux containers need this one capability to read their own RO bind. + cap_add: [DAC_OVERRIDE] security_opt: [no-new-privileges:true] tmpfs: [/tmp] environment: {SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt} @@ -283,6 +286,8 @@ services: user: "0:0" read_only: true cap_drop: [ALL] + # See x-evidence: this local runtime receives only Mint's RO secret tree. + cap_add: [DAC_OVERRIDE] security_opt: [no-new-privileges:true] tmpfs: [/tmp] volumes: diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index 73cb4af..4ae1c12 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -301,6 +301,31 @@ def test_local_root_signers_explicitly_accept_read_only_host_bind_owners(self) - self.assertIn("--allow-root-bind-owner", signer["command"]) self.assertTrue(any(volume.endswith("signing.jwk:ro") for volume in signer["volumes"])) + def test_local_authority_runtimes_can_read_only_their_bound_secret_trees(self) -> None: + compose = yaml.safe_load((SCRIPT.parents[1] / "compose.yaml").read_text()) + + for service_name in ( + "mint", + "cra-evidence", + "nia-evidence", + "sro-evidence", + "mosd-programme-evidence", + "sipf-evidence", + "nagdi-evidence", + ): + service = compose["services"][service_name] + self.assertEqual(service["user"], "0:0") + self.assertEqual(service["cap_drop"], ["ALL"]) + self.assertEqual(service["cap_add"], ["DAC_OVERRIDE"]) + self.assertTrue(service["read_only"]) + secret_mounts = [ + volume + for volume in service["volumes"] + if "/run/secrets/" in volume + ] + self.assertEqual(len(secret_mounts), 1, service_name) + self.assertTrue(secret_mounts[0].endswith(":ro"), service_name) + def test_local_transit_signers_are_one_key_one_socket_sidecars(self) -> None: compose_path = SCRIPT.parents[1] / "compose.yaml" compose = yaml.safe_load(compose_path.read_text(encoding="utf-8")) From 8ef18e79f1aba80508882f09484005ea1ff7465d Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:46:53 +0700 Subject: [PATCH 19/53] fix: initialize local authority audit volumes Signed-off-by: Jeremi Joslin --- compose.yaml | 58 ++++++++++++++++++++++++++++---- scripts/test_runtime_topology.py | 36 ++++++++++++++++++++ 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/compose.yaml b/compose.yaml index a77f3df..3be62a7 100644 --- a/compose.yaml +++ b/compose.yaml @@ -121,6 +121,8 @@ services: paths = ['/audit/cra', '/audit/nia', '/audit/mosd', '/audit/sipf', '/audit/nagdi'] for path in paths: + # Reclaim the directory long enough to validate existing metadata. + # The final chown returns it to Relay before this process exits. os.chown(path, 0, 0) os.chmod(path, 0o700) for entry in os.scandir(path): @@ -146,6 +148,48 @@ services: - sipf-relay-audit:/audit/sipf - nagdi-relay-audit:/audit/nagdi + authority-audit-init: + image: ${PYTHON_STATIC_IMAGE:?versions.env must pin the static server image} + command: + - python + - -c + - |- + import os + import stat + + paths = ['/audit/mint', '/audit/cra', '/audit/nia', '/audit/sro', '/audit/mosd-programme', '/audit/sipf', '/audit/nagdi'] + for path in paths: + metadata = os.lstat(path) + if not stat.S_ISDIR(metadata.st_mode): + raise RuntimeError('audit sink has unsafe metadata') + os.chown(path, 0, 0) + os.chmod(path, 0o700) + mint_chain = '/audit/mint/audit' + try: + os.mkdir(mint_chain, 0o700) + except FileExistsError: + pass + metadata = os.lstat(mint_chain) + if not stat.S_ISDIR(metadata.st_mode): + raise RuntimeError('audit sink has unsafe metadata') + os.chown(mint_chain, 0, 0) + os.chmod(mint_chain, 0o700) + user: "0:0" + read_only: true + cap_drop: [ALL] + cap_add: [CHOWN, FOWNER] + security_opt: [no-new-privileges:true] + network_mode: none + restart: "no" + volumes: + - mint-v2-audit:/audit/mint + - cra-evidence-audit:/audit/cra + - nia-evidence-audit:/audit/nia + - sro-evidence-audit:/audit/sro + - mosd-evidence-audit:/audit/mosd-programme + - sipf-evidence-audit:/audit/sipf + - nagdi-evidence-audit:/audit/nagdi + # A fresh Relay can create its audit chain before OIDC discovery completes. # If discovery then fails, the interrupted empty chain makes every later # consultation fail closed. Prove the shared issuer through the same strict @@ -297,49 +341,49 @@ services: - mint-v2-audit:/var/lib/registry-mint networks: runtime: {ipv4_address: 172.29.1.20} - depends_on: {mint-signer: {condition: service_healthy}} + depends_on: {mint-signer: {condition: service_healthy}, authority-audit-init: {condition: service_completed_successfully}} cra-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/cra/runtime.yaml", "serve"] volumes: ["./runtime/evidence-cells/cells/cra:/etc/registry-evidence/cra:ro", "./runtime/evidence-cells/secrets/cra:/run/secrets/registry-evidence/cra:ro", "cra-transit:/run/registry-evidence/cra:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/cra/extracts:ro", "cra-evidence-audit:/var/lib/registry-evidence/cra/audit"] networks: {runtime: {ipv4_address: 172.29.1.21}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, cra-signer: {condition: service_healthy}, cra-relay: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, cra-signer: {condition: service_healthy}, cra-relay: {condition: service_healthy}} nia-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/nia/runtime.yaml", "serve"] volumes: ["./runtime/evidence-cells/cells/nia:/etc/registry-evidence/nia:ro", "./runtime/evidence-cells/secrets/nia:/run/secrets/registry-evidence/nia:ro", "nia-transit:/run/registry-evidence/nia:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/nia/extracts:ro", "nia-evidence-audit:/var/lib/registry-evidence/nia/audit"] networks: {runtime: {ipv4_address: 172.29.1.22}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, nia-signer: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, nia-signer: {condition: service_healthy}} sro-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/sro/runtime.yaml", "serve"] volumes: ["./runtime/evidence-cells/cells/sro:/etc/registry-evidence/sro:ro", "./runtime/evidence-cells/secrets/sro:/run/secrets/registry-evidence/sro:ro", "sro-transit:/run/registry-evidence/sro:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/sro/extracts:ro", "sro-evidence-audit:/var/lib/registry-evidence/sro/audit"] networks: {runtime: {ipv4_address: 172.29.1.23}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, sro-signer: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, sro-signer: {condition: service_healthy}} mosd-programme-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/mosd-programme/runtime.yaml", "serve"] volumes: ["./runtime/evidence-cells/cells/mosd-programme:/etc/registry-evidence/mosd-programme:ro", "./runtime/evidence-cells/secrets/mosd-programme:/run/secrets/registry-evidence/mosd-programme:ro", "mosd-programme-transit:/run/registry-evidence/mosd-programme:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "mosd-evidence-audit:/var/lib/registry-evidence/mosd-programme/audit"] networks: {runtime: {ipv4_address: 172.29.1.24}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, mosd-programme-signer: {condition: service_healthy}, mosd-relay: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, mosd-programme-signer: {condition: service_healthy}, mosd-relay: {condition: service_healthy}} sipf-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/sipf/runtime.yaml", "serve"] volumes: ["./runtime/evidence-cells/cells/sipf:/etc/registry-evidence/sipf:ro", "./runtime/evidence-cells/secrets/sipf:/run/secrets/registry-evidence/sipf:ro", "sipf-transit:/run/registry-evidence/sipf:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "sipf-evidence-audit:/var/lib/registry-evidence/sipf/audit"] networks: {runtime: {ipv4_address: 172.29.1.25}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, sipf-signer: {condition: service_healthy}, sipf-relay: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, sipf-signer: {condition: service_healthy}, sipf-relay: {condition: service_healthy}} nagdi-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/nagdi/runtime.yaml", "serve"] volumes: ["./runtime/evidence-cells/cells/nagdi:/etc/registry-evidence/nagdi:ro", "./runtime/evidence-cells/secrets/nagdi:/run/secrets/registry-evidence/nagdi:ro", "nagdi-transit:/run/registry-evidence/nagdi:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "nagdi-evidence-audit:/var/lib/registry-evidence/nagdi/audit"] networks: {runtime: {ipv4_address: 172.29.1.26}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, nagdi-signer: {condition: service_healthy}, nagdi-relay: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, nagdi-signer: {condition: service_healthy}, nagdi-relay: {condition: service_healthy}} evidence-gateway: image: ${EVIDENCE_GATEWAY_IMAGE:?versions.env must pin Caddy} diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index 4ae1c12..3909d5b 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -184,6 +184,8 @@ def test_local_relays_use_the_v2_runtime_filesystem_contract(self) -> None: self.assertIn("metadata.st_nlink != 1", audit_init) self.assertIn("audit\\.jsonl\\.\\d{8}", audit_init) self.assertIn("follow_symlinks=False", audit_init) + self.assertIn("os.chown(path, 0, 0)", audit_init) + self.assertIn("os.chown(path, 65532, 65532)", audit_init) for authority in ("cra", "nia", "mosd", "sipf", "nagdi"): self.assertIn( f"{authority}-relay-audit:/audit/{authority}", @@ -325,6 +327,40 @@ def test_local_authority_runtimes_can_read_only_their_bound_secret_trees(self) - ] self.assertEqual(len(secret_mounts), 1, service_name) self.assertTrue(secret_mounts[0].endswith(":ro"), service_name) + self.assertEqual( + service["depends_on"]["authority-audit-init"]["condition"], + "service_completed_successfully", + ) + + def test_local_authority_audit_initializer_is_metadata_only_and_isolated(self) -> None: + compose = yaml.safe_load((SCRIPT.parents[1] / "compose.yaml").read_text()) + initializer = compose["services"]["authority-audit-init"] + + self.assertEqual(initializer["network_mode"], "none") + self.assertEqual(initializer["user"], "0:0") + self.assertEqual(initializer["cap_drop"], ["ALL"]) + self.assertEqual(set(initializer["cap_add"]), {"CHOWN", "FOWNER"}) + self.assertIn("no-new-privileges:true", initializer["security_opt"]) + self.assertEqual( + set(initializer["volumes"]), + { + "mint-v2-audit:/audit/mint", + "cra-evidence-audit:/audit/cra", + "nia-evidence-audit:/audit/nia", + "sro-evidence-audit:/audit/sro", + "mosd-evidence-audit:/audit/mosd-programme", + "sipf-evidence-audit:/audit/sipf", + "nagdi-evidence-audit:/audit/nagdi", + }, + ) + command = initializer["command"][2] + self.assertIn("os.lstat(path)", command) + self.assertIn("stat.S_ISDIR", command) + self.assertIn("os.chown(path, 0, 0)", command) + self.assertIn("mint_chain = '/audit/mint/audit'", command) + self.assertIn("os.mkdir(mint_chain, 0o700)", command) + self.assertIn("os.lstat(mint_chain)", command) + self.assertNotIn("os.scandir", command) def test_local_transit_signers_are_one_key_one_socket_sidecars(self) -> None: compose_path = SCRIPT.parents[1] / "compose.yaml" From f4ed0eae4a76c31c356c93ae3d01e18c5bb6a749 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:50:57 +0700 Subject: [PATCH 20/53] fix: reject unsafe authority audit files Signed-off-by: Jeremi Joslin --- compose.yaml | 20 +++++++++ scripts/test_runtime_topology.py | 69 +++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/compose.yaml b/compose.yaml index 3be62a7..192502b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -155,6 +155,7 @@ services: - -c - |- import os + import re import stat paths = ['/audit/mint', '/audit/cra', '/audit/nia', '/audit/sro', '/audit/mosd-programme', '/audit/sipf', '/audit/nagdi'] @@ -174,6 +175,25 @@ services: raise RuntimeError('audit sink has unsafe metadata') os.chown(mint_chain, 0, 0) os.chmod(mint_chain, 0o700) + audit_files = { + '/audit/cra': 'evidence.jsonl', + '/audit/nia': 'evidence.jsonl', + '/audit/sro': 'evidence.jsonl', + '/audit/mosd-programme': 'evidence.jsonl', + '/audit/sipf': 'evidence.jsonl', + '/audit/nagdi': 'evidence.jsonl', + mint_chain: 'mint.jsonl', + } + for path, active_name in audit_files.items(): + with os.scandir(path) as entries: + for entry in entries: + if entry.name not in {active_name, active_name + '.lock'} and not re.fullmatch(re.escape(active_name) + r'\.\d{8}', entry.name): + continue + metadata = entry.stat(follow_symlinks=False) + if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1: + raise RuntimeError('audit sink has unsafe metadata') + os.chown(entry.path, 0, 0, follow_symlinks=False) + os.chmod(entry.path, 0o600, follow_symlinks=False) user: "0:0" read_only: true cap_drop: [ALL] diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index 3909d5b..bb49bf4 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -7,6 +7,7 @@ import tempfile import unittest from pathlib import Path +from unittest import mock import yaml @@ -178,6 +179,8 @@ def test_local_relays_use_the_v2_runtime_filesystem_contract(self) -> None: self.assertEqual(initializer["cap_drop"], ["ALL"]) self.assertEqual(set(initializer["cap_add"]), {"CHOWN", "FOWNER"}) self.assertIn("no-new-privileges:true", initializer["security_opt"]) + self.assertTrue(initializer["read_only"]) + self.assertEqual(initializer["restart"], "no") audit_init = initializer["command"][2] self.assertIn("os.scandir(path)", audit_init) self.assertIn("stat.S_ISREG", audit_init) @@ -360,7 +363,71 @@ def test_local_authority_audit_initializer_is_metadata_only_and_isolated(self) - self.assertIn("mint_chain = '/audit/mint/audit'", command) self.assertIn("os.mkdir(mint_chain, 0o700)", command) self.assertIn("os.lstat(mint_chain)", command) - self.assertNotIn("os.scandir", command) + self.assertIn("with os.scandir(path) as entries", command) + self.assertIn("entry.stat(follow_symlinks=False)", command) + self.assertIn("metadata.st_nlink != 1", command) + self.assertIn("re.escape(active_name) + r'\\.\\d{8}'", command) + + def test_local_authority_audit_initializer_rejects_unsafe_known_files(self) -> None: + compose = yaml.safe_load((SCRIPT.parents[1] / "compose.yaml").read_text()) + command = compose["services"]["authority-audit-init"]["command"][2] + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + providers = ( + "mint", + "cra", + "nia", + "sro", + "mosd-programme", + "sipf", + "nagdi", + ) + paths = [root / provider for provider in providers] + for path in paths: + path.mkdir() + mint_chain = paths[0] / "audit" + patched = command.replace( + "['/audit/mint', '/audit/cra', '/audit/nia', '/audit/sro', '/audit/mosd-programme', '/audit/sipf', '/audit/nagdi']", + repr([str(path) for path in paths]), + ).replace("'/audit/mint/audit'", repr(str(mint_chain))) + for original, replacement in zip( + ( + "/audit/cra", + "/audit/nia", + "/audit/sro", + "/audit/mosd-programme", + "/audit/sipf", + "/audit/nagdi", + ), + paths[1:], + strict=True, + ): + patched = patched.replace(repr(original), repr(str(replacement))) + + target = root / "target" + target.write_text("audit-canary\n", encoding="utf-8") + unsafe = paths[1] / "evidence.jsonl" + unsafe.symlink_to(target) + with ( + mock.patch("os.chown"), + mock.patch("os.chmod"), + self.assertRaisesRegex( + RuntimeError, "audit sink has unsafe metadata" + ), + ): + exec(patched, {}) + + unsafe.unlink() + os.link(target, unsafe) + with ( + mock.patch("os.chown"), + mock.patch("os.chmod"), + self.assertRaisesRegex( + RuntimeError, "audit sink has unsafe metadata" + ), + ): + exec(patched, {}) def test_local_transit_signers_are_one_key_one_socket_sidecars(self) -> None: compose_path = SCRIPT.parents[1] / "compose.yaml" From 70a96ae2948215caf4efed9ff8b24971b33471f6 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:51:52 +0700 Subject: [PATCH 21/53] test: pin authority audit initializer lifecycle Signed-off-by: Jeremi Joslin --- scripts/test_runtime_topology.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index bb49bf4..6b8fc8f 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -344,6 +344,8 @@ def test_local_authority_audit_initializer_is_metadata_only_and_isolated(self) - self.assertEqual(initializer["cap_drop"], ["ALL"]) self.assertEqual(set(initializer["cap_add"]), {"CHOWN", "FOWNER"}) self.assertIn("no-new-privileges:true", initializer["security_opt"]) + self.assertTrue(initializer["read_only"]) + self.assertEqual(initializer["restart"], "no") self.assertEqual( set(initializer["volumes"]), { From 28a6e6ab97a84b7b25203d079646e76b9ca498da Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 04:57:27 +0700 Subject: [PATCH 22/53] ci: report authority startup failures Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 2f3c8a5..5c43999 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -112,6 +112,13 @@ jobs: just lifecycle-proof just portal-live-e2e just home-live-e2e + - name: Report authority-cell startup failure + if: failure() + run: | + project_name="$(python3 scripts/compose_project_name.py)" + docker compose --env-file versions.env --env-file .env --project-name "$project_name" -f compose.yaml ps --all + docker compose --env-file versions.env --env-file .env --project-name "$project_name" -f compose.yaml logs --no-color \ + authority-audit-init mint evidence-gateway relay-issuer-readiness - name: Record recoverable rollout phases run: | just rollout side-by-side From d62d37bc8b52d97a73dcd4d6ecfee587ac9e0ade Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 05:28:23 +0700 Subject: [PATCH 23/53] fix(local): stage authority runtime secrets Signed-off-by: Jeremi Joslin --- compose.yaml | 160 ++++++++++++++++++--- justfile | 4 +- scripts/test_project_runtime_secrets.py | 6 + scripts/test_runtime_topology.py | 181 ++++++++++++++++++++++-- 4 files changed, 319 insertions(+), 32 deletions(-) diff --git a/compose.yaml b/compose.yaml index 192502b..d626e1d 100644 --- a/compose.yaml +++ b/compose.yaml @@ -21,14 +21,96 @@ x-evidence: &evidence user: "0:0" read_only: true cap_drop: [ALL] - # Generated local secrets remain mode 0400 and owned by the host user. - # Linux containers need this one capability to read their own RO bind. - cap_add: [DAC_OVERRIDE] security_opt: [no-new-privileges:true] tmpfs: [/tmp] environment: {SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt} networks: [runtime, issuer-validation] +# Docker Desktop and Linux preserve different owners for host bind mounts. Copy +# each authority's closed secret set into its own volume so the official +# runtimes see the exact ownership and mode required by SecretResolver. +x-authority-secret-stager: &authority-secret-stager + image: ${PYTHON_STATIC_IMAGE:?versions.env must pin the static server image} + entrypoint: + - python + - -c + - |- + import os + import stat + import sys + + max_secret_bytes = 64 * 1024 + expected = set(sys.argv[2:]) + if sys.argv[1:2] != ['stage'] or not expected or len(expected) != len(sys.argv) - 2: + raise RuntimeError('invalid secret allowlist') + source_fd = os.open('/source', os.O_RDONLY | os.O_DIRECTORY) + destination_fd = os.open('/staged', os.O_RDONLY | os.O_DIRECTORY) + try: + os.fchown(destination_fd, 0, 0) + os.fchmod(destination_fd, 0o700) + with os.scandir(source_fd) as entries: + source_entries = {entry.name: entry.stat(follow_symlinks=False) for entry in entries} + if set(source_entries) != expected: + raise RuntimeError('secret source does not match allowlist') + for metadata in source_entries.values(): + if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1 or metadata.st_size > max_secret_bytes or stat.S_IMODE(metadata.st_mode) not in {0o400, 0o600}: + raise RuntimeError('secret source has unsafe metadata') + + with os.scandir(destination_fd) as entries: + destination_entries = {entry.name: entry.stat(follow_symlinks=False) for entry in entries} + permitted_destination_names = expected | {f'.stage-{name}' for name in expected} + if not set(destination_entries) <= permitted_destination_names: + raise RuntimeError('secret destination contains unexpected entries') + for name, metadata in destination_entries.items(): + if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1: + raise RuntimeError('secret destination has unsafe metadata') + if name.startswith('.stage-'): + os.unlink(name, dir_fd=destination_fd) + + staged = [] + for name in sorted(expected): + metadata = source_entries[name] + source = os.open(name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=source_fd) + temporary = f'.stage-{name}' + destination = os.open( + temporary, + os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o600, + dir_fd=destination_fd, + ) + try: + opened = os.fstat(source) + if (opened.st_dev, opened.st_ino) != (metadata.st_dev, metadata.st_ino) or not stat.S_ISREG(opened.st_mode) or opened.st_nlink != 1 or opened.st_size > max_secret_bytes or stat.S_IMODE(opened.st_mode) not in {0o400, 0o600}: + raise RuntimeError('secret source changed during staging') + copied = 0 + while chunk := os.read(source, min(65536, max_secret_bytes + 1 - copied)): + copied += len(chunk) + if copied > max_secret_bytes: + raise RuntimeError('secret source changed during staging') + remaining = memoryview(chunk) + while remaining: + remaining = remaining[os.write(destination, remaining):] + os.fchmod(destination, 0o600) + os.fchown(destination, 0, 0) + os.fsync(destination) + finally: + os.close(destination) + os.close(source) + staged.append((temporary, name)) + for temporary, name in staged: + os.replace(temporary, name, src_dir_fd=destination_fd, dst_dir_fd=destination_fd) + os.fsync(destination_fd) + finally: + os.close(destination_fd) + os.close(source_fd) + user: "0:0" + read_only: true + cap_drop: [ALL] + cap_add: [CHOWN, DAC_OVERRIDE] + security_opt: [no-new-privileges:true] + network_mode: none + restart: "no" + # Docker Desktop cannot carry a host Unix listener through a bind mount. Each # local authority therefore gets one isolated signer container and one private # named socket volume. Registry services receive the socket, never the key. @@ -79,6 +161,41 @@ x-evidence-client-environment: &evidence-client-environment SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt services: + mint-secret-stager: + <<: *authority-secret-stager + command: [stage, audit-hmac-key] + volumes: ["./runtime/evidence-cells/secrets/mint:/source:ro", "mint-runtime-secrets:/staged"] + + cra-secret-stager: + <<: *authority-secret-stager + command: [stage, audit-hmac-key, subject-binding-hmac-key, cra-pension-evidence-client-id, cra-pension-evidence-client-key, cra-citizen-evidence-client-id, cra-citizen-evidence-client-key] + volumes: ["./runtime/evidence-cells/secrets/cra:/source:ro", "cra-evidence-runtime-secrets:/staged"] + + nia-secret-stager: + <<: *authority-secret-stager + command: [stage, audit-hmac-key, subject-binding-hmac-key] + volumes: ["./runtime/evidence-cells/secrets/nia:/source:ro", "nia-evidence-runtime-secrets:/staged"] + + sro-secret-stager: + <<: *authority-secret-stager + command: [stage, audit-hmac-key, subject-binding-hmac-key] + volumes: ["./runtime/evidence-cells/secrets/sro:/source:ro", "sro-evidence-runtime-secrets:/staged"] + + mosd-programme-secret-stager: + <<: *authority-secret-stager + command: [stage, audit-hmac-key, subject-binding-hmac-key, mosd-child-benefit-evidence-client-id, mosd-child-benefit-evidence-client-key] + volumes: ["./runtime/evidence-cells/secrets/mosd-programme:/source:ro", "mosd-programme-evidence-runtime-secrets:/staged"] + + sipf-secret-stager: + <<: *authority-secret-stager + command: [stage, audit-hmac-key, subject-binding-hmac-key, sipf-pension-evidence-client-id, sipf-pension-evidence-client-key, sipf-survivor-evidence-client-id, sipf-survivor-evidence-client-key] + volumes: ["./runtime/evidence-cells/secrets/sipf:/source:ro", "sipf-evidence-runtime-secrets:/staged"] + + nagdi-secret-stager: + <<: *authority-secret-stager + command: [stage, audit-hmac-key, subject-binding-hmac-key, nagdi-voucher-evidence-client-id, nagdi-voucher-evidence-client-key, nagdi-livestock-evidence-client-id, nagdi-livestock-evidence-client-key] + volumes: ["./runtime/evidence-cells/secrets/nagdi:/source:ro", "nagdi-evidence-runtime-secrets:/staged"] + cra-source-publisher: <<: *relay-source-publisher entrypoint: ["python", "/app/local-relay-source-publisher.py", "--authority", "cra", "--database", "/var/lib/relay/source/cra.sqlite", "--seed", "/seed/cra.sqlite"] @@ -350,60 +467,58 @@ services: user: "0:0" read_only: true cap_drop: [ALL] - # See x-evidence: this local runtime receives only Mint's RO secret tree. - cap_add: [DAC_OVERRIDE] security_opt: [no-new-privileges:true] tmpfs: [/tmp] volumes: - ./runtime/evidence-cells/mint:/etc/registry-mint:ro - - ./runtime/evidence-cells/secrets/mint:/run/secrets/registry-mint:ro + - mint-runtime-secrets:/run/secrets/registry-mint:ro - mint-transit:/run/registry-mint:ro - mint-v2-audit:/var/lib/registry-mint networks: runtime: {ipv4_address: 172.29.1.20} - depends_on: {mint-signer: {condition: service_healthy}, authority-audit-init: {condition: service_completed_successfully}} + depends_on: {mint-signer: {condition: service_healthy}, mint-secret-stager: {condition: service_completed_successfully}, authority-audit-init: {condition: service_completed_successfully}} cra-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/cra/runtime.yaml", "serve"] - volumes: ["./runtime/evidence-cells/cells/cra:/etc/registry-evidence/cra:ro", "./runtime/evidence-cells/secrets/cra:/run/secrets/registry-evidence/cra:ro", "cra-transit:/run/registry-evidence/cra:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/cra/extracts:ro", "cra-evidence-audit:/var/lib/registry-evidence/cra/audit"] + volumes: ["./runtime/evidence-cells/cells/cra:/etc/registry-evidence/cra:ro", "cra-evidence-runtime-secrets:/run/secrets/registry-evidence/cra:ro", "cra-transit:/run/registry-evidence/cra:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/cra/extracts:ro", "cra-evidence-audit:/var/lib/registry-evidence/cra/audit"] networks: {runtime: {ipv4_address: 172.29.1.21}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, cra-signer: {condition: service_healthy}, cra-relay: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, cra-secret-stager: {condition: service_completed_successfully}, authority-audit-init: {condition: service_completed_successfully}, cra-signer: {condition: service_healthy}, cra-relay: {condition: service_healthy}} nia-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/nia/runtime.yaml", "serve"] - volumes: ["./runtime/evidence-cells/cells/nia:/etc/registry-evidence/nia:ro", "./runtime/evidence-cells/secrets/nia:/run/secrets/registry-evidence/nia:ro", "nia-transit:/run/registry-evidence/nia:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/nia/extracts:ro", "nia-evidence-audit:/var/lib/registry-evidence/nia/audit"] + volumes: ["./runtime/evidence-cells/cells/nia:/etc/registry-evidence/nia:ro", "nia-evidence-runtime-secrets:/run/secrets/registry-evidence/nia:ro", "nia-transit:/run/registry-evidence/nia:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/nia/extracts:ro", "nia-evidence-audit:/var/lib/registry-evidence/nia/audit"] networks: {runtime: {ipv4_address: 172.29.1.22}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, nia-signer: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, nia-secret-stager: {condition: service_completed_successfully}, authority-audit-init: {condition: service_completed_successfully}, nia-signer: {condition: service_healthy}} sro-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/sro/runtime.yaml", "serve"] - volumes: ["./runtime/evidence-cells/cells/sro:/etc/registry-evidence/sro:ro", "./runtime/evidence-cells/secrets/sro:/run/secrets/registry-evidence/sro:ro", "sro-transit:/run/registry-evidence/sro:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/sro/extracts:ro", "sro-evidence-audit:/var/lib/registry-evidence/sro/audit"] + volumes: ["./runtime/evidence-cells/cells/sro:/etc/registry-evidence/sro:ro", "sro-evidence-runtime-secrets:/run/secrets/registry-evidence/sro:ro", "sro-transit:/run/registry-evidence/sro:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "./output/sqlite/evidence:/var/lib/registry-evidence/sro/extracts:ro", "sro-evidence-audit:/var/lib/registry-evidence/sro/audit"] networks: {runtime: {ipv4_address: 172.29.1.23}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, sro-signer: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, sro-secret-stager: {condition: service_completed_successfully}, authority-audit-init: {condition: service_completed_successfully}, sro-signer: {condition: service_healthy}} mosd-programme-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/mosd-programme/runtime.yaml", "serve"] - volumes: ["./runtime/evidence-cells/cells/mosd-programme:/etc/registry-evidence/mosd-programme:ro", "./runtime/evidence-cells/secrets/mosd-programme:/run/secrets/registry-evidence/mosd-programme:ro", "mosd-programme-transit:/run/registry-evidence/mosd-programme:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "mosd-evidence-audit:/var/lib/registry-evidence/mosd-programme/audit"] + volumes: ["./runtime/evidence-cells/cells/mosd-programme:/etc/registry-evidence/mosd-programme:ro", "mosd-programme-evidence-runtime-secrets:/run/secrets/registry-evidence/mosd-programme:ro", "mosd-programme-transit:/run/registry-evidence/mosd-programme:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "mosd-evidence-audit:/var/lib/registry-evidence/mosd-programme/audit"] networks: {runtime: {ipv4_address: 172.29.1.24}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, mosd-programme-signer: {condition: service_healthy}, mosd-relay: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, mosd-programme-secret-stager: {condition: service_completed_successfully}, authority-audit-init: {condition: service_completed_successfully}, mosd-programme-signer: {condition: service_healthy}, mosd-relay: {condition: service_healthy}} sipf-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/sipf/runtime.yaml", "serve"] - volumes: ["./runtime/evidence-cells/cells/sipf:/etc/registry-evidence/sipf:ro", "./runtime/evidence-cells/secrets/sipf:/run/secrets/registry-evidence/sipf:ro", "sipf-transit:/run/registry-evidence/sipf:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "sipf-evidence-audit:/var/lib/registry-evidence/sipf/audit"] + volumes: ["./runtime/evidence-cells/cells/sipf:/etc/registry-evidence/sipf:ro", "sipf-evidence-runtime-secrets:/run/secrets/registry-evidence/sipf:ro", "sipf-transit:/run/registry-evidence/sipf:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "sipf-evidence-audit:/var/lib/registry-evidence/sipf/audit"] networks: {runtime: {ipv4_address: 172.29.1.25}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, sipf-signer: {condition: service_healthy}, sipf-relay: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, sipf-secret-stager: {condition: service_completed_successfully}, authority-audit-init: {condition: service_completed_successfully}, sipf-signer: {condition: service_healthy}, sipf-relay: {condition: service_healthy}} nagdi-evidence: <<: *evidence command: ["--runtime", "/etc/registry-evidence/nagdi/runtime.yaml", "serve"] - volumes: ["./runtime/evidence-cells/cells/nagdi:/etc/registry-evidence/nagdi:ro", "./runtime/evidence-cells/secrets/nagdi:/run/secrets/registry-evidence/nagdi:ro", "nagdi-transit:/run/registry-evidence/nagdi:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "nagdi-evidence-audit:/var/lib/registry-evidence/nagdi/audit"] + volumes: ["./runtime/evidence-cells/cells/nagdi:/etc/registry-evidence/nagdi:ro", "nagdi-evidence-runtime-secrets:/run/secrets/registry-evidence/nagdi:ro", "nagdi-transit:/run/registry-evidence/nagdi:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "nagdi-evidence-audit:/var/lib/registry-evidence/nagdi/audit"] networks: {runtime: {ipv4_address: 172.29.1.26}, issuer-validation: {}} - depends_on: {mint: {condition: service_started}, authority-audit-init: {condition: service_completed_successfully}, nagdi-signer: {condition: service_healthy}, nagdi-relay: {condition: service_healthy}} + depends_on: {mint: {condition: service_started}, nagdi-secret-stager: {condition: service_completed_successfully}, authority-audit-init: {condition: service_completed_successfully}, nagdi-signer: {condition: service_healthy}, nagdi-relay: {condition: service_healthy}} evidence-gateway: image: ${EVIDENCE_GATEWAY_IMAGE:?versions.env must pin Caddy} @@ -498,6 +613,13 @@ services: networks: [runtime] volumes: + mint-runtime-secrets: + cra-evidence-runtime-secrets: + nia-evidence-runtime-secrets: + sro-evidence-runtime-secrets: + mosd-programme-evidence-runtime-secrets: + sipf-evidence-runtime-secrets: + nagdi-evidence-runtime-secrets: cra-relay-source: nia-relay-source: mosd-relay-source: diff --git a/justfile b/justfile index b1a2ff5..29f8fb7 100644 --- a/justfile +++ b/justfile @@ -87,10 +87,10 @@ signers-down: uv run scripts/local-transit-signers.py down up: prepare - COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml up -d --build + COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml up -d --build --force-recreate up-esignet: prepare - COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml -f compose.esignet.yaml up -d --build + COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml -f compose.esignet.yaml up -d --build --force-recreate down: COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{ compose_project_name }}}" docker compose --env-file versions.env --env-file .env -f compose.yaml down diff --git a/scripts/test_project_runtime_secrets.py b/scripts/test_project_runtime_secrets.py index 845fbe7..b64d515 100644 --- a/scripts/test_project_runtime_secrets.py +++ b/scripts/test_project_runtime_secrets.py @@ -1,6 +1,7 @@ from __future__ import annotations import importlib.util +import stat import tempfile import unittest from pathlib import Path @@ -35,6 +36,11 @@ def test_signing_keys_never_enter_runtime_projection(self) -> None: "cra-pension-evidence-client-id", ) self.assertEqual((output / "mint/audit-hmac-key").read_text(), "audit") + for path in output.glob("*/*"): + metadata = path.stat(follow_symlinks=False) + self.assertTrue(stat.S_ISREG(metadata.st_mode), path) + self.assertEqual(metadata.st_nlink, 1, path) + self.assertEqual(stat.S_IMODE(metadata.st_mode), 0o600, path) if __name__ == "__main__": diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index 6b8fc8f..154bc3a 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -3,6 +3,7 @@ import importlib.util import os import shutil +import stat import subprocess import tempfile import unittest @@ -19,6 +20,14 @@ class RuntimeTopologyTests(unittest.TestCase): + def test_local_up_recreates_immutable_runtime_consumers(self) -> None: + justfile = (SCRIPT.parents[1] / "justfile").read_text() + for recipe in ("up: prepare", "up-esignet: prepare"): + start = justfile.index(recipe) + command = justfile[start:].splitlines()[1] + self.assertIn("docker compose", command) + self.assertIn("--force-recreate", command) + def test_authority_runtime_uses_the_versioned_relayctl_image(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) @@ -306,22 +315,26 @@ def test_local_root_signers_explicitly_accept_read_only_host_bind_owners(self) - self.assertIn("--allow-root-bind-owner", signer["command"]) self.assertTrue(any(volume.endswith("signing.jwk:ro") for volume in signer["volumes"])) - def test_local_authority_runtimes_can_read_only_their_bound_secret_trees(self) -> None: + def test_local_authority_runtimes_use_owned_read_only_secret_volumes(self) -> None: compose = yaml.safe_load((SCRIPT.parents[1] / "compose.yaml").read_text()) - for service_name in ( - "mint", - "cra-evidence", - "nia-evidence", - "sro-evidence", - "mosd-programme-evidence", - "sipf-evidence", - "nagdi-evidence", - ): + services = { + "mint": ("mint", "mint-runtime-secrets"), + "cra-evidence": ("cra", "cra-evidence-runtime-secrets"), + "nia-evidence": ("nia", "nia-evidence-runtime-secrets"), + "sro-evidence": ("sro", "sro-evidence-runtime-secrets"), + "mosd-programme-evidence": ( + "mosd-programme", + "mosd-programme-evidence-runtime-secrets", + ), + "sipf-evidence": ("sipf", "sipf-evidence-runtime-secrets"), + "nagdi-evidence": ("nagdi", "nagdi-evidence-runtime-secrets"), + } + for service_name, (provider, secret_volume) in services.items(): service = compose["services"][service_name] self.assertEqual(service["user"], "0:0") self.assertEqual(service["cap_drop"], ["ALL"]) - self.assertEqual(service["cap_add"], ["DAC_OVERRIDE"]) + self.assertNotIn("cap_add", service) self.assertTrue(service["read_only"]) secret_mounts = [ volume @@ -329,12 +342,155 @@ def test_local_authority_runtimes_can_read_only_their_bound_secret_trees(self) - if "/run/secrets/" in volume ] self.assertEqual(len(secret_mounts), 1, service_name) + self.assertTrue( + secret_mounts[0].startswith(f"{secret_volume}:"), service_name + ) self.assertTrue(secret_mounts[0].endswith(":ro"), service_name) + self.assertNotIn("./runtime/evidence-cells/secrets/", secret_mounts[0]) + self.assertEqual( + service["depends_on"][f"{provider}-secret-stager"]["condition"], + "service_completed_successfully", + ) self.assertEqual( service["depends_on"]["authority-audit-init"]["condition"], "service_completed_successfully", ) + def test_local_secret_stagers_are_authority_scoped_and_fail_closed(self) -> None: + compose = yaml.safe_load((SCRIPT.parents[1] / "compose.yaml").read_text()) + expected = { + "mint": {"audit-hmac-key"}, + "cra": { + "audit-hmac-key", + "subject-binding-hmac-key", + "cra-pension-evidence-client-id", + "cra-pension-evidence-client-key", + "cra-citizen-evidence-client-id", + "cra-citizen-evidence-client-key", + }, + "nia": {"audit-hmac-key", "subject-binding-hmac-key"}, + "sro": {"audit-hmac-key", "subject-binding-hmac-key"}, + "mosd-programme": { + "audit-hmac-key", + "subject-binding-hmac-key", + "mosd-child-benefit-evidence-client-id", + "mosd-child-benefit-evidence-client-key", + }, + "sipf": { + "audit-hmac-key", + "subject-binding-hmac-key", + "sipf-pension-evidence-client-id", + "sipf-pension-evidence-client-key", + "sipf-survivor-evidence-client-id", + "sipf-survivor-evidence-client-key", + }, + "nagdi": { + "audit-hmac-key", + "subject-binding-hmac-key", + "nagdi-voucher-evidence-client-id", + "nagdi-voucher-evidence-client-key", + "nagdi-livestock-evidence-client-id", + "nagdi-livestock-evidence-client-key", + }, + } + volumes = { + "mint": "mint-runtime-secrets", + "cra": "cra-evidence-runtime-secrets", + "nia": "nia-evidence-runtime-secrets", + "sro": "sro-evidence-runtime-secrets", + "mosd-programme": "mosd-programme-evidence-runtime-secrets", + "sipf": "sipf-evidence-runtime-secrets", + "nagdi": "nagdi-evidence-runtime-secrets", + } + for provider, names in expected.items(): + stager = compose["services"][f"{provider}-secret-stager"] + self.assertEqual(stager["command"][0], "stage") + self.assertEqual(set(stager["command"][1:]), names) + self.assertEqual(stager["user"], "0:0") + self.assertTrue(stager["read_only"]) + self.assertEqual(stager["cap_drop"], ["ALL"]) + self.assertEqual(set(stager["cap_add"]), {"CHOWN", "DAC_OVERRIDE"}) + self.assertIn("no-new-privileges:true", stager["security_opt"]) + self.assertEqual(stager["network_mode"], "none") + self.assertEqual(stager["restart"], "no") + self.assertEqual( + set(stager["volumes"]), + { + f"./runtime/evidence-cells/secrets/{provider}:/source:ro", + f"{volumes[provider]}:/staged", + }, + ) + + command = compose["services"]["mint-secret-stager"]["entrypoint"][2] + for required in ( + "set(source_entries) != expected", + "stat.S_ISREG", + "metadata.st_nlink != 1", + "metadata.st_size > max_secret_bytes", + "opened.st_size > max_secret_bytes", + "max_secret_bytes = 64 * 1024", + "if copied > max_secret_bytes", + "{0o400, 0o600}", + "os.O_NOFOLLOW", + "os.fchown(destination_fd, 0, 0)", + "os.fchmod(destination_fd, 0o700)", + "os.fchown(destination, 0, 0)", + "os.fchmod(destination, 0o600)", + "os.replace", + ): + self.assertIn(required, command) + self.assertNotIn("print(", command) + + def test_local_secret_stager_copies_without_rendering_secret_values(self) -> None: + compose = yaml.safe_load((SCRIPT.parents[1] / "compose.yaml").read_text()) + command = compose["services"]["mint-secret-stager"]["entrypoint"][2] + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = root / "source" + destination = root / "staged" + source.mkdir() + destination.mkdir() + secret = source / "audit-hmac-key" + secret.write_bytes(b"secret-canary\n") + secret.chmod(0o600) + patched = command.replace("'/source'", repr(str(source))).replace( + "'/staged'", repr(str(destination)) + ) + with ( + mock.patch("sys.argv", ["-c", "stage", "audit-hmac-key"]), + mock.patch("os.fchown"), + ): + exec(patched, {}) + + staged = destination / "audit-hmac-key" + self.assertEqual(stat.S_IMODE(destination.stat().st_mode), 0o700) + self.assertEqual(staged.read_bytes(), b"secret-canary\n") + self.assertEqual(stat.S_IMODE(staged.stat().st_mode), 0o600) + self.assertEqual(staged.stat().st_nlink, 1) + + peer = root / "peer" + os.link(secret, peer) + with ( + mock.patch("sys.argv", ["-c", "stage", "audit-hmac-key"]), + mock.patch("os.fchown"), + self.assertRaisesRegex( + RuntimeError, "secret source has unsafe metadata" + ), + ): + exec(patched, {}) + + peer.unlink() + secret.write_bytes(b"x" * (64 * 1024 + 1)) + with ( + mock.patch("sys.argv", ["-c", "stage", "audit-hmac-key"]), + mock.patch("os.fchown"), + self.assertRaisesRegex( + RuntimeError, "secret source has unsafe metadata" + ), + ): + exec(patched, {}) + def test_local_authority_audit_initializer_is_metadata_only_and_isolated(self) -> None: compose = yaml.safe_load((SCRIPT.parents[1] / "compose.yaml").read_text()) initializer = compose["services"]["authority-audit-init"] @@ -369,6 +525,9 @@ def test_local_authority_audit_initializer_is_metadata_only_and_isolated(self) - self.assertIn("entry.stat(follow_symlinks=False)", command) self.assertIn("metadata.st_nlink != 1", command) self.assertIn("re.escape(active_name) + r'\\.\\d{8}'", command) + self.assertIn("os.chown(entry.path, 0, 0", command) + self.assertNotIn("os.chown(mint_chain, 65532, 65532)", command) + self.assertNotIn("os.chown(path, 65532, 65532)", command) def test_local_authority_audit_initializer_rejects_unsafe_known_files(self) -> None: compose = yaml.safe_load((SCRIPT.parents[1] / "compose.yaml").read_text()) From dbf2455dd582c3f4f6c958d4733f0a7eb3acdac6 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 05:37:28 +0700 Subject: [PATCH 24/53] ci: report relay startup failures Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 5c43999..d90ebce 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -118,7 +118,8 @@ jobs: project_name="$(python3 scripts/compose_project_name.py)" docker compose --env-file versions.env --env-file .env --project-name "$project_name" -f compose.yaml ps --all docker compose --env-file versions.env --env-file .env --project-name "$project_name" -f compose.yaml logs --no-color \ - authority-audit-init mint evidence-gateway relay-issuer-readiness + authority-audit-init mint evidence-gateway relay-issuer-readiness \ + cra-relay nia-relay mosd-relay sipf-relay nagdi-relay - name: Record recoverable rollout phases run: | just rollout side-by-side From 58a943ec7e8b609352eb869c03119209d6946c34 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 05:56:00 +0700 Subject: [PATCH 25/53] fix(local): align Relay runtime ownership Signed-off-by: Jeremi Joslin --- .env.example | 2 ++ compose.yaml | 21 ++++++++++++++++++-- scripts/gen-secrets.py | 23 +++++++++++++++++++++ scripts/test_gen_secrets.py | 23 +++++++++++++++++++++ scripts/test_runtime_topology.py | 34 +++++++++++++++++++++++++++++++- 5 files changed, 100 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 7e12e99..83406d7 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,8 @@ # Values here are placeholders only. COMPOSE_PROJECT_NAME=solmara-lab-replace-me +SOLMARA_LOCAL_RUNTIME_UID= +SOLMARA_LOCAL_RUNTIME_GID= CRA_RELAY_AUDIT_KEY= NIA_RELAY_AUDIT_KEY= diff --git a/compose.yaml b/compose.yaml index d626e1d..bb19fee 100644 --- a/compose.yaml +++ b/compose.yaml @@ -3,8 +3,14 @@ name: ${COMPOSE_PROJECT_NAME:-solmara-lab} x-relay: &relay image: ${REGISTRY_RELAY_IMAGE:?versions.env must name the digest-pinned Relay V2 image} platform: linux/amd64 + # Relay validates that its bind-mounted runtime is owned by root or its + # effective UID. Match the generated workspace owner for this local-only + # topology; hosted runtimes use authority-provisioned volumes and UID 65532. + user: "${SOLMARA_LOCAL_RUNTIME_UID:?run just gen-secrets}:${SOLMARA_LOCAL_RUNTIME_GID:?run just gen-secrets}" restart: unless-stopped read_only: true + cap_drop: [ALL] + security_opt: [no-new-privileges:true] tmpfs: [/tmp] networks: [runtime, issuer-validation] depends_on: {relay-audit-init: {condition: service_completed_successfully}} @@ -236,6 +242,14 @@ services: import re import stat + uid_value = os.environ['SOLMARA_LOCAL_RUNTIME_UID'] + gid_value = os.environ['SOLMARA_LOCAL_RUNTIME_GID'] + if not uid_value.isascii() or not uid_value.isdecimal() or not gid_value.isascii() or not gid_value.isdecimal(): + raise RuntimeError('runtime identity is invalid') + target_uid = int(uid_value) + target_gid = int(gid_value) + if target_uid == 0 or target_gid == 0 or target_uid > 4294967294 or target_gid > 4294967294: + raise RuntimeError('runtime identity is invalid') paths = ['/audit/cra', '/audit/nia', '/audit/mosd', '/audit/sipf', '/audit/nagdi'] for path in paths: # Reclaim the directory long enough to validate existing metadata. @@ -248,9 +262,12 @@ services: metadata = entry.stat(follow_symlinks=False) if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1: raise RuntimeError('audit sink has unsafe metadata') - os.chown(entry.path, 65532, 65532, follow_symlinks=False) + os.chown(entry.path, target_uid, target_gid, follow_symlinks=False) os.chmod(entry.path, 0o600, follow_symlinks=False) - os.chown(path, 65532, 65532) + os.chown(path, target_uid, target_gid) + environment: + SOLMARA_LOCAL_RUNTIME_UID: ${SOLMARA_LOCAL_RUNTIME_UID:?run just gen-secrets} + SOLMARA_LOCAL_RUNTIME_GID: ${SOLMARA_LOCAL_RUNTIME_GID:?run just gen-secrets} user: "0:0" read_only: true cap_drop: [ALL] diff --git a/scripts/gen-secrets.py b/scripts/gen-secrets.py index c4ab270..5179aaf 100755 --- a/scripts/gen-secrets.py +++ b/scripts/gen-secrets.py @@ -9,6 +9,7 @@ import secrets import shlex import subprocess +import sys from pathlib import Path from typing import Callable @@ -121,9 +122,26 @@ def create_environment_value( return factory() +def local_runtime_identity() -> tuple[int, int]: + """Return the UID/GID that can trust a local Relay runtime bind.""" + + # Docker Desktop presents bind-mounted files as root-owned inside its Linux + # VM. Keep the upstream non-root image identity there. Native Linux + # preserves the checkout owner, which Relay must match to trust the file. + if sys.platform == "darwin": + return (65532, 65532) + runtime = ROOT / "relays" / "cra" / "runtime.yaml" + metadata = runtime.stat() + if metadata.st_uid == 0: + return (65532, 65532) + runtime_gid = metadata.st_gid if metadata.st_gid != 0 else 65532 + return (metadata.st_uid, runtime_gid) + + def compose_environment_values( existing: dict[str, str], operator_values: dict[str, str] ) -> dict[str, str]: + runtime_uid, runtime_gid = local_runtime_identity() values = { key: create_environment_value(existing, key, raw_key) for key in RANDOM_ENV_KEYS @@ -134,6 +152,11 @@ def compose_environment_values( values.update( { "COMPOSE_PROJECT_NAME": compose_project_name(ROOT), + # Relay validates the complete Unix ownership chain of its + # bind-mounted runtime. Match the local workspace owner on Linux; + # hosted deployments use provisioned volumes and the image UID. + "SOLMARA_LOCAL_RUNTIME_UID": str(runtime_uid), + "SOLMARA_LOCAL_RUNTIME_GID": str(runtime_gid), "PORTAL_AUTH_PROVIDER": "mock", "PORTAL_ESIGNET_CLIENT_ID": "solmara-portal", "PORTAL_ESIGNET_CLIENT_KEY_ID": "solmara-portal-key-1", diff --git a/scripts/test_gen_secrets.py b/scripts/test_gen_secrets.py index ae37f05..0e8406c 100644 --- a/scripts/test_gen_secrets.py +++ b/scripts/test_gen_secrets.py @@ -84,6 +84,29 @@ def test_generated_environment_preserves_runtime_secrets(self) -> None: for key, value in existing.items(): self.assertEqual(values[key], value) self.assertEqual(values["NIA_ESIGNET_CLIENT_PRIVATE_JWK"], "operator-jwk") + runtime_uid, runtime_gid = MODULE.local_runtime_identity() + self.assertEqual(values["SOLMARA_LOCAL_RUNTIME_UID"], str(runtime_uid)) + self.assertEqual(values["SOLMARA_LOCAL_RUNTIME_GID"], str(runtime_gid)) + + def test_local_runtime_identity_preserves_the_relay_trust_boundary(self) -> None: + with mock.patch.object(MODULE.sys, "platform", "darwin"): + self.assertEqual(MODULE.local_runtime_identity(), (65532, 65532)) + + metadata = MODULE.ROOT.joinpath("relays/cra/runtime.yaml").stat() + with mock.patch.object(MODULE.sys, "platform", "linux"): + expected = ( + (65532, 65532) + if metadata.st_uid == 0 + else (metadata.st_uid, metadata.st_gid or 65532) + ) + self.assertEqual(MODULE.local_runtime_identity(), expected) + + root_group_metadata = mock.Mock(st_uid=1000, st_gid=0) + with ( + mock.patch.object(MODULE.sys, "platform", "linux"), + mock.patch.object(Path, "stat", return_value=root_group_metadata), + ): + self.assertEqual(MODULE.local_runtime_identity(), (1000, 65532)) def test_generated_environment_rejects_duplicate_or_empty_values(self) -> None: with tempfile.TemporaryDirectory() as temporary: diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index 154bc3a..a882331 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -122,6 +122,12 @@ def test_local_relays_use_the_v2_runtime_filesystem_contract(self) -> None: for authority in ("cra", "nia", "mosd", "sipf", "nagdi"): service = compose["services"][f"{authority}-relay"] + self.assertEqual( + service["user"], + "${SOLMARA_LOCAL_RUNTIME_UID:?run just gen-secrets}:${SOLMARA_LOCAL_RUNTIME_GID:?run just gen-secrets}", + ) + self.assertEqual(service["cap_drop"], ["ALL"]) + self.assertIn("no-new-privileges:true", service["security_opt"]) self.assertEqual( service["command"], ["serve", "--runtime", f"/etc/relay/{authority}/runtime.yaml"], @@ -197,13 +203,39 @@ def test_local_relays_use_the_v2_runtime_filesystem_contract(self) -> None: self.assertIn("audit\\.jsonl\\.\\d{8}", audit_init) self.assertIn("follow_symlinks=False", audit_init) self.assertIn("os.chown(path, 0, 0)", audit_init) - self.assertIn("os.chown(path, 65532, 65532)", audit_init) + self.assertIn("os.chown(path, target_uid, target_gid)", audit_init) + self.assertIn("target_uid == 0 or target_gid == 0", audit_init) + self.assertIn( + "os.chown(entry.path, target_uid, target_gid, follow_symlinks=False)", + audit_init, + ) + self.assertEqual( + initializer["environment"], + { + "SOLMARA_LOCAL_RUNTIME_UID": "${SOLMARA_LOCAL_RUNTIME_UID:?run just gen-secrets}", + "SOLMARA_LOCAL_RUNTIME_GID": "${SOLMARA_LOCAL_RUNTIME_GID:?run just gen-secrets}", + }, + ) for authority in ("cra", "nia", "mosd", "sipf", "nagdi"): self.assertIn( f"{authority}-relay-audit:/audit/{authority}", initializer["volumes"], ) + for uid_value, gid_value in (("0", "65532"), ("65532", "0")): + with ( + mock.patch.dict( + os.environ, + { + "SOLMARA_LOCAL_RUNTIME_UID": uid_value, + "SOLMARA_LOCAL_RUNTIME_GID": gid_value, + }, + clear=False, + ), + self.assertRaisesRegex(RuntimeError, "runtime identity is invalid"), + ): + exec(audit_init, {}) + issuer_readiness = compose["services"]["relay-issuer-readiness"] self.assertEqual(issuer_readiness["user"], "65532:65532") self.assertTrue(issuer_readiness["read_only"]) From ff93f4bdbf9120bc80fed00c621228c24e7a3082 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 06:03:38 +0700 Subject: [PATCH 26/53] fix(hosted): isolate authority networks Signed-off-by: Jeremi Joslin --- compose.coolify.agriculture.yaml | 4 +- compose.coolify.interior.yaml | 6 +- compose.coolify.labour-pensions.yaml | 4 +- compose.coolify.provision.yaml | 12 +-- compose.coolify.social-development.yaml | 6 +- justfile | 2 +- scripts/test_hosted_network_isolation.py | 104 +++++++++++++++++++++++ 7 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 scripts/test_hosted_network_isolation.py diff --git a/compose.coolify.agriculture.yaml b/compose.coolify.agriculture.yaml index f87a84a..090604c 100644 --- a/compose.coolify.agriculture.yaml +++ b/compose.coolify.agriculture.yaml @@ -38,7 +38,7 @@ services: image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/nagdi/runtime.yaml, serve] volumes: [nagdi-evidence-runtime:/etc/registry-evidence/nagdi:ro, nagdi-evidence-secrets:/run/secrets/registry-evidence/nagdi:ro, nagdi-evidence-transit:/run/registry-evidence/nagdi:ro, nagdi-evidence-audit:/var/lib/registry-evidence/nagdi/audit] - networks: {runtime: {ipv4_address: 172.29.1.26}} + networks: {runtime: {ipv4_address: 172.29.5.26}} depends_on: audit-permissions: {condition: service_completed_successfully} nagdi-relay: {condition: service_started} @@ -57,4 +57,4 @@ volumes: networks: runtime: ipam: - config: [{subnet: 172.29.1.0/24}] + config: [{subnet: 172.29.5.0/24}] diff --git a/compose.coolify.interior.yaml b/compose.coolify.interior.yaml index 24ae44a..8c8830b 100644 --- a/compose.coolify.interior.yaml +++ b/compose.coolify.interior.yaml @@ -46,7 +46,7 @@ services: image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/cra/runtime.yaml, serve] volumes: [cra-evidence-runtime:/etc/registry-evidence/cra:ro, cra-evidence-secrets:/run/secrets/registry-evidence/cra:ro, cra-evidence-transit:/run/registry-evidence/cra:ro, cra-evidence-extracts:/var/lib/registry-evidence/cra/extracts:ro, cra-evidence-audit:/var/lib/registry-evidence/cra/audit] - networks: {runtime: {ipv4_address: 172.29.1.21}} + networks: {runtime: {ipv4_address: 172.29.2.21}} depends_on: audit-permissions: {condition: service_completed_successfully} cra-relay: {condition: service_started} @@ -57,7 +57,7 @@ services: image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/nia/runtime.yaml, serve] volumes: [nia-evidence-runtime:/etc/registry-evidence/nia:ro, nia-evidence-secrets:/run/secrets/registry-evidence/nia:ro, nia-evidence-transit:/run/registry-evidence/nia:ro, nia-evidence-extracts:/var/lib/registry-evidence/nia/extracts:ro, nia-evidence-audit:/var/lib/registry-evidence/nia/audit] - networks: {runtime: {ipv4_address: 172.29.1.22}} + networks: {runtime: {ipv4_address: 172.29.2.22}} labels: {solmara.lab.host: nia-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} volumes: @@ -82,4 +82,4 @@ volumes: networks: runtime: ipam: - config: [{subnet: 172.29.1.0/24}] + config: [{subnet: 172.29.2.0/24}] diff --git a/compose.coolify.labour-pensions.yaml b/compose.coolify.labour-pensions.yaml index a8c9d10..2eec0b7 100644 --- a/compose.coolify.labour-pensions.yaml +++ b/compose.coolify.labour-pensions.yaml @@ -38,7 +38,7 @@ services: image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/sipf/runtime.yaml, serve] volumes: [sipf-evidence-runtime:/etc/registry-evidence/sipf:ro, sipf-evidence-secrets:/run/secrets/registry-evidence/sipf:ro, sipf-evidence-transit:/run/registry-evidence/sipf:ro, sipf-evidence-audit:/var/lib/registry-evidence/sipf/audit] - networks: {runtime: {ipv4_address: 172.29.1.25}} + networks: {runtime: {ipv4_address: 172.29.4.25}} depends_on: audit-permissions: {condition: service_completed_successfully} sipf-relay: {condition: service_started} @@ -57,4 +57,4 @@ volumes: networks: runtime: ipam: - config: [{subnet: 172.29.1.0/24}] + config: [{subnet: 172.29.4.0/24}] diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml index 9f0707e..b47ca3d 100644 --- a/compose.coolify.provision.yaml +++ b/compose.coolify.provision.yaml @@ -69,7 +69,7 @@ services: cra-evidence-provisioner: <<: *provisioner - command: [provision, --target, cra-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.1.21] + command: [provision, --target, cra-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.21] volumes: [cra-evidence-runtime:/provisioned/runtime, cra-evidence-secrets:/provisioned/secrets, cra-evidence-extracts:/provisioned/extracts] secrets: - {source: cra-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -80,7 +80,7 @@ services: nia-evidence-provisioner: <<: *provisioner - command: [provision, --target, nia-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.1.22] + command: [provision, --target, nia-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.22] volumes: [nia-evidence-runtime:/provisioned/runtime, nia-evidence-secrets:/provisioned/secrets, nia-evidence-extracts:/provisioned/extracts] secrets: - {source: nia-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -89,7 +89,7 @@ services: sro-evidence-provisioner: <<: *provisioner - command: [provision, --target, sro-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.1.23] + command: [provision, --target, sro-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.3.23] volumes: [sro-evidence-runtime:/provisioned/runtime, sro-evidence-secrets:/provisioned/secrets, sro-evidence-extracts:/provisioned/extracts] secrets: - {source: sro-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -98,7 +98,7 @@ services: mosd-evidence-provisioner: <<: *provisioner - command: [provision, --target, mosd-programme-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.24] + command: [provision, --target, mosd-programme-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.3.24] volumes: [mosd-evidence-runtime:/provisioned/runtime, mosd-evidence-secrets:/provisioned/secrets] secrets: - {source: mosd-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -108,7 +108,7 @@ services: sipf-evidence-provisioner: <<: *provisioner - command: [provision, --target, sipf-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.25] + command: [provision, --target, sipf-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.4.25] volumes: [sipf-evidence-runtime:/provisioned/runtime, sipf-evidence-secrets:/provisioned/secrets] secrets: - {source: sipf-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -119,7 +119,7 @@ services: nagdi-evidence-provisioner: <<: *provisioner - command: [provision, --target, nagdi-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.26] + command: [provision, --target, nagdi-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.5.26] volumes: [nagdi-evidence-runtime:/provisioned/runtime, nagdi-evidence-secrets:/provisioned/secrets] secrets: - {source: nagdi-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} diff --git a/compose.coolify.social-development.yaml b/compose.coolify.social-development.yaml index 583c53d..64d8a8d 100644 --- a/compose.coolify.social-development.yaml +++ b/compose.coolify.social-development.yaml @@ -38,7 +38,7 @@ services: image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/sro/runtime.yaml, serve] volumes: [sro-evidence-runtime:/etc/registry-evidence/sro:ro, sro-evidence-secrets:/run/secrets/registry-evidence/sro:ro, sro-evidence-transit:/run/registry-evidence/sro:ro, sro-evidence-extracts:/var/lib/registry-evidence/sro/extracts:ro, sro-evidence-audit:/var/lib/registry-evidence/sro/audit] - networks: {runtime: {ipv4_address: 172.29.1.23}} + networks: {runtime: {ipv4_address: 172.29.3.23}} labels: {solmara.lab.host: sro-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} mosd-programme-evidence: @@ -46,7 +46,7 @@ services: image: ${SOLMARA_EVIDENCE_IMAGE:?set the digest-pinned official Registry Stack Evidence image} command: [--runtime, /etc/registry-evidence/mosd-programme/runtime.yaml, serve] volumes: [mosd-evidence-runtime:/etc/registry-evidence/mosd-programme:ro, mosd-evidence-secrets:/run/secrets/registry-evidence/mosd-programme:ro, mosd-evidence-transit:/run/registry-evidence/mosd-programme:ro, mosd-evidence-audit:/var/lib/registry-evidence/mosd-programme/audit] - networks: {runtime: {ipv4_address: 172.29.1.24}} + networks: {runtime: {ipv4_address: 172.29.3.24}} depends_on: audit-permissions: {condition: service_completed_successfully} mosd-relay: {condition: service_started} @@ -70,4 +70,4 @@ volumes: networks: runtime: ipam: - config: [{subnet: 172.29.1.0/24}] + config: [{subnet: 172.29.3.0/24}] diff --git a/justfile b/justfile index 29f8fb7..94cdda6 100644 --- a/justfile +++ b/justfile @@ -54,7 +54,7 @@ lint: test: cd generator && uv run python -m unittest discover -s tests uv run python -m unittest discover -s scenario-runner -p 'test_*.py' - uv run python -m unittest relays/test_relay_projects.py evidence/tests/test_cells.py scripts/test_metadata_authority_contracts.py scripts/test_image_pins.py scripts/test_build_registry_stack_runtime.py scripts/test_hosted_image_manifest.py scripts/test_hosted_provisioning_topology.py scripts/test_hosted_home_topology.py scripts/test_hosted_runtime_assets.py scripts/test_provision_hosted_runtime.py scripts/test_hosted_transit_signer.py scripts/test_smoke_hosted_provisioner_image.py scripts/test_runtime_topology.py scripts/test_registry_stack_release_pin.py scripts/test_hosted_authority_rollout.py scripts/test_local_transit_proxy.py scripts/test_local_transit_signers.py scripts/test_local_transit_providers.py scripts/test_signer_public_keys.py scripts/test_project_runtime_secrets.py scripts/test_gen_secrets.py scripts/test_publish_runtime_extracts.py scripts/test_lifecycle_proof.py scripts/test_live_lifecycle_proof.py scripts/test_local_relay_source_publisher.py scripts/test_smoke_programme_acceptance.py scripts/test_smoke_esignet.py + uv run python -m unittest relays/test_relay_projects.py evidence/tests/test_cells.py scripts/test_metadata_authority_contracts.py scripts/test_image_pins.py scripts/test_build_registry_stack_runtime.py scripts/test_hosted_image_manifest.py scripts/test_hosted_provisioning_topology.py scripts/test_hosted_network_isolation.py scripts/test_hosted_home_topology.py scripts/test_hosted_runtime_assets.py scripts/test_provision_hosted_runtime.py scripts/test_hosted_transit_signer.py scripts/test_smoke_hosted_provisioner_image.py scripts/test_runtime_topology.py scripts/test_registry_stack_release_pin.py scripts/test_hosted_authority_rollout.py scripts/test_local_transit_proxy.py scripts/test_local_transit_signers.py scripts/test_local_transit_providers.py scripts/test_signer_public_keys.py scripts/test_project_runtime_secrets.py scripts/test_gen_secrets.py scripts/test_publish_runtime_extracts.py scripts/test_lifecycle_proof.py scripts/test_live_lifecycle_proof.py scripts/test_local_relay_source_publisher.py scripts/test_smoke_programme_acceptance.py scripts/test_smoke_esignet.py cd portal && pnpm test cd home && pnpm test diff --git a/scripts/test_hosted_network_isolation.py b/scripts/test_hosted_network_isolation.py new file mode 100644 index 0000000..9b1f496 --- /dev/null +++ b/scripts/test_hosted_network_isolation.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import ipaddress +import unittest +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +PROVISION_PATH = ROOT / "compose.coolify.provision.yaml" +RUNTIME_PATHS = { + "core": ROOT / "compose.coolify.yaml", + "interior": ROOT / "compose.coolify.interior.yaml", + "social": ROOT / "compose.coolify.social-development.yaml", + "pensions": ROOT / "compose.coolify.labour-pensions.yaml", + "agriculture": ROOT / "compose.coolify.agriculture.yaml", +} +EXPECTED_SUBNETS = { + "core": "172.29.1.0/24", + "interior": "172.29.2.0/24", + "social": "172.29.3.0/24", + "pensions": "172.29.4.0/24", + "agriculture": "172.29.5.0/24", +} +BINDINGS = { + "mint-provisioner": ("core", "mint", "172.29.1.20"), + "cra-evidence-provisioner": ("interior", "cra-evidence", "172.29.2.21"), + "nia-evidence-provisioner": ("interior", "nia-evidence", "172.29.2.22"), + "sro-evidence-provisioner": ("social", "sro-evidence", "172.29.3.23"), + "mosd-evidence-provisioner": ( + "social", + "mosd-programme-evidence", + "172.29.3.24", + ), + "sipf-evidence-provisioner": ("pensions", "sipf-evidence", "172.29.4.25"), + "nagdi-evidence-provisioner": ( + "agriculture", + "nagdi-evidence", + "172.29.5.26", + ), +} + + +class HostedNetworkIsolationTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.provision = yaml.safe_load(PROVISION_PATH.read_text(encoding="utf-8")) + cls.runtime = { + name: yaml.safe_load(path.read_text(encoding="utf-8")) + for name, path in RUNTIME_PATHS.items() + } + + def test_runtime_apps_use_distinct_expected_private_subnets(self) -> None: + networks = {} + for name, compose in self.runtime.items(): + subnet_text = compose["networks"]["runtime"]["ipam"]["config"][0]["subnet"] + self.assertEqual(subnet_text, EXPECTED_SUBNETS[name]) + network = ipaddress.ip_network(subnet_text) + self.assertTrue(network.is_private) + networks[name] = network + + for name, network in networks.items(): + for other_name, other_network in networks.items(): + if name >= other_name: + continue + self.assertFalse( + network.overlaps(other_network), + f"{name} {network} overlaps {other_name} {other_network}", + ) + + def test_all_bind_hosts_match_their_runtime_addresses(self) -> None: + services_with_bind_hosts = { + name + for name, service in self.provision["services"].items() + if "--bind-host" in service.get("command", []) + } + self.assertEqual(services_with_bind_hosts, set(BINDINGS)) + + for provisioner_name, ( + app, + service_name, + expected_address, + ) in BINDINGS.items(): + command = self.provision["services"][provisioner_name]["command"] + bind_host = command[command.index("--bind-host") + 1] + runtime_address = self.runtime[app]["services"][service_name]["networks"][ + "runtime" + ]["ipv4_address"] + self.assertEqual(bind_host, expected_address) + self.assertEqual(runtime_address, expected_address) + self.assertIn( + ipaddress.ip_address(expected_address), + ipaddress.ip_network(EXPECTED_SUBNETS[app]), + ) + + def test_provisioning_services_remain_networkless(self) -> None: + self.assertNotIn("networks", self.provision) + for service_name, service in self.provision["services"].items(): + self.assertEqual(service.get("network_mode"), "none", service_name) + + +if __name__ == "__main__": + unittest.main() From 1c495fc0d4ad249d6475727024193ca5679361cf Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 06:20:53 +0700 Subject: [PATCH 27/53] fix(local): stage sealed Relay runtimes Signed-off-by: Jeremi Joslin --- .env.example | 2 - compose.yaml | 84 ++- scripts/gen-secrets.py | 23 - scripts/local-relay-runtime-stager.py | 624 +++++++++++++++++++++ scripts/test_gen_secrets.py | 23 - scripts/test_local_relay_runtime_stager.py | 279 +++++++++ 6 files changed, 962 insertions(+), 73 deletions(-) create mode 100755 scripts/local-relay-runtime-stager.py create mode 100644 scripts/test_local_relay_runtime_stager.py diff --git a/.env.example b/.env.example index 83406d7..7e12e99 100644 --- a/.env.example +++ b/.env.example @@ -2,8 +2,6 @@ # Values here are placeholders only. COMPOSE_PROJECT_NAME=solmara-lab-replace-me -SOLMARA_LOCAL_RUNTIME_UID= -SOLMARA_LOCAL_RUNTIME_GID= CRA_RELAY_AUDIT_KEY= NIA_RELAY_AUDIT_KEY= diff --git a/compose.yaml b/compose.yaml index bb19fee..5af6d5c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -3,10 +3,7 @@ name: ${COMPOSE_PROJECT_NAME:-solmara-lab} x-relay: &relay image: ${REGISTRY_RELAY_IMAGE:?versions.env must name the digest-pinned Relay V2 image} platform: linux/amd64 - # Relay validates that its bind-mounted runtime is owned by root or its - # effective UID. Match the generated workspace owner for this local-only - # topology; hosted runtimes use authority-provisioned volumes and UID 65532. - user: "${SOLMARA_LOCAL_RUNTIME_UID:?run just gen-secrets}:${SOLMARA_LOCAL_RUNTIME_GID:?run just gen-secrets}" + user: "65532:65532" restart: unless-stopped read_only: true cap_drop: [ALL] @@ -152,6 +149,22 @@ x-relay-source-publisher: &relay-source-publisher tmpfs: [/tmp] restart: "no" +# Native Linux preserves host ownership on bind mounts, while Relay accepts a +# runtime only when every path component is owned by root or its effective UID. +# Stage each authority's public runtime and sealed package into a dedicated +# volume so the official image can keep its fixed unprivileged identity. +x-relay-runtime-stager: &relay-runtime-stager + image: ${PYTHON_STATIC_IMAGE:?versions.env must pin the static server image} + entrypoint: ["python", "/app/local-relay-runtime-stager.py"] + user: "0:0" + read_only: true + cap_drop: [ALL] + cap_add: [CHOWN, DAC_OVERRIDE, FOWNER] + security_opt: [no-new-privileges:true] + network_mode: none + tmpfs: [/tmp] + restart: "no" + x-evidence-client-environment: &evidence-client-environment SOLMARA_MINT_URL: https://mint.solmara.registrystack.org SOLMARA_MINT_ASSERTION_AUDIENCE: https://mint.solmara.registrystack.org/token @@ -232,6 +245,31 @@ services: command: ["ensure-seeded"] volumes: ["./scripts/local-relay-source-publisher.py:/app/local-relay-source-publisher.py:ro", "./generator/solmara_lab:/app/solmara_lab:ro", "./output/sqlite/relay/nagdi.sqlite:/seed/nagdi.sqlite:ro", "nagdi-relay-source:/var/lib/relay/source"] + cra-relay-runtime-stager: + <<: *relay-runtime-stager + command: ["--authority", "cra", "--source", "/source", "--destination", "/staged", "stage"] + volumes: ["./scripts/local-relay-runtime-stager.py:/app/local-relay-runtime-stager.py:ro", "./relays/cra/runtime.yaml:/source/runtime.yaml:ro", "./relays/cra/package:/source/package:ro", "cra-relay-runtime:/staged"] + + nia-relay-runtime-stager: + <<: *relay-runtime-stager + command: ["--authority", "nia", "--source", "/source", "--destination", "/staged", "stage"] + volumes: ["./scripts/local-relay-runtime-stager.py:/app/local-relay-runtime-stager.py:ro", "./relays/nia/runtime.yaml:/source/runtime.yaml:ro", "./relays/nia/package:/source/package:ro", "nia-relay-runtime:/staged"] + + mosd-relay-runtime-stager: + <<: *relay-runtime-stager + command: ["--authority", "mosd", "--source", "/source", "--destination", "/staged", "stage"] + volumes: ["./scripts/local-relay-runtime-stager.py:/app/local-relay-runtime-stager.py:ro", "./relays/mosd/runtime.yaml:/source/runtime.yaml:ro", "./relays/mosd/package:/source/package:ro", "mosd-relay-runtime:/staged"] + + sipf-relay-runtime-stager: + <<: *relay-runtime-stager + command: ["--authority", "sipf", "--source", "/source", "--destination", "/staged", "stage"] + volumes: ["./scripts/local-relay-runtime-stager.py:/app/local-relay-runtime-stager.py:ro", "./relays/sipf/runtime.yaml:/source/runtime.yaml:ro", "./relays/sipf/package:/source/package:ro", "sipf-relay-runtime:/staged"] + + nagdi-relay-runtime-stager: + <<: *relay-runtime-stager + command: ["--authority", "nagdi", "--source", "/source", "--destination", "/staged", "stage"] + volumes: ["./scripts/local-relay-runtime-stager.py:/app/local-relay-runtime-stager.py:ro", "./relays/nagdi/runtime.yaml:/source/runtime.yaml:ro", "./relays/nagdi/package:/source/package:ro", "nagdi-relay-runtime:/staged"] + relay-audit-init: image: ${PYTHON_STATIC_IMAGE:?versions.env must pin the static server image} command: @@ -242,14 +280,8 @@ services: import re import stat - uid_value = os.environ['SOLMARA_LOCAL_RUNTIME_UID'] - gid_value = os.environ['SOLMARA_LOCAL_RUNTIME_GID'] - if not uid_value.isascii() or not uid_value.isdecimal() or not gid_value.isascii() or not gid_value.isdecimal(): - raise RuntimeError('runtime identity is invalid') - target_uid = int(uid_value) - target_gid = int(gid_value) - if target_uid == 0 or target_gid == 0 or target_uid > 4294967294 or target_gid > 4294967294: - raise RuntimeError('runtime identity is invalid') + target_uid = 65532 + target_gid = 65532 paths = ['/audit/cra', '/audit/nia', '/audit/mosd', '/audit/sipf', '/audit/nagdi'] for path in paths: # Reclaim the directory long enough to validate existing metadata. @@ -265,9 +297,6 @@ services: os.chown(entry.path, target_uid, target_gid, follow_symlinks=False) os.chmod(entry.path, 0o600, follow_symlinks=False) os.chown(path, target_uid, target_gid) - environment: - SOLMARA_LOCAL_RUNTIME_UID: ${SOLMARA_LOCAL_RUNTIME_UID:?run just gen-secrets} - SOLMARA_LOCAL_RUNTIME_GID: ${SOLMARA_LOCAL_RUNTIME_GID:?run just gen-secrets} user: "0:0" read_only: true cap_drop: [ALL] @@ -434,24 +463,24 @@ services: <<: *relay environment: {SOLMARA_RELAY_AUDIT_KEY: "${CRA_RELAY_AUDIT_KEY:?run just gen-secrets}", SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt} command: ["serve", "--runtime", "/etc/relay/cra/runtime.yaml"] - volumes: ["./relays/cra/runtime.yaml:/etc/relay/cra/runtime.yaml:ro", "./relays/cra/package:/etc/relay/cra/package:ro", "cra-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "cra-relay-audit:/var/lib/relay/audit"] - depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, cra-source-publisher: {condition: service_completed_successfully}} + volumes: ["cra-relay-runtime:/etc/relay/cra:ro", "cra-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "cra-relay-audit:/var/lib/relay/audit"] + depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, cra-source-publisher: {condition: service_completed_successfully}, cra-relay-runtime-stager: {condition: service_completed_successfully}} ports: ["${SOLMARA_CRA_RELAY_PORT:-4311}:8080"] nia-relay: <<: *relay environment: {SOLMARA_RELAY_AUDIT_KEY: "${NIA_RELAY_AUDIT_KEY:?run just gen-secrets}", SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt} command: ["serve", "--runtime", "/etc/relay/nia/runtime.yaml"] - volumes: ["./relays/nia/runtime.yaml:/etc/relay/nia/runtime.yaml:ro", "./relays/nia/package:/etc/relay/nia/package:ro", "nia-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "nia-relay-audit:/var/lib/relay/audit"] - depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, nia-source-publisher: {condition: service_completed_successfully}} + volumes: ["nia-relay-runtime:/etc/relay/nia:ro", "nia-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "nia-relay-audit:/var/lib/relay/audit"] + depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, nia-source-publisher: {condition: service_completed_successfully}, nia-relay-runtime-stager: {condition: service_completed_successfully}} ports: ["${SOLMARA_NIA_RELAY_PORT:-4312}:8080"] mosd-relay: <<: *relay environment: {SOLMARA_RELAY_AUDIT_KEY: "${MOSD_RELAY_AUDIT_KEY:?run just gen-secrets}", SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt} command: ["serve", "--runtime", "/etc/relay/mosd/runtime.yaml"] - volumes: ["./relays/mosd/runtime.yaml:/etc/relay/mosd/runtime.yaml:ro", "./relays/mosd/package:/etc/relay/mosd/package:ro", "mosd-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "mosd-relay-audit:/var/lib/relay/audit"] - depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, mosd-source-publisher: {condition: service_completed_successfully}} + volumes: ["mosd-relay-runtime:/etc/relay/mosd:ro", "mosd-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "mosd-relay-audit:/var/lib/relay/audit"] + depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, mosd-source-publisher: {condition: service_completed_successfully}, mosd-relay-runtime-stager: {condition: service_completed_successfully}} ports: ["${SOLMARA_MOSD_RELAY_PORT:-4314}:8080"] sipf-relay: @@ -461,8 +490,8 @@ services: SOLMARA_RELAY_CURSOR_KEY: ${SIPF_RELAY_CURSOR_KEY:?run just gen-secrets} SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt command: ["serve", "--runtime", "/etc/relay/sipf/runtime.yaml"] - volumes: ["./relays/sipf/runtime.yaml:/etc/relay/sipf/runtime.yaml:ro", "./relays/sipf/package:/etc/relay/sipf/package:ro", "sipf-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "sipf-relay-audit:/var/lib/relay/audit"] - depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, sipf-source-publisher: {condition: service_completed_successfully}} + volumes: ["sipf-relay-runtime:/etc/relay/sipf:ro", "sipf-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "sipf-relay-audit:/var/lib/relay/audit"] + depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, sipf-source-publisher: {condition: service_completed_successfully}, sipf-relay-runtime-stager: {condition: service_completed_successfully}} ports: ["${SOLMARA_SIPF_RELAY_PORT:-4315}:8080"] nagdi-relay: @@ -472,8 +501,8 @@ services: SOLMARA_RELAY_CURSOR_KEY: ${NAGDI_RELAY_CURSOR_KEY:?run just gen-secrets} SSL_CERT_FILE: /etc/solmara-evidence/tls/ca.crt command: ["serve", "--runtime", "/etc/relay/nagdi/runtime.yaml"] - volumes: ["./relays/nagdi/runtime.yaml:/etc/relay/nagdi/runtime.yaml:ro", "./relays/nagdi/package:/etc/relay/nagdi/package:ro", "nagdi-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "nagdi-relay-audit:/var/lib/relay/audit"] - depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, nagdi-source-publisher: {condition: service_completed_successfully}} + volumes: ["nagdi-relay-runtime:/etc/relay/nagdi:ro", "nagdi-relay-source:/var/lib/relay/source:ro", "./config/evidence/local/tls/ca.crt:/etc/solmara-evidence/tls/ca.crt:ro", "nagdi-relay-audit:/var/lib/relay/audit"] + depends_on: {relay-audit-init: {condition: service_completed_successfully}, relay-issuer-readiness: {condition: service_completed_successfully}, nagdi-source-publisher: {condition: service_completed_successfully}, nagdi-relay-runtime-stager: {condition: service_completed_successfully}} ports: ["${SOLMARA_NAGDI_RELAY_PORT:-4316}:8080"] mint: @@ -637,6 +666,11 @@ volumes: mosd-programme-evidence-runtime-secrets: sipf-evidence-runtime-secrets: nagdi-evidence-runtime-secrets: + cra-relay-runtime: + nia-relay-runtime: + mosd-relay-runtime: + sipf-relay-runtime: + nagdi-relay-runtime: cra-relay-source: nia-relay-source: mosd-relay-source: diff --git a/scripts/gen-secrets.py b/scripts/gen-secrets.py index 5179aaf..c4ab270 100755 --- a/scripts/gen-secrets.py +++ b/scripts/gen-secrets.py @@ -9,7 +9,6 @@ import secrets import shlex import subprocess -import sys from pathlib import Path from typing import Callable @@ -122,26 +121,9 @@ def create_environment_value( return factory() -def local_runtime_identity() -> tuple[int, int]: - """Return the UID/GID that can trust a local Relay runtime bind.""" - - # Docker Desktop presents bind-mounted files as root-owned inside its Linux - # VM. Keep the upstream non-root image identity there. Native Linux - # preserves the checkout owner, which Relay must match to trust the file. - if sys.platform == "darwin": - return (65532, 65532) - runtime = ROOT / "relays" / "cra" / "runtime.yaml" - metadata = runtime.stat() - if metadata.st_uid == 0: - return (65532, 65532) - runtime_gid = metadata.st_gid if metadata.st_gid != 0 else 65532 - return (metadata.st_uid, runtime_gid) - - def compose_environment_values( existing: dict[str, str], operator_values: dict[str, str] ) -> dict[str, str]: - runtime_uid, runtime_gid = local_runtime_identity() values = { key: create_environment_value(existing, key, raw_key) for key in RANDOM_ENV_KEYS @@ -152,11 +134,6 @@ def compose_environment_values( values.update( { "COMPOSE_PROJECT_NAME": compose_project_name(ROOT), - # Relay validates the complete Unix ownership chain of its - # bind-mounted runtime. Match the local workspace owner on Linux; - # hosted deployments use provisioned volumes and the image UID. - "SOLMARA_LOCAL_RUNTIME_UID": str(runtime_uid), - "SOLMARA_LOCAL_RUNTIME_GID": str(runtime_gid), "PORTAL_AUTH_PROVIDER": "mock", "PORTAL_ESIGNET_CLIENT_ID": "solmara-portal", "PORTAL_ESIGNET_CLIENT_KEY_ID": "solmara-portal-key-1", diff --git a/scripts/local-relay-runtime-stager.py b/scripts/local-relay-runtime-stager.py new file mode 100755 index 0000000..6743549 --- /dev/null +++ b/scripts/local-relay-runtime-stager.py @@ -0,0 +1,624 @@ +#!/usr/bin/env python3 +"""Stage one authority's Relay runtime and sealed package into a local volume.""" + +from __future__ import annotations + +import argparse +import fcntl +import hashlib +import json +import os +import re +import stat +import sys +from dataclasses import dataclass +from pathlib import Path, PurePosixPath + +AUTHORITIES = ("cra", "nia", "mosd", "sipf", "nagdi") +TARGET_UID = 65532 +TARGET_GID = 65532 +MAX_RUNTIME_BYTES = 64 * 1024 +MAX_MANIFEST_BYTES = 1024 * 1024 +MAX_PACKAGE_FILE_BYTES = 1024 * 1024 +MAX_PACKAGE_BYTES = 8 * 1024 * 1024 +MAX_PACKAGE_FILES = 128 +SUCCESS_MESSAGE = "Relay runtime staged" +FAILURE_MESSAGE = "Relay runtime staging failed" +LOCK_NAME = ".stager.lock" +STAGING_PACKAGE = ".staging-package" +STAGING_RUNTIME = ".staging-runtime.yaml" + + +class StagingError(RuntimeError): + """A deliberately value-free staging failure.""" + + +class QuietArgumentParser(argparse.ArgumentParser): + def error(self, message: str) -> None: + del message + raise StagingError(FAILURE_MESSAGE) + + +@dataclass(frozen=True) +class FileSnapshot: + device: int + inode: int + mode: int + links: int + size: int + modified_ns: int + + +def _fail() -> StagingError: + return StagingError(FAILURE_MESSAGE) + + +def _snapshot(metadata: os.stat_result) -> FileSnapshot: + return FileSnapshot( + metadata.st_dev, + metadata.st_ino, + metadata.st_mode, + metadata.st_nlink, + metadata.st_size, + metadata.st_mtime_ns, + ) + + +def _safe_file(metadata: os.stat_result, *, max_bytes: int) -> bool: + mode = stat.S_IMODE(metadata.st_mode) + return ( + stat.S_ISREG(metadata.st_mode) + and metadata.st_nlink == 1 + and 0 <= metadata.st_size <= max_bytes + and mode in {0o400, 0o440, 0o444, 0o600, 0o640, 0o644} + ) + + +def _safe_directory(metadata: os.stat_result) -> bool: + return stat.S_ISDIR(metadata.st_mode) and stat.S_IMODE(metadata.st_mode) in { + 0o500, + 0o550, + 0o555, + 0o700, + 0o750, + 0o755, + } + + +def _open_directory(path: Path) -> int: + return os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + + +def _scan_tree(directory_fd: int) -> tuple[dict[str, FileSnapshot], set[str]]: + files: dict[str, FileSnapshot] = {} + directories: set[str] = set() + total_bytes = 0 + + def visit(parent_fd: int, prefix: PurePosixPath) -> None: + nonlocal total_bytes + with os.scandir(parent_fd) as iterator: + entries = sorted(iterator, key=lambda entry: entry.name) + for entry in entries: + if entry.name in {".", ".."} or "/" in entry.name: + raise _fail() + metadata = entry.stat(follow_symlinks=False) + relative = str(prefix / entry.name) + if stat.S_ISDIR(metadata.st_mode): + if not _safe_directory(metadata): + raise _fail() + directories.add(relative) + child_fd = os.open( + entry.name, + os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=parent_fd, + ) + try: + if _snapshot(os.fstat(child_fd)) != _snapshot(metadata): + raise _fail() + visit(child_fd, prefix / entry.name) + finally: + os.close(child_fd) + continue + if not _safe_file(metadata, max_bytes=MAX_PACKAGE_FILE_BYTES): + raise _fail() + files[relative] = _snapshot(metadata) + total_bytes += metadata.st_size + if len(files) > MAX_PACKAGE_FILES or total_bytes > MAX_PACKAGE_BYTES: + raise _fail() + + visit(directory_fd, PurePosixPath()) + return files, directories + + +def _open_relative(directory_fd: int, relative: str) -> int: + parts = PurePosixPath(relative).parts + if not parts: + raise _fail() + current_fd = os.dup(directory_fd) + try: + for part in parts[:-1]: + next_fd = os.open( + part, + os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=current_fd, + ) + os.close(current_fd) + current_fd = next_fd + return os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW, dir_fd=current_fd) + finally: + os.close(current_fd) + + +def _read_exact( + directory_fd: int, + relative: str, + expected: FileSnapshot, + *, + max_bytes: int, +) -> bytes: + descriptor = _open_relative(directory_fd, relative) + try: + before = os.fstat(descriptor) + if _snapshot(before) != expected or not _safe_file(before, max_bytes=max_bytes): + raise _fail() + chunks: list[bytes] = [] + copied = 0 + while True: + chunk = os.read(descriptor, min(65536, max_bytes + 1 - copied)) + if not chunk: + break + copied += len(chunk) + if copied > max_bytes: + raise _fail() + chunks.append(chunk) + after = os.fstat(descriptor) + if _snapshot(after) != expected or copied != expected.size: + raise _fail() + return b"".join(chunks) + finally: + os.close(descriptor) + + +def _manifest_inventory(manifest_bytes: bytes) -> dict[str, tuple[int, str]]: + try: + document = json.loads(manifest_bytes) + files = document["files"] + except (KeyError, TypeError, ValueError, json.JSONDecodeError): + raise _fail() from None + if not isinstance(document, dict) or not isinstance(files, list) or not files: + raise _fail() + + inventory: dict[str, tuple[int, str]] = {} + digest_pattern = re.compile(r"sha256:[0-9a-f]{64}") + for record in files: + if not isinstance(record, dict): + raise _fail() + path = record.get("path") + size = record.get("size") + digest = record.get("sha256") + if ( + not isinstance(path, str) + or not path + or len(path) > 512 + or PurePosixPath(path).is_absolute() + or any(part in {"", ".", ".."} for part in PurePosixPath(path).parts) + or not isinstance(size, int) + or isinstance(size, bool) + or size < 0 + or size > MAX_PACKAGE_FILE_BYTES + or not isinstance(digest, str) + or digest_pattern.fullmatch(digest) is None + or path in inventory + or path == "relay-package.json" + ): + raise _fail() + inventory[path] = (size, digest.removeprefix("sha256:")) + if len(inventory) + 1 > MAX_PACKAGE_FILES: + raise _fail() + return inventory + + +def _read_package(directory_fd: int) -> dict[str, bytes]: + files, directories = _scan_tree(directory_fd) + manifest_snapshot = files.get("relay-package.json") + if manifest_snapshot is None or manifest_snapshot.size > MAX_MANIFEST_BYTES: + raise _fail() + manifest_bytes = _read_exact( + directory_fd, + "relay-package.json", + manifest_snapshot, + max_bytes=MAX_MANIFEST_BYTES, + ) + inventory = _manifest_inventory(manifest_bytes) + if set(files) != set(inventory) | {"relay-package.json"}: + raise _fail() + expected_directories = { + str(parent) + for path in inventory + for parent in PurePosixPath(path).parents + if str(parent) != "." + } + if directories != expected_directories: + raise _fail() + + payloads = {"relay-package.json": manifest_bytes} + total_bytes = len(manifest_bytes) + for path, (expected_size, expected_digest) in sorted(inventory.items()): + payload = _read_exact( + directory_fd, + path, + files[path], + max_bytes=MAX_PACKAGE_FILE_BYTES, + ) + if len(payload) != expected_size: + raise _fail() + if hashlib.sha256(payload).hexdigest() != expected_digest: + raise _fail() + payloads[path] = payload + total_bytes += len(payload) + if total_bytes > MAX_PACKAGE_BYTES: + raise _fail() + + if _scan_tree(directory_fd) != (files, directories): + raise _fail() + return payloads + + +def _validate_runtime(runtime: bytes, authority: str) -> None: + try: + text = runtime.decode("utf-8") + except UnicodeDecodeError: + raise _fail() from None + expected = f"/etc/relay/{authority}/package" + exact_package_path = re.compile( + rf"^packagePath:[ \t]+(?:{re.escape(expected)}|" + rf'"{re.escape(expected)}"|\'{re.escape(expected)}\')' + r"(?:[ \t]+#.*)?[ \t]*$" + ) + entries = [] + for line in text.splitlines(): + if not line.startswith("packagePath:"): + continue + entries.append(line) + if ( + "\x00" in text + or len(entries) != 1 + or exact_package_path.fullmatch(entries[0]) is None + ): + raise _fail() + + +def _read_source(source: Path, authority: str) -> tuple[bytes, dict[str, bytes]]: + source_fd = _open_directory(source) + try: + with os.scandir(source_fd) as iterator: + entries = { + entry.name: entry.stat(follow_symlinks=False) for entry in iterator + } + if set(entries) != {"runtime.yaml", "package"}: + raise _fail() + runtime_metadata = entries["runtime.yaml"] + package_metadata = entries["package"] + if not _safe_file(runtime_metadata, max_bytes=MAX_RUNTIME_BYTES): + raise _fail() + if not _safe_directory(package_metadata): + raise _fail() + runtime = _read_exact( + source_fd, + "runtime.yaml", + _snapshot(runtime_metadata), + max_bytes=MAX_RUNTIME_BYTES, + ) + _validate_runtime(runtime, authority) + package_fd = os.open( + "package", + os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=source_fd, + ) + try: + if _snapshot(os.fstat(package_fd)) != _snapshot(package_metadata): + raise _fail() + package = _read_package(package_fd) + finally: + os.close(package_fd) + with os.scandir(source_fd) as iterator: + after = { + entry.name: _snapshot(entry.stat(follow_symlinks=False)) + for entry in iterator + } + if after != {name: _snapshot(metadata) for name, metadata in entries.items()}: + raise _fail() + return runtime, package + finally: + os.close(source_fd) + + +def _remove_tree(parent_fd: int, name: str) -> None: + descriptor = os.open( + name, + os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=parent_fd, + ) + try: + os.fchmod(descriptor, 0o700) + with os.scandir(descriptor) as iterator: + entries = sorted(iterator, key=lambda entry: entry.name) + for entry in entries: + metadata = entry.stat(follow_symlinks=False) + if stat.S_ISDIR(metadata.st_mode): + if not _safe_directory(metadata): + raise _fail() + _remove_tree(descriptor, entry.name) + elif _safe_file(metadata, max_bytes=MAX_PACKAGE_FILE_BYTES): + os.unlink(entry.name, dir_fd=descriptor) + else: + raise _fail() + finally: + os.close(descriptor) + os.rmdir(name, dir_fd=parent_fd) + + +def _validate_owned_tree(directory_fd: int, uid: int, gid: int) -> None: + files, directories = _scan_tree(directory_fd) + for relative in directories: + descriptor = _open_relative(directory_fd, relative + "/.") + try: + metadata = os.fstat(descriptor) + finally: + os.close(descriptor) + if metadata.st_uid != uid or metadata.st_gid != gid: + raise _fail() + for relative in files: + descriptor = _open_relative(directory_fd, relative) + try: + metadata = os.fstat(descriptor) + finally: + os.close(descriptor) + if metadata.st_uid != uid or metadata.st_gid != gid: + raise _fail() + + +def _read_existing_package( + parent_fd: int, name: str, uid: int, gid: int +) -> dict[str, bytes]: + descriptor = os.open( + name, + os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=parent_fd, + ) + try: + _validate_owned_tree(descriptor, uid, gid) + return _read_package(descriptor) + finally: + os.close(descriptor) + + +def _read_existing_runtime(parent_fd: int, authority: str, uid: int, gid: int) -> bytes: + metadata = os.stat("runtime.yaml", dir_fd=parent_fd, follow_symlinks=False) + if ( + metadata.st_uid != uid + or metadata.st_gid != gid + or not _safe_file(metadata, max_bytes=MAX_RUNTIME_BYTES) + ): + raise _fail() + runtime = _read_exact( + parent_fd, + "runtime.yaml", + _snapshot(metadata), + max_bytes=MAX_RUNTIME_BYTES, + ) + _validate_runtime(runtime, authority) + return runtime + + +def _make_directory(parent_fd: int, name: str, uid: int, gid: int) -> int: + os.mkdir(name, 0o700, dir_fd=parent_fd) + descriptor = os.open( + name, + os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=parent_fd, + ) + os.fchown(descriptor, uid, gid) + return descriptor + + +def _write_file(parent_fd: int, name: str, payload: bytes, uid: int, gid: int) -> None: + descriptor = os.open( + name, + os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o600, + dir_fd=parent_fd, + ) + try: + remaining = memoryview(payload) + while remaining: + written = os.write(descriptor, remaining) + if written <= 0: + raise _fail() + remaining = remaining[written:] + os.fchown(descriptor, uid, gid) + os.fchmod(descriptor, 0o400) + os.fsync(descriptor) + finally: + os.close(descriptor) + + +def _write_package( + parent_fd: int, payloads: dict[str, bytes], uid: int, gid: int +) -> None: + root_fd = _make_directory(parent_fd, STAGING_PACKAGE, uid, gid) + opened: dict[PurePosixPath, int] = {PurePosixPath(): root_fd} + try: + directories = sorted( + { + parent + for path in payloads + for parent in PurePosixPath(path).parents + if str(parent) != "." + }, + key=lambda path: (len(path.parts), str(path)), + ) + for relative in directories: + parent = relative.parent + opened[relative] = _make_directory(opened[parent], relative.name, uid, gid) + for relative_string, payload in sorted(payloads.items()): + relative = PurePosixPath(relative_string) + _write_file(opened[relative.parent], relative.name, payload, uid, gid) + for relative in sorted(opened, key=lambda path: len(path.parts), reverse=True): + os.fchmod(opened[relative], 0o500) + os.fsync(opened[relative]) + finally: + for descriptor in set(opened.values()): + os.close(descriptor) + + +def _recover_destination( + destination_fd: int, authority: str, uid: int, gid: int +) -> None: + with os.scandir(destination_fd) as iterator: + entries = {entry.name: entry.stat(follow_symlinks=False) for entry in iterator} + allowed = { + LOCK_NAME, + "runtime.yaml", + "package", + STAGING_PACKAGE, + STAGING_RUNTIME, + } + if not set(entries) <= allowed: + raise _fail() + for name, metadata in entries.items(): + if name in {"package", STAGING_PACKAGE}: + if not _safe_directory(metadata): + raise _fail() + elif not _safe_file( + metadata, + max_bytes=MAX_RUNTIME_BYTES if "runtime" in name else 0, + ): + raise _fail() + + if STAGING_PACKAGE in entries: + _read_existing_package(destination_fd, STAGING_PACKAGE, uid, gid) + _remove_tree(destination_fd, STAGING_PACKAGE) + if STAGING_RUNTIME in entries: + os.unlink(STAGING_RUNTIME, dir_fd=destination_fd) + with os.scandir(destination_fd) as iterator: + active = {entry.name for entry in iterator} + has_runtime = "runtime.yaml" in active + has_package = "package" in active + if has_runtime != has_package: + if has_runtime: + os.unlink("runtime.yaml", dir_fd=destination_fd) + if has_package: + _read_existing_package(destination_fd, "package", uid, gid) + _remove_tree(destination_fd, "package") + return + if has_runtime: + _read_existing_runtime(destination_fd, authority, uid, gid) + _read_existing_package(destination_fd, "package", uid, gid) + + +def _open_lock(destination_fd: int, uid: int, gid: int) -> int: + descriptor = os.open( + LOCK_NAME, + os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, + 0o600, + dir_fd=destination_fd, + ) + metadata = os.fstat(descriptor) + if not _safe_file(metadata, max_bytes=0): + os.close(descriptor) + raise _fail() + os.fchown(descriptor, uid, gid) + os.fchmod(descriptor, 0o600) + fcntl.flock(descriptor, fcntl.LOCK_EX) + return descriptor + + +def stage( + authority: str, + source: Path, + destination: Path, + *, + target_uid: int = TARGET_UID, + target_gid: int = TARGET_GID, +) -> None: + if authority not in AUTHORITIES or target_uid <= 0 or target_gid <= 0: + raise _fail() + runtime, package = _read_source(source, authority) + + destination_fd = _open_directory(destination) + lock_fd = -1 + try: + os.fchown(destination_fd, target_uid, target_gid) + os.fchmod(destination_fd, 0o700) + lock_fd = _open_lock(destination_fd, target_uid, target_gid) + _recover_destination(destination_fd, authority, target_uid, target_gid) + + with os.scandir(destination_fd) as iterator: + active = {entry.name for entry in iterator} + if "runtime.yaml" in active: + existing_runtime = _read_existing_runtime( + destination_fd, authority, target_uid, target_gid + ) + existing_package = _read_existing_package( + destination_fd, "package", target_uid, target_gid + ) + if existing_runtime != runtime or existing_package != package: + raise _fail() + return + + _write_package(destination_fd, package, target_uid, target_gid) + _write_file( + destination_fd, + STAGING_RUNTIME, + runtime, + target_uid, + target_gid, + ) + os.rename( + STAGING_PACKAGE, + "package", + src_dir_fd=destination_fd, + dst_dir_fd=destination_fd, + ) + os.replace( + STAGING_RUNTIME, + "runtime.yaml", + src_dir_fd=destination_fd, + dst_dir_fd=destination_fd, + ) + os.fsync(destination_fd) + _read_existing_runtime(destination_fd, authority, target_uid, target_gid) + _read_existing_package(destination_fd, "package", target_uid, target_gid) + except StagingError: + raise + except OSError: + raise _fail() from None + finally: + if lock_fd >= 0: + os.close(lock_fd) + os.close(destination_fd) + + +def main(argv: list[str] | None = None) -> int: + parser = QuietArgumentParser(add_help=False) + parser.add_argument("--authority", required=True) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--destination", type=Path, required=True) + parser.add_argument("command", choices=("stage",)) + try: + arguments = parser.parse_args(argv) + stage( + arguments.authority, + arguments.source, + arguments.destination, + ) + except (StagingError, OSError): + print(FAILURE_MESSAGE, file=sys.stderr) + return 1 + print(SUCCESS_MESSAGE) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/test_gen_secrets.py b/scripts/test_gen_secrets.py index 0e8406c..ae37f05 100644 --- a/scripts/test_gen_secrets.py +++ b/scripts/test_gen_secrets.py @@ -84,29 +84,6 @@ def test_generated_environment_preserves_runtime_secrets(self) -> None: for key, value in existing.items(): self.assertEqual(values[key], value) self.assertEqual(values["NIA_ESIGNET_CLIENT_PRIVATE_JWK"], "operator-jwk") - runtime_uid, runtime_gid = MODULE.local_runtime_identity() - self.assertEqual(values["SOLMARA_LOCAL_RUNTIME_UID"], str(runtime_uid)) - self.assertEqual(values["SOLMARA_LOCAL_RUNTIME_GID"], str(runtime_gid)) - - def test_local_runtime_identity_preserves_the_relay_trust_boundary(self) -> None: - with mock.patch.object(MODULE.sys, "platform", "darwin"): - self.assertEqual(MODULE.local_runtime_identity(), (65532, 65532)) - - metadata = MODULE.ROOT.joinpath("relays/cra/runtime.yaml").stat() - with mock.patch.object(MODULE.sys, "platform", "linux"): - expected = ( - (65532, 65532) - if metadata.st_uid == 0 - else (metadata.st_uid, metadata.st_gid or 65532) - ) - self.assertEqual(MODULE.local_runtime_identity(), expected) - - root_group_metadata = mock.Mock(st_uid=1000, st_gid=0) - with ( - mock.patch.object(MODULE.sys, "platform", "linux"), - mock.patch.object(Path, "stat", return_value=root_group_metadata), - ): - self.assertEqual(MODULE.local_runtime_identity(), (1000, 65532)) def test_generated_environment_rejects_duplicate_or_empty_values(self) -> None: with tempfile.TemporaryDirectory() as temporary: diff --git a/scripts/test_local_relay_runtime_stager.py b/scripts/test_local_relay_runtime_stager.py new file mode 100644 index 0000000..4ff0ef4 --- /dev/null +++ b/scripts/test_local_relay_runtime_stager.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +import importlib.util +import os +import shutil +import stat +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +import yaml + +SCRIPT = Path(__file__).with_name("local-relay-runtime-stager.py") +SPEC = importlib.util.spec_from_file_location("local_relay_runtime_stager", SCRIPT) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = MODULE +SPEC.loader.exec_module(MODULE) +ROOT = SCRIPT.parents[1] +AUTHORITIES = ("cra", "nia", "mosd", "sipf", "nagdi") + + +def isolated_source(root: Path, authority: str) -> Path: + source = root / "source" + source.mkdir(parents=True) + shutil.copy2(ROOT / "relays" / authority / "runtime.yaml", source / "runtime.yaml") + shutil.copytree(ROOT / "relays" / authority / "package", source / "package") + return source + + +class LocalRelayRuntimeStagerTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.source = isolated_source(self.root, "cra") + self.destination = self.root / "destination" + self.destination.mkdir() + self.uid = os.getuid() + self.gid = os.getgid() + + def tearDown(self) -> None: + self.temporary.cleanup() + + def stage(self) -> None: + MODULE.stage( + "cra", + self.source, + self.destination, + target_uid=self.uid, + target_gid=self.gid, + ) + + def test_stages_complete_package_and_preserves_matching_existing_volume( + self, + ) -> None: + self.stage() + + self.assertEqual( + {entry.name for entry in self.destination.iterdir()}, + {MODULE.LOCK_NAME, "runtime.yaml", "package"}, + ) + self.assertEqual( + (self.destination / "runtime.yaml").read_bytes(), + (self.source / "runtime.yaml").read_bytes(), + ) + source_files = { + path.relative_to(self.source / "package") + for path in (self.source / "package").rglob("*") + if path.is_file() + } + staged_files = { + path.relative_to(self.destination / "package") + for path in (self.destination / "package").rglob("*") + if path.is_file() + } + self.assertEqual(staged_files, source_files) + for path in (self.destination / "package").rglob("*"): + metadata = path.stat() + self.assertEqual((metadata.st_uid, metadata.st_gid), (self.uid, self.gid)) + self.assertEqual( + stat.S_IMODE(metadata.st_mode), 0o500 if path.is_dir() else 0o400 + ) + + first_inode = (self.destination / "package").stat().st_ino + self.stage() + self.assertEqual((self.destination / "package").stat().st_ino, first_inode) + self.assertFalse((self.destination / MODULE.STAGING_PACKAGE).exists()) + self.assertFalse((self.destination / MODULE.STAGING_RUNTIME).exists()) + + (self.source / "runtime.yaml").write_bytes( + (self.source / "runtime.yaml").read_bytes() + b"\n" + ) + with self.assertRaisesRegex(MODULE.StagingError, MODULE.FAILURE_MESSAGE): + self.stage() + self.assertEqual((self.destination / "package").stat().st_ino, first_inode) + + def test_source_inventory_rejects_links_unsafe_modes_and_extra_entries( + self, + ) -> None: + attacks = [] + + extra_source = isolated_source(self.root / "extra", "cra") + (extra_source / "unexpected").write_text("value", encoding="utf-8") + attacks.append(extra_source) + + symlink_source = isolated_source(self.root / "symlink", "cra") + artifact = next((symlink_source / "package" / "generated").rglob("*.json")) + artifact.unlink() + artifact.symlink_to(symlink_source / "runtime.yaml") + attacks.append(symlink_source) + + hardlink_source = isolated_source(self.root / "hardlink", "cra") + artifact = next((hardlink_source / "package" / "generated").rglob("*.json")) + os.link(artifact, hardlink_source / "package" / "duplicate.json") + attacks.append(hardlink_source) + + writable_source = isolated_source(self.root / "writable", "cra") + (writable_source / "runtime.yaml").chmod(0o666) + attacks.append(writable_source) + + for source in attacks: + with self.subTest(source=source.parent.name): + destination = source.parent / "destination" + destination.mkdir() + with self.assertRaisesRegex( + MODULE.StagingError, MODULE.FAILURE_MESSAGE + ): + MODULE.stage( + "cra", + source, + destination, + target_uid=self.uid, + target_gid=self.gid, + ) + + def test_package_bounds_manifest_digest_and_authority_binding_fail_closed( + self, + ) -> None: + digest_source = isolated_source(self.root / "digest", "cra") + artifact = next((digest_source / "package" / "generated").rglob("*.json")) + artifact.write_bytes(artifact.read_bytes() + b" ") + + oversized_source = isolated_source(self.root / "oversized", "cra") + (oversized_source / "runtime.yaml").write_bytes( + b"x" * (MODULE.MAX_RUNTIME_BYTES + 1) + ) + + wrong_authority_source = isolated_source(self.root / "authority", "nia") + + comment_bypass_source = isolated_source(self.root / "comment-bypass", "cra") + (comment_bypass_source / "runtime.yaml").write_text( + "# packagePath: /etc/relay/cra/package\n" + "packagePath: /tmp/not-the-staged-package\n", + encoding="utf-8", + ) + + hash_bypass_source = isolated_source(self.root / "hash-bypass", "cra") + (hash_bypass_source / "runtime.yaml").write_text( + "packagePath: /etc/relay/cra/package#outside-staged\n", + encoding="utf-8", + ) + + for label, source in ( + ("digest", digest_source), + ("oversized", oversized_source), + ("authority", wrong_authority_source), + ("comment-bypass", comment_bypass_source), + ("hash-bypass", hash_bypass_source), + ): + with self.subTest(label=label): + destination = source.parent / "destination" + destination.mkdir() + with self.assertRaisesRegex( + MODULE.StagingError, MODULE.FAILURE_MESSAGE + ): + MODULE.stage( + "cra", + source, + destination, + target_uid=self.uid, + target_gid=self.gid, + ) + + def test_read_detects_same_path_mutation_after_open(self) -> None: + directory_fd = os.open(self.source, os.O_RDONLY | os.O_DIRECTORY) + path = self.source / "runtime.yaml" + metadata = path.stat() + snapshot = MODULE._snapshot(metadata) + real_fstat = os.fstat + calls = 0 + + def racing_fstat(descriptor: int): + nonlocal calls + result = real_fstat(descriptor) + calls += 1 + if calls == 1: + path.write_bytes(path.read_bytes() + b"\n") + return result + + try: + with ( + mock.patch.object(MODULE.os, "fstat", side_effect=racing_fstat), + self.assertRaisesRegex(MODULE.StagingError, MODULE.FAILURE_MESSAGE), + ): + MODULE._read_exact( + directory_fd, + "runtime.yaml", + snapshot, + max_bytes=MODULE.MAX_RUNTIME_BYTES, + ) + finally: + os.close(directory_fd) + + def test_cli_failure_is_generic(self) -> None: + result = MODULE.main( + [ + "--authority", + "unknown-sensitive-authority", + "--source", + str(self.source), + "--destination", + str(self.destination), + "stage", + ] + ) + self.assertEqual(result, 1) + + +class LocalRelayRuntimeComposeTests(unittest.TestCase): + def test_each_relay_has_one_isolated_stager_and_fixed_runtime_identity( + self, + ) -> None: + compose = yaml.safe_load((ROOT / "compose.yaml").read_text(encoding="utf-8")) + services = compose["services"] + + for authority in AUTHORITIES: + stager = services[f"{authority}-relay-runtime-stager"] + relay = services[f"{authority}-relay"] + self.assertEqual(stager["network_mode"], "none") + self.assertTrue(stager["read_only"]) + self.assertEqual(stager["cap_drop"], ["ALL"]) + self.assertEqual( + set(stager["cap_add"]), {"CHOWN", "DAC_OVERRIDE", "FOWNER"} + ) + self.assertIn("no-new-privileges:true", stager["security_opt"]) + self.assertEqual(stager["restart"], "no") + volumes = set(stager["volumes"]) + self.assertIn( + f"./relays/{authority}/runtime.yaml:/source/runtime.yaml:ro", + volumes, + ) + self.assertIn(f"./relays/{authority}/package:/source/package:ro", volumes) + self.assertIn(f"{authority}-relay-runtime:/staged", volumes) + for other in set(AUTHORITIES) - {authority}: + self.assertFalse(any(f"relays/{other}" in volume for volume in volumes)) + + self.assertEqual(relay["user"], "65532:65532") + self.assertIn( + f"{authority}-relay-runtime:/etc/relay/{authority}:ro", + relay["volumes"], + ) + self.assertFalse( + any(f"./relays/{authority}/" in volume for volume in relay["volumes"]) + ) + self.assertEqual( + relay["depends_on"][f"{authority}-relay-runtime-stager"]["condition"], + "service_completed_successfully", + ) + + audit_initializer = services["relay-audit-init"] + self.assertNotIn("environment", audit_initializer) + self.assertIn("target_uid = 65532", audit_initializer["command"][2]) + self.assertIn("target_gid = 65532", audit_initializer["command"][2]) + + +if __name__ == "__main__": + unittest.main() From 60c7c1b4e8b659d83a1e6459fc2c7d89f8bcb5d4 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 06:26:16 +0700 Subject: [PATCH 28/53] fix(hosted): close authority rollout origins Signed-off-by: Jeremi Joslin --- compose.coolify.agriculture.yaml | 2 +- compose.coolify.esignet.yaml | 76 ++--- compose.coolify.interior.yaml | 4 +- compose.coolify.labour-pensions.yaml | 2 +- compose.coolify.provision.yaml | 24 +- compose.coolify.social-development.yaml | 2 +- compose.coolify.yaml | 115 ++++---- config/esignet/nginx-hosted.conf | 30 +- docker/esignet-ui/Dockerfile | 15 +- docker/esignet-ui/hosted-entrypoint.sh | 8 + docker/esignet-ui/render-hosted-nginx.sh | 39 +++ justfile | 2 +- scripts/check-coolify-compose.sh | 7 - scripts/provision-hosted-runtime.py | 125 +++++++- scripts/test_hosted_esignet_topology.py | 130 ++++++++ scripts/test_hosted_home_topology.py | 47 +-- scripts/test_hosted_provisioning_topology.py | 26 ++ scripts/test_provision_hosted_runtime.py | 294 ++++++++++++++++++- scripts/test_runtime_topology.py | 92 +++--- scripts/test_smoke_esignet.py | 17 +- 20 files changed, 842 insertions(+), 215 deletions(-) create mode 100755 docker/esignet-ui/hosted-entrypoint.sh create mode 100755 docker/esignet-ui/render-hosted-nginx.sh create mode 100644 scripts/test_hosted_esignet_topology.py diff --git a/compose.coolify.agriculture.yaml b/compose.coolify.agriculture.yaml index 090604c..436ca25 100644 --- a/compose.coolify.agriculture.yaml +++ b/compose.coolify.agriculture.yaml @@ -31,7 +31,7 @@ services: command: [serve, --runtime, /etc/relay/nagdi/runtime.yaml] environment: {SOLMARA_RELAY_AUDIT_KEY: "${NAGDI_RELAY_AUDIT_KEY:?required}", SOLMARA_RELAY_CURSOR_KEY: "${NAGDI_RELAY_CURSOR_KEY:?required}"} volumes: [nagdi-relay-runtime:/etc/relay/nagdi:ro, nagdi-relay-source:/var/lib/relay/source:ro, nagdi-relay-audit:/var/lib/relay/audit] - labels: {solmara.lab.host: nagdi-relay.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: nagdi-relay-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells} nagdi-evidence: <<: *runtime diff --git a/compose.coolify.esignet.yaml b/compose.coolify.esignet.yaml index 618d84a..aae556c 100644 --- a/compose.coolify.esignet.yaml +++ b/compose.coolify.esignet.yaml @@ -1,20 +1,24 @@ +name: solmara-esignet-authority-cells + services: esignet-database: image: ${SOLMARA_ESIGNET_POSTGRES_IMAGE:?set digest-pinned eSignet database image} - environment: {POSTGRES_USER: esignet, POSTGRES_PASSWORD: "${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?required}"} - volumes: ["esignet-db-data:/var/lib/postgresql"] + environment: + POSTGRES_USER: esignet + POSTGRES_PASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?required} + volumes: [esignet-db-data:/var/lib/postgresql] healthcheck: - test: ["CMD-SHELL", "pg_isready -U esignet -d esignet"] + test: [CMD-SHELL, pg_isready -U esignet -d esignet] interval: 5s timeout: 5s retries: 30 esignet-redis: image: ${ESIGNET_REDIS_IMAGE:?versions.env must pin Redis} - command: ["redis-server"] - volumes: ["esignet-redis-data:/data"] + command: [redis-server] + volumes: [esignet-redis-data:/data] healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: [CMD, redis-cli, ping] interval: 10s timeout: 5s retries: 30 @@ -40,10 +44,10 @@ services: SPRING_AUTOCONFIGURE_EXCLUDE: org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration SPRING_CACHE_TYPE: redis SPRING_DATA_REDIS_HOST: esignet-redis - MOSIP_ESIGNET_HOST: esignet.solmara.registrystack.org - MOSIP_ESIGNET_DISCOVERY_ISSUER_ID: https://esignet.solmara.registrystack.org - MOSIP_ESIGNET_DISCOVERY_KEY_VALUES: "{'issuer':'https://esignet.solmara.registrystack.org','authorization_endpoint':'https://esignet-ui.solmara.registrystack.org/authorize','token_endpoint':'https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token','userinfo_endpoint':'https://esignet.solmara.registrystack.org/v1/esignet/oidc/userinfo','jwks_uri':'https://esignet.solmara.registrystack.org/v1/esignet/oauth/.well-known/jwks.json','token_endpoint_auth_methods_supported':{'private_key_jwt'},'token_endpoint_auth_signing_alg_values_supported':{'RS256','PS256','ES256'}}" - MOSIP_ESIGNET_OAUTH_KEY_VALUES: "{'issuer':'https://esignet.solmara.registrystack.org','authorization_endpoint':'https://esignet-ui.solmara.registrystack.org/authorize','token_endpoint':'https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token','jwks_uri':'https://esignet.solmara.registrystack.org/v1/esignet/oauth/.well-known/jwks.json','token_endpoint_auth_methods_supported':{'private_key_jwt'},'token_endpoint_auth_signing_alg_values_supported':{'RS256','PS256','ES256'}}" + MOSIP_ESIGNET_HOST: ${SOLMARA_ESIGNET_PUBLIC_HOST:-esignet.solmara.registrystack.org} + MOSIP_ESIGNET_DISCOVERY_ISSUER_ID: ${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org} + MOSIP_ESIGNET_DISCOVERY_KEY_VALUES: "{'issuer':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}','authorization_endpoint':'${SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL:-https://esignet-ui.solmara.registrystack.org}/authorize','token_endpoint':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oauth/v2/token','userinfo_endpoint':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oidc/userinfo','jwks_uri':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oauth/.well-known/jwks.json','token_endpoint_auth_methods_supported':{'private_key_jwt'},'token_endpoint_auth_signing_alg_values_supported':{'RS256','PS256','ES256'}}" + MOSIP_ESIGNET_OAUTH_KEY_VALUES: "{'issuer':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}','authorization_endpoint':'${SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL:-https://esignet-ui.solmara.registrystack.org}/authorize','token_endpoint':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oauth/v2/token','jwks_uri':'${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oauth/.well-known/jwks.json','token_endpoint_auth_methods_supported':{'private_key_jwt'},'token_endpoint_auth_signing_alg_values_supported':{'RS256','PS256','ES256'}}" MOSIP_ESIGNET_DATABASE_URL: jdbc:postgresql://esignet-database:5432/mosip_esignet?currentSchema=esignet MOSIP_ESIGNET_DATABASE_USERNAME: esignet MOSIP_ESIGNET_DATABASE_PASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD:?required} @@ -51,13 +55,13 @@ services: MOSIP_ESIGNET_INTEGRATION_AUTHENTICATOR: RelayAuthenticationService MOSIP_ESIGNET_INTEGRATION_KEY_BINDER: MockKeyBindingWrapperService MOSIP_ESIGNET_OPENID_SCOPE_CLAIMS: "{'profile' : {'given_name','family_name','gender','birthdate','individual_id'}}" - REGISTRY_RELAY_BASE_URL: https://nia-relay.solmara.registrystack.org + REGISTRY_RELAY_BASE_URL: https://nia-relay-authority-cells.solmara.registrystack.org REGISTRY_RELAY_RESOURCE: population-person REGISTRY_RELAY_LOOKUP: esignet-userinfo REGISTRY_RELAY_ACCESS_PROFILE: esignet REGISTRY_RELAY_DEFAULT_CLAIMS: individualId,givenName,familyName,birthdate,gender SPRING_APPLICATION_JSON: '{"registry":{"esignet":{"claim-map":{"sub":"$$psut","individual_id":"individualId","given_name":"givenName","family_name":"familyName","birthdate":"birthdate","gender":"gender"}}}}' - REGISTRY_MINT_TOKEN_ENDPOINT: https://mint.solmara.registrystack.org/token + REGISTRY_MINT_TOKEN_ENDPOINT: https://mint-authority-cells.solmara.registrystack.org/token REGISTRY_MINT_CLIENT_ID: nia-esignet REGISTRY_MINT_PRIVATE_JWK: ${NIA_ESIGNET_CLIENT_PRIVATE_JWK:?required} REGISTRY_MINT_TOKEN_CACHE_MAX_SECONDS: "300" @@ -74,14 +78,26 @@ services: REGISTRY_ESIGNET_KYC_SIGNING_KEY_ALIAS: esignet-relay-kyc REGISTRY_ESIGNET_KYC_SIGNING_KEY_PASSWORD: ${REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD:?required} MOSIP_KERNEL_KEYMANAGER_HSM_CONFIG_PATH: /home/mosip/keystore/esignet_local.p12 - volumes: ["esignet-keystore:/home/mosip/keystore", "esignet-kyc-signing:/home/mosip/kyc-signing"] - depends_on: {esignet-database: {condition: service_healthy}, esignet-redis: {condition: service_healthy}} - labels: {solmara.lab.host: esignet.solmara.registrystack.org} + volumes: + - esignet-keystore:/home/mosip/keystore + - esignet-kyc-signing:/home/mosip/kyc-signing + depends_on: + esignet-database: {condition: service_healthy} + esignet-redis: {condition: service_healthy} + labels: + solmara.lab.host: ${SOLMARA_ESIGNET_PUBLIC_HOST:-esignet.solmara.registrystack.org} + solmara.rollout.slot: authority-cells esignet-ui: image: ${SOLMARA_ESIGNET_UI_IMAGE:?set digest-pinned eSignet UI image} - depends_on: {esignet: {condition: service_started}} - labels: {solmara.lab.host: esignet-ui.solmara.registrystack.org} + environment: + SOLMARA_ESIGNET_PUBLIC_HOST: ${SOLMARA_ESIGNET_PUBLIC_HOST:-esignet.solmara.registrystack.org} + SOLMARA_ESIGNET_UI_PUBLIC_HOST: ${SOLMARA_ESIGNET_UI_PUBLIC_HOST:-esignet-ui.solmara.registrystack.org} + depends_on: + esignet: {condition: service_started} + labels: + solmara.lab.host: ${SOLMARA_ESIGNET_UI_PUBLIC_HOST:-esignet-ui.solmara.registrystack.org} + solmara.rollout.slot: authority-cells esignet-seed: image: ${SOLMARA_ESIGNET_SEED_IMAGE:?set digest-pinned eSignet seed image} @@ -94,27 +110,11 @@ services: ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?required} ESIGNET_REDIS_HOST: esignet-redis ESIGNET_REQUIRE_HTTPS_REDIRECTS: "true" - ESIGNET_CLIENT_REDIRECT_URIS_JSON: '["https://portal.solmara.registrystack.org/auth/callback"]' - entrypoint: ["seed-esignet.py"] - depends_on: {esignet-database: {condition: service_healthy}, esignet: {condition: service_started}} - - # This file is an overlay on compose.coolify.yaml. It switches the main - # Portal from mock login to the hosted eSignet client seeded above. - portal: - environment: - PORTAL_AUTH_PROVIDER: esignet - PORTAL_SECURE_COOKIES: "true" - PORTAL_ESIGNET_CLIENT_ID: ${PORTAL_ESIGNET_CLIENT_ID:-solmara-portal} - PORTAL_ESIGNET_CLIENT_KEY_ID: ${PORTAL_ESIGNET_CLIENT_KEY_ID:-solmara-portal-key-1} - PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?required} - PORTAL_ESIGNET_ISSUER: https://esignet.solmara.registrystack.org - PORTAL_ESIGNET_AUTHORIZATION_ENDPOINT: https://esignet-ui.solmara.registrystack.org/authorize - PORTAL_ESIGNET_TOKEN_ENDPOINT: https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token - PORTAL_ESIGNET_CLIENT_ASSERTION_AUDIENCE: https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token - PORTAL_ESIGNET_USERINFO_ENDPOINT: https://esignet.solmara.registrystack.org/v1/esignet/oidc/userinfo - PORTAL_ESIGNET_REDIRECT_URI: https://portal.solmara.registrystack.org/auth/callback - PORTAL_ESIGNET_SCOPE: openid profile - PORTAL_ESIGNET_SUBJECT_CLAIM: sub + ESIGNET_CLIENT_REDIRECT_URIS_JSON: '["${SOLMARA_PORTAL_PUBLIC_BASE_URL:-https://portal.solmara.registrystack.org}/auth/callback"]' + entrypoint: [seed-esignet.py] + depends_on: + esignet-database: {condition: service_healthy} + esignet: {condition: service_started} volumes: esignet-db-data: diff --git a/compose.coolify.interior.yaml b/compose.coolify.interior.yaml index 8c8830b..170d71e 100644 --- a/compose.coolify.interior.yaml +++ b/compose.coolify.interior.yaml @@ -31,7 +31,7 @@ services: command: [serve, --runtime, /etc/relay/cra/runtime.yaml] environment: {SOLMARA_RELAY_AUDIT_KEY: "${CRA_RELAY_AUDIT_KEY:?required}"} volumes: [cra-relay-runtime:/etc/relay/cra:ro, cra-relay-source:/var/lib/relay/source:ro, cra-relay-audit:/var/lib/relay/audit] - labels: {solmara.lab.host: cra-relay.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: cra-relay-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells} nia-relay: <<: *runtime @@ -39,7 +39,7 @@ services: command: [serve, --runtime, /etc/relay/nia/runtime.yaml] environment: {SOLMARA_RELAY_AUDIT_KEY: "${NIA_RELAY_AUDIT_KEY:?required}"} volumes: [nia-relay-runtime:/etc/relay/nia:ro, nia-relay-source:/var/lib/relay/source:ro, nia-relay-audit:/var/lib/relay/audit] - labels: {solmara.lab.host: nia-relay.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: nia-relay-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells} cra-evidence: <<: *runtime diff --git a/compose.coolify.labour-pensions.yaml b/compose.coolify.labour-pensions.yaml index 2eec0b7..ea76684 100644 --- a/compose.coolify.labour-pensions.yaml +++ b/compose.coolify.labour-pensions.yaml @@ -31,7 +31,7 @@ services: command: [serve, --runtime, /etc/relay/sipf/runtime.yaml] environment: {SOLMARA_RELAY_AUDIT_KEY: "${SIPF_RELAY_AUDIT_KEY:?required}", SOLMARA_RELAY_CURSOR_KEY: "${SIPF_RELAY_CURSOR_KEY:?required}"} volumes: [sipf-relay-runtime:/etc/relay/sipf:ro, sipf-relay-source:/var/lib/relay/source:ro, sipf-relay-audit:/var/lib/relay/audit] - labels: {solmara.lab.host: sipf-relay.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: sipf-relay-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells} sipf-evidence: <<: *runtime diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml index b47ca3d..687e90b 100644 --- a/compose.coolify.provision.yaml +++ b/compose.coolify.provision.yaml @@ -44,32 +44,32 @@ x-signer: &signer services: cra-relay-provisioner: <<: *provisioner - command: [provision, --target, cra-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source] + command: [provision, --target, cra-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [cra-relay-runtime:/provisioned/runtime, cra-relay-source:/provisioned/source] nia-relay-provisioner: <<: *provisioner - command: [provision, --target, nia-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source] + command: [provision, --target, nia-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [nia-relay-runtime:/provisioned/runtime, nia-relay-source:/provisioned/source] mosd-relay-provisioner: <<: *provisioner - command: [provision, --target, mosd-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source] + command: [provision, --target, mosd-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [mosd-relay-runtime:/provisioned/runtime, mosd-relay-source:/provisioned/source] sipf-relay-provisioner: <<: *provisioner - command: [provision, --target, sipf-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source] + command: [provision, --target, sipf-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [sipf-relay-runtime:/provisioned/runtime, sipf-relay-source:/provisioned/source] nagdi-relay-provisioner: <<: *provisioner - command: [provision, --target, nagdi-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source] + command: [provision, --target, nagdi-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [nagdi-relay-runtime:/provisioned/runtime, nagdi-relay-source:/provisioned/source] cra-evidence-provisioner: <<: *provisioner - command: [provision, --target, cra-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.21] + command: [provision, --target, cra-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.21, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://cra-relay-authority-cells.solmara.registrystack.org] volumes: [cra-evidence-runtime:/provisioned/runtime, cra-evidence-secrets:/provisioned/secrets, cra-evidence-extracts:/provisioned/extracts] secrets: - {source: cra-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -80,7 +80,7 @@ services: nia-evidence-provisioner: <<: *provisioner - command: [provision, --target, nia-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.22] + command: [provision, --target, nia-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.22, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [nia-evidence-runtime:/provisioned/runtime, nia-evidence-secrets:/provisioned/secrets, nia-evidence-extracts:/provisioned/extracts] secrets: - {source: nia-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -89,7 +89,7 @@ services: sro-evidence-provisioner: <<: *provisioner - command: [provision, --target, sro-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.3.23] + command: [provision, --target, sro-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.3.23, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [sro-evidence-runtime:/provisioned/runtime, sro-evidence-secrets:/provisioned/secrets, sro-evidence-extracts:/provisioned/extracts] secrets: - {source: sro-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -98,7 +98,7 @@ services: mosd-evidence-provisioner: <<: *provisioner - command: [provision, --target, mosd-programme-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.3.24] + command: [provision, --target, mosd-programme-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.3.24, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://mosd-programme-relay-authority-cells.solmara.registrystack.org] volumes: [mosd-evidence-runtime:/provisioned/runtime, mosd-evidence-secrets:/provisioned/secrets] secrets: - {source: mosd-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -108,7 +108,7 @@ services: sipf-evidence-provisioner: <<: *provisioner - command: [provision, --target, sipf-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.4.25] + command: [provision, --target, sipf-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.4.25, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://sipf-relay-authority-cells.solmara.registrystack.org] volumes: [sipf-evidence-runtime:/provisioned/runtime, sipf-evidence-secrets:/provisioned/secrets] secrets: - {source: sipf-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -119,7 +119,7 @@ services: nagdi-evidence-provisioner: <<: *provisioner - command: [provision, --target, nagdi-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.5.26] + command: [provision, --target, nagdi-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.5.26, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://nagdi-relay-authority-cells.solmara.registrystack.org] volumes: [nagdi-evidence-runtime:/provisioned/runtime, nagdi-evidence-secrets:/provisioned/secrets] secrets: - {source: nagdi-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} @@ -130,7 +130,7 @@ services: mint-provisioner: <<: *provisioner - command: [provision, --target, mint, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.20] + command: [provision, --target, mint, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.20, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [mint-runtime:/provisioned/runtime, mint-secrets:/provisioned/secrets] secrets: - {source: mint-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} diff --git a/compose.coolify.social-development.yaml b/compose.coolify.social-development.yaml index 64d8a8d..5268390 100644 --- a/compose.coolify.social-development.yaml +++ b/compose.coolify.social-development.yaml @@ -31,7 +31,7 @@ services: command: [serve, --runtime, /etc/relay/mosd/runtime.yaml] environment: {SOLMARA_RELAY_AUDIT_KEY: "${MOSD_RELAY_AUDIT_KEY:?required}"} volumes: [mosd-relay-runtime:/etc/relay/mosd:ro, mosd-relay-source:/var/lib/relay/source:ro, mosd-relay-audit:/var/lib/relay/audit] - labels: {solmara.lab.host: mosd-programme-relay.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: mosd-programme-relay-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells} sro-evidence: <<: *runtime diff --git a/compose.coolify.yaml b/compose.coolify.yaml index ab80a60..b64986a 100644 --- a/compose.coolify.yaml +++ b/compose.coolify.yaml @@ -1,16 +1,16 @@ name: solmara-lab x-hosted-evidence-environment: &hosted-evidence SOLMARA_LAB_MODE: hosted - SOLMARA_MINT_URL: https://mint.solmara.registrystack.org - SOLMARA_MINT_ASSERTION_AUDIENCE: https://mint.solmara.registrystack.org/token + SOLMARA_MINT_URL: https://mint-authority-cells.solmara.registrystack.org + SOLMARA_MINT_ASSERTION_AUDIENCE: https://mint-authority-cells.solmara.registrystack.org/token SOLMARA_EVIDENCE_CLIENT_ID: solmara-demo SOLMARA_EVIDENCE_CLIENT_KEY: /run/secrets/solmara-evidence-client.jwk - SOLMARA_CRA_EVIDENCE_URL: https://cra-evidence.solmara.registrystack.org - SOLMARA_NIA_EVIDENCE_URL: https://nia-evidence.solmara.registrystack.org - SOLMARA_SRO_EVIDENCE_URL: https://sro-evidence.solmara.registrystack.org - SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL: https://mosd-programme-evidence.solmara.registrystack.org - SOLMARA_SIPF_EVIDENCE_URL: https://sipf-evidence.solmara.registrystack.org - SOLMARA_NAGDI_EVIDENCE_URL: https://nagdi-evidence.solmara.registrystack.org + SOLMARA_CRA_EVIDENCE_URL: ${SOLMARA_CRA_EVIDENCE_PUBLIC_BASE_URL:-https://cra-evidence.solmara.registrystack.org} + SOLMARA_NIA_EVIDENCE_URL: ${SOLMARA_NIA_EVIDENCE_PUBLIC_BASE_URL:-https://nia-evidence.solmara.registrystack.org} + SOLMARA_SRO_EVIDENCE_URL: ${SOLMARA_SRO_EVIDENCE_PUBLIC_BASE_URL:-https://sro-evidence.solmara.registrystack.org} + SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL: ${SOLMARA_MOSD_PROGRAMME_EVIDENCE_PUBLIC_BASE_URL:-https://mosd-programme-evidence.solmara.registrystack.org} + SOLMARA_SIPF_EVIDENCE_URL: ${SOLMARA_SIPF_EVIDENCE_PUBLIC_BASE_URL:-https://sipf-evidence.solmara.registrystack.org} + SOLMARA_NAGDI_EVIDENCE_URL: ${SOLMARA_NAGDI_EVIDENCE_PUBLIC_BASE_URL:-https://nagdi-evidence.solmara.registrystack.org} CHILD_BENEFIT_FEDERATOR_TOKEN: ${CHILD_BENEFIT_FEDERATOR_TOKEN:?required} services: audit-permissions: @@ -36,23 +36,23 @@ services: networks: {runtime: {ipv4_address: 172.29.1.20}} depends_on: audit-permissions: {condition: service_completed_successfully} - labels: {solmara.lab.host: mint.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: mint-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells} static-metadata: image: ${SOLMARA_STATIC_METADATA_IMAGE:?set a digest-pinned Solmara static metadata image} restart: unless-stopped - labels: {solmara.lab.host: metadata.solmara.registrystack.org} + labels: {solmara.lab.host: "${SOLMARA_METADATA_PUBLIC_HOST:-metadata.solmara.registrystack.org}"} scenario-runner: image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set a digest-pinned Solmara scenario runner image} restart: unless-stopped environment: <<: *hosted-evidence - CHILD_BENEFIT_FEDERATOR_URL: https://child-benefit.solmara.registrystack.org + CHILD_BENEFIT_FEDERATOR_URL: ${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_BASE_URL:-https://child-benefit.solmara.registrystack.org} SCENARIO_RUNNER_HOST: 0.0.0.0 SCENARIO_RUNNER_PORT: "8080" secrets: - source: solmara-demo-client-key target: solmara-evidence-client.jwk - labels: {solmara.lab.host: scenarios.solmara.registrystack.org} + labels: {solmara.lab.host: "${SOLMARA_SCENARIO_RUNNER_PUBLIC_HOST:-scenarios.solmara.registrystack.org}"} child-benefit-federator: image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set a digest-pinned Solmara scenario runner image} command: ["python", "scenario-runner/child_benefit_federator.py"] @@ -64,61 +64,72 @@ services: secrets: - source: solmara-demo-client-key target: solmara-evidence-client.jwk - labels: {solmara.lab.host: child-benefit.solmara.registrystack.org} + labels: {solmara.lab.host: "${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_HOST:-child-benefit.solmara.registrystack.org}"} home: image: ${SOLMARA_HOME_IMAGE:?set a digest-pinned Solmara home image} restart: unless-stopped environment: - PORTAL_URL: https://portal.solmara.registrystack.org - STATIC_METADATA_URL: https://metadata.solmara.registrystack.org - CRA_CIVIL_RELAY_URL: https://cra-relay.solmara.registrystack.org - NIA_POPULATION_RELAY_URL: https://nia-relay.solmara.registrystack.org - MOSD_PROGRAMME_RELAY_URL: https://mosd-programme-relay.solmara.registrystack.org - SIPF_PENSIONS_RELAY_URL: https://sipf-relay.solmara.registrystack.org - NAGDI_AGRICULTURE_RELAY_URL: https://nagdi-relay.solmara.registrystack.org - SOLMARA_CRA_EVIDENCE_URL: https://cra-evidence.solmara.registrystack.org - SOLMARA_NIA_EVIDENCE_URL: https://nia-evidence.solmara.registrystack.org - SOLMARA_SRO_EVIDENCE_URL: https://sro-evidence.solmara.registrystack.org - SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL: https://mosd-programme-evidence.solmara.registrystack.org - SOLMARA_SIPF_EVIDENCE_URL: https://sipf-evidence.solmara.registrystack.org - SOLMARA_NAGDI_EVIDENCE_URL: https://nagdi-evidence.solmara.registrystack.org - MINT_URL: https://mint.solmara.registrystack.org - CHILD_BENEFIT_FEDERATOR_URL: https://child-benefit.solmara.registrystack.org - SCENARIO_RUNNER_URL: https://scenarios.solmara.registrystack.org - PORTAL_PROBE_URL: https://portal.solmara.registrystack.org + PORTAL_URL: ${SOLMARA_PORTAL_PUBLIC_BASE_URL:-https://portal.solmara.registrystack.org} + STATIC_METADATA_URL: ${SOLMARA_METADATA_PUBLIC_BASE_URL:-https://metadata.solmara.registrystack.org} + CRA_CIVIL_RELAY_URL: https://cra-relay-authority-cells.solmara.registrystack.org + NIA_POPULATION_RELAY_URL: https://nia-relay-authority-cells.solmara.registrystack.org + MOSD_PROGRAMME_RELAY_URL: https://mosd-programme-relay-authority-cells.solmara.registrystack.org + SIPF_PENSIONS_RELAY_URL: https://sipf-relay-authority-cells.solmara.registrystack.org + NAGDI_AGRICULTURE_RELAY_URL: https://nagdi-relay-authority-cells.solmara.registrystack.org + SOLMARA_CRA_EVIDENCE_URL: ${SOLMARA_CRA_EVIDENCE_PUBLIC_BASE_URL:-https://cra-evidence.solmara.registrystack.org} + SOLMARA_NIA_EVIDENCE_URL: ${SOLMARA_NIA_EVIDENCE_PUBLIC_BASE_URL:-https://nia-evidence.solmara.registrystack.org} + SOLMARA_SRO_EVIDENCE_URL: ${SOLMARA_SRO_EVIDENCE_PUBLIC_BASE_URL:-https://sro-evidence.solmara.registrystack.org} + SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL: ${SOLMARA_MOSD_PROGRAMME_EVIDENCE_PUBLIC_BASE_URL:-https://mosd-programme-evidence.solmara.registrystack.org} + SOLMARA_SIPF_EVIDENCE_URL: ${SOLMARA_SIPF_EVIDENCE_PUBLIC_BASE_URL:-https://sipf-evidence.solmara.registrystack.org} + SOLMARA_NAGDI_EVIDENCE_URL: ${SOLMARA_NAGDI_EVIDENCE_PUBLIC_BASE_URL:-https://nagdi-evidence.solmara.registrystack.org} + MINT_URL: https://mint-authority-cells.solmara.registrystack.org + CHILD_BENEFIT_FEDERATOR_URL: ${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_BASE_URL:-https://child-benefit.solmara.registrystack.org} + SCENARIO_RUNNER_URL: ${SOLMARA_SCENARIO_RUNNER_PUBLIC_BASE_URL:-https://scenarios.solmara.registrystack.org} + PORTAL_PROBE_URL: ${SOLMARA_PORTAL_PUBLIC_BASE_URL:-https://portal.solmara.registrystack.org} SOLMARA_PUBLIC_URL_MAP: >- - {"child-benefit-federator:8080":"https://child-benefit.solmara.registrystack.org", - "deterministic-publisher:8080":"https://metadata.solmara.registrystack.org", - "cra-evidence:8080":"https://cra-evidence.solmara.registrystack.org", - "nia-evidence:8080":"https://nia-evidence.solmara.registrystack.org", - "sro-evidence:8080":"https://sro-evidence.solmara.registrystack.org", - "mosd-programme-evidence:8080":"https://mosd-programme-evidence.solmara.registrystack.org", - "sipf-evidence:8080":"https://sipf-evidence.solmara.registrystack.org", - "nagdi-evidence:8080":"https://nagdi-evidence.solmara.registrystack.org", - "mint:8081":"https://mint.solmara.registrystack.org", - "cra-relay:8080":"https://cra-relay.solmara.registrystack.org", - "nia-relay:8080":"https://nia-relay.solmara.registrystack.org", - "mosd-relay:8080":"https://mosd-programme-relay.solmara.registrystack.org", - "sipf-relay:8080":"https://sipf-relay.solmara.registrystack.org", - "nagdi-relay:8080":"https://nagdi-relay.solmara.registrystack.org", - "static-metadata:8080":"https://metadata.solmara.registrystack.org", - "scenario-runner:8080":"https://scenarios.solmara.registrystack.org", - "portal:4000":"https://portal.solmara.registrystack.org"} - labels: {solmara.lab.host: solmara.registrystack.org} + {"child-benefit-federator:8080":"${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_BASE_URL:-https://child-benefit.solmara.registrystack.org}", + "deterministic-publisher:8080":"${SOLMARA_METADATA_PUBLIC_BASE_URL:-https://metadata.solmara.registrystack.org}", + "cra-evidence:8080":"${SOLMARA_CRA_EVIDENCE_PUBLIC_BASE_URL:-https://cra-evidence.solmara.registrystack.org}", + "nia-evidence:8080":"${SOLMARA_NIA_EVIDENCE_PUBLIC_BASE_URL:-https://nia-evidence.solmara.registrystack.org}", + "sro-evidence:8080":"${SOLMARA_SRO_EVIDENCE_PUBLIC_BASE_URL:-https://sro-evidence.solmara.registrystack.org}", + "mosd-programme-evidence:8080":"${SOLMARA_MOSD_PROGRAMME_EVIDENCE_PUBLIC_BASE_URL:-https://mosd-programme-evidence.solmara.registrystack.org}", + "sipf-evidence:8080":"${SOLMARA_SIPF_EVIDENCE_PUBLIC_BASE_URL:-https://sipf-evidence.solmara.registrystack.org}", + "nagdi-evidence:8080":"${SOLMARA_NAGDI_EVIDENCE_PUBLIC_BASE_URL:-https://nagdi-evidence.solmara.registrystack.org}", + "mint:8081":"https://mint-authority-cells.solmara.registrystack.org", + "cra-relay:8080":"https://cra-relay-authority-cells.solmara.registrystack.org", + "nia-relay:8080":"https://nia-relay-authority-cells.solmara.registrystack.org", + "mosd-relay:8080":"https://mosd-programme-relay-authority-cells.solmara.registrystack.org", + "sipf-relay:8080":"https://sipf-relay-authority-cells.solmara.registrystack.org", + "nagdi-relay:8080":"https://nagdi-relay-authority-cells.solmara.registrystack.org", + "static-metadata:8080":"${SOLMARA_METADATA_PUBLIC_BASE_URL:-https://metadata.solmara.registrystack.org}", + "scenario-runner:8080":"${SOLMARA_SCENARIO_RUNNER_PUBLIC_BASE_URL:-https://scenarios.solmara.registrystack.org}", + "portal:4000":"${SOLMARA_PORTAL_PUBLIC_BASE_URL:-https://portal.solmara.registrystack.org}"} + labels: {solmara.lab.host: "${SOLMARA_HOME_PUBLIC_HOST:-solmara.registrystack.org}"} portal: image: ${SOLMARA_PORTAL_IMAGE:?set a digest-pinned Solmara portal image} restart: unless-stopped environment: <<: *hosted-evidence PORTAL_PROVIDER: live - PORTAL_AUTH_PROVIDER: mock + PORTAL_AUTH_PROVIDER: ${PORTAL_AUTH_PROVIDER:-mock} PORTAL_SECURE_COOKIES: "true" - SCENARIO_RUNNER_URL: https://scenarios.solmara.registrystack.org - CHILD_BENEFIT_FEDERATOR_URL: https://child-benefit.solmara.registrystack.org + PORTAL_ESIGNET_CLIENT_ID: ${PORTAL_ESIGNET_CLIENT_ID:-solmara-portal} + PORTAL_ESIGNET_CLIENT_KEY_ID: ${PORTAL_ESIGNET_CLIENT_KEY_ID:-solmara-portal-key-1} + PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64: ${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:-} + PORTAL_ESIGNET_ISSUER: ${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org} + PORTAL_ESIGNET_AUTHORIZATION_ENDPOINT: ${SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL:-https://esignet-ui.solmara.registrystack.org}/authorize + PORTAL_ESIGNET_TOKEN_ENDPOINT: ${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oauth/v2/token + PORTAL_ESIGNET_CLIENT_ASSERTION_AUDIENCE: ${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oauth/v2/token + PORTAL_ESIGNET_USERINFO_ENDPOINT: ${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oidc/userinfo + PORTAL_ESIGNET_REDIRECT_URI: ${SOLMARA_PORTAL_PUBLIC_BASE_URL:-https://portal.solmara.registrystack.org}/auth/callback + PORTAL_ESIGNET_SCOPE: openid profile + PORTAL_ESIGNET_SUBJECT_CLAIM: sub + SCENARIO_RUNNER_URL: ${SOLMARA_SCENARIO_RUNNER_PUBLIC_BASE_URL:-https://scenarios.solmara.registrystack.org} + CHILD_BENEFIT_FEDERATOR_URL: ${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_BASE_URL:-https://child-benefit.solmara.registrystack.org} secrets: - source: solmara-demo-client-key target: solmara-evidence-client.jwk - labels: {solmara.lab.host: portal.solmara.registrystack.org} + labels: {solmara.lab.host: "${SOLMARA_PORTAL_PUBLIC_HOST:-portal.solmara.registrystack.org}"} secrets: solmara-demo-client-key: environment: SOLMARA_DEMO_CLIENT_PRIVATE_JWK diff --git a/config/esignet/nginx-hosted.conf b/config/esignet/nginx-hosted.conf index 03d503c..b0fd9c0 100644 --- a/config/esignet/nginx-hosted.conf +++ b/config/esignet/nginx-hosted.conf @@ -1,4 +1,5 @@ worker_processes 1; +pid /tmp/nginx.pid; events { worker_connections 1024; @@ -7,10 +8,15 @@ events { http { access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; + client_body_temp_path /tmp/nginx-client-body; + proxy_temp_path /tmp/nginx-proxy; + fastcgi_temp_path /tmp/nginx-fastcgi; + uwsgi_temp_path /tmp/nginx-uwsgi; + scgi_temp_path /tmp/nginx-scgi; server { listen 3000; - server_name esignet-ui.solmara.registrystack.org; + server_name __ESIGNET_UI_PUBLIC_HOST__; server_tokens off; root /usr/share/nginx/html; @@ -25,10 +31,10 @@ http { location /v1/esignet { proxy_pass http://esignet:8088/v1/esignet; proxy_redirect off; - proxy_set_header Host esignet.solmara.registrystack.org; + proxy_set_header Host __ESIGNET_PUBLIC_HOST__; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Host esignet.solmara.registrystack.org; + proxy_set_header X-Forwarded-Host __ESIGNET_PUBLIC_HOST__; proxy_set_header X-Forwarded-Proto https; add_header Content-Security-Policy "default-src 'none'" always; add_header Referrer-Policy "no-referrer" always; @@ -37,10 +43,10 @@ http { location /.well-known/openid-configuration { proxy_pass http://esignet:8088/v1/esignet/oidc/.well-known/openid-configuration; proxy_redirect off; - proxy_set_header Host esignet.solmara.registrystack.org; + proxy_set_header Host __ESIGNET_PUBLIC_HOST__; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Host esignet.solmara.registrystack.org; + proxy_set_header X-Forwarded-Host __ESIGNET_PUBLIC_HOST__; proxy_set_header X-Forwarded-Proto https; add_header Content-Security-Policy "default-src 'none'" always; add_header Referrer-Policy "no-referrer" always; @@ -49,10 +55,10 @@ http { location /.well-known/jwks.json { proxy_pass http://esignet:8088/v1/esignet/oauth/.well-known/jwks.json; proxy_redirect off; - proxy_set_header Host esignet.solmara.registrystack.org; + proxy_set_header Host __ESIGNET_PUBLIC_HOST__; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Host esignet.solmara.registrystack.org; + proxy_set_header X-Forwarded-Host __ESIGNET_PUBLIC_HOST__; proxy_set_header X-Forwarded-Proto https; add_header Content-Security-Policy "default-src 'none'" always; add_header Referrer-Policy "no-referrer" always; @@ -61,10 +67,10 @@ http { location /.well-known/oauth-authorization-server { proxy_pass http://esignet:8088/v1/esignet/oauth/.well-known/oauth-authorization-server; proxy_redirect off; - proxy_set_header Host esignet.solmara.registrystack.org; + proxy_set_header Host __ESIGNET_PUBLIC_HOST__; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Host esignet.solmara.registrystack.org; + proxy_set_header X-Forwarded-Host __ESIGNET_PUBLIC_HOST__; proxy_set_header X-Forwarded-Proto https; add_header Content-Security-Policy "default-src 'none'" always; add_header Referrer-Policy "no-referrer" always; @@ -73,10 +79,10 @@ http { location /.well-known/openid-credential-issuer { proxy_pass http://esignet:8088/v1/esignet/vci/.well-known/openid-credential-issuer; proxy_redirect off; - proxy_set_header Host esignet.solmara.registrystack.org; + proxy_set_header Host __ESIGNET_PUBLIC_HOST__; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Host esignet.solmara.registrystack.org; + proxy_set_header X-Forwarded-Host __ESIGNET_PUBLIC_HOST__; proxy_set_header X-Forwarded-Proto https; add_header Content-Security-Policy "default-src 'none'" always; add_header Referrer-Policy "no-referrer" always; @@ -91,7 +97,7 @@ http { img-src 'self' data: https://cdn.jsdelivr.net https://*.mosip.net; script-src 'self' https://www.google.com https://www.gstatic.com; frame-src https://www.google.com; - connect-src 'self' https://esignet.solmara.registrystack.org https://esignet-ui.solmara.registrystack.org; + connect-src 'self' https://__ESIGNET_PUBLIC_HOST__ https://__ESIGNET_UI_PUBLIC_HOST__; " always; add_header Referrer-Policy "no-referrer" always; } diff --git a/docker/esignet-ui/Dockerfile b/docker/esignet-ui/Dockerfile index 374b945..6d6b955 100644 --- a/docker/esignet-ui/Dockerfile +++ b/docker/esignet-ui/Dockerfile @@ -2,4 +2,17 @@ ARG ESIGNET_UI_IMAGE=mosipid/oidc-ui@sha256:8a2a6839b4e22be6c967dabc6308190c165c FROM ${ESIGNET_UI_IMAGE} ARG ESIGNET_NGINX_CONF=config/esignet/nginx-hosted.conf -COPY ${ESIGNET_NGINX_CONF} /etc/nginx/nginx.conf +COPY ${ESIGNET_NGINX_CONF} /home/mosip/nginx-hosted.conf.template +COPY docker/esignet-ui/render-hosted-nginx.sh /home/mosip/render-hosted-nginx.sh +COPY docker/esignet-ui/hosted-entrypoint.sh /home/mosip/hosted-entrypoint.sh + +USER root +RUN mv /home/mosip/configure_start.sh /home/mosip/configure-ui.sh && \ + chmod 0555 /home/mosip/configure-ui.sh \ + /home/mosip/render-hosted-nginx.sh \ + /home/mosip/hosted-entrypoint.sh && \ + chmod 0444 /home/mosip/nginx-hosted.conf.template +USER 1001:1001 + +ENTRYPOINT ["/home/mosip/hosted-entrypoint.sh"] +CMD ["nginx", "-c", "/tmp/solmara-nginx.conf", "-g", "daemon off;"] diff --git a/docker/esignet-ui/hosted-entrypoint.sh b/docker/esignet-ui/hosted-entrypoint.sh new file mode 100755 index 0000000..4d6b2dd --- /dev/null +++ b/docker/esignet-ui/hosted-entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -eu + +/home/mosip/render-hosted-nginx.sh \ + /home/mosip/nginx-hosted.conf.template \ + /tmp/solmara-nginx.conf + +exec sh /home/mosip/configure-ui.sh "$@" diff --git a/docker/esignet-ui/render-hosted-nginx.sh b/docker/esignet-ui/render-hosted-nginx.sh new file mode 100755 index 0000000..6b7bdc9 --- /dev/null +++ b/docker/esignet-ui/render-hosted-nginx.sh @@ -0,0 +1,39 @@ +#!/bin/sh +set -eu + +if [ "$#" -ne 2 ]; then + exit 64 +fi + +template=$1 +output=$2 + +validate_host() { + candidate=$1 + [ -n "$candidate" ] || return 1 + [ "${#candidate}" -le 253 ] || return 1 + printf '%s\n' "$candidate" | awk -F. ' + NF < 2 { exit 1 } + { + for (i = 1; i <= NF; i++) { + if (length($i) < 1 || length($i) > 63 || + $i !~ /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/) { + exit 1 + } + } + } + ' +} + +if ! validate_host "${SOLMARA_ESIGNET_PUBLIC_HOST:-}" || + ! validate_host "${SOLMARA_ESIGNET_UI_PUBLIC_HOST:-}"; then + echo "eSignet hosted nginx host configuration is invalid" >&2 + exit 78 +fi + +# The validated values contain only lower-case DNS hostname characters. This +# makes these two literal substitutions safe in nginx directive and CSP slots. +sed \ + -e "s/__ESIGNET_PUBLIC_HOST__/${SOLMARA_ESIGNET_PUBLIC_HOST}/g" \ + -e "s/__ESIGNET_UI_PUBLIC_HOST__/${SOLMARA_ESIGNET_UI_PUBLIC_HOST}/g" \ + "$template" >"$output" diff --git a/justfile b/justfile index 94cdda6..52d4e76 100644 --- a/justfile +++ b/justfile @@ -54,7 +54,7 @@ lint: test: cd generator && uv run python -m unittest discover -s tests uv run python -m unittest discover -s scenario-runner -p 'test_*.py' - uv run python -m unittest relays/test_relay_projects.py evidence/tests/test_cells.py scripts/test_metadata_authority_contracts.py scripts/test_image_pins.py scripts/test_build_registry_stack_runtime.py scripts/test_hosted_image_manifest.py scripts/test_hosted_provisioning_topology.py scripts/test_hosted_network_isolation.py scripts/test_hosted_home_topology.py scripts/test_hosted_runtime_assets.py scripts/test_provision_hosted_runtime.py scripts/test_hosted_transit_signer.py scripts/test_smoke_hosted_provisioner_image.py scripts/test_runtime_topology.py scripts/test_registry_stack_release_pin.py scripts/test_hosted_authority_rollout.py scripts/test_local_transit_proxy.py scripts/test_local_transit_signers.py scripts/test_local_transit_providers.py scripts/test_signer_public_keys.py scripts/test_project_runtime_secrets.py scripts/test_gen_secrets.py scripts/test_publish_runtime_extracts.py scripts/test_lifecycle_proof.py scripts/test_live_lifecycle_proof.py scripts/test_local_relay_source_publisher.py scripts/test_smoke_programme_acceptance.py scripts/test_smoke_esignet.py + uv run python -m unittest relays/test_relay_projects.py evidence/tests/test_cells.py scripts/test_metadata_authority_contracts.py scripts/test_image_pins.py scripts/test_build_registry_stack_runtime.py scripts/test_hosted_image_manifest.py scripts/test_hosted_provisioning_topology.py scripts/test_hosted_network_isolation.py scripts/test_hosted_home_topology.py scripts/test_hosted_esignet_topology.py scripts/test_hosted_runtime_assets.py scripts/test_provision_hosted_runtime.py scripts/test_hosted_transit_signer.py scripts/test_smoke_hosted_provisioner_image.py scripts/test_runtime_topology.py scripts/test_registry_stack_release_pin.py scripts/test_hosted_authority_rollout.py scripts/test_local_relay_runtime_stager.py scripts/test_local_transit_proxy.py scripts/test_local_transit_signers.py scripts/test_local_transit_providers.py scripts/test_signer_public_keys.py scripts/test_project_runtime_secrets.py scripts/test_gen_secrets.py scripts/test_publish_runtime_extracts.py scripts/test_lifecycle_proof.py scripts/test_live_lifecycle_proof.py scripts/test_local_relay_source_publisher.py scripts/test_smoke_programme_acceptance.py scripts/test_smoke_esignet.py cd portal && pnpm test cd home && pnpm test diff --git a/scripts/check-coolify-compose.sh b/scripts/check-coolify-compose.sh index dd7db36..5cc4cda 100755 --- a/scripts/check-coolify-compose.sh +++ b/scripts/check-coolify-compose.sh @@ -52,12 +52,5 @@ check_compose() { } for compose in "$root"/compose.coolify*.yaml; do - if [ "$(basename "$compose")" = "compose.coolify.esignet.yaml" ]; then - continue - fi check_compose -f "$compose" done - -# eSignet is intentionally an overlay: validate that it augments the same -# Portal service rather than accidentally declaring a second Portal deployment. -check_compose -f "$root/compose.coolify.yaml" -f "$root/compose.coolify.esignet.yaml" diff --git a/scripts/provision-hosted-runtime.py b/scripts/provision-hosted-runtime.py index 56128a2..2ca4fa4 100644 --- a/scripts/provision-hosted-runtime.py +++ b/scripts/provision-hosted-runtime.py @@ -19,6 +19,8 @@ from collections.abc import Callable from datetime import UTC, datetime from pathlib import Path +from urllib.parse import urlsplit + import yaml from cryptography.hazmat.primitives.asymmetric import ec, rsa @@ -27,6 +29,15 @@ MAX_SECRET_BYTES = 16 * 1024 RELAYS = ("cra", "nia", "mosd", "sipf", "nagdi") CELLS = ("cra", "nia", "sro", "mosd-programme", "sipf", "nagdi") +MINT_ORIGIN = "https://mint-authority-cells.solmara.registrystack.org" +RELAY_ORIGINS = { + "cra": "https://cra-relay-authority-cells.solmara.registrystack.org", + "mosd-programme": ( + "https://mosd-programme-relay-authority-cells.solmara.registrystack.org" + ), + "sipf": "https://sipf-relay-authority-cells.solmara.registrystack.org", + "nagdi": "https://nagdi-relay-authority-cells.solmara.registrystack.org", +} DIRECT = { "cra": ("cra-birth-extract", "cra-birth"), "nia": ("nia-population-extract", "nia-population"), @@ -404,6 +415,69 @@ def _write(path: Path, value: bytes, mode: int) -> None: path.chmod(mode) +def _validated_origin(value: str | None, expected: str) -> str: + try: + if value is None or value != expected: + raise ProvisionError("invalid origin") + parsed = urlsplit(value) + if ( + parsed.scheme != "https" + or parsed.netloc != parsed.hostname + or not parsed.hostname + or parsed.username is not None + or parsed.password is not None + or parsed.port is not None + or parsed.path + or parsed.query + or parsed.fragment + or value != f"https://{parsed.hostname}" + ): + raise ProvisionError("invalid origin") + return value + except (TypeError, ValueError): + raise ProvisionError("invalid origin") from None + + +def _patch_mint_origin(config: dict, mint_origin: str) -> None: + mint_origin = _validated_origin(mint_origin, MINT_ORIGIN) + config["issuer"] = mint_origin + config["clientAssertion"]["audience"] = f"{mint_origin}/token" + + +def _patch_relay_origin(config: dict, mint_origin: str) -> None: + mint_origin = _validated_origin(mint_origin, MINT_ORIGIN) + config["authentication"]["issuer"]["discoveryUrl"] = ( + f"{mint_origin}/.well-known/openid-configuration" + ) + + +def _patch_evidence_origins( + config: dict, cell: str, mint_origin: str, relay_origin: str | None +) -> None: + mint_origin = _validated_origin(mint_origin, MINT_ORIGIN) + expected_relay_origin = RELAY_ORIGINS.get(cell) + if (expected_relay_origin is None) != (relay_origin is None): + raise ProvisionError("invalid source origin") + if expected_relay_origin is not None: + relay_origin = _validated_origin(relay_origin, expected_relay_origin) + config["authentication"]["issuer"] = mint_origin + config["authentication"]["jwksUri"] = f"{mint_origin}/.well-known/jwks.json" + relay_sources = [ + source_config + for source_config in config["sources"].values() + if source_config["transport"] == "http-json" + ] + if (cell in RELAY_ORIGINS) != bool(relay_sources) or (relay_origin is None) != ( + cell not in RELAY_ORIGINS + ): + raise ProvisionError("invalid source origin") + for source_config in relay_sources: + source_config["baseUrl"] = relay_origin + authentication = source_config["authentication"] + authentication["tokenEndpoint"] = f"{mint_origin}/token" + authentication["clientAssertionAudience"] = f"{mint_origin}/token" + + def _patch_runtime(path: Path, bind_host: str, extract_name: str | None = None) -> None: config = yaml.safe_load(path.read_text(encoding="utf-8")) config["listener"]["bindHost"] = bind_host @@ -416,9 +490,21 @@ def _patch_runtime(path: Path, bind_host: str, extract_name: str | None = None) path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") -def _stage_relay(assets: Path, authority: str, runtime: Path, source: Path) -> None: +def _stage_relay( + assets: Path, + authority: str, + runtime: Path, + source: Path, + mint_origin: str, +) -> None: relay = assets / "relays" / authority - _write(runtime / "runtime.yaml", (relay / "runtime.yaml").read_bytes(), 0o444) + config = yaml.safe_load((relay / "runtime.yaml").read_text(encoding="utf-8")) + _patch_relay_origin(config, mint_origin) + _write( + runtime / "runtime.yaml", + yaml.safe_dump(config, sort_keys=False).encode(), + 0o444, + ) _copy_tree(relay / "package", runtime / "package") _write( source / f"{authority}.sqlite", @@ -653,6 +739,8 @@ def _stage_evidence( bind_host: str, published_at: str, observed_at: str, + mint_origin: str, + relay_origin: str | None, ) -> None: source = assets / "evidence" / "cells" / cell _copy_tree(source, runtime) @@ -660,6 +748,7 @@ def _stage_evidence( bundle_config = runtime / "bundle" / "evidence.yaml" config = yaml.safe_load(bundle_config.read_text(encoding="utf-8")) config["signing"]["activePublicJwkFile"] = f"public-keys/{public['kid']}.jwk.json" + _patch_evidence_origins(config, cell, mint_origin, relay_origin) bundle_config.chmod(0o644) bundle_config.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") bundle_config.chmod(0o444) @@ -697,10 +786,16 @@ def _stage_evidence( def _stage_mint( - assets: Path, secrets: Path, runtime: Path, secret_output: Path, bind_host: str + assets: Path, + secrets: Path, + runtime: Path, + secret_output: Path, + bind_host: str, + mint_origin: str, ) -> None: public = _public_jwk(_read_secret(secrets, "signing-public.jwk"), allow_rsa=False) config = yaml.safe_load((assets / "mint" / "mint.yaml").read_text(encoding="utf-8")) + _patch_mint_origin(config, mint_origin) config["listener"]["address"] = bind_host config["signing"]["activePublicJwkFile"] = f"public-keys/{public['kid']}.jwk.json" _write( @@ -751,6 +846,7 @@ def _stage_mint( def provision(args: argparse.Namespace) -> None: assets, target = args.assets.resolve(), args.target verify_assets(assets) + mint_origin = _validated_origin(args.mint_origin, MINT_ORIGIN) now = datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z") with tempfile.TemporaryDirectory(prefix="solmara-provision-") as temporary: root = Path(temporary) @@ -766,9 +862,10 @@ def provision(args: argparse.Namespace) -> None: or args.secret_output or args.extract_output or args.bind_host + or args.relay_origin ): raise ProvisionError("invalid target") - _stage_relay(assets, authority, runtime, source) + _stage_relay(assets, authority, runtime, source, mint_origin) _check_install_tree(source, args.source_output.resolve()) _check_install_tree(runtime, args.runtime_output.resolve()) _install_tree(source, args.source_output.resolve(), root_mode=0o555) @@ -778,6 +875,7 @@ def provision(args: argparse.Namespace) -> None: or not args.bind_host or args.source_output or args.extract_output + or args.relay_origin ): raise ProvisionError("invalid target") if args.bind_host != EXPECTED_BIND_HOST["mint"]: @@ -790,7 +888,12 @@ def provision(args: argparse.Namespace) -> None: } _validate_secret_inventory(args.secrets.resolve(), mint_secrets) _stage_mint( - assets, args.secrets.resolve(), runtime, secret_output, args.bind_host + assets, + args.secrets.resolve(), + runtime, + secret_output, + args.bind_host, + mint_origin, ) _check_install_tree(secret_output, args.secret_output.resolve()) _check_install_tree(runtime, args.runtime_output.resolve()) @@ -811,6 +914,14 @@ def provision(args: argparse.Namespace) -> None: raise ProvisionError("invalid target") if (cell in DIRECT) != (args.extract_output is not None): raise ProvisionError("invalid target") + expected_relay_origin = RELAY_ORIGINS.get(cell) + if (expected_relay_origin is None) != (args.relay_origin is None): + raise ProvisionError("invalid origin") + relay_origin = ( + _validated_origin(args.relay_origin, expected_relay_origin) + if expected_relay_origin is not None + else None + ) if args.bind_host != EXPECTED_BIND_HOST[cell]: raise ProvisionError("invalid bind host") cell_secrets = { @@ -841,6 +952,8 @@ def provision(args: argparse.Namespace) -> None: args.bind_host, published_at, now, + mint_origin, + relay_origin, ) runtime_preserve = _preserve_extract_rollback if cell in DIRECT else None _check_install_tree(secret_output, args.secret_output.resolve()) @@ -896,6 +1009,8 @@ def parser() -> argparse.ArgumentParser: ready.add_argument("--secret-output", type=Path) ready.add_argument("--extract-output", type=Path) ready.add_argument("--bind-host") + ready.add_argument("--mint-origin", required=True) + ready.add_argument("--relay-origin") publication = sub.add_parser("publish-extract", add_help=False) publication.add_argument("--target", required=True) publication.add_argument("--assets", required=True, type=Path) diff --git a/scripts/test_hosted_esignet_topology.py b/scripts/test_hosted_esignet_topology.py new file mode 100644 index 0000000..4d2758a --- /dev/null +++ b/scripts/test_hosted_esignet_topology.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +COMPOSE_PATH = ROOT / "compose.coolify.esignet.yaml" +CORE_COMPOSE_PATH = ROOT / "compose.coolify.yaml" +RENDERER = ROOT / "docker" / "esignet-ui" / "render-hosted-nginx.sh" +TEMPLATE = ROOT / "config" / "esignet" / "nginx-hosted.conf" + + +class HostedEsignetTopologyTests(unittest.TestCase): + def test_mint_and_relay_dependency_origins_are_not_operator_overridable( + self, + ) -> None: + compose_text = "\n".join( + path.read_text(encoding="utf-8") + for path in (CORE_COMPOSE_PATH, COMPOSE_PATH) + ) + for variable in ( + "SOLMARA_MINT_PUBLIC_BASE_URL", + "SOLMARA_MINT_PUBLIC_HOST", + "SOLMARA_CRA_RELAY_PUBLIC_BASE_URL", + "SOLMARA_NIA_RELAY_PUBLIC_BASE_URL", + "SOLMARA_MOSD_RELAY_PUBLIC_BASE_URL", + "SOLMARA_SIPF_RELAY_PUBLIC_BASE_URL", + "SOLMARA_NAGDI_RELAY_PUBLIC_BASE_URL", + ): + self.assertNotIn(variable, compose_text) + + def test_compose_is_a_standalone_esignet_app(self) -> None: + compose = yaml.safe_load(COMPOSE_PATH.read_text(encoding="utf-8")) + services = compose["services"] + self.assertEqual( + set(services), + { + "esignet-database", + "esignet-redis", + "esignet", + "esignet-ui", + "esignet-seed", + }, + ) + self.assertNotIn("portal", services) + self.assertEqual( + services["esignet"]["environment"]["REGISTRY_MINT_TOKEN_ENDPOINT"], + "https://mint-authority-cells.solmara.registrystack.org/token", + ) + self.assertEqual( + services["esignet"]["environment"]["REGISTRY_RELAY_BASE_URL"], + "https://nia-relay-authority-cells.solmara.registrystack.org", + ) + self.assertEqual( + services["esignet-seed"]["environment"][ + "ESIGNET_CLIENT_REDIRECT_URIS_JSON" + ], + '["${SOLMARA_PORTAL_PUBLIC_BASE_URL:-https://portal.solmara.registrystack.org}/auth/callback"]', + ) + + def test_renderer_accepts_only_dns_hosts_and_preserves_security_headers( + self, + ) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + output = Path(temporary_directory) / "nginx.conf" + environment = os.environ | { + "SOLMARA_ESIGNET_PUBLIC_HOST": "login-authority-cells.example.org", + "SOLMARA_ESIGNET_UI_PUBLIC_HOST": "login-ui-authority-cells.example.org", + } + subprocess.run( + [str(RENDERER), str(TEMPLATE), str(output)], + check=True, + env=environment, + capture_output=True, + text=True, + ) + rendered = output.read_text(encoding="utf-8") + self.assertNotIn("__ESIGNET_", rendered) + self.assertIn( + "proxy_set_header Host login-authority-cells.example.org;", rendered + ) + self.assertIn( + "proxy_set_header X-Forwarded-Host login-authority-cells.example.org;", + rendered, + ) + self.assertIn("default-src 'none'", rendered) + self.assertIn( + "connect-src 'self' https://login-authority-cells.example.org https://login-ui-authority-cells.example.org;", + rendered, + ) + + def test_renderer_rejects_directive_injection(self) -> None: + invalid_hosts = ( + "login.example.org;return 200", + "login.example.org/path", + "login.example.org example.net", + "UPPER.example.org", + "localhost", + ) + for invalid_host in invalid_hosts: + with ( + self.subTest(invalid_host=invalid_host), + tempfile.TemporaryDirectory() as directory, + ): + result = subprocess.run( + [str(RENDERER), str(TEMPLATE), str(Path(directory) / "nginx.conf")], + env=os.environ + | { + "SOLMARA_ESIGNET_PUBLIC_HOST": invalid_host, + "SOLMARA_ESIGNET_UI_PUBLIC_HOST": "ui.example.org", + }, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 78) + self.assertEqual( + result.stderr, + "eSignet hosted nginx host configuration is invalid\n", + ) + self.assertNotIn(invalid_host, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_hosted_home_topology.py b/scripts/test_hosted_home_topology.py index 4d38497..70d4670 100644 --- a/scripts/test_hosted_home_topology.py +++ b/scripts/test_hosted_home_topology.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import re import unittest from pathlib import Path from urllib.parse import urlparse @@ -12,6 +13,11 @@ COMPOSE_PATH = ROOT / "compose.coolify.yaml" +def interpolation_default(value: str) -> str: + match = re.fullmatch(r"\$\{[A-Z0-9_]+:-(.+)\}", value) + return match.group(1) if match else value + + class HostedHomeTopologyTests(unittest.TestCase): def test_home_build_context_exposes_only_public_generated_inputs(self) -> None: patterns = (ROOT / ".dockerignore").read_text(encoding="utf-8").splitlines() @@ -28,23 +34,23 @@ def test_home_uses_the_declared_public_topology_without_secrets(self) -> None: environment = home["environment"] expected_origins = { - "PORTAL_URL": "https://portal.solmara.registrystack.org", - "STATIC_METADATA_URL": "https://metadata.solmara.registrystack.org", - "CRA_CIVIL_RELAY_URL": "https://cra-relay.solmara.registrystack.org", - "NIA_POPULATION_RELAY_URL": "https://nia-relay.solmara.registrystack.org", - "MOSD_PROGRAMME_RELAY_URL": "https://mosd-programme-relay.solmara.registrystack.org", - "SIPF_PENSIONS_RELAY_URL": "https://sipf-relay.solmara.registrystack.org", - "NAGDI_AGRICULTURE_RELAY_URL": "https://nagdi-relay.solmara.registrystack.org", - "SOLMARA_CRA_EVIDENCE_URL": "https://cra-evidence.solmara.registrystack.org", - "SOLMARA_NIA_EVIDENCE_URL": "https://nia-evidence.solmara.registrystack.org", - "SOLMARA_SRO_EVIDENCE_URL": "https://sro-evidence.solmara.registrystack.org", - "SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL": "https://mosd-programme-evidence.solmara.registrystack.org", - "SOLMARA_SIPF_EVIDENCE_URL": "https://sipf-evidence.solmara.registrystack.org", - "SOLMARA_NAGDI_EVIDENCE_URL": "https://nagdi-evidence.solmara.registrystack.org", - "MINT_URL": "https://mint.solmara.registrystack.org", - "CHILD_BENEFIT_FEDERATOR_URL": "https://child-benefit.solmara.registrystack.org", - "SCENARIO_RUNNER_URL": "https://scenarios.solmara.registrystack.org", - "PORTAL_PROBE_URL": "https://portal.solmara.registrystack.org", + "PORTAL_URL": "${SOLMARA_PORTAL_PUBLIC_BASE_URL:-https://portal.solmara.registrystack.org}", + "STATIC_METADATA_URL": "${SOLMARA_METADATA_PUBLIC_BASE_URL:-https://metadata.solmara.registrystack.org}", + "CRA_CIVIL_RELAY_URL": "https://cra-relay-authority-cells.solmara.registrystack.org", + "NIA_POPULATION_RELAY_URL": "https://nia-relay-authority-cells.solmara.registrystack.org", + "MOSD_PROGRAMME_RELAY_URL": "https://mosd-programme-relay-authority-cells.solmara.registrystack.org", + "SIPF_PENSIONS_RELAY_URL": "https://sipf-relay-authority-cells.solmara.registrystack.org", + "NAGDI_AGRICULTURE_RELAY_URL": "https://nagdi-relay-authority-cells.solmara.registrystack.org", + "SOLMARA_CRA_EVIDENCE_URL": "${SOLMARA_CRA_EVIDENCE_PUBLIC_BASE_URL:-https://cra-evidence.solmara.registrystack.org}", + "SOLMARA_NIA_EVIDENCE_URL": "${SOLMARA_NIA_EVIDENCE_PUBLIC_BASE_URL:-https://nia-evidence.solmara.registrystack.org}", + "SOLMARA_SRO_EVIDENCE_URL": "${SOLMARA_SRO_EVIDENCE_PUBLIC_BASE_URL:-https://sro-evidence.solmara.registrystack.org}", + "SOLMARA_MOSD_PROGRAMME_EVIDENCE_URL": "${SOLMARA_MOSD_PROGRAMME_EVIDENCE_PUBLIC_BASE_URL:-https://mosd-programme-evidence.solmara.registrystack.org}", + "SOLMARA_SIPF_EVIDENCE_URL": "${SOLMARA_SIPF_EVIDENCE_PUBLIC_BASE_URL:-https://sipf-evidence.solmara.registrystack.org}", + "SOLMARA_NAGDI_EVIDENCE_URL": "${SOLMARA_NAGDI_EVIDENCE_PUBLIC_BASE_URL:-https://nagdi-evidence.solmara.registrystack.org}", + "MINT_URL": "https://mint-authority-cells.solmara.registrystack.org", + "CHILD_BENEFIT_FEDERATOR_URL": "${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_BASE_URL:-https://child-benefit.solmara.registrystack.org}", + "SCENARIO_RUNNER_URL": "${SOLMARA_SCENARIO_RUNNER_PUBLIC_BASE_URL:-https://scenarios.solmara.registrystack.org}", + "PORTAL_PROBE_URL": "${SOLMARA_PORTAL_PUBLIC_BASE_URL:-https://portal.solmara.registrystack.org}", } self.assertEqual( {key: environment.get(key) for key in expected_origins}, @@ -57,9 +63,12 @@ def test_home_uses_the_declared_public_topology_without_secrets(self) -> None: for service in hosted_compose.get("services", {}).values(): host = service.get("labels", {}).get("solmara.lab.host") if host: - declared_hosts.add(host) + declared_hosts.add(interpolation_default(host)) self.assertTrue( - {urlparse(origin).hostname for origin in expected_origins.values()} + { + urlparse(interpolation_default(origin)).hostname + for origin in expected_origins.values() + } <= declared_hosts ) diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index cb994a6..3cfd9a2 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -17,6 +17,13 @@ ) PROVIDERS = ("mint", "cra", "nia", "sro", "mosd", "sipf", "nagdi") RELAYS = ("cra", "nia", "mosd", "sipf", "nagdi") +MINT_ORIGIN = "https://mint-authority-cells.solmara.registrystack.org" +RELAY_ORIGINS = { + "cra": "https://cra-relay-authority-cells.solmara.registrystack.org", + "mosd": "https://mosd-programme-relay-authority-cells.solmara.registrystack.org", + "sipf": "https://sipf-relay-authority-cells.solmara.registrystack.org", + "nagdi": "https://nagdi-relay-authority-cells.solmara.registrystack.org", +} class HostedProvisioningTopologyTests(unittest.TestCase): @@ -126,6 +133,25 @@ def test_relay_provisioners_receive_no_secret(self) -> None: for authority in RELAYS: self.assertNotIn("secrets", services[f"{authority}-relay-provisioner"]) + def test_provisioners_receive_closed_permanent_dependency_origins(self) -> None: + services = self.provision["services"] + for name, service in services.items(): + if not name.endswith("provisioner"): + continue + command = service["command"] + self.assertEqual(command[command.index("--mint-origin") + 1], MINT_ORIGIN) + if name.endswith("-evidence-provisioner"): + provider = name.removesuffix("-evidence-provisioner") + if provider in RELAY_ORIGINS: + self.assertEqual( + command[command.index("--relay-origin") + 1], + RELAY_ORIGINS[provider], + ) + else: + self.assertNotIn("--relay-origin", command) + else: + self.assertNotIn("--relay-origin", command) + def test_each_signer_and_transit_initializer_mount_only_its_matching_volume(self) -> None: services = self.provision["services"] for provider in PROVIDERS: diff --git a/scripts/test_provision_hosted_runtime.py b/scripts/test_provision_hosted_runtime.py index d332651..7ac1acb 100644 --- a/scripts/test_provision_hosted_runtime.py +++ b/scripts/test_provision_hosted_runtime.py @@ -1,6 +1,7 @@ from __future__ import annotations import base64 +import copy import contextlib import hashlib import importlib.util @@ -26,6 +27,20 @@ assert SPEC and SPEC.loader provisioner = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(provisioner) +REPOSITORY = SCRIPT.parent.parent + + +def changed_paths(before, after, prefix: tuple[str, ...] = ()) -> set[tuple[str, ...]]: + if isinstance(before, dict) and isinstance(after, dict): + result: set[tuple[str, ...]] = set() + for key in before.keys() | after.keys(): + result.update( + changed_paths(before.get(key), after.get(key), (*prefix, str(key))) + ) + return result + if before != after: + return {prefix} + return set() def write_manifest(root: Path) -> None: @@ -76,6 +91,181 @@ def write_secret(root: Path, name: str, value: bytes | dict[str, str]) -> None: class HostedProvisionerTests(unittest.TestCase): + def test_rollout_origins_are_exact_canonical_https_origins(self) -> None: + origins = [provisioner.MINT_ORIGIN, *provisioner.RELAY_ORIGINS.values()] + for origin in origins: + with self.subTest(origin=origin): + self.assertEqual(provisioner._validated_origin(origin, origin), origin) + + expected = provisioner.MINT_ORIGIN + hostname = expected.removeprefix("https://") + invalid = [ + None, + f"http://{hostname}", + f"https://user@{hostname}", + f"https://{hostname}:443", + f"https://{hostname}/", + f"https://{hostname}/path", + f"https://{hostname}?query=yes", + f"https://{hostname}#fragment", + "https://mint.solmara.registrystack.org", + provisioner.RELAY_ORIGINS["cra"], + ] + for origin in invalid: + with self.subTest(origin=origin): + with self.assertRaises(provisioner.ProvisionError): + provisioner._validated_origin(origin, expected) + + def test_mint_origin_is_required_for_every_provision_target(self) -> None: + for target in [ + "mint", + *(f"{authority}-relay" for authority in provisioner.RELAYS), + *(f"{cell}-evidence" for cell in provisioner.CELLS), + ]: + with self.subTest(target=target): + with self.assertRaises(provisioner.ProvisionError): + provisioner.parser().parse_args( + [ + "provision", + "--target", + target, + "--assets", + "/assets", + "--secrets", + "/secrets", + "--runtime-output", + "/runtime", + ] + ) + + def test_only_relay_backed_evidence_accepts_its_exact_relay_origin(self) -> None: + for cell in provisioner.CELLS: + config = yaml.safe_load( + ( + REPOSITORY + / "evidence" + / "cells" + / cell + / "bundle" + / "evidence.yaml" + ).read_text(encoding="utf-8") + ) + expected = provisioner.RELAY_ORIGINS.get(cell) + if expected is None: + provisioner._patch_evidence_origins( + copy.deepcopy(config), cell, provisioner.MINT_ORIGIN, None + ) + with self.assertRaises(provisioner.ProvisionError): + provisioner._patch_evidence_origins( + copy.deepcopy(config), + cell, + provisioner.MINT_ORIGIN, + provisioner.RELAY_ORIGINS["cra"], + ) + else: + provisioner._patch_evidence_origins( + copy.deepcopy(config), cell, provisioner.MINT_ORIGIN, expected + ) + with self.assertRaises(provisioner.ProvisionError): + provisioner._patch_evidence_origins( + copy.deepcopy(config), cell, provisioner.MINT_ORIGIN, None + ) + foreign = next( + origin + for authority, origin in provisioner.RELAY_ORIGINS.items() + if authority != cell + ) + with self.assertRaises(provisioner.ProvisionError): + provisioner._patch_evidence_origins( + copy.deepcopy(config), cell, provisioner.MINT_ORIGIN, foreign + ) + + def test_origin_patching_changes_only_closed_authentication_fields(self) -> None: + mint_before = yaml.safe_load( + (REPOSITORY / "evidence" / "mint.yaml").read_text(encoding="utf-8") + ) + mint_after = copy.deepcopy(mint_before) + provisioner._patch_mint_origin(mint_after, provisioner.MINT_ORIGIN) + self.assertEqual( + changed_paths(mint_before, mint_after), + {("issuer",), ("clientAssertion", "audience")}, + ) + self.assertEqual(mint_after["accessTokens"], mint_before["accessTokens"]) + self.assertEqual(mint_after["signing"], mint_before["signing"]) + + for authority in provisioner.RELAYS: + before = yaml.safe_load( + (REPOSITORY / "relays" / authority / "runtime.yaml").read_text( + encoding="utf-8" + ) + ) + after = copy.deepcopy(before) + provisioner._patch_relay_origin(after, provisioner.MINT_ORIGIN) + self.assertEqual( + changed_paths(before, after), + {("authentication", "issuer", "discoveryUrl")}, + ) + self.assertEqual(after["sources"], before["sources"]) + self.assertEqual( + after["authentication"]["issuer"]["audience"], + before["authentication"]["issuer"]["audience"], + ) + + for cell in provisioner.CELLS: + before = yaml.safe_load( + ( + REPOSITORY + / "evidence" + / "cells" + / cell + / "bundle" + / "evidence.yaml" + ).read_text(encoding="utf-8") + ) + after = copy.deepcopy(before) + provisioner._patch_evidence_origins( + after, + cell, + provisioner.MINT_ORIGIN, + provisioner.RELAY_ORIGINS.get(cell), + ) + expected_paths = { + ("authentication", "issuer"), + ("authentication", "jwksUri"), + } + for source_name, source in before["sources"].items(): + if source["transport"] == "http-json": + expected_paths.update( + { + ("sources", source_name, "baseUrl"), + ( + "sources", + source_name, + "authentication", + "tokenEndpoint", + ), + ( + "sources", + source_name, + "authentication", + "clientAssertionAudience", + ), + } + ) + for field in ("clientIdRef", "scope", "audience"): + self.assertEqual( + after["sources"][source_name]["authentication"][field], + source["authentication"][field], + ) + self.assertEqual( + after["sources"][source_name]["request"], source["request"] + ) + self.assertEqual(changed_paths(before, after), expected_paths) + self.assertEqual(after["issuer"], before["issuer"]) + self.assertEqual(after["signing"], before["signing"]) + self.assertEqual(after["requirements"], before["requirements"]) + self.assertEqual(after["authorityProfiles"], before["authorityProfiles"]) + def test_no_argument_cli_failure_is_one_generic_line(self) -> None: completed = subprocess.run( [sys.executable, str(SCRIPT)], @@ -108,6 +298,8 @@ def test_unexpected_failures_are_redacted_at_the_command_boundary(self) -> None: "/canary/runtime", "--source-output", "/canary/source", + "--mint-origin", + provisioner.MINT_ORIGIN, ] ) self.assertEqual(result, 1) @@ -199,7 +391,22 @@ def test_evidence_output_contains_only_its_public_signer_and_own_secrets( cell = assets / "evidence" / "cells" / "sipf" (cell / "bundle").mkdir(parents=True) (cell / "bundle" / "evidence.yaml").write_text( - yaml.safe_dump({"signing": {"activePublicJwkFile": "old"}}), + yaml.safe_dump( + { + "authentication": {"issuer": "old", "jwksUri": "old"}, + "signing": {"activePublicJwkFile": "old"}, + "sources": { + "pension": { + "transport": "http-json", + "baseUrl": "old", + "authentication": { + "tokenEndpoint": "old", + "clientAssertionAudience": "old", + }, + } + }, + } + ), encoding="utf-8", ) (cell / "runtime.yaml").write_text( @@ -226,6 +433,8 @@ def test_evidence_output_contains_only_its_public_signer_and_own_secrets( provisioner.EXPECTED_BIND_HOST["sipf"], "2026-08-12T00:00:00Z", "2026-08-12T00:00:00Z", + provisioner.MINT_ORIGIN, + provisioner.RELAY_ORIGINS["sipf"], ) self.assertEqual( @@ -247,6 +456,21 @@ def test_evidence_output_contains_only_its_public_signer_and_own_secrets( ) self.assertEqual(json.loads(public_file.read_text()), signing_public) self.assertNotIn(signing_private["d"], public_file.read_text()) + authored = yaml.safe_load( + (cell / "bundle" / "evidence.yaml").read_text(encoding="utf-8") + ) + provisioned = yaml.safe_load( + (runtime / "bundle" / "evidence.yaml").read_text(encoding="utf-8") + ) + self.assertEqual(authored["authentication"]["issuer"], "old") + self.assertEqual(authored["sources"]["pension"]["baseUrl"], "old") + self.assertEqual( + provisioned["authentication"]["issuer"], provisioner.MINT_ORIGIN + ) + self.assertEqual( + provisioned["sources"]["pension"]["baseUrl"], + provisioner.RELAY_ORIGINS["sipf"], + ) def test_mint_writes_only_audit_secret_and_public_client_registrations( self, @@ -260,6 +484,8 @@ def test_mint_writes_only_audit_secret_and_public_client_registrations( { "listener": {"address": "old"}, "signing": {"activePublicJwkFile": "old"}, + "issuer": "old", + "clientAssertion": {"audience": "old"}, } ), encoding="utf-8", @@ -279,6 +505,7 @@ def test_mint_writes_only_audit_secret_and_public_client_registrations( runtime, output_secrets, provisioner.EXPECTED_BIND_HOST["mint"], + provisioner.MINT_ORIGIN, ) self.assertEqual( {path.name for path in output_secrets.iterdir()}, {"audit-hmac-key"} @@ -288,6 +515,18 @@ def test_mint_writes_only_audit_secret_and_public_client_registrations( ) self.assertNotIn(private["d"], emitted) self.assertEqual(len(list((runtime / "clients").glob("*.yaml"))), 9) + authored = yaml.safe_load( + (assets / "mint" / "mint.yaml").read_text(encoding="utf-8") + ) + provisioned = yaml.safe_load( + (runtime / "mint.yaml").read_text(encoding="utf-8") + ) + self.assertEqual(authored["issuer"], "old") + self.assertEqual(provisioned["issuer"], provisioner.MINT_ORIGIN) + self.assertEqual( + provisioned["clientAssertion"]["audience"], + f"{provisioner.MINT_ORIGIN}/token", + ) write_secret(secrets, "signing-public.jwk", private) with self.assertRaises(provisioner.ProvisionError): @@ -297,6 +536,7 @@ def test_mint_writes_only_audit_secret_and_public_client_registrations( root / "bad-runtime", root / "bad-secrets", provisioner.EXPECTED_BIND_HOST["mint"], + provisioner.MINT_ORIGIN, ) def test_relay_provision_preserves_database_and_is_idempotent(self) -> None: @@ -309,7 +549,17 @@ def test_relay_provision_preserves_database_and_is_idempotent(self) -> None: (relay / "source").mkdir() database = b"SQLite format 3\x00\n\xff\x00" (relay / "source" / "cra.sqlite").write_bytes(database) - (relay / "runtime.yaml").write_text("version: 1\n", encoding="utf-8") + (relay / "runtime.yaml").write_text( + yaml.safe_dump( + { + "version": 1, + "authentication": { + "issuer": {"discoveryUrl": "https://old.invalid"} + }, + } + ), + encoding="utf-8", + ) write_manifest(assets) secrets = root / "unused-secrets" secrets.mkdir() @@ -327,6 +577,8 @@ def test_relay_provision_preserves_database_and_is_idempotent(self) -> None: str(runtime), "--source-output", str(source), + "--mint-origin", + provisioner.MINT_ORIGIN, ] ) @@ -339,6 +591,20 @@ def test_relay_provision_preserves_database_and_is_idempotent(self) -> None: self.assertEqual( stat.S_IMODE((source / "cra.sqlite").stat().st_mode), 0o444 ) + authored = yaml.safe_load( + (relay / "runtime.yaml").read_text(encoding="utf-8") + ) + provisioned = yaml.safe_load( + (runtime / "runtime.yaml").read_text(encoding="utf-8") + ) + self.assertEqual( + authored["authentication"]["issuer"]["discoveryUrl"], + "https://old.invalid", + ) + self.assertEqual( + provisioned["authentication"]["issuer"]["discoveryUrl"], + f"{provisioner.MINT_ORIGIN}/.well-known/openid-configuration", + ) def test_all_outputs_are_preflighted_before_any_volume_changes(self) -> None: with tempfile.TemporaryDirectory() as temporary: @@ -362,10 +628,12 @@ def test_all_outputs_are_preflighted_before_any_volume_changes(self) -> None: str(secret_output), "--bind-host", provisioner.EXPECTED_BIND_HOST["mint"], + "--mint-origin", + provisioner.MINT_ORIGIN, ] ) - def stage(_assets, _inputs, runtime, secrets, _bind): + def stage(_assets, _inputs, runtime, secrets, _bind, _mint_origin): provisioner._write(runtime / "runtime.yaml", b"replacement", 0o444) provisioner._write(secrets / "audit-hmac-key", b"a" * 32, 0o400) @@ -388,7 +656,17 @@ def test_relay_cli_loads_manifest_verifier_directly(self) -> None: (relay / "package" / "sealed.json").write_bytes(b"sealed") (relay / "source").mkdir() (relay / "source" / "cra.sqlite").write_bytes(b"SQLite format 3\x00") - (relay / "runtime.yaml").write_text("version: 1\n", encoding="utf-8") + (relay / "runtime.yaml").write_text( + yaml.safe_dump( + { + "version": 1, + "authentication": { + "issuer": {"discoveryUrl": "https://old.invalid"} + }, + } + ), + encoding="utf-8", + ) write_manifest(assets) runtime, source = root / "runtime", root / "source" completed = subprocess.run( @@ -406,6 +684,8 @@ def test_relay_cli_loads_manifest_verifier_directly(self) -> None: str(runtime), "--source-output", str(source), + "--mint-origin", + provisioner.MINT_ORIGIN, ], check=False, capture_output=True, @@ -439,6 +719,8 @@ def test_manifest_failure_precedes_every_output_mutation(self) -> None: str(runtime), "--source-output", str(source), + "--mint-origin", + provisioner.MINT_ORIGIN, ] ) with self.assertRaises(provisioner.ProvisionError): @@ -653,6 +935,10 @@ def test_evidence_bind_address_is_closed_before_staging(self) -> None: str(root / "output-secrets"), "--bind-host", "172.29.1.99", + "--mint-origin", + provisioner.MINT_ORIGIN, + "--relay-origin", + provisioner.RELAY_ORIGINS["sipf"], ] ) with ( diff --git a/scripts/test_runtime_topology.py b/scripts/test_runtime_topology.py index a882331..98d4659 100644 --- a/scripts/test_runtime_topology.py +++ b/scripts/test_runtime_topology.py @@ -122,10 +122,7 @@ def test_local_relays_use_the_v2_runtime_filesystem_contract(self) -> None: for authority in ("cra", "nia", "mosd", "sipf", "nagdi"): service = compose["services"][f"{authority}-relay"] - self.assertEqual( - service["user"], - "${SOLMARA_LOCAL_RUNTIME_UID:?run just gen-secrets}:${SOLMARA_LOCAL_RUNTIME_GID:?run just gen-secrets}", - ) + self.assertEqual(service["user"], "65532:65532") self.assertEqual(service["cap_drop"], ["ALL"]) self.assertIn("no-new-privileges:true", service["security_opt"]) self.assertEqual( @@ -133,14 +130,7 @@ def test_local_relays_use_the_v2_runtime_filesystem_contract(self) -> None: ["serve", "--runtime", f"/etc/relay/{authority}/runtime.yaml"], ) volumes = set(service["volumes"]) - self.assertIn( - f"./relays/{authority}/runtime.yaml:/etc/relay/{authority}/runtime.yaml:ro", - volumes, - ) - self.assertIn( - f"./relays/{authority}/package:/etc/relay/{authority}/package:ro", - volumes, - ) + self.assertIn(f"{authority}-relay-runtime:/etc/relay/{authority}:ro", volumes) self.assertIn( f"{authority}-relay-source:/var/lib/relay/source:ro", volumes, @@ -153,6 +143,10 @@ def test_local_relays_use_the_v2_runtime_filesystem_contract(self) -> None: service["depends_on"]["relay-audit-init"]["condition"], "service_completed_successfully", ) + self.assertEqual( + service["depends_on"][f"{authority}-relay-runtime-stager"]["condition"], + "service_completed_successfully", + ) self.assertEqual( service["depends_on"]["relay-issuer-readiness"]["condition"], "service_completed_successfully", @@ -204,38 +198,18 @@ def test_local_relays_use_the_v2_runtime_filesystem_contract(self) -> None: self.assertIn("follow_symlinks=False", audit_init) self.assertIn("os.chown(path, 0, 0)", audit_init) self.assertIn("os.chown(path, target_uid, target_gid)", audit_init) - self.assertIn("target_uid == 0 or target_gid == 0", audit_init) self.assertIn( "os.chown(entry.path, target_uid, target_gid, follow_symlinks=False)", audit_init, ) - self.assertEqual( - initializer["environment"], - { - "SOLMARA_LOCAL_RUNTIME_UID": "${SOLMARA_LOCAL_RUNTIME_UID:?run just gen-secrets}", - "SOLMARA_LOCAL_RUNTIME_GID": "${SOLMARA_LOCAL_RUNTIME_GID:?run just gen-secrets}", - }, - ) + self.assertIn("target_uid = 65532", audit_init) + self.assertIn("target_gid = 65532", audit_init) for authority in ("cra", "nia", "mosd", "sipf", "nagdi"): self.assertIn( f"{authority}-relay-audit:/audit/{authority}", initializer["volumes"], ) - for uid_value, gid_value in (("0", "65532"), ("65532", "0")): - with ( - mock.patch.dict( - os.environ, - { - "SOLMARA_LOCAL_RUNTIME_UID": uid_value, - "SOLMARA_LOCAL_RUNTIME_GID": gid_value, - }, - clear=False, - ), - self.assertRaisesRegex(RuntimeError, "runtime identity is invalid"), - ): - exec(audit_init, {}) - issuer_readiness = compose["services"]["relay-issuer-readiness"] self.assertEqual(issuer_readiness["user"], "65532:65532") self.assertTrue(issuer_readiness["read_only"]) @@ -739,11 +713,11 @@ def test_hosted_programme_services_receive_only_their_application_secrets( self.assertEqual( services["scenario-runner"]["environment"]["CHILD_BENEFIT_FEDERATOR_URL"], - "https://child-benefit.solmara.registrystack.org", + "${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_BASE_URL:-https://child-benefit.solmara.registrystack.org}", ) self.assertEqual( services["portal"]["environment"]["CHILD_BENEFIT_FEDERATOR_URL"], - "https://child-benefit.solmara.registrystack.org", + "${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_BASE_URL:-https://child-benefit.solmara.registrystack.org}", ) self.assertEqual( services["child-benefit-federator"]["environment"][ @@ -765,36 +739,45 @@ def test_hosted_programme_services_receive_only_their_application_secrets( self.assertNotIn("SOLMARA_EVIDENCE_CLIENT_KEY", environment, service_name) self.assertNotIn("CHILD_BENEFIT_FEDERATOR_TOKEN", environment, service_name) - def test_hosted_esignet_overlay_wires_the_main_portal_with_a_separate_key( + def test_hosted_esignet_is_standalone_and_core_portal_owns_login_config( self, ) -> None: - overlay_path = SCRIPT.parents[1] / "compose.coolify.esignet.yaml" - overlay = yaml.safe_load(overlay_path.read_text(encoding="utf-8")) - services = overlay["services"] - portal = services["portal"]["environment"] + esignet_path = SCRIPT.parents[1] / "compose.coolify.esignet.yaml" + esignet = yaml.safe_load(esignet_path.read_text(encoding="utf-8")) + services = esignet["services"] + self.assertEqual( + set(services), + {"esignet-database", "esignet-redis", "esignet", "esignet-ui", "esignet-seed"}, + ) + self.assertNotIn("portal", services) + + core = yaml.safe_load( + (SCRIPT.parents[1] / "compose.coolify.yaml").read_text(encoding="utf-8") + ) + portal = core["services"]["portal"]["environment"] expected = { - "PORTAL_AUTH_PROVIDER": "esignet", + "PORTAL_AUTH_PROVIDER": "${PORTAL_AUTH_PROVIDER:-mock}", "PORTAL_SECURE_COOKIES": "true", "PORTAL_ESIGNET_CLIENT_ID": "${PORTAL_ESIGNET_CLIENT_ID:-solmara-portal}", "PORTAL_ESIGNET_CLIENT_KEY_ID": "${PORTAL_ESIGNET_CLIENT_KEY_ID:-solmara-portal-key-1}", - "PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64": "${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?required}", - "PORTAL_ESIGNET_ISSUER": "https://esignet.solmara.registrystack.org", - "PORTAL_ESIGNET_AUTHORIZATION_ENDPOINT": "https://esignet-ui.solmara.registrystack.org/authorize", - "PORTAL_ESIGNET_TOKEN_ENDPOINT": "https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token", - "PORTAL_ESIGNET_CLIENT_ASSERTION_AUDIENCE": "https://esignet.solmara.registrystack.org/v1/esignet/oauth/v2/token", - "PORTAL_ESIGNET_USERINFO_ENDPOINT": "https://esignet.solmara.registrystack.org/v1/esignet/oidc/userinfo", - "PORTAL_ESIGNET_REDIRECT_URI": "https://portal.solmara.registrystack.org/auth/callback", + "PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64": "${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:-}", + "PORTAL_ESIGNET_ISSUER": "${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}", + "PORTAL_ESIGNET_AUTHORIZATION_ENDPOINT": "${SOLMARA_ESIGNET_UI_PUBLIC_BASE_URL:-https://esignet-ui.solmara.registrystack.org}/authorize", + "PORTAL_ESIGNET_TOKEN_ENDPOINT": "${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oauth/v2/token", + "PORTAL_ESIGNET_CLIENT_ASSERTION_AUDIENCE": "${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oauth/v2/token", + "PORTAL_ESIGNET_USERINFO_ENDPOINT": "${SOLMARA_ESIGNET_PUBLIC_BASE_URL:-https://esignet.solmara.registrystack.org}/v1/esignet/oidc/userinfo", + "PORTAL_ESIGNET_REDIRECT_URI": "${SOLMARA_PORTAL_PUBLIC_BASE_URL:-https://portal.solmara.registrystack.org}/auth/callback", "PORTAL_ESIGNET_SCOPE": "openid profile", "PORTAL_ESIGNET_SUBJECT_CLAIM": "sub", } - self.assertEqual(portal, expected) - self.assertNotIn("name", overlay) + for key, value in expected.items(): + self.assertEqual(portal[key], value) - private_key_value = "${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?required}" + private_key_value = "${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:-}" self.assertEqual( { service_name - for service_name, service in services.items() + for service_name, service in core["services"].items() if service.get("environment", {}).get( "PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64" ) @@ -804,9 +787,8 @@ def test_hosted_esignet_overlay_wires_the_main_portal_with_a_separate_key( ) self.assertEqual( services["esignet-seed"]["environment"]["ESIGNET_CLIENT_PRIVATE_KEY_B64"], - private_key_value, + "${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?required}", ) - self.assertNotIn("SOLMARA_EVIDENCE_CLIENT_KEY", portal) self.assertNotIn("NIA_ESIGNET_CLIENT_PRIVATE_JWK", portal) def test_bruno_workspace_covers_only_the_eight_governed_v2_lookups(self) -> None: diff --git a/scripts/test_smoke_esignet.py b/scripts/test_smoke_esignet.py index b34bb5b..75a7ba2 100644 --- a/scripts/test_smoke_esignet.py +++ b/scripts/test_smoke_esignet.py @@ -96,10 +96,6 @@ def test_local_and_hosted_profiles_use_the_v020_authenticator_contract(self) -> for environment in (local_env, hosted_env): self.assertIn("REGISTRY_MINT_PRIVATE_JWK", environment) self.assertNotIn("REGISTRY_MINT_CLIENT_PRIVATE_JWK", environment) - self.assertEqual( - environment["REGISTRY_MINT_TOKEN_ENDPOINT"], - "https://mint.solmara.registrystack.org/token", - ) self.assertEqual( environment["MOSIP_ESIGNET_DATABASE_URL"], "jdbc:postgresql://esignet-database:5432/mosip_esignet?currentSchema=esignet", @@ -137,6 +133,19 @@ def test_local_and_hosted_profiles_use_the_v020_authenticator_contract(self) -> }, ) + self.assertEqual( + local_env["REGISTRY_MINT_TOKEN_ENDPOINT"], + "https://mint.solmara.registrystack.org/token", + ) + self.assertEqual( + hosted_env["REGISTRY_MINT_TOKEN_ENDPOINT"], + "https://mint-authority-cells.solmara.registrystack.org/token", + ) + self.assertEqual( + hosted_env["REGISTRY_RELAY_BASE_URL"], + "https://nia-relay-authority-cells.solmara.registrystack.org", + ) + mint = yaml.safe_load((ROOT / "evidence" / "mint.yaml").read_text()) self.assertEqual(mint["clientAssertion"]["algorithms"], ["ES256", "RS256"]) From b3c2335c2a1197904149ae2e3f15447f4d9e7b21 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 06:28:48 +0700 Subject: [PATCH 29/53] test(local): build sealed Relay fixture Signed-off-by: Jeremi Joslin --- scripts/test_local_relay_runtime_stager.py | 28 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/scripts/test_local_relay_runtime_stager.py b/scripts/test_local_relay_runtime_stager.py index 4ff0ef4..c77413a 100644 --- a/scripts/test_local_relay_runtime_stager.py +++ b/scripts/test_local_relay_runtime_stager.py @@ -1,8 +1,9 @@ from __future__ import annotations +import hashlib import importlib.util +import json import os -import shutil import stat import sys import tempfile @@ -24,9 +25,28 @@ def isolated_source(root: Path, authority: str) -> Path: source = root / "source" - source.mkdir(parents=True) - shutil.copy2(ROOT / "relays" / authority / "runtime.yaml", source / "runtime.yaml") - shutil.copytree(ROOT / "relays" / authority / "package", source / "package") + generated = source / "package" / "generated" + generated.mkdir(parents=True) + (source / "runtime.yaml").write_text( + f"packagePath: /etc/relay/{authority}/package\n", + encoding="utf-8", + ) + payload = b'{"fixture":"sealed"}\n' + artifact = generated / "artifact.json" + artifact.write_bytes(payload) + manifest = { + "files": [ + { + "path": "generated/artifact.json", + "size": len(payload), + "sha256": f"sha256:{hashlib.sha256(payload).hexdigest()}", + } + ] + } + (source / "package" / "relay-package.json").write_text( + json.dumps(manifest, sort_keys=True), + encoding="utf-8", + ) return source From 5edf0d67332a95ac705019986996ceee457d6e8f Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 06:36:50 +0700 Subject: [PATCH 30/53] fix(release): pass hosted provision origins Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 7 +++++-- scripts/smoke-hosted-provisioner-image.py | 8 ++++++++ scripts/test_hosted_image_manifest.py | 6 ++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index d90ebce..b365b8d 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -208,7 +208,8 @@ jobs: --assets /opt/solmara-hosted-assets \ --secrets /run/secrets/solmara-provisioning \ --runtime-output /provisioned/runtime \ - --source-output /provisioned/source + --source-output /provisioned/source \ + --mint-origin https://mint-authority-cells.solmara.registrystack.org done python3 - "$state" <<'PY' import sqlite3 @@ -233,7 +234,9 @@ jobs: --assets /opt/solmara-hosted-assets \ --secrets /run/secrets/solmara-provisioning \ --runtime-output /provisioned/runtime \ - --source-output /provisioned/source >"$stdout" 2>"$stderr" + --source-output /provisioned/source \ + --mint-origin https://mint-authority-cells.solmara.registrystack.org \ + >"$stdout" 2>"$stderr" status=$? set -e test "$status" -eq 1 diff --git a/scripts/smoke-hosted-provisioner-image.py b/scripts/smoke-hosted-provisioner-image.py index c3bebb6..f5d367a 100755 --- a/scripts/smoke-hosted-provisioner-image.py +++ b/scripts/smoke-hosted-provisioner-image.py @@ -18,6 +18,8 @@ ROOT = Path(__file__).resolve().parents[1] LOCAL = ROOT / "config" / "evidence" / "local" / "cells" GENERIC_ERROR = "hosted provisioner image smoke failed" +MINT_ORIGIN = "https://mint-authority-cells.solmara.registrystack.org" +CRA_RELAY_ORIGIN = "https://cra-relay-authority-cells.solmara.registrystack.org" def _build_cells_module(): @@ -109,6 +111,10 @@ def smoke(image: str, state_root: Path) -> None: "/provisioned/extracts", "--bind-host", "172.29.1.21", + "--mint-origin", + MINT_ORIGIN, + "--relay-origin", + CRA_RELAY_ORIGIN, ] _run(image, cra_arguments, cra_mounts) _run(image, cra_arguments, cra_mounts) @@ -165,6 +171,8 @@ def smoke(image: str, state_root: Path) -> None: "/provisioned/secrets", "--bind-host", "172.29.1.20", + "--mint-origin", + MINT_ORIGIN, ] _run(image, mint_arguments, mint_mounts) _run(image, mint_arguments, mint_mounts) diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index 95eb53c..0834134 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -299,6 +299,12 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( self.assertIn("--network none --read-only", provisioner_smoke) self.assertIn('test "$status" -eq 1', provisioner_smoke) self.assertIn("hosted target provisioning failed", provisioner_smoke) + self.assertEqual( + provisioner_smoke.count( + "--mint-origin https://mint-authority-cells.solmara.registrystack.org" + ), + 2, + ) signer_smoke = next( step From 37f0a347325c99f253759e0548731e1c1a1d50b9 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 06:45:26 +0700 Subject: [PATCH 31/53] fix(hosted): initialize provisioned volumes in image smoke Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 6 +- compose.coolify.provision.yaml | 5 ++ scripts/smoke-hosted-provisioner-image.py | 6 ++ scripts/test_hosted_image_manifest.py | 13 ++- scripts/test_hosted_provisioning_topology.py | 80 +++++++++++++++---- .../test_smoke_hosted_provisioner_image.py | 16 ++++ 6 files changed, 106 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index b365b8d..88a5f79 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -200,7 +200,8 @@ jobs: state="$RUNNER_TEMP/solmara-authority-provisioner-state" mkdir -p "$state/runtime" "$state/source" for _ in 1 2; do - docker run --rm --network none --read-only --tmpfs /tmp \ + docker run --rm --platform linux/amd64 --network none --read-only --tmpfs /tmp \ + --user 0:0 --cap-drop ALL --cap-add CHOWN \ --mount "type=bind,source=$state/runtime,target=/provisioned/runtime" \ --mount "type=bind,source=$state/source,target=/provisioned/source" \ "$image" provision \ @@ -226,7 +227,8 @@ jobs: stdout="$RUNNER_TEMP/solmara-authority-provisioner.stdout" stderr="$RUNNER_TEMP/solmara-authority-provisioner.stderr" set +e - docker run --rm --network none --read-only --tmpfs /tmp \ + docker run --rm --platform linux/amd64 --network none --read-only --tmpfs /tmp \ + --user 0:0 --cap-drop ALL --cap-add CHOWN \ --mount "type=bind,source=$state/runtime,target=/provisioned/runtime" \ --mount "type=bind,source=$state/source,target=/provisioned/source" \ "$image" provision \ diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml index 687e90b..cc6b96a 100644 --- a/compose.coolify.provision.yaml +++ b/compose.coolify.provision.yaml @@ -6,10 +6,15 @@ name: solmara-authority-cells x-provisioner: &provisioner image: ${SOLMARA_AUTHORITY_PROVISIONER_IMAGE:?set the digest-pinned Solmara authority provisioner image} restart: "no" + # Each one-shot service is confined to one target's volumes and needs root + # only to initialize Docker-owned volume roots and hand private runtime + # material to UID 65532. It has no network or writable container root. + user: "0:0" read_only: true tmpfs: [/tmp] network_mode: none cap_drop: [ALL] + cap_add: [CHOWN] security_opt: [no-new-privileges:true] x-transit-init: &transit-init diff --git a/scripts/smoke-hosted-provisioner-image.py b/scripts/smoke-hosted-provisioner-image.py index f5d367a..cb47b79 100755 --- a/scripts/smoke-hosted-provisioner-image.py +++ b/scripts/smoke-hosted-provisioner-image.py @@ -54,6 +54,12 @@ def _run( "--network", "none", "--read-only", + "--user", + "0:0", + "--cap-drop", + "ALL", + "--cap-add", + "CHOWN", "--tmpfs", "/tmp", ] diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index 0834134..6faa924 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -232,7 +232,9 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( == "Verify official Registry Stack runtime images and Relayctl" )["run"] self.assertIn("for component in relay evidence mint", runtime_verification) - self.assertIn("REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256", runtime_verification) + self.assertIn( + "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256", runtime_verification + ) self.assertNotIn("REGISTRY_STACK_RELEASE_EVIDENCE_ASSET", runtime_verification) self.assertNotIn("REGISTRY_STACK_RELEASE_MINT_ASSET", runtime_verification) provisioner_index = names.index("Build and push authority provisioner") @@ -288,6 +290,15 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( self.assertIn("steps.authority_provisioner.outputs.digest", label_verification) self.assertIn("steps.transit_signer.outputs.digest", label_verification) self.assertIn("org.opencontainers.image.revision", label_verification) + + provisioner_smoke = next( + step + for step in steps + if step.get("name") == "Smoke hosted authority provisioner image" + )["run"] + self.assertEqual(provisioner_smoke.count("--platform linux/amd64"), 2) + self.assertEqual(provisioner_smoke.count("--user 0:0"), 2) + self.assertEqual(provisioner_smoke.count("--cap-add CHOWN"), 2) self.assertIn("org.registrystack.release.revision", label_verification) provisioner_smoke = next( diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index 3cfd9a2..e43dbb6 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -38,13 +38,20 @@ def setUpClass(cls) -> None: def test_provisioning_application_owns_exactly_34_active_volumes(self) -> None: volumes = self.provision["volumes"] self.assertEqual(len(volumes), 34) - self.assertTrue(all(not value.get("external", False) for value in volumes.values())) - self.assertTrue(all(value["name"].startswith("solmara-authority-cells-") for value in volumes.values())) + self.assertTrue( + all(not value.get("external", False) for value in volumes.values()) + ) + self.assertTrue( + all( + value["name"].startswith("solmara-authority-cells-") + for value in volumes.values() + ) + ) - def test_runtime_applications_attach_active_volumes_read_only_by_fixed_name(self) -> None: - provisioned = { - value["name"] for value in self.provision["volumes"].values() - } + def test_runtime_applications_attach_active_volumes_read_only_by_fixed_name( + self, + ) -> None: + provisioned = {value["name"] for value in self.provision["volumes"].values()} attached: set[str] = set() for compose in self.runtime.values(): for key, value in compose.get("volumes", {}).items(): @@ -62,7 +69,11 @@ def test_runtime_applications_attach_active_volumes_read_only_by_fixed_name(self def test_private_signing_keys_are_mounted_only_into_matching_signers(self) -> None: services = self.provision["services"] for provider in PROVIDERS: - secret = "mint-signing-jwk" if provider == "mint" else f"{provider}-evidence-signing-jwk" + secret = ( + "mint-signing-jwk" + if provider == "mint" + else f"{provider}-evidence-signing-jwk" + ) consumers = { service_name for service_name, service in services.items() @@ -109,11 +120,11 @@ def test_mint_client_public_keys_use_provisioner_contract_names(self) -> None: ) for client in clients: self.assertIn(f"solmara-provisioning/{client}-public.jwk", targets) - self.assertIn( - "solmara-provisioning/solmara-demo-client-public.jwk", targets - ) + self.assertIn("solmara-provisioning/solmara-demo-client-public.jwk", targets) - def test_each_evidence_provisioner_receives_only_its_public_signing_key(self) -> None: + def test_each_evidence_provisioner_receives_only_its_public_signing_key( + self, + ) -> None: services = self.provision["services"] for provider in PROVIDERS[1:]: provisioner = services[f"{provider}-evidence-provisioner"] @@ -133,6 +144,19 @@ def test_relay_provisioners_receive_no_secret(self) -> None: for authority in RELAYS: self.assertNotIn("secrets", services[f"{authority}-relay-provisioner"]) + def test_provisioners_only_elevate_for_authority_volume_initialization( + self, + ) -> None: + for name, service in self.provision["services"].items(): + if not name.endswith("-provisioner"): + continue + self.assertEqual(service["user"], "0:0") + self.assertEqual(service["network_mode"], "none") + self.assertTrue(service["read_only"]) + self.assertEqual(service["cap_drop"], ["ALL"]) + self.assertEqual(service["cap_add"], ["CHOWN"]) + self.assertEqual(service["security_opt"], ["no-new-privileges:true"]) + def test_provisioners_receive_closed_permanent_dependency_origins(self) -> None: services = self.provision["services"] for name, service in services.items(): @@ -152,14 +176,27 @@ def test_provisioners_receive_closed_permanent_dependency_origins(self) -> None: else: self.assertNotIn("--relay-origin", command) - def test_each_signer_and_transit_initializer_mount_only_its_matching_volume(self) -> None: + def test_each_signer_and_transit_initializer_mount_only_its_matching_volume( + self, + ) -> None: services = self.provision["services"] for provider in PROVIDERS: - volume = "mint-transit" if provider == "mint" else f"{provider}-evidence-transit" - self.assertEqual(services[f"{provider}-transit-init"]["volumes"], [f"{volume}:/transit"]) + volume = ( + "mint-transit" if provider == "mint" else f"{provider}-evidence-transit" + ) + self.assertEqual( + services[f"{provider}-transit-init"]["volumes"], [f"{volume}:/transit"] + ) signer = services[f"{provider}-signer"] self.assertEqual(signer["volumes"], [f"{volume}:/transit"]) - self.assertEqual(signer["depends_on"], {f"{provider}-transit-init": {"condition": "service_completed_successfully"}}) + self.assertEqual( + signer["depends_on"], + { + f"{provider}-transit-init": { + "condition": "service_completed_successfully" + } + }, + ) def test_relay_runtime_secrets_are_authority_scoped(self) -> None: services = {} @@ -171,7 +208,9 @@ def test_relay_runtime_secrets_are_authority_scoped(self) -> None: "SOLMARA_RELAY_AUDIT_KEY": f"${{{authority.upper()}_RELAY_AUDIT_KEY:?required}}" } if authority in {"sipf", "nagdi"}: - expected["SOLMARA_RELAY_CURSOR_KEY"] = f"${{{authority.upper()}_RELAY_CURSOR_KEY:?required}}" + expected["SOLMARA_RELAY_CURSOR_KEY"] = ( + f"${{{authority.upper()}_RELAY_CURSOR_KEY:?required}}" + ) self.assertEqual(environment, expected) def test_runtime_consumers_depend_on_app_local_audit_init(self) -> None: @@ -181,7 +220,14 @@ def test_runtime_consumers_depend_on_app_local_audit_init(self) -> None: self.assertEqual(audit_init["network_mode"], "none") self.assertEqual(audit_init["cap_add"], ["CHOWN", "FOWNER"]) for name, service in services.items(): - if name in {"audit-permissions", "static-metadata", "scenario-runner", "child-benefit-federator", "home", "portal"}: + if name in { + "audit-permissions", + "static-metadata", + "scenario-runner", + "child-benefit-federator", + "home", + "portal", + }: continue self.assertEqual( service["depends_on"]["audit-permissions"], diff --git a/scripts/test_smoke_hosted_provisioner_image.py b/scripts/test_smoke_hosted_provisioner_image.py index bebb3c3..9b695c3 100644 --- a/scripts/test_smoke_hosted_provisioner_image.py +++ b/scripts/test_smoke_hosted_provisioner_image.py @@ -17,6 +17,22 @@ class HostedProvisionerImageSmokeTests(unittest.TestCase): + def test_container_uses_the_same_bounded_volume_initializer_identity_as_hosted( + self, + ) -> None: + with mock.patch.object(MODULE.subprocess, "run") as run: + MODULE._run( + "image@sha256:" + "a" * 64, + ["provision"], + [(Path("/state"), "/provisioned/runtime", False)], + ) + command = run.call_args.args[0] + self.assertEqual(command[command.index("--user") + 1], "0:0") + self.assertEqual(command[command.index("--cap-drop") + 1], "ALL") + self.assertEqual(command[command.index("--cap-add") + 1], "CHOWN") + self.assertIn("none", command) + self.assertIn("--read-only", command) + def test_success_is_sanitized(self) -> None: with tempfile.TemporaryDirectory() as temporary: output, error = StringIO(), StringIO() From 1a8b1c2956acf3fac3ea421c826685d1cdc950cb Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 06:50:44 +0700 Subject: [PATCH 32/53] fix(hosted): preserve provisioner idempotence Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 6 ++++-- compose.coolify.provision.yaml | 2 +- scripts/smoke-hosted-provisioner-image.py | 4 ++++ scripts/test_hosted_image_manifest.py | 3 ++- scripts/test_hosted_provisioning_topology.py | 4 +++- scripts/test_smoke_hosted_provisioner_image.py | 7 ++++++- 6 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 88a5f79..345c887 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -201,7 +201,8 @@ jobs: mkdir -p "$state/runtime" "$state/source" for _ in 1 2; do docker run --rm --platform linux/amd64 --network none --read-only --tmpfs /tmp \ - --user 0:0 --cap-drop ALL --cap-add CHOWN \ + --user 0:0 --cap-drop ALL \ + --cap-add CHOWN --cap-add DAC_READ_SEARCH --cap-add FOWNER \ --mount "type=bind,source=$state/runtime,target=/provisioned/runtime" \ --mount "type=bind,source=$state/source,target=/provisioned/source" \ "$image" provision \ @@ -228,7 +229,8 @@ jobs: stderr="$RUNNER_TEMP/solmara-authority-provisioner.stderr" set +e docker run --rm --platform linux/amd64 --network none --read-only --tmpfs /tmp \ - --user 0:0 --cap-drop ALL --cap-add CHOWN \ + --user 0:0 --cap-drop ALL \ + --cap-add CHOWN --cap-add DAC_READ_SEARCH --cap-add FOWNER \ --mount "type=bind,source=$state/runtime,target=/provisioned/runtime" \ --mount "type=bind,source=$state/source,target=/provisioned/source" \ "$image" provision \ diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml index cc6b96a..92acf96 100644 --- a/compose.coolify.provision.yaml +++ b/compose.coolify.provision.yaml @@ -14,7 +14,7 @@ x-provisioner: &provisioner tmpfs: [/tmp] network_mode: none cap_drop: [ALL] - cap_add: [CHOWN] + cap_add: [CHOWN, DAC_READ_SEARCH, FOWNER] security_opt: [no-new-privileges:true] x-transit-init: &transit-init diff --git a/scripts/smoke-hosted-provisioner-image.py b/scripts/smoke-hosted-provisioner-image.py index cb47b79..fb7f3e2 100755 --- a/scripts/smoke-hosted-provisioner-image.py +++ b/scripts/smoke-hosted-provisioner-image.py @@ -60,6 +60,10 @@ def _run( "ALL", "--cap-add", "CHOWN", + "--cap-add", + "DAC_READ_SEARCH", + "--cap-add", + "FOWNER", "--tmpfs", "/tmp", ] diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index 6faa924..24a1c30 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -298,7 +298,8 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( )["run"] self.assertEqual(provisioner_smoke.count("--platform linux/amd64"), 2) self.assertEqual(provisioner_smoke.count("--user 0:0"), 2) - self.assertEqual(provisioner_smoke.count("--cap-add CHOWN"), 2) + for capability in ("CHOWN", "DAC_READ_SEARCH", "FOWNER"): + self.assertEqual(provisioner_smoke.count(f"--cap-add {capability}"), 2) self.assertIn("org.registrystack.release.revision", label_verification) provisioner_smoke = next( diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index e43dbb6..1a58e8e 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -154,7 +154,9 @@ def test_provisioners_only_elevate_for_authority_volume_initialization( self.assertEqual(service["network_mode"], "none") self.assertTrue(service["read_only"]) self.assertEqual(service["cap_drop"], ["ALL"]) - self.assertEqual(service["cap_add"], ["CHOWN"]) + self.assertEqual( + set(service["cap_add"]), {"CHOWN", "DAC_READ_SEARCH", "FOWNER"} + ) self.assertEqual(service["security_opt"], ["no-new-privileges:true"]) def test_provisioners_receive_closed_permanent_dependency_origins(self) -> None: diff --git a/scripts/test_smoke_hosted_provisioner_image.py b/scripts/test_smoke_hosted_provisioner_image.py index 9b695c3..b9e6225 100644 --- a/scripts/test_smoke_hosted_provisioner_image.py +++ b/scripts/test_smoke_hosted_provisioner_image.py @@ -29,7 +29,12 @@ def test_container_uses_the_same_bounded_volume_initializer_identity_as_hosted( command = run.call_args.args[0] self.assertEqual(command[command.index("--user") + 1], "0:0") self.assertEqual(command[command.index("--cap-drop") + 1], "ALL") - self.assertEqual(command[command.index("--cap-add") + 1], "CHOWN") + capabilities = { + command[index + 1] + for index, argument in enumerate(command) + if argument == "--cap-add" + } + self.assertEqual(capabilities, {"CHOWN", "DAC_READ_SEARCH", "FOWNER"}) self.assertIn("none", command) self.assertIn("--read-only", command) From d17c8abd19af819621b3776d1f48975f61546c97 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 06:56:59 +0700 Subject: [PATCH 33/53] fix(hosted): initialize runner-owned volumes Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 4 ++-- compose.coolify.provision.yaml | 2 +- scripts/smoke-hosted-provisioner-image.py | 2 +- scripts/test_hosted_image_manifest.py | 2 +- scripts/test_hosted_provisioning_topology.py | 2 +- scripts/test_smoke_hosted_provisioner_image.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 345c887..1c9dc73 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -202,7 +202,7 @@ jobs: for _ in 1 2; do docker run --rm --platform linux/amd64 --network none --read-only --tmpfs /tmp \ --user 0:0 --cap-drop ALL \ - --cap-add CHOWN --cap-add DAC_READ_SEARCH --cap-add FOWNER \ + --cap-add CHOWN --cap-add DAC_OVERRIDE --cap-add FOWNER \ --mount "type=bind,source=$state/runtime,target=/provisioned/runtime" \ --mount "type=bind,source=$state/source,target=/provisioned/source" \ "$image" provision \ @@ -230,7 +230,7 @@ jobs: set +e docker run --rm --platform linux/amd64 --network none --read-only --tmpfs /tmp \ --user 0:0 --cap-drop ALL \ - --cap-add CHOWN --cap-add DAC_READ_SEARCH --cap-add FOWNER \ + --cap-add CHOWN --cap-add DAC_OVERRIDE --cap-add FOWNER \ --mount "type=bind,source=$state/runtime,target=/provisioned/runtime" \ --mount "type=bind,source=$state/source,target=/provisioned/source" \ "$image" provision \ diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml index 92acf96..48ff7ca 100644 --- a/compose.coolify.provision.yaml +++ b/compose.coolify.provision.yaml @@ -14,7 +14,7 @@ x-provisioner: &provisioner tmpfs: [/tmp] network_mode: none cap_drop: [ALL] - cap_add: [CHOWN, DAC_READ_SEARCH, FOWNER] + cap_add: [CHOWN, DAC_OVERRIDE, FOWNER] security_opt: [no-new-privileges:true] x-transit-init: &transit-init diff --git a/scripts/smoke-hosted-provisioner-image.py b/scripts/smoke-hosted-provisioner-image.py index fb7f3e2..e1efc09 100755 --- a/scripts/smoke-hosted-provisioner-image.py +++ b/scripts/smoke-hosted-provisioner-image.py @@ -61,7 +61,7 @@ def _run( "--cap-add", "CHOWN", "--cap-add", - "DAC_READ_SEARCH", + "DAC_OVERRIDE", "--cap-add", "FOWNER", "--tmpfs", diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index 24a1c30..f97837f 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -298,7 +298,7 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( )["run"] self.assertEqual(provisioner_smoke.count("--platform linux/amd64"), 2) self.assertEqual(provisioner_smoke.count("--user 0:0"), 2) - for capability in ("CHOWN", "DAC_READ_SEARCH", "FOWNER"): + for capability in ("CHOWN", "DAC_OVERRIDE", "FOWNER"): self.assertEqual(provisioner_smoke.count(f"--cap-add {capability}"), 2) self.assertIn("org.registrystack.release.revision", label_verification) diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index 1a58e8e..8dfc23a 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -155,7 +155,7 @@ def test_provisioners_only_elevate_for_authority_volume_initialization( self.assertTrue(service["read_only"]) self.assertEqual(service["cap_drop"], ["ALL"]) self.assertEqual( - set(service["cap_add"]), {"CHOWN", "DAC_READ_SEARCH", "FOWNER"} + set(service["cap_add"]), {"CHOWN", "DAC_OVERRIDE", "FOWNER"} ) self.assertEqual(service["security_opt"], ["no-new-privileges:true"]) diff --git a/scripts/test_smoke_hosted_provisioner_image.py b/scripts/test_smoke_hosted_provisioner_image.py index b9e6225..87ef272 100644 --- a/scripts/test_smoke_hosted_provisioner_image.py +++ b/scripts/test_smoke_hosted_provisioner_image.py @@ -34,7 +34,7 @@ def test_container_uses_the_same_bounded_volume_initializer_identity_as_hosted( for index, argument in enumerate(command) if argument == "--cap-add" } - self.assertEqual(capabilities, {"CHOWN", "DAC_READ_SEARCH", "FOWNER"}) + self.assertEqual(capabilities, {"CHOWN", "DAC_OVERRIDE", "FOWNER"}) self.assertIn("none", command) self.assertIn("--read-only", command) From 51b82053abd5f3375336bdc7cbaddd41e422a8da Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 07:04:30 +0700 Subject: [PATCH 34/53] test(hosted): tamper immutable runtime in container Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 8 ++++++-- scripts/test_hosted_image_manifest.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 1c9dc73..de745d8 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -223,8 +223,12 @@ jobs: assert (root / "runtime/runtime.yaml").is_file() assert any((root / "runtime/package").iterdir()) PY - chmod u+w "$state/runtime/runtime.yaml" - printf '\n' >> "$state/runtime/runtime.yaml" + docker run --rm --platform linux/amd64 --network none --read-only --tmpfs /tmp \ + --user 0:0 --cap-drop ALL --cap-add DAC_OVERRIDE --cap-add FOWNER \ + --entrypoint python \ + --mount "type=bind,source=$state/runtime,target=/provisioned/runtime" \ + "$image" -c \ + "from pathlib import Path; p=Path('/provisioned/runtime/runtime.yaml'); p.chmod(0o644); p.open('a', encoding='utf-8').write('\\n')" stdout="$RUNNER_TEMP/solmara-authority-provisioner.stdout" stderr="$RUNNER_TEMP/solmara-authority-provisioner.stderr" set +e diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index f97837f..c1e7a2c 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -296,10 +296,11 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( for step in steps if step.get("name") == "Smoke hosted authority provisioner image" )["run"] - self.assertEqual(provisioner_smoke.count("--platform linux/amd64"), 2) - self.assertEqual(provisioner_smoke.count("--user 0:0"), 2) - for capability in ("CHOWN", "DAC_OVERRIDE", "FOWNER"): - self.assertEqual(provisioner_smoke.count(f"--cap-add {capability}"), 2) + self.assertEqual(provisioner_smoke.count("--platform linux/amd64"), 3) + self.assertEqual(provisioner_smoke.count("--user 0:0"), 3) + expected_capability_uses = {"CHOWN": 2, "DAC_OVERRIDE": 3, "FOWNER": 3} + for capability, uses in expected_capability_uses.items(): + self.assertEqual(provisioner_smoke.count(f"--cap-add {capability}"), uses) self.assertIn("org.registrystack.release.revision", label_verification) provisioner_smoke = next( @@ -311,6 +312,9 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( self.assertIn("--network none --read-only", provisioner_smoke) self.assertIn('test "$status" -eq 1', provisioner_smoke) self.assertIn("hosted target provisioning failed", provisioner_smoke) + self.assertNotIn('chmod u+w "$state/runtime/runtime.yaml"', provisioner_smoke) + self.assertIn("--entrypoint python", provisioner_smoke) + self.assertIn("--cap-add DAC_OVERRIDE --cap-add FOWNER", provisioner_smoke) self.assertEqual( provisioner_smoke.count( "--mint-origin https://mint-authority-cells.solmara.registrystack.org" From ce3bbf627f5b607ff685cee9eaef63d997abf472 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 07:10:43 +0700 Subject: [PATCH 35/53] test(hosted): bound signer startup wait Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 2 +- scripts/test_hosted_image_manifest.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index de745d8..a8074ef 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -278,7 +278,7 @@ jobs: --public-jwk /run/secrets/signing-public.jwk \ --socket /transit/transit-proxy.sock \ --key-name solmara-evidence-cra - for _ in $(seq 1 50); do + for _ in $(seq 1 200); do test -S "$state/transit/transit-proxy.sock" && break sleep 0.1 done diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index c1e7a2c..c4d1ec1 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -332,6 +332,7 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( self.assertIn("import cryptography", signer_smoke) self.assertIn("--public-jwk /run/secrets/signing-public.jwk", signer_smoke) self.assertIn("v1/transit/keys/solmara-evidence-cra", signer_smoke) + self.assertIn("seq 1 200", signer_smoke) self.assertIn("cells/nia/bundle/public-keys", signer_smoke) self.assertIn('test "$status" -eq 1', signer_smoke) self.assertIn("hosted Transit signer could not start", signer_smoke) From 0162ea956b80bbec41037990c67fc246f8a375e7 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 07:20:26 +0700 Subject: [PATCH 36/53] test(hosted): exercise signer socket volume Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 55 +++++++++++++++++++------ scripts/test_hosted_image_manifest.py | 6 +++ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index a8074ef..27001cc 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -259,36 +259,63 @@ jobs: image="$SOLMARA_IMAGE_REGISTRY/solmara-lab-transit-signer@${{ steps.transit_signer.outputs.digest }}" docker run --rm --network none --read-only --entrypoint python "$image" -c 'import cryptography' state="$RUNNER_TEMP/solmara-transit-signer-state" - install -d -m 0700 "$state/secrets" "$state/transit" - sudo chown 65532:65532 "$state/secrets" "$state/transit" + install -d -m 0700 "$state/secrets" + sudo chown 65532:65532 "$state/secrets" sudo install -o 65532 -g 65532 -m 0400 \ config/evidence/local/cells/cra/secrets/signing.jwk \ "$state/secrets/signing.jwk" sudo install -o 65532 -g 65532 -m 0400 \ runtime/evidence-cells/cells/cra/bundle/public-keys/*.jwk.json \ "$state/secrets/signing-public.jwk" - name="solmara-transit-signer-${GITHUB_RUN_ID}" - trap 'docker rm --force "$name" >/dev/null 2>&1 || true' EXIT + name="solmara-transit-signer-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + volume="solmara-transit-signer-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + docker volume create "$volume" >/dev/null + cleanup() { + docker rm --force "$name" >/dev/null 2>&1 || true + docker volume rm --force "$volume" >/dev/null 2>&1 || true + } + trap cleanup EXIT + docker run --rm --network none --read-only --tmpfs /tmp \ + --user 0:0 --cap-drop ALL --cap-add CHOWN --entrypoint python \ + --mount "type=volume,source=$volume,target=/transit" \ + "$image" -c \ + 'import os; os.chmod("/transit", 0o700); os.chown("/transit", 65532, 65532)' docker run --detach --name "$name" --network none --read-only --tmpfs /tmp \ --mount "type=bind,source=$state/secrets/signing.jwk,target=/run/secrets/signing.jwk,readonly" \ --mount "type=bind,source=$state/secrets/signing-public.jwk,target=/run/secrets/signing-public.jwk,readonly" \ - --mount "type=bind,source=$state/transit,target=/transit" \ + --mount "type=volume,source=$volume,target=/transit" \ "$image" \ --private-jwk /run/secrets/signing.jwk \ --public-jwk /run/secrets/signing-public.jwk \ --socket /transit/transit-proxy.sock \ --key-name solmara-evidence-cra for _ in $(seq 1 200); do - test -S "$state/transit/transit-proxy.sock" && break + docker exec "$name" python -c \ + 'from pathlib import Path; raise SystemExit(0 if Path("/transit/transit-proxy.sock").is_socket() else 1)' \ + && break sleep 0.1 done - test -S "$state/transit/transit-proxy.sock" - curl --fail --silent --show-error \ - --unix-socket "$state/transit/transit-proxy.sock" \ - -H 'X-Vault-Request: true' \ - http://localhost/v1/transit/keys/solmara-evidence-cra >/dev/null + docker exec "$name" python -c \ + 'from pathlib import Path; raise SystemExit(0 if Path("/transit/transit-proxy.sock").is_socket() else 1)' + docker run --rm --network none --read-only --tmpfs /tmp \ + --user 65532:65532 --entrypoint python \ + --mount "type=volume,source=$volume,target=/transit,readonly" \ + "$image" -c ' + import socket + client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + client.settimeout(5) + client.connect("/transit/transit-proxy.sock") + client.sendall(b"GET /v1/transit/keys/solmara-evidence-cra HTTP/1.1\r\nHost: localhost\r\nX-Vault-Request: true\r\nConnection: close\r\n\r\n") + response = bytearray() + while True: + chunk = client.recv(65536) + if not chunk: + break + response.extend(chunk) + client.close() + raise SystemExit(0 if response.startswith(b"HTTP/1.1 200 ") else 1) + ' docker rm --force "$name" >/dev/null - trap - EXIT sudo install -o 65532 -g 65532 -m 0400 \ runtime/evidence-cells/cells/nia/bundle/public-keys/*.jwk.json \ @@ -299,7 +326,7 @@ jobs: docker run --rm --network none --read-only --tmpfs /tmp \ --mount "type=bind,source=$state/secrets/signing.jwk,target=/run/secrets/signing.jwk,readonly" \ --mount "type=bind,source=$state/secrets/signing-public.jwk,target=/run/secrets/signing-public.jwk,readonly" \ - --mount "type=bind,source=$state/transit,target=/transit" \ + --mount "type=volume,source=$volume,target=/transit" \ "$image" \ --private-jwk /run/secrets/signing.jwk \ --public-jwk /run/secrets/signing-public.jwk \ @@ -310,6 +337,8 @@ jobs: test "$status" -eq 1 test ! -s "$stdout" test "$(cat "$stderr")" = "hosted Transit signer could not start" + cleanup + trap - EXIT - name: Build and push static metadata id: static_metadata uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index c4d1ec1..c47e73f 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -333,6 +333,12 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( self.assertIn("--public-jwk /run/secrets/signing-public.jwk", signer_smoke) self.assertIn("v1/transit/keys/solmara-evidence-cra", signer_smoke) self.assertIn("seq 1 200", signer_smoke) + self.assertIn("docker volume create", signer_smoke) + self.assertIn("type=volume,source=$volume,target=/transit", signer_smoke) + self.assertIn("target=/transit,readonly", signer_smoke) + self.assertIn('client.connect("/transit/transit-proxy.sock")', signer_smoke) + self.assertNotIn("--unix-socket", signer_smoke) + self.assertNotIn("$state/transit", signer_smoke) self.assertIn("cells/nia/bundle/public-keys", signer_smoke) self.assertIn('test "$status" -eq 1', signer_smoke) self.assertIn("hosted Transit signer could not start", signer_smoke) From f146fdb25ee36b687e867fc1fa7f70253b43c58b Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 07:27:33 +0700 Subject: [PATCH 37/53] feat(hosted): add side-by-side evidence routes Signed-off-by: Jeremi Joslin --- compose.coolify.agriculture.yaml | 2 +- compose.coolify.interior.yaml | 4 +- compose.coolify.labour-pensions.yaml | 2 +- compose.coolify.social-development.yaml | 4 +- justfile | 2 +- scripts/test_hosted_evidence_routes.py | 163 ++++++++++++++++++++++++ 6 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 scripts/test_hosted_evidence_routes.py diff --git a/compose.coolify.agriculture.yaml b/compose.coolify.agriculture.yaml index 436ca25..3f1bd7f 100644 --- a/compose.coolify.agriculture.yaml +++ b/compose.coolify.agriculture.yaml @@ -42,7 +42,7 @@ services: depends_on: audit-permissions: {condition: service_completed_successfully} nagdi-relay: {condition: service_started} - labels: {solmara.lab.host: nagdi-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: "${SOLMARA_NAGDI_EVIDENCE_PUBLIC_HOST:-nagdi-evidence.solmara.registrystack.org}", solmara.rollout.slot: authority-cells} volumes: nagdi-relay-runtime: {external: true, name: solmara-authority-cells-nagdi-relay-runtime} diff --git a/compose.coolify.interior.yaml b/compose.coolify.interior.yaml index 170d71e..9f2947e 100644 --- a/compose.coolify.interior.yaml +++ b/compose.coolify.interior.yaml @@ -50,7 +50,7 @@ services: depends_on: audit-permissions: {condition: service_completed_successfully} cra-relay: {condition: service_started} - labels: {solmara.lab.host: cra-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: "${SOLMARA_CRA_EVIDENCE_PUBLIC_HOST:-cra-evidence.solmara.registrystack.org}", solmara.rollout.slot: authority-cells} nia-evidence: <<: *runtime @@ -58,7 +58,7 @@ services: command: [--runtime, /etc/registry-evidence/nia/runtime.yaml, serve] volumes: [nia-evidence-runtime:/etc/registry-evidence/nia:ro, nia-evidence-secrets:/run/secrets/registry-evidence/nia:ro, nia-evidence-transit:/run/registry-evidence/nia:ro, nia-evidence-extracts:/var/lib/registry-evidence/nia/extracts:ro, nia-evidence-audit:/var/lib/registry-evidence/nia/audit] networks: {runtime: {ipv4_address: 172.29.2.22}} - labels: {solmara.lab.host: nia-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: "${SOLMARA_NIA_EVIDENCE_PUBLIC_HOST:-nia-evidence.solmara.registrystack.org}", solmara.rollout.slot: authority-cells} volumes: cra-relay-runtime: {external: true, name: solmara-authority-cells-cra-relay-runtime} diff --git a/compose.coolify.labour-pensions.yaml b/compose.coolify.labour-pensions.yaml index ea76684..00e7cc4 100644 --- a/compose.coolify.labour-pensions.yaml +++ b/compose.coolify.labour-pensions.yaml @@ -42,7 +42,7 @@ services: depends_on: audit-permissions: {condition: service_completed_successfully} sipf-relay: {condition: service_started} - labels: {solmara.lab.host: sipf-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: "${SOLMARA_SIPF_EVIDENCE_PUBLIC_HOST:-sipf-evidence.solmara.registrystack.org}", solmara.rollout.slot: authority-cells} volumes: sipf-relay-runtime: {external: true, name: solmara-authority-cells-sipf-relay-runtime} diff --git a/compose.coolify.social-development.yaml b/compose.coolify.social-development.yaml index 5268390..c6b574a 100644 --- a/compose.coolify.social-development.yaml +++ b/compose.coolify.social-development.yaml @@ -39,7 +39,7 @@ services: command: [--runtime, /etc/registry-evidence/sro/runtime.yaml, serve] volumes: [sro-evidence-runtime:/etc/registry-evidence/sro:ro, sro-evidence-secrets:/run/secrets/registry-evidence/sro:ro, sro-evidence-transit:/run/registry-evidence/sro:ro, sro-evidence-extracts:/var/lib/registry-evidence/sro/extracts:ro, sro-evidence-audit:/var/lib/registry-evidence/sro/audit] networks: {runtime: {ipv4_address: 172.29.3.23}} - labels: {solmara.lab.host: sro-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: "${SOLMARA_SRO_EVIDENCE_PUBLIC_HOST:-sro-evidence.solmara.registrystack.org}", solmara.rollout.slot: authority-cells} mosd-programme-evidence: <<: *runtime @@ -50,7 +50,7 @@ services: depends_on: audit-permissions: {condition: service_completed_successfully} mosd-relay: {condition: service_started} - labels: {solmara.lab.host: mosd-programme-evidence.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + labels: {solmara.lab.host: "${SOLMARA_MOSD_PROGRAMME_EVIDENCE_PUBLIC_HOST:-mosd-programme-evidence.solmara.registrystack.org}", solmara.rollout.slot: authority-cells} volumes: mosd-relay-runtime: {external: true, name: solmara-authority-cells-mosd-relay-runtime} diff --git a/justfile b/justfile index 52d4e76..400663b 100644 --- a/justfile +++ b/justfile @@ -54,7 +54,7 @@ lint: test: cd generator && uv run python -m unittest discover -s tests uv run python -m unittest discover -s scenario-runner -p 'test_*.py' - uv run python -m unittest relays/test_relay_projects.py evidence/tests/test_cells.py scripts/test_metadata_authority_contracts.py scripts/test_image_pins.py scripts/test_build_registry_stack_runtime.py scripts/test_hosted_image_manifest.py scripts/test_hosted_provisioning_topology.py scripts/test_hosted_network_isolation.py scripts/test_hosted_home_topology.py scripts/test_hosted_esignet_topology.py scripts/test_hosted_runtime_assets.py scripts/test_provision_hosted_runtime.py scripts/test_hosted_transit_signer.py scripts/test_smoke_hosted_provisioner_image.py scripts/test_runtime_topology.py scripts/test_registry_stack_release_pin.py scripts/test_hosted_authority_rollout.py scripts/test_local_relay_runtime_stager.py scripts/test_local_transit_proxy.py scripts/test_local_transit_signers.py scripts/test_local_transit_providers.py scripts/test_signer_public_keys.py scripts/test_project_runtime_secrets.py scripts/test_gen_secrets.py scripts/test_publish_runtime_extracts.py scripts/test_lifecycle_proof.py scripts/test_live_lifecycle_proof.py scripts/test_local_relay_source_publisher.py scripts/test_smoke_programme_acceptance.py scripts/test_smoke_esignet.py + uv run python -m unittest relays/test_relay_projects.py evidence/tests/test_cells.py scripts/test_metadata_authority_contracts.py scripts/test_image_pins.py scripts/test_build_registry_stack_runtime.py scripts/test_hosted_image_manifest.py scripts/test_hosted_provisioning_topology.py scripts/test_hosted_network_isolation.py scripts/test_hosted_home_topology.py scripts/test_hosted_esignet_topology.py scripts/test_hosted_evidence_routes.py scripts/test_hosted_runtime_assets.py scripts/test_provision_hosted_runtime.py scripts/test_hosted_transit_signer.py scripts/test_smoke_hosted_provisioner_image.py scripts/test_runtime_topology.py scripts/test_registry_stack_release_pin.py scripts/test_hosted_authority_rollout.py scripts/test_local_relay_runtime_stager.py scripts/test_local_transit_proxy.py scripts/test_local_transit_signers.py scripts/test_local_transit_providers.py scripts/test_signer_public_keys.py scripts/test_project_runtime_secrets.py scripts/test_gen_secrets.py scripts/test_publish_runtime_extracts.py scripts/test_lifecycle_proof.py scripts/test_live_lifecycle_proof.py scripts/test_local_relay_source_publisher.py scripts/test_smoke_programme_acceptance.py scripts/test_smoke_esignet.py cd portal && pnpm test cd home && pnpm test diff --git a/scripts/test_hosted_evidence_routes.py b/scripts/test_hosted_evidence_routes.py new file mode 100644 index 0000000..f585eec --- /dev/null +++ b/scripts/test_hosted_evidence_routes.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import json +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +TEST_IMAGE = "example.invalid/solmara@sha256:" + "a" * 64 + +EVIDENCE_ROUTES = { + "cra-evidence": ( + ROOT / "compose.coolify.interior.yaml", + "SOLMARA_CRA_EVIDENCE_PUBLIC_HOST", + "cra-evidence.solmara.registrystack.org", + ), + "nia-evidence": ( + ROOT / "compose.coolify.interior.yaml", + "SOLMARA_NIA_EVIDENCE_PUBLIC_HOST", + "nia-evidence.solmara.registrystack.org", + ), + "sro-evidence": ( + ROOT / "compose.coolify.social-development.yaml", + "SOLMARA_SRO_EVIDENCE_PUBLIC_HOST", + "sro-evidence.solmara.registrystack.org", + ), + "mosd-programme-evidence": ( + ROOT / "compose.coolify.social-development.yaml", + "SOLMARA_MOSD_PROGRAMME_EVIDENCE_PUBLIC_HOST", + "mosd-programme-evidence.solmara.registrystack.org", + ), + "sipf-evidence": ( + ROOT / "compose.coolify.labour-pensions.yaml", + "SOLMARA_SIPF_EVIDENCE_PUBLIC_HOST", + "sipf-evidence.solmara.registrystack.org", + ), + "nagdi-evidence": ( + ROOT / "compose.coolify.agriculture.yaml", + "SOLMARA_NAGDI_EVIDENCE_PUBLIC_HOST", + "nagdi-evidence.solmara.registrystack.org", + ), +} + +RELAY_ROUTES = { + "cra-relay": ( + ROOT / "compose.coolify.interior.yaml", + "cra-relay-authority-cells.solmara.registrystack.org", + ), + "nia-relay": ( + ROOT / "compose.coolify.interior.yaml", + "nia-relay-authority-cells.solmara.registrystack.org", + ), + "mosd-relay": ( + ROOT / "compose.coolify.social-development.yaml", + "mosd-programme-relay-authority-cells.solmara.registrystack.org", + ), + "sipf-relay": ( + ROOT / "compose.coolify.labour-pensions.yaml", + "sipf-relay-authority-cells.solmara.registrystack.org", + ), + "nagdi-relay": ( + ROOT / "compose.coolify.agriculture.yaml", + "nagdi-relay-authority-cells.solmara.registrystack.org", + ), +} + + +def compose_environment() -> dict[str, str]: + environment = os.environ | { + "SOLMARA_AUTHORITY_PROVISIONER_IMAGE": TEST_IMAGE, + "REGISTRY_RELAY_IMAGE": TEST_IMAGE, + "SOLMARA_EVIDENCE_IMAGE": TEST_IMAGE, + "CRA_RELAY_AUDIT_KEY": "test", + "NIA_RELAY_AUDIT_KEY": "test", + "MOSD_RELAY_AUDIT_KEY": "test", + "SIPF_RELAY_AUDIT_KEY": "test", + "SIPF_RELAY_CURSOR_KEY": "test", + "NAGDI_RELAY_AUDIT_KEY": "test", + "NAGDI_RELAY_CURSOR_KEY": "test", + } + for _, variable, _ in EVIDENCE_ROUTES.values(): + environment.pop(variable, None) + return environment + + +def render_compose(path: Path, overrides: dict[str, str] | None = None) -> dict: + environment = compose_environment() + environment.update(overrides or {}) + with tempfile.NamedTemporaryFile() as empty_env: + result = subprocess.run( + [ + "docker", + "compose", + "--env-file", + empty_env.name, + "-f", + str(path), + "config", + "--format", + "json", + ], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + env=environment, + ) + return json.loads(result.stdout) + + +class HostedEvidenceRouteTests(unittest.TestCase): + def test_raw_compose_declares_canonical_evidence_defaults(self) -> None: + for service, (path, variable, canonical_host) in EVIDENCE_ROUTES.items(): + with self.subTest(service=service): + compose = yaml.safe_load(path.read_text(encoding="utf-8")) + self.assertEqual( + compose["services"][service]["labels"]["solmara.lab.host"], + f"${{{variable}:-{canonical_host}}}", + ) + + def test_canonical_evidence_defaults_render_across_apps(self) -> None: + rendered = { + path: render_compose(path) + for path in {route[0] for route in EVIDENCE_ROUTES.values()} + } + for service, (path, _, canonical_host) in EVIDENCE_ROUTES.items(): + with self.subTest(service=service): + self.assertEqual( + rendered[path]["services"][service]["labels"]["solmara.lab.host"], + canonical_host, + ) + + def test_staging_evidence_hosts_render_without_changing_relay_routes(self) -> None: + overrides = { + variable: f"{service}.staging.example.org" + for service, (_, variable, _) in EVIDENCE_ROUTES.items() + } + rendered = { + path: render_compose(path, overrides) + for path in {route[0] for route in EVIDENCE_ROUTES.values()} + } + for service, (path, variable, _) in EVIDENCE_ROUTES.items(): + with self.subTest(service=service): + self.assertEqual( + rendered[path]["services"][service]["labels"]["solmara.lab.host"], + overrides[variable], + ) + + for service, (path, permanent_host) in RELAY_ROUTES.items(): + with self.subTest(service=service): + self.assertEqual( + rendered[path]["services"][service]["labels"]["solmara.lab.host"], + permanent_host, + ) + + +if __name__ == "__main__": + unittest.main() From 690c5562fb68b6de484fa73f13fdbb9f7b07629b Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 08:08:03 +0700 Subject: [PATCH 38/53] fix(hosted): split authority signers Signed-off-by: Jeremi Joslin --- compose.coolify.provision.yaml | 108 --------------- compose.coolify.signers.yaml | 130 ++++++++++++++++++ docs/hosted-deployment.md | 28 ++-- scripts/check-coolify-compose.sh | 48 ++++--- scripts/test_hosted_provisioning_topology.py | 133 ++++++++++++++++++- 5 files changed, 305 insertions(+), 142 deletions(-) create mode 100644 compose.coolify.signers.yaml diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml index 48ff7ca..ef57614 100644 --- a/compose.coolify.provision.yaml +++ b/compose.coolify.provision.yaml @@ -17,35 +17,6 @@ x-provisioner: &provisioner cap_add: [CHOWN, DAC_OVERRIDE, FOWNER] security_opt: [no-new-privileges:true] -x-transit-init: &transit-init - image: ${SOLMARA_TRANSIT_SIGNER_IMAGE:?set the digest-pinned Solmara Transit signer image} - entrypoint: [/usr/local/bin/python, -c] - command: ["import os,stat,sys; p='/transit'; s=os.lstat(p); ok=stat.S_ISDIR(s.st_mode) and not stat.S_ISLNK(s.st_mode); not ok and sys.exit(1); os.chown(p,65532,65532); os.chmod(p,0o700)"] - restart: "no" - user: "0:0" - read_only: true - tmpfs: [/tmp] - network_mode: none - cap_drop: [ALL] - cap_add: [CHOWN, FOWNER] - security_opt: [no-new-privileges:true] - -x-signer: &signer - image: ${SOLMARA_TRANSIT_SIGNER_IMAGE:?set the digest-pinned Solmara Transit signer image} - restart: unless-stopped - user: "65532:65532" - read_only: true - tmpfs: [/tmp] - network_mode: none - cap_drop: [ALL] - security_opt: [no-new-privileges:true] - healthcheck: - test: [CMD, /usr/local/bin/python, -c, "import socket; s=socket.socket(socket.AF_UNIX); s.connect('/transit/transit-proxy.sock'); s.close()"] - interval: 2s - timeout: 1s - retries: 30 - start_period: 2s - services: cra-relay-provisioner: <<: *provisioner @@ -150,103 +121,24 @@ services: - {source: nia-esignet-client-public-jwk, target: solmara-provisioning/nia-esignet-public.jwk} - {source: solmara-demo-client-public-jwk, target: solmara-provisioning/solmara-demo-client-public.jwk} - mint-transit-init: - <<: *transit-init - volumes: [mint-transit:/transit] - cra-transit-init: - <<: *transit-init - volumes: [cra-evidence-transit:/transit] - nia-transit-init: - <<: *transit-init - volumes: [nia-evidence-transit:/transit] - sro-transit-init: - <<: *transit-init - volumes: [sro-evidence-transit:/transit] - mosd-transit-init: - <<: *transit-init - volumes: [mosd-evidence-transit:/transit] - sipf-transit-init: - <<: *transit-init - volumes: [sipf-evidence-transit:/transit] - nagdi-transit-init: - <<: *transit-init - volumes: [nagdi-evidence-transit:/transit] - - mint-signer: - <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-mint] - volumes: [mint-transit:/transit] - secrets: [{source: mint-signing-jwk, target: signing.jwk}, {source: mint-signing-public-jwk, target: signing-public.jwk}] - depends_on: - mint-transit-init: {condition: service_completed_successfully} - cra-signer: - <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-cra] - volumes: [cra-evidence-transit:/transit] - secrets: [{source: cra-evidence-signing-jwk, target: signing.jwk}, {source: cra-evidence-signing-public-jwk, target: signing-public.jwk}] - depends_on: - cra-transit-init: {condition: service_completed_successfully} - nia-signer: - <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-nia] - volumes: [nia-evidence-transit:/transit] - secrets: [{source: nia-evidence-signing-jwk, target: signing.jwk}, {source: nia-evidence-signing-public-jwk, target: signing-public.jwk}] - depends_on: - nia-transit-init: {condition: service_completed_successfully} - sro-signer: - <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-sro] - volumes: [sro-evidence-transit:/transit] - secrets: [{source: sro-evidence-signing-jwk, target: signing.jwk}, {source: sro-evidence-signing-public-jwk, target: signing-public.jwk}] - depends_on: - sro-transit-init: {condition: service_completed_successfully} - mosd-signer: - <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-mosd-programme] - volumes: [mosd-evidence-transit:/transit] - secrets: [{source: mosd-evidence-signing-jwk, target: signing.jwk}, {source: mosd-evidence-signing-public-jwk, target: signing-public.jwk}] - depends_on: - mosd-transit-init: {condition: service_completed_successfully} - sipf-signer: - <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-sipf] - volumes: [sipf-evidence-transit:/transit] - secrets: [{source: sipf-evidence-signing-jwk, target: signing.jwk}, {source: sipf-evidence-signing-public-jwk, target: signing-public.jwk}] - depends_on: - sipf-transit-init: {condition: service_completed_successfully} - nagdi-signer: - <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-nagdi] - volumes: [nagdi-evidence-transit:/transit] - secrets: [{source: nagdi-evidence-signing-jwk, target: signing.jwk}, {source: nagdi-evidence-signing-public-jwk, target: signing-public.jwk}] - depends_on: - nagdi-transit-init: {condition: service_completed_successfully} - secrets: - mint-signing-jwk: {environment: MINT_SIGNING_JWK} mint-signing-public-jwk: {environment: MINT_SIGNING_PUBLIC_JWK} mint-audit-hmac-key: {environment: MINT_AUDIT_HMAC_KEY} - cra-evidence-signing-jwk: {environment: CRA_EVIDENCE_SIGNING_JWK} cra-evidence-signing-public-jwk: {environment: CRA_EVIDENCE_SIGNING_PUBLIC_JWK} cra-evidence-audit-hmac-key: {environment: CRA_EVIDENCE_AUDIT_HMAC_KEY} cra-evidence-subject-binding-hmac-key: {environment: CRA_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} - nia-evidence-signing-jwk: {environment: NIA_EVIDENCE_SIGNING_JWK} nia-evidence-signing-public-jwk: {environment: NIA_EVIDENCE_SIGNING_PUBLIC_JWK} nia-evidence-audit-hmac-key: {environment: NIA_EVIDENCE_AUDIT_HMAC_KEY} nia-evidence-subject-binding-hmac-key: {environment: NIA_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} - sro-evidence-signing-jwk: {environment: SRO_EVIDENCE_SIGNING_JWK} sro-evidence-signing-public-jwk: {environment: SRO_EVIDENCE_SIGNING_PUBLIC_JWK} sro-evidence-audit-hmac-key: {environment: SRO_EVIDENCE_AUDIT_HMAC_KEY} sro-evidence-subject-binding-hmac-key: {environment: SRO_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} - mosd-evidence-signing-jwk: {environment: MOSD_EVIDENCE_SIGNING_JWK} mosd-evidence-signing-public-jwk: {environment: MOSD_EVIDENCE_SIGNING_PUBLIC_JWK} mosd-evidence-audit-hmac-key: {environment: MOSD_EVIDENCE_AUDIT_HMAC_KEY} mosd-evidence-subject-binding-hmac-key: {environment: MOSD_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} - sipf-evidence-signing-jwk: {environment: SIPF_EVIDENCE_SIGNING_JWK} sipf-evidence-signing-public-jwk: {environment: SIPF_EVIDENCE_SIGNING_PUBLIC_JWK} sipf-evidence-audit-hmac-key: {environment: SIPF_EVIDENCE_AUDIT_HMAC_KEY} sipf-evidence-subject-binding-hmac-key: {environment: SIPF_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} - nagdi-evidence-signing-jwk: {environment: NAGDI_EVIDENCE_SIGNING_JWK} nagdi-evidence-signing-public-jwk: {environment: NAGDI_EVIDENCE_SIGNING_PUBLIC_JWK} nagdi-evidence-audit-hmac-key: {environment: NAGDI_EVIDENCE_AUDIT_HMAC_KEY} nagdi-evidence-subject-binding-hmac-key: {environment: NAGDI_EVIDENCE_SUBJECT_BINDING_HMAC_KEY} diff --git a/compose.coolify.signers.yaml b/compose.coolify.signers.yaml new file mode 100644 index 0000000..593652b --- /dev/null +++ b/compose.coolify.signers.yaml @@ -0,0 +1,130 @@ +name: solmara-authority-cell-signers + +# This operator-only application is the sole private issuer-key boundary. It +# attaches only the fixed Transit volumes owned by the provisioning application. +x-transit-init: &transit-init + image: ${SOLMARA_TRANSIT_SIGNER_IMAGE:?set the digest-pinned Solmara Transit signer image} + entrypoint: [/usr/local/bin/python, -c] + command: ["import os,stat,sys; p='/transit'; s=os.lstat(p); ok=stat.S_ISDIR(s.st_mode) and not stat.S_ISLNK(s.st_mode); not ok and sys.exit(1); os.chown(p,65532,65532); os.chmod(p,0o700)"] + restart: "no" + user: "0:0" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + cap_add: [CHOWN, FOWNER] + security_opt: [no-new-privileges:true] + +x-signer: &signer + image: ${SOLMARA_TRANSIT_SIGNER_IMAGE:?set the digest-pinned Solmara Transit signer image} + restart: unless-stopped + user: "65532:65532" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + healthcheck: + test: [CMD, /usr/local/bin/python, -c, "import socket; s=socket.socket(socket.AF_UNIX); s.connect('/transit/transit-proxy.sock'); s.close()"] + interval: 2s + timeout: 1s + retries: 30 + start_period: 2s + +services: + mint-transit-init: + <<: *transit-init + volumes: [mint-transit:/transit] + cra-transit-init: + <<: *transit-init + volumes: [cra-evidence-transit:/transit] + nia-transit-init: + <<: *transit-init + volumes: [nia-evidence-transit:/transit] + sro-transit-init: + <<: *transit-init + volumes: [sro-evidence-transit:/transit] + mosd-transit-init: + <<: *transit-init + volumes: [mosd-evidence-transit:/transit] + sipf-transit-init: + <<: *transit-init + volumes: [sipf-evidence-transit:/transit] + nagdi-transit-init: + <<: *transit-init + volumes: [nagdi-evidence-transit:/transit] + + mint-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-mint] + volumes: [mint-transit:/transit] + secrets: [{source: mint-signing-jwk, target: signing.jwk}, {source: mint-signing-public-jwk, target: signing-public.jwk}] + depends_on: + mint-transit-init: {condition: service_completed_successfully} + cra-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-cra] + volumes: [cra-evidence-transit:/transit] + secrets: [{source: cra-evidence-signing-jwk, target: signing.jwk}, {source: cra-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + cra-transit-init: {condition: service_completed_successfully} + nia-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-nia] + volumes: [nia-evidence-transit:/transit] + secrets: [{source: nia-evidence-signing-jwk, target: signing.jwk}, {source: nia-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + nia-transit-init: {condition: service_completed_successfully} + sro-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-sro] + volumes: [sro-evidence-transit:/transit] + secrets: [{source: sro-evidence-signing-jwk, target: signing.jwk}, {source: sro-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + sro-transit-init: {condition: service_completed_successfully} + mosd-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-mosd-programme] + volumes: [mosd-evidence-transit:/transit] + secrets: [{source: mosd-evidence-signing-jwk, target: signing.jwk}, {source: mosd-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + mosd-transit-init: {condition: service_completed_successfully} + sipf-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-sipf] + volumes: [sipf-evidence-transit:/transit] + secrets: [{source: sipf-evidence-signing-jwk, target: signing.jwk}, {source: sipf-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + sipf-transit-init: {condition: service_completed_successfully} + nagdi-signer: + <<: *signer + command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-nagdi] + volumes: [nagdi-evidence-transit:/transit] + secrets: [{source: nagdi-evidence-signing-jwk, target: signing.jwk}, {source: nagdi-evidence-signing-public-jwk, target: signing-public.jwk}] + depends_on: + nagdi-transit-init: {condition: service_completed_successfully} + +secrets: + mint-signing-jwk: {environment: MINT_SIGNING_JWK} + mint-signing-public-jwk: {environment: MINT_SIGNING_PUBLIC_JWK} + cra-evidence-signing-jwk: {environment: CRA_EVIDENCE_SIGNING_JWK} + cra-evidence-signing-public-jwk: {environment: CRA_EVIDENCE_SIGNING_PUBLIC_JWK} + nia-evidence-signing-jwk: {environment: NIA_EVIDENCE_SIGNING_JWK} + nia-evidence-signing-public-jwk: {environment: NIA_EVIDENCE_SIGNING_PUBLIC_JWK} + sro-evidence-signing-jwk: {environment: SRO_EVIDENCE_SIGNING_JWK} + sro-evidence-signing-public-jwk: {environment: SRO_EVIDENCE_SIGNING_PUBLIC_JWK} + mosd-evidence-signing-jwk: {environment: MOSD_EVIDENCE_SIGNING_JWK} + mosd-evidence-signing-public-jwk: {environment: MOSD_EVIDENCE_SIGNING_PUBLIC_JWK} + sipf-evidence-signing-jwk: {environment: SIPF_EVIDENCE_SIGNING_JWK} + sipf-evidence-signing-public-jwk: {environment: SIPF_EVIDENCE_SIGNING_PUBLIC_JWK} + nagdi-evidence-signing-jwk: {environment: NAGDI_EVIDENCE_SIGNING_JWK} + nagdi-evidence-signing-public-jwk: {environment: NAGDI_EVIDENCE_SIGNING_PUBLIC_JWK} + +volumes: + mint-transit: {external: true, name: solmara-authority-cells-mint-transit} + cra-evidence-transit: {external: true, name: solmara-authority-cells-cra-evidence-transit} + nia-evidence-transit: {external: true, name: solmara-authority-cells-nia-evidence-transit} + sro-evidence-transit: {external: true, name: solmara-authority-cells-sro-evidence-transit} + mosd-evidence-transit: {external: true, name: solmara-authority-cells-mosd-evidence-transit} + sipf-evidence-transit: {external: true, name: solmara-authority-cells-sipf-evidence-transit} + nagdi-evidence-transit: {external: true, name: solmara-authority-cells-nagdi-evidence-transit} diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index cae80fc..c0128a9 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -53,7 +53,8 @@ Evidence hosts are: - `nagdi-evidence.solmara.registrystack.org` `compose.coolify.provision.yaml` is a dedicated operator-only application. It -owns 34 fixed-name active volumes: +owns 34 fixed-name active volumes and runs only the one-shot target +provisioners: | Owner | Volumes | Contents | |---|---:|---| @@ -61,11 +62,14 @@ owns 34 fixed-name active volumes: | Five Relays | 10 | One runtime and one mutable source volume per authority | | Six Evidence cells | 21 | Runtime, secrets, and Transit socket per cell, plus the CRA, NIA, and SRO immutable-extract volumes | -The same application runs seven isolated signers: one for Mint and one for each -Evidence cell. Runtime applications attach the fixed-name runtime, source, -secret, extract, and Transit volumes as external read-only volumes. Each runtime -application owns the writable audit volumes for its services and initializes -their permissions without reading or replacing existing audit records. +`compose.coolify.signers.yaml` is a separate operator-only application. It +attaches only the seven fixed Transit volumes as external volumes and runs one +isolated signer for Mint and one for each Evidence cell. The provision +application never receives a private issuer signing key. Runtime applications +attach the fixed-name runtime, source, secret, extract, and Transit volumes as +external read-only volumes. Each runtime application owns the writable audit +volumes for its services and initializes their permissions without reading or +replacing existing audit records. ## Mint clients @@ -141,20 +145,22 @@ Deploy the reset alongside the existing deployment in this order: 1. Record the old deployment's exact image references, routes, and retained volume names. Do not attach an old writer to a new source volume. 2. Deploy `compose.coolify.provision.yaml`. Require every one-shot provisioner + to complete successfully. +3. Deploy `compose.coolify.signers.yaml`. Require all seven Transit initializers to complete successfully and all seven signers to become healthy. -3. Deploy the authority runtime applications from +4. Deploy the authority runtime applications from `compose.coolify.interior.yaml`, `compose.coolify.social-development.yaml`, `compose.coolify.labour-pensions.yaml`, and `compose.coolify.agriculture.yaml`. Confirm all five Relays and all six Evidence cells are ready on the private routes. -4. Deploy the shared Mint, programme services, portal, Visitor Center, and +5. Deploy the shared Mint, programme services, portal, Visitor Center, and static metadata from `compose.coolify.yaml`. -5. Run the hosted programme, denial, JWKS, source-label, redaction, and UI smoke +6. Run the hosted programme, denial, JWKS, source-label, redaction, and UI smoke against the new routes before changing public routing. -6. If required for this deployment, add `compose.coolify.esignet.yaml` and run +7. If required for this deployment, add `compose.coolify.esignet.yaml` and run the citizen-login smoke through the NIA Relay V2 lookup. -7. Switch programme authority URLs and public metadata routing only after every +8. Switch programme authority URLs and public metadata routing only after every required smoke passes. Disable the superseded services, but retain their volumes and exact deployment references for recovery. diff --git a/scripts/check-coolify-compose.sh b/scripts/check-coolify-compose.sh index 5cc4cda..507b2ff 100755 --- a/scripts/check-coolify-compose.sh +++ b/scripts/check-coolify-compose.sh @@ -4,6 +4,10 @@ set -eu root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) test_digest=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa test_image="ghcr.io/registrystack/solmara-test@sha256:$test_digest" +test_private_jwk='{"kty":"EC","crv":"P-384","x":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","y":"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","d":"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC","kid":"solmara-test","alg":"ES384"}' +test_public_jwk='{"kty":"EC","crv":"P-384","x":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","y":"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","kid":"solmara-test","alg":"ES384"}' +scratch=$(mktemp -d) +trap 'rm -rf -- "$scratch"' EXIT HUP INT TERM check_compose() { REGISTRY_RELAY_IMAGE="ghcr.io/registrystack/relay@sha256:$test_digest" \ @@ -26,29 +30,35 @@ check_compose() { REGISTRY_ESIGNET_PSUT_SECRET=test \ REGISTRY_ESIGNET_KYC_KEYSTORE_PASSWORD=test \ PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64=test \ - NIA_ESIGNET_CLIENT_PRIVATE_JWK='{"kty":"RSA"}' \ - SOLMARA_DEMO_CLIENT_PRIVATE_JWK='{"kty":"EC"}' \ + NIA_ESIGNET_CLIENT_PRIVATE_JWK="$test_private_jwk" \ + SOLMARA_DEMO_CLIENT_PRIVATE_JWK="$test_private_jwk" \ CHILD_BENEFIT_FEDERATOR_TOKEN=test \ CRA_RELAY_AUDIT_KEY=test NIA_RELAY_AUDIT_KEY=test MOSD_RELAY_AUDIT_KEY=test \ SIPF_RELAY_AUDIT_KEY=test SIPF_RELAY_CURSOR_KEY=test \ NAGDI_RELAY_AUDIT_KEY=test NAGDI_RELAY_CURSOR_KEY=test \ - MINT_SIGNING_JWK=test MINT_SIGNING_PUBLIC_JWK=test MINT_AUDIT_HMAC_KEY=test \ - CRA_EVIDENCE_SIGNING_JWK=test CRA_EVIDENCE_SIGNING_PUBLIC_JWK=test CRA_EVIDENCE_AUDIT_HMAC_KEY=test CRA_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ - NIA_EVIDENCE_SIGNING_JWK=test NIA_EVIDENCE_SIGNING_PUBLIC_JWK=test NIA_EVIDENCE_AUDIT_HMAC_KEY=test NIA_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ - SRO_EVIDENCE_SIGNING_JWK=test SRO_EVIDENCE_SIGNING_PUBLIC_JWK=test SRO_EVIDENCE_AUDIT_HMAC_KEY=test SRO_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ - MOSD_EVIDENCE_SIGNING_JWK=test MOSD_EVIDENCE_SIGNING_PUBLIC_JWK=test MOSD_EVIDENCE_AUDIT_HMAC_KEY=test MOSD_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ - SIPF_EVIDENCE_SIGNING_JWK=test SIPF_EVIDENCE_SIGNING_PUBLIC_JWK=test SIPF_EVIDENCE_AUDIT_HMAC_KEY=test SIPF_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ - NAGDI_EVIDENCE_SIGNING_JWK=test NAGDI_EVIDENCE_SIGNING_PUBLIC_JWK=test NAGDI_EVIDENCE_AUDIT_HMAC_KEY=test NAGDI_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ - CRA_PENSION_EVIDENCE_CLIENT_PRIVATE_JWK=test CRA_CITIZEN_EVIDENCE_CLIENT_PRIVATE_JWK=test \ - MOSD_CHILD_BENEFIT_EVIDENCE_CLIENT_PRIVATE_JWK=test \ - SIPF_PENSION_EVIDENCE_CLIENT_PRIVATE_JWK=test SIPF_SURVIVOR_EVIDENCE_CLIENT_PRIVATE_JWK=test \ - NAGDI_VOUCHER_EVIDENCE_CLIENT_PRIVATE_JWK=test NAGDI_LIVESTOCK_EVIDENCE_CLIENT_PRIVATE_JWK=test \ - CRA_PENSION_EVIDENCE_CLIENT_PUBLIC_JWK=test CRA_CITIZEN_EVIDENCE_CLIENT_PUBLIC_JWK=test \ - MOSD_CHILD_BENEFIT_EVIDENCE_CLIENT_PUBLIC_JWK=test \ - SIPF_PENSION_EVIDENCE_CLIENT_PUBLIC_JWK=test SIPF_SURVIVOR_EVIDENCE_CLIENT_PUBLIC_JWK=test \ - NAGDI_VOUCHER_EVIDENCE_CLIENT_PUBLIC_JWK=test NAGDI_LIVESTOCK_EVIDENCE_CLIENT_PUBLIC_JWK=test \ - NIA_ESIGNET_CLIENT_PUBLIC_JWK=test SOLMARA_DEMO_CLIENT_PUBLIC_JWK=test \ - docker compose "$@" config >/dev/null + MINT_SIGNING_JWK="$test_private_jwk" MINT_SIGNING_PUBLIC_JWK="$test_public_jwk" MINT_AUDIT_HMAC_KEY=test \ + CRA_EVIDENCE_SIGNING_JWK="$test_private_jwk" CRA_EVIDENCE_SIGNING_PUBLIC_JWK="$test_public_jwk" CRA_EVIDENCE_AUDIT_HMAC_KEY=test CRA_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + NIA_EVIDENCE_SIGNING_JWK="$test_private_jwk" NIA_EVIDENCE_SIGNING_PUBLIC_JWK="$test_public_jwk" NIA_EVIDENCE_AUDIT_HMAC_KEY=test NIA_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + SRO_EVIDENCE_SIGNING_JWK="$test_private_jwk" SRO_EVIDENCE_SIGNING_PUBLIC_JWK="$test_public_jwk" SRO_EVIDENCE_AUDIT_HMAC_KEY=test SRO_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + MOSD_EVIDENCE_SIGNING_JWK="$test_private_jwk" MOSD_EVIDENCE_SIGNING_PUBLIC_JWK="$test_public_jwk" MOSD_EVIDENCE_AUDIT_HMAC_KEY=test MOSD_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + SIPF_EVIDENCE_SIGNING_JWK="$test_private_jwk" SIPF_EVIDENCE_SIGNING_PUBLIC_JWK="$test_public_jwk" SIPF_EVIDENCE_AUDIT_HMAC_KEY=test SIPF_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + NAGDI_EVIDENCE_SIGNING_JWK="$test_private_jwk" NAGDI_EVIDENCE_SIGNING_PUBLIC_JWK="$test_public_jwk" NAGDI_EVIDENCE_AUDIT_HMAC_KEY=test NAGDI_EVIDENCE_SUBJECT_BINDING_HMAC_KEY=test \ + CRA_PENSION_EVIDENCE_CLIENT_PRIVATE_JWK="$test_private_jwk" CRA_CITIZEN_EVIDENCE_CLIENT_PRIVATE_JWK="$test_private_jwk" \ + MOSD_CHILD_BENEFIT_EVIDENCE_CLIENT_PRIVATE_JWK="$test_private_jwk" \ + SIPF_PENSION_EVIDENCE_CLIENT_PRIVATE_JWK="$test_private_jwk" SIPF_SURVIVOR_EVIDENCE_CLIENT_PRIVATE_JWK="$test_private_jwk" \ + NAGDI_VOUCHER_EVIDENCE_CLIENT_PRIVATE_JWK="$test_private_jwk" NAGDI_LIVESTOCK_EVIDENCE_CLIENT_PRIVATE_JWK="$test_private_jwk" \ + CRA_PENSION_EVIDENCE_CLIENT_PUBLIC_JWK="$test_public_jwk" CRA_CITIZEN_EVIDENCE_CLIENT_PUBLIC_JWK="$test_public_jwk" \ + MOSD_CHILD_BENEFIT_EVIDENCE_CLIENT_PUBLIC_JWK="$test_public_jwk" \ + SIPF_PENSION_EVIDENCE_CLIENT_PUBLIC_JWK="$test_public_jwk" SIPF_SURVIVOR_EVIDENCE_CLIENT_PUBLIC_JWK="$test_public_jwk" \ + NAGDI_VOUCHER_EVIDENCE_CLIENT_PUBLIC_JWK="$test_public_jwk" NAGDI_LIVESTOCK_EVIDENCE_CLIENT_PUBLIC_JWK="$test_public_jwk" \ + NIA_ESIGNET_CLIENT_PUBLIC_JWK="$test_public_jwk" SOLMARA_DEMO_CLIENT_PUBLIC_JWK="$test_public_jwk" \ + docker compose "$@" config >"$scratch/rendered.yaml" + + rendered_bytes=$(wc -c <"$scratch/rendered.yaml" | tr -d ' ') + if [ "$rendered_bytes" -ge 65536 ]; then + echo "rendered Coolify Compose exceeds 65,535 bytes: $2 ($rendered_bytes bytes)" >&2 + return 1 + fi } for compose in "$root"/compose.coolify*.yaml; do diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index 8dfc23a..77dbfe8 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -1,5 +1,7 @@ from __future__ import annotations +import json +import re import unittest from pathlib import Path @@ -8,6 +10,7 @@ ROOT = Path(__file__).resolve().parents[1] PROVISION = ROOT / "compose.coolify.provision.yaml" +SIGNERS = ROOT / "compose.coolify.signers.yaml" RUNTIME_COMPOSES = ( ROOT / "compose.coolify.yaml", ROOT / "compose.coolify.interior.yaml", @@ -17,6 +20,25 @@ ) PROVIDERS = ("mint", "cra", "nia", "sro", "mosd", "sipf", "nagdi") RELAYS = ("cra", "nia", "mosd", "sipf", "nagdi") +INTERPOLATION = re.compile(r"^\$\{([A-Z][A-Z0-9_]*):\?[^}]+\}$") +FIXTURE_IMAGE = ( + "ghcr.io/registrystack/solmara-test@sha256:" + + "a" * 64 +) +FIXTURE_PRIVATE_JWK_MEMBERS = { + "kty": "EC", + "crv": "P-384", + "x": "A" * 64, + "y": "B" * 64, + "d": "C" * 64, + "kid": "solmara-test", + "alg": "ES384", +} +FIXTURE_PRIVATE_JWK = json.dumps(FIXTURE_PRIVATE_JWK_MEMBERS, separators=(",", ":")) +FIXTURE_PUBLIC_JWK = json.dumps( + {key: value for key, value in FIXTURE_PRIVATE_JWK_MEMBERS.items() if key != "d"}, + separators=(",", ":"), +) MINT_ORIGIN = "https://mint-authority-cells.solmara.registrystack.org" RELAY_ORIGINS = { "cra": "https://cra-relay-authority-cells.solmara.registrystack.org", @@ -26,10 +48,31 @@ } +def fixture_render(value): + if isinstance(value, dict): + return {key: fixture_render(item) for key, item in value.items()} + if isinstance(value, list): + return [fixture_render(item) for item in value] + if not isinstance(value, str): + return value + match = INTERPOLATION.fullmatch(value) + if match is None: + return value + variable = match.group(1) + if variable.endswith("_IMAGE"): + return FIXTURE_IMAGE + if variable.endswith("_PUBLIC_JWK"): + return FIXTURE_PUBLIC_JWK + if variable.endswith("_PRIVATE_JWK") or variable.endswith("_SIGNING_JWK"): + return FIXTURE_PRIVATE_JWK + return "fixture-value" + + class HostedProvisioningTopologyTests(unittest.TestCase): @classmethod def setUpClass(cls) -> None: cls.provision = yaml.safe_load(PROVISION.read_text(encoding="utf-8")) + cls.signers = yaml.safe_load(SIGNERS.read_text(encoding="utf-8")) cls.runtime = { path.name: yaml.safe_load(path.read_text(encoding="utf-8")) for path in RUNTIME_COMPOSES @@ -48,6 +91,18 @@ def test_provisioning_application_owns_exactly_34_active_volumes(self) -> None: ) ) + def test_provisioning_application_contains_only_target_provisioners(self) -> None: + services = self.provision["services"] + self.assertEqual(len(services), 12) + self.assertTrue(all(name.endswith("-provisioner") for name in services)) + self.assertNotIn("SOLMARA_TRANSIT_SIGNER_IMAGE", PROVISION.read_text()) + self.assertNotIn("--private-jwk", PROVISION.read_text()) + private_signing_secrets = { + "mint-signing-jwk", + *(f"{provider}-evidence-signing-jwk" for provider in PROVIDERS[1:]), + } + self.assertTrue(private_signing_secrets.isdisjoint(self.provision["secrets"])) + def test_runtime_applications_attach_active_volumes_read_only_by_fixed_name( self, ) -> None: @@ -67,7 +122,7 @@ def test_runtime_applications_attach_active_volumes_read_only_by_fixed_name( self.assertEqual(attached, provisioned) def test_private_signing_keys_are_mounted_only_into_matching_signers(self) -> None: - services = self.provision["services"] + services = self.signers["services"] for provider in PROVIDERS: secret = ( "mint-signing-jwk" @@ -80,17 +135,18 @@ def test_private_signing_keys_are_mounted_only_into_matching_signers(self) -> No if any(item["source"] == secret for item in service.get("secrets", [])) } self.assertEqual(consumers, {f"{provider}-signer"}) - for name, service in services.items(): + for name, service in self.provision["services"].items(): if name.endswith("provisioner"): targets = {item["target"] for item in service.get("secrets", [])} self.assertNotIn("solmara-provisioning/signing.jwk", targets) def test_each_signer_requires_its_matching_public_projection(self) -> None: - services = self.provision["services"] + services = self.signers["services"] for provider in PROVIDERS: prefix = "mint" if provider == "mint" else f"{provider}-evidence" signer = services[f"{provider}-signer"] secrets = {item["target"]: item["source"] for item in signer["secrets"]} + self.assertEqual(len(secrets), 2) self.assertEqual(secrets["signing.jwk"], f"{prefix}-signing-jwk") self.assertEqual( secrets["signing-public.jwk"], f"{prefix}-signing-public-jwk" @@ -181,7 +237,7 @@ def test_provisioners_receive_closed_permanent_dependency_origins(self) -> None: def test_each_signer_and_transit_initializer_mount_only_its_matching_volume( self, ) -> None: - services = self.provision["services"] + services = self.signers["services"] for provider in PROVIDERS: volume = ( "mint-transit" if provider == "mint" else f"{provider}-evidence-transit" @@ -200,6 +256,75 @@ def test_each_signer_and_transit_initializer_mount_only_its_matching_volume( }, ) + def test_signer_application_has_exact_services_secrets_and_external_volumes( + self, + ) -> None: + expected_services = { + *(f"{provider}-transit-init" for provider in PROVIDERS), + *(f"{provider}-signer" for provider in PROVIDERS), + } + self.assertEqual(set(self.signers["services"]), expected_services) + + expected_secrets: set[str] = set() + expected_volumes: set[str] = set() + for provider in PROVIDERS: + prefix = "mint" if provider == "mint" else f"{provider}-evidence" + expected_secrets.update( + {f"{prefix}-signing-jwk", f"{prefix}-signing-public-jwk"} + ) + expected_volumes.add(f"{prefix}-transit") + self.assertEqual(set(self.signers["secrets"]), expected_secrets) + self.assertEqual(set(self.signers["volumes"]), expected_volumes) + self.assertTrue( + all( + value == { + "external": True, + "name": f"solmara-authority-cells-{key}", + } + for key, value in self.signers["volumes"].items() + ) + ) + + self.assertEqual( + {service["image"] for service in self.signers["services"].values()}, + { + "${SOLMARA_TRANSIT_SIGNER_IMAGE:?set the digest-pinned Solmara Transit signer image}" + }, + ) + + def test_rendered_operator_applications_fit_coolify_payload_limit(self) -> None: + for path, compose in ( + (PROVISION, self.provision), + (SIGNERS, self.signers), + ): + with self.subTest(compose=path.name): + rendered = yaml.safe_dump( + fixture_render(compose), sort_keys=False + ).encode("utf-8") + self.assertLess(len(rendered), 65_536) + + def test_signer_application_preserves_process_confinement(self) -> None: + services = self.signers["services"] + for provider in PROVIDERS: + initializer = services[f"{provider}-transit-init"] + self.assertEqual(initializer["user"], "0:0") + self.assertEqual(initializer["network_mode"], "none") + self.assertTrue(initializer["read_only"]) + self.assertEqual(initializer["cap_drop"], ["ALL"]) + self.assertEqual(set(initializer["cap_add"]), {"CHOWN", "FOWNER"}) + self.assertEqual( + initializer["security_opt"], ["no-new-privileges:true"] + ) + + signer = services[f"{provider}-signer"] + self.assertEqual(signer["user"], "65532:65532") + self.assertEqual(signer["network_mode"], "none") + self.assertTrue(signer["read_only"]) + self.assertEqual(signer["cap_drop"], ["ALL"]) + self.assertNotIn("cap_add", signer) + self.assertEqual(signer["security_opt"], ["no-new-privileges:true"]) + self.assertEqual(signer["healthcheck"]["retries"], 30) + def test_relay_runtime_secrets_are_authority_scoped(self) -> None: services = {} for compose in self.runtime.values(): From 092037d7111a70d04b0199aaba3f6dfb60d9028a Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 08:23:12 +0700 Subject: [PATCH 39/53] fix(hosted): inject secrets through tmpfs Signed-off-by: Jeremi Joslin --- compose.coolify.provision.yaml | 23 ++++++++++++++------ compose.coolify.signers.yaml | 8 +++++-- docs/hosted-deployment.md | 8 +++++++ scripts/test_hosted_provisioning_topology.py | 10 +++++++-- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml index ef57614..5f70a24 100644 --- a/compose.coolify.provision.yaml +++ b/compose.coolify.provision.yaml @@ -17,6 +17,15 @@ x-provisioner: &provisioner cap_add: [CHOWN, DAC_OVERRIDE, FOWNER] security_opt: [no-new-privileges:true] +# Docker Compose injects environment-backed secrets after creating a +# container, which it refuses to do through a read-only root filesystem. Keep +# the injected files in tmpfs and relax only the one-shot targets that consume +# bootstrap secrets. Relay provisioners remain fully read-only. +x-secret-provisioner: &secret-provisioner + <<: *provisioner + read_only: false + tmpfs: [/tmp, /run/secrets] + services: cra-relay-provisioner: <<: *provisioner @@ -44,7 +53,7 @@ services: volumes: [nagdi-relay-runtime:/provisioned/runtime, nagdi-relay-source:/provisioned/source] cra-evidence-provisioner: - <<: *provisioner + <<: *secret-provisioner command: [provision, --target, cra-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.21, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://cra-relay-authority-cells.solmara.registrystack.org] volumes: [cra-evidence-runtime:/provisioned/runtime, cra-evidence-secrets:/provisioned/secrets, cra-evidence-extracts:/provisioned/extracts] secrets: @@ -55,7 +64,7 @@ services: - {source: cra-citizen-evidence-client-key, target: solmara-provisioning/cra-citizen-evidence-client-key} nia-evidence-provisioner: - <<: *provisioner + <<: *secret-provisioner command: [provision, --target, nia-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.22, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [nia-evidence-runtime:/provisioned/runtime, nia-evidence-secrets:/provisioned/secrets, nia-evidence-extracts:/provisioned/extracts] secrets: @@ -64,7 +73,7 @@ services: - {source: nia-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} sro-evidence-provisioner: - <<: *provisioner + <<: *secret-provisioner command: [provision, --target, sro-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.3.23, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [sro-evidence-runtime:/provisioned/runtime, sro-evidence-secrets:/provisioned/secrets, sro-evidence-extracts:/provisioned/extracts] secrets: @@ -73,7 +82,7 @@ services: - {source: sro-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} mosd-evidence-provisioner: - <<: *provisioner + <<: *secret-provisioner command: [provision, --target, mosd-programme-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.3.24, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://mosd-programme-relay-authority-cells.solmara.registrystack.org] volumes: [mosd-evidence-runtime:/provisioned/runtime, mosd-evidence-secrets:/provisioned/secrets] secrets: @@ -83,7 +92,7 @@ services: - {source: mosd-child-benefit-evidence-client-key, target: solmara-provisioning/mosd-child-benefit-evidence-client-key} sipf-evidence-provisioner: - <<: *provisioner + <<: *secret-provisioner command: [provision, --target, sipf-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.4.25, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://sipf-relay-authority-cells.solmara.registrystack.org] volumes: [sipf-evidence-runtime:/provisioned/runtime, sipf-evidence-secrets:/provisioned/secrets] secrets: @@ -94,7 +103,7 @@ services: - {source: sipf-survivor-evidence-client-key, target: solmara-provisioning/sipf-survivor-evidence-client-key} nagdi-evidence-provisioner: - <<: *provisioner + <<: *secret-provisioner command: [provision, --target, nagdi-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.5.26, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://nagdi-relay-authority-cells.solmara.registrystack.org] volumes: [nagdi-evidence-runtime:/provisioned/runtime, nagdi-evidence-secrets:/provisioned/secrets] secrets: @@ -105,7 +114,7 @@ services: - {source: nagdi-livestock-evidence-client-key, target: solmara-provisioning/nagdi-livestock-evidence-client-key} mint-provisioner: - <<: *provisioner + <<: *secret-provisioner command: [provision, --target, mint, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.20, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [mint-runtime:/provisioned/runtime, mint-secrets:/provisioned/secrets] secrets: diff --git a/compose.coolify.signers.yaml b/compose.coolify.signers.yaml index 593652b..6a50246 100644 --- a/compose.coolify.signers.yaml +++ b/compose.coolify.signers.yaml @@ -19,8 +19,12 @@ x-signer: &signer image: ${SOLMARA_TRANSIT_SIGNER_IMAGE:?set the digest-pinned Solmara Transit signer image} restart: unless-stopped user: "65532:65532" - read_only: true - tmpfs: [/tmp] + # Docker Compose copies environment-backed secrets after container creation, + # so the secret-consuming signer cannot use a read-only root filesystem. + # The key files live in tmpfs, and the non-root process remains networkless + # with every capability dropped. + read_only: false + tmpfs: [/tmp, /run/secrets] network_mode: none cap_drop: [ALL] security_opt: [no-new-privileges:true] diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index c0128a9..3dd3560 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -71,6 +71,14 @@ external read-only volumes. Each runtime application owns the writable audit volumes for its services and initializes their permissions without reading or replacing existing audit records. +Docker Compose injects an environment-backed secret after creating its target +container and rejects that operation when the root filesystem is read-only. +Relay provisioners and Transit initializers remain read-only. The one-shot +Evidence and Mint provisioners and the non-root signer processes instead mount +`/run/secrets` as tmpfs. They remain networkless with no new privileges; signer +processes also run with every Linux capability dropped. Secret values are not +placed in container environment variables or persistent runtime volumes. + ## Mint clients The shared Mint registers nine clients under the common lab audience. Eight are diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index 77dbfe8..0e1eadd 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -208,12 +208,17 @@ def test_provisioners_only_elevate_for_authority_volume_initialization( continue self.assertEqual(service["user"], "0:0") self.assertEqual(service["network_mode"], "none") - self.assertTrue(service["read_only"]) self.assertEqual(service["cap_drop"], ["ALL"]) self.assertEqual( set(service["cap_add"]), {"CHOWN", "DAC_OVERRIDE", "FOWNER"} ) self.assertEqual(service["security_opt"], ["no-new-privileges:true"]) + if service.get("secrets"): + self.assertFalse(service["read_only"]) + self.assertEqual(set(service["tmpfs"]), {"/tmp", "/run/secrets"}) + else: + self.assertTrue(service["read_only"]) + self.assertEqual(service["tmpfs"], ["/tmp"]) def test_provisioners_receive_closed_permanent_dependency_origins(self) -> None: services = self.provision["services"] @@ -319,7 +324,8 @@ def test_signer_application_preserves_process_confinement(self) -> None: signer = services[f"{provider}-signer"] self.assertEqual(signer["user"], "65532:65532") self.assertEqual(signer["network_mode"], "none") - self.assertTrue(signer["read_only"]) + self.assertFalse(signer["read_only"]) + self.assertEqual(set(signer["tmpfs"]), {"/tmp", "/run/secrets"}) self.assertEqual(signer["cap_drop"], ["ALL"]) self.assertNotIn("cap_add", signer) self.assertEqual(signer["security_opt"], ["no-new-privileges:true"]) From 2ac5afcfc86eab3fe2a9aa4dfabf0f6f09576d88 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 08:26:32 +0700 Subject: [PATCH 40/53] docs(hosted): clarify runtime secret storage Signed-off-by: Jeremi Joslin --- docs/hosted-deployment.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index 3dd3560..9fd0e0f 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -77,7 +77,9 @@ Relay provisioners and Transit initializers remain read-only. The one-shot Evidence and Mint provisioners and the non-root signer processes instead mount `/run/secrets` as tmpfs. They remain networkless with no new privileges; signer processes also run with every Linux capability dropped. Secret values are not -placed in container environment variables or persistent runtime volumes. +placed in container environment variables. The provisioner copies only the +required non-signing runtime secrets into each authority's isolated secret +volume; private issuer signing keys remain confined to signer tmpfs. ## Mint clients From c7872989ce91f0a90cf9eb75982be8e2a04007fb Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 08:42:45 +0700 Subject: [PATCH 41/53] docs(hosted): start Mint before Relays Signed-off-by: Jeremi Joslin --- docs/hosted-deployment.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index 9fd0e0f..b42fbe5 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -158,14 +158,16 @@ Deploy the reset alongside the existing deployment in this order: to complete successfully. 3. Deploy `compose.coolify.signers.yaml`. Require all seven Transit initializers to complete successfully and all seven signers to become healthy. -4. Deploy the authority runtime applications from +4. Deploy the shared Mint, programme services, portal, Visitor Center, and + static metadata from `compose.coolify.yaml`. Require Mint health, discovery, + and JWKS to pass before starting a Relay, because every Relay validates the + permanent Mint issuer during startup. Do not send programme requests yet. +5. Deploy the authority runtime applications from `compose.coolify.interior.yaml`, `compose.coolify.social-development.yaml`, `compose.coolify.labour-pensions.yaml`, and `compose.coolify.agriculture.yaml`. Confirm all five Relays and all six Evidence cells are ready on the private routes. -5. Deploy the shared Mint, programme services, portal, Visitor Center, and - static metadata from `compose.coolify.yaml`. 6. Run the hosted programme, denial, JWKS, source-label, redaction, and UI smoke against the new routes before changing public routing. 7. If required for this deployment, add `compose.coolify.esignet.yaml` and run From 2b7f7b41586f3cff8746b9ada6e3ad093f878585 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 08:48:26 +0700 Subject: [PATCH 42/53] fix(hosted): gate rollout on signer and Mint readiness Signed-off-by: Jeremi Joslin --- compose.coolify.signers.yaml | 20 +++++++++++++++ compose.coolify.yaml | 18 ++++++++++++++ scripts/test_hosted_provisioning_topology.py | 26 ++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/compose.coolify.signers.yaml b/compose.coolify.signers.yaml index 6a50246..b57f97d 100644 --- a/compose.coolify.signers.yaml +++ b/compose.coolify.signers.yaml @@ -108,6 +108,26 @@ services: depends_on: nagdi-transit-init: {condition: service_completed_successfully} + signers-ready: + image: ${SOLMARA_TRANSIT_SIGNER_IMAGE:?set the digest-pinned Solmara Transit signer image} + entrypoint: [/usr/local/bin/python, -c] + command: ["print('authority signers ready')"] + restart: "no" + user: "65532:65532" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + depends_on: + mint-signer: {condition: service_healthy} + cra-signer: {condition: service_healthy} + nia-signer: {condition: service_healthy} + sro-signer: {condition: service_healthy} + mosd-signer: {condition: service_healthy} + sipf-signer: {condition: service_healthy} + nagdi-signer: {condition: service_healthy} + secrets: mint-signing-jwk: {environment: MINT_SIGNING_JWK} mint-signing-public-jwk: {environment: MINT_SIGNING_PUBLIC_JWK} diff --git a/compose.coolify.yaml b/compose.coolify.yaml index b64986a..80a97f3 100644 --- a/compose.coolify.yaml +++ b/compose.coolify.yaml @@ -37,6 +37,24 @@ services: depends_on: audit-permissions: {condition: service_completed_successfully} labels: {solmara.lab.host: mint-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells} + mint-readiness: + image: ${SOLMARA_AUTHORITY_PROVISIONER_IMAGE:?set the digest-pinned Solmara authority provisioner image} + entrypoint: [python, -c] + command: + - >- + import sys,time,urllib.request; + ok=False; + exec("for _ in range(60):\n try:\n with urllib.request.urlopen('http://172.29.1.20:8081/health', timeout=1) as response:\n if response.status == 200:\n ok=True; break\n except Exception:\n pass\n time.sleep(1)"); + sys.exit(0 if ok else 'mint readiness failed') + restart: "no" + user: "65532:65532" + read_only: true + tmpfs: [/tmp] + networks: [runtime] + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + depends_on: + mint: {condition: service_started} static-metadata: image: ${SOLMARA_STATIC_METADATA_IMAGE:?set a digest-pinned Solmara static metadata image} restart: unless-stopped diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index 0e1eadd..c6922f6 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -267,6 +267,7 @@ def test_signer_application_has_exact_services_secrets_and_external_volumes( expected_services = { *(f"{provider}-transit-init" for provider in PROVIDERS), *(f"{provider}-signer" for provider in PROVIDERS), + "signers-ready", } self.assertEqual(set(self.signers["services"]), expected_services) @@ -290,6 +291,18 @@ def test_signer_application_has_exact_services_secrets_and_external_volumes( ) ) + readiness = self.signers["services"]["signers-ready"] + self.assertEqual( + readiness["depends_on"], + { + f"{provider}-signer": {"condition": "service_healthy"} + for provider in PROVIDERS + }, + ) + self.assertEqual(readiness["network_mode"], "none") + self.assertTrue(readiness["read_only"]) + self.assertEqual(readiness["cap_drop"], ["ALL"]) + self.assertEqual( {service["image"] for service in self.signers["services"].values()}, { @@ -355,6 +368,7 @@ def test_runtime_consumers_depend_on_app_local_audit_init(self) -> None: for name, service in services.items(): if name in { "audit-permissions", + "mint-readiness", "static-metadata", "scenario-runner", "child-benefit-federator", @@ -368,6 +382,18 @@ def test_runtime_consumers_depend_on_app_local_audit_init(self) -> None: name, ) + def test_core_deployment_waits_for_private_mint_health(self) -> None: + core = self.runtime["compose.coolify.yaml"]["services"] + readiness = core["mint-readiness"] + self.assertEqual( + readiness["depends_on"], {"mint": {"condition": "service_started"}} + ) + self.assertEqual(readiness["networks"], ["runtime"]) + self.assertEqual(readiness["cap_drop"], ["ALL"]) + self.assertTrue(readiness["read_only"]) + self.assertNotIn("secrets", readiness) + self.assertIn("172.29.1.20:8081/health", readiness["command"][0]) + if __name__ == "__main__": unittest.main() From aa364b636806e89ebc3a5213c85df45083a32e53 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 08:52:48 +0700 Subject: [PATCH 43/53] fix(hosted): make Mint readiness deployment-blocking Signed-off-by: Jeremi Joslin --- compose.coolify.yaml | 2 ++ scripts/test_hosted_provisioning_topology.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/compose.coolify.yaml b/compose.coolify.yaml index 80a97f3..4ed850d 100644 --- a/compose.coolify.yaml +++ b/compose.coolify.yaml @@ -70,6 +70,8 @@ services: secrets: - source: solmara-demo-client-key target: solmara-evidence-client.jwk + depends_on: + mint-readiness: {condition: service_completed_successfully} labels: {solmara.lab.host: "${SOLMARA_SCENARIO_RUNNER_PUBLIC_HOST:-scenarios.solmara.registrystack.org}"} child-benefit-federator: image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set a digest-pinned Solmara scenario runner image} diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index c6922f6..14a351a 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -393,6 +393,10 @@ def test_core_deployment_waits_for_private_mint_health(self) -> None: self.assertTrue(readiness["read_only"]) self.assertNotIn("secrets", readiness) self.assertIn("172.29.1.20:8081/health", readiness["command"][0]) + self.assertEqual( + core["scenario-runner"]["depends_on"], + {"mint-readiness": {"condition": "service_completed_successfully"}}, + ) if __name__ == "__main__": From 108b161003a025ba7937129dc9cc9124f8da1b14 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 09:34:43 +0700 Subject: [PATCH 44/53] fix(hosted): consume bootstrap secrets before runtime Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 126 +++++------ compose.coolify.provision.yaml | 131 +++++++----- compose.coolify.signers.yaml | 62 ++++-- scripts/hosted-transit-signer.py | 207 ++++++++++++++----- scripts/local-transit-proxy.py | 189 ++++++++++++----- scripts/provision-hosted-runtime.py | 169 +++++++++++++-- scripts/test_hosted_image_manifest.py | 21 +- scripts/test_hosted_provisioning_topology.py | 125 +++++++++-- scripts/test_hosted_transit_signer.py | 72 ++++++- scripts/test_local_transit_proxy.py | 58 +++++- scripts/test_provision_hosted_runtime.py | 92 ++++++++- 11 files changed, 965 insertions(+), 287 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 27001cc..8d2091c 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -258,49 +258,73 @@ jobs: set -euo pipefail image="$SOLMARA_IMAGE_REGISTRY/solmara-lab-transit-signer@${{ steps.transit_signer.outputs.digest }}" docker run --rm --network none --read-only --entrypoint python "$image" -c 'import cryptography' - state="$RUNNER_TEMP/solmara-transit-signer-state" - install -d -m 0700 "$state/secrets" - sudo chown 65532:65532 "$state/secrets" - sudo install -o 65532 -g 65532 -m 0400 \ - config/evidence/local/cells/cra/secrets/signing.jwk \ - "$state/secrets/signing.jwk" - sudo install -o 65532 -g 65532 -m 0400 \ - runtime/evidence-cells/cells/cra/bundle/public-keys/*.jwk.json \ - "$state/secrets/signing-public.jwk" - name="solmara-transit-signer-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - volume="solmara-transit-signer-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - docker volume create "$volume" >/dev/null + export SIGNER_IMAGE="$image" + export SIGNING_PRIVATE_JWK + SIGNING_PRIVATE_JWK="$(cat config/evidence/local/cells/cra/secrets/signing.jwk)" + export SIGNING_PUBLIC_JWK + SIGNING_PUBLIC_JWK="$(cat runtime/evidence-cells/cells/cra/bundle/public-keys/*.jwk.json)" + project="solmara-transit-signer-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + compose="$RUNNER_TEMP/solmara-transit-signer.compose.yaml" + cat >"$compose" <<'YAML' + services: + transit-init: + image: ${SIGNER_IMAGE:?required} + entrypoint: [/usr/local/bin/python, -c] + command: ["import os; os.chown('/transit', 65532, 65532); os.chmod('/transit', 0o700)"] + restart: "no" + user: "0:0" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + cap_add: [CHOWN, FOWNER] + security_opt: [no-new-privileges:true] + volumes: [transit:/transit] + signer: + image: ${SIGNER_IMAGE:?required} + command: [--private-jwk, /tmp/solmara-signing.jwk, --public-jwk, /tmp/solmara-signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-cra] + restart: "no" + user: "65532:65532" + read_only: false + network_mode: none + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + volumes: [transit:/transit] + secrets: + - {source: signing-private, target: /tmp/solmara-signing.jwk, uid: "65532", gid: "65532", mode: 0400} + - {source: signing-public, target: /tmp/solmara-signing-public.jwk, uid: "65532", gid: "65532", mode: 0400} + healthcheck: + test: [CMD, /usr/local/bin/python, -c, "import socket; s=socket.socket(socket.AF_UNIX); s.connect('/transit/transit-proxy.sock'); s.close()"] + interval: 1s + timeout: 1s + retries: 30 + start_period: 1s + depends_on: + transit-init: {condition: service_completed_successfully} + secrets: + signing-private: {environment: SIGNING_PRIVATE_JWK} + signing-public: {environment: SIGNING_PUBLIC_JWK} + volumes: + transit: {} + YAML + chmod 0600 "$compose" cleanup() { - docker rm --force "$name" >/dev/null 2>&1 || true - docker volume rm --force "$volume" >/dev/null 2>&1 || true + docker compose -p "$project" -f "$compose" down -v >/dev/null 2>&1 || true + unset SIGNING_PRIVATE_JWK SIGNING_PUBLIC_JWK } trap cleanup EXIT - docker run --rm --network none --read-only --tmpfs /tmp \ - --user 0:0 --cap-drop ALL --cap-add CHOWN --entrypoint python \ - --mount "type=volume,source=$volume,target=/transit" \ - "$image" -c \ - 'import os; os.chmod("/transit", 0o700); os.chown("/transit", 65532, 65532)' - docker run --detach --name "$name" --network none --read-only --tmpfs /tmp \ - --mount "type=bind,source=$state/secrets/signing.jwk,target=/run/secrets/signing.jwk,readonly" \ - --mount "type=bind,source=$state/secrets/signing-public.jwk,target=/run/secrets/signing-public.jwk,readonly" \ - --mount "type=volume,source=$volume,target=/transit" \ - "$image" \ - --private-jwk /run/secrets/signing.jwk \ - --public-jwk /run/secrets/signing-public.jwk \ - --socket /transit/transit-proxy.sock \ - --key-name solmara-evidence-cra - for _ in $(seq 1 200); do - docker exec "$name" python -c \ - 'from pathlib import Path; raise SystemExit(0 if Path("/transit/transit-proxy.sock").is_socket() else 1)' \ - && break - sleep 0.1 - done - docker exec "$name" python -c \ - 'from pathlib import Path; raise SystemExit(0 if Path("/transit/transit-proxy.sock").is_socket() else 1)' - docker run --rm --network none --read-only --tmpfs /tmp \ - --user 65532:65532 --entrypoint python \ - --mount "type=volume,source=$volume,target=/transit,readonly" \ - "$image" -c ' + docker compose -p "$project" -f "$compose" up -d --wait --wait-timeout 60 + docker compose -p "$project" -f "$compose" exec -T signer python -c ' + import glob + import os + from pathlib import Path + assert os.geteuid() == 65532 + assert not Path("/tmp/solmara-signing.jwk").exists() + assert not Path("/tmp/solmara-signing-public.jwk").exists() + assert not glob.glob("/tmp/solmara-transit-*") + assert Path("/transit/transit-proxy.sock").is_socket() + ' + docker compose -p "$project" -f "$compose" exec -T signer python -c ' import socket client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) client.settimeout(5) @@ -315,28 +339,6 @@ jobs: client.close() raise SystemExit(0 if response.startswith(b"HTTP/1.1 200 ") else 1) ' - docker rm --force "$name" >/dev/null - - sudo install -o 65532 -g 65532 -m 0400 \ - runtime/evidence-cells/cells/nia/bundle/public-keys/*.jwk.json \ - "$state/secrets/signing-public.jwk" - stdout="$RUNNER_TEMP/solmara-transit-signer.stdout" - stderr="$RUNNER_TEMP/solmara-transit-signer.stderr" - set +e - docker run --rm --network none --read-only --tmpfs /tmp \ - --mount "type=bind,source=$state/secrets/signing.jwk,target=/run/secrets/signing.jwk,readonly" \ - --mount "type=bind,source=$state/secrets/signing-public.jwk,target=/run/secrets/signing-public.jwk,readonly" \ - --mount "type=volume,source=$volume,target=/transit" \ - "$image" \ - --private-jwk /run/secrets/signing.jwk \ - --public-jwk /run/secrets/signing-public.jwk \ - --socket /transit/transit-proxy.sock \ - --key-name solmara-evidence-cra >"$stdout" 2>"$stderr" - status=$? - set -e - test "$status" -eq 1 - test ! -s "$stdout" - test "$(cat "$stderr")" = "hosted Transit signer could not start" cleanup trap - EXIT - name: Build and push static metadata diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml index 5f70a24..fb39727 100644 --- a/compose.coolify.provision.yaml +++ b/compose.coolify.provision.yaml @@ -17,118 +17,143 @@ x-provisioner: &provisioner cap_add: [CHOWN, DAC_OVERRIDE, FOWNER] security_opt: [no-new-privileges:true] -# Docker Compose injects environment-backed secrets after creating a -# container, which it refuses to do through a read-only root filesystem. Keep -# the injected files in tmpfs and relax only the one-shot targets that consume -# bootstrap secrets. Relay provisioners remain fully read-only. +# Docker Compose copies environment-backed secrets before starting a container. +# Keep those inputs on the one-shot container's ephemeral root filesystem so a +# tmpfs mount cannot mask them, and relax only the targets that consume them. +# The provisioner removes the closed input inventory on every exit. Relay +# provisioners remain fully read-only and receive no secret path. x-secret-provisioner: &secret-provisioner <<: *provisioner read_only: false - tmpfs: [/tmp, /run/secrets] + tmpfs: [] services: cra-relay-provisioner: <<: *provisioner - command: [provision, --target, cra-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] + command: [provision, --target, cra-relay, --assets, /opt/solmara-hosted-assets, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [cra-relay-runtime:/provisioned/runtime, cra-relay-source:/provisioned/source] nia-relay-provisioner: <<: *provisioner - command: [provision, --target, nia-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] + command: [provision, --target, nia-relay, --assets, /opt/solmara-hosted-assets, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [nia-relay-runtime:/provisioned/runtime, nia-relay-source:/provisioned/source] mosd-relay-provisioner: <<: *provisioner - command: [provision, --target, mosd-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] + command: [provision, --target, mosd-relay, --assets, /opt/solmara-hosted-assets, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [mosd-relay-runtime:/provisioned/runtime, mosd-relay-source:/provisioned/source] sipf-relay-provisioner: <<: *provisioner - command: [provision, --target, sipf-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] + command: [provision, --target, sipf-relay, --assets, /opt/solmara-hosted-assets, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [sipf-relay-runtime:/provisioned/runtime, sipf-relay-source:/provisioned/source] nagdi-relay-provisioner: <<: *provisioner - command: [provision, --target, nagdi-relay, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] + command: [provision, --target, nagdi-relay, --assets, /opt/solmara-hosted-assets, --runtime-output, /provisioned/runtime, --source-output, /provisioned/source, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [nagdi-relay-runtime:/provisioned/runtime, nagdi-relay-source:/provisioned/source] cra-evidence-provisioner: <<: *secret-provisioner - command: [provision, --target, cra-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.21, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://cra-relay-authority-cells.solmara.registrystack.org] + command: [provision, --target, cra-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /tmp/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.21, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://cra-relay-authority-cells.solmara.registrystack.org] volumes: [cra-evidence-runtime:/provisioned/runtime, cra-evidence-secrets:/provisioned/secrets, cra-evidence-extracts:/provisioned/extracts] secrets: - - {source: cra-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} - - {source: cra-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} - - {source: cra-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} - - {source: cra-pension-evidence-client-key, target: solmara-provisioning/cra-pension-evidence-client-key} - - {source: cra-citizen-evidence-client-key, target: solmara-provisioning/cra-citizen-evidence-client-key} + - {source: cra-evidence-signing-public-jwk, target: /tmp/solmara-provisioning/signing-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: cra-evidence-audit-hmac-key, target: /tmp/solmara-provisioning/audit-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: cra-evidence-subject-binding-hmac-key, target: /tmp/solmara-provisioning/subject-binding-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: cra-pension-evidence-client-key, target: /tmp/solmara-provisioning/cra-pension-evidence-client-key, uid: "0", gid: "0", mode: 0400} + - {source: cra-citizen-evidence-client-key, target: /tmp/solmara-provisioning/cra-citizen-evidence-client-key, uid: "0", gid: "0", mode: 0400} nia-evidence-provisioner: <<: *secret-provisioner - command: [provision, --target, nia-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.22, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] + command: [provision, --target, nia-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /tmp/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.2.22, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [nia-evidence-runtime:/provisioned/runtime, nia-evidence-secrets:/provisioned/secrets, nia-evidence-extracts:/provisioned/extracts] secrets: - - {source: nia-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} - - {source: nia-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} - - {source: nia-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} + - {source: nia-evidence-signing-public-jwk, target: /tmp/solmara-provisioning/signing-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: nia-evidence-audit-hmac-key, target: /tmp/solmara-provisioning/audit-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: nia-evidence-subject-binding-hmac-key, target: /tmp/solmara-provisioning/subject-binding-hmac-key, uid: "0", gid: "0", mode: 0400} sro-evidence-provisioner: <<: *secret-provisioner - command: [provision, --target, sro-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.3.23, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] + command: [provision, --target, sro-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /tmp/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --extract-output, /provisioned/extracts, --bind-host, 172.29.3.23, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [sro-evidence-runtime:/provisioned/runtime, sro-evidence-secrets:/provisioned/secrets, sro-evidence-extracts:/provisioned/extracts] secrets: - - {source: sro-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} - - {source: sro-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} - - {source: sro-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} + - {source: sro-evidence-signing-public-jwk, target: /tmp/solmara-provisioning/signing-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: sro-evidence-audit-hmac-key, target: /tmp/solmara-provisioning/audit-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: sro-evidence-subject-binding-hmac-key, target: /tmp/solmara-provisioning/subject-binding-hmac-key, uid: "0", gid: "0", mode: 0400} mosd-evidence-provisioner: <<: *secret-provisioner - command: [provision, --target, mosd-programme-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.3.24, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://mosd-programme-relay-authority-cells.solmara.registrystack.org] + command: [provision, --target, mosd-programme-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /tmp/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.3.24, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://mosd-programme-relay-authority-cells.solmara.registrystack.org] volumes: [mosd-evidence-runtime:/provisioned/runtime, mosd-evidence-secrets:/provisioned/secrets] secrets: - - {source: mosd-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} - - {source: mosd-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} - - {source: mosd-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} - - {source: mosd-child-benefit-evidence-client-key, target: solmara-provisioning/mosd-child-benefit-evidence-client-key} + - {source: mosd-evidence-signing-public-jwk, target: /tmp/solmara-provisioning/signing-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: mosd-evidence-audit-hmac-key, target: /tmp/solmara-provisioning/audit-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: mosd-evidence-subject-binding-hmac-key, target: /tmp/solmara-provisioning/subject-binding-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: mosd-child-benefit-evidence-client-key, target: /tmp/solmara-provisioning/mosd-child-benefit-evidence-client-key, uid: "0", gid: "0", mode: 0400} sipf-evidence-provisioner: <<: *secret-provisioner - command: [provision, --target, sipf-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.4.25, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://sipf-relay-authority-cells.solmara.registrystack.org] + command: [provision, --target, sipf-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /tmp/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.4.25, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://sipf-relay-authority-cells.solmara.registrystack.org] volumes: [sipf-evidence-runtime:/provisioned/runtime, sipf-evidence-secrets:/provisioned/secrets] secrets: - - {source: sipf-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} - - {source: sipf-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} - - {source: sipf-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} - - {source: sipf-pension-evidence-client-key, target: solmara-provisioning/sipf-pension-evidence-client-key} - - {source: sipf-survivor-evidence-client-key, target: solmara-provisioning/sipf-survivor-evidence-client-key} + - {source: sipf-evidence-signing-public-jwk, target: /tmp/solmara-provisioning/signing-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: sipf-evidence-audit-hmac-key, target: /tmp/solmara-provisioning/audit-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: sipf-evidence-subject-binding-hmac-key, target: /tmp/solmara-provisioning/subject-binding-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: sipf-pension-evidence-client-key, target: /tmp/solmara-provisioning/sipf-pension-evidence-client-key, uid: "0", gid: "0", mode: 0400} + - {source: sipf-survivor-evidence-client-key, target: /tmp/solmara-provisioning/sipf-survivor-evidence-client-key, uid: "0", gid: "0", mode: 0400} nagdi-evidence-provisioner: <<: *secret-provisioner - command: [provision, --target, nagdi-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.5.26, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://nagdi-relay-authority-cells.solmara.registrystack.org] + command: [provision, --target, nagdi-evidence, --assets, /opt/solmara-hosted-assets, --secrets, /tmp/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.5.26, --mint-origin, https://mint-authority-cells.solmara.registrystack.org, --relay-origin, https://nagdi-relay-authority-cells.solmara.registrystack.org] volumes: [nagdi-evidence-runtime:/provisioned/runtime, nagdi-evidence-secrets:/provisioned/secrets] secrets: - - {source: nagdi-evidence-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} - - {source: nagdi-evidence-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} - - {source: nagdi-evidence-subject-binding-hmac-key, target: solmara-provisioning/subject-binding-hmac-key} - - {source: nagdi-voucher-evidence-client-key, target: solmara-provisioning/nagdi-voucher-evidence-client-key} - - {source: nagdi-livestock-evidence-client-key, target: solmara-provisioning/nagdi-livestock-evidence-client-key} + - {source: nagdi-evidence-signing-public-jwk, target: /tmp/solmara-provisioning/signing-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: nagdi-evidence-audit-hmac-key, target: /tmp/solmara-provisioning/audit-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: nagdi-evidence-subject-binding-hmac-key, target: /tmp/solmara-provisioning/subject-binding-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: nagdi-voucher-evidence-client-key, target: /tmp/solmara-provisioning/nagdi-voucher-evidence-client-key, uid: "0", gid: "0", mode: 0400} + - {source: nagdi-livestock-evidence-client-key, target: /tmp/solmara-provisioning/nagdi-livestock-evidence-client-key, uid: "0", gid: "0", mode: 0400} mint-provisioner: <<: *secret-provisioner - command: [provision, --target, mint, --assets, /opt/solmara-hosted-assets, --secrets, /run/secrets/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.20, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] + command: [provision, --target, mint, --assets, /opt/solmara-hosted-assets, --secrets, /tmp/solmara-provisioning, --runtime-output, /provisioned/runtime, --secret-output, /provisioned/secrets, --bind-host, 172.29.1.20, --mint-origin, https://mint-authority-cells.solmara.registrystack.org] volumes: [mint-runtime:/provisioned/runtime, mint-secrets:/provisioned/secrets] secrets: - - {source: mint-signing-public-jwk, target: solmara-provisioning/signing-public.jwk} - - {source: mint-audit-hmac-key, target: solmara-provisioning/audit-hmac-key} - - {source: cra-pension-evidence-client-public-jwk, target: solmara-provisioning/cra-pension-evidence-public.jwk} - - {source: cra-citizen-evidence-client-public-jwk, target: solmara-provisioning/cra-citizen-evidence-public.jwk} - - {source: mosd-child-benefit-evidence-client-public-jwk, target: solmara-provisioning/mosd-child-benefit-evidence-public.jwk} - - {source: sipf-pension-evidence-client-public-jwk, target: solmara-provisioning/sipf-pension-evidence-public.jwk} - - {source: sipf-survivor-evidence-client-public-jwk, target: solmara-provisioning/sipf-survivor-evidence-public.jwk} - - {source: nagdi-voucher-evidence-client-public-jwk, target: solmara-provisioning/nagdi-voucher-evidence-public.jwk} - - {source: nagdi-livestock-evidence-client-public-jwk, target: solmara-provisioning/nagdi-livestock-evidence-public.jwk} - - {source: nia-esignet-client-public-jwk, target: solmara-provisioning/nia-esignet-public.jwk} - - {source: solmara-demo-client-public-jwk, target: solmara-provisioning/solmara-demo-client-public.jwk} + - {source: mint-signing-public-jwk, target: /tmp/solmara-provisioning/signing-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: mint-audit-hmac-key, target: /tmp/solmara-provisioning/audit-hmac-key, uid: "0", gid: "0", mode: 0400} + - {source: cra-pension-evidence-client-public-jwk, target: /tmp/solmara-provisioning/cra-pension-evidence-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: cra-citizen-evidence-client-public-jwk, target: /tmp/solmara-provisioning/cra-citizen-evidence-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: mosd-child-benefit-evidence-client-public-jwk, target: /tmp/solmara-provisioning/mosd-child-benefit-evidence-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: sipf-pension-evidence-client-public-jwk, target: /tmp/solmara-provisioning/sipf-pension-evidence-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: sipf-survivor-evidence-client-public-jwk, target: /tmp/solmara-provisioning/sipf-survivor-evidence-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: nagdi-voucher-evidence-client-public-jwk, target: /tmp/solmara-provisioning/nagdi-voucher-evidence-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: nagdi-livestock-evidence-client-public-jwk, target: /tmp/solmara-provisioning/nagdi-livestock-evidence-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: nia-esignet-client-public-jwk, target: /tmp/solmara-provisioning/nia-esignet-public.jwk, uid: "0", gid: "0", mode: 0400} + - {source: solmara-demo-client-public-jwk, target: /tmp/solmara-provisioning/solmara-demo-client-public.jwk, uid: "0", gid: "0", mode: 0400} + + provisioning-ready: + image: ${SOLMARA_AUTHORITY_PROVISIONER_IMAGE:?set the digest-pinned Solmara authority provisioner image} + command: [ready] + restart: "no" + user: "65532:65532" + read_only: true + tmpfs: [/tmp] + network_mode: none + cap_drop: [ALL] + security_opt: [no-new-privileges:true] + depends_on: + cra-relay-provisioner: {condition: service_completed_successfully} + nia-relay-provisioner: {condition: service_completed_successfully} + mosd-relay-provisioner: {condition: service_completed_successfully} + sipf-relay-provisioner: {condition: service_completed_successfully} + nagdi-relay-provisioner: {condition: service_completed_successfully} + cra-evidence-provisioner: {condition: service_completed_successfully} + nia-evidence-provisioner: {condition: service_completed_successfully} + sro-evidence-provisioner: {condition: service_completed_successfully} + mosd-evidence-provisioner: {condition: service_completed_successfully} + sipf-evidence-provisioner: {condition: service_completed_successfully} + nagdi-evidence-provisioner: {condition: service_completed_successfully} + mint-provisioner: {condition: service_completed_successfully} secrets: mint-signing-public-jwk: {environment: MINT_SIGNING_PUBLIC_JWK} diff --git a/compose.coolify.signers.yaml b/compose.coolify.signers.yaml index b57f97d..addc0ac 100644 --- a/compose.coolify.signers.yaml +++ b/compose.coolify.signers.yaml @@ -19,12 +19,10 @@ x-signer: &signer image: ${SOLMARA_TRANSIT_SIGNER_IMAGE:?set the digest-pinned Solmara Transit signer image} restart: unless-stopped user: "65532:65532" - # Docker Compose copies environment-backed secrets after container creation, - # so the secret-consuming signer cannot use a read-only root filesystem. - # The key files live in tmpfs, and the non-root process remains networkless - # with every capability dropped. + # Docker Compose copies environment-backed secrets into the container rootfs + # before start, so this process cannot use a read-only root filesystem. The + # non-root wrapper consumes and unlinks those owner-only inputs before exec. read_only: false - tmpfs: [/tmp, /run/secrets] network_mode: none cap_drop: [ALL] security_opt: [no-new-privileges:true] @@ -35,6 +33,18 @@ x-signer: &signer retries: 30 start_period: 2s +x-private-secret-target: &private-secret-target + target: /tmp/solmara-signing.jwk + uid: "65532" + gid: "65532" + mode: 0400 + +x-public-secret-target: &public-secret-target + target: /tmp/solmara-signing-public.jwk + uid: "65532" + gid: "65532" + mode: 0400 + services: mint-transit-init: <<: *transit-init @@ -60,51 +70,65 @@ services: mint-signer: <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-mint] + command: [--private-jwk, /tmp/solmara-signing.jwk, --public-jwk, /tmp/solmara-signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-mint] volumes: [mint-transit:/transit] - secrets: [{source: mint-signing-jwk, target: signing.jwk}, {source: mint-signing-public-jwk, target: signing-public.jwk}] + secrets: + - {source: mint-signing-jwk, <<: *private-secret-target} + - {source: mint-signing-public-jwk, <<: *public-secret-target} depends_on: mint-transit-init: {condition: service_completed_successfully} cra-signer: <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-cra] + command: [--private-jwk, /tmp/solmara-signing.jwk, --public-jwk, /tmp/solmara-signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-cra] volumes: [cra-evidence-transit:/transit] - secrets: [{source: cra-evidence-signing-jwk, target: signing.jwk}, {source: cra-evidence-signing-public-jwk, target: signing-public.jwk}] + secrets: + - {source: cra-evidence-signing-jwk, <<: *private-secret-target} + - {source: cra-evidence-signing-public-jwk, <<: *public-secret-target} depends_on: cra-transit-init: {condition: service_completed_successfully} nia-signer: <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-nia] + command: [--private-jwk, /tmp/solmara-signing.jwk, --public-jwk, /tmp/solmara-signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-nia] volumes: [nia-evidence-transit:/transit] - secrets: [{source: nia-evidence-signing-jwk, target: signing.jwk}, {source: nia-evidence-signing-public-jwk, target: signing-public.jwk}] + secrets: + - {source: nia-evidence-signing-jwk, <<: *private-secret-target} + - {source: nia-evidence-signing-public-jwk, <<: *public-secret-target} depends_on: nia-transit-init: {condition: service_completed_successfully} sro-signer: <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-sro] + command: [--private-jwk, /tmp/solmara-signing.jwk, --public-jwk, /tmp/solmara-signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-sro] volumes: [sro-evidence-transit:/transit] - secrets: [{source: sro-evidence-signing-jwk, target: signing.jwk}, {source: sro-evidence-signing-public-jwk, target: signing-public.jwk}] + secrets: + - {source: sro-evidence-signing-jwk, <<: *private-secret-target} + - {source: sro-evidence-signing-public-jwk, <<: *public-secret-target} depends_on: sro-transit-init: {condition: service_completed_successfully} mosd-signer: <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-mosd-programme] + command: [--private-jwk, /tmp/solmara-signing.jwk, --public-jwk, /tmp/solmara-signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-mosd-programme] volumes: [mosd-evidence-transit:/transit] - secrets: [{source: mosd-evidence-signing-jwk, target: signing.jwk}, {source: mosd-evidence-signing-public-jwk, target: signing-public.jwk}] + secrets: + - {source: mosd-evidence-signing-jwk, <<: *private-secret-target} + - {source: mosd-evidence-signing-public-jwk, <<: *public-secret-target} depends_on: mosd-transit-init: {condition: service_completed_successfully} sipf-signer: <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-sipf] + command: [--private-jwk, /tmp/solmara-signing.jwk, --public-jwk, /tmp/solmara-signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-sipf] volumes: [sipf-evidence-transit:/transit] - secrets: [{source: sipf-evidence-signing-jwk, target: signing.jwk}, {source: sipf-evidence-signing-public-jwk, target: signing-public.jwk}] + secrets: + - {source: sipf-evidence-signing-jwk, <<: *private-secret-target} + - {source: sipf-evidence-signing-public-jwk, <<: *public-secret-target} depends_on: sipf-transit-init: {condition: service_completed_successfully} nagdi-signer: <<: *signer - command: [--private-jwk, /run/secrets/signing.jwk, --public-jwk, /run/secrets/signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-nagdi] + command: [--private-jwk, /tmp/solmara-signing.jwk, --public-jwk, /tmp/solmara-signing-public.jwk, --socket, /transit/transit-proxy.sock, --key-name, solmara-evidence-nagdi] volumes: [nagdi-evidence-transit:/transit] - secrets: [{source: nagdi-evidence-signing-jwk, target: signing.jwk}, {source: nagdi-evidence-signing-public-jwk, target: signing-public.jwk}] + secrets: + - {source: nagdi-evidence-signing-jwk, <<: *private-secret-target} + - {source: nagdi-evidence-signing-public-jwk, <<: *public-secret-target} depends_on: nagdi-transit-init: {condition: service_completed_successfully} diff --git a/scripts/hosted-transit-signer.py b/scripts/hosted-transit-signer.py index 08f6f87..a5a95cf 100644 --- a/scripts/hosted-transit-signer.py +++ b/scripts/hosted-transit-signer.py @@ -18,8 +18,8 @@ from cryptography.hazmat.primitives.asymmetric import ec MAX_SECRET_BYTES = 16 * 1024 -SECRET_PATH = Path("/run/secrets/signing.jwk") -PUBLIC_PATH = Path("/run/secrets/signing-public.jwk") +SECRET_PATH = Path("/tmp/solmara-signing.jwk") +PUBLIC_PATH = Path("/tmp/solmara-signing-public.jwk") SOCKET_PATH = Path("/transit/transit-proxy.sock") STAGING_ROOT = Path("/tmp") ALLOWED_KEY_NAMES = frozenset( @@ -79,65 +79,97 @@ def _directory_is_confined(metadata: os.stat_result) -> bool: return metadata.st_uid == 0 and metadata.st_mode & stat.S_ISVTX != 0 -def _open_confined_secret(path: Path) -> int: - """Open an absolute secret without following any path component.""" - - if not path.is_absolute() or path.name in {"", ".", ".."}: +def _open_confined_directory(path: Path) -> int: + if not path.is_absolute(): raise SignerError("invalid secret") components = path.parts[1:] - if not components or any(component in {"", ".", ".."} for component in components): + if any(component in {"", ".", ".."} for component in components): raise SignerError("invalid secret") - directory = os.open("/", _directory_flags()) try: - for component in components[:-1]: + root_metadata = os.fstat(directory) + if not _directory_is_confined(root_metadata): + raise SignerError("invalid secret") + for component in components: next_directory = os.open(component, _directory_flags(), dir_fd=directory) os.close(directory) directory = next_directory metadata = os.fstat(directory) if not _directory_is_confined(metadata): raise SignerError("invalid secret") - return os.open(components[-1], _file_flags(), dir_fd=directory) + return directory + except OSError as error: + os.close(directory) + raise SignerError("invalid secret") from error + except SignerError: + os.close(directory) + raise + + +def _open_confined_secret(path: Path) -> int: + """Open an absolute secret without following any path component.""" + + if not path.is_absolute() or path.name in {"", ".", ".."}: + raise SignerError("invalid secret") + directory = _open_confined_directory(path.parent) + try: + return os.open(path.name, _file_flags(), dir_fd=directory) except OSError as error: raise SignerError("invalid secret") from error finally: os.close(directory) +def _secret_metadata_is_confined(metadata: os.stat_result) -> bool: + return ( + stat.S_ISREG(metadata.st_mode) + and metadata.st_uid in {0, os.geteuid()} + and metadata.st_mode & 0o022 == 0 + and metadata.st_nlink == 1 + and 0 < metadata.st_size <= MAX_SECRET_BYTES + ) + + +def _read_secret_descriptor(descriptor: int) -> tuple[bytearray, os.stat_result]: + before = os.fstat(descriptor) + if not _secret_metadata_is_confined(before): + raise SignerError("invalid secret") + + value = bytearray() + while len(value) <= MAX_SECRET_BYTES: + chunk = os.read(descriptor, min(4096, MAX_SECRET_BYTES + 1 - len(value))) + if not chunk: + break + value.extend(chunk) + after = os.fstat(descriptor) + if ( + len(value) > MAX_SECRET_BYTES + or len(value) != before.st_size + or ( + before.st_dev, + before.st_ino, + before.st_uid, + before.st_mode, + before.st_size, + before.st_nlink, + ) + != ( + after.st_dev, + after.st_ino, + after.st_uid, + after.st_mode, + after.st_size, + after.st_nlink, + ) + ): + raise SignerError("invalid secret") + return value, before + + def _read_secret(path: Path) -> bytearray: descriptor = _open_confined_secret(path) try: - before = os.fstat(descriptor) - if ( - not stat.S_ISREG(before.st_mode) - or before.st_uid not in {0, os.geteuid()} - or before.st_mode & 0o022 - or before.st_nlink != 1 - or before.st_size <= 0 - or before.st_size > MAX_SECRET_BYTES - ): - raise SignerError("invalid secret") - - value = bytearray() - while len(value) <= MAX_SECRET_BYTES: - chunk = os.read(descriptor, min(4096, MAX_SECRET_BYTES + 1 - len(value))) - if not chunk: - break - value.extend(chunk) - after = os.fstat(descriptor) - if ( - len(value) > MAX_SECRET_BYTES - or len(value) != before.st_size - or ( - before.st_dev, - before.st_ino, - before.st_uid, - before.st_mode, - before.st_size, - ) - != (after.st_dev, after.st_ino, after.st_uid, after.st_mode, after.st_size) - ): - raise SignerError("invalid secret") + value, _ = _read_secret_descriptor(descriptor) return value except OSError as error: raise SignerError("invalid secret") from error @@ -145,9 +177,19 @@ def _read_secret(path: Path) -> bytearray: os.close(descriptor) -def _stage_secret(source: Path, staging_root: Path = STAGING_ROOT) -> Path: +def _stage_secret( + source: Path, + staging_root: Path = STAGING_ROOT, + *, + expected_digest: bytes | None = None, +) -> Path: value = _read_secret(source) try: + if ( + expected_digest is not None + and hashlib.sha256(value).digest() != expected_digest + ): + raise SignerError("invalid secret") directory = Path(tempfile.mkdtemp(prefix="solmara-transit-", dir=staging_root)) directory.chmod(0o700) metadata = directory.lstat() @@ -187,6 +229,64 @@ def _stage_secret(source: Path, staging_root: Path = STAGING_ROOT) -> Path: value[:] = b"\0" * len(value) +def _consume_secret(path: Path, expected_digest: bytes) -> None: + descriptor = _open_confined_secret(path) + value = bytearray() + directory = -1 + try: + value, opened = _read_secret_descriptor(descriptor) + if hashlib.sha256(value).digest() != expected_digest: + raise SignerError("invalid secret") + directory = _open_confined_directory(path.parent) + current = os.stat(path.name, dir_fd=directory, follow_symlinks=False) + + def identity(metadata: os.stat_result) -> tuple[int, int, int, int, int, int]: + return ( + metadata.st_dev, + metadata.st_ino, + metadata.st_uid, + metadata.st_mode, + metadata.st_size, + metadata.st_nlink, + ) + + if identity(current) != identity(opened): + raise SignerError("invalid secret") + os.unlink(path.name, dir_fd=directory) + except OSError as error: + raise SignerError("invalid secret") from error + finally: + if directory >= 0: + os.close(directory) + value[:] = b"\0" * len(value) + os.close(descriptor) + + +def _remove_empty_staging_directory( + path: Path, staging_root: Path = STAGING_ROOT +) -> None: + directory = path.parent + try: + metadata = directory.lstat() + if ( + directory.parent != staging_root + or not directory.name.startswith("solmara-transit-") + or not stat.S_ISDIR(metadata.st_mode) + or stat.S_ISLNK(metadata.st_mode) + or metadata.st_uid != os.geteuid() + or stat.S_IMODE(metadata.st_mode) != 0o700 + ): + raise SignerError("invalid staging area") + directory.rmdir() + except OSError as error: + raise SignerError("invalid staging area") from error + + +def _discard_staged_secret(path: Path, expected_digest: bytes) -> None: + _consume_secret(path, expected_digest) + _remove_empty_staging_directory(path, path.parent.parent) + + def _b64url(raw: bytes) -> str: return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") @@ -195,7 +295,7 @@ def _b64decode(value: str) -> bytes: return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) -def _verify_public_match(private_path: Path, public_path: Path) -> None: +def _verify_public_match(private_path: Path, public_path: Path) -> tuple[bytes, bytes]: private_bytes = _read_secret(private_path) public_bytes = _read_secret(public_path) try: @@ -232,6 +332,10 @@ def _verify_public_match(private_path: Path, public_path: Path) -> None: ) if public != expected or any(private[key] != expected[key] for key in expected): raise SignerError("invalid key pair") + return ( + hashlib.sha256(private_bytes).digest(), + hashlib.sha256(public_bytes).digest(), + ) except SignerError: raise except (UnicodeError, json.JSONDecodeError, KeyError, TypeError, ValueError): @@ -326,15 +430,22 @@ def exec_signer( or key_name not in ALLOWED_KEY_NAMES ): raise SignerError("invalid signer configuration") - _verify_public_match(private_jwk, public_jwk) + private_digest, public_digest = _verify_public_match(private_jwk, public_jwk) _validate_socket(socket_path) _validate_proxy(proxy) - staged = _stage_secret(private_jwk) + staged = _stage_secret(private_jwk, expected_digest=private_digest) + try: + _consume_secret(private_jwk, private_digest) + _consume_secret(public_jwk, public_digest) + except SignerError: + _discard_staged_secret(staged, private_digest) + raise arguments = [ sys.executable, str(proxy), "--private-jwk", str(staged), + "--consume-private-jwk", "--socket", str(socket_path), "--key-name", @@ -345,7 +456,11 @@ def exec_signer( "PYTHONDONTWRITEBYTECODE": "1", "PYTHONUNBUFFERED": "1", } - os.execve(sys.executable, arguments, environment) + try: + os.execve(sys.executable, arguments, environment) + except OSError: + _discard_staged_secret(staged, private_digest) + raise def main() -> int: diff --git a/scripts/local-transit-proxy.py b/scripts/local-transit-proxy.py index 4dceb80..a0f6d16 100755 --- a/scripts/local-transit-proxy.py +++ b/scripts/local-transit-proxy.py @@ -65,7 +65,10 @@ def _decode_base64url(value: Any) -> bytes: def _read_private_jwk( - path: Path, *, allow_root_bind_owner: bool = False + path: Path, + *, + allow_root_bind_owner: bool = False, + consume_private_jwk: bool = False, ) -> tuple[ec.EllipticCurvePrivateKey, str]: if not path.is_absolute(): raise ProxyError("invalid key") @@ -78,6 +81,7 @@ def _read_private_jwk( descriptor = os.open(path, flags) except OSError as error: raise ProxyError("invalid key") from error + chunks = bytearray() try: metadata = os.fstat(descriptor) if ( @@ -91,7 +95,6 @@ def _read_private_jwk( or metadata.st_size > MAX_KEY_BYTES ): raise ProxyError("invalid key") - chunks = bytearray() while len(chunks) <= MAX_KEY_BYTES: chunk = os.read(descriptor, min(4096, MAX_KEY_BYTES + 1 - len(chunks))) if not chunk: @@ -99,51 +102,120 @@ def _read_private_jwk( chunks.extend(chunk) if len(chunks) > MAX_KEY_BYTES: raise ProxyError("invalid key") + try: + document = json.loads(chunks, object_pairs_hook=_strict_object) + except (json.JSONDecodeError, UnicodeDecodeError, ProxyError) as error: + raise ProxyError("invalid key") from error + if not isinstance(document, dict) or set(document) != JWK_MEMBERS: + raise ProxyError("invalid key") + if ( + document["kty"] != "EC" + or document["crv"] != "P-256" + or document["alg"] != "ES256" + or not isinstance(document["kid"], str) + or not document["kid"].strip() + or len(document["kid"]) > 256 + or any( + ord(character) < 0x20 or ord(character) == 0x7F + for character in document["kid"] + ) + ): + raise ProxyError("invalid key") + + x_bytes = _decode_base64url(document["x"]) + y_bytes = _decode_base64url(document["y"]) + scalar_bytes = _decode_base64url(document["d"]) + if len(x_bytes) != 32 or len(y_bytes) != 32 or len(scalar_bytes) != 32: + raise ProxyError("invalid key") + try: + private_key = ec.derive_private_key( + int.from_bytes(scalar_bytes, "big"), ec.SECP256R1() + ) + except ValueError as error: + raise ProxyError("invalid key") from error + public = private_key.public_key().public_numbers() + if ( + public.x.to_bytes(32, "big") != x_bytes + or public.y.to_bytes(32, "big") != y_bytes + ): + raise ProxyError("invalid key") + public_pem = ( + private_key.public_key() + .public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + .decode("ascii") + ) + if consume_private_jwk: + _consume_staged_private_jwk(path, metadata) + return private_key, public_pem finally: + chunks[:] = b"\0" * len(chunks) os.close(descriptor) - try: - document = json.loads(chunks, object_pairs_hook=_strict_object) - except (json.JSONDecodeError, UnicodeDecodeError, ProxyError) as error: - raise ProxyError("invalid key") from error - if not isinstance(document, dict) or set(document) != JWK_MEMBERS: - raise ProxyError("invalid key") + +def _consume_staged_private_jwk(path: Path, opened: os.stat_result) -> None: + parent = path.parent + staging_root = Path("/tmp").resolve() if ( - document["kty"] != "EC" - or document["crv"] != "P-256" - or document["alg"] != "ES256" - or not isinstance(document["kid"], str) - or not document["kid"].strip() - or len(document["kid"]) > 256 - or any( - ord(character) < 0x20 or ord(character) == 0x7F - for character in document["kid"] - ) + parent.parent not in {Path("/tmp"), staging_root} + or not parent.name.startswith("solmara-transit-") + or path.name != "signing.jwk" ): raise ProxyError("invalid key") - x_bytes = _decode_base64url(document["x"]) - y_bytes = _decode_base64url(document["y"]) - scalar_bytes = _decode_base64url(document["d"]) - if len(x_bytes) != 32 or len(y_bytes) != 32 or len(scalar_bytes) != 32: - raise ProxyError("invalid key") + flags = os.O_RDONLY + if hasattr(os, "O_DIRECTORY"): + flags |= os.O_DIRECTORY + if hasattr(os, "O_CLOEXEC"): + flags |= os.O_CLOEXEC + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + directory = -1 + staging_root_descriptor = -1 try: - private_key = ec.derive_private_key( - int.from_bytes(scalar_bytes, "big"), ec.SECP256R1() + directory = os.open(parent, flags) + parent_metadata = os.fstat(directory) + if ( + not stat.S_ISDIR(parent_metadata.st_mode) + or parent_metadata.st_uid != os.geteuid() + or stat.S_IMODE(parent_metadata.st_mode) != 0o700 + or os.listdir(directory) != [path.name] + ): + raise ProxyError("invalid key") + current = os.stat(path.name, dir_fd=directory, follow_symlinks=False) + + def identity(metadata: os.stat_result) -> tuple[int, int, int, int, int, int]: + return ( + metadata.st_dev, + metadata.st_ino, + metadata.st_uid, + metadata.st_mode, + metadata.st_size, + metadata.st_nlink, + ) + + if identity(current) != identity(opened): + raise ProxyError("invalid key") + os.unlink(path.name, dir_fd=directory) + if os.listdir(directory): + raise ProxyError("invalid key") + + staging_root_descriptor = os.open(staging_root, flags) + current_parent = os.stat( + parent.name, dir_fd=staging_root_descriptor, follow_symlinks=False ) - except ValueError as error: + if identity(current_parent)[:4] != identity(parent_metadata)[:4]: + raise ProxyError("invalid key") + os.rmdir(parent.name, dir_fd=staging_root_descriptor) + except OSError as error: raise ProxyError("invalid key") from error - public = private_key.public_key().public_numbers() - if ( - public.x.to_bytes(32, "big") != x_bytes - or public.y.to_bytes(32, "big") != y_bytes - ): - raise ProxyError("invalid key") - public_pem = private_key.public_key().public_bytes( - serialization.Encoding.PEM, - serialization.PublicFormat.SubjectPublicKeyInfo, - ).decode("ascii") - return private_key, public_pem + finally: + if staging_root_descriptor >= 0: + os.close(staging_root_descriptor) + if directory >= 0: + os.close(directory) def _validate_socket_path(path: Path) -> None: @@ -163,12 +235,19 @@ def _validate_socket_path(path: Path) -> None: class TransitApplication: def __init__( - self, private_jwk: Path, key_name: str, *, allow_root_bind_owner: bool = False + self, + private_jwk: Path, + key_name: str, + *, + allow_root_bind_owner: bool = False, + consume_private_jwk: bool = False, ) -> None: if not KEY_NAME.fullmatch(key_name): raise ProxyError("invalid key") self._private_key, public_pem = _read_private_jwk( - private_jwk, allow_root_bind_owner=allow_root_bind_owner + private_jwk, + allow_root_bind_owner=allow_root_bind_owner, + consume_private_jwk=consume_private_jwk, ) self._metadata = json.dumps( { @@ -214,7 +293,8 @@ def dispatch( return 400, ERROR_DOCUMENT if ( not isinstance(document, dict) - or set(document) != { + or set(document) + != { "input", "key_version", "marshaling_algorithm", @@ -231,14 +311,19 @@ def dispatch( digest = base64.b64decode(document["input"], validate=True) except (binascii.Error, ValueError): return 400, ERROR_DOCUMENT - if len(digest) != 32 or base64.b64encode(digest).decode("ascii") != document["input"]: + if ( + len(digest) != 32 + or base64.b64encode(digest).decode("ascii") != document["input"] + ): return 400, ERROR_DOCUMENT der_signature = self._private_key.sign( digest, ec.ECDSA(utils.Prehashed(hashes.SHA256())) ) r_value, s_value = utils.decode_dss_signature(der_signature) raw_signature = r_value.to_bytes(32, "big") + s_value.to_bytes(32, "big") - encoded = base64.urlsafe_b64encode(raw_signature).rstrip(b"=").decode("ascii") + encoded = ( + base64.urlsafe_b64encode(raw_signature).rstrip(b"=").decode("ascii") + ) response = json.dumps( {"data": {"signature": f"vault:v1:{encoded}"}}, separators=(",", ":"), @@ -255,7 +340,9 @@ def handle(self) -> None: self.connection.settimeout(SOCKET_TIMEOUT_SECONDS) try: method, path, headers, body = self._read_request() - status, response = self.server.application.dispatch(method, path, headers, body) + status, response = self.server.application.dispatch( + method, path, headers, body + ) except ProxyError: status, response = 400, ERROR_DOCUMENT except (OSError, TimeoutError): @@ -312,8 +399,7 @@ def _read_request(self) -> tuple[str, str, dict[str, str], bytes]: except UnicodeDecodeError as error: raise ProxyError("invalid request") from error if any( - ord(character) < 0x20 or ord(character) == 0x7F - for character in decoded + ord(character) < 0x20 or ord(character) == 0x7F for character in decoded ): raise ProxyError("invalid request") headers[normalized] = decoded @@ -367,7 +453,10 @@ def __init__(self, socket_path: Path, application: TransitApplication) -> None: super().__init__(str(socket_path), TransitRequestHandler) socket_path.chmod(0o600) metadata = socket_path.stat() - if not stat.S_ISSOCK(metadata.st_mode) or stat.S_IMODE(metadata.st_mode) != 0o600: + if ( + not stat.S_ISSOCK(metadata.st_mode) + or stat.S_IMODE(metadata.st_mode) != 0o600 + ): raise ProxyError("invalid socket") self._socket_identity = (metadata.st_dev, metadata.st_ino) except Exception: @@ -400,11 +489,15 @@ def build_server( key_name: str, *, allow_root_bind_owner: bool = False, + consume_private_jwk: bool = False, ) -> TransitServer: return TransitServer( socket_path, TransitApplication( - private_jwk, key_name, allow_root_bind_owner=allow_root_bind_owner + private_jwk, + key_name, + allow_root_bind_owner=allow_root_bind_owner, + consume_private_jwk=consume_private_jwk, ), ) @@ -417,6 +510,7 @@ def main() -> int: parser.add_argument("--socket", required=True, type=Path) parser.add_argument("--key-name", required=True) parser.add_argument("--allow-root-bind-owner", action="store_true") + parser.add_argument("--consume-private-jwk", action="store_true") arguments = parser.parse_args() try: server = build_server( @@ -424,6 +518,7 @@ def main() -> int: arguments.socket, arguments.key_name, allow_root_bind_owner=arguments.allow_root_bind_owner, + consume_private_jwk=arguments.consume_private_jwk, ) except (OSError, ProxyError, ValueError): print("local Transit proxy could not start", file=os.sys.stderr) diff --git a/scripts/provision-hosted-runtime.py b/scripts/provision-hosted-runtime.py index 2ca4fa4..67c09d2 100644 --- a/scripts/provision-hosted-runtime.py +++ b/scripts/provision-hosted-runtime.py @@ -169,13 +169,25 @@ def _hmac_secret(root: Path, name: str) -> bytes: def _validate_secret_inventory(root: Path, expected: set[str]) -> None: try: - if root.is_symlink() or not root.is_dir(): + root_metadata = root.lstat() + if ( + not stat.S_ISDIR(root_metadata.st_mode) + or stat.S_IMODE(root_metadata.st_mode) != 0o700 + or root_metadata.st_uid != os.geteuid() + or root_metadata.st_gid != os.getegid() + ): raise ProvisionError("invalid secret inventory") observed: set[str] = set() with os.scandir(root) as entries: for entry in entries: metadata = entry.stat(follow_symlinks=False) - if not stat.S_ISREG(metadata.st_mode) or entry.name not in expected: + if ( + not stat.S_ISREG(metadata.st_mode) + or stat.S_IMODE(metadata.st_mode) != 0o400 + or metadata.st_uid != os.geteuid() + or metadata.st_gid != os.getegid() + or entry.name not in expected + ): raise ProvisionError("invalid secret inventory") observed.add(entry.name) if observed != expected: @@ -188,6 +200,109 @@ def _validate_secret_inventory(root: Path, expected: set[str]) -> None: raise ProvisionError("invalid secret inventory") from None +def _confine_secret_inventory(root: Path) -> None: + descriptor: int | None = None + try: + descriptor = os.open( + root, + os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0), + ) + metadata = os.fstat(descriptor) + if ( + not stat.S_ISDIR(metadata.st_mode) + or stat.S_IMODE(metadata.st_mode) != 0o755 + or metadata.st_uid != os.geteuid() + or metadata.st_gid != os.getegid() + ): + raise ProvisionError("invalid secret inventory") + os.fchmod(descriptor, 0o700) + except ProvisionError: + raise + except OSError: + raise ProvisionError("invalid secret inventory") from None + finally: + if descriptor is not None: + try: + os.close(descriptor) + except OSError: + raise ProvisionError("invalid secret inventory") from None + + +def _provision_secret_inventory(target: str) -> set[str] | None: + if target.endswith("-relay") and target.removesuffix("-relay") in RELAYS: + return None + if target == "mint": + return { + "signing-public.jwk", + "audit-hmac-key", + "solmara-demo-client-public.jwk", + *(f"{client}-public.jwk" for client in MINT_CLIENTS), + } + if target.endswith("-evidence"): + cell = target.removesuffix("-evidence") + if cell in CELLS: + return { + "signing-public.jwk", + "audit-hmac-key", + "subject-binding-hmac-key", + *(f"{client}-client-key" for client in CELL_CLIENTS[cell]), + } + raise ProvisionError("invalid target") + + +def _consume_secret_inventory(root: Path, expected: set[str]) -> None: + """Remove only the closed injected input inventory, never output material.""" + descriptor: int | None = None + valid = True + observed: set[str] = set() + try: + descriptor = os.open( + root, + os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0), + ) + root_metadata = os.fstat(descriptor) + if ( + not stat.S_ISDIR(root_metadata.st_mode) + or stat.S_IMODE(root_metadata.st_mode) != 0o700 + or root_metadata.st_uid != os.geteuid() + or root_metadata.st_gid != os.getegid() + ): + valid = False + observed = set(os.listdir(descriptor)) + if observed != expected: + valid = False + for name in sorted(observed & expected): + try: + metadata = os.stat(name, dir_fd=descriptor, follow_symlinks=False) + if ( + not stat.S_ISREG(metadata.st_mode) + or stat.S_IMODE(metadata.st_mode) != 0o400 + or metadata.st_uid != os.geteuid() + or metadata.st_gid != os.getegid() + ): + valid = False + os.unlink(name, dir_fd=descriptor) + except OSError: + valid = False + if observed - expected: + valid = False + except OSError: + valid = False + finally: + if descriptor is not None: + try: + os.close(descriptor) + except OSError: + valid = False + if not observed - expected: + try: + root.rmdir() + except OSError: + valid = False + if not valid: + raise ProvisionError("secret cleanup failed") + + def _b64decode(value: str) -> bytes: return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) @@ -843,7 +958,7 @@ def _stage_mint( directory.chmod(0o700) -def provision(args: argparse.Namespace) -> None: +def _provision_target(args: argparse.Namespace) -> None: assets, target = args.assets.resolve(), args.target verify_assets(assets) mint_origin = _validated_origin(args.mint_origin, MINT_ORIGIN) @@ -880,16 +995,13 @@ def provision(args: argparse.Namespace) -> None: raise ProvisionError("invalid target") if args.bind_host != EXPECTED_BIND_HOST["mint"]: raise ProvisionError("invalid bind host") - mint_secrets = { - "signing-public.jwk", - "audit-hmac-key", - "solmara-demo-client-public.jwk", - *(f"{client}-public.jwk" for client in MINT_CLIENTS), - } - _validate_secret_inventory(args.secrets.resolve(), mint_secrets) + mint_secrets = _provision_secret_inventory(target) + if mint_secrets is None: + raise ProvisionError("invalid target") + _validate_secret_inventory(args.secrets.absolute(), mint_secrets) _stage_mint( assets, - args.secrets.resolve(), + args.secrets.absolute(), runtime, secret_output, args.bind_host, @@ -924,13 +1036,10 @@ def provision(args: argparse.Namespace) -> None: ) if args.bind_host != EXPECTED_BIND_HOST[cell]: raise ProvisionError("invalid bind host") - cell_secrets = { - "signing-public.jwk", - "audit-hmac-key", - "subject-binding-hmac-key", - *(f"{client}-client-key" for client in CELL_CLIENTS[cell]), - } - _validate_secret_inventory(args.secrets.resolve(), cell_secrets) + cell_secrets = _provision_secret_inventory(target) + if cell_secrets is None: + raise ProvisionError("invalid target") + _validate_secret_inventory(args.secrets.absolute(), cell_secrets) published_at = ( _publication_time( assets, @@ -945,7 +1054,7 @@ def provision(args: argparse.Namespace) -> None: _stage_evidence( assets, cell, - args.secrets.resolve(), + args.secrets.absolute(), runtime, secret_output, extracts if cell in DIRECT else None, @@ -985,6 +1094,23 @@ def provision(args: argparse.Namespace) -> None: ) +def provision(args: argparse.Namespace) -> None: + expected = _provision_secret_inventory(args.target) + if expected is None: + if args.secrets is not None: + raise ProvisionError("invalid target") + _provision_target(args) + return + if args.secrets is None: + raise ProvisionError("invalid target") + secret_root = args.secrets.absolute() + try: + _confine_secret_inventory(secret_root) + _provision_target(args) + finally: + _consume_secret_inventory(secret_root, expected) + + def init_audit(destinations: list[Path], uid: int, gid: int) -> None: if not destinations or uid != 65532 or gid != 65532: raise ProvisionError("invalid audit target") @@ -1003,7 +1129,7 @@ def parser() -> argparse.ArgumentParser: ready = sub.add_parser("provision", add_help=False) ready.add_argument("--target", required=True) ready.add_argument("--assets", required=True, type=Path) - ready.add_argument("--secrets", required=True, type=Path) + ready.add_argument("--secrets", type=Path) ready.add_argument("--runtime-output", required=True, type=Path) ready.add_argument("--source-output", type=Path) ready.add_argument("--secret-output", type=Path) @@ -1020,6 +1146,7 @@ def parser() -> argparse.ArgumentParser: audit.add_argument("--destination", action="append", required=True, type=Path) audit.add_argument("--uid", required=True, type=int) audit.add_argument("--gid", required=True, type=int) + sub.add_parser("ready", add_help=False) return result @@ -1031,7 +1158,7 @@ def main(argv: list[str] | None = None) -> int: provision(args) elif args.command == "publish-extract": publish_extract(args) - else: + elif args.command == "init-audit": init_audit(args.destination, args.uid, args.gid) except Exception: # noqa: BLE001 - the command boundary is deliberately value-free. print(GENERIC_ERROR, file=sys.stderr) diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index c47e73f..7271308 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -330,18 +330,23 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( self.assertIn("steps.transit_signer.outputs.digest", signer_smoke) self.assertIn("--network none --read-only --entrypoint python", signer_smoke) self.assertIn("import cryptography", signer_smoke) - self.assertIn("--public-jwk /run/secrets/signing-public.jwk", signer_smoke) + self.assertIn("--public-jwk, /tmp/solmara-signing-public.jwk", signer_smoke) self.assertIn("v1/transit/keys/solmara-evidence-cra", signer_smoke) - self.assertIn("seq 1 200", signer_smoke) - self.assertIn("docker volume create", signer_smoke) - self.assertIn("type=volume,source=$volume,target=/transit", signer_smoke) - self.assertIn("target=/transit,readonly", signer_smoke) + self.assertIn( + "signing-private: {environment: SIGNING_PRIVATE_JWK}", signer_smoke + ) + self.assertIn("target: /tmp/solmara-signing.jwk", signer_smoke) + self.assertIn( + "transit-init: {condition: service_completed_successfully}", signer_smoke + ) + self.assertIn('docker compose -p "$project"', signer_smoke) + self.assertIn("up -d --wait --wait-timeout 60", signer_smoke) + self.assertIn('not Path("/tmp/solmara-signing.jwk").exists()', signer_smoke) + self.assertIn('not glob.glob("/tmp/solmara-transit-*")', signer_smoke) self.assertIn('client.connect("/transit/transit-proxy.sock")', signer_smoke) self.assertNotIn("--unix-socket", signer_smoke) self.assertNotIn("$state/transit", signer_smoke) - self.assertIn("cells/nia/bundle/public-keys", signer_smoke) - self.assertIn('test "$status" -eq 1', signer_smoke) - self.assertIn("hosted Transit signer could not start", signer_smoke) + self.assertNotIn("type=bind", signer_smoke) packages = next( step diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index 14a351a..518d50c 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -20,11 +20,25 @@ ) PROVIDERS = ("mint", "cra", "nia", "sro", "mosd", "sipf", "nagdi") RELAYS = ("cra", "nia", "mosd", "sipf", "nagdi") +EVIDENCE_CLIENTS = { + "cra": {"cra-pension-evidence", "cra-citizen-evidence"}, + "nia": set(), + "sro": set(), + "mosd": {"mosd-child-benefit-evidence"}, + "sipf": {"sipf-pension-evidence", "sipf-survivor-evidence"}, + "nagdi": {"nagdi-voucher-evidence", "nagdi-livestock-evidence"}, +} +MINT_CLIENTS = { + *(client for clients in EVIDENCE_CLIENTS.values() for client in clients), + "nia-esignet", +} +TARGET_PROVISIONERS = { + *(f"{authority}-relay-provisioner" for authority in RELAYS), + *(f"{provider}-evidence-provisioner" for provider in PROVIDERS[1:]), + "mint-provisioner", +} INTERPOLATION = re.compile(r"^\$\{([A-Z][A-Z0-9_]*):\?[^}]+\}$") -FIXTURE_IMAGE = ( - "ghcr.io/registrystack/solmara-test@sha256:" - + "a" * 64 -) +FIXTURE_IMAGE = "ghcr.io/registrystack/solmara-test@sha256:" + "a" * 64 FIXTURE_PRIVATE_JWK_MEMBERS = { "kty": "EC", "crv": "P-384", @@ -93,8 +107,7 @@ def test_provisioning_application_owns_exactly_34_active_volumes(self) -> None: def test_provisioning_application_contains_only_target_provisioners(self) -> None: services = self.provision["services"] - self.assertEqual(len(services), 12) - self.assertTrue(all(name.endswith("-provisioner") for name in services)) + self.assertEqual(set(services), {*TARGET_PROVISIONERS, "provisioning-ready"}) self.assertNotIn("SOLMARA_TRANSIT_SIGNER_IMAGE", PROVISION.read_text()) self.assertNotIn("--private-jwk", PROVISION.read_text()) private_signing_secrets = { @@ -147,17 +160,20 @@ def test_each_signer_requires_its_matching_public_projection(self) -> None: signer = services[f"{provider}-signer"] secrets = {item["target"]: item["source"] for item in signer["secrets"]} self.assertEqual(len(secrets), 2) - self.assertEqual(secrets["signing.jwk"], f"{prefix}-signing-jwk") self.assertEqual( - secrets["signing-public.jwk"], f"{prefix}-signing-public-jwk" + secrets["/tmp/solmara-signing.jwk"], f"{prefix}-signing-jwk" + ) + self.assertEqual( + secrets["/tmp/solmara-signing-public.jwk"], + f"{prefix}-signing-public-jwk", ) self.assertEqual( signer["command"][0:4], [ "--private-jwk", - "/run/secrets/signing.jwk", + "/tmp/solmara-signing.jwk", "--public-jwk", - "/run/secrets/signing-public.jwk", + "/tmp/solmara-signing-public.jwk", ], ) @@ -175,8 +191,10 @@ def test_mint_client_public_keys_use_provisioner_contract_names(self) -> None: "nia-esignet", ) for client in clients: - self.assertIn(f"solmara-provisioning/{client}-public.jwk", targets) - self.assertIn("solmara-provisioning/solmara-demo-client-public.jwk", targets) + self.assertIn(f"/tmp/solmara-provisioning/{client}-public.jwk", targets) + self.assertIn( + "/tmp/solmara-provisioning/solmara-demo-client-public.jwk", targets + ) def test_each_evidence_provisioner_receives_only_its_public_signing_key( self, @@ -195,10 +213,50 @@ def test_each_evidence_provisioner_receives_only_its_public_signing_key( (provider, sources), ) + def test_provisioner_secret_inventory_is_exact(self) -> None: + consumed_sources: set[str] = set() + for provider in PROVIDERS[1:]: + service = self.provision["services"][f"{provider}-evidence-provisioner"] + targets = { + Path(item["target"]).name: item["source"] for item in service["secrets"] + } + expected = { + "signing-public.jwk": f"{provider}-evidence-signing-public-jwk", + "audit-hmac-key": f"{provider}-evidence-audit-hmac-key", + "subject-binding-hmac-key": ( + f"{provider}-evidence-subject-binding-hmac-key" + ), + **{ + f"{client}-client-key": f"{client}-client-key" + for client in EVIDENCE_CLIENTS[provider] + }, + } + self.assertEqual(targets, expected) + consumed_sources.update(targets.values()) + + mint = self.provision["services"]["mint-provisioner"] + mint_targets = { + Path(item["target"]).name: item["source"] for item in mint["secrets"] + } + expected_mint = { + "signing-public.jwk": "mint-signing-public-jwk", + "audit-hmac-key": "mint-audit-hmac-key", + "solmara-demo-client-public.jwk": "solmara-demo-client-public-jwk", + **{ + f"{client}-public.jwk": f"{client}-client-public-jwk" + for client in MINT_CLIENTS + }, + } + self.assertEqual(mint_targets, expected_mint) + consumed_sources.update(mint_targets.values()) + self.assertEqual(set(self.provision["secrets"]), consumed_sources) + def test_relay_provisioners_receive_no_secret(self) -> None: services = self.provision["services"] for authority in RELAYS: - self.assertNotIn("secrets", services[f"{authority}-relay-provisioner"]) + relay = services[f"{authority}-relay-provisioner"] + self.assertNotIn("secrets", relay) + self.assertNotIn("--secrets", relay["command"]) def test_provisioners_only_elevate_for_authority_volume_initialization( self, @@ -215,11 +273,41 @@ def test_provisioners_only_elevate_for_authority_volume_initialization( self.assertEqual(service["security_opt"], ["no-new-privileges:true"]) if service.get("secrets"): self.assertFalse(service["read_only"]) - self.assertEqual(set(service["tmpfs"]), {"/tmp", "/run/secrets"}) + self.assertEqual(service["tmpfs"], []) + self.assertEqual( + service["command"][service["command"].index("--secrets") + 1], + "/tmp/solmara-provisioning", + ) + for secret in service["secrets"]: + self.assertTrue( + secret["target"].startswith("/tmp/solmara-provisioning/") + ) + self.assertEqual(secret["uid"], "0") + self.assertEqual(secret["gid"], "0") + self.assertEqual(secret["mode"], 0o400) else: self.assertTrue(service["read_only"]) self.assertEqual(service["tmpfs"], ["/tmp"]) + def test_provisioning_readiness_requires_all_targets_to_complete(self) -> None: + readiness = self.provision["services"]["provisioning-ready"] + self.assertEqual( + readiness["depends_on"], + { + target: {"condition": "service_completed_successfully"} + for target in TARGET_PROVISIONERS + }, + ) + self.assertEqual(readiness["command"], ["ready"]) + self.assertEqual(readiness["user"], "65532:65532") + self.assertEqual(readiness["network_mode"], "none") + self.assertTrue(readiness["read_only"]) + self.assertEqual(readiness["tmpfs"], ["/tmp"]) + self.assertEqual(readiness["cap_drop"], ["ALL"]) + self.assertNotIn("cap_add", readiness) + self.assertNotIn("secrets", readiness) + self.assertNotIn("volumes", readiness) + def test_provisioners_receive_closed_permanent_dependency_origins(self) -> None: services = self.provision["services"] for name, service in services.items(): @@ -283,7 +371,8 @@ def test_signer_application_has_exact_services_secrets_and_external_volumes( self.assertEqual(set(self.signers["volumes"]), expected_volumes) self.assertTrue( all( - value == { + value + == { "external": True, "name": f"solmara-authority-cells-{key}", } @@ -330,15 +419,13 @@ def test_signer_application_preserves_process_confinement(self) -> None: self.assertTrue(initializer["read_only"]) self.assertEqual(initializer["cap_drop"], ["ALL"]) self.assertEqual(set(initializer["cap_add"]), {"CHOWN", "FOWNER"}) - self.assertEqual( - initializer["security_opt"], ["no-new-privileges:true"] - ) + self.assertEqual(initializer["security_opt"], ["no-new-privileges:true"]) signer = services[f"{provider}-signer"] self.assertEqual(signer["user"], "65532:65532") self.assertEqual(signer["network_mode"], "none") self.assertFalse(signer["read_only"]) - self.assertEqual(set(signer["tmpfs"]), {"/tmp", "/run/secrets"}) + self.assertNotIn("tmpfs", signer) self.assertEqual(signer["cap_drop"], ["ALL"]) self.assertNotIn("cap_add", signer) self.assertEqual(signer["security_opt"], ["no-new-privileges:true"]) diff --git a/scripts/test_hosted_transit_signer.py b/scripts/test_hosted_transit_signer.py index 82e0785..3fdd187 100644 --- a/scripts/test_hosted_transit_signer.py +++ b/scripts/test_hosted_transit_signer.py @@ -54,6 +54,20 @@ def test_compose_secret_is_copied_to_owner_only_staging(self) -> None: self.assertEqual(stat.S_IMODE(staged.parent.stat().st_mode), 0o700) self.assertEqual(staged.stat().st_uid, os.geteuid()) + def test_consumed_secret_is_unlinked_only_when_its_digest_still_matches( + self, + ) -> None: + digest = hashlib.sha256(self.canary).digest() + MODULE._consume_secret(self.secret, digest) + self.assertFalse(self.secret.exists()) + + replacement = self.secret_directory / "replacement.jwk" + replacement.write_bytes(self.canary) + replacement.chmod(0o400) + with self.assertRaisesRegex(MODULE.SignerError, "invalid secret"): + MODULE._consume_secret(replacement, hashlib.sha256(b"other").digest()) + self.assertTrue(replacement.exists()) + def test_only_root_owned_sticky_writable_parent_is_confined(self) -> None: def directory(mode: int, uid: int = 0) -> os.stat_result: return os.stat_result([stat.S_IFDIR | mode, 1, 0, 1, uid, 0, 0, 0, 0, 0]) @@ -286,6 +300,8 @@ def test_exec_is_one_proxy_one_staged_key_and_one_exact_socket(self) -> None: proxy.chmod(0o500) key_name = "solmara-evidence-cra" stage_secret = MODULE._stage_secret + private_digest = hashlib.sha256(self.canary).digest() + public_digest = hashlib.sha256(b"{}").digest() with ( mock.patch.object(MODULE, "SECRET_PATH", self.secret), @@ -295,9 +311,15 @@ def test_exec_is_one_proxy_one_staged_key_and_one_exact_socket(self) -> None: mock.patch.object( MODULE, "_stage_secret", - side_effect=lambda source: stage_secret(source, self.staging), + side_effect=lambda source, **kwargs: stage_secret( + source, self.staging, **kwargs + ), + ), + mock.patch.object( + MODULE, + "_verify_public_match", + return_value=(private_digest, public_digest), ), - mock.patch.object(MODULE, "_verify_public_match"), mock.patch.object( MODULE.os, "execve", side_effect=RuntimeError("exec captured") ) as execute, @@ -314,11 +336,57 @@ def test_exec_is_one_proxy_one_staged_key_and_one_exact_socket(self) -> None: staged = Path(arguments[3]) self.assertNotEqual(staged, self.secret) self.assertEqual(staged.read_bytes(), self.canary) + self.assertIn("--consume-private-jwk", arguments) + self.assertFalse(self.secret.exists()) + self.assertFalse(self.public.exists()) self.assertNotIn(self.canary.decode("ascii"), repr(execute.call_args)) self.assertEqual( set(environment), {"LANG", "PYTHONDONTWRITEBYTECODE", "PYTHONUNBUFFERED"} ) + def test_failed_exec_removes_the_private_staging_file_and_directory(self) -> None: + socket_directory = self.socket_root / "transit" + socket_directory.mkdir(mode=0o700) + socket_path = socket_directory / "transit-proxy.sock" + proxy = self.root / "local-transit-proxy.py" + proxy.write_text("# fixed proxy\n", encoding="ascii") + proxy.chmod(0o500) + private_digest = hashlib.sha256(self.canary).digest() + public_digest = hashlib.sha256(b"{}").digest() + stage_secret = MODULE._stage_secret + + with ( + mock.patch.object(MODULE, "SECRET_PATH", self.secret), + mock.patch.object(MODULE, "PUBLIC_PATH", self.public), + mock.patch.object(MODULE, "SOCKET_PATH", socket_path), + mock.patch.object(MODULE, "STAGING_ROOT", self.staging), + mock.patch.object( + MODULE, + "_stage_secret", + side_effect=lambda source, **kwargs: stage_secret( + source, self.staging, **kwargs + ), + ), + mock.patch.object( + MODULE, + "_verify_public_match", + return_value=(private_digest, public_digest), + ), + mock.patch.object(MODULE.os, "execve", side_effect=OSError("refused")), + self.assertRaises(OSError), + ): + MODULE.exec_signer( + self.secret, + self.public, + socket_path, + "solmara-evidence-cra", + proxy, + ) + + self.assertEqual(list(self.staging.iterdir()), []) + self.assertFalse(self.secret.exists()) + self.assertFalse(self.public.exists()) + def test_unlisted_key_and_noncanonical_secret_fail_before_staging(self) -> None: with mock.patch.object(MODULE, "_stage_secret") as stage: with self.assertRaisesRegex( diff --git a/scripts/test_local_transit_proxy.py b/scripts/test_local_transit_proxy.py index e075720..6f3bd29 100644 --- a/scripts/test_local_transit_proxy.py +++ b/scripts/test_local_transit_proxy.py @@ -133,6 +133,7 @@ def sign_body(self, payload: bytes, **updates: object) -> bytes: def test_metadata_and_prehashed_signature_match_the_transit_contract(self) -> None: self.start() + self.assertTrue(self.key_path.exists()) status, _, body = self.request("GET", "/v1/transit/keys/solmara-test-key") self.assertEqual(status, 200) data = json.loads(body)["data"] @@ -172,14 +173,61 @@ def test_metadata_and_prehashed_signature_match_the_transit_contract(self) -> No ec.ECDSA(utils.Prehashed(hashes.SHA256())), ) + def test_opt_in_consumes_the_hosted_staged_key_and_private_directory(self) -> None: + with tempfile.TemporaryDirectory( + dir="/tmp", prefix="solmara-transit-" + ) as staged_name: + staged_directory = Path(staged_name) + staged_directory.chmod(0o700) + staged_key = staged_directory / "signing.jwk" + staged_key.write_text( + json.dumps(self.jwk, separators=(",", ":"), sort_keys=True), + encoding="utf-8", + ) + staged_key.chmod(0o600) + + application = MODULE.TransitApplication( + staged_key, + "solmara-test-key", + consume_private_jwk=True, + ) + + self.assertFalse(staged_key.exists()) + self.assertFalse(staged_directory.exists()) + self.assertEqual( + application.metadata_path, + "/v1/transit/keys/solmara-test-key", + ) + + def test_opt_in_refuses_nonstaged_or_invalid_keys_without_unlinking(self) -> None: + with self.assertRaisesRegex(MODULE.ProxyError, "invalid key"): + MODULE.TransitApplication( + self.key_path, + "solmara-test-key", + consume_private_jwk=True, + ) + self.assertTrue(self.key_path.exists()) + + with tempfile.TemporaryDirectory( + dir="/tmp", prefix="solmara-transit-" + ) as staged_name: + staged_key = Path(staged_name) / "signing.jwk" + staged_key.write_text("{}", encoding="utf-8") + staged_key.chmod(0o600) + with self.assertRaisesRegex(MODULE.ProxyError, "invalid key"): + MODULE.TransitApplication( + staged_key, + "solmara-test-key", + consume_private_jwk=True, + ) + self.assertTrue(staged_key.exists()) + def test_wrong_path_header_and_key_are_generically_refused(self) -> None: self.start() cases = [ self.request("GET", "/v1/transit/keys/another-key"), self.request("GET", "/v1/other/keys/solmara-test-key"), - self.request( - "GET", "/v1/transit/keys/solmara-test-key", vault_header=None - ), + self.request("GET", "/v1/transit/keys/solmara-test-key", vault_header=None), self.request( "GET", "/v1/transit/keys/solmara-test-key", vault_header="false" ), @@ -206,7 +254,9 @@ def test_wrong_version_and_every_nonexact_body_are_refused(self) -> None: ) self.assertEqual(status, 400) self.assertEqual(response, MODULE.ERROR_DOCUMENT) - status, _, response = self.request("POST", path, body=self.sign_body(b"payload")) + status, _, response = self.request( + "POST", path, body=self.sign_body(b"payload") + ) self.assertEqual((status, response), (400, MODULE.ERROR_DOCUMENT)) def test_request_and_header_bounds_fail_closed(self) -> None: diff --git a/scripts/test_provision_hosted_runtime.py b/scripts/test_provision_hosted_runtime.py index 7ac1acb..4654cb6 100644 --- a/scripts/test_provision_hosted_runtime.py +++ b/scripts/test_provision_hosted_runtime.py @@ -375,6 +375,7 @@ def test_secret_inventory_is_exact_and_authority_scoped(self) -> None: root = Path(temporary) write_secret(root, "signing-public.jwk", b"public") write_secret(root, "audit-hmac-key", b"a" * 32) + root.chmod(0o700) expected = {"signing-public.jwk", "audit-hmac-key"} provisioner._validate_secret_inventory(root, expected) @@ -382,6 +383,87 @@ def test_secret_inventory_is_exact_and_authority_scoped(self) -> None: with self.assertRaises(provisioner.ProvisionError): provisioner._validate_secret_inventory(root, expected) + def test_injected_secret_root_is_exact_and_confined_before_reading(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + parent = Path(temporary) + inputs = parent / "inputs" + inputs.mkdir(mode=0o755) + provisioner._confine_secret_inventory(inputs) + self.assertEqual(stat.S_IMODE(inputs.stat().st_mode), 0o700) + + wrong_mode = parent / "wrong-mode" + wrong_mode.mkdir(mode=0o700) + with self.assertRaises(provisioner.ProvisionError): + provisioner._confine_secret_inventory(wrong_mode) + + symlink = parent / "link" + symlink.symlink_to(inputs, target_is_directory=True) + with self.assertRaises(provisioner.ProvisionError): + provisioner._confine_secret_inventory(symlink) + + def test_injected_secret_inventory_is_consumed_on_success(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs = root / "inputs" + outputs = root / "generated-output-secrets" + outputs.mkdir() + generated = outputs / "audit-hmac-key" + generated.write_bytes(b"generated-canary") + expected = provisioner._provision_secret_inventory("mint") + assert expected is not None + for name in expected: + write_secret(inputs, name, b"input-canary") + arguments = SimpleNamespace(target="mint", secrets=inputs) + + with mock.patch.object(provisioner, "_provision_target") as target: + provisioner.provision(arguments) + + target.assert_called_once_with(arguments) + self.assertFalse(inputs.exists()) + self.assertEqual(generated.read_bytes(), b"generated-canary") + + def test_injected_secret_inventory_is_consumed_on_failure(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + inputs = Path(temporary) / "inputs" + expected = provisioner._provision_secret_inventory("sipf-evidence") + assert expected is not None + for name in expected: + write_secret(inputs, name, b"input-canary") + arguments = SimpleNamespace(target="sipf-evidence", secrets=inputs) + + with ( + mock.patch.object( + provisioner, + "_provision_target", + side_effect=RuntimeError("failure-canary"), + ), + self.assertRaisesRegex(RuntimeError, "failure-canary"), + ): + provisioner.provision(arguments) + + self.assertFalse(inputs.exists()) + + def test_cleanup_never_removes_an_undeclared_entry(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + inputs = Path(temporary) / "inputs" + write_secret(inputs, "declared", b"declared-canary") + write_secret(inputs, "undeclared", b"undeclared-canary") + + with self.assertRaisesRegex( + provisioner.ProvisionError, "secret cleanup failed" + ): + provisioner._consume_secret_inventory(inputs, {"declared"}) + + self.assertFalse((inputs / "declared").exists()) + self.assertEqual((inputs / "undeclared").read_bytes(), b"undeclared-canary") + + def test_ready_command_has_no_inputs_and_succeeds(self) -> None: + stdout = StringIO() + with contextlib.redirect_stdout(stdout): + result = provisioner.main(["ready"]) + self.assertEqual(result, 0) + self.assertEqual(stdout.getvalue(), f"{provisioner.SUCCESS}\n") + def test_evidence_output_contains_only_its_public_signer_and_own_secrets( self, ) -> None: @@ -571,8 +653,6 @@ def test_relay_provision_preserves_database_and_is_idempotent(self) -> None: "cra-relay", "--assets", str(assets), - "--secrets", - str(secrets), "--runtime-output", str(runtime), "--source-output", @@ -640,6 +720,8 @@ def stage(_assets, _inputs, runtime, secrets, _bind, _mint_origin): with ( mock.patch.object(provisioner, "verify_assets"), mock.patch.object(provisioner, "_validate_secret_inventory"), + mock.patch.object(provisioner, "_confine_secret_inventory"), + mock.patch.object(provisioner, "_consume_secret_inventory"), mock.patch.object(provisioner, "_stage_mint", side_effect=stage), self.assertRaises(provisioner.ProvisionError), ): @@ -678,8 +760,6 @@ def test_relay_cli_loads_manifest_verifier_directly(self) -> None: "cra-relay", "--assets", str(assets), - "--secrets", - str(root / "unused"), "--runtime-output", str(runtime), "--source-output", @@ -713,8 +793,6 @@ def test_manifest_failure_precedes_every_output_mutation(self) -> None: "cra-relay", "--assets", str(assets), - "--secrets", - str(root / "unused"), "--runtime-output", str(runtime), "--source-output", @@ -943,6 +1021,8 @@ def test_evidence_bind_address_is_closed_before_staging(self) -> None: ) with ( mock.patch.object(provisioner, "verify_assets"), + mock.patch.object(provisioner, "_confine_secret_inventory"), + mock.patch.object(provisioner, "_consume_secret_inventory"), mock.patch.object(provisioner, "_stage_evidence") as stage, self.assertRaises(provisioner.ProvisionError), ): From 43aba9de162a91a87a0cce862a0a66bd7984eee7 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 09:43:32 +0700 Subject: [PATCH 45/53] fix(ci): keep Relay provision smoke secret-free Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 2 -- scripts/test_hosted_image_manifest.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 8d2091c..88b856e 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -208,7 +208,6 @@ jobs: "$image" provision \ --target cra-relay \ --assets /opt/solmara-hosted-assets \ - --secrets /run/secrets/solmara-provisioning \ --runtime-output /provisioned/runtime \ --source-output /provisioned/source \ --mint-origin https://mint-authority-cells.solmara.registrystack.org @@ -240,7 +239,6 @@ jobs: "$image" provision \ --target cra-relay \ --assets /opt/solmara-hosted-assets \ - --secrets /run/secrets/solmara-provisioning \ --runtime-output /provisioned/runtime \ --source-output /provisioned/source \ --mint-origin https://mint-authority-cells.solmara.registrystack.org \ diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index 7271308..f2a3217 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -310,6 +310,7 @@ def test_release_workflow_generates_validates_and_uploads_the_manifest( )["run"] self.assertIn("steps.authority_provisioner.outputs.digest", provisioner_smoke) self.assertIn("--network none --read-only", provisioner_smoke) + self.assertNotIn("--secrets", provisioner_smoke) self.assertIn('test "$status" -eq 1', provisioner_smoke) self.assertIn("hosted target provisioning failed", provisioner_smoke) self.assertNotIn('chmod u+w "$state/runtime/runtime.yaml"', provisioner_smoke) From bbfe01f63cf658c149bbc3f1fb5fedf09b11343d Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 09:55:42 +0700 Subject: [PATCH 46/53] fix(ci): smoke disposable provisioner secrets Signed-off-by: Jeremi Joslin --- scripts/smoke-hosted-provisioner-image.py | 136 +++++++++++++----- .../test_smoke_hosted_provisioner_image.py | 42 +++--- 2 files changed, 124 insertions(+), 54 deletions(-) diff --git a/scripts/smoke-hosted-provisioner-image.py b/scripts/smoke-hosted-provisioner-image.py index e1efc09..fcb168e 100755 --- a/scripts/smoke-hosted-provisioner-image.py +++ b/scripts/smoke-hosted-provisioner-image.py @@ -11,6 +11,7 @@ import subprocess import sys import tempfile +import uuid from pathlib import Path import yaml @@ -45,40 +46,101 @@ def _copy_secret(source: Path, destination: Path) -> None: def _run( image: str, arguments: list[str], mounts: list[tuple[Path, str, bool]] ) -> None: - command = [ - "docker", - "run", - "--rm", - "--platform", - "linux/amd64", - "--network", - "none", - "--read-only", - "--user", - "0:0", - "--cap-drop", - "ALL", - "--cap-add", - "CHOWN", - "--cap-add", - "DAC_OVERRIDE", - "--cap-add", - "FOWNER", - "--tmpfs", - "/tmp", + secret_mounts = [ + mount for mount in mounts if mount[1] == "/tmp/solmara-provisioning" ] - for source, target, readonly in mounts: - specification = f"type=bind,source={source},target={target}" - if readonly: - specification += ",readonly" - command.extend(["--mount", specification]) - command.extend([image, *arguments]) - subprocess.run( - command, - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) + if len(secret_mounts) != 1 or not secret_mounts[0][2]: + raise RuntimeError("invalid secret mount") + secret_root = secret_mounts[0][0] + secret_paths = sorted(secret_root.iterdir()) + if not secret_paths: + raise RuntimeError("missing secrets") + environment = os.environ.copy() + secrets: dict[str, dict[str, str]] = {} + service_secrets: list[dict[str, object]] = [] + for index, path in enumerate(secret_paths): + if not path.is_file() or path.is_symlink(): + raise RuntimeError("invalid secret") + secret_name = f"input-{index}" + environment_name = f"SOLMARA_SMOKE_SECRET_{index}" + environment[environment_name] = path.read_text(encoding="utf-8") + secrets[secret_name] = {"environment": environment_name} + service_secrets.append( + { + "source": secret_name, + "target": f"/tmp/solmara-provisioning/{path.name}", + "uid": "0", + "gid": "0", + "mode": 0o400, + } + ) + volumes = [ + { + "type": "bind", + "source": str(source), + "target": target, + "read_only": readonly, + } + for source, target, readonly in mounts + if target != "/tmp/solmara-provisioning" + ] + compose = { + "services": { + "provision": { + "image": image, + "platform": "linux/amd64", + "pull_policy": "never", + "network_mode": "none", + "read_only": False, + "user": "0:0", + "cap_drop": ["ALL"], + "cap_add": ["CHOWN", "DAC_OVERRIDE", "FOWNER"], + "security_opt": ["no-new-privileges:true"], + "volumes": volumes, + "secrets": service_secrets, + "command": arguments, + } + }, + "secrets": secrets, + } + project = f"solmara-provisioner-smoke-{uuid.uuid4().hex}" + with tempfile.TemporaryDirectory( + prefix="solmara-provisioner-compose-" + ) as temporary: + compose_file = Path(temporary) / "compose.yaml" + compose_file.write_text( + yaml.safe_dump(compose, sort_keys=True), encoding="utf-8" + ) + command = [ + "docker", + "compose", + "--project-name", + project, + "--file", + str(compose_file), + ] + try: + subprocess.run( + [ + *command, + "up", + "--abort-on-container-exit", + "--exit-code-from", + "provision", + ], + check=True, + env=environment, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + finally: + subprocess.run( + [*command, "down", "--volumes", "--remove-orphans"], + check=False, + env=environment, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) def smoke(image: str, state_root: Path) -> None: @@ -100,7 +162,7 @@ def smoke(image: str, state_root: Path) -> None: for name in ("runtime", "secrets", "extracts"): (cra / name).mkdir(parents=True) cra_mounts = [ - (cra_input, "/run/secrets/solmara-provisioning", True), + (cra_input, "/tmp/solmara-provisioning", True), (cra / "runtime", "/provisioned/runtime", False), (cra / "secrets", "/provisioned/secrets", False), (cra / "extracts", "/provisioned/extracts", False), @@ -112,7 +174,7 @@ def smoke(image: str, state_root: Path) -> None: "--assets", "/opt/solmara-hosted-assets", "--secrets", - "/run/secrets/solmara-provisioning", + "/tmp/solmara-provisioning", "--runtime-output", "/provisioned/runtime", "--secret-output", @@ -163,7 +225,7 @@ def smoke(image: str, state_root: Path) -> None: for name in ("runtime", "secrets"): (mint / name).mkdir(parents=True) mint_mounts = [ - (mint_input, "/run/secrets/solmara-provisioning", True), + (mint_input, "/tmp/solmara-provisioning", True), (mint / "runtime", "/provisioned/runtime", False), (mint / "secrets", "/provisioned/secrets", False), ] @@ -174,7 +236,7 @@ def smoke(image: str, state_root: Path) -> None: "--assets", "/opt/solmara-hosted-assets", "--secrets", - "/run/secrets/solmara-provisioning", + "/tmp/solmara-provisioning", "--runtime-output", "/provisioned/runtime", "--secret-output", diff --git a/scripts/test_smoke_hosted_provisioner_image.py b/scripts/test_smoke_hosted_provisioner_image.py index 87ef272..fe29c82 100644 --- a/scripts/test_smoke_hosted_provisioner_image.py +++ b/scripts/test_smoke_hosted_provisioner_image.py @@ -20,23 +20,31 @@ class HostedProvisionerImageSmokeTests(unittest.TestCase): def test_container_uses_the_same_bounded_volume_initializer_identity_as_hosted( self, ) -> None: - with mock.patch.object(MODULE.subprocess, "run") as run: - MODULE._run( - "image@sha256:" + "a" * 64, - ["provision"], - [(Path("/state"), "/provisioned/runtime", False)], - ) - command = run.call_args.args[0] - self.assertEqual(command[command.index("--user") + 1], "0:0") - self.assertEqual(command[command.index("--cap-drop") + 1], "ALL") - capabilities = { - command[index + 1] - for index, argument in enumerate(command) - if argument == "--cap-add" - } - self.assertEqual(capabilities, {"CHOWN", "DAC_OVERRIDE", "FOWNER"}) - self.assertIn("none", command) - self.assertIn("--read-only", command) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + inputs = root / "inputs" + inputs.mkdir() + (inputs / "secret").write_text("value", encoding="utf-8") + output = root / "output" + output.mkdir() + with mock.patch.object(MODULE.subprocess, "run") as run: + MODULE._run( + "image@sha256:" + "a" * 64, + ["provision"], + [ + (inputs, "/tmp/solmara-provisioning", True), + (output, "/provisioned/runtime", False), + ], + ) + self.assertEqual(run.call_count, 2) + up = run.call_args_list[0] + self.assertIn("compose", up.args[0]) + self.assertIn("--exit-code-from", up.args[0]) + self.assertNotIn("value", " ".join(up.args[0])) + self.assertEqual(up.kwargs["env"]["SOLMARA_SMOKE_SECRET_0"], "value") + down = run.call_args_list[1] + self.assertIn("down", down.args[0]) + self.assertIn("--volumes", down.args[0]) def test_success_is_sanitized(self) -> None: with tempfile.TemporaryDirectory() as temporary: From 154c08590bfc303249e9c924807935b39491680c Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 10:13:14 +0700 Subject: [PATCH 47/53] fix(hosted): omit tmpfs for secret provisioners Signed-off-by: Jeremi Joslin --- compose.coolify.provision.yaml | 14 ++++++++------ scripts/test_hosted_provisioning_topology.py | 6 +++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml index fb39727..37e4a5d 100644 --- a/compose.coolify.provision.yaml +++ b/compose.coolify.provision.yaml @@ -3,29 +3,31 @@ name: solmara-authority-cells # This operator-only application owns every active runtime volume. Runtime # applications attach them by fixed external name and never receive write # access to authored contracts, source publications, extracts, or secrets. -x-provisioner: &provisioner +x-provisioner-common: &provisioner-common image: ${SOLMARA_AUTHORITY_PROVISIONER_IMAGE:?set the digest-pinned Solmara authority provisioner image} restart: "no" # Each one-shot service is confined to one target's volumes and needs root # only to initialize Docker-owned volume roots and hand private runtime # material to UID 65532. It has no network or writable container root. user: "0:0" - read_only: true - tmpfs: [/tmp] network_mode: none cap_drop: [ALL] cap_add: [CHOWN, DAC_OVERRIDE, FOWNER] security_opt: [no-new-privileges:true] +x-provisioner: &provisioner + <<: *provisioner-common + read_only: true + tmpfs: /tmp + # Docker Compose copies environment-backed secrets before starting a container. # Keep those inputs on the one-shot container's ephemeral root filesystem so a # tmpfs mount cannot mask them, and relax only the targets that consume them. # The provisioner removes the closed input inventory on every exit. Relay # provisioners remain fully read-only and receive no secret path. x-secret-provisioner: &secret-provisioner - <<: *provisioner + <<: *provisioner-common read_only: false - tmpfs: [] services: cra-relay-provisioner: @@ -137,7 +139,7 @@ services: restart: "no" user: "65532:65532" read_only: true - tmpfs: [/tmp] + tmpfs: /tmp network_mode: none cap_drop: [ALL] security_opt: [no-new-privileges:true] diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index 518d50c..c736ce8 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -273,7 +273,7 @@ def test_provisioners_only_elevate_for_authority_volume_initialization( self.assertEqual(service["security_opt"], ["no-new-privileges:true"]) if service.get("secrets"): self.assertFalse(service["read_only"]) - self.assertEqual(service["tmpfs"], []) + self.assertNotIn("tmpfs", service) self.assertEqual( service["command"][service["command"].index("--secrets") + 1], "/tmp/solmara-provisioning", @@ -287,7 +287,7 @@ def test_provisioners_only_elevate_for_authority_volume_initialization( self.assertEqual(secret["mode"], 0o400) else: self.assertTrue(service["read_only"]) - self.assertEqual(service["tmpfs"], ["/tmp"]) + self.assertEqual(service["tmpfs"], "/tmp") def test_provisioning_readiness_requires_all_targets_to_complete(self) -> None: readiness = self.provision["services"]["provisioning-ready"] @@ -302,7 +302,7 @@ def test_provisioning_readiness_requires_all_targets_to_complete(self) -> None: self.assertEqual(readiness["user"], "65532:65532") self.assertEqual(readiness["network_mode"], "none") self.assertTrue(readiness["read_only"]) - self.assertEqual(readiness["tmpfs"], ["/tmp"]) + self.assertEqual(readiness["tmpfs"], "/tmp") self.assertEqual(readiness["cap_drop"], ["ALL"]) self.assertNotIn("cap_add", readiness) self.assertNotIn("secrets", readiness) From 8eea72745db373554e26f3e2b889482900c1ac94 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 10:33:36 +0700 Subject: [PATCH 48/53] fix(hosted): bind Evidence to isolated networks Signed-off-by: Jeremi Joslin --- scripts/provision-hosted-runtime.py | 12 ++++---- scripts/test_hosted_provisioning_topology.py | 31 ++++++++++++++++++++ scripts/test_provision_hosted_runtime.py | 2 +- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/scripts/provision-hosted-runtime.py b/scripts/provision-hosted-runtime.py index 67c09d2..e3248da 100644 --- a/scripts/provision-hosted-runtime.py +++ b/scripts/provision-hosted-runtime.py @@ -45,12 +45,12 @@ } EXPECTED_BIND_HOST = { "mint": "172.29.1.20", - "cra": "172.29.1.21", - "nia": "172.29.1.22", - "sro": "172.29.1.23", - "mosd-programme": "172.29.1.24", - "sipf": "172.29.1.25", - "nagdi": "172.29.1.26", + "cra": "172.29.2.21", + "nia": "172.29.2.22", + "sro": "172.29.3.23", + "mosd-programme": "172.29.3.24", + "sipf": "172.29.4.25", + "nagdi": "172.29.5.26", } CELL_CLIENTS = { "cra": ("cra-pension-evidence", "cra-citizen-evidence"), diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py index c736ce8..bde5214 100644 --- a/scripts/test_hosted_provisioning_topology.py +++ b/scripts/test_hosted_provisioning_topology.py @@ -327,6 +327,37 @@ def test_provisioners_receive_closed_permanent_dependency_origins(self) -> None: else: self.assertNotIn("--relay-origin", command) + def test_evidence_bind_hosts_match_their_isolated_runtime_networks(self) -> None: + runtime_services = { + "cra": self.runtime["compose.coolify.interior.yaml"]["services"][ + "cra-evidence" + ], + "nia": self.runtime["compose.coolify.interior.yaml"]["services"][ + "nia-evidence" + ], + "sro": self.runtime["compose.coolify.social-development.yaml"]["services"][ + "sro-evidence" + ], + "mosd": self.runtime["compose.coolify.social-development.yaml"]["services"][ + "mosd-programme-evidence" + ], + "sipf": self.runtime["compose.coolify.labour-pensions.yaml"]["services"][ + "sipf-evidence" + ], + "nagdi": self.runtime["compose.coolify.agriculture.yaml"]["services"][ + "nagdi-evidence" + ], + } + for authority, runtime in runtime_services.items(): + provisioner = self.provision["services"][ + f"{authority}-evidence-provisioner" + ] + command = provisioner["command"] + self.assertEqual( + command[command.index("--bind-host") + 1], + runtime["networks"]["runtime"]["ipv4_address"], + ) + def test_each_signer_and_transit_initializer_mount_only_its_matching_volume( self, ) -> None: diff --git a/scripts/test_provision_hosted_runtime.py b/scripts/test_provision_hosted_runtime.py index 4654cb6..d55f3e7 100644 --- a/scripts/test_provision_hosted_runtime.py +++ b/scripts/test_provision_hosted_runtime.py @@ -917,7 +917,7 @@ def test_extract_publication_appends_and_atomically_rebinds(self) -> None: (runtime / "runtime.yaml").write_text( yaml.safe_dump( { - "listener": {"bindHost": "172.29.1.23"}, + "listener": {"bindHost": provisioner.EXPECTED_BIND_HOST["sro"]}, "sourceExtracts": { "sro-poverty-extract": { "path": f"/var/lib/registry-evidence/sro/extracts/{previous_name}" From c920307e24653e358e37303bb55f043ce7a01dfd Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 10:41:45 +0700 Subject: [PATCH 49/53] fix(ci): share hosted bind-host contract Signed-off-by: Jeremi Joslin --- scripts/smoke-hosted-provisioner-image.py | 16 ++++++++++++++-- scripts/test_smoke_hosted_provisioner_image.py | 5 +++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/scripts/smoke-hosted-provisioner-image.py b/scripts/smoke-hosted-provisioner-image.py index fcb168e..34bd362 100755 --- a/scripts/smoke-hosted-provisioner-image.py +++ b/scripts/smoke-hosted-provisioner-image.py @@ -33,6 +33,17 @@ def _build_cells_module(): return module +def _provisioner_module(): + path = ROOT / "scripts" / "provision-hosted-runtime.py" + spec = importlib.util.spec_from_file_location("provision_hosted_runtime", path) + if spec is None or spec.loader is None: + raise RuntimeError("unavailable helper") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + def _write_json(path: Path, value: object) -> None: path.write_text(json.dumps(value, sort_keys=True), encoding="utf-8") path.chmod(0o400) @@ -145,6 +156,7 @@ def _run( def smoke(image: str, state_root: Path) -> None: helper = _build_cells_module() + provisioner = _provisioner_module() cra_input = state_root / "cra-input" cra_input.mkdir(parents=True) _write_json( @@ -182,7 +194,7 @@ def smoke(image: str, state_root: Path) -> None: "--extract-output", "/provisioned/extracts", "--bind-host", - "172.29.1.21", + provisioner.EXPECTED_BIND_HOST["cra"], "--mint-origin", MINT_ORIGIN, "--relay-origin", @@ -242,7 +254,7 @@ def smoke(image: str, state_root: Path) -> None: "--secret-output", "/provisioned/secrets", "--bind-host", - "172.29.1.20", + provisioner.EXPECTED_BIND_HOST["mint"], "--mint-origin", MINT_ORIGIN, ] diff --git a/scripts/test_smoke_hosted_provisioner_image.py b/scripts/test_smoke_hosted_provisioner_image.py index fe29c82..a5ad19f 100644 --- a/scripts/test_smoke_hosted_provisioner_image.py +++ b/scripts/test_smoke_hosted_provisioner_image.py @@ -17,6 +17,11 @@ class HostedProvisionerImageSmokeTests(unittest.TestCase): + def test_smoke_uses_the_provisioner_owned_bind_hosts(self) -> None: + provisioner = MODULE._provisioner_module() + self.assertEqual(provisioner.EXPECTED_BIND_HOST["cra"], "172.29.2.21") + self.assertEqual(provisioner.EXPECTED_BIND_HOST["mint"], "172.29.1.20") + def test_container_uses_the_same_bounded_volume_initializer_identity_as_hosted( self, ) -> None: From b9da809f4c5b39061e82622cc243cd4d8ddd0edc Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Thu, 13 Aug 2026 11:09:38 +0700 Subject: [PATCH 50/53] fix(hosted): install runtime secrets for nonroot services Signed-off-by: Jeremi Joslin --- scripts/provision-hosted-runtime.py | 68 +++++++++++++++++------- scripts/test_provision_hosted_runtime.py | 49 ++++++++++++++++- 2 files changed, 96 insertions(+), 21 deletions(-) diff --git a/scripts/provision-hosted-runtime.py b/scripts/provision-hosted-runtime.py index e3248da..e21eb79 100644 --- a/scripts/provision-hosted-runtime.py +++ b/scripts/provision-hosted-runtime.py @@ -455,6 +455,33 @@ def _check_install_tree( raise ProvisionError("existing output mismatch") +def _legacy_secret_tree(staged: Path, destination: Path) -> bool: + if destination.is_symlink(): + raise ProvisionError("invalid existing output") + expected = _tree_digest(staged) + current = _tree_digest(destination) + if current: + for path in destination.rglob("*"): + metadata = path.lstat() + if path.is_file() and ( + not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1 + ): + raise ProvisionError("invalid existing output") + if not current or current == expected: + return False + legacy = { + relative: (digest, 0o400 if digest != "directory" and mode == 0o600 else mode) + for relative, (digest, mode) in expected.items() + } + if current != legacy: + raise ProvisionError("existing output mismatch") + return True + + +def _check_secret_install_tree(staged: Path, destination: Path) -> None: + _legacy_secret_tree(staged, destination) + + def _install_tree( staged: Path, destination: Path, @@ -505,6 +532,19 @@ def _install_tree( raise ProvisionError("output verification failed") +def _install_secret_tree(staged: Path, destination: Path) -> None: + if _legacy_secret_tree(staged, destination): + for path in destination.rglob("*"): + if path.is_file(): + path.chmod(0o600) + _install_tree( + staged, + destination, + root_mode=0o700, + owner=(65532, 65532), + ) + + def _preserve_extract_rollback(relative: str, value: tuple[str, int]) -> bool: return ( "/" not in relative @@ -882,18 +922,18 @@ def _stage_evidence( _patch_runtime(runtime_file, bind_host, extract_name) runtime_file.chmod(0o444) _write( - secret_output / "audit-hmac-key", _hmac_secret(secrets, "audit-hmac-key"), 0o400 + secret_output / "audit-hmac-key", _hmac_secret(secrets, "audit-hmac-key"), 0o600 ) _write( secret_output / "subject-binding-hmac-key", _hmac_secret(secrets, "subject-binding-hmac-key"), - 0o400, + 0o600, ) for client in CELL_CLIENTS[cell]: value = _read_secret(secrets, f"{client}-client-key") _private_client_jwk(value) - _write(secret_output / f"{client}-client-key", value, 0o400) - _write(secret_output / f"{client}-client-id", client.encode(), 0o400) + _write(secret_output / f"{client}-client-key", value, 0o600) + _write(secret_output / f"{client}-client-id", client.encode(), 0o600) _freeze_tree(runtime) for directory in [secret_output, *secret_output.rglob("*")]: if directory.is_dir(): @@ -950,7 +990,7 @@ def _stage_mint( 0o444, ) _write( - secret_output / "audit-hmac-key", _hmac_secret(secrets, "audit-hmac-key"), 0o400 + secret_output / "audit-hmac-key", _hmac_secret(secrets, "audit-hmac-key"), 0o600 ) _freeze_tree(runtime) for directory in [secret_output, *secret_output.rglob("*")]: @@ -1007,14 +1047,9 @@ def _provision_target(args: argparse.Namespace) -> None: args.bind_host, mint_origin, ) - _check_install_tree(secret_output, args.secret_output.resolve()) + _check_secret_install_tree(secret_output, args.secret_output.resolve()) _check_install_tree(runtime, args.runtime_output.resolve()) - _install_tree( - secret_output, - args.secret_output.resolve(), - root_mode=0o700, - owner=(65532, 65532), - ) + _install_secret_tree(secret_output, args.secret_output.resolve()) elif target.endswith("-evidence"): cell = target.removesuffix("-evidence") if ( @@ -1065,18 +1100,13 @@ def _provision_target(args: argparse.Namespace) -> None: relay_origin, ) runtime_preserve = _preserve_extract_rollback if cell in DIRECT else None - _check_install_tree(secret_output, args.secret_output.resolve()) + _check_secret_install_tree(secret_output, args.secret_output.resolve()) if cell in DIRECT: _check_install_tree(extracts, args.extract_output.resolve()) _check_install_tree( runtime, args.runtime_output.resolve(), preserve=runtime_preserve ) - _install_tree( - secret_output, - args.secret_output.resolve(), - root_mode=0o700, - owner=(65532, 65532), - ) + _install_secret_tree(secret_output, args.secret_output.resolve()) if cell in DIRECT: _install_tree(extracts, args.extract_output.resolve(), root_mode=0o555) else: diff --git a/scripts/test_provision_hosted_runtime.py b/scripts/test_provision_hosted_runtime.py index d55f3e7..6f2d87c 100644 --- a/scripts/test_provision_hosted_runtime.py +++ b/scripts/test_provision_hosted_runtime.py @@ -533,6 +533,8 @@ def test_evidence_output_contains_only_its_public_signer_and_own_secrets( self.assertNotIn( "signing", " ".join(path.name for path in output_secrets.iterdir()) ) + for secret in output_secrets.iterdir(): + self.assertEqual(stat.S_IMODE(secret.stat().st_mode), 0o600) public_file = ( runtime / "bundle" / "public-keys" / f"{signing_public['kid']}.jwk.json" ) @@ -592,6 +594,10 @@ def test_mint_writes_only_audit_secret_and_public_client_registrations( self.assertEqual( {path.name for path in output_secrets.iterdir()}, {"audit-hmac-key"} ) + self.assertEqual( + stat.S_IMODE((output_secrets / "audit-hmac-key").stat().st_mode), + 0o600, + ) emitted = "\n".join( path.read_text() for path in runtime.rglob("*") if path.is_file() ) @@ -693,6 +699,7 @@ def test_all_outputs_are_preflighted_before_any_volume_changes(self) -> None: runtime_output.mkdir() provisioner._write(runtime_output / "runtime.yaml", b"active", 0o444) secret_output = root / "secret-output" + provisioner._write(secret_output / "audit-hmac-key", b"a" * 32, 0o400) arguments = provisioner.parser().parse_args( [ "provision", @@ -715,7 +722,7 @@ def test_all_outputs_are_preflighted_before_any_volume_changes(self) -> None: def stage(_assets, _inputs, runtime, secrets, _bind, _mint_origin): provisioner._write(runtime / "runtime.yaml", b"replacement", 0o444) - provisioner._write(secrets / "audit-hmac-key", b"a" * 32, 0o400) + provisioner._write(secrets / "audit-hmac-key", b"a" * 32, 0o600) with ( mock.patch.object(provisioner, "verify_assets"), @@ -726,9 +733,47 @@ def stage(_assets, _inputs, runtime, secrets, _bind, _mint_origin): self.assertRaises(provisioner.ProvisionError), ): provisioner.provision(arguments) - self.assertFalse(secret_output.exists()) + self.assertEqual( + stat.S_IMODE((secret_output / "audit-hmac-key").stat().st_mode), + 0o400, + ) self.assertEqual((runtime_output / "runtime.yaml").read_bytes(), b"active") + def test_generated_secret_mode_upgrade_is_preflighted_and_exact(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + staged = root / "staged" + destination = root / "destination" + provisioner._write(staged / "audit-hmac-key", b"a" * 32, 0o600) + provisioner._write(destination / "audit-hmac-key", b"a" * 32, 0o400) + + provisioner._check_secret_install_tree(staged, destination) + self.assertEqual( + stat.S_IMODE((destination / "audit-hmac-key").stat().st_mode), + 0o400, + ) + with mock.patch.object(provisioner.os, "chown"): + provisioner._install_secret_tree(staged, destination) + self.assertEqual( + stat.S_IMODE((destination / "audit-hmac-key").stat().st_mode), + 0o600, + ) + + (destination / "audit-hmac-key").write_bytes(b"mismatch") + (destination / "audit-hmac-key").chmod(0o400) + with self.assertRaises(provisioner.ProvisionError): + provisioner._check_secret_install_tree(staged, destination) + + hardlink_destination = root / "hardlink-destination" + provisioner._write( + hardlink_destination / "audit-hmac-key", b"a" * 32, 0o600 + ) + os.link( + hardlink_destination / "audit-hmac-key", root / "external-hard-link" + ) + with self.assertRaises(provisioner.ProvisionError): + provisioner._check_secret_install_tree(staged, hardlink_destination) + def test_relay_cli_loads_manifest_verifier_directly(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) From 8b676c04ad2ed445d6c806a021e6c7e9f0d90c61 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 19 Aug 2026 16:36:07 +0200 Subject: [PATCH 51/53] chore(deps): adopt Registry Stack v0.22.0 Move the pinned release from v0.21.0 to v0.22.0 so the authority-cell reset lands on the current release rather than one that is already superseded. Relay, Evidence, and Mint move to the v0.22.0 digests, and Relayctl to the v0.22.0 linux-amd64 asset. Every pin was verified against the published artifacts: the three image digests and their org.opencontainers.image version/revision labels come from GHCR, and the Relayctl checksum matches both the release SHA256SUMS and a locally hashed download. All three images carry revision d9a278a5, the v0.22.0 tag commit, so the builder label check holds. The "first coherent release" claims in the README and hosted-deployment docs still refer to v0.21.0, which remains the earliest release publishing all three official images; only the pinned version moves. Signed-off-by: Jeremi Joslin --- README.md | 11 +++++---- docs/changelog.md | 11 +++++++++ docs/hosted-deployment.md | 5 ++-- home/src/lib/server/data.test.ts | 4 ++-- justfile | 2 +- scripts/check-registry-stack-release-pin.py | 2 +- scripts/test_build_registry_stack_runtime.py | 10 ++++---- scripts/test_hosted_image_manifest.py | 2 +- scripts/test_registry_stack_release_pin.py | 18 +++++++------- versions.env | 25 ++++++++++---------- 10 files changed, 52 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index c1c885d..89c5d21 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,12 @@ Relay-compatible scope and purpose claims required by Relay V2, and its Evidence runtime cannot classify Relay's concealed `consultation.unresolved` response as a data-free unresolved consultation. Registry Stack v0.21.0 is the first coherent release that contains both capabilities and publishes official -Relay, Evidence, and Mint runtime images. Solmara pins its release tag and -source commit, those official OCI references by digest, and the `relayctl` -binary checksum in `versions.env`. The release handoff and every hosted -deployment consume those same full references without reconstructing them from -a second deployment input. +Relay, Evidence, and Mint runtime images. Solmara tracks that release line and +currently pins v0.22.0, recording its release tag and source commit, those +official OCI references by digest, and the `relayctl` binary checksum in +`versions.env`. The release handoff and every hosted deployment consume those +same full references without reconstructing them from a second deployment +input. The eSignet profile uses the separately released `esignet-relay-authenticator` v0.2.0 JAR and its matching SHA-256 checksum. No diff --git a/docs/changelog.md b/docs/changelog.md index 4626697..cbae640 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,17 @@ A small dated log of what changed in the visitor center and the lab topology. Newest entry first. +## 2026-08-19 Registry Stack v0.22.0 + +Solmara pins Registry Stack v0.22.0 across `versions.env`, the release-pin +gate, and the home release-identity surface. The Relay, Evidence, and Mint OCI +references move to the v0.22.0 digests, and the Relayctl authoring helper is +built from the v0.22.0 `linux-amd64` release asset and its checksum. The +topology is unchanged; v0.22.0 adds a container runtime deployment preflight, +a strict Mint readiness probe, Mint support for compatible OAuth clients, +eager validation of Evidence trusted public keys, and root-owned Relay +configuration. + ## 2026-08-13 Official Registry Stack runtime images Solmara no longer assembles or publishes local Evidence and Mint runtime diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index b42fbe5..cbc98ff 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -17,8 +17,9 @@ Registry Stack v0.20.0 remains immutable and does not contain the Evidence capability required by this lab. v0.20.1 contains the required runtime capabilities but does not publish official Evidence and Mint OCI images. Registry Stack v0.21.0 is the first coherent release with all three official -runtime images. Do not move a release, substitute a floating source reference, -or recreate those runtime images in Solmara. +runtime images; the lab currently pins v0.22.0 from that line. Do not move a +release, substitute a floating source reference, or recreate those runtime +images in Solmara. ## Release package precondition diff --git a/home/src/lib/server/data.test.ts b/home/src/lib/server/data.test.ts index 8946cd4..ef28a42 100644 --- a/home/src/lib/server/data.test.ts +++ b/home/src/lib/server/data.test.ts @@ -18,8 +18,8 @@ describe('home data adapters', () => { it('publishes the exact Registry Stack release identity', async () => { const versions = await readVersions(); - expect(versions.REGISTRY_STACK_REQUIRED_VERSION).toBe('0.21.0'); - expect(versions.REGISTRY_STACK_SOURCE_REF).toBe('v0.21.0'); + expect(versions.REGISTRY_STACK_REQUIRED_VERSION).toBe('0.22.0'); + expect(versions.REGISTRY_STACK_SOURCE_REF).toBe('v0.22.0'); expect(versions.REGISTRY_STACK_SOURCE_COMMIT).toMatch(/^[0-9a-f]{40}$/); expect(versions.REGISTRY_RELAY_IMAGE).toMatch(/@sha256:[0-9a-f]{64}$/); expect(versions.SOLMARA_EVIDENCE_IMAGE).toMatch(/^ghcr\.io\/registrystack\/evidence@sha256:[0-9a-f]{64}$/); diff --git a/justfile b/justfile index 400663b..0be2f77 100644 --- a/justfile +++ b/justfile @@ -66,7 +66,7 @@ compose: @scripts/check-hosted-compose.sh @scripts/check-coolify-compose.sh -# Verify that every Registry Stack v0.21.0 release reference is public and immutable. +# Verify that every Registry Stack v0.22.0 release reference is public and immutable. hosted-pin-check: build-runtime-images scripts/check-image-pins.py scripts/check-registry-stack-release-pin.py --require-public diff --git a/scripts/check-registry-stack-release-pin.py b/scripts/check-registry-stack-release-pin.py index a4e783e..c20ae0d 100755 --- a/scripts/check-registry-stack-release-pin.py +++ b/scripts/check-registry-stack-release-pin.py @@ -10,7 +10,7 @@ ROOT = Path(__file__).resolve().parents[1] DIGEST = re.compile(r"^[0-9a-f]{64}$") HTTPS = re.compile(r"^https://[^\s]+$") -REQUIRED_VERSION = "0.21.0" +REQUIRED_VERSION = "0.22.0" def read_versions(path: Path) -> dict[str, str]: diff --git a/scripts/test_build_registry_stack_runtime.py b/scripts/test_build_registry_stack_runtime.py index 85e4937..c66b838 100644 --- a/scripts/test_build_registry_stack_runtime.py +++ b/scripts/test_build_registry_stack_runtime.py @@ -36,13 +36,13 @@ def setUp(self) -> None: self.relayctl_asset.write_bytes(b"published relayctl fixture") relayctl_sha256 = hashlib.sha256(self.relayctl_asset.read_bytes()).hexdigest() (self.root / "versions.env").write_text( - "REGISTRY_STACK_REQUIRED_VERSION=0.21.0\n" + "REGISTRY_STACK_REQUIRED_VERSION=0.22.0\n" "REGISTRY_STACK_SOURCE_COMMIT=" + "a" * 40 + "\n" f"REGISTRY_STACK_RELEASE_RELAY_DIGEST={self.digests['RELAY']}\n" f"REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/relay@sha256:{self.digests['RELAY']}\n" f"SOLMARA_EVIDENCE_IMAGE=ghcr.io/registrystack/evidence@sha256:{self.digests['EVIDENCE']}\n" f"SOLMARA_MINT_IMAGE=ghcr.io/registrystack/mint@sha256:{self.digests['MINT']}\n" - "REGISTRY_RELAYCTL_IMAGE=solmara-lab-relayctl:v0.21.0\n" + "REGISTRY_RELAYCTL_IMAGE=solmara-lab-relayctl:v0.22.0\n" "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=https://example.invalid/relayctl\n" f"REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256={relayctl_sha256}\n", encoding="utf-8", @@ -72,7 +72,7 @@ def setUp(self) -> None: elif "org.opencontainers.image.revision" in output_format: print("a" * 40) elif "org.opencontainers.image.version" in output_format: - print("0.21.0") + print("0.22.0") elif "org.opencontainers.image.source" in output_format: print("https://github.com/registrystack/registry-stack") elif arguments[0] == "pull": @@ -134,7 +134,7 @@ def test_all_official_version_tags_match_their_pinned_digests(self) -> None: calls = self.log.read_text(encoding="utf-8") for component in ("relay", "evidence", "mint"): self.assertIn( - f"buildx imagetools inspect ghcr.io/registrystack/{component}:v0.21.0", + f"buildx imagetools inspect ghcr.io/registrystack/{component}:v0.22.0", calls, ) @@ -149,7 +149,7 @@ def test_mismatched_official_tag_digest_fails_closed(self) -> None: result.stderr, ) calls = self.log.read_text(encoding="utf-8") - self.assertNotIn("ghcr.io/registrystack/mint:v0.21.0", calls) + self.assertNotIn("ghcr.io/registrystack/mint:v0.22.0", calls) def test_relayctl_is_verified_on_the_host_before_the_minimal_image_build(self) -> None: self.environment["FORCE_RELAYCTL_BUILD"] = "1" diff --git a/scripts/test_hosted_image_manifest.py b/scripts/test_hosted_image_manifest.py index f2a3217..047e58d 100644 --- a/scripts/test_hosted_image_manifest.py +++ b/scripts/test_hosted_image_manifest.py @@ -130,7 +130,7 @@ def test_every_image_must_use_its_exact_ghcr_repository_and_digest(self) -> None def test_relay_must_use_the_canonical_official_reference(self) -> None: invalid = ( - "ghcr.io/registrystack/relay:v0.21.0", + "ghcr.io/registrystack/relay:v0.22.0", f"ghcr.io/registrystack/solmara-lab-relay@sha256:{'1' * 64}", f"ghcr.io/registrystack/relay@sha256:{'A' * 64}", ) diff --git a/scripts/test_registry_stack_release_pin.py b/scripts/test_registry_stack_release_pin.py index ce4026e..0a99b69 100644 --- a/scripts/test_registry_stack_release_pin.py +++ b/scripts/test_registry_stack_release_pin.py @@ -24,13 +24,13 @@ def authenticator_values() -> dict[str, str]: class ReleasePinTests(unittest.TestCase): def test_older_release_is_rejected_in_favour_of_coherent_release(self) -> None: - values = {"REGISTRY_STACK_REQUIRED_VERSION": "0.20.1", **authenticator_values()} - self.assertIn("must be 0.21.0", MODULE.validate(values, require_public=False)[0]) + values = {"REGISTRY_STACK_REQUIRED_VERSION": "0.21.0", **authenticator_values()} + self.assertIn("must be 0.22.0", MODULE.validate(values, require_public=False)[0]) def test_missing_public_digest_is_an_explicit_blocker(self) -> None: values = { - "REGISTRY_STACK_REQUIRED_VERSION": "0.21.0", - "REGISTRY_STACK_SOURCE_REF": "v0.21.0", + "REGISTRY_STACK_REQUIRED_VERSION": "0.22.0", + "REGISTRY_STACK_SOURCE_REF": "v0.22.0", **authenticator_values(), } self.assertEqual(MODULE.validate(values, require_public=False), []) @@ -38,11 +38,11 @@ def test_missing_public_digest_is_an_explicit_blocker(self) -> None: def test_digest_must_be_exact(self) -> None: values = { - "REGISTRY_STACK_REQUIRED_VERSION": "0.21.0", - "REGISTRY_STACK_SOURCE_REF": "v0.21.0", + "REGISTRY_STACK_REQUIRED_VERSION": "0.22.0", + "REGISTRY_STACK_SOURCE_REF": "v0.22.0", "REGISTRY_STACK_SOURCE_COMMIT": "f" * 40, "REGISTRY_STACK_RELEASE_RELAY_DIGEST": "a" * 64, - "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL": "https://github.com/registrystack/registry-stack/releases/download/v0.21.0/relayctl-v0.21.0-linux-amd64", + "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL": "https://github.com/registrystack/registry-stack/releases/download/v0.22.0/relayctl-v0.22.0-linux-amd64", "REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256": "d" * 64, **authenticator_values(), } @@ -52,12 +52,12 @@ def test_digest_must_be_exact(self) -> None: def test_public_source_ref_must_bind_the_release_tag(self) -> None: values = { - "REGISTRY_STACK_REQUIRED_VERSION": "0.21.0", + "REGISTRY_STACK_REQUIRED_VERSION": "0.22.0", "REGISTRY_STACK_SOURCE_REF": "main", **authenticator_values(), } self.assertIn( - "REGISTRY_STACK_SOURCE_REF must be v0.21.0", + "REGISTRY_STACK_SOURCE_REF must be v0.22.0", MODULE.validate(values, require_public=False), ) diff --git a/versions.env b/versions.env index f53e493..fd381c2 100644 --- a/versions.env +++ b/versions.env @@ -1,18 +1,19 @@ -# Registry Stack v0.21.0 is the first coherent release that publishes the -# Relay, Evidence, and Mint images required by the authority-cell reset. -REGISTRY_STACK_REQUIRED_VERSION=0.21.0 -REGISTRY_STACK_RELEASE_RELAY_DIGEST=366f926009a39c26e6bdeb8fb47f1dee6ecaef54b57451097993314ab76ad296 -REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=https://github.com/registrystack/registry-stack/releases/download/v0.21.0/relayctl-v0.21.0-linux-amd64 -REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256=7330b36f04306c71605a0b648597cd8f36b6eb2ddff8fcc4a9a3619f20cdfa00 +# Registry Stack v0.21.0 is the earliest release that publishes the Relay, +# Evidence, and Mint images required by the authority-cell reset. Solmara +# pins v0.22.0, the current release on that line. +REGISTRY_STACK_REQUIRED_VERSION=0.22.0 +REGISTRY_STACK_RELEASE_RELAY_DIGEST=6d58ea62b78db451409e77a96ed45c7f82a72cc17ebc9d4eb65230319fe13d83 +REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_URL=https://github.com/registrystack/registry-stack/releases/download/v0.22.0/relayctl-v0.22.0-linux-amd64 +REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_SHA256=1354946ffca568d0a95e32de37fb7b45805c3756f21217aca44d1d34babbaebc # Exact release source identity and local thin-image tags. Every Registry Stack # executable is fetched from the published release or the digest-pinned OCI. -REGISTRY_STACK_SOURCE_REF=v0.21.0 -REGISTRY_STACK_SOURCE_COMMIT=06f3ec27bdc3861a81a973135a4c09f397e7f0c4 -REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/relay@sha256:366f926009a39c26e6bdeb8fb47f1dee6ecaef54b57451097993314ab76ad296 -REGISTRY_RELAYCTL_IMAGE=solmara-lab-relayctl:v0.21.0 -SOLMARA_EVIDENCE_IMAGE=ghcr.io/registrystack/evidence@sha256:3d58439222bf4fef1588eb72d2ba3542e566e2a00b92b525252b8fc8ba3793ab -SOLMARA_MINT_IMAGE=ghcr.io/registrystack/mint@sha256:b85d02ec86253707275898e64677f18d110518a9b543225f0a9c01104df1f1bb +REGISTRY_STACK_SOURCE_REF=v0.22.0 +REGISTRY_STACK_SOURCE_COMMIT=d9a278a55fa3cd275e391cdf2d94dae342c65edc +REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/relay@sha256:6d58ea62b78db451409e77a96ed45c7f82a72cc17ebc9d4eb65230319fe13d83 +REGISTRY_RELAYCTL_IMAGE=solmara-lab-relayctl:v0.22.0 +SOLMARA_EVIDENCE_IMAGE=ghcr.io/registrystack/evidence@sha256:175cb81e12125bbb17425bdc2df34b109b0da95b3b1df2d6cb2bfa3223f2c577 +SOLMARA_MINT_IMAGE=ghcr.io/registrystack/mint@sha256:a4aaf418493ce2d14994082b93fb1ccb32b1351d71b3850a694045ad00174e54 # Third-party runtime images are immutable. Refresh intentionally, never by tag. PYTHON_STATIC_IMAGE=python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df From afc3eda937c63875a704a8fe68ccac2843a9a449 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 19 Aug 2026 17:49:32 +0200 Subject: [PATCH 52/53] fix(esignet): start the local eSignet UI without hosted public hosts The UI entrypoint validated SOLMARA_ESIGNET_PUBLIC_HOST and SOLMARA_ESIGNET_UI_PUBLIC_HOST on every start, but only the hosted template carries the placeholders those values substitute. The local topology sets neither, so esignet-ui and esignet-edge exited 78 and nothing published the eSignet ports. Require the public hosts only when the template actually carries placeholders. The hosted template keeps all 12, so hosted rendering stays fail-closed on an absent or injected host. Signed-off-by: Jeremi Joslin --- docker/esignet-ui/render-hosted-nginx.sh | 7 +++ scripts/test_hosted_esignet_topology.py | 59 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/docker/esignet-ui/render-hosted-nginx.sh b/docker/esignet-ui/render-hosted-nginx.sh index 6b7bdc9..cfed5d4 100755 --- a/docker/esignet-ui/render-hosted-nginx.sh +++ b/docker/esignet-ui/render-hosted-nginx.sh @@ -25,6 +25,13 @@ validate_host() { ' } +# Only the hosted template carries host placeholders. The local template binds +# loopback ports instead, so it renders unchanged and needs no public hosts. +if ! grep -q '__ESIGNET_' "$template"; then + cat "$template" >"$output" + exit 0 +fi + if ! validate_host "${SOLMARA_ESIGNET_PUBLIC_HOST:-}" || ! validate_host "${SOLMARA_ESIGNET_UI_PUBLIC_HOST:-}"; then echo "eSignet hosted nginx host configuration is invalid" >&2 diff --git a/scripts/test_hosted_esignet_topology.py b/scripts/test_hosted_esignet_topology.py index 4d2758a..f956194 100644 --- a/scripts/test_hosted_esignet_topology.py +++ b/scripts/test_hosted_esignet_topology.py @@ -14,6 +14,8 @@ CORE_COMPOSE_PATH = ROOT / "compose.coolify.yaml" RENDERER = ROOT / "docker" / "esignet-ui" / "render-hosted-nginx.sh" TEMPLATE = ROOT / "config" / "esignet" / "nginx-hosted.conf" +LOCAL_COMPOSE_PATH = ROOT / "compose.esignet.yaml" +LOCAL_TEMPLATE = ROOT / "config" / "esignet" / "nginx.conf" class HostedEsignetTopologyTests(unittest.TestCase): @@ -125,6 +127,63 @@ def test_renderer_rejects_directive_injection(self) -> None: ) self.assertNotIn(invalid_host, result.stderr) + def test_renderer_passes_through_a_template_without_host_placeholders(self) -> None: + environment = { + key: value + for key, value in os.environ.items() + if key + not in ("SOLMARA_ESIGNET_PUBLIC_HOST", "SOLMARA_ESIGNET_UI_PUBLIC_HOST") + } + with tempfile.TemporaryDirectory() as temporary_directory: + output = Path(temporary_directory) / "nginx.conf" + result = subprocess.run( + [str(RENDERER), str(LOCAL_TEMPLATE), str(output)], + env=environment, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + output.read_text(encoding="utf-8"), + LOCAL_TEMPLATE.read_text(encoding="utf-8"), + ) + + def test_renderer_rejects_absent_hosts_when_the_template_has_placeholders( + self, + ) -> None: + environment = { + key: value + for key, value in os.environ.items() + if key + not in ("SOLMARA_ESIGNET_PUBLIC_HOST", "SOLMARA_ESIGNET_UI_PUBLIC_HOST") + } + with tempfile.TemporaryDirectory() as temporary_directory: + result = subprocess.run( + [ + str(RENDERER), + str(TEMPLATE), + str(Path(temporary_directory) / "nginx.conf"), + ], + env=environment, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 78) + self.assertEqual( + result.stderr, + "eSignet hosted nginx host configuration is invalid\n", + ) + + def test_local_esignet_ui_services_build_a_placeholder_free_template(self) -> None: + compose = yaml.safe_load(LOCAL_COMPOSE_PATH.read_text(encoding="utf-8")) + services = compose["services"] + for name in ("esignet-ui", "esignet-edge"): + with self.subTest(service=name): + template = ROOT / services[name]["build"]["args"]["ESIGNET_NGINX_CONF"] + self.assertNotIn( + "__ESIGNET_", template.read_text(encoding="utf-8") + ) + if __name__ == "__main__": unittest.main() From 98cbe8a04820513768e35815ae75a9c97b941a0a Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Wed, 19 Aug 2026 22:11:34 +0200 Subject: [PATCH 53/53] ci: install just from a checksum-pinned release Both workflows installed just with an unbounded apt-get against Ubuntu's mirrors. That step wedged twice in a row on the merge gate, once for over four hours, so the check never returned and the PR stayed unstable. Download the pinned 1.58.0 linux-musl asset instead and verify its published SHA-256 before use, matching how this repo already pins the Relayctl release asset. Bound both jobs with timeout-minutes so an unreachable dependency fails fast rather than holding the gate open. Signed-off-by: Jeremi Joslin --- .github/workflows/ci.yml | 17 +++++++++++++++-- .github/workflows/release-candidate.yml | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63c1fbe..3fc57a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ permissions: jobs: authority-cell-checks: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -22,9 +23,21 @@ jobs: with: node-version: "22" - name: Install just + env: + JUST_VERSION: 1.58.0 + JUST_SHA256: 4a5cc2f53e6f0f8c59092a6cc38291eb729d46a7dd95d3ae582008881b84931d run: | - sudo apt-get update - sudo apt-get install --yes just + set -euo pipefail + archive="$RUNNER_TEMP/just.tar.gz" + curl --fail --location --silent --show-error \ + --retry 5 --retry-all-errors --connect-timeout 30 --max-time 120 \ + --output "$archive" \ + "https://github.com/casey/just/releases/download/$JUST_VERSION/just-$JUST_VERSION-x86_64-unknown-linux-musl.tar.gz" + printf '%s %s\n' "$JUST_SHA256" "$archive" | sha256sum --check --status + mkdir -p "$RUNNER_TEMP/just-bin" + tar --extract --file "$archive" --directory "$RUNNER_TEMP/just-bin" just + printf '%s\n' "$RUNNER_TEMP/just-bin" >> "$GITHUB_PATH" + "$RUNNER_TEMP/just-bin/just" --version - name: Install dependencies run: | corepack enable diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 88b856e..c84a616 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -14,6 +14,7 @@ permissions: jobs: verify-and-publish: runs-on: ubuntu-latest + timeout-minutes: 60 env: SOLMARA_IMAGE_REGISTRY: ghcr.io/registrystack SOLMARA_IMAGE_TAG: ${{ inputs.solmara_image_tag || github.sha }} @@ -80,9 +81,21 @@ jobs: node-version: "22" - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v3.11.1 - name: Install just + env: + JUST_VERSION: 1.58.0 + JUST_SHA256: 4a5cc2f53e6f0f8c59092a6cc38291eb729d46a7dd95d3ae582008881b84931d run: | - sudo apt-get update - sudo apt-get install --yes just + set -euo pipefail + archive="$RUNNER_TEMP/just.tar.gz" + curl --fail --location --silent --show-error \ + --retry 5 --retry-all-errors --connect-timeout 30 --max-time 120 \ + --output "$archive" \ + "https://github.com/casey/just/releases/download/$JUST_VERSION/just-$JUST_VERSION-x86_64-unknown-linux-musl.tar.gz" + printf '%s %s\n' "$JUST_SHA256" "$archive" | sha256sum --check --status + mkdir -p "$RUNNER_TEMP/just-bin" + tar --extract --file "$archive" --directory "$RUNNER_TEMP/just-bin" just + printf '%s\n' "$RUNNER_TEMP/just-bin" >> "$GITHUB_PATH" + "$RUNNER_TEMP/just-bin/just" --version - name: Install dependencies run: | corepack enable