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 8ba82c1..7e12e99 100644
--- a/.env.example
+++ b/.env.example
@@ -1,135 +1,26 @@
-# 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
-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=
-
+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=
-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=
-SRO_RELAY_WORKLOAD_JWK=
-PROGRAMME_RELAY_WORKLOAD_JWK=
-SIPF_RELAY_WORKLOAD_JWK=
-NAGDI_RELAY_WORKLOAD_JWK=
+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
-
-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/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dc92583..3fc57a7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,43 +7,50 @@ on:
- main
workflow_dispatch:
+permissions:
+ contents: read
+
jobs:
- checks:
+ authority-cell-checks:
runs-on: ubuntu-latest
+ timeout-minutes: 30
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
+ env:
+ JUST_VERSION: 1.58.0
+ JUST_SHA256: 4a5cc2f53e6f0f8c59092a6cc38291eb729d46a7dd95d3ae582008881b84931d
+ run: |
+ 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
+ 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..c84a616 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,398 @@ permissions:
packages: write
jobs:
- smoke:
+ verify-and-publish:
runs-on: ubuntu-latest
+ timeout-minutes: 60
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 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_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 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 official Registry Stack runtime images and Relayctl
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
+ 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="$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
+ 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:
node-version: "22"
- - uses: docker/setup-buildx-action@v3
- - name: Install tools
+ - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v3.11.1
+ - name: Install just
+ env:
+ JUST_VERSION: 1.58.0
+ JUST_SHA256: 4a5cc2f53e6f0f8c59092a6cc38291eb729d46a7dd95d3ae582008881b84931d
+ run: |
+ 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
- sudo apt-get update
- sudo apt-get install -y just
- - name: Setup
- run: just setup
- - name: Install Playwright browser
+ 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: 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 \
+ cra-relay nia-relay mosd-relay sipf-relay nagdi-relay
+ - 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
- with:
- context: .
- file: docker/relay/Dockerfile
- platforms: linux/amd64
- push: true
- tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay:${{ env.SOLMARA_IMAGE_TAG }}
- build-args: |
- REGISTRY_RELAY_IMAGE=${{ env.REGISTRY_RELAY_IMAGE }}
- VOLUME_INIT_IMAGE=${{ env.VOLUME_INIT_IMAGE }}
- - name: Verify hosted Relay signed bundles
+ - name: Require pre-provisioned public Solmara packages
env:
- SOLMARA_HOSTED_RELAY_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay:${{ env.SOLMARA_IMAGE_TAG }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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 package in \
+ 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 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=${{ env.REGISTRY_STACK_REQUIRED_VERSION }}
+ 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=${{ env.REGISTRY_STACK_REQUIRED_VERSION }}
+ 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" }}')" = "$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
+ 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 --platform linux/amd64 --network none --read-only --tmpfs /tmp \
+ --user 0:0 --cap-drop ALL \
+ --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 \
+ --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
+ 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
+ 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
+ docker run --rm --platform linux/amd64 --network none --read-only --tmpfs /tmp \
+ --user 0:0 --cap-drop ALL \
+ --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 \
+ --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 \
+ >"$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'
+ 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 compose -p "$project" -f "$compose" down -v >/dev/null 2>&1 || true
+ unset SIGNING_PRIVATE_JWK SIGNING_PUBLIC_JWK
+ }
+ trap cleanup EXIT
+ 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)
+ 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)
+ '
+ cleanup
+ trap - EXIT
+ - 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 +412,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 +425,50 @@ 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:
+ 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 }}
+ 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 264ab1e..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/
@@ -27,3 +31,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..89c5d21 100644
--- a/README.md
+++ b/README.md
@@ -1,236 +1,151 @@
# 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 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.
-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.
+The reset has two evidence cadences:
-## Quick Start
+- 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.
-From this repository:
+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.21.0 is the
+first coherent release that contains both capabilities and publishes official
+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
+source-build, locally wrapped runtime, floating-tag, or v0.19 compatibility
+fallback is accepted.
+
+## Quick start
+
+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 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.
+
+Local entry points after startup are:
+
+- Visitor Center: `http://127.0.0.1:4301`
+- Programme portal: `http://127.0.0.1:4300`
+- Authority Evidence gateway: `https://localhost:4341/evidence/{authority}`
+- Static metadata: `http://127.0.0.1:4331`
+
+The gateway strips `/evidence/{authority}` before forwarding. Application code
+uses six authority base URLs and never assumes a national Evidence host.
+
+## Architecture
+
+| 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` |
+
+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.
+
+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.
+
+## Publisher lifecycle
+
+From `generator/`:
+
+```bash
+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 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
+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.
+
+## Verification
```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 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
```
-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`.
+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
+
+- `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..3f1bd7f 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: ${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]
+ labels: {solmara.lab.host: nagdi-relay-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells}
+
+ nagdi-evidence:
+ <<: *runtime
+ 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.5.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: "${SOLMARA_NAGDI_EVIDENCE_PUBLIC_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.5.0/24}]
diff --git a/compose.coolify.esignet.yaml b/compose.coolify.esignet.yaml
index ac5a630..aae556c 100644
--- a/compose.coolify.esignet.yaml
+++ b/compose.coolify.esignet.yaml
@@ -1,52 +1,32 @@
-name: solmara-lab-esignet
+name: solmara-esignet-authority-cells
services:
esignet-database:
- image: ${SOLMARA_ESIGNET_POSTGRES_IMAGE:?set digest-pinned Solmara eSignet Postgres image}
+ image: ${SOLMARA_ESIGNET_POSTGRES_IMAGE:?set digest-pinned eSignet database image}
environment:
- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: ${SOLMARA_ESIGNET_POSTGRES_PASSWORD}
- volumes:
- - esignet-pgdata:/var/lib/postgresql/data
+ 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}
- command: ["redis-server"]
- restart: unless-stopped
- volumes:
- - esignet-redis-data:/data
+ image: ${ESIGNET_REDIS_IMAGE:?versions.env must pin Redis}
+ 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
- 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 +44,80 @@ 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_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: 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'}}"
- 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
+ MOSIP_ESIGNET_OPENID_SCOPE_CLAIMS: "{'profile' : {'given_name','family_name','gender','birthdate','individual_id'}}"
+ 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-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"
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
-
- 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
+ esignet-database: {condition: service_healthy}
+ esignet-redis: {condition: service_healthy}
labels:
- solmara.lab.host: esignet-ui.solmara.registrystack.org
+ solmara.lab.host: ${SOLMARA_ESIGNET_PUBLIC_HOST:-esignet.solmara.registrystack.org}
+ solmara.rollout.slot: authority-cells
- esignet-edge:
- image: ${SOLMARA_ESIGNET_UI_IMAGE:?set digest-pinned Solmara eSignet UI image}
- user: root
- restart: unless-stopped
+ esignet-ui:
+ image: ${SOLMARA_ESIGNET_UI_IMAGE:?set digest-pinned eSignet UI image}
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"
+ 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_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
+ esignet: {condition: service_started}
labels:
- solmara.lab.host: esignet.solmara.registrystack.org
+ 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 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"
- entrypoint: ["seed-esignet.py"]
+ ESIGNET_REQUIRE_HTTPS_REDIRECTS: "true"
+ 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_healthy
- restart: unless-stopped
- healthcheck:
- test: ["CMD", "test", "-f", "/tmp/ready"]
- interval: 10s
- timeout: 5s
- retries: 30
+ esignet-database: {condition: service_healthy}
+ esignet: {condition: service_started}
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..9f2947e 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: ${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]
+ labels: {solmara.lab.host: cra-relay-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells}
+
+ nia-relay:
+ <<: *runtime
+ 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]
+ labels: {solmara.lab.host: nia-relay-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells}
+
+ cra-evidence:
+ <<: *runtime
+ 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.2.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: "${SOLMARA_CRA_EVIDENCE_PUBLIC_HOST:-cra-evidence.solmara.registrystack.org}", solmara.rollout.slot: authority-cells}
+
+ nia-evidence:
+ <<: *runtime
+ 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.2.22}}
+ labels: {solmara.lab.host: "${SOLMARA_NIA_EVIDENCE_PUBLIC_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.2.0/24}]
diff --git a/compose.coolify.labour-pensions.yaml b/compose.coolify.labour-pensions.yaml
index 0ea82e6..00e7cc4 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: ${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]
+ labels: {solmara.lab.host: sipf-relay-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells}
+
+ sipf-evidence:
+ <<: *runtime
+ 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.4.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: "${SOLMARA_SIPF_EVIDENCE_PUBLIC_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.4.0/24}]
diff --git a/compose.coolify.provision.yaml b/compose.coolify.provision.yaml
new file mode 100644
index 0000000..37e4a5d
--- /dev/null
+++ b/compose.coolify.provision.yaml
@@ -0,0 +1,232 @@
+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-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"
+ 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-common
+ read_only: false
+
+services:
+ cra-relay-provisioner:
+ <<: *provisioner
+ 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, --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, --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, --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, --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, /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: /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, /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: /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, /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: /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, /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: /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, /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: /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, /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: /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, /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: /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}
+ mint-audit-hmac-key: {environment: MINT_AUDIT_HMAC_KEY}
+ 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-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-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-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-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-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.signers.yaml b/compose.coolify.signers.yaml
new file mode 100644
index 0000000..addc0ac
--- /dev/null
+++ b/compose.coolify.signers.yaml
@@ -0,0 +1,178 @@
+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"
+ # 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
+ 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
+
+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
+ 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, /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, <<: *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, /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, <<: *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, /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, <<: *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, /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, <<: *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, /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, <<: *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, /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, <<: *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, /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, <<: *private-secret-target}
+ - {source: nagdi-evidence-signing-public-jwk, <<: *public-secret-target}
+ 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}
+ 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/compose.coolify.social-development.yaml b/compose.coolify.social-development.yaml
index 0e45f52..c6b574a 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: ${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]
+ labels: {solmara.lab.host: mosd-programme-relay-authority-cells.solmara.registrystack.org, solmara.rollout.slot: authority-cells}
+
+ sro-evidence:
+ <<: *runtime
+ 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.3.23}}
+ labels: {solmara.lab.host: "${SOLMARA_SRO_EVIDENCE_PUBLIC_HOST:-sro-evidence.solmara.registrystack.org}", solmara.rollout.slot: authority-cells}
+
+ mosd-programme-evidence:
+ <<: *runtime
+ 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.3.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: "${SOLMARA_MOSD_PROGRAMME_EVIDENCE_PUBLIC_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.3.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..4ed850d 100644
--- a/compose.coolify.yaml
+++ b/compose.coolify.yaml
@@ -1,179 +1,164 @@
-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-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: ${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:
+ 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 official Registry Stack Mint image}
+ 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-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 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: "${SOLMARA_METADATA_PUBLIC_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: ${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_BASE_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
+ SCENARIO_RUNNER_PORT: "8080"
+ secrets:
+ - source: solmara-demo-client-key
+ target: solmara-evidence-client.jwk
depends_on:
- child-benefit-federator:
- condition: service_healthy
- labels:
- solmara.lab.visibility: internal
-
+ 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 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: "${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_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
- 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
- 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}"}
+ 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-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"}
- 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
-
+ {"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 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: ${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_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: 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
+ 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: "${SOLMARA_PORTAL_PUBLIC_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..8cec047 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: ${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 official Registry Stack Evidence image}
+ 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 official Registry Stack Mint image}
+ 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 535bcda..5af6d5c 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,1097 +1,706 @@
-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-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}
+x-relay: &relay
+ image: ${REGISTRY_RELAY_IMAGE:?versions.env must name the digest-pinned Relay V2 image}
+ platform: linux/amd64
+ user: "65532:65532"
restart: unless-stopped
- environment: *relay-environment
+ 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}}
healthcheck:
- test: ["CMD", "/usr/local/bin/registry-relay", "healthcheck"]
- interval: 30s
- timeout: 10s
- retries: 3
+ test: ["CMD", "/usr/local/bin/relay", "healthcheck", "--url", "http://127.0.0.1:8080/health"]
+ interval: 10s
+ timeout: 5s
+ retries: 12
-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"
+x-evidence: &evidence
+ image: ${SOLMARA_EVIDENCE_IMAGE:?versions.env must name the digest-pinned official Registry Stack Evidence image}
+ platform: linux/amd64
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}
+ 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 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"
-x-workload-agent-common: &workload-agent-common
- build:
- context: .
- dockerfile: scenario-runner/Dockerfile
+# 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]
+ # 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:
- 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
+ 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"
+
+# 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
+ 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"]
+ 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"]
+ 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"]
+
+ 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:
- - postgres
+ - python
- -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}
- 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"
+ - |-
+ import os
+ import re
+ import stat
+
+ 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.
+ # 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):
+ 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, target_uid, target_gid, follow_symlinks=False)
+ os.chmod(entry.path, 0o600, follow_symlinks=False)
+ os.chown(path, target_uid, target_gid)
+ 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-notary-postgresql.sh:/docker-entrypoint-initdb.d/010-init-notary-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}"]
- 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
- registry-postgresql-bootstrap:
- image: ${POSTGRES_IMAGE:-postgres:16-alpine}
+ authority-audit-init:
+ image: ${PYTHON_STATIC_IMAGE:?versions.env must pin the static server image}
+ command:
+ - python
+ - -c
+ - |-
+ import os
+ import re
+ 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)
+ 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]
+ cap_add: [CHOWN, FOWNER]
+ security_opt: [no-new-privileges:true]
+ network_mode: none
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
+ - 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
+ # 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", "--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", "--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", "--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", "--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", "--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", "--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", "--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:
- 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
-
- 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"
- 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
- 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
-
- 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
-
- sipf-pensions-relay:
- <<: *relay-common
- command: ["--config", "/etc/registry-relay/relay.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
- 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
-
- nagdi-agriculture-relay:
- <<: *relay-common
- command: ["--config", "/etc/registry-relay/relay.yaml"]
- environment:
- <<: *relay-environment
- REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NAGDI_RELAY_AUDIT_HASH_SECRET}
- 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"
+ volumes: ["./metadata/public:/srv/static:ro"]
+ ports: ["${SOLMARA_METADATA_PORT:-4331}:8080"]
+ networks: [runtime]
+
+ 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: ["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: ["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: ["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:
+ <<: *relay
+ environment:
+ 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: ["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:
+ <<: *relay
+ environment:
+ 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: ["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:
+ 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
+ user: "0:0"
+ read_only: true
+ cap_drop: [ALL]
+ security_opt: [no-new-privileges:true]
+ tmpfs: [/tmp]
+ volumes:
+ - ./runtime/evidence-cells/mint:/etc/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:
- 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"
+ runtime: {ipv4_address: 172.29.1.20}
+ 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", "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}, 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", "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}, 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", "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}, 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", "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}, 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", "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}, 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", "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}, 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}
+ user: "0:0"
+ restart: unless-stopped
+ 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:
- 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]
- 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
-
- 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":"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
- depends_on:
- 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":"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-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
- - nia-esignet-workload-token:/run/esignet-secrets
- depends_on:
- nia-population-relay:
- condition: service_started
-
- 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":"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
- depends_on:
- sro-social-relay:
- condition: service_started
-
- 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":"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
- depends_on:
- programme-mis-relay:
- condition: service_started
-
- 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":"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
- depends_on:
- sipf-pensions-relay:
- condition: service_started
-
- 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":"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
- 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
- depends_on:
- nia-population-relay-consultation:
- condition: service_healthy
- nia-consultation-workload-agent:
- condition: service_healthy
- nia-notary-state-install:
- condition: service_completed_successfully
-
- nia-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_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}
- 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
-
- 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"
- 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:
- registry-postgresql-bootstrap:
- condition: service_completed_successfully
- sro-consultation-workload-agent:
- condition: service_healthy
-
- 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"
- 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:
- registry-postgresql-bootstrap:
- condition: service_completed_successfully
- programme-consultation-workload-agent:
- condition: service_healthy
+ runtime:
+ ipv4_address: 172.29.1.10
+ aliases:
+ - evidence.solmara.invalid
+ - 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.62
+ 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}
- 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"
- 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
+ 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]
- 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
- depends_on:
- nagdi-agriculture-relay-consultation:
- condition: service_healthy
- nagdi-consultation-workload-agent:
- condition: service_healthy
- nagdi-notary-state-install:
- condition: service_completed_successfully
-
- 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"
- 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
- depends_on:
- registry-postgresql-bootstrap:
- condition: service_completed_successfully
- nagdi-consultation-workload-agent:
- condition: service_healthy
+ <<: *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:?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}
- 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"
- 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
+ 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/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}
+ image: ${SOLMARA_HOME_IMAGE:-solmara-lab-home:local}
+ environment:
+ HOST: 0.0.0.0
+ PORT: "4301"
+ SOLMARA_LAB_ROOT: /app
+ 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"]
+ networks: [runtime]
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:
- 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:
+ 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-runtime:
+ nia-relay-runtime:
+ mosd-relay-runtime:
+ sipf-relay-runtime:
+ nagdi-relay-runtime:
+ 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:
+ runtime:
+ ipam: {config: [{subnet: 172.29.1.0/24}]}
+ issuer-validation:
+ internal: true
+ ipam: {config: [{subnet: 192.0.2.0/26, ip_range: 192.0.2.0/27}]}
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/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/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/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..6d6b955 100644
--- a/docker/esignet-ui/Dockerfile
+++ b/docker/esignet-ui/Dockerfile
@@ -1,5 +1,18 @@
-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
-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..cfed5d4
--- /dev/null
+++ b/docker/esignet-ui/render-hosted-nginx.sh
@@ -0,0 +1,46 @@
+#!/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
+ }
+ }
+ }
+ '
+}
+
+# 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
+ 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/docker/hosted-authority-provisioner/Dockerfile b/docker/hosted-authority-provisioner/Dockerfile
new file mode 100644
index 0000000..0d62d10
--- /dev/null
+++ b/docker/hosted-authority-provisioner/Dockerfile
@@ -0,0 +1,53 @@
+# 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
+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/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..6ddf53f
--- /dev/null
+++ b/docker/registry-stack-release-binary/Dockerfile
@@ -0,0 +1,5 @@
+# syntax=docker/dockerfile:1.7
+
+FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS relayctl
+COPY --chown=65532:65532 --chmod=0755 relayctl /usr/local/bin/relayctl
+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 877d4b5..cbae640 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -3,6 +3,67 @@
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
+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.
+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. 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
+
+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/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..cbc98ff 100644
--- a/docs/hosted-deployment.md
+++ b/docs/hosted-deployment.md
@@ -1,285 +1,204 @@
-# 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 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; 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
+
+Before the first release-candidate build, an organization owner must provision
+these two public, anonymously pullable GitHub Container Registry packages and
+grant the repository's GitHub Actions workflow write access:
+
+- `ghcr.io/registrystack/solmara-lab-authority-provisioner`
+- `ghcr.io/registrystack/solmara-lab-transit-signer`
+
+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
+
+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 and runs only the one-shot target
+provisioners:
+
+| 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 |
+
+`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.
+
+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. 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
+
+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.
+3. Deploy `compose.coolify.signers.yaml`. Require all seven Transit initializers
+ to complete successfully and all seven signers to become healthy.
+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.
+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
+ the citizen-login smoke through the NIA Relay V2 lookup.
+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.
+
+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 7e27d8a..2935d6b 100644
--- a/docs/purposes.md
+++ b/docs/purposes.md
@@ -1,77 +1,33 @@
-# Solmara Purpose Catalogue
+# Solmara purpose catalogue
-Status: normative for Solmara Lab wave 1.
+Status: normative for the authority-owned Evidence reset.
-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.
+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 IRI | 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.
-
-## 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` |
+| Purpose code | Authority requirements | Evidence source |
|---|---|---|
-| 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` |
-
-## Denial Assertions
-
-Purpose-denial smoke 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.
-
-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.
+| `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/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/cells/cra/bundle/derivations/deceased.rhai b/evidence/cells/cra/bundle/derivations/deceased.rhai
new file mode 100644
index 0000000..df6d5fc
--- /dev/null
+++ b/evidence/cells/cra/bundle/derivations/deceased.rhai
@@ -0,0 +1,3 @@
+fn derive(facts, selectors, evaluation_context) {
+ [#{concept_id: "https://id.registrystack.org/solmara/concept/person-is-deceased", value: required(facts["deceased"], "required_fact_missing")}]
+}
diff --git a/evidence/cells/cra/bundle/derivations/linked.rhai b/evidence/cells/cra/bundle/derivations/linked.rhai
new file mode 100644
index 0000000..a7323a1
--- /dev/null
+++ b/evidence/cells/cra/bundle/derivations/linked.rhai
@@ -0,0 +1,4 @@
+fn derive(facts, selectors, evaluation_context) {
+ 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/cells/mosd-programme/bundle/derivations/not-enrolled.rhai b/evidence/cells/mosd-programme/bundle/derivations/not-enrolled.rhai
new file mode 100644
index 0000000..a2b73b5
--- /dev/null
+++ b/evidence/cells/mosd-programme/bundle/derivations/not-enrolled.rhai
@@ -0,0 +1,3 @@
+fn derive(facts, selectors, evaluation_context) {
+ [#{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/cells/nagdi/bundle/derivations/livestock.rhai b/evidence/cells/nagdi/bundle/derivations/livestock.rhai
new file mode 100644
index 0000000..5585e3d
--- /dev/null
+++ b/evidence/cells/nagdi/bundle/derivations/livestock.rhai
@@ -0,0 +1,10 @@
+fn derive(facts, selectors, evaluation_context) {
+ 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/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/cells/sipf/bundle/derivations/pension-active.rhai b/evidence/cells/sipf/bundle/derivations/pension-active.rhai
new file mode 100644
index 0000000..2fd220b
--- /dev/null
+++ b/evidence/cells/sipf/bundle/derivations/pension-active.rhai
@@ -0,0 +1,3 @@
+fn derive(facts, selectors, evaluation_context) {
+ [#{concept_id: "https://id.registrystack.org/solmara/concept/pension-payment-active", value: required(facts["payment_status"], "required_fact_missing") == "active"}]
+}
diff --git a/evidence/cells/sipf/bundle/derivations/survivor-eligible.rhai b/evidence/cells/sipf/bundle/derivations/survivor-eligible.rhai
new file mode 100644
index 0000000..47e44d7
--- /dev/null
+++ b/evidence/cells/sipf/bundle/derivations/survivor-eligible.rhai
@@ -0,0 +1,3 @@
+fn derive(facts, selectors, evaluation_context) {
+ [#{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/cells/sro/bundle/derivations/poverty-priority.rhai b/evidence/cells/sro/bundle/derivations/poverty-priority.rhai
new file mode 100644
index 0000000..7ec7398
--- /dev/null
+++ b/evidence/cells/sro/bundle/derivations/poverty-priority.rhai
@@ -0,0 +1,3 @@
+fn derive(facts, selectors, evaluation_context) {
+ [#{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
new file mode 100644
index 0000000..e86d900
--- /dev/null
+++ b/evidence/mint.yaml
@@ -0,0 +1,40 @@
+version: 1
+issuer: https://mint.solmara.registrystack.org
+listener:
+ address: 172.29.1.20
+ port: 8081
+signing:
+ 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
+ hashKeyRef: secret:file/audit-hmac-key
+ hashKeyVersion: 1
+accessTokens:
+ audiences: [solmara-runtime]
+ lifetimeSeconds: 300
+ claims:
+ principal: sub
+ requesterTags: evidence_tags
+ evidenceAudience: evidence_audience
+ grantId: evidence_grant_id
+ grantAuthority: evidence_authority
+clientAssertion:
+ audience: https://mint.solmara.registrystack.org/token
+ maximumLifetimeSeconds: 300
+ algorithms: [ES256, RS256]
+clients:
+ directory: clients
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/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..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"
@@ -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/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_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/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 f106175..9b369b0 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 @@
1 synthetic country
{authorities ?? 'Unavailable'} live authorities
-
{registries ?? 'Unavailable'} live registries
+
{evidenceCells ?? 'Unavailable'} authority Evidence cells
{journeys ?? 'Unavailable'} guided policy journeys
0 real resident records
diff --git a/home/src/lib/components/PurposeLens.svelte b/home/src/lib/components/PurposeLens.svelte
index 56200de..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.
-
+
{reasking ? 'Testing the safeguard…' : 'Test the safeguard'}
@@ -298,34 +274,9 @@
{/if}
-
- Explore other purposes or inspect the technical request
-
-
-
- Alternate purpose
-
- {#each purposes as purpose}
- {purpose.story}
- {/each}
-
-
-
- The request will carry {selectedPurposeView?.slug ?? 'no-purpose-selected'}.
-
-
askUnderPurpose(selectedPurpose)} disabled={reasking || !positivePreview}>
- Test this purpose
-
-
-
- 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 b723e1e..1a56a41 100644
--- a/home/src/lib/components/TrustStrip.svelte
+++ b/home/src/lib/components/TrustStrip.svelte
@@ -45,8 +45,8 @@
Pinned release
-
Relay {shortDigest(versions.REGISTRY_RELAY_IMAGE)}
-
Notary {shortDigest(versions.REGISTRY_NOTARY_IMAGE)}
+
Registry Stack {versions.REGISTRY_STACK_SOURCE_REF || 'missing'} {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..e95d8a0 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://authority-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://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.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..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();
@@ -64,7 +65,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 +74,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 +91,13 @@ 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) ?? 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 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 b9b7bee..ef28a42 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,14 @@ describe('home data adapters', () => {
expect(services).toContain('static-metadata');
});
- it('reads pinned versions for the trust strip', async () => {
+ it('publishes the exact Registry Stack release identity', async () => {
const versions = await readVersions();
- expect(versions.REGISTRY_RELAY_IMAGE).toMatch(/@sha256:/);
- expect(versions.REGISTRY_NOTARY_IMAGE).toMatch(/@sha256:/);
+ 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}$/);
+ 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 () => {
@@ -27,7 +31,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 870028c..9e3dd7e 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 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..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 actually observed in Notary
+ * 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
@@ -12,20 +12,27 @@ 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. The authority Evidence service 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. 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.'
}
};
@@ -63,12 +70,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..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 |
|---|---|---|---|---|
-| \`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 | 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: []
};
@@ -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('Authority Evidence cells');
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..f58dba7 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,11 +16,22 @@ 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) && [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,
@@ -61,15 +72,16 @@ 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 body = preview?.body && typeof preview.body === 'object' ? preview.body as Record : {};
+ 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 19f856c..0eeca14 100644
--- a/home/src/lib/server/services.test.ts
+++ b/home/src/lib/server/services.test.ts
@@ -1,110 +1,39 @@
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', () => {
- 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);
- });
-
- it('probes the federator on health, notaries on claims, and relays on their gated root', () => {
- 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', () => {
+describe('authority-cell topology', () => {
+ it('has exactly five Relays, six Evidence cells, and one shared Mint', () => {
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.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('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 === 'child-benefit-federator')?.probeUrl).toBe(
- 'http://child-benefit-federator:8080/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');
+ 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('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(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('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);
- });
-});
-
-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']);
- 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');
+ it('links only browser-facing services', () => {
+ const probes = statusProbes('http://portal.example');
+ 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 === 'cra-evidence')?.href).toBeUndefined();
});
- 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('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 a4d4e71..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 relays and notaries are reachable for probing from inside 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';
type ServiceDef = {
@@ -32,244 +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 the live notaries.',
- 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: '/health',
- semantics: 'health',
- configPaths: [{ label: 'Federator service', path: 'scenario-runner/child_benefit_federator.py' }]
- },
- {
- id: 'cra-notary',
- label: 'CRA notary',
- role: 'notary',
+ probePath: '/ready',
+ configPaths: [{ label: 'Relay V2 project', path: 'relays/nagdi' }]
+ }),
+ evidence({
+ id: 'cra-evidence',
+ label: 'CRA Evidence',
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',
+ 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',
+ configPaths: [{ label: 'Authority Evidence cell', path: 'evidence/cells/cra' }]
+ }),
+ evidence({
+ id: 'nia-evidence',
+ label: 'NIA Evidence',
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',
+ blurb: 'Issues NIA-signed population status values.',
+ probeEnv: 'SOLMARA_NIA_EVIDENCE_URL',
+ probeDefault: 'https://nia-evidence.solmara.registrystack.org',
+ probePath: '/health',
+ configPaths: [{ label: 'Authority Evidence cell', path: 'evidence/cells/nia' }]
+ }),
+ evidence({
+ id: 'sro-evidence',
+ label: 'SRO Evidence',
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',
+ 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',
- 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',
+ 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',
- 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: 'nagdi-notary',
- label: 'NAgDI notary',
- role: 'notary',
+ 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',
- 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' }]
- }
+ 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',
+ 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 = {
@@ -281,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: relays and Notaries under their
- * authority, and shared services such as the child-benefit evidence collector.
- * 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 notaries = SERVICES.filter((service) => service.role === 'notary');
+ 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: '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: 'notaries',
- title: 'Source-owned Notaries',
- 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))
+ 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 66cf3ab..4ecc12b 100644
--- a/home/src/lib/server/tokens.test.ts
+++ b/home/src/lib/server/tokens.test.ts
@@ -1,205 +1,32 @@
import { describe, expect, it } from 'vitest';
-import { buildCurlExamples, parsePublishedTokens, publishRequestTokens } from './tokens';
+import { buildCurlExamples } 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']);
- });
-
- 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.
- 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([]);
- });
+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 four curl examples including the skeptic wrong-purpose call', () => {
- const tokens = parsePublishedTokens('{"child-benefit-federator":"tok-child"}');
- const examples = buildCurlExamples(tokens);
expect(examples.map((example) => example.id)).toEqual([
'metadata-get',
- 'claims-get',
- 'evaluate-post',
+ 'cra-evidence-post',
+ 'nia-evidence-post',
+ 'programme-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');
- });
-
- 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', () => {
- 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'
- }
- },
- 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]' }
- }
- ]
- },
- 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(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('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 1d695be..d8bb1c2 100644
--- a/home/src/lib/server/tokens.ts
+++ b/home/src/lib/server/tokens.ts
@@ -1,234 +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 = '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 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]
- }
-};
-
-/**
- * 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.'
- },
- '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.'
- }
-};
-
-/**
- * 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 notary.'
- });
- }
- return tokens;
-}
/**
- * 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 1286bbd..7a23124 100644
--- a/home/src/lib/server/urlmap.test.ts
+++ b/home/src/lib/server/urlmap.test.ts
@@ -2,104 +2,35 @@ 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('keeps each public authority Evidence origin distinct', () => {
const map = buildPublicUrlMap();
- expect(mapPublicUrl('http://child-benefit-federator:8080/v1/evaluations', map)).toBe(
- 'http://localhost:4321/v1/evaluations'
- );
- 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'
- ]);
+ 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 metadata 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://static-metadata:8080/metadata/catalog.json', map)).toBe(
- 'http://localhost:4331/metadata/catalog.json'
- );
+ 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 the origin', () => {
- const map = buildPublicUrlMap();
- expect(mapPublicUrl('http://cra-notary:8081/v1/evaluations?trace=1', map)).toBe(
- 'http://localhost:4325/v1/evaluations?trace=1'
- );
- });
-
- it('leaves already host-reachable URLs 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 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 request_source and credential_source urls inside a run result', () => {
+ it('rewrites nested 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: '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('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).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 aa50027..eb6e750 100644
--- a/home/src/lib/server/urlmap.ts
+++ b/home/src/lib/server/urlmap.ts
@@ -15,18 +15,19 @@ 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',
- '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',
+ '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-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 9e21db1..f4a6839 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;
@@ -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;
@@ -108,8 +101,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 +130,10 @@ export type Scenario = {
export type RequestSource = {
method: string;
url: string;
- headers: Record;
+ headers?: Record;
+ purpose?: string;
body?: unknown;
+ requests?: RequestSource[];
};
export type ResponseSource = {
@@ -186,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 = {
@@ -196,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;
@@ -213,6 +236,7 @@ export type MetadataBundle = {
datasets: MetadataDataset[];
gray_registries: GrayRegistry[];
authorities: MetadataAuthority[];
+ data_services: MetadataDataService[];
public_services: MetadataPublicService[];
};
offerings: MetadataOffering[];
@@ -240,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;
};
@@ -266,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 6c9c369..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.
Metadata explorer
@@ -33,7 +33,6 @@
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 @@
On this page
Catalog
- Datasets
Public services
Evidence offerings
Policies
@@ -93,39 +92,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}
- 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 2aaaaed..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 the Notary 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 7167187..0be2f77 100644
--- a/justfile
+++ b/justfile
@@ -1,192 +1,129 @@
-set dotenv-load := true
-set positional-arguments := true
+set dotenv-load
+set positional-arguments
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
+ 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
-# Generate a clean checkout, verify compiler output, and start the local topology.
-up-generated:
- just generate
- just registry-projects-runtime-check
- just up
+build-runtime-images:
+ scripts/build-registry-stack-runtime.sh
-# Validate every authority-owned Registry project in both deployment profiles.
-registry-projects-check:
- scripts/registry-projects.sh check
+# 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
-# Print the complete redacted acquisition and disclosure plan for every authority.
-registry-projects-review:
- scripts/registry-projects.sh review
+prepare: generate prepare-runtime
-# 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 Relay and Notary inputs for every authority-owned 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
-
-# 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
-
-# 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 --exclude vendor .
scripts/check-fiction.sh
- scripts/check-image-pins.py
scripts/check-config-secrets.py
- just hosted-relay-bundles-check
+ scripts/check-image-pins.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 discover -s scripts -p 'test_*.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_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
-# 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.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
-
-# 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
-
-# Build Registry Relay from the pinned source for explicit local development.
-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
-
-# 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
+ @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
+
+# 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
+
+# Run all four relayctl production gates against the five authored projects.
+relay-check: build-runtime-images
+ scripts/check-relay-projects.sh
+
+# 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:
- @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
-# Build Registry Relay from the pinned source for explicit local eSignet development.
-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
+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 --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 --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
+ 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
-
-# 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:
+ @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 Relay source endpoints used by live Notary smoke.
-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 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
+ 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/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/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 @@
-
-
-
+
{#each inFlightTraces as trace (trace.id)}
-
+
- {authorityIcon(trace.authority)}
- {trace.id}
-
- {formatTs(trace.ts)}
+ {authorityIcon(trace.authority)}
+ {trace.id}
+ In flight
+ {formatTs(trace.ts)}
- {trace.headline}
-
- Not disclosed:
- {trace.notDisclosed}
-
-
+
{trace.headline}
+
Not disclosed: {trace.notDisclosed}
+
{/each}
-
{#each mainTraces as trace (trace.id)}
- {@const isActive = trace.id === activeTraceId}
- {@const d2open = expandedDepth2[trace.id] ?? false}
- {@const d3open = expandedDepth3[trace.id] ?? false}
- {@const rawOpen = expandedRaw[trace.id] ?? false}
- {@const flashVerify = verifyFlash[trace.id] ?? false}
- {@const copied = copiedCurl[trace.id] ?? false}
-
-
+
- {authorityIcon(trace.authority)}
- {trace.id}
-
- {statusLabel(trace)}
-
- {formatTs(trace.ts)}
+ {authorityIcon(trace.authority)}
+ {trace.id}
+ {statusLabel(trace)}
+ {formatTs(trace.ts)}
-
- {trace.headline}
- {trace.answered}
-
-
-
- Not disclosed:
- {trace.notDisclosed}
-
-
- {#if trace.fieldId}
-
- ↳ for field
- "{trace.fieldId}"
-
- {/if}
-
-
- {#if trace.response}
- toggleDepth2(trace.id)}
- aria-expanded={d2open}
- aria-controls="depth2-{trace.id}"
- >
- {d2open ? '▾' : '▸'}
- Request and response
- {#if !copied}
- { e.stopPropagation(); copyCurl(trace); }}
- onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); copyCurl(trace); } }}
- aria-label="Copy request as curl command"
- >
- [Copy as curl]
-
- {:else}
- Copied
- {/if}
-
-
- {#if d2open}
-
-
- {trace.request.method} {trace.request.url}
-
-
- x-api-key: •••••••• (redacted)
-
-
{formatJson(trace.request.body)}
-
- {#if trace.response}
-
- {trace.response.status} {trace.response.status === 200 ? 'OK' : trace.response.status === 403 ? 'Forbidden' : ''}
-
-
{formatJson(trace.response.body)}
- {/if}
-
-
- {#if trace.proof}
-
toggleDepth3(trace.id)}
- aria-expanded={d3open}
- aria-controls="depth3-{trace.id}"
- >
- {d3open ? '▾' : '▸'}
- Cryptographic proof (signature, issuer, artifact)
-
-
- {#if d3open}
-
-
-
-
- Signed by
- {trace.proof.signedBy}
-
-
- Algorithm
- {trace.proof.algorithm}
-
-
- Issuer key
- {trace.proof.issuerKey}
-
-
- Holder bound
- {trace.proof.holderBound}
-
-
- Evidence artifact
- {trace.proof.credential}
-
-
- Audit id
- {trace.proof.auditId}
-
-
-
-
-
- verifyAgain(trace.id)}
- aria-label="Verify cryptographic proof again"
- >
- [verify again]
-
- {#if flashVerify}
-
- ✓ valid
-
- {/if}
-
-
- {#if trace.proof.credential === 'SD-JWT VC'}
-
toggleRaw(trace.id)}
- aria-expanded={rawOpen}
- aria-controls="raw-{trace.id}"
- >
- {rawOpen ? '▾' : '▸'}
- Raw SD-JWT
-
- {#if rawOpen}
-
-
eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCJ9.+[synthetic-demo-data]+.+_disclosure_+
-
- (Synthetic demo data - not a real credential)
-
-
- {/if}
- {/if}
-
- {/if}
- {/if}
-
- {/if}
- {/if}
-
- {/each}
-
-
- {#if identityTrace}
- {@const trace = identityTrace}
- {@const d2open = expandedDepth2[trace.id] ?? false}
- {@const d3open = expandedDepth3[trace.id] ?? false}
- {@const rawOpen = expandedRaw[trace.id] ?? false}
- {@const flashVerify = verifyFlash[trace.id] ?? false}
- {@const copied = copiedCurl[trace.id] ?? false}
-
-
- {authorityIcon(trace.authority)}
- {trace.id}
- Identity foundation
- {formatTs(trace.ts)}
-
-
{trace.headline}
-
{trace.answered}
-
-
- Not disclosed:
- {trace.notDisclosed}
-
+
{trace.answered}
+
Not disclosed: {trace.notDisclosed}
+ {#if trace.fieldId}
↳ for field “{trace.fieldId}”
{/if}
toggleDepth2(trace.id)}
- aria-expanded={d2open}
- aria-controls="depth2-{trace.id}"
+ type="button"
+ aria-expanded={expanded[trace.id] ?? false}
+ onclick={() => (expanded[trace.id] = !(expanded[trace.id] ?? false))}
>
- {d2open ? '▾' : '▸'}
- Request and response
- {#if !copied}
- { e.stopPropagation(); copyCurl(trace); }}
- onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); copyCurl(trace); } }}
- aria-label="Copy request as curl command"
- >
- [Copy as curl]
-
- {:else}
- Copied
- {/if}
+ {(expanded[trace.id] ?? false) ? '▾' : '▸'} Authority evidence
- {#if d2open}
-
-
- {trace.request.method} {trace.request.url}
-
-
- x-api-key: •••••••• (redacted)
-
-
{formatJson(trace.request.body)}
- {#if trace.response}
-
- {trace.response.status} OK
-
-
{formatJson(trace.response.body)}
+ {#if expanded[trace.id] ?? false}
+
+ {#if trace.purpose}Purpose: {trace.purpose}
{/if}
+ {#if trace.responseStatus}Result status: {trace.responseStatus}
{/if}
+ Stable result state: {trace.resultState}
+ {#if trace.presentations.length === 0}
+ No authority Evidence service was called.
+ {:else}
+
+ {#each trace.presentations as presentation}
+
+ {presentation.authority}
+ Issuer: {presentation.issuer}
+ Evidence service: {presentation.serviceId}
+ Source: {presentation.source}
+
+ {/each}
+
{/if}
-
{#if trace.proof}
- toggleDepth3(trace.id)}
- aria-expanded={d3open}
- aria-controls="depth3-{trace.id}"
- >
- {d3open ? '▾' : '▸'}
- Cryptographic proof (signature, issuer, artifact)
-
-
- {#if d3open}
-
-
-
-
- Signed by
- {trace.proof.signedBy}
-
-
- Algorithm
- {trace.proof.algorithm}
-
-
- Issuer key
- {trace.proof.issuerKey}
-
-
- Holder bound
- {trace.proof.holderBound}
-
-
- Evidence artifact
- {trace.proof.credential}
-
-
- Audit id
- {trace.proof.auditId}
-
-
-
-
-
- verifyAgain(trace.id)}
- aria-label="Verify cryptographic proof again"
- >
- [verify again]
-
- {#if flashVerify}
-
- ✓ valid
-
- {/if}
-
-
- {#if trace.proof.credential === 'SD-JWT VC'}
-
toggleRaw(trace.id)}
- aria-expanded={rawOpen}
- aria-controls="raw-{trace.id}"
- >
- {rawOpen ? '▾' : '▸'}
- Raw SD-JWT
-
- {#if rawOpen}
-
-
eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCJ9.+[synthetic-demo-data]+.+_disclosure_+
-
(Synthetic demo data - not a real credential)
-
- {/if}
- {/if}
-
- {/if}
+
+ Verified as {trace.proof.credential}
+ Issued by {trace.proof.signedBy}
+ Verification {trace.proof.algorithm}
+ Key discovery {trace.proof.issuerKey}
+ Bound to {trace.proof.holderBound}
+
{/if}
-
+
{/if}
-
+
+ {/each}
+
+ {#if identityTrace}
+
+ 🪪 {identityTrace.id} Identity
+ {identityTrace.headline}
+ {identityTrace.answered}
+ Not disclosed: {identityTrace.notDisclosed}
+
+ {/if}
+
+ {#if traces.length === 0}
+
No proof entries yet. Open a service to begin.
{/if}
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 fd49e3f..daa081f 100644
--- a/portal/src/lib/providers/live/index.test.ts
+++ b/portal/src/lib/providers/live/index.test.ts
@@ -1,793 +1,109 @@
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'
+ 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'
},
- 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'
+ ...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 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(
- { 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({});
+ expect(evaluation.result.state).toBe('verified');
+ expect(evaluation.proof.crypto.algorithm).toBe('Verified Evidence assertion');
+ 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..c5326bb 100644
--- a/portal/src/lib/providers/live/index.ts
+++ b/portal/src/lib/providers/live/index.ts
@@ -1,343 +1,182 @@
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 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 = {
- 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;
+ 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 authority 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, 'not_authorized');
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)));
+ 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 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,
+ 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),
+ result: {
+ state: proofStatus === 'ok' ? scenario.state : proofStatus === 'false' ? 'false' : 'error',
+ display,
+ ...(!applicationOwned ? { authority: scenario.authority } : {}),
+ traceId: `event ${seq}`
+ },
raw: {
- request: { method: 'POST', url, body: request },
- response: { status: response.status, body: responseBody }
+ request: {
+ method: calls.length > 1 ? 'MULTI' : 'POST',
+ url: `${requiredRunnerUrl(this.#runnerUrl)}/v1/scenarios`,
+ body: requestBody
+ },
+ response: { status: typeof status === 'number' ? status : 503, body: responseBody }
},
proof: {
headline: scenario.headline,
- answered: childAnswered(attributed ? childBody : undefined, scenario.claimId),
+ answered: outcome.found
+ ? `${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,
- 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.authority,
+ purpose: scenario.purpose,
+ presentations,
+ crypto: evidenceProof(presentations, proofStatus)
},
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): 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({})
});
- 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(() => ({})));
+ // 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: {
+ type: 'https://registrystack.org/problems/evidence/service_unavailable',
+ title: 'Authority Evidence is unavailable',
+ status: response.status
+ }
+ }
+ };
+ }
+ 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 = {
+ 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',
- 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 }
},
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 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; 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}`
+ holderBound: 'Portal session and server-selected subject',
+ credential: 'No credential or evidence assertion returned'
}
},
timing: { latencyMs: 0, staggerOrder: scenario.staggerOrder, slow: false }
@@ -345,515 +184,147 @@ 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 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
- };
+function responseResults(response: RunnerResult): Dict[] {
+ if (Array.isArray(response.results)) {
+ return response.results.map(asDict).filter((item) => Object.keys(item).length > 0);
}
- 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
- };
+ const body = asDict(response.response_source?.body);
+ return Array.isArray(body.results) ? body.results.map(asDict).filter((item) => Object.keys(item).length > 0) : [];
}
-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;
+ 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 } };
}
- 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;
+ 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 } };
}
- 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;
+ 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 } };
}
- 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 } : {})
- };
+ 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 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;
- }
+function evidenceProof(
+ presentations: EvidencePresentation[],
+ status: ProofTrace['status']
+): NonNullable {
+ const authorities = [...new Set(presentations.map((item) => item.authority))];
+ const returned = status === 'ok' || status === 'false';
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: 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 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 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);
-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;
+ 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 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 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 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 isPresentation(value: EvidencePresentation | null): value is EvidencePresentation {
+ return value !== null;
}
-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 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 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`;
+function sourceForScenario(scenario: (typeof SCENARIOS)[string]): EvidenceSource {
+ if (scenario.service === 'childBenefit' && ['childCivil', 'population', 'socialRegistry'].includes(scenario.authority)) {
+ return 'immutable extract';
}
- if (status === 'ok' && satisfied === true) return scenario.display;
- return `${scenario.claimId}: ${displayValue(value, satisfied)}`;
+ return 'Relay lookup';
}
-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 {
+ // The runtime guard establishes the dictionary shape used by safe readers.
+ 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/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/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/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 @@
-
-
+
(auditOpen = !auditOpen)}
+ data-testid="proof-log-toggle"
+ aria-expanded={proofLogOpen}
+ aria-controls="proof-log"
+ onclick={() => (proofLogOpen = !proofLogOpen)}
>
- Proof audit log
+ Proof log
{clientFeed.traces.length}
- {auditOpen ? '▾' : '▴'}
- {auditOpen ? 'collapse' : 'expand'}
+ {proofLogOpen ? '▾' : '▴'}
+ {proofLogOpen ? 'collapse' : 'expand'}
-
+
@@ -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 f5fdebd..0000000
--- a/projects/cra-civil/environments/hosted.yaml
+++ /dev/null
@@ -1,44 +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
-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
-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
deleted file mode 100644
index 70299bb..0000000
--- a/projects/cra-civil/environments/local.yaml
+++ /dev/null
@@ -1,49 +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
-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
-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
deleted file mode 100644
index a114369..0000000
--- a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/future-birth-date.yaml
+++ /dev/null
@@ -1,32 +0,0 @@
-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:
- - 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
- 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
deleted file mode 100644
index 9576003..0000000
--- a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/match.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-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:
- - 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
- 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
deleted file mode 100644
index 364dfc2..0000000
--- a/projects/cra-civil/integrations/civil-person-by-uin/fixtures/no-match.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-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:
- - expect: { method: GET, path: /snapshot }
- respond: { status: 200, body: [] }
-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/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 db54e87..0000000
--- a/projects/cra-civil/registry-stack.yaml
+++ /dev/null
@@ -1,78 +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-child-benefit:
- kind: evidence
- 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/.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 26fb468..0000000
--- a/projects/mosd-programme/environments/hosted.yaml
+++ /dev/null
@@ -1,42 +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"
-
-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
-
-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
deleted file mode 100644
index 4e3aca0..0000000
--- a/projects/mosd-programme/environments/local.yaml
+++ /dev/null
@@ -1,48 +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"
-
-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
-
-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
deleted file mode 100644
index b063bc4..0000000
--- a/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/match.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-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 }
- respond: { status: 200, body: { duplicate_flag: false } }
-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
deleted file mode 100644
index 990e5e0..0000000
--- a/projects/mosd-programme/integrations/enrollment-by-uin/fixtures/no-match.yaml
+++ /dev/null
@@ -1,10 +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: {}
- claims: { not-already-enrolled: false }
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 341ccb5..0000000
--- a/projects/mosd-programme/registry-stack.yaml
+++ /dev/null
@@ -1,33 +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:
- child-benefit:
- kind: evidence
- 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/.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 e7a65f1..0000000
--- a/projects/nagdi-agriculture/environments/hosted.yaml
+++ /dev/null
@@ -1,70 +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"
-
-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
-
-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
deleted file mode 100644
index 9ae1c1d..0000000
--- a/projects/nagdi-agriculture/environments/local.yaml
+++ /dev/null
@@ -1,76 +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"
-
-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
-
-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
deleted file mode 100644
index 4e1f809..0000000
--- a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/match.yaml
+++ /dev/null
@@ -1,42 +0,0 @@
-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 }
- 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
- 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
deleted file mode 100644
index 9ba7e3b..0000000
--- a/projects/nagdi-agriculture/integrations/farmer-voucher-by-farmer/fixtures/no-match.yaml
+++ /dev/null
@@ -1,13 +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: {}
- claims:
- farmer-registered: false
- data-use-authorized-for-purpose: false
- eligible-for-climate-smart-input-voucher: false
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 bdf033c..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: {}, claims: {} }
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 18c87e1..0000000
--- a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/match.yaml
+++ /dev/null
@@ -1,36 +0,0 @@
-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 }
- 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
- 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
deleted file mode 100644
index 10d7312..0000000
--- a/projects/nagdi-agriculture/integrations/livestock-movement-by-farmer/fixtures/no-match.yaml
+++ /dev/null
@@ -1,13 +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: {}
- claims:
- registered-herd: false
- origin-district-not-quarantined-for-species: false
- eligible-for-livestock-movement-permit: false
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 0333d7b..0000000
--- a/projects/nagdi-agriculture/registry-stack.yaml
+++ /dev/null
@@ -1,108 +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:
- voucher:
- kind: evidence
- 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
- 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
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 d542939..0000000
--- a/projects/nia-population/environments/hosted.yaml
+++ /dev/null
@@ -1,51 +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
-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
-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
deleted file mode 100644
index 6df2457..0000000
--- a/projects/nia-population/environments/local.yaml
+++ /dev/null
@@ -1,56 +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
-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
-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
deleted file mode 100644
index 11438f3..0000000
--- a/projects/nia-population/integrations/population-person-by-uin/fixtures/match.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-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 }
- respond:
- status: 200
- body: { identity_status: active, alive: true }
-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
deleted file mode 100644
index 4bf76c4..0000000
--- a/projects/nia-population/integrations/population-person-by-uin/fixtures/no-match.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-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 }
- respond: { status: 200, body: [] }
-expect:
- outcome: no_match
- outputs: {}
- claims:
- population-record-active: false
- citizen-population-record-active: false
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 60e0cd3..0000000
--- a/projects/nia-population/registry-stack.yaml
+++ /dev/null
@@ -1,129 +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
- 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-child-benefit:
- kind: evidence
- 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/.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 9977b87..0000000
--- a/projects/sipf-pensions/environments/hosted.yaml
+++ /dev/null
@@ -1,55 +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
-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
-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
deleted file mode 100644
index ccebf5f..0000000
--- a/projects/sipf-pensions/environments/local.yaml
+++ /dev/null
@@ -1,60 +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
-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
-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
deleted file mode 100644
index 7ae1bef..0000000
--- a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/ambiguous.yaml
+++ /dev/null
@@ -1,14 +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: {}
- 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
deleted file mode 100644
index 9c7695a..0000000
--- a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/match.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-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 }
- respond: { status: 200, body: { payment_status: active } }
-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
deleted file mode 100644
index 8dc5416..0000000
--- a/projects/sipf-pensions/integrations/pension-case-by-pensioner/fixtures/no-match.yaml
+++ /dev/null
@@ -1,10 +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: {}
- claims: { pension-payment-active: false }
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 b364d2b..0000000
--- a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/ambiguous.yaml
+++ /dev/null
@@ -1,14 +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: {}
- 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
deleted file mode 100644
index 1bdd88b..0000000
--- a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/match.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-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 }
- respond: { status: 200, body: { survivor_eligible: true } }
-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
deleted file mode 100644
index ae04744..0000000
--- a/projects/sipf-pensions/integrations/pension-case-by-survivor/fixtures/no-match.yaml
+++ /dev/null
@@ -1,10 +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: {}
- claims: { survivor-is-eligible: false }
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 ed64b25..0000000
--- a/projects/sipf-pensions/registry-stack.yaml
+++ /dev/null
@@ -1,59 +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-payment-review:
- 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: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
- 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]
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 b2888ea..0000000
--- a/projects/sro-social/environments/hosted.yaml
+++ /dev/null
@@ -1,42 +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"
-
-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
-
-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
deleted file mode 100644
index 2e494bd..0000000
--- a/projects/sro-social/environments/local.yaml
+++ /dev/null
@@ -1,48 +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"
-
-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
-
-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
deleted file mode 100644
index ad3f15f..0000000
--- a/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/match.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-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 }
- respond: { status: 200, body: { poverty_band: priority } }
-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
deleted file mode 100644
index 6092d49..0000000
--- a/projects/sro-social/integrations/child-benefit-household-by-uin/fixtures/no-match.yaml
+++ /dev/null
@@ -1,10 +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: {}
- claims: { household-below-poverty-threshold: false }
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 7e7974b..0000000
--- a/projects/sro-social/registry-stack.yaml
+++ /dev/null
@@ -1,33 +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:
- child-benefit:
- kind: evidence
- 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/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/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/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/evidence/civil-person-by-uin/minimization.json b/runtime/registry-projects/hosted/cra-civil/relay/artifacts/evidence/civil-person-by-uin/minimization.json
deleted file mode 100644
index 9d92e3c..0000000
--- a/runtime/registry-projects/hosted/cra-civil/relay/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/artifacts/evidence/civil-person-by-uin/negative-security.json b/runtime/registry-projects/hosted/cra-civil/relay/artifacts/evidence/civil-person-by-uin/negative-security.json
deleted file mode 100644
index 18ba42a..0000000
--- a/runtime/registry-projects/hosted/cra-civil/relay/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/artifacts/integration-packs/civil-person-by-uin.json b/runtime/registry-projects/hosted/cra-civil/relay/artifacts/integration-packs/civil-person-by-uin.json
deleted file mode 100644
index 5533854..0000000
--- a/runtime/registry-projects/hosted/cra-civil/relay/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/runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-child-benefit-civil.json b/runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-child-benefit-civil.json
deleted file mode 100644
index 329227a..0000000
--- a/runtime/registry-projects/hosted/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/hosted/cra-civil/relay/artifacts/private-bindings/cra-citizen-record-civil.json b/runtime/registry-projects/hosted/cra-civil/relay/artifacts/private-bindings/cra-citizen-record-civil.json
deleted file mode 100644
index ea8fb47..0000000
--- a/runtime/registry-projects/hosted/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/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-consultation.yaml b/runtime/registry-projects/hosted/cra-civil/relay/relay-consultation.yaml
deleted file mode 100644
index e82fd3d..0000000
--- a/runtime/registry-projects/hosted/cra-civil/relay/relay-consultation.yaml
+++ /dev/null
@@ -1,138 +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: {}
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 66bd9da..0000000
--- a/runtime/registry-projects/hosted/cra-civil/relay/relay.yaml
+++ /dev/null
@@ -1,83 +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: {}
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/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/artifacts/evidence/enrollment-by-uin/minimization.json b/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/minimization.json
deleted file mode 100644
index 1d539e7..0000000
--- a/runtime/registry-projects/hosted/mosd-programme/relay/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/artifacts/evidence/enrollment-by-uin/negative-security.json b/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/negative-security.json
deleted file mode 100644
index 0b38f83..0000000
--- a/runtime/registry-projects/hosted/mosd-programme/relay/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/artifacts/integration-packs/enrollment-by-uin.json b/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/integration-packs/enrollment-by-uin.json
deleted file mode 100644
index 729f6a0..0000000
--- a/runtime/registry-projects/hosted/mosd-programme/relay/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/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json b/runtime/registry-projects/hosted/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json
deleted file mode 100644
index 078b94e..0000000
--- a/runtime/registry-projects/hosted/mosd-programme/relay/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/runtime/registry-projects/hosted/mosd-programme/relay/relay-consultation.yaml b/runtime/registry-projects/hosted/mosd-programme/relay/relay-consultation.yaml
deleted file mode 100644
index 63d65e5..0000000
--- a/runtime/registry-projects/hosted/mosd-programme/relay/relay-consultation.yaml
+++ /dev/null
@@ -1,118 +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: {}
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 915fc47..0000000
--- a/runtime/registry-projects/hosted/mosd-programme/relay/relay.yaml
+++ /dev/null
@@ -1,76 +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: {}
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/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/farmer-voucher-by-farmer/minimization.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/minimization.json
deleted file mode 100644
index 59e808a..0000000
--- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/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/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json
deleted file mode 100644
index 7dfbf8c..0000000
--- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/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/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/artifacts/evidence/livestock-movement-by-farmer/minimization.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/minimization.json
deleted file mode 100644
index 6768a09..0000000
--- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/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/artifacts/evidence/livestock-movement-by-farmer/negative-security.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/negative-security.json
deleted file mode 100644
index a823e60..0000000
--- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/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/artifacts/integration-packs/farmer-voucher-by-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/integration-packs/farmer-voucher-by-farmer.json
deleted file mode 100644
index ff96512..0000000
--- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/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/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/integration-packs/livestock-movement-by-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/integration-packs/livestock-movement-by-farmer.json
deleted file mode 100644
index 043c50a..0000000
--- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/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/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/private-bindings/livestock-herd.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/private-bindings/livestock-herd.json
deleted file mode 100644
index 7db8adf..0000000
--- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/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/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/private-bindings/voucher-farmer.json b/runtime/registry-projects/hosted/nagdi-agriculture/relay/artifacts/private-bindings/voucher-farmer.json
deleted file mode 100644
index eddcecb..0000000
--- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/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/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
deleted file mode 100644
index d8d88d5..0000000
--- a/runtime/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml
+++ /dev/null
@@ -1,174 +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: {}
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/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/artifacts/evidence/population-person-by-uin/minimization.json b/runtime/registry-projects/hosted/nia-population/relay/artifacts/evidence/population-person-by-uin/minimization.json
deleted file mode 100644
index 223b391..0000000
--- a/runtime/registry-projects/hosted/nia-population/relay/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/artifacts/evidence/population-person-by-uin/negative-security.json b/runtime/registry-projects/hosted/nia-population/relay/artifacts/evidence/population-person-by-uin/negative-security.json
deleted file mode 100644
index 53dcfa4..0000000
--- a/runtime/registry-projects/hosted/nia-population/relay/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/artifacts/integration-packs/population-person-by-uin.json b/runtime/registry-projects/hosted/nia-population/relay/artifacts/integration-packs/population-person-by-uin.json
deleted file mode 100644
index 7430026..0000000
--- a/runtime/registry-projects/hosted/nia-population/relay/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/runtime/registry-projects/hosted/nia-population/relay/artifacts/private-bindings/nia-child-benefit-population.json b/runtime/registry-projects/hosted/nia-population/relay/artifacts/private-bindings/nia-child-benefit-population.json
deleted file mode 100644
index 9d14494..0000000
--- a/runtime/registry-projects/hosted/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/hosted/nia-population/relay/artifacts/private-bindings/nia-citizen-status-population.json b/runtime/registry-projects/hosted/nia-population/relay/artifacts/private-bindings/nia-citizen-status-population.json
deleted file mode 100644
index 735159f..0000000
--- a/runtime/registry-projects/hosted/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/hosted/nia-population/relay/relay-consultation.yaml b/runtime/registry-projects/hosted/nia-population/relay/relay-consultation.yaml
deleted file mode 100644
index 8b7c257..0000000
--- a/runtime/registry-projects/hosted/nia-population/relay/relay-consultation.yaml
+++ /dev/null
@@ -1,251 +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: {}
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 49e1a9b..0000000
--- a/runtime/registry-projects/hosted/nia-population/relay/relay.yaml
+++ /dev/null
@@ -1,203 +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: {}
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/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-pensioner/minimization.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/minimization.json
deleted file mode 100644
index 27b99c4..0000000
--- a/runtime/registry-projects/hosted/sipf-pensions/relay/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/artifacts/evidence/pension-case-by-pensioner/negative-security.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/negative-security.json
deleted file mode 100644
index c62fd58..0000000
--- a/runtime/registry-projects/hosted/sipf-pensions/relay/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/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/artifacts/evidence/pension-case-by-survivor/minimization.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/minimization.json
deleted file mode 100644
index e246e22..0000000
--- a/runtime/registry-projects/hosted/sipf-pensions/relay/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/artifacts/evidence/pension-case-by-survivor/negative-security.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/negative-security.json
deleted file mode 100644
index 72f6ab7..0000000
--- a/runtime/registry-projects/hosted/sipf-pensions/relay/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/artifacts/integration-packs/pension-case-by-pensioner.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-pensioner.json
deleted file mode 100644
index 7fac5ec..0000000
--- a/runtime/registry-projects/hosted/sipf-pensions/relay/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/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-survivor.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-survivor.json
deleted file mode 100644
index a0e0dda..0000000
--- a/runtime/registry-projects/hosted/sipf-pensions/relay/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/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json
deleted file mode 100644
index 2b3b78b..0000000
--- a/runtime/registry-projects/hosted/sipf-pensions/relay/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/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json b/runtime/registry-projects/hosted/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json
deleted file mode 100644
index 8485dcd..0000000
--- a/runtime/registry-projects/hosted/sipf-pensions/relay/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/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
deleted file mode 100644
index 5a2c71f..0000000
--- a/runtime/registry-projects/hosted/sipf-pensions/relay/relay.yaml
+++ /dev/null
@@ -1,121 +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: {}
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/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/artifacts/evidence/child-benefit-household-by-uin/minimization.json b/runtime/registry-projects/hosted/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/minimization.json
deleted file mode 100644
index fc5378a..0000000
--- a/runtime/registry-projects/hosted/sro-social/relay/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/artifacts/evidence/child-benefit-household-by-uin/negative-security.json b/runtime/registry-projects/hosted/sro-social/relay/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/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/artifacts/integration-packs/child-benefit-household-by-uin.json b/runtime/registry-projects/hosted/sro-social/relay/artifacts/integration-packs/child-benefit-household-by-uin.json
deleted file mode 100644
index 7133664..0000000
--- a/runtime/registry-projects/hosted/sro-social/relay/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/runtime/registry-projects/hosted/sro-social/relay/artifacts/private-bindings/child-benefit-household.json b/runtime/registry-projects/hosted/sro-social/relay/artifacts/private-bindings/child-benefit-household.json
deleted file mode 100644
index e46d4f8..0000000
--- a/runtime/registry-projects/hosted/sro-social/relay/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/runtime/registry-projects/hosted/sro-social/relay/relay-consultation.yaml b/runtime/registry-projects/hosted/sro-social/relay/relay-consultation.yaml
deleted file mode 100644
index dcacafe..0000000
--- a/runtime/registry-projects/hosted/sro-social/relay/relay-consultation.yaml
+++ /dev/null
@@ -1,118 +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: {}
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 fe45cb9..0000000
--- a/runtime/registry-projects/hosted/sro-social/relay/relay.yaml
+++ /dev/null
@@ -1,76 +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: {}
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/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/evidence/civil-person-by-uin/minimization.json b/runtime/registry-projects/local/cra-civil/relay/artifacts/evidence/civil-person-by-uin/minimization.json
deleted file mode 100644
index 9d92e3c..0000000
--- a/runtime/registry-projects/local/cra-civil/relay/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/artifacts/evidence/civil-person-by-uin/negative-security.json b/runtime/registry-projects/local/cra-civil/relay/artifacts/evidence/civil-person-by-uin/negative-security.json
deleted file mode 100644
index 18ba42a..0000000
--- a/runtime/registry-projects/local/cra-civil/relay/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/artifacts/integration-packs/civil-person-by-uin.json b/runtime/registry-projects/local/cra-civil/relay/artifacts/integration-packs/civil-person-by-uin.json
deleted file mode 100644
index 5533854..0000000
--- a/runtime/registry-projects/local/cra-civil/relay/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/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-consultation.yaml b/runtime/registry-projects/local/cra-civil/relay/relay-consultation.yaml
deleted file mode 100644
index 797c8b3..0000000
--- a/runtime/registry-projects/local/cra-civil/relay/relay-consultation.yaml
+++ /dev/null
@@ -1,138 +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:
- - cra-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-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: local
-instance:
- environment: local
- id: cra-civil-relay-consultation
-server:
- bind: 127.0.0.1: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 53cef5a..0000000
--- a/runtime/registry-projects/local/cra-civil/relay/relay.yaml
+++ /dev/null
@@ -1,83 +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: []
- 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: 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: local
-instance:
- environment: local
- id: cra-civil-relay
-server:
- bind: 127.0.0.1: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/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/artifacts/evidence/enrollment-by-uin/minimization.json b/runtime/registry-projects/local/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/minimization.json
deleted file mode 100644
index 1d539e7..0000000
--- a/runtime/registry-projects/local/mosd-programme/relay/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/artifacts/evidence/enrollment-by-uin/negative-security.json b/runtime/registry-projects/local/mosd-programme/relay/artifacts/evidence/enrollment-by-uin/negative-security.json
deleted file mode 100644
index 0b38f83..0000000
--- a/runtime/registry-projects/local/mosd-programme/relay/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/artifacts/integration-packs/enrollment-by-uin.json b/runtime/registry-projects/local/mosd-programme/relay/artifacts/integration-packs/enrollment-by-uin.json
deleted file mode 100644
index 729f6a0..0000000
--- a/runtime/registry-projects/local/mosd-programme/relay/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/runtime/registry-projects/local/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json b/runtime/registry-projects/local/mosd-programme/relay/artifacts/private-bindings/child-benefit-enrollment.json
deleted file mode 100644
index 078b94e..0000000
--- a/runtime/registry-projects/local/mosd-programme/relay/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/runtime/registry-projects/local/mosd-programme/relay/relay-consultation.yaml b/runtime/registry-projects/local/mosd-programme/relay/relay-consultation.yaml
deleted file mode 100644
index 263cc9e..0000000
--- a/runtime/registry-projects/local/mosd-programme/relay/relay-consultation.yaml
+++ /dev/null
@@ -1,118 +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:
- - programme-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-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: local
-instance:
- environment: local
- id: programme-mis-relay-consultation
-server:
- bind: 127.0.0.1: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 bb3f817..0000000
--- a/runtime/registry-projects/local/mosd-programme/relay/relay.yaml
+++ /dev/null
@@ -1,76 +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:
- - programme-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-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: local
-instance:
- environment: local
- id: programme-mis-relay
-server:
- bind: 127.0.0.1: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/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/farmer-voucher-by-farmer/minimization.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/minimization.json
deleted file mode 100644
index 59e808a..0000000
--- a/runtime/registry-projects/local/nagdi-agriculture/relay/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/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json
deleted file mode 100644
index 7dfbf8c..0000000
--- a/runtime/registry-projects/local/nagdi-agriculture/relay/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/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/artifacts/evidence/livestock-movement-by-farmer/minimization.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/minimization.json
deleted file mode 100644
index 6768a09..0000000
--- a/runtime/registry-projects/local/nagdi-agriculture/relay/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/artifacts/evidence/livestock-movement-by-farmer/negative-security.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/evidence/livestock-movement-by-farmer/negative-security.json
deleted file mode 100644
index a823e60..0000000
--- a/runtime/registry-projects/local/nagdi-agriculture/relay/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/artifacts/integration-packs/farmer-voucher-by-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/integration-packs/farmer-voucher-by-farmer.json
deleted file mode 100644
index ff96512..0000000
--- a/runtime/registry-projects/local/nagdi-agriculture/relay/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/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/integration-packs/livestock-movement-by-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/integration-packs/livestock-movement-by-farmer.json
deleted file mode 100644
index 043c50a..0000000
--- a/runtime/registry-projects/local/nagdi-agriculture/relay/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/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/private-bindings/livestock-herd.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/private-bindings/livestock-herd.json
deleted file mode 100644
index 7db8adf..0000000
--- a/runtime/registry-projects/local/nagdi-agriculture/relay/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/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/private-bindings/voucher-farmer.json b/runtime/registry-projects/local/nagdi-agriculture/relay/artifacts/private-bindings/voucher-farmer.json
deleted file mode 100644
index eddcecb..0000000
--- a/runtime/registry-projects/local/nagdi-agriculture/relay/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/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
deleted file mode 100644
index e58cf8f..0000000
--- a/runtime/registry-projects/local/nagdi-agriculture/relay/relay.yaml
+++ /dev/null
@@ -1,174 +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
-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
-server:
- bind: 127.0.0.1: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/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/evidence/population-person-by-uin/minimization.json b/runtime/registry-projects/local/nia-population/relay/artifacts/evidence/population-person-by-uin/minimization.json
deleted file mode 100644
index 223b391..0000000
--- a/runtime/registry-projects/local/nia-population/relay/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/artifacts/evidence/population-person-by-uin/negative-security.json b/runtime/registry-projects/local/nia-population/relay/artifacts/evidence/population-person-by-uin/negative-security.json
deleted file mode 100644
index 53dcfa4..0000000
--- a/runtime/registry-projects/local/nia-population/relay/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/artifacts/integration-packs/population-person-by-uin.json b/runtime/registry-projects/local/nia-population/relay/artifacts/integration-packs/population-person-by-uin.json
deleted file mode 100644
index 7430026..0000000
--- a/runtime/registry-projects/local/nia-population/relay/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/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-consultation.yaml b/runtime/registry-projects/local/nia-population/relay/relay-consultation.yaml
deleted file mode 100644
index da20425..0000000
--- a/runtime/registry-projects/local/nia-population/relay/relay-consultation.yaml
+++ /dev/null
@@ -1,251 +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:
- - nia-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-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: local
-instance:
- environment: local
- id: nia-population-relay-consultation
-server:
- bind: 127.0.0.1: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 3ea4469..0000000
--- a/runtime/registry-projects/local/nia-population/relay/relay.yaml
+++ /dev/null
@@ -1,203 +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:
- - solmara-esignet
- 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
- 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: 127.0.0.1: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/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-pensioner/minimization.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/minimization.json
deleted file mode 100644
index 27b99c4..0000000
--- a/runtime/registry-projects/local/sipf-pensions/relay/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/artifacts/evidence/pension-case-by-pensioner/negative-security.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-pensioner/negative-security.json
deleted file mode 100644
index c62fd58..0000000
--- a/runtime/registry-projects/local/sipf-pensions/relay/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/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/artifacts/evidence/pension-case-by-survivor/minimization.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/minimization.json
deleted file mode 100644
index e246e22..0000000
--- a/runtime/registry-projects/local/sipf-pensions/relay/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/artifacts/evidence/pension-case-by-survivor/negative-security.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/evidence/pension-case-by-survivor/negative-security.json
deleted file mode 100644
index 72f6ab7..0000000
--- a/runtime/registry-projects/local/sipf-pensions/relay/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/artifacts/integration-packs/pension-case-by-pensioner.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-pensioner.json
deleted file mode 100644
index 7fac5ec..0000000
--- a/runtime/registry-projects/local/sipf-pensions/relay/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/runtime/registry-projects/local/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-survivor.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/integration-packs/pension-case-by-survivor.json
deleted file mode 100644
index a0e0dda..0000000
--- a/runtime/registry-projects/local/sipf-pensions/relay/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/runtime/registry-projects/local/sipf-pensions/relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/private-bindings/sipf-pension-payment-review-pension.json
deleted file mode 100644
index 2b3b78b..0000000
--- a/runtime/registry-projects/local/sipf-pensions/relay/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/runtime/registry-projects/local/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json b/runtime/registry-projects/local/sipf-pensions/relay/artifacts/private-bindings/sipf-survivor-benefit-survivor.json
deleted file mode 100644
index 8485dcd..0000000
--- a/runtime/registry-projects/local/sipf-pensions/relay/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/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
deleted file mode 100644
index 0fc6e7f..0000000
--- a/runtime/registry-projects/local/sipf-pensions/relay/relay.yaml
+++ /dev/null
@@ -1,121 +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: []
- 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: 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
-server:
- bind: 127.0.0.1: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/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/artifacts/evidence/child-benefit-household-by-uin/minimization.json b/runtime/registry-projects/local/sro-social/relay/artifacts/evidence/child-benefit-household-by-uin/minimization.json
deleted file mode 100644
index fc5378a..0000000
--- a/runtime/registry-projects/local/sro-social/relay/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/artifacts/evidence/child-benefit-household-by-uin/negative-security.json b/runtime/registry-projects/local/sro-social/relay/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/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/artifacts/integration-packs/child-benefit-household-by-uin.json b/runtime/registry-projects/local/sro-social/relay/artifacts/integration-packs/child-benefit-household-by-uin.json
deleted file mode 100644
index 7133664..0000000
--- a/runtime/registry-projects/local/sro-social/relay/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/runtime/registry-projects/local/sro-social/relay/artifacts/private-bindings/child-benefit-household.json b/runtime/registry-projects/local/sro-social/relay/artifacts/private-bindings/child-benefit-household.json
deleted file mode 100644
index e46d4f8..0000000
--- a/runtime/registry-projects/local/sro-social/relay/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/runtime/registry-projects/local/sro-social/relay/relay-consultation.yaml b/runtime/registry-projects/local/sro-social/relay/relay-consultation.yaml
deleted file mode 100644
index 9c9289e..0000000
--- a/runtime/registry-projects/local/sro-social/relay/relay-consultation.yaml
+++ /dev/null
@@ -1,118 +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:
- - sro-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-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: local
-instance:
- environment: local
- id: sro-social-relay-consultation
-server:
- bind: 127.0.0.1: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 989731e..0000000
--- a/runtime/registry-projects/local/sro-social/relay/relay.yaml
+++ /dev/null
@@ -1,76 +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:
- - sro-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-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: local
-instance:
- environment: local
- id: sro-social-relay
-server:
- bind: 127.0.0.1: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 4535e85..c1151ea 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,156 @@
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 authority_service_id, requirement_config, 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", "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_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": requirement_config(route["client_id"])["name"]} 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.",
- )
- return
-
- try:
- body = self.read_body()
- except RequestBodyError as error:
- self.write_problem(
- error.status,
- "request.invalid",
- "Invalid evidence request",
- error.detail,
- )
- 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.",
- )
+ body = self.read_body()
+ if body is None:
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.",
- )
+ 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)
- 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),
+ 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", "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_unavailable", "A required Evidence assertion was unavailable.")
+ return
+ 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": 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:
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..8fd649f 100644
--- a/scenario-runner/test_server.py
+++ b/scenario-runner/test_server.py
@@ -1,57 +1,114 @@
#!/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
+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"))
-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.common import StepHttpResult # noqa: E402
+from scenarios import service_config # 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))
+
+
+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": protected,
+ "payload": payload_segment,
+ "signature": common.b64url_nopad(r.to_bytes(32, "big") + s.to_bytes(32, "big")),
+ }
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 +120,52 @@ 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"]))
+ 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", {})
+ 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.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)
- 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"
- )
-
- 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_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"]["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",
- )
-
- 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
- )
+ self.assertNotIn("body", payload["result"]["request_source"])
+ self.assertNotIn(purpose, json.dumps(payload["result"]["request_source"]))
- 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.assertNotIn("body", farmer_result["request_source"])
def test_unknown_scenario_returns_404(self) -> None:
with self.assertRaises(urllib.error.HTTPError) as raised:
@@ -438,625 +173,465 @@ 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"])
-
- 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)
-
- 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 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"}, {})
-
- original = common.http_json
- common.http_json = fake_http_json
- 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
+ return json.loads(response.read())
- 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"])
-
-
-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 MintAndEvidenceContractTest(unittest.TestCase):
def setUp(self) -> None:
- for name in self.TOKEN_ENVS:
- os.environ[name] = f"token-{name.lower()}"
+ install_authority_keys()
def tearDown(self) -> None:
- for name in self.TOKEN_ENVS:
+ common._TOKEN_CACHE = ("", 0.0)
+ common._JWKS_CACHE.clear()
+ 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)
- 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 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
-
- 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(
+ 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 test_signed_evidence_is_preserved_and_normalized(self) -> None:
+ 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": [
{
- "evaluation_id": f"internal-{claim_id}",
- "claim_id": claim_id,
- "claim_version": "1",
+ "claim_id": "person-is-deceased",
+ "concept_id": "https://id.registrystack.org/solmara/concept/person-is-deceased",
"satisfied": True,
- "disclosure": "predicate",
- "issued_at": "2026-07-15T00:00:00Z",
- "source_row": {"private": "must-not-cross"},
+ "value": True,
+ "presentation": presentation,
}
- for claim_id in body["claims"]
- ]
+ ],
+ "presentation": presentation,
+ "assertion": {"secret": "must-not-leave-server"},
+ "signed_evidence": {"payload": "must-not-leave-server"},
},
- {"content-type": common.CLAIM_RESULT_FORMAT},
+ {},
)
-
- try:
- child_benefit_federator.http_json = fake_http_json
- evidence = child_benefit_federator.collect_evidence(
- "2300010248",
- list(child_benefit.CLAIMS),
- common.PURPOSES["child_benefit"],
+ )
+ 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,
{
- "type": "Person",
- "identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}],
+ "results": [
+ {
+ "claim_id": "farmer-registered",
+ "concept_id": "https://id.registrystack.org/solmara/concept/farmer-registered",
+ "satisfied": True,
+ "value": True,
+ }
+ ],
+ "presentation": presentation,
},
- {"as_of_date": common.CHILD_BENEFIT_AS_OF_DATE},
- )
- finally:
- child_benefit_federator.http_json = original_http_json
-
- self.assertEqual(len(calls), 4)
- self.assertEqual(
- calls[0][2]["claims"], ["birth-is-registered", "child-age-under-5"]
- )
- self.assertEqual(
- calls[0][2]["variables"],
- {"as_of_date": common.CHILD_BENEFIT_AS_OF_DATE},
- )
- 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)
-
- 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")
+ self.assertEqual(projection["results"][0]["presentation"], presentation)
- def test_authority_must_return_exactly_the_requested_predicate_set(self) -> None:
- route = child_benefit_federator.CLAIM_ROUTES["birth-is-registered"]
- response = StepHttpResult(
+ 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": "population-record-active",
- "satisfied": True,
- "disclosure": "predicate",
- }
- ]
+ {"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)
- with self.assertRaises(
- child_benefit_federator.AuthorityUpstreamError
- ) as raised:
- child_benefit_federator.minimized_results(
- route, ["birth-is-registered"], response
- )
+ 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()))
- self.assertEqual(raised.exception.code, "unexpected_claim_results")
+ 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_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)
+ def test_private_key_jwt_and_mint_token_request(self) -> None:
+ try:
+ from cryptography.hazmat.primitives import hashes
+ from cryptography.hazmat.primitives.asymmetric import ec, utils
+ except ModuleNotFoundError:
+ self.skipTest("cryptography is not installed")
+
+ 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")),
}
- handler.rfile = io.BytesIO(b"")
+ captured: dict[str, Any] = {}
- with self.assertRaises(child_benefit_federator.RequestBodyError) as rejected:
- handler.read_body()
+ class Response:
+ def __enter__(self):
+ return self
- self.assertEqual(rejected.exception.status, 413)
+ def __exit__(self, *_args):
+ return None
- 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",
- }
- )
+ def read(self):
+ return json.dumps({"access_token": "minted-token", "expires_in": 120}).encode()
- self.assertEqual(status, 400)
- self.assertEqual(content_type, "application/problem+json")
- self.assertEqual(payload["code"], "request.invalid")
+ original = urllib.request.urlopen
- 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 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()
- 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 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": [],
- }
- try:
- status, content_type, _payload = self.post_application(
- {
- "target": {
- "identifiers": [
- {"scheme": "solmara_uin", "value": "2300010248"}
- ]
- },
- "claims": ["birth-is-registered"],
- "disclosure": "predicate",
- "format": "application/json",
- }
+ 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.collect_evidence = original_collect_evidence
+ urllib.request.urlopen = fake_urlopen
+ try:
+ token = common.evidence_access_token()
+ finally:
+ urllib.request.urlopen = original
+
+ 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": "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)
+ 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
- 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",
- )
+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]] = []
+ original = pension_survivor.http_json
+
+ def fake_http_json(method, url, headers, body=None, timeout=8.0):
+ calls.append(body)
+ requirement = body["requirement"]
+ 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:
- 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()
+ result = pension_survivor.run_step({}, "stop-payment")
finally:
- server.shutdown()
- thread.join(timeout=5)
- server.server_close()
-
- self.assertEqual(rejected.exception.code, 404)
- self.assertEqual(payload["code"], "not_found")
-
- 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",
- )
+ 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"]}, {"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] = []
+ original = citizen.http_json
+
+ def fake_http_json(method, url, headers, body=None, timeout=8.0):
+ calls.append(body["requirement"])
+ 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:
- 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)
+ citizen.http_json = original
+ self.assertEqual(calls, [citizen.requirement_id(client) for client in citizen.CLIENTS])
+ self.assertEqual(len(result["source_trace"]), 2)
-class CredentialSummaryVctTest(unittest.TestCase):
- VCT = (
- "https://id.registrystack.org/solmara/vct/child-benefit-enrollment-eligibility"
- )
+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()
- @staticmethod
- def compact_sd_jwt(payload: dict[str, Any]) -> str:
- def b64url(data: bytes) -> str:
- return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
+ @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)
- 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 setUp(self) -> None:
+ os.environ["SOLMARA_EVIDENCE_ACCESS_TOKEN"] = "runtime-token"
+ install_authority_keys()
- 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 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 = 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_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_rejects_duplicate_or_unknown_concepts_before_evidence(self) -> None:
+ 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_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"])
-
-
-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_composes_only_four_independently_verified_assertions(self) -> None:
+ original = child_benefit_federator.http_json
- 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 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})
- 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"])
+ 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_unmet_claim_uses_unmet_copy_and_names_claim(self) -> None:
+ def test_purpose_comes_from_json_body_not_header(self) -> None:
body = {
- "results": [
- {"claim_id": "household-below-poverty-threshold", "satisfied": False}
- ]
+ "purpose": "unsupported-purpose",
+ "target": {"identifiers": [{"scheme": "solmara_uin", "value": "2300010248"}]},
+ "claims": child_benefit.CLAIMS,
}
- 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."
- )
-
- 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")
-
- 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 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"])
+ 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__":
diff --git a/scenarios/child_benefit.py b/scenarios/child_benefit.py
index bea583a..cadee1d 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.", "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.")},
+ "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,43 @@ 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"}
+ 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
- )
+ 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
+ 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:
- 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..feade1b 100644
--- a/scenarios/citizen.py
+++ b/scenarios/citizen.py
@@ -1,111 +1,23 @@
#!/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, 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 = "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")
+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 +29,57 @@ 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}
-
- 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
+ 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"]), request=item["body"], service_id=item["client_id"])) for item in requests]
+ aggregate = _aggregate(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 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 []
+ 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 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 _preview(requests: list[dict[str, Any]], purpose: str) -> dict[str, Any]:
+ 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 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 _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_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 = []
+ 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 bc6ee46..bbc377d 100644
--- a/scenarios/common.py
+++ b/scenarios/common.py
@@ -1,5 +1,5 @@
#!/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
@@ -7,33 +7,50 @@
import hashlib
import json
import os
+import re
+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 datetime import datetime, timedelta, timezone
+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"
+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"
-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)
+_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
class StepHttpResult:
@@ -41,128 +58,61 @@ class StepHttpResult:
body: Any
headers: dict[str, str]
error: str = ""
+ request: Any | None = None
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 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)}
- 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 list(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]"
- 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 json_b64url(value: Any) -> str:
- return b64url_nopad(json.dumps(value, separators=(",", ":")).encode("utf-8"))
+def b64url_decode(value: str) -> bytes:
+ return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
-@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.
+def request_source(
+ method: str,
+ url: str,
+ headers: dict[str, str],
+ body: Any | None = None,
+) -> dict[str, Any]:
+ parsed_url = urllib.parse.urlsplit(url)
+ source: dict[str, Any] = {
+ "method": method,
+ "url": parsed_url._replace(query="", fragment="").geturl(),
+ "headers": redact_headers(headers),
+ }
+ return source
- 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 redact_headers(headers: dict[str, str]) -> dict[str, str]:
+ redacted: dict[str, str] = {}
+ for key, value in headers.items():
+ lowered = key.lower()
+ if lowered == "authorization":
+ redacted[key] = "Bearer [runtime token hidden]"
+ elif lowered == "x-api-key":
+ redacted[key] = "[runtime token hidden]"
+ elif lowered in {"accept", "content-type"}:
+ redacted[key] = value
+ return redacted
-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 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 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,223 +120,574 @@ 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()},
+ 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()})
- except Exception as error:
- return StepHttpResult(None, {}, {}, error.__class__.__name__)
+ 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__, 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]:
+ 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 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": "ES256", "typ": "JWT", "kid": jwk["kid"]}
+ claims = {
+ "iss": client_id,
+ "sub": client_id,
+ "aud": audience,
+ "iat": now,
+ "exp": now + 120,
+ "jti": str(uuid.uuid4()),
+ }
+ signing_input = ".".join(
+ b64url_nopad(json.dumps(value, separators=(",", ":")).encode())
+ for value in (header, claims)
+ )
+ 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)}"
+
+
+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")
+ 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",
+ "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 {
- "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,
+ "Authorization": f"Bearer {token}",
+ "Accept": "application/json" if discover else EVIDENCE_JWS_MEDIA_TYPE,
}
-def credential_attempt(
- url: str,
- token: str,
+def evidence_body(
+ subject: str,
+ requirement: str,
purpose: str,
- evaluation_result: StepHttpResult,
- profile: str,
- claim_ids: list[str],
- service_id: str,
*,
- disclosure: str = "predicate",
+ selector_profile: str = "solmara-uin-v1",
+ selector_field: str = "uin",
) -> 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},
- }
- headers = auth_headers(token, purpose, "application/json")
- request = request_source("POST", url, headers, body)
- result = http_json("POST", url, headers, body)
return {
- "credential_source": request,
- "credential_response_source": source_response(result),
- "credential": credential_summary(profile, keypair.holder_id, result),
+ "requestNonce": b64url_nopad(secrets.token_bytes(32)),
+ "requirement": requirement,
+ "purpose": purpose,
+ "subjects": [
+ {
+ "role": "subject",
+ "selector": {
+ "profile": selector_profile,
+ "values": {selector_field: subject},
+ },
+ }
+ ],
}
-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 _invalid_evidence_result() -> StepHttpResult:
+ return StepHttpResult(
+ 502,
+ {
+ "code": "evidence.invalid_response",
+ "detail": "The Evidence assertion could not be verified.",
+ },
+ {},
+ )
-def sd_jwt_vct(credential: Any) -> str | None:
- """Read the `vct` claim from a compact SD-JWT.
+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
- 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):
+
+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
- segments = credential.split("~")[0].split(".")
- if len(segments) != 3:
+
+
+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 set(body) != {"protected", "payload", "signature"}:
return None
try:
- payload = json.loads(base64.urlsafe_b64decode(segments[1] + "=" * (-len(segments[1]) % 4)))
- except (ValueError, UnicodeDecodeError):
+ return _json_segment(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,
+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
+ 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 payload["supportedValues"]:
+ concept = entry["providesValueFor"]
+ value = entry["value"]
+ results.append(
+ {
+ "claim_id": concept.rsplit("/", 1)[-1],
+ "concept_id": concept,
+ "satisfied": value if isinstance(value, bool) else None,
+ "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, "presentation": presentation},
+ 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.
+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}
+
- `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", ""))
- if result.status >= 400 and "refused" in copy.get(step_id, {}):
+ return {"title": "No response from the service.", "message": "Check that Mint and Evidence are running.", "status": "needs_attention", "facts": facts}
+ 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 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": "Evidence could not complete 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": {"status": None, "code": "not_configured"},
}
diff --git a/scenarios/farmer_voucher.py b/scenarios/farmer_voucher.py
index a2db5d2..ec6c3f9 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, 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"
-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,26 @@ 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, 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:
@@ -141,6 +63,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..83f2731 100644
--- a/scenarios/pension_survivor.py
+++ b/scenarios/pension_survivor.py
@@ -1,161 +1,31 @@
#!/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, 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"
-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",
-}
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 +37,67 @@ 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://authority-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"]), 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": [_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:
- 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 = []
+ 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"]
- 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"]
+
+
+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 e656e59..3bb1624 100644
--- a/scenarios/service_config.py
+++ b/scenarios/service_config.py
@@ -1,96 +1,124 @@
#!/usr/bin/env python3
-"""Runtime endpoint lookup for guided Solmara scenarios."""
+"""Authority-owned Registry Evidence endpoints and requirement identifiers."""
from __future__ import annotations
import os
-from urllib.parse import urljoin
+from typing import Any
+from .common import evidence_access_token, joined_url
-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",
+
+BASE = "https://id.registrystack.org/solmara/requirement"
+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",
},
- "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",
+ "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",
},
- "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",
+ "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",
},
- "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",
+ "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",
},
- "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",
+ "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-notary": {
- "service_id": "nagdi-notary",
- "url_env": "NAGDI_NOTARY_URL",
- "token_env": "NAGDI_NOTARY_TOKEN",
- "default_url": "http://127.0.0.1:4323",
+ "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 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 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":
+ return joined_url(
+ os.environ.get("CHILD_BENEFIT_FEDERATOR_URL", "http://127.0.0.1:4321"),
+ 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:
- return os.environ.get(service_token_env(service_id), "")
+ if service_id == "child-benefit-federator":
+ return os.environ.get("CHILD_BENEFIT_FEDERATOR_TOKEN", "")
+ requirement_config(service_id)
+ return evidence_access_token()
def service_token_env(service_id: str) -> str:
- return service_entry(service_id)["token_env"]
+ return (
+ "CHILD_BENEFIT_FEDERATOR_TOKEN"
+ if service_id == "child-benefit-federator"
+ else "SOLMARA_EVIDENCE_CLIENT_KEY"
+ )
-def authority_service_id(service_id: str) -> str:
- entry = service_entry(service_id)
- return entry.get("service_id", service_id)
+def requirement_id(service_id: str) -> str:
+ return str(requirement_config(service_id)["requirement"])
-def service_entry(service_id: str) -> dict[str, str]:
- try:
- return SERVICE_ENDPOINTS[service_id]
- except KeyError as error:
- raise ValueError(f"unknown service endpoint: {service_id}") from error
+def authority_service_id(service_id: str) -> str:
+ return f"{requirement_config(service_id)['authority']}-evidence"
diff --git a/scripts/build-registry-stack-runtime.sh b/scripts/build-registry-stack-runtime.sh
new file mode 100755
index 0000000..1ae3abc
--- /dev/null
+++ b/scripts/build-registry-stack-runtime.sh
@@ -0,0 +1,117 @@
+#!/usr/bin/env sh
+set -eu
+
+root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+
+set -a
+# shellcheck disable=SC1091
+. "$root/versions.env"
+set +a
+
+"$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}
+
+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
+
+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"
+
+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 "relayctl image already matches Registry Stack v$version"
+ return
+ fi
+
+ relayctl_context=$(mktemp -d)
+ trap 'rm -rf -- "$relayctl_context"' EXIT HUP INT TERM
+ 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
+ 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 \
+ --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 relayctl \
+ "$relayctl_context"
+ rm -rf -- "$relayctl_context"
+ trap - EXIT HUP INT TERM
+}
+
+build_relayctl "$relayctl_image"
diff --git a/scripts/build-relay-runtime.sh b/scripts/build-relay-runtime.sh
deleted file mode 100755
index 92b85f9..0000000
--- a/scripts/build-relay-runtime.sh
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/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}
-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"
diff --git a/scripts/check-config-secrets.py b/scripts/check-config-secrets.py
index 1d9d819..59e9b1a 100755
--- a/scripts/check-config-secrets.py
+++ b/scripts/check-config-secrets.py
@@ -11,8 +11,8 @@
SCAN_DIRS = [
"ministries",
"metadata",
- "projects",
- "runtime/registry-projects",
+ "relays",
+ "evidence",
"config/hosted-relay-bundles",
"compose.yaml",
"compose.hosted.yaml",
@@ -25,8 +25,9 @@
"token_env:",
"private_jwk_env:",
"hash_secret_env:",
+ "secret:file/",
+ "secret:env/",
"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-coolify-compose.sh b/scripts/check-coolify-compose.sh
index c84da3b..507b2ff 100755
--- a/scripts/check-coolify-compose.sh
+++ b/scripts/check-coolify-compose.sh
@@ -1,31 +1,66 @@
#!/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"
+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
-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_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" \
+ 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="$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_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
+ check_compose -f "$compose"
done
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-hosted-compose.sh b/scripts/check-hosted-compose.sh
new file mode 100755
index 0000000..d7cc2aa
--- /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_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" \
+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 d781465..e0fb5b6 100755
--- a/scripts/check-image-pins.py
+++ b/scripts/check-image-pins.py
@@ -8,15 +8,20 @@
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}$")
-REGISTRY_STACK_IMAGE_KEYS = {"REGISTRY_RELAY_IMAGE", "REGISTRY_NOTARY_IMAGE"}
-PINNED_IMAGE_KEYS = REGISTRY_STACK_IMAGE_KEYS | {
- "VOLUME_INIT_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",
+ "ESIGNET_REDIS_IMAGE", "ESIGNET_BASE_IMAGE", "ESIGNET_UI_IMAGE",
+ "ESIGNET_POSTGRES_IMAGE",
}
-COMPOSE_FALLBACK_RE = re.compile(
- r"\$\{(?PREGISTRY_(?:RELAY|NOTARY)_IMAGE):-(?P[^}]+)\}"
-)
def main() -> int:
@@ -37,36 +42,37 @@ 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:
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/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..c20ae0d
--- /dev/null
+++ b/scripts/check-registry-stack-release-pin.py
@@ -0,0 +1,91 @@
+#!/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.22.0"
+
+
+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_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_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..6964f49
--- /dev/null
+++ b/scripts/check-relay-projects.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env sh
+set -eu
+
+root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+set -a
+. "$root/versions.env"
+set +a
+
+"$root/scripts/publish-relay-sources.sh"
+
+temporary_root=$(mktemp -d "$root/output/relay-check.XXXXXX")
+cleanup() {
+ rm -rf -- "$temporary_root"
+}
+trap cleanup EXIT HUP INT TERM
+
+cd "$root"
+run_relayctl() {
+ authority=$1
+ database=$2
+ stage=$3
+ shift 3
+ report="$temporary_root/$authority-$stage.json"
+ 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 "$@" >"$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
+}
+
+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'
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 67fde6a..c4ab270 100755
--- a/scripts/gen-secrets.py
+++ b/scripts/gen-secrets.py
@@ -1,9 +1,8 @@
#!/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
@@ -11,248 +10,228 @@
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"
-
-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"),
-]
-
-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",
- "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",
-}
+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 raw_key() -> str:
return secrets.token_urlsafe(32)
-def fingerprint(value: str) -> str:
- return "sha256:" + hashlib.sha256(value.encode("ascii")).hexdigest()
+def p256_jwk() -> str:
+ private = ec.generate_private_key(ec.SECP256R1()).private_numbers()
+ public = private.public_numbers
+ jwk = {
+ "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 b64url(raw: bytes) -> str:
- return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
+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"))
-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:]
jwk = {
- "kty": "OKP",
- "crv": "Ed25519",
- "kid": kid,
- "alg": "EdDSA",
- "x": b64url(public_key),
- "d": b64url(private_seed),
+ "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 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,
+def write_private(path: Path, value: str) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(value.rstrip("\n") + "\n", encoding="utf-8")
+ path.chmod(0o600)
+
+
+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
+ }
+ values["PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64"] = create_environment_value(
+ existing, "PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64", rsa_private_key_b64
)
- key_path.chmod(0o600)
- cert_path.chmod(0o644)
-
-
-def env_line(key: str, value: str) -> str:
- return f"{key}={shlex.quote(value)}"
-
+ 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,
+ }
+ )
+ return values
+
+
+def ensure_client_identifier(path: Path, client_id: str) -> None:
+ """Write an exact public identifier while preserving unrelated material."""
+
+ 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 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 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)
+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 main(argv: list[str] | None = None) -> int:
- parser = argparse.ArgumentParser(description=__doc__)
- parser.parse_args(argv)
-
- ensure_postgres_tls()
- 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(),
- "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(),
- "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": "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",
- }
- for raw_name, hash_name in RAW_HASH_PAIRS:
- raw = raw_key()
- values[raw_name] = raw
- values[hash_name] = fingerprint(raw)
+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..32bfbc5
--- /dev/null
+++ b/scripts/hosted-image-manifest.py
@@ -0,0 +1,202 @@
+#!/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 = (
+ ("REGISTRY_RELAY_IMAGE", "relay"),
+ ("SOLMARA_EVIDENCE_IMAGE", "evidence"),
+ ("SOLMARA_MINT_IMAGE", "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*\$\{(REGISTRY_RELAY_IMAGE|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 not any(
+ marker in line
+ for marker in ("${REGISTRY_RELAY_IMAGE", "${SOLMARA_")
+ ):
+ 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 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..fdead54
--- /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(Path(temporary) / "published")
+ 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..a5a95cf
--- /dev/null
+++ b/scripts/hosted-transit-signer.py
@@ -0,0 +1,490 @@
+#!/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("/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(
+ {
+ "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_directory(path: Path) -> int:
+ if not path.is_absolute():
+ raise SignerError("invalid secret")
+ components = path.parts[1:]
+ if any(component in {"", ".", ".."} for component in components):
+ raise SignerError("invalid secret")
+ directory = os.open("/", _directory_flags())
+ try:
+ 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 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:
+ value, _ = _read_secret_descriptor(descriptor)
+ 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,
+ *,
+ 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()
+ 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 _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")
+
+
+def _b64decode(value: str) -> bytes:
+ return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
+
+
+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:
+ 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")
+ return (
+ hashlib.sha256(private_bytes).digest(),
+ hashlib.sha256(public_bytes).digest(),
+ )
+ 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")
+ private_digest, public_digest = _verify_public_match(private_jwk, public_jwk)
+ _validate_socket(socket_path)
+ _validate_proxy(proxy)
+ 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",
+ key_name,
+ ]
+ environment = {
+ "LANG": "C.UTF-8",
+ "PYTHONDONTWRITEBYTECODE": "1",
+ "PYTHONUNBUFFERED": "1",
+ }
+ try:
+ os.execve(sys.executable, arguments, environment)
+ except OSError:
+ _discard_staged_secret(staged, private_digest)
+ raise
+
+
+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/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-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/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..a0f6d16
--- /dev/null
+++ b/scripts/local-transit-proxy.py
@@ -0,0 +1,542 @@
+#!/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,
+ *,
+ allow_root_bind_owner: bool = False,
+ consume_private_jwk: bool = False,
+) -> 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
+ chunks = bytearray()
+ try:
+ metadata = os.fstat(descriptor)
+ if (
+ not stat.S_ISREG(metadata.st_mode)
+ 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
+ ):
+ raise ProxyError("invalid key")
+ 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")
+ 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)
+
+
+def _consume_staged_private_jwk(path: Path, opened: os.stat_result) -> None:
+ parent = path.parent
+ staging_root = Path("/tmp").resolve()
+ if (
+ parent.parent not in {Path("/tmp"), staging_root}
+ or not parent.name.startswith("solmara-transit-")
+ or path.name != "signing.jwk"
+ ):
+ 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:
+ 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
+ )
+ 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
+ finally:
+ if staging_root_descriptor >= 0:
+ os.close(staging_root_descriptor)
+ if directory >= 0:
+ os.close(directory)
+
+
+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,
+ *,
+ 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,
+ consume_private_jwk=consume_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,
+ *,
+ 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,
+ consume_private_jwk=consume_private_jwk,
+ ),
+ )
+
+
+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)
+ 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(
+ arguments.private_jwk,
+ 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)
+ 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 7b55ea1..0000000
--- a/scripts/postgres-entrypoint.sh
+++ /dev/null
@@ -1,14 +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"
-
-exec /usr/local/bin/docker-entrypoint.sh "$@"
diff --git a/scripts/prepare-authority-runtime.sh b/scripts/prepare-authority-runtime.sh
new file mode 100755
index 0000000..feddf15
--- /dev/null
+++ b/scripts/prepare-authority-runtime.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env sh
+set -eu
+
+root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+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"
+
+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 \
+ --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" \
+ --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..e21eb79
--- /dev/null
+++ b/scripts/provision-hosted-runtime.py
@@ -0,0 +1,1201 @@
+#!/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 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
+from urllib.parse import urlsplit
+
+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")
+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"),
+ "sro": ("sro-poverty-extract", "sro-poverty"),
+}
+EXPECTED_BIND_HOST = {
+ "mint": "172.29.1.20",
+ "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"),
+ "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",
+ ),
+}
+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):
+ """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 _validate_secret_inventory(root: Path, expected: set[str]) -> None:
+ try:
+ 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 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:
+ 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 _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))
+
+
+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_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 _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,
+ *,
+ 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)
+ 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)
+ 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 _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
+ 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)
+
+
+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 _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
+ 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,
+ mint_origin: str,
+) -> None:
+ relay = assets / "relays" / authority
+ 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",
+ (relay / "source" / f"{authority}.sqlite").read_bytes(),
+ 0o444,
+ )
+ _freeze_tree(source)
+ _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 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 = 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(
+ "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 Exception: # noqa: BLE001 - dependency errors become one refusal.
+ 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")
+ 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]}:
+ 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)
+ 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
+ )
+ 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)
+ 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:
+ 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,
+ mint_origin: str,
+ relay_origin: str | None,
+) -> 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"
+ _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)
+ _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"), 0o600
+ )
+ _write(
+ secret_output / "subject-binding-hmac-key",
+ _hmac_secret(secrets, "subject-binding-hmac-key"),
+ 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, 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():
+ directory.chmod(0o700)
+
+
+def _stage_mint(
+ 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(
+ 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"), 0o600
+ )
+ _freeze_tree(runtime)
+ for directory in [secret_output, *secret_output.rglob("*")]:
+ if directory.is_dir():
+ directory.chmod(0o700)
+
+
+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)
+ 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
+ or args.relay_origin
+ ):
+ raise ProvisionError("invalid target")
+ _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)
+ elif target == "mint":
+ if (
+ args.secret_output is 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"]:
+ raise ProvisionError("invalid bind host")
+ 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.absolute(),
+ runtime,
+ secret_output,
+ args.bind_host,
+ mint_origin,
+ )
+ _check_secret_install_tree(secret_output, args.secret_output.resolve())
+ _check_install_tree(runtime, args.runtime_output.resolve())
+ _install_secret_tree(secret_output, args.secret_output.resolve())
+ 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")
+ 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 = _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,
+ cell,
+ args.runtime_output.resolve(),
+ args.extract_output.resolve(),
+ now,
+ )
+ if cell in DIRECT
+ else now
+ )
+ _stage_evidence(
+ assets,
+ cell,
+ args.secrets.absolute(),
+ runtime,
+ secret_output,
+ extracts if cell in DIRECT else None,
+ args.bind_host,
+ published_at,
+ now,
+ mint_origin,
+ relay_origin,
+ )
+ runtime_preserve = _preserve_extract_rollback if cell in DIRECT else None
+ _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_secret_tree(secret_output, args.secret_output.resolve())
+ 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,
+ preserve=(
+ _preserve_extract_rollback
+ if target.endswith("-evidence")
+ and target.removesuffix("-evidence") in DIRECT
+ else 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")
+ 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", 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")
+ 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)
+ 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)
+ sub.add_parser("ready", add_help=False)
+ 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)
+ 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)
+ 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-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/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 b92ed15..0000000
--- a/scripts/registry-projects.sh
+++ /dev/null
@@ -1,175 +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 capabilities; 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
- 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" capabilities \
- --project-dir "$ROOT/projects/$project" \
- --environment "$environment"
- done
- done
-}
-
-sync_editor_support() {
- for project in $projects; do
- echo "registryctl authoring editor: $project"
- "$REGISTRYCTL" authoring 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/notary"
- cp -R "$source/relay/config/." "$target/relay/"
- cp "$source/notary/config/notary.yaml" "$target/notary/notary.yaml"
- done
- done
- chmod -R u=rwX,go=rX "$destination"
-}
-
-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"
- 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 5c805ed..0000000
--- a/scripts/registryctl-build-output.py
+++ /dev/null
@@ -1,109 +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.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")
- if report.get("status") != "built":
- raise BuildReportError("registryctl did not report a completed build")
- if report.get("environment") != environment:
- raise BuildReportError("the JSON report has the wrong environment binding")
- if not isinstance(report.get("project"), str) or not report["project"]:
- raise BuildReportError("the JSON report has no project identity")
-
- output_value = report.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_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():
- raise BuildReportError(
- "the generated Relay or Notary configuration closure is incomplete"
- )
- for required in (relay_config, notary_config):
- 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 c813a48..0000000
--- a/scripts/registryctl-pinned.sh
+++ /dev/null
@@ -1,87 +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
- 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"
- 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
- 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 4e15ddf..0000000
--- a/scripts/registryctl-test-output.py
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/usr/bin/env python3
-"""Validate a registryctl test report and require governed request witnesses."""
-
-from __future__ import annotations
-
-import json
-import sys
-from typing import Any
-
-
-REPORT_SCHEMA = "registryctl.project_command.v1"
-REQUEST_BINDING_REQUIREMENT = "request_to_consultation_binding"
-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:
- 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(evidence, list)
- or not evidence
- or any(
- not isinstance(item, dict)
- or item.get("kind") != "authored_fixture"
- for item in evidence
- )
- ):
- raise TestReportError(
- "request-to-consultation binding requires authored fixture evidence"
- )
- 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"request witnesses cover {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 a6b7174..0000000
--- a/scripts/relay_workload_identity_agent.py
+++ /dev/null
@@ -1,755 +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.write(b"\n")
- 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") + b"\n"
- ).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") + b"\n"
- ).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 e666dcd..0000000
--- a/scripts/review.sh
+++ /dev/null
@@ -1,34 +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"
-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. 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.
-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..34bd362
--- /dev/null
+++ b/scripts/smoke-hosted-provisioner-image.py
@@ -0,0 +1,298 @@
+#!/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
+import uuid
+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"
+MINT_ORIGIN = "https://mint-authority-cells.solmara.registrystack.org"
+CRA_RELAY_ORIGIN = "https://cra-relay-authority-cells.solmara.registrystack.org"
+
+
+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 _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)
+
+
+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:
+ secret_mounts = [
+ mount for mount in mounts if mount[1] == "/tmp/solmara-provisioning"
+ ]
+ 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:
+ helper = _build_cells_module()
+ provisioner = _provisioner_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, "/tmp/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",
+ "/tmp/solmara-provisioning",
+ "--runtime-output",
+ "/provisioned/runtime",
+ "--secret-output",
+ "/provisioned/secrets",
+ "--extract-output",
+ "/provisioned/extracts",
+ "--bind-host",
+ provisioner.EXPECTED_BIND_HOST["cra"],
+ "--mint-origin",
+ MINT_ORIGIN,
+ "--relay-origin",
+ CRA_RELAY_ORIGIN,
+ ]
+ _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, "/tmp/solmara-provisioning", True),
+ (mint / "runtime", "/provisioned/runtime", False),
+ (mint / "secrets", "/provisioned/secrets", False),
+ ]
+ mint_arguments = [
+ "provision",
+ "--target",
+ "mint",
+ "--assets",
+ "/opt/solmara-hosted-assets",
+ "--secrets",
+ "/tmp/solmara-provisioning",
+ "--runtime-output",
+ "/provisioned/runtime",
+ "--secret-output",
+ "/provisioned/secrets",
+ "--bind-host",
+ provisioner.EXPECTED_BIND_HOST["mint"],
+ "--mint-origin",
+ MINT_ORIGIN,
+ ]
+ _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 bfe01f4..0000000
--- a/scripts/smoke-live.py
+++ /dev/null
@@ -1,353 +0,0 @@
-#!/usr/bin/env python3
-"""Smoke the running Solmara Lab Notary 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 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, ...]
-
-
-@dataclass(frozen=True)
-class ScenarioCase:
- name: str
- module_name: str
- step_id: str
- expected_status: int | tuple[int, ...] | range
- 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",
- "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 enrollment control",
- "child_benefit",
- "duplicate-control",
- 200,
- {"not-already-enrolled": False},
- ),
- ScenarioCase("child unsupported 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 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("livestock purpose denial", "farmer_voucher", "purpose-denial", range(400, 500), {}),
- ScenarioCase(
- "citizen self-service summary",
- "citizen",
- "positive",
- 200,
- {
- "citizen-population-record-active": True,
- "civil-record-linked": True,
- },
- ),
- ScenarioCase("citizen unsupported 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))
-
- 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: {len(SERVICES)} services 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
- 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]:
- 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)
- 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)}")
- return failures
-
-
-def wait_for_readiness(base_url: str, name: str) -> str | None:
- url = joined_url(base_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"{name}: /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}; 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
-
-
-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
-
-
-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 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
-
-
-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 114408c..0000000
--- a/scripts/smoke-relay-sources.py
+++ /dev/null
@@ -1,235 +0,0 @@
-#!/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.
-"""
-
-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
- consultation_profile: str
-
-
-RELAYS = (
- RelayBoundary(
- "CRA Relay",
- "SOLMARA_CRA_RELAY_URL",
- "http://127.0.0.1:4311",
- "solmara-cra-civil.cra-child-benefit.civil",
- ),
- RelayBoundary(
- "NIA Relay",
- "SOLMARA_NIA_RELAY_URL",
- "http://127.0.0.1:4312",
- "solmara-nia-population.nia-child-benefit.population",
- ),
- RelayBoundary(
- "SRO Relay",
- "SOLMARA_SRO_RELAY_URL",
- "http://127.0.0.1:4313",
- "solmara-sro-social.child-benefit.household",
- ),
- RelayBoundary(
- "Programme Relay",
- "SOLMARA_PROGRAMME_RELAY_URL",
- "http://127.0.0.1:4314",
- "solmara-mosd-programme.child-benefit.enrollment",
- ),
- RelayBoundary(
- "SIPF Relay",
- "SOLMARA_SIPF_RELAY_URL",
- "http://127.0.0.1:4315",
- "solmara-sipf-pensions.sipf-pension-payment-review.pension",
- ),
- RelayBoundary(
- "NAgDI Relay",
- "SOLMARA_NAGDI_RELAY_URL",
- "http://127.0.0.1:4316",
- "solmara-nagdi-agriculture.voucher.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),
- }
-
- profile = urllib.parse.quote(relay.consultation_profile, safe=".-_")
- denial = http_get(joined_url(base_url, f"/v1/consultations/{profile}"))
- denial_failure = validate_unauthenticated_denial(denial)
- if denial_failure:
- return {
- "name": relay.name,
- "status": "consultation_boundary_failed",
- "detail": denial_failure,
- }
-
- return {
- "name": relay.name,
- "status": "ok",
- "liveness_status": health[0],
- "readiness_status": ready[0],
- "unauthenticated_consultation_status": denial[0],
- "unauthenticated_consultation_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 bb7d230..0000000
--- a/scripts/smoke-story-previews.py
+++ /dev/null
@@ -1,83 +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://authority-notaries":
- raise ValueError(
- f"{module_name}:{step_id}: expected authority 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")
- requests = request.get("requests")
- if not isinstance(requests, list) or not requests:
- raise ValueError(
- f"{module_name}:{step_id}: authority 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"
- )
- 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:"):
- 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")
- 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 79a5f69..25e387b 100755
--- a/scripts/smoke.sh
+++ b/scripts/smoke.sh
@@ -1,45 +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 signing smokes need cryptography from the locked project 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
+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_build_registry_stack_runtime.py b/scripts/test_build_registry_stack_runtime.py
new file mode 100644
index 0000000..c66b838
--- /dev/null
+++ b/scripts/test_build_registry_stack_runtime.py
@@ -0,0 +1,175 @@
+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.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.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",
+ )
+
+ 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.22.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
+
+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)
+""",
+ 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()
+ },
+ }
+ self.environment.pop("REGISTRY_STACK_RELEASE_RELAYCTL_ASSET_FILE", None)
+
+ 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.22.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.22.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)
+
+ 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)
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
new file mode 100644
index 0000000..ae37f05
--- /dev/null
+++ b/scripts/test_gen_secrets.py
@@ -0,0 +1,103 @@
+from __future__ import annotations
+
+import importlib.util
+import json
+import tempfile
+import unittest
+from pathlib import Path
+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__":
+ unittest.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_esignet_topology.py b/scripts/test_hosted_esignet_topology.py
new file mode 100644
index 0000000..f956194
--- /dev/null
+++ b/scripts/test_hosted_esignet_topology.py
@@ -0,0 +1,189 @@
+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"
+LOCAL_COMPOSE_PATH = ROOT / "compose.esignet.yaml"
+LOCAL_TEMPLATE = ROOT / "config" / "esignet" / "nginx.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)
+
+ 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()
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()
diff --git a/scripts/test_hosted_home_topology.py b/scripts/test_hosted_home_topology.py
new file mode 100644
index 0000000..70d4670
--- /dev/null
+++ b/scripts/test_hosted_home_topology.py
@@ -0,0 +1,108 @@
+from __future__ import annotations
+
+import json
+import re
+import unittest
+from pathlib import Path
+from urllib.parse import urlparse
+
+import yaml
+
+
+ROOT = Path(__file__).parents[1]
+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()
+ 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": "${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},
+ 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(interpolation_default(host))
+ self.assertTrue(
+ {
+ urlparse(interpolation_default(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..047e58d
--- /dev/null
+++ b/scripts/test_hosted_image_manifest.py
@@ -0,0 +1,397 @@
+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 = {
+ "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,
+}
+
+
+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/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):
+ 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_relay_must_use_the_canonical_official_reference(self) -> None:
+ invalid = (
+ "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}",
+ )
+ 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"]
+ 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]
+ 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")
+ 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)
+
+ 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"), 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(
+ 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.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)
+ 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"
+ ),
+ 2,
+ )
+
+ 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("--public-jwk, /tmp/solmara-signing-public.jwk", signer_smoke)
+ self.assertIn("v1/transit/keys/solmara-evidence-cra", 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.assertNotIn("type=bind", 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)
+ 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 }}",
+ )
+ 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_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()
diff --git a/scripts/test_hosted_provisioning_topology.py b/scripts/test_hosted_provisioning_topology.py
new file mode 100644
index 0000000..bde5214
--- /dev/null
+++ b/scripts/test_hosted_provisioning_topology.py
@@ -0,0 +1,521 @@
+from __future__ import annotations
+
+import json
+import re
+import unittest
+from pathlib import Path
+
+import yaml
+
+
+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",
+ 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")
+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_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",
+ "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",
+}
+
+
+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
+ }
+
+ 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_provisioning_application_contains_only_target_provisioners(self) -> None:
+ services = self.provision["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 = {
+ "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:
+ 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.signers["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 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.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["/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",
+ "/tmp/solmara-signing.jwk",
+ "--public-jwk",
+ "/tmp/solmara-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"/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,
+ ) -> 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_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:
+ 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,
+ ) -> 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.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.assertNotIn("tmpfs", service)
+ 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():
+ 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_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:
+ services = self.signers["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_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),
+ "signers-ready",
+ }
+ 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()
+ )
+ )
+
+ 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()},
+ {
+ "${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.assertFalse(signer["read_only"])
+ self.assertNotIn("tmpfs", signer)
+ 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():
+ 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",
+ "mint-readiness",
+ "static-metadata",
+ "scenario-runner",
+ "child-benefit-federator",
+ "home",
+ "portal",
+ }:
+ continue
+ self.assertEqual(
+ service["depends_on"]["audit-permissions"],
+ {"condition": "service_completed_successfully"},
+ 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])
+ self.assertEqual(
+ core["scenario-runner"]["depends_on"],
+ {"mint-readiness": {"condition": "service_completed_successfully"}},
+ )
+
+
+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..644050f
--- /dev/null
+++ b/scripts/test_hosted_runtime_assets.py
@@ -0,0 +1,245 @@
+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 _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)
+ MODULE.verify_manifest(output)
+
+ 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}"
+ 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_files.update(
+ f"evidence/cells/{cell}/{relative}"
+ for relative in MODULE.EVIDENCE_FILES[cell]
+ )
+ 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 relative, content in 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)
+ 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"
+ 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")
+ combined = completed.stdout + completed.stderr
+ for forbidden in (canary, str(self.root), str(output), str(failing)):
+ self.assertNotIn(forbidden, combined)
+ 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..3fdd187
--- /dev/null
+++ b/scripts/test_hosted_transit_signer.py
@@ -0,0 +1,431 @@
+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_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])
+
+ 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)
+ replacement_path = socket_directory / "replacement.sock"
+ replacement.bind(str(replacement_path))
+ replacement_path.chmod(0o600)
+
+ 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
+ os.replace(replacement_path, path)
+ 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
+ private_digest = hashlib.sha256(self.canary).digest()
+ public_digest = hashlib.sha256(b"{}").digest()
+
+ 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=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.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(
+ 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 4601752..9a0c1fb 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 = "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
def load_check_image_pins():
@@ -35,19 +37,30 @@ 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",
)
+ 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()
- 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 +68,69 @@ 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)
+
+ 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,
+ )
+ (self.root / "versions.env").write_text(versions)
+ result, stderr = self.run_check()
+
+ self.assertEqual(result, 1)
+ 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)
+
+ 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__":
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_runtime_stager.py b/scripts/test_local_relay_runtime_stager.py
new file mode 100644
index 0000000..c77413a
--- /dev/null
+++ b/scripts/test_local_relay_runtime_stager.py
@@ -0,0 +1,299 @@
+from __future__ import annotations
+
+import hashlib
+import importlib.util
+import json
+import os
+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"
+ 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
+
+
+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()
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..6f3bd29
--- /dev/null
+++ b/scripts/test_local_transit_proxy.py
@@ -0,0 +1,375 @@
+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()
+ 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"]
+ 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_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="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..b64d515
--- /dev/null
+++ b/scripts/test_project_runtime_secrets.py
@@ -0,0 +1,47 @@
+from __future__ import annotations
+
+import importlib.util
+import stat
+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")
+ 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__":
+ unittest.main()
diff --git a/scripts/test_provision_hosted_runtime.py b/scripts/test_provision_hosted_runtime.py
new file mode 100644
index 0000000..6f2d87c
--- /dev/null
+++ b/scripts/test_provision_hosted_runtime.py
@@ -0,0 +1,1096 @@
+from __future__ import annotations
+
+import base64
+import copy
+import contextlib
+import hashlib
+import importlib.util
+import json
+import os
+import sqlite3
+import stat
+import subprocess
+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")
+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)
+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:
+ 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",
+ )
+
+
+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_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)],
+ 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",
+ "--mint-origin",
+ provisioner.MINT_ORIGIN,
+ ]
+ )
+ 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"
+ 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_is_idempotent_only_for_an_exact_existing_tree(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)
+
+ 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)
+
+ (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_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)
+ root.chmod(0o700)
+ 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_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:
+ 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(
+ {
+ "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(
+ 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",
+ provisioner.MINT_ORIGIN,
+ provisioner.RELAY_ORIGINS["sipf"],
+ )
+
+ 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())
+ )
+ 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"
+ )
+ 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,
+ ) -> 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"},
+ "issuer": "old",
+ "clientAssertion": {"audience": "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"],
+ provisioner.MINT_ORIGIN,
+ )
+ 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()
+ )
+ 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):
+ provisioner._stage_mint(
+ assets,
+ secrets,
+ 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:
+ 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(
+ yaml.safe_dump(
+ {
+ "version": 1,
+ "authentication": {
+ "issuer": {"discoveryUrl": "https://old.invalid"}
+ },
+ }
+ ),
+ 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),
+ "--runtime-output",
+ str(runtime),
+ "--source-output",
+ str(source),
+ "--mint-origin",
+ provisioner.MINT_ORIGIN,
+ ]
+ )
+
+ provisioner.provision(arguments)
+ provisioner.provision(arguments)
+ self.assertEqual((source / "cra.sqlite").read_bytes(), database)
+ self.assertEqual(
+ (runtime / "package" / "sealed.json").read_bytes(), b"sealed"
+ )
+ 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:
+ 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"
+ provisioner._write(secret_output / "audit-hmac-key", b"a" * 32, 0o400)
+ 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"],
+ "--mint-origin",
+ provisioner.MINT_ORIGIN,
+ ]
+ )
+
+ 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, 0o600)
+
+ 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),
+ ):
+ provisioner.provision(arguments)
+ 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)
+ 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(
+ 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(
+ [
+ sys.executable,
+ str(SCRIPT),
+ "provision",
+ "--target",
+ "cra-relay",
+ "--assets",
+ str(assets),
+ "--runtime-output",
+ str(runtime),
+ "--source-output",
+ str(source),
+ "--mint-origin",
+ provisioner.MINT_ORIGIN,
+ ],
+ 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_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),
+ "--runtime-output",
+ str(runtime),
+ "--source-output",
+ str(source),
+ "--mint-origin",
+ provisioner.MINT_ORIGIN,
+ ]
+ )
+ 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)
+ 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"/var/lib/registry-evidence/sro/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_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)
+ 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": provisioner.EXPECTED_BIND_HOST["sro"]},
+ "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)
+ 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:
+ 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",
+ "--mint-origin",
+ provisioner.MINT_ORIGIN,
+ "--relay-origin",
+ provisioner.RELAY_ORIGINS["sipf"],
+ ]
+ )
+ 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),
+ ):
+ 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()
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..0a99b69
--- /dev/null
+++ b/scripts/test_registry_stack_release_pin.py
@@ -0,0 +1,66 @@
+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_older_release_is_rejected_in_favour_of_coherent_release(self) -> None:
+ 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.22.0",
+ "REGISTRY_STACK_SOURCE_REF": "v0.22.0",
+ **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.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.22.0/relayctl-v0.22.0-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.22.0",
+ "REGISTRY_STACK_SOURCE_REF": "main",
+ **authenticator_values(),
+ }
+ self.assertIn(
+ "REGISTRY_STACK_SOURCE_REF must be v0.22.0",
+ 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 ab4663f..0000000
--- a/scripts/test_registryctl_build_output.py
+++ /dev/null
@@ -1,124 +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"
- 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")
-
- def report(self, **overrides: object) -> bytes:
- report: dict[str, object] = {
- "schema_version": MODULE.REPORT_SCHEMA,
- "status": "built",
- "project": "example",
- "environment": "local",
- "fixtures": [],
- "semantic_changes": [],
- "baseline": "initial_without_baseline",
- "output": ".registry-stack/build/local",
- }
- report.update(overrides)
- 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" / "notary" / "config" / "notary.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 a79c048..0000000
--- a/scripts/test_registryctl_test_output.py
+++ /dev/null
@@ -1,118 +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": [
- {
- "requirements": [
- {
- "state": "covered",
- "requirement": MODULE.REQUEST_BINDING_REQUIREMENT,
- "evidence": [
- {
- "kind": "authored_fixture",
- "id": "target/example/fixture/match",
- }
- ],
- }
- ]
- }
- ]
- },
- }
- report.update(overrides)
- return json.dumps(report).encode("utf-8")
-
- def test_accepts_passing_authored_request_witnesses(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": [],
- }
- ]
- }
- ]
- }
- with self.assertRaisesRegex(
- MODULE.TestReportError,
- "every fixture target must cover",
- ):
- 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"}],
- }
- ]
- }
- ]
- }
- with self.assertRaisesRegex(
- MODULE.TestReportError,
- "requires authored fixture evidence",
- ):
- MODULE.validate_test_report(
- self.report(fixture_coverage=coverage)
- )
-
- 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 96167bc..0000000
--- a/scripts/test_relay_workload_identity_agent.py
+++ /dev/null
@@ -1,590 +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.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..98d4659
--- /dev/null
+++ b/scripts/test_runtime_topology.py
@@ -0,0 +1,819 @@
+from __future__ import annotations
+
+import importlib.util
+import os
+import shutil
+import stat
+import subprocess
+import tempfile
+import unittest
+from pathlib import Path
+from unittest import mock
+
+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_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)
+ 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"
+ )
+ 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"]
+ )
+
+ 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["user"], "65532:65532")
+ 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"],
+ )
+ 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,
+ )
+ self.assertEqual(
+ 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",
+ )
+ 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"])
+ 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)
+ 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, target_uid, target_gid)", audit_init)
+ self.assertIn(
+ "os.chown(entry.path, target_uid, target_gid, follow_symlinks=False)",
+ audit_init,
+ )
+ 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"],
+ )
+
+ 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/26", "ip_range": "192.0.2.0/27"}],
+ )
+
+ 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.62",
+ "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_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.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"]))
+
+ def test_local_authority_runtimes_use_owned_read_only_secret_volumes(self) -> None:
+ compose = yaml.safe_load((SCRIPT.parents[1] / "compose.yaml").read_text())
+
+ 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.assertNotIn("cap_add", service)
+ 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].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"]
+
+ 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.assertTrue(initializer["read_only"])
+ self.assertEqual(initializer["restart"], "no")
+ 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.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)
+ 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())
+ 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"
+ 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"])
+ 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",
+ 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"],
+ "${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_BASE_URL:-https://child-benefit.solmara.registrystack.org}",
+ )
+ self.assertEqual(
+ services["portal"]["environment"]["CHILD_BENEFIT_FEDERATOR_URL"],
+ "${SOLMARA_CHILD_BENEFIT_FEDERATOR_PUBLIC_BASE_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_is_standalone_and_core_portal_owns_login_config(
+ self,
+ ) -> None:
+ 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": "${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": "${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",
+ }
+ for key, value in expected.items():
+ self.assertEqual(portal[key], value)
+
+ private_key_value = "${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:-}"
+ self.assertEqual(
+ {
+ service_name
+ for service_name, service in core["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"],
+ "${PORTAL_ESIGNET_CLIENT_PRIVATE_KEY_B64:?required}",
+ )
+ 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..75a7ba2 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,87 @@ 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["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",
+ },
+ )
+
+ 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"])
+
+ 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 +176,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..a5ad19f
--- /dev/null
+++ b/scripts/test_smoke_hosted_provisioner_image.py
@@ -0,0 +1,96 @@
+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_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:
+ 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:
+ 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 8f9f620..fd381c2 100644
--- a/versions.env
+++ b/versions.env
@@ -1,12 +1,37 @@
-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
-SOLMARA_RELAY_DEV_IMAGE=solmara-lab-registry-relay:source
-SOLMARA_RELAY_BUNDLE_SEQUENCE=2
-REGISTRY_RELAY_STATE_EPOCH=v015
-POSTGRES_IMAGE=postgres:16-alpine
-STATIC_METADATA_IMAGE=python:3.12-alpine
+# 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.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
+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